Skip to main content

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

Apache-2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vulnctl-0.1.0.tar.gz (665.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vulnctl-0.1.0-py3-none-any.whl (483.2 kB view details)

Uploaded Python 3

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

Hashes for vulnctl-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a3f2e76732974213d9b72e2b4a3de014be5c3e09dcbf5fcb9ab354de1067d47d
MD5 d07f241438acabad1414cd4d007ce455
BLAKE2b-256 4ff9c66a052d601b348914c72b46efbff22223eee38aac366fe7eb0f5ef755ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for vulnctl-0.1.0.tar.gz:

Publisher: release.yml on NokiGuard/vulnctl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for vulnctl-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 17e53d1e43c8516c39124b9b6a367c012b2d1761606d3aeacca9da9c782afcb9
MD5 0ae5fcb171f911e9dbde722c15a9b658
BLAKE2b-256 2b6475c8213caf790b3f3faa03e25b2cb4b13af5760a6ec739025c2f553d8f92

See more details on using hashes here.

Provenance

The following attestation bundles were made for vulnctl-0.1.0-py3-none-any.whl:

Publisher: release.yml on NokiGuard/vulnctl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page