◇ OpenCyber Academy
Engineering · Chapter 2 · English

Chapter 2 — Discrete mathematics

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 2 — Discrete mathematics

Lesson map

Lesson 02 published. Map, example, EX004.

flowchart TD
  N0["Learning outcomes"]
  N1["2.1 Scope is a set"]
  N2["2.2 Predicates and proofs"]
  N3["2.3 Counting the denominator"]
  N4["Synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4
  N4 --> EX["Worked example"]
  EX --> Q["Exercise EX004"]

Logical drawing

flowchart LR
  N0["Learning outcomes"]
  N1["2.1 Scope is a set"]
  N2["2.2 Predicates and proofs"]
  N3["2.3 Counting the denominator"]
  N4["Synthesis"]
  N0 --> N1 --> N2 --> N3 --> N4 --> Q["EX004"]
[Learning outcomes]
    |
    v
[2.1 Scope is a set]
    |
    v
[2.2 Predicates and proofs]
    |
    v
[2.3 Counting the denominator]
    |
    v
[Synthesis]
    |
    v
[Worked example] --> [Exercise EX004]

Learning outcomes

You will (1) express scope as sets and predicates; (2) prove a simple property of a scope rule; (3) count the size of a scope so that coverage claims have a denominator.

2.1 Scope is a set

Written scope lists what may be tested: in-scope networks I, in-scope ports P and a deny-list D. The set of permitted targets is T = {(h, p) : h ∈ I, h ∉ D, p ∈ P}. Writing scope this way exposes ambiguities a prose ROE hides. "The 10.20.0.0/16 network except the gateway" is the set difference I \ D with D = {10.20.0.1}. "Web services" is not a set until someone lists the ports.

Once targets are pairs of host and port, every proposed action can be checked against the same predicate. Ambiguous phrases such as 'web services' disappear, because the set either contains a port or it does not. The deny-list sits inside the definition of T, so forgetting to apply it is a coding error rather than a judgement call during the test. Sam keeps the written ROE and the machine-readable set in the same change, so they cannot drift apart.

2.2 Predicates and proofs

TrackScope implements the predicate allowed(h, p) = inCIDR(h, I) ∧ ¬(h ∈ D) ∧ (p ∈ P). Because every clause is a conjunction, adding a host to D can only shrink T: if (h, p) ∈ T after the change then it was in T before. That monotonicity is worth proving and testing, because the opposite (a change that silently widens scope) is the incident engineers most want to prevent. A unit test checks the property on all pairs drawn from a small synthetic universe.

"""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

2.3 Counting the denominator

A /16 holds 2¹⁶ = 65,536 addresses; a /24 holds 256. With four permitted ports and one denied host, |T| = (65,536 − 1 + 256) × 4 = 263,164 host–port pairs. Nobody tests every pair, so coverage must be stated as "pairs examined / |T|" or as a sampled fraction, never as "the network was covered".

Synthetic case — Riverstone Transit. The first ROE draft said "all depot systems". Sam rewrote it as two CIDRs, a port set and a one-host deny-list, and the client's network lead found that one depot CIDR also contained a supplier's kiosk, which was added to D before signing.

2.4 Errors and limits

Sets describe intent, not reality: DHCP can move a host into or out of a range during the test, so the predicate must be re-evaluated per action. Overlapping CIDRs double-count if you add sizes naively; use inclusion–exclusion.

Re-evaluating the predicate before each action costs little and catches a host that moved during the day. When two ranges overlap, adding their sizes twice is the common mistake; compute the union instead. A scope file that is correct at kickoff can be wrong by lunchtime if the client renumbers a subnet, which is why TrackScope logs the version of the scope file with every decision.

Exercises

Synthesis

Treat scope as a predicate over a countable set. It can then be tested and counted, and a change to it can be shown not to widen it.

Continue studying — chapter 02 of 50

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