CLI-first vulnerability prioritization: auditable SSVC verdicts from fused threat intelligence
Project description
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 CVE list, a CycloneDX SBOM (1.4–1.6), or Grype scanner JSON.
- 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; 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.
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 2021-12-10 │ EDB·3 MSF·5 │
│ │ │ │ │ ransomware │ nuclei·1 │
└────────────────┴──────────┴───────────────┴────────────────┴──────────────────┴──────────────────┘
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]
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.
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/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
Project details
Release history Release notifications | RSS feed
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.1.0.tar.gz.
File metadata
- Download URL: vulnctl-0.1.0.tar.gz
- Upload date:
- Size: 665.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 |
a3f2e76732974213d9b72e2b4a3de014be5c3e09dcbf5fcb9ab354de1067d47d
|
|
| MD5 |
d07f241438acabad1414cd4d007ce455
|
|
| BLAKE2b-256 |
4ff9c66a052d601b348914c72b46efbff22223eee38aac366fe7eb0f5ef755ab
|
Provenance
The following attestation bundles were made for vulnctl-0.1.0.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.1.0.tar.gz -
Subject digest:
a3f2e76732974213d9b72e2b4a3de014be5c3e09dcbf5fcb9ab354de1067d47d - Sigstore transparency entry: 2133968615
- Sigstore integration time:
-
Permalink:
NokiGuard/vulnctl@825591d942d8ddfb2bb4cd0a83d5f4515170e1be -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/NokiGuard
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@825591d942d8ddfb2bb4cd0a83d5f4515170e1be -
Trigger Event:
push
-
Statement type:
File details
Details for the file vulnctl-0.1.0-py3-none-any.whl.
File metadata
- Download URL: vulnctl-0.1.0-py3-none-any.whl
- Upload date:
- Size: 483.2 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 |
17e53d1e43c8516c39124b9b6a367c012b2d1761606d3aeacca9da9c782afcb9
|
|
| MD5 |
0ae5fcb171f911e9dbde722c15a9b658
|
|
| BLAKE2b-256 |
2b6475c8213caf790b3f3faa03e25b2cb4b13af5760a6ec739025c2f553d8f92
|
Provenance
The following attestation bundles were made for vulnctl-0.1.0-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.1.0-py3-none-any.whl -
Subject digest:
17e53d1e43c8516c39124b9b6a367c012b2d1761606d3aeacca9da9c782afcb9 - Sigstore transparency entry: 2133968683
- Sigstore integration time:
-
Permalink:
NokiGuard/vulnctl@825591d942d8ddfb2bb4cd0a83d5f4515170e1be -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/NokiGuard
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@825591d942d8ddfb2bb4cd0a83d5f4515170e1be -
Trigger Event:
push
-
Statement type: