◇ OpenCyber Academy
Engineering · Chapter 4 · English

Chapter 4 — Programming 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 4 — Programming I

Lesson map

Lesson 04 published. Map, example, EX010.

flowchart TD
  N0["Learning outcomes"]
  N1["4.1 Functions that return decisions"]
  N2["4.2 Data structures that resist errors"]
  N3["4.3 Test first"]
  N4["Synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4
  N4 --> EX["Worked example"]
  EX --> Q["Exercise EX010"]

Logical drawing

flowchart LR
  N0["Learning outcomes"]
  N1["4.1 Functions that return decisions"]
  N2["4.2 Data structures that resist errors"]
  N3["4.3 Test first"]
  N4["Synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4 --> Q["EX010"]
[Learning outcomes]
    |
    v
[4.1 Functions that return decisions]
    |
    v
[4.2 Data structures that resist errors]
    |
    v
[4.3 Test first]
    |
    v
[Synthesis]
    |
    v
[Worked example] --> [Exercise EX010]

Learning outcomes

You will (1) write small, testable functions for assessment bookkeeping; (2) choose data structures that make wrong states hard to represent; (3) write a unit test before trusting a tool's output.

4.1 Functions that return decisions

Assessment tooling is mostly bookkeeping: is this target allowed, which finding is a duplicate, how old is this ticket. Each question should be a pure function with explicit inputs and a return value, such as allowed(host, port, scope) -> (bool, reason). Returning the reason as well as the decision makes logs useful: "DENY outside-cidr" tells the reviewer which rule fired. Avoid global state; a function that reads a hidden global scope file cannot be tested against a synthetic scope.

A pure function can be called from a unit test with a fake scope and from the live tool with the real one, and both paths share the same logic. Returning a reason string turns every denial into an explanation the engagement log can store. Global state is the enemy of that pattern: if the scope is read from a file inside the function, a test cannot inject a synthetic scope without touching the filesystem. Sam keeps scope as an argument everywhere TrackScope decides allow or deny.

4.2 Data structures that resist errors

Store ports as a set of integers, not a comma-separated string: membership is exact and "443" cannot match "4430". Store CIDRs as parsed network objects (the standard ipaddress module) rather than strings, because string prefix matching thinks 10.200.0.5 is inside "10.20". Store findings as dictionaries with a fixed schema and validate required keys on creation.

Wrong types create silent bugs that look like scope mistakes. A string of ports invites a substring match; a set of integers does not. String prefix checks on addresses are a classic trap, which is why the standard library's network objects exist. Validating finding records on creation catches a missing ticket reference before the report is written, when fixing it still costs seconds rather than an awkward correction to the client.

4.3 Test first

Before using a scope checker on a client network, write five tests on synthetic data: one allow, one deny for each rule, and one boundary case (the first and last address in a range). The lab in Chapter 2 is exactly such a table of decisions; reproducing it in a unit test means a later refactor that breaks the deny-list is caught before it reaches a real engagement.

import ipaddress
def in_scope(h, nets):
    a = ipaddress.ip_address(h)
    return any(a in ipaddress.ip_network(n) for n in nets)
assert in_scope("10.20.255.255", ["10.20.0.0/16"])
assert not in_scope("10.2.0.5", ["10.20.0.0/16"])
print("scope tests passed")

Synthetic case — Riverstone Transit. An intern script matched scope with host.startswith("10.2"). Sam's boundary test showed it accepted 10.200.0.0/16, a partner network. The fix used ipaddress and the test now runs in CI.

4.4 Errors and limits

Tests prove behaviour on the cases you wrote, not on all inputs. Keep test data synthetic; never paste a client's real host list into a public repository.

A passing suite means the written cases behave; an untested boundary can still be wrong. Synthetic hosts keep client networks out of git history and out of student machines. When a new rule is added to the scope checker, Sam adds a test for it in the same change, so the suite and the rules stay aligned.

Exercises

Synthesis

Small pure functions, exact data types and tests written on synthetic data are the cheapest scope-safety mechanism an engineer has.

Continue studying — chapter 04 of 50

← Chapter 03 (draft) · Course chapter index · Chapter 05 (draft) →

Worked answers

EX010. in on strings tests for a substring, and "8443,22" contains "443". Parse the field into a set of integers, ports = {int(x) for x in s.split(",")}, and test 443 in ports, which is False here.

EX011. Test 192.168.50.0 and 192.168.50.255 (both inside the /24 network object) and 192.168.51.0, which must be outside. A string-prefix implementation often fails the last case.

EX012. The reason identifies which rule fired, so logs show why an action was blocked ("deny-list" vs "outside-cidr") and reviewers can check the right rule. Tests can then assert the specific reason, catching a case where the right answer is reached for the wrong reason.