◇ OpenCyber Academy
Engineering · Chapter 28 · English

Chapter 28 — Authorized offensive security and red team: implementation I

Open, free learning for anyone who wants to protect people and technology. No account is required to read this text or its worked answers.

Chapter 28 — Authorized offensive security and red team: implementation I

Lesson map

Lesson 28 published. Map, example, EX082.

flowchart TD
  N0["Learning outcomes"]
  N1["28.1 Implementation shape"]
  N2["28.2 Clause tests"]
  N3["28.3 Logging"]
  N4["28.2b Reason-code catalogue"]
  N5["28.3b Fuzzing inputs"]
  N6["28.1b Property-based hours"]
  N7["Practice note"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> N7
  N7 --> EX["Worked example"]
  EX --> Q["Exercise EX082"]

Logical drawing

flowchart LR
  N0["Learning outcomes"]
  N1["28.1 Implementation shape"]
  N2["28.2 Clause tests"]
  N3["28.3 Logging"]
  N4["28.2b Reason-code catalogue"]
  N5["28.3b Fuzzing inputs"]
  N6["28.1b Property-based hours"]
  N7["Practice note"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> N7 --> Q["EX082"]
[Learning outcomes]
    |
    v
[28.1 Implementation shape]
    |
    v
[28.2 Clause tests]
    |
    v
[28.3 Logging]
    |
    v
[28.2b Reason-code catalogue]
    |
    v
[28.3b Fuzzing inputs]
    |
    v
[28.1b Property-based hours]
    |
    v
[Practice note]
    |
    v
[Worked example] --> [Exercise EX082]

Learning outcomes

You will (1) implement allowed() with explicit reason codes; (2) add tests for each clause; (3) log without storing secrets.

28.1 Implementation shape

Pure function: inputs host, port, ScopeVersion → (allow: bool, reason: str). No network I/O inside the predicate; I/O belongs to the caller. That keeps tests fast and free of client contact.

Keeping I/O out of the predicate makes synthetic tests complete. Callers handle network and files; the function stays a pure decision. Sam benchmarks the predicate to keep it under a millisecond for burst denies. Burst tests send ten thousand synthetic denies and fail if p95 latency crosses the millisecond budget.

28.2 Clause tests

One test file per clause: cidr, deny-list, port, hours, concurrency, approval flag. Synthetic fixtures only. Mutation: break a clause and watch the test fail.

"""Check proposed engagement targets against a written scope (synthetic)."""
SCOPE_CIDRS = [("10.20.0.0", 16), ("192.168.50.0", 24)] # inclusive synthetic ranges
SCOPE_PORTS = {22, 80, 443, 8080}
DENY_HOSTS = {"10.20.0.1", "192.168.50.1"} # out-of-scope infra

def ip_int(s):
a,b,c,d = map(int, s.split("."))
return (a<<24)|(b<<16)|(c<<8)|d

def in_cidr(ip, net, bits):
mask = (0xFFFFFFFF << (32-bits)) & 0xFFFFFFFF
return (ip_int(ip) & mask) == (ip_int(net) & mask)

def check(host, port):
reasons = []
if host in DENY_HOSTS:
reasons.append("deny-list")
if not any(in_cidr(host, n, b) for n,b in SCOPE_CIDRS):
reasons.append("outside-cidr")
if port not in SCOPE_PORTS:
reasons.append("port-not-in-scope")
return ("ALLOW" if not reasons else "DENY"), reasons

tests = [
("10.20.1.10", 443),
("10.20.0.1", 443),
("192.168.50.20", 22),
("192.168.50.20", 3389),
("8.8.8.8", 80),
("192.168.1.10", 80),
]
for h,p in tests:
v, r = check(h,p)
print(f"target={h}:{p} decision={v} reasons={','.join(r) or '-'}")

Expected output:

target=10.20.1.10:443 decision=ALLOW reasons=-
target=10.20.0.1:443 decision=DENY reasons=deny-list
target=192.168.50.20:22 decision=ALLOW reasons=-
target=192.168.50.20:3389 decision=DENY reasons=port-not-in-scope
target=8.8.8.8:80 decision=DENY reasons=outside-cidr
target=192.168.1.10:80 decision=DENY reasons=outside-cidr

Per-clause files show which rule regressed. Synthetic fixtures keep client networks out. Sam's review checks that each ROE clause has a file. Unimplemented clauses block release trains even when a manual demo passed on a laptop build. Laptop demos can be attached as extras but they never green-light a train by themselves. Each clause file names the ROE clause id it tests, so a failing test points straight back to the signed text.

28.3 Logging

Log decision, reason, policy version, actor. Do not log tokens or passwords. Redact query parameters that might contain secrets.

Synthetic case — Riverstone Transit. Implementation I shipped reason codes that the timeline UI shows — operators finally saw why a probe stopped.

Policy version and actor make a DENY explainable months later. Tokens in logs are incidents. Redaction of query parameters is tested with a fixture that tries to sneak a password through. Sam fails builds that print Authorization headers. Secret-sneak fixtures run on every commit, not only on release candidates, so redaction regressions fail fast.

28.4 Errors and limits

Stringly-typed reasons drift; use an enum shared with PulseMap.

Reason codes that invent new strings every week break dashboards and training. A catalogue with stable ids keeps operators fluent. Sam rejects pull requests that add a reason without a catalogue row and a unit test, and he archives retired codes instead of recycling their ids so old logs stay readable. Archived codes remain in a read-only table queried by the log UI when analysts open old engagements. Read-only archive access is granted to analysts for the engagement retention period only.

28.2b Reason-code catalogue

Publish the enum of reason codes as a versioned document PulseMap and operators share: outside-cidr, deny-list, port-not-in-scope, outside-hours, concurrency, needs-approval, policy-unavailable. Unknown codes must fail closed in consumers, not display as "OK".

Stable codes let PulseMap and training docs speak one language. Human text can change; ids should not. Sam versions the catalogue and requires a migration note when meanings change. Yearly metric notebooks pin the catalogue version so a meaning change cannot silently reshape trends. Notebooks that omit the pin fail the metrics review the same way unpinned tool versions fail kickoff.

28.3b Fuzzing inputs

Feed the predicate random hosts, ports and broken UTF-8. It must DENY or error closed, never throw uncaught and never ALLOW on parse failure. A five-minute fuzz harness in CI catches embarrassing crashes before an engagement does.

Garbage hosts and ports must deny closed, not crash open. Fuzzing the predicate catches panics that would fail open in a wrapper. Sam runs a nightly fuzz job on allowed() with a crash-as-fail gate. Release notes list fuzz hours and crash counts beside feature highlights for visibility.

28.1b Property-based hours

For the hours gate, generate random timestamps across a week and assert DENY outside the ROE window and ALLOW inside (with other clauses satisfied). Example tests miss timezone edges that property tests catch.

Hours clauses fail at boundaries: 08:59 versus 09:00. Property tests over time zones catch gaps example tests miss. Sam includes a case just inside and just outside the window in every hours file. Hours-change tickets paste the boundary table from the suite so reviewers see what will be asserted.

Practice note

Code review checklists for predicate changes include: new clause? new tests? reason-code docs? fuzz case? fail-closed on parse error? Reviewers who skip the list are doing social review, not safety review. TrackScope's deny path is the wrong place for informal LGTM culture; the checklist is short on purpose so it actually gets used under time pressure.

Exercises

Synthesis

Pure predicate, clause tests, safe logs.

Continue studying — chapter 28 of 50

← Chapter 27 (draft) · Course chapter index · Chapter 29 (draft) →