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 theipaddressstdlib; 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 (realipaddressmath, 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
ALLOWwith 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-scope —
10.20.99.10sits inside the in-scope10.20.0.0/16but inside the out-of-scope10.20.99.0/24, so it is denied. - Substring trick not fooled —
evil-meridian-freight.testandcorp.meridian-freight.test.evil.exampledo not match in-scopecorp.meridian-freight.test. - Forbidden-method deny —
dosis not on the allowlist. - Before/after window deny —
window_endis exclusive; exactlywindow_endis out. - Rate-limit deny — a 4th action inside a 60-second window with
rate_limit=3is 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
DenyoverridingAllow, 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
DENYenumerates 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.