Package vulnerability triage and remediation for Debian-family systems. pygen
takes the packages actually installed on a host, compares their versions against
published security notices to find what is genuinely unpatched, prioritises each
issue with CVSS + EPSS + CISA KEV, assigns it to an owning team, and produces
ready-to-run remediation reports.
It does real version-aware detection (a faithful pure-Python port of dpkg's version-comparison algorithm), not CVE-id string matching, so it does not over-report issues that are already fixed in the version you run.
Generic "match the CVE feed" tooling over-reports: a CVE can appear in a feed but
already be fixed in the exact package version installed. pygen answers the
question an operator actually has:
Given what is installed on this host and this release, what is genuinely unpatched, how urgent is each item, who owns it, and what is the exact command to fix it?
- Version-aware detection. A package is vulnerable only when its installed
version is strictly older than the fixed version for its release, decided by a
faithful port of dpkg
verrevcmp(epochs,~pre-release ordering, leading-zero normalisation). Seeversion.py. - Risk-based prioritisation. Combines CVSS (severity), EPSS (30-day exploit probability) and CISA KEV (confirmed active exploitation) into a transparent 0-100 score and an SSVC-style action: Act / Attend / Track.
- Ownership routing. A small YAML policy turns a flat CVE list into per-team work queues.
- Reports for humans and machines. Markdown and HTML (per-owner remediation plans) plus JSON and CSV for pipelines and ticketing.
- Offline / hermetic. Every feed can be a local snapshot;
--offlinenever touches the network, so demos, CI and air-gapped hosts are deterministic. - Hardening companion. A CIS-aligned check of SSH, AppArmor and auditd configuration with pass/fail and concrete remediation.
dpkg-query ─┐
│ ┌── EPSS (FIRST.org) ──┐
USN feed ───┼─► match ──►│ KEV (CISA) ├─► prioritise ─► assign ─► report
│ (version │ CVSS │ (SSVC-ish) owner md/html/
release ────┘ compare) └──────────────────────┘ json/csv
| Stage | Module |
|---|---|
| Collect installed packages / release | sources/dpkg.py |
| Version comparison (core algorithm) | version.py |
| Ingest security-notice database | sources/usn.py |
| Match installed vs. fixed | matching.py |
| Enrich (EPSS / KEV / CVSS) | sources/ + prioritize.py |
| Assign owners | ownership.py |
| Reports | report/ |
| Pipeline orchestration | scan.py |
| Hardening companion | hardening/ |
See docs/architecture.md for the data flow in detail.
git clone https://github.com/PhinehasNarh/pygen.git
cd pygen
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"Bundled example data shows a full, enriched report immediately:
pygen scan --offline --release jammy \
--snapshot examples/sample-dpkg.txt \
--usn-db examples/feeds/usn.json \
--epss-file examples/feeds/epss.json \
--kev-file examples/feeds/kev.json \
--cve-file examples/feeds/cve.json \
--owners examples/owners.yaml \
--format md# Scan the running system against live feeds (cached for 24h).
pygen scan --owners examples/owners.yaml --format html --output report.html
# Or triage a remote host from your laptop by copying its package list:
dpkg-query -W -f='${Package}\t${Version}\t${Architecture}\n' > host.tsv # on the server
pygen scan --snapshot host.tsv --release jammy --format md # anywhereThe process exits 2 when any finding is Act, so it slots straight into CI
or cron as a gate.
## web-platform (5)
| Action | Risk | Sev | Package | Installed | Fixed | CVE | USN | EPSS | KEV |
|--------|-----:|-----|---------|-----------|-------|-----|-----|-----:|-----|
| Act | 97.1 | high | `apache2` | 2.4.52-1ubuntu4.0 | 2.4.52-1ubuntu4.1 | CVE-2022-31813 | USN-5678-1 | 0.94321 | yes |
| Attend | 44.5 | high | `libssl3` | 3.0.2-0ubuntu1.5 | 3.0.2-0ubuntu1.8 | CVE-2023-0286 | USN-6001-1 | 0.0044 | no |
Remediation:
\`\`\`bash
sudo apt-get install --only-upgrade apache2
sudo apt-get install --only-upgrade libssl3
\`\`\`An HTML version is at examples/sample-report.html.
Each finding gets a transparent risk score in 0-100:
risk = cvss * 6 (0-60, intrinsic severity)
+ epss * 30 (0-30, 30-day exploit probability)
+ 10 if in CISA KEV
and an action by a simplified SSVC rule:
- Act when in KEV, or EPSS ≥ 0.20, or CVSS ≥ 9.0, or severity critical.
- Attend when EPSS ≥ 0.05, or CVSS ≥ 7.0, or severity high.
- Track otherwise.
Every input is visible in the report, so a priority can always be explained rather than trusted blindly.
The summary reports estimated manual triage time eliminated:
findings x minutes-per-finding (default 15 min, configurable with --minutes).
Each finding otherwise costs an analyst time to look up the CVE, find the fixed
version, confirm exposure and route it. The constant is stated up front so the
figure is auditable, not hand-waved.
# Evaluate a config file (portable, no host access):
pygen hardening --sshd-config /etc/ssh/sshd_config
# Or collect SSH + AppArmor + auditd facts from the live host:
pygen hardeningChecks a representative CIS subset (SSH daemon hardening, AppArmor enforcement,
audit daemon) and prints pass/fail with remediation. Exits 1 on any failure.
ruff check src tests # lint
black --check src tests # format
mypy # strict type-check
pytest --cov=pygen # tests (hermetic, no network)
# or run all of the above:
./scripts/run_checks.shCI runs the same gate on Python 3.10-3.12 (see .github/workflows/ci.yml).
- OVAL-based matching using published OVAL feeds.
- SLA / vulnerability-aging tracking with breach alerts.
--diffmode to report only newly-introduced findings between scans.
All feed data is treated as untrusted input: fixed-argument subprocess calls
(no shell), cache-key sanitisation, strict CVE-id validation before URL use,
Jinja2 autoescaping, and yaml.safe_load only. Reviewed with bandit (no
medium/high findings) and pip-audit. See SECURITY.md.