vulnctl 0.2.2__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vulnctl-0.2.2 → vulnctl-0.3.0}/.github/workflows/release.yml +1 -1
- {vulnctl-0.2.2 → vulnctl-0.3.0}/CHANGELOG.md +47 -1
- {vulnctl-0.2.2 → vulnctl-0.3.0}/CLAUDE.md +4 -2
- {vulnctl-0.2.2 → vulnctl-0.3.0}/FRAMEWORK.md +1 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/PKG-INFO +23 -6
- {vulnctl-0.2.2 → vulnctl-0.3.0}/README.md +20 -4
- {vulnctl-0.2.2 → vulnctl-0.3.0}/SPEC.md +1 -1
- {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/cli.md +88 -0
- vulnctl-0.3.0/examples/large-app.cdx.json +1134 -0
- vulnctl-0.3.0/examples/medium-app.cdx.json +385 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/pyproject.toml +7 -2
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/base.py +13 -9
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/exploits.py +1 -1
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/ghsa.py +10 -3
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/nvd.py +55 -6
- vulnctl-0.3.0/src/vulnctl/auth.py +194 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/cli.py +2 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/cli_help.py +4 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/commands.py +5 -0
- vulnctl-0.3.0/src/vulnctl/credentials.py +144 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/models.py +3 -2
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/pipeline.py +38 -20
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ssvc/tree.py +2 -3
- vulnctl-0.3.0/tests/conftest.py +80 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_nvd.py +86 -1
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapters_base.py +1 -8
- vulnctl-0.3.0/tests/test_auth.py +315 -0
- vulnctl-0.3.0/tests/test_credentials.py +137 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_pipeline.py +31 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/uv.lock +243 -141
- vulnctl-0.2.2/.github/workflows/publish-testpypi.yml +0 -29
- vulnctl-0.2.2/.pre-commit-config.yaml +0 -27
- vulnctl-0.2.2/ROADMAP.md +0 -198
- vulnctl-0.2.2/docs/announcement.md +0 -46
- vulnctl-0.2.2/docs/case-study.md +0 -95
- vulnctl-0.2.2/docs/demo.tape +0 -43
- vulnctl-0.2.2/scripts/case_study_stats.py +0 -139
- vulnctl-0.2.2/tests/conftest.py +0 -39
- {vulnctl-0.2.2 → vulnctl-0.3.0}/.github/workflows/ci.yml +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/.github/workflows/live-smoke.yml +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/.gitignore +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/.python-version +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/LICENSE +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/context.md +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/exit-codes.md +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/output.md +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/releasing.md +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/schema.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/schema.md +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/trees.md +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/examples/app.cdx.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/examples/ci/vulnctl-gate.yml +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/examples/context.yaml +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/scripts/build_exploit_index.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/__init__.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/__init__.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/epss.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/kev.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/osv.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/cache.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/context.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/data/__init__.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/data/epss_snapshot.csv.gz +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/data/exploit_index.json.gz +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/data/kev_snapshot.json.gz +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ingest/__init__.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ingest/cve_list.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ingest/cyclonedx.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ingest/grype.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/__init__.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/explain.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/json_out.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/markdown.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/progress.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/render.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/sarif.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/table.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/py.typed +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ssvc/__init__.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ssvc/engine.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ssvc/trees/cisa-deployer-v1.yaml +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/epss/batch.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/epss/malformed.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/epss/missing.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/advisory-by-cve.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/advisory-by-ghsa-id.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/malformed.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/not-found-empty-list.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/rate-limited.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/golden/enrich.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/golden/enrich.md +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/grype/duplicate-layers.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/grype/ghsa-only.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/grype/npm-app.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/kev/catalog.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/cve-2021-44228.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/multiple-cvss.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/not-found.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/rejected.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/v2-only.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/cve-2021-44228.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/malformed.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/not-found.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/querybatch-npm-app.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/querybatch.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-cve-record.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-ghsa-no-cve.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-ghsa-with-cve.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-go.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-pypi.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/sarif/sarif-schema-2.1.0.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/sbom/npm-app-1.6.cdx.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/sbom/npm-app.cdx.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/sbom/py-app.cdx.json +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/trees/toy.yaml +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/live/test_live_smoke.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_epss.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_exploits.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_ghsa.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_kev.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_osv.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_cache.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_cli.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_context.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ingest_cve_list.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ingest_cyclonedx.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ingest_grype.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_models.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_explain.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_json.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_markdown.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_progress.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_sarif.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_table.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ssvc_cisa_tree.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ssvc_engine.py +0 -0
- {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ssvc_tree.py +0 -0
|
@@ -196,7 +196,7 @@ jobs:
|
|
|
196
196
|
# Uploads only dist/*.whl and dist/*.tar.gz (signatures/SBOM live in
|
|
197
197
|
# separate artifacts). Generates PEP 740 attestations via the same OIDC
|
|
198
198
|
# identity by default.
|
|
199
|
-
uses: pypa/gh-action-pypi-publish@
|
|
199
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
200
200
|
|
|
201
201
|
release:
|
|
202
202
|
name: GitHub Release
|
|
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-10-09
|
|
11
|
+
|
|
12
|
+
Makes large runs practical: save a free NVD API key once and NVD lookups,
|
|
13
|
+
the bulk of a big SBOM's runtime, get about 10x faster. Also patches a
|
|
14
|
+
critical TLS advisory in a transitive dependency and trims unused files.
|
|
15
|
+
The JSON output shape and its `schema_version` (`"1"`) are unchanged.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Save your NVD API key once.** `vulnctl auth nvd` prompts for the key with
|
|
20
|
+
hidden input, verifies it with one NVD request, and stores it in the OS
|
|
21
|
+
keychain (macOS Keychain, Linux Secret Service, Windows Credential Locker).
|
|
22
|
+
`vulnctl auth status` shows where the active key comes from (masked), and
|
|
23
|
+
`vulnctl auth remove nvd` deletes it. A key makes NVD lookups — the
|
|
24
|
+
slowest part of enriching a large input — about 10x faster.
|
|
25
|
+
- **First-run key prompt**: the first interactive online `enrich` or `explain`
|
|
26
|
+
with no key configured offers to save one. Skipping is remembered and the
|
|
27
|
+
prompt never repeats; it never appears in scripts, pipes, CI, `--offline`
|
|
28
|
+
runs, or with `--grype -`.
|
|
29
|
+
- **Sample SBOMs** `examples/medium-app.cdx.json` (~230 CVEs) and
|
|
30
|
+
`examples/large-app.cdx.json` (~1,700 CVEs): real npm, PyPI, Maven, and Go
|
|
31
|
+
packages pinned to old, vulnerable versions, for trying vulnctl on
|
|
32
|
+
realistic volume and timing enrichment.
|
|
33
|
+
- **CLI reference additions**: a command index and an environment-variables
|
|
34
|
+
table in `docs/cli.md`; a commands table in the README.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **NVD key resolution**: `VULNCTL_NVD_API_KEY` still works and takes
|
|
39
|
+
precedence; the OS keychain is consulted when it is unset. Adds the
|
|
40
|
+
`keyring` dependency.
|
|
41
|
+
- **Security: `anyio` 4.15.1** in the lockfile (transitive, via httpx) for
|
|
42
|
+
GHSA-82r6-8w77-94w6 (Critical: TLS host name encoding enabling potential
|
|
43
|
+
certificate spoofing) and two subprocess advisories vulnctl never
|
|
44
|
+
triggers. Fresh installs already resolved a fixed version.
|
|
45
|
+
- **Pre-release cleanup**: removed unused project files (the unfinished
|
|
46
|
+
case-study and announcement drafts with their stats script, the unrecorded
|
|
47
|
+
demo tape, the completed v0.1 `ROADMAP.md`, and the superseded TestPyPI
|
|
48
|
+
workflow), the uninstalled `pre-commit` dev tooling, and the unused
|
|
49
|
+
`vulnctl.adapters.base.get_adapter()` helper. Local sdist builds now
|
|
50
|
+
exclude the maintainer's local-only notes.
|
|
51
|
+
- **A rejected NVD key is named as such**: NVD answers an invalid key with
|
|
52
|
+
HTTP 404; findings now report `NVD rejected the API key (Invalid apiKey)`
|
|
53
|
+
instead of a bare `HTTP 404`.
|
|
54
|
+
|
|
10
55
|
## [0.2.2] - 2026-08-01
|
|
11
56
|
|
|
12
57
|
### Fixed
|
|
@@ -142,7 +187,8 @@ that produced it.
|
|
|
142
187
|
sign with keyless cosign (OIDC), and publish to PyPI via trusted publishing
|
|
143
188
|
(no stored token).
|
|
144
189
|
|
|
145
|
-
[Unreleased]: https://github.com/NokiGuard/vulnctl/compare/v0.
|
|
190
|
+
[Unreleased]: https://github.com/NokiGuard/vulnctl/compare/v0.3.0...HEAD
|
|
191
|
+
[0.3.0]: https://github.com/NokiGuard/vulnctl/compare/v0.2.2...v0.3.0
|
|
146
192
|
[0.2.2]: https://github.com/NokiGuard/vulnctl/compare/v0.2.1...v0.2.2
|
|
147
193
|
[0.2.1]: https://github.com/NokiGuard/vulnctl/compare/v0.2.0...v0.2.1
|
|
148
194
|
[0.2.0]: https://github.com/NokiGuard/vulnctl/compare/v0.1.0...v0.2.0
|
|
@@ -40,8 +40,10 @@ src/vulnctl/
|
|
|
40
40
|
├── cli.py # Typer app wiring + cache commands; thin — no business logic here
|
|
41
41
|
├── cli_help.py # root --help extras: generated flag summary + examples panels
|
|
42
42
|
├── commands.py # enrich/explain command signatures; dispatch only
|
|
43
|
+
├── auth.py # `vulnctl auth` commands + first-run NVD key prompt
|
|
44
|
+
├── credentials.py # NVD key resolution (env var > OS keychain), save/remove
|
|
43
45
|
├── models.py # Pydantic: Finding, Enrichment, Verdict, DecisionPath
|
|
44
|
-
├── ingest/ # cve_list.py, cyclonedx.py,
|
|
46
|
+
├── ingest/ # cve_list.py, cyclonedx.py, grype.py (spdx/trivy: FR-4, not yet built)
|
|
45
47
|
├── adapters/ # one module per intel source; all implement SourceAdapter ABC
|
|
46
48
|
│ ├── base.py # SourceAdapter ABC + registry
|
|
47
49
|
│ ├── epss.py, kev.py, nvd.py, osv.py, ghsa.py, exploits.py
|
|
@@ -62,7 +64,7 @@ tests/
|
|
|
62
64
|
3. **Fail open on enrichment, fail loud on input.** A source being down degrades the enrichment (mark the field `unavailable`, note it in the decision path) — it never crashes a run. Malformed SBOMs/scanner files are hard errors with actionable messages.
|
|
63
65
|
4. **Every verdict is explainable.** `DecisionPath` records each tree node visited, the input value used, and which source supplied it. If you add a scoring signal, you must thread it through `DecisionPath`.
|
|
64
66
|
5. **Offline mode is first-class.** `--offline` must work using only cached/bundled data (EPSS CSV snapshot, KEV JSON snapshot). Adapters declare whether they support offline.
|
|
65
|
-
6. **No secrets in code or fixtures.** NVD API key comes from `VULNCTL_NVD_API_KEY` env var
|
|
67
|
+
6. **No secrets in code or fixtures.** The NVD API key comes from the `VULNCTL_NVD_API_KEY` env var, else the OS keychain (`keyring`, service `vulnctl`) — never a config file, the cache, or a CLI flag. Only the CLI layer (`credentials.py`/`auth.py`) touches the keychain; the pipeline hands adapters the resolved key as their `credential`. Tests run against an in-memory keychain (`tests/conftest.py`). Scrub any recorded fixtures of keys before committing.
|
|
66
68
|
|
|
67
69
|
## Security posture (this is a security tool — hold the bar)
|
|
68
70
|
|
|
@@ -79,6 +79,7 @@ class SourceAdapter(ABC):
|
|
|
79
79
|
- Registered in a simple registry (same pluggable pattern as composeguard's rule engine)
|
|
80
80
|
- Each adapter: check cache → batch-fetch misses (bounded concurrency, per-source rate limits) → validate against strict Pydantic schema → write cache → return
|
|
81
81
|
- Isolation rule: adapters import only `base`, `cache`, `models`. Adding a source touches exactly one new module + fixtures + registry line
|
|
82
|
+
- `credential` constructor argument: the source's optional API key, resolved by the CLI layer (env var, then OS keychain — `credentials.py`) and passed per adapter name by the pipeline. Adapters never read the keychain themselves
|
|
82
83
|
- `progress` attribute (display plumbing, assigned by the pipeline, no-op by default): adapters call `self.progress(n)` as work for `n` IDs completes; total advances per `fetch` must sum to `len(cve_ids)`. Never awaited, never load-bearing
|
|
83
84
|
|
|
84
85
|
### 3.3 Enrichment pipeline
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: vulnctl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: CLI-first vulnerability prioritization: auditable SSVC verdicts from fused threat intelligence
|
|
5
5
|
Project-URL: Homepage, https://github.com/NokiGuard/vulnctl
|
|
6
6
|
Author: Brian Truong
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Classifier: Topic :: Security
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
Requires-Dist: httpx>=0.28
|
|
21
|
+
Requires-Dist: keyring>=25
|
|
21
22
|
Requires-Dist: pydantic>=2.9
|
|
22
23
|
Requires-Dist: rich>=13.9
|
|
23
24
|
Requires-Dist: ruamel-yaml>=0.18
|
|
@@ -63,13 +64,26 @@ pipx install vulnctl
|
|
|
63
64
|
vulnctl --version
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
Requires Python 3.11+ on Linux or macOS. No credentials are required
|
|
67
|
-
API key
|
|
68
|
-
`
|
|
67
|
+
Requires Python 3.11+ on Linux or macOS. No credentials are required, but a
|
|
68
|
+
free NVD API key makes enrichment about 10x faster on large inputs: the first
|
|
69
|
+
online run offers to save one to your OS keychain, or run `vulnctl auth nvd`
|
|
70
|
+
any time. The `VULNCTL_NVD_API_KEY` environment variable overrides the saved
|
|
71
|
+
key (use it in CI). See [docs/cli.md](docs/cli.md#vulnctl-auth).
|
|
69
72
|
|
|
70
73
|
Enable tab completion for commands, flags, and values with
|
|
71
74
|
`vulnctl --install-completion` (see [docs/cli.md](docs/cli.md)).
|
|
72
75
|
|
|
76
|
+
## Commands
|
|
77
|
+
|
|
78
|
+
| Command | What it does |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `vulnctl enrich` | Enrich CVE/GHSA IDs, an SBOM (`--sbom`), or a Grype scan (`--grype`) and rank with SSVC verdicts |
|
|
81
|
+
| `vulnctl explain <ID>` | One finding in depth: every source's answer, the decision path, and what would flip it |
|
|
82
|
+
| `vulnctl auth nvd` · `status` · `remove nvd` | Save, inspect, or delete your NVD API key (OS keychain) |
|
|
83
|
+
| `vulnctl cache stats` · `purge` | Inspect or clear the local response cache |
|
|
84
|
+
|
|
85
|
+
Every flag and example: `vulnctl --help`, or [docs/cli.md](docs/cli.md).
|
|
86
|
+
|
|
73
87
|
## Quickstart (60 seconds)
|
|
74
88
|
|
|
75
89
|
**1. See a verdict and the decision path behind it.** This runs entirely from
|
|
@@ -123,6 +137,10 @@ vulnctl enrich --sbom app.cdx.json --context context.yaml
|
|
|
123
137
|
grype my-image:latest -o json | vulnctl enrich --grype - --context context.yaml
|
|
124
138
|
```
|
|
125
139
|
|
|
140
|
+
No SBOM handy? [`examples/medium-app.cdx.json`](examples/medium-app.cdx.json)
|
|
141
|
+
(~230 CVEs) and [`examples/large-app.cdx.json`](examples/large-app.cdx.json)
|
|
142
|
+
(~1,700 CVEs) are real packages pinned to old, vulnerable versions.
|
|
143
|
+
|
|
126
144
|
**4. Gate CI on risk, not raw CVSS.** Emit SARIF for code scanning, then fail
|
|
127
145
|
the build only when something crosses your threshold:
|
|
128
146
|
|
|
@@ -168,7 +186,6 @@ uv run ruff check . # lint
|
|
|
168
186
|
uv run ruff format --check . # format check
|
|
169
187
|
uv run mypy src/ # type check
|
|
170
188
|
uv run vulnctl --help # smoke-test the CLI
|
|
171
|
-
uv run pre-commit install # wire the git hook
|
|
172
189
|
```
|
|
173
190
|
|
|
174
191
|
All four checks (pytest, ruff check, ruff format, mypy) must pass before any
|
|
@@ -37,13 +37,26 @@ pipx install vulnctl
|
|
|
37
37
|
vulnctl --version
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Requires Python 3.11+ on Linux or macOS. No credentials are required
|
|
41
|
-
API key
|
|
42
|
-
`
|
|
40
|
+
Requires Python 3.11+ on Linux or macOS. No credentials are required, but a
|
|
41
|
+
free NVD API key makes enrichment about 10x faster on large inputs: the first
|
|
42
|
+
online run offers to save one to your OS keychain, or run `vulnctl auth nvd`
|
|
43
|
+
any time. The `VULNCTL_NVD_API_KEY` environment variable overrides the saved
|
|
44
|
+
key (use it in CI). See [docs/cli.md](docs/cli.md#vulnctl-auth).
|
|
43
45
|
|
|
44
46
|
Enable tab completion for commands, flags, and values with
|
|
45
47
|
`vulnctl --install-completion` (see [docs/cli.md](docs/cli.md)).
|
|
46
48
|
|
|
49
|
+
## Commands
|
|
50
|
+
|
|
51
|
+
| Command | What it does |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `vulnctl enrich` | Enrich CVE/GHSA IDs, an SBOM (`--sbom`), or a Grype scan (`--grype`) and rank with SSVC verdicts |
|
|
54
|
+
| `vulnctl explain <ID>` | One finding in depth: every source's answer, the decision path, and what would flip it |
|
|
55
|
+
| `vulnctl auth nvd` · `status` · `remove nvd` | Save, inspect, or delete your NVD API key (OS keychain) |
|
|
56
|
+
| `vulnctl cache stats` · `purge` | Inspect or clear the local response cache |
|
|
57
|
+
|
|
58
|
+
Every flag and example: `vulnctl --help`, or [docs/cli.md](docs/cli.md).
|
|
59
|
+
|
|
47
60
|
## Quickstart (60 seconds)
|
|
48
61
|
|
|
49
62
|
**1. See a verdict and the decision path behind it.** This runs entirely from
|
|
@@ -97,6 +110,10 @@ vulnctl enrich --sbom app.cdx.json --context context.yaml
|
|
|
97
110
|
grype my-image:latest -o json | vulnctl enrich --grype - --context context.yaml
|
|
98
111
|
```
|
|
99
112
|
|
|
113
|
+
No SBOM handy? [`examples/medium-app.cdx.json`](examples/medium-app.cdx.json)
|
|
114
|
+
(~230 CVEs) and [`examples/large-app.cdx.json`](examples/large-app.cdx.json)
|
|
115
|
+
(~1,700 CVEs) are real packages pinned to old, vulnerable versions.
|
|
116
|
+
|
|
100
117
|
**4. Gate CI on risk, not raw CVSS.** Emit SARIF for code scanning, then fail
|
|
101
118
|
the build only when something crosses your threshold:
|
|
102
119
|
|
|
@@ -142,7 +159,6 @@ uv run ruff check . # lint
|
|
|
142
159
|
uv run ruff format --check . # format check
|
|
143
160
|
uv run mypy src/ # type check
|
|
144
161
|
uv run vulnctl --help # smoke-test the CLI
|
|
145
|
-
uv run pre-commit install # wire the git hook
|
|
146
162
|
```
|
|
147
163
|
|
|
148
164
|
All four checks (pytest, ruff check, ruff format, mypy) must pass before any
|
|
@@ -62,7 +62,7 @@ A CLI-first vulnerability prioritization engine that turns raw findings into **a
|
|
|
62
62
|
## 5. Non-functional requirements
|
|
63
63
|
|
|
64
64
|
- **NFR-1:** Enrich 500 CVEs in < 60s warm-cache, < 5min cold (API rate limits permitting)
|
|
65
|
-
- **NFR-2:** Zero required credentials; NVD API key optional (higher rate limits) via env var
|
|
65
|
+
- **NFR-2:** Zero required credentials; NVD API key optional (higher rate limits) via env var or the OS keychain (`vulnctl auth nvd`, offered once on the first interactive online run)
|
|
66
66
|
- **NFR-3:** Runs on Linux and macOS; Python 3.11+; installable via `pipx install vulnctl`
|
|
67
67
|
- **NFR-4:** Test coverage ≥ 85% on `ssvc/` and `adapters/`; SSVC engine at 100% branch coverage
|
|
68
68
|
- **NFR-5:** All third-party API schemas validated defensively; malformed upstream data degrades, never crashes
|
|
@@ -11,6 +11,18 @@ from the real signatures.
|
|
|
11
11
|
vulnctl [OPTIONS] COMMAND [ARGS]...
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
| Command | What it does |
|
|
17
|
+
|---|---|
|
|
18
|
+
| [`enrich`](#vulnctl-enrich) | Enrich CVE/GHSA IDs, an SBOM, or a Grype scan and rank the findings with SSVC verdicts. |
|
|
19
|
+
| [`explain`](#vulnctl-explain) | One finding in depth: every source's answer, the decision path, and what would change the verdict. |
|
|
20
|
+
| [`auth nvd`](#vulnctl-auth) | Save or replace your NVD API key in the OS keychain (verified first). |
|
|
21
|
+
| [`auth status`](#vulnctl-auth) | Show which NVD key vulnctl will use and where it comes from (masked). |
|
|
22
|
+
| [`auth remove nvd`](#vulnctl-auth) | Delete the saved NVD key from the keychain. |
|
|
23
|
+
| [`cache stats`](#vulnctl-cache) | Show cache location, size, and entries per source. |
|
|
24
|
+
| [`cache purge`](#vulnctl-cache) | Delete cached entries, all or one source's. |
|
|
25
|
+
|
|
14
26
|
## Global options
|
|
15
27
|
|
|
16
28
|
| Option | What it does |
|
|
@@ -54,6 +66,10 @@ piped stdout — `-f json | jq` — stays byte-clean. It appears only when
|
|
|
54
66
|
stderr is a real terminal, clears itself when the run completes, and is
|
|
55
67
|
skipped entirely for `--offline` runs (they finish instantly).
|
|
56
68
|
|
|
69
|
+
Online runs read the NVD API key from `VULNCTL_NVD_API_KEY` or the OS
|
|
70
|
+
keychain; with neither, the first interactive run offers to save one — see
|
|
71
|
+
[`vulnctl auth`](#vulnctl-auth).
|
|
72
|
+
|
|
57
73
|
Exit codes: `0` success, `1` input/config error, `2` `--fail-on` threshold
|
|
58
74
|
met — full semantics in [exit-codes.md](exit-codes.md).
|
|
59
75
|
|
|
@@ -68,8 +84,18 @@ vulnctl enrich --sbom app.cdx.json --context context.yaml --format json
|
|
|
68
84
|
|
|
69
85
|
# Grype scan from stdin, gate CI on Attend or worse
|
|
70
86
|
grype my-image:latest -o json | vulnctl enrich --grype - --fail-on attend
|
|
87
|
+
|
|
88
|
+
# Bundled sample SBOMs of old, vulnerable packages (npm, PyPI, Maven, Go)
|
|
89
|
+
vulnctl enrich --sbom examples/medium-app.cdx.json --limit 25 # ~230 CVEs
|
|
90
|
+
vulnctl enrich --sbom examples/large-app.cdx.json -f json > large.json # ~1,700 CVEs
|
|
71
91
|
```
|
|
72
92
|
|
|
93
|
+
The two sample SBOMs in `examples/` are sized for trying vulnctl on realistic
|
|
94
|
+
volume and for timing runs: with an NVD key, NVD lookups for the medium file
|
|
95
|
+
take about 3 minutes cold and the large one about 22 (roughly 10x longer
|
|
96
|
+
without a key). Repeat runs answer from the cache in seconds. CVE counts drift
|
|
97
|
+
as new advisories are published against these old versions.
|
|
98
|
+
|
|
73
99
|
## `vulnctl explain`
|
|
74
100
|
|
|
75
101
|
```
|
|
@@ -106,6 +132,68 @@ vulnctl cache COMMAND [ARGS]...
|
|
|
106
132
|
| `cache stats` | Show cache location, size, and entry counts per source. |
|
|
107
133
|
| `cache purge` | Delete all cached entries, or one source's with `--source <name>` (e.g. `--source epss`). |
|
|
108
134
|
|
|
135
|
+
## `vulnctl auth`
|
|
136
|
+
|
|
137
|
+
Manage the optional NVD API key. No credentials are ever required, but
|
|
138
|
+
without a key NVD allows about 7 lookups a minute — on a large SBOM that is
|
|
139
|
+
most of the run. A free key ([request one here](https://nvd.nist.gov/developers/request-an-api-key))
|
|
140
|
+
makes NVD lookups roughly 10x faster.
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
vulnctl auth COMMAND [ARGS]...
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| Command | What it does |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `auth nvd` | Prompt for your key (hidden input), verify it with one NVD request, and save it to your OS keychain. Re-run to replace it. |
|
|
149
|
+
| `auth status` | Show whether a key is configured and where it comes from; the key itself is masked to its last four characters. |
|
|
150
|
+
| `auth remove nvd` | Delete the saved key from the keychain. |
|
|
151
|
+
|
|
152
|
+
**Where the key lives.** In the OS keychain only — macOS Keychain, Linux
|
|
153
|
+
Secret Service (GNOME Keyring / KWallet), or Windows Credential Locker — never
|
|
154
|
+
a config file, the cache, or a command-line flag. The
|
|
155
|
+
`VULNCTL_NVD_API_KEY` environment variable, when set, takes precedence over
|
|
156
|
+
the saved key; use it in CI and anywhere without a keychain.
|
|
157
|
+
|
|
158
|
+
**First-run prompt.** The first time `enrich` or `explain` runs online with no
|
|
159
|
+
key configured, vulnctl offers to save one:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
No NVD API key found. Without one NVD allows about 7 lookups a minute;
|
|
163
|
+
a free key makes enrichment ~10x faster: https://nvd.nist.gov/developers/request-an-api-key
|
|
164
|
+
NVD API key (input hidden; Enter to skip):
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A key NVD rejects is asked for again (up to three tries); a key that can't be
|
|
168
|
+
verified because NVD is unreachable is saved anyway with a warning. Pressing
|
|
169
|
+
Enter skips, and vulnctl remembers that and never asks again
|
|
170
|
+
(`vulnctl auth nvd` still works any time). The marker is
|
|
171
|
+
`~/.config/vulnctl/nvd-key-prompt-dismissed` — delete it to see the prompt
|
|
172
|
+
again.
|
|
173
|
+
|
|
174
|
+
The prompt never appears where it could get in the way: when stdin or stderr
|
|
175
|
+
isn't a terminal (scripts, pipes, CI — or when `CI` is set), with `--offline`,
|
|
176
|
+
or with `--grype -` (stdin is the scan). It writes to stderr and reads from
|
|
177
|
+
the terminal, so `-f json > out.json` stays clean.
|
|
178
|
+
|
|
179
|
+
### Examples
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
vulnctl auth nvd # paste your key (hidden); verified, then saved
|
|
183
|
+
vulnctl auth status # NVD API key: …a1b2 from macOS Keyring
|
|
184
|
+
vulnctl auth remove nvd # forget the saved key
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Environment variables
|
|
188
|
+
|
|
189
|
+
| Variable | What it does |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `VULNCTL_NVD_API_KEY` | NVD API key; takes precedence over a key saved with `vulnctl auth nvd`. Use in CI. |
|
|
192
|
+
| `VULNCTL_GITHUB_TOKEN` | Optional GitHub token for advisory lookups (5,000 req/h instead of 60). |
|
|
193
|
+
| `XDG_CACHE_HOME` | Cache location: `$XDG_CACHE_HOME/vulnctl/cache.db` (default `~/.cache`). |
|
|
194
|
+
| `XDG_CONFIG_HOME` | Config location for the prompt-dismissed marker (default `~/.config`). |
|
|
195
|
+
| `CI` | When set, vulnctl never prompts. |
|
|
196
|
+
|
|
109
197
|
## Shell completion
|
|
110
198
|
|
|
111
199
|
Typer ships tab completion for commands, flags, and enum values
|