Phase 00 — Lab 01 — Engagement ROE Authorization Guard

Concept: Authorization, scope, deny-by-default. WARMUP: Chapters 2–6.

The problem

A red team consultant takes an action only when its target, method, time, and rate are all inside the signed Rules of Engagement (ROE). On a real engagement that check happens in the operator's head before every action — and the day it is wrong by one CIDR, you have attacked a third party and the engagement is a crime, not a contract. This lab makes that check a machine-checkable, auditable, deterministic function so the boundary is never ambiguous and never depends on a tired operator at 2 a.m.

The governing principle is deny-by-default (fail closed): an action is allowed only if every gate passes, and anything not explicitly permitted is denied. Crucially, out-of-scope listings override in-scope ones — so a protected subnet carved out of a broad in-scope range is never silently authorized.

What you build (lab.py)

  • in_scope(target, roe) — is the target explicitly in scope and not explicitly out of scope? IPs are matched against CIDRs with the ipaddress stdlib; domains by case-insensitive exact-or-subdomain; accounts by exact id. Out-of-scope overrides in-scope.
  • method_allowed(method, roe) — is the method on the permitted allowlist? (deny-by-default)
  • within_window(ts, roe) — is the timestamp inside the half-open window [start, end)?
  • _ip_in_cidrs(target, cidrs) — the CIDR-membership helper (real ipaddress math, not strings).
  • _domain_match(target, domains) — the exact-or-subdomain matcher that defeats the substring trick.
  • check_action(action, roe, history) -> {decision, reasons} — the full gate. It collects every failing reason (it does not short-circuit), because a deconfliction report needs all of them.

Attack/abuse cases the tests cover

  • In-scope allow — an in-scope IP / domain / subdomain / account, with a permitted method, inside the window and under the rate limit, is ALLOW with no reasons.
  • Out-of-scope deny — a public IP, an out-of-scope account, an out-of-scope domain is DENY.
  • Out-of-scope overrides in-scope10.20.99.10 sits inside the in-scope 10.20.0.0/16 but inside the out-of-scope 10.20.99.0/24, so it is denied.
  • Substring trick not fooledevil-meridian-freight.test and corp.meridian-freight.test.evil.example do not match in-scope corp.meridian-freight.test.
  • Forbidden-method denydos is not on the allowlist.
  • Before/after window denywindow_end is exclusive; exactly window_end is out.
  • Rate-limit deny — a 4th action inside a 60-second window with rate_limit=3 is denied; old actions outside the rolling window do not count.
  • Deny-by-default — an unknown target, and an empty ROE, deny everything.
  • Stop condition — an engaged stop latch halts every action regardless of the other gates.
  • Multiple reasons — a bad target and bad method and bad time yields at least three reasons.
  • Determinism — same inputs → identical output.

Run

pip install -r requirements.txt
LAB_MODULE=solution pytest -q   # reference passes
pytest -q                        # your implementation after the TODOs

Hardening / detection (what this models for the blue team)

This guard is a control. Two lessons carry into the engagement and the report:

  • Deny-by-default is the only safe default. The same pattern protects the client: egress allowlists, IAM Deny overriding Allow, and segmentation are deny-by-default guards. An engagement that bypasses one is exactly the path your report should recommend closing.
  • The decision must be auditable. Every DENY enumerates its reasons; pair this log with the deconfliction log (Lab 02) and the SOC can reconstruct exactly what was authorized when.

Extensions (build in your own isolated range)

  • Add permitted-hours-of-day and blackout windows (e.g., no testing during month-end close).
  • Resolve a hostname through a controlled resolver and re-check the resolved IP against the out-of-scope CIDRs — the DNS-rebinding-style gap this lab documents as a limitation.
  • Emit a signed, hash-chained decision log so the authorization trail is tamper-evident.
  • Re-implement the guard as a policy engine (OPA/Rego or Cedar) and diff the decisions.

Interview / resume

"Built a deny-by-default ROE authorization guard that decides ALLOW/DENY for a proposed engagement action against signed scope (CIDRs/domains/accounts), a method allowlist, a time window, and a rate limit — with out-of-scope overriding in-scope, real CIDR math via ipaddress, a subdomain matcher that defeats the substring trick, and a full reason trail for deconfliction."

Limitations: targets are matched as exact IP literals or exact/subdomain domains or exact account ids — the guard does not resolve DNS, so a domain that resolves into an out-of-scope CIDR must be caught by the range's resolver/proxy (documented in the module docstring). The window is a single interval; rate limiting is count-per-rolling-window, not bytes/sec or concurrency.