Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Patch Compliance Tracker

Which patches does this roadside controller actually need? Resolve the supersedence graph, prove no CVE gets dropped, and order the install set for a window that might get cut short.

Patching a fleet of roadside controllers is not patching a fleet of laptops. The devices are on poles, behind isolated networks, reachable only when a technician drives to the site. A site visit costs hours. You get one shot.

Python Tests Dependencies


The 30-second version

Problem Vendors publish updates in supersedence chains — C replaces B replaces A. Install all three and you waste a maintenance window; install A when C exists and you deploy a patch that's obsolete before the technician's back in the van. Operators resolve this by eyeballing icons in a console, under time pressure, at a roadside cabinet. The failure mode is silent.
Solution Model supersedence as a DAG. Approve the leaves, decline everything upstream of them. Refuse to answer on a corrupt catalogue. Prove that declining a patch never drops a CVE.
Result 17 tests. Branch and merge cases that version-number comparison gets wrong. Cycle detection. CVE continuity checking. Zero dependencies.
Stack Python 3.12 · pytest · GitHub Actions

Why a graph, not a version comparison

Supersedence is not a total order:

flowchart LR
    A["Patch A<br/>CVE-2026-1111"] --> B["Patch B<br/>supersedes A"]
    A --> C["Patch C<br/>supersedes A"]
    D["Patch D"] --> E["Patch E<br/>supersedes B, D"]
    B --> E

    style B fill:#e8f5e9,stroke:#2e7d32
    style C fill:#e8f5e9,stroke:#2e7d32
    style E fill:#e8f5e9,stroke:#2e7d32
    style A fill:#ffebee,stroke:#c62828
    style D fill:#ffebee,stroke:#c62828
Loading

One patch can supersede several. Several can supersede the same one. Chains branch when a vendor forks a fix across product versions, and merge when they consolidate. "Compare version numbers" breaks on the first branch — quietly, which is the worst way.

What you want is the set of nodes with no outgoing supersedence edge. That's a graph problem, so it's solved as one.

g = SupersedenceGraph([a, b, c])   # B and C both supersede A
[r.update_id for r in g.resolve() if r.decision is Decision.APPROVE]
# ['B', 'C']   ← both are current; neither supersedes the other

The three things it refuses to get wrong

A cycle means the catalogue is corrupt — so it raises. A supersedes B supersedes A cannot be true. If it appears, something upstream is broken and every ruling derived from it is untrustworthy. Refusing to answer beats answering wrongly when the answer sends someone to a roadside cabinet.

A hole in the chain gets surfaced, not swallowed.

g.dropped_cves()   # CVEs in declined patches that the approved set doesn't cover

This should always be empty — a superseding patch is supposed to carry the fix forward. When it doesn't, a device comes back from a site visit still vulnerable, and nobody's driving out there again for three months. Worth failing a build over.

References outside the sync window are tolerated. Real catalogues reach back further than your sync. A resolver that errors on an unknown reference is a resolver that never runs on real data.


Install ordering, because windows get cut short

g.approved()      # criticals first; within a severity, no-reboot first
g.reboots_required()

A technician who runs out of time should already have the criticals in. Within a severity, no-reboot patches go first — they cost nothing to abandon halfway.


Run it

pip install -e '.[dev]'
pytest          # 17 passed

What's not here — and won't be

No deployment mechanism. No credentials. No device inventory. No network topology.

This is the decision layer: given a catalogue, what should be installed and in what order. How updates reach a device, which devices exist, and how to reach them are operational specifics that belong in an internal system, not a public repository. See docs/SOURCE-POLICY.md — the source gate in CI blocks hostnames, IP ranges and credentials on every push.

Sources

Original work. Supersedence semantics follow publicly documented vendor update practice. No operational procedure, runbook, or infrastructure detail is reproduced here.

Licence

MIT

About

No description or website provided.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages