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
- EX004 — Set notation. Write the permitted-target set for networks 192.168.50.0/24, ports {22, 443} and deny-list {192.168.50.1, 192.168.50.254}. Rubric: 3 = correct set expression and |T| = 508; 2 = expression without count; 1 = partial; 0 = none.
- EX005 — Monotonicity. Prove that adding a port to P can only enlarge T, and state which review rule this implies. Rubric: 3 = proof plus "port additions need a second approver"; 2 = proof only; 1 = assertion; 0 = none.
- EX006 — Lab reading. In the lab output, why is 192.168.50.20:3389 denied while 192.168.50.20:22 is allowed? Rubric: 3 = port 3389 not in P, host in I and not in D; 2 = reason without the other clauses; 1 = vague; 0 = wrong.
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) →