vulnctl
Auditable decisions, not scores.
CVSS base scores are a poor way to decide what to fix first: they ignore
exploitation likelihood, exploit availability, your asset exposure, and your
risk tolerance. Teams end up drowning in "criticals" that will never be
exploited while missing medium-severity CVEs under active attack. vulnctl
fuses the public intelligence that actually predicts risk — EPSS, CISA KEV,
NVD, OSV, GHSA, and public exploit feeds — evaluates each finding against a
declarative SSVC decision tree using your organizational context, and
emits a ranked set of Track / Track* / Attend / Act verdicts. Every
verdict ships with its full decision path: which input, what value, and which
source supplied it. The audit trail is the product — you can defend each call
to engineering and leadership instead of hand-waving at a number.
What it does
- Ingest a list of CVE or GHSA IDs, a CycloneDX SBOM (1.4–1.6), or Grype scanner JSON. GHSA identifiers alias-resolve to CVEs via OSV.
- Enrich each finding from EPSS (exploit probability), CISA KEV (known exploited + ransomware), NVD (CVSS vector, CWE), OSV/GHSA (affected/fixed versions), and exploit presence (Exploit-DB, Metasploit, nuclei).
- Decide with a bundled CISA-style SSVC deployer tree — or bring your own
with
--tree. - Explain every verdict with the full path that produced it — and one
finding in depth with
vulnctl explain <ID>: each source's answer and provenance, plus which single input change would flip the decision. Degraded inputs (a source down, or--offline) are visibly flagged, never hidden. - Output a rich table, JSON, SARIF 2.1.0 (GitHub code scanning), or a
stakeholder Markdown report — and gate CI with
--fail-on.
Install
pipx install vulnctl
vulnctl --version
Requires Python 3.11+ on Linux or macOS. No credentials are required; an NVD
API key (optional, for higher rate limits) is read from the
VULNCTL_NVD_API_KEY environment variable only.
Enable tab completion for commands, flags, and values with
vulnctl --install-completion (see docs/cli.md).
Quickstart (60 seconds)
1. See a verdict and the decision path behind it. This runs entirely from bundled snapshots — no network, no API key:
$ vulnctl enrich CVE-2021-44228 --offline --show-path
vulnctl enrichment
┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ CVE ┃ Decision ┃ CVSS ┃ EPSS ┃ KEV ┃ Exploits ┃
┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ CVE-2021-44228 │ ACT │ n/a (offline) │ 1.000 (p100.0) │ yes ransomware │ EDB·3 MSF·5 nuclei·1 │
└────────────────┴──────────┴───────────────┴────────────────┴────────────────┴──────────────────────┘
sources: epss, exploits, ghsa, kev, nvd, osv · cache hits: 0% avg · degraded: ghsa 1 (offline), nvd 1
(offline), osv 1 (offline) · offline mode
CVE-2021-44228 → ACT (tree cisa-deployer-v1) [degraded: defaults applied]
1. exploitation = active [kev]
2. exposure = open [context]
3. automatable = yes [default]
4. human_impact = high [context]
1 finding(s) · 1 ACT · 1 KEV-listed
The path is the point. This is ACT because CISA KEV lists it as actively
exploited (exploitation = active, from kev), it's treated as internet-exposed
(exposure = open, from the default org context), and mission impact is high.
automatable fell back to the tree default here because --offline has no
CVSS vector to derive it from — so the verdict is flagged degraded. Drop
--offline and the live CVSS vector resolves automatable from data
(value_source = cvss), clearing the flag.
For the whole evidence file behind one verdict — every source's answer with its
provenance, the fixed versions, and each single input change that would land on
a different decision — run vulnctl explain CVE-2021-44228.
2. Feed it your context. Exposure, mission impact, and overrides that no
intel source can know come from a small context.yaml:
vulnctl enrich CVE-2021-44228 --context examples/context.yaml --show-path
3. Prioritize a whole SBOM or scanner report:
# CycloneDX SBOM: components resolve to CVEs via OSV, then rank
vulnctl enrich --sbom app.cdx.json --context context.yaml
# Grype JSON straight off a scan (‘-’ reads stdin)
grype my-image:latest -o json | vulnctl enrich --grype - --context context.yaml
4. Gate CI on risk, not raw CVSS. Emit SARIF for code scanning, then fail the build only when something crosses your threshold:
vulnctl enrich --sbom app.cdx.json --format sarif > vulnctl.sarif # always written
vulnctl enrich --sbom app.cdx.json --fail-on act # exit 2 blocks the PR
A complete GitHub Actions gate lives in
examples/ci/vulnctl-gate.yml.
How it works
Four strictly-ordered layers, data flowing one way — Ingest → Enrich →
Decide → Output. Source adapters are isolated and fail open (a source
outage degrades one field, never the run); the SSVC engine is a pure,
deterministic tree-walker that records every node visit. Verdicts come from a
declarative SSVC tree (bundled: cisa-deployer-v1, the CERT/CC deployer model
with CISA's Track/Track*/Attend/Act labels). See
FRAMEWORK.md for the architecture.
Documentation
| Doc | What's in it |
|---|---|
| docs/cli.md | CLI reference: every command, argument, and option |
| docs/output.md | Reading the output: what Track/Track*/Attend/Act mean, the decision path, and every enrichment column |
| docs/context.md | Every context.yaml field, its values, and how each maps to an SSVC decision point |
| docs/trees.md | The YAML decision-tree format and how to author/validate a custom tree |
| docs/schema.md | The --format json output schema (machine-readable schema.json) |
| docs/exit-codes.md | Exit codes and --fail-on semantics for CI gating |
| docs/releasing.md | Cut-a-release runbook: build, sign, publish, verify |
| SPEC.md · FRAMEWORK.md · CLAUDE.md | Product spec, architecture, and contributor conventions |
Development
Requires uv and Python 3.11+.
uv sync # install deps (incl. dev group)
uv run pytest # tests
uv run ruff check . # lint
uv run ruff format --check . # format check
uv run mypy src/ # type check
uv run vulnctl --help # smoke-test the CLI
uv run pre-commit install # wire the git hook
All four checks (pytest, ruff check, ruff format, mypy) must pass before any commit. The SSVC engine holds a 100% branch-coverage gate in CI.
Security posture
vulnctl is a security tool and holds itself to the bar it enforces: GitHub
Actions pinned by full commit SHA, least-privilege token scopes, a committed
lockfile, no eval and no pickle of untrusted data, and strict Pydantic
validation of every byte of external JSON before it becomes a model. Releases
are built with an SBOM (Syft), scanned (Grype, and dogfooded through vulnctl
itself), and signed with keyless cosign — see
docs/releasing.md.
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vulnctl-0.2.1.tar.gz.
File metadata
- Download URL: vulnctl-0.2.1.tar.gz
- Upload date:
- Size: 691.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
762c76b33ab613e3d2861d592a4e0366f3dbc41b5932acce50c7794b5503caa6
|
|
| MD5 |
7ceb642bcddc186b896d6feb26fa7580
|
|
| BLAKE2b-256 |
d73e025ffd9d8d261c05d2c2ed783a767d10c3b09b920750b0fa0c6cb9028b7e
|
Provenance
The following attestation bundles were made for vulnctl-0.2.1.tar.gz:
Publisher:
release.yml on NokiGuard/vulnctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vulnctl-0.2.1.tar.gz -
Subject digest:
762c76b33ab613e3d2861d592a4e0366f3dbc41b5932acce50c7794b5503caa6 - Sigstore transparency entry: 2310521463
- Sigstore integration time:
-
Permalink:
NokiGuard/vulnctl@10afa3c2932e060ac015a4e1e84600ff01738539 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/NokiGuard
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@10afa3c2932e060ac015a4e1e84600ff01738539 -
Trigger Event:
push
-
Statement type:
File details
Details for the file vulnctl-0.2.1-py3-none-any.whl.
File metadata
- Download URL: vulnctl-0.2.1-py3-none-any.whl
- Upload date:
- Size: 498.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8a9733c028637df6e4b00783d9ca1eb9ad15bc6613a2f5f4bbc3f3d9e37af68
|
|
| MD5 |
5ed22fa9b99d57a9a9371cb5d3bbda1b
|
|
| BLAKE2b-256 |
e21cf03a042bccc3b5e0dd1743df361b3131d64f7143fdf925d06e745ee29975
|
Provenance
The following attestation bundles were made for vulnctl-0.2.1-py3-none-any.whl:
Publisher:
release.yml on NokiGuard/vulnctl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vulnctl-0.2.1-py3-none-any.whl -
Subject digest:
c8a9733c028637df6e4b00783d9ca1eb9ad15bc6613a2f5f4bbc3f3d9e37af68 - Sigstore transparency entry: 2310521493
- Sigstore integration time:
-
Permalink:
NokiGuard/vulnctl@10afa3c2932e060ac015a4e1e84600ff01738539 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/NokiGuard
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@10afa3c2932e060ac015a4e1e84600ff01738539 -
Trigger Event:
push
-
Statement type: