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.
Files changed (137) hide show
  1. {vulnctl-0.2.2 → vulnctl-0.3.0}/.github/workflows/release.yml +1 -1
  2. {vulnctl-0.2.2 → vulnctl-0.3.0}/CHANGELOG.md +47 -1
  3. {vulnctl-0.2.2 → vulnctl-0.3.0}/CLAUDE.md +4 -2
  4. {vulnctl-0.2.2 → vulnctl-0.3.0}/FRAMEWORK.md +1 -0
  5. {vulnctl-0.2.2 → vulnctl-0.3.0}/PKG-INFO +23 -6
  6. {vulnctl-0.2.2 → vulnctl-0.3.0}/README.md +20 -4
  7. {vulnctl-0.2.2 → vulnctl-0.3.0}/SPEC.md +1 -1
  8. {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/cli.md +88 -0
  9. vulnctl-0.3.0/examples/large-app.cdx.json +1134 -0
  10. vulnctl-0.3.0/examples/medium-app.cdx.json +385 -0
  11. {vulnctl-0.2.2 → vulnctl-0.3.0}/pyproject.toml +7 -2
  12. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/base.py +13 -9
  13. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/exploits.py +1 -1
  14. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/ghsa.py +10 -3
  15. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/nvd.py +55 -6
  16. vulnctl-0.3.0/src/vulnctl/auth.py +194 -0
  17. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/cli.py +2 -0
  18. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/cli_help.py +4 -0
  19. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/commands.py +5 -0
  20. vulnctl-0.3.0/src/vulnctl/credentials.py +144 -0
  21. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/models.py +3 -2
  22. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/pipeline.py +38 -20
  23. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ssvc/tree.py +2 -3
  24. vulnctl-0.3.0/tests/conftest.py +80 -0
  25. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_nvd.py +86 -1
  26. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapters_base.py +1 -8
  27. vulnctl-0.3.0/tests/test_auth.py +315 -0
  28. vulnctl-0.3.0/tests/test_credentials.py +137 -0
  29. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_pipeline.py +31 -0
  30. {vulnctl-0.2.2 → vulnctl-0.3.0}/uv.lock +243 -141
  31. vulnctl-0.2.2/.github/workflows/publish-testpypi.yml +0 -29
  32. vulnctl-0.2.2/.pre-commit-config.yaml +0 -27
  33. vulnctl-0.2.2/ROADMAP.md +0 -198
  34. vulnctl-0.2.2/docs/announcement.md +0 -46
  35. vulnctl-0.2.2/docs/case-study.md +0 -95
  36. vulnctl-0.2.2/docs/demo.tape +0 -43
  37. vulnctl-0.2.2/scripts/case_study_stats.py +0 -139
  38. vulnctl-0.2.2/tests/conftest.py +0 -39
  39. {vulnctl-0.2.2 → vulnctl-0.3.0}/.github/workflows/ci.yml +0 -0
  40. {vulnctl-0.2.2 → vulnctl-0.3.0}/.github/workflows/live-smoke.yml +0 -0
  41. {vulnctl-0.2.2 → vulnctl-0.3.0}/.gitignore +0 -0
  42. {vulnctl-0.2.2 → vulnctl-0.3.0}/.python-version +0 -0
  43. {vulnctl-0.2.2 → vulnctl-0.3.0}/LICENSE +0 -0
  44. {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/context.md +0 -0
  45. {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/exit-codes.md +0 -0
  46. {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/output.md +0 -0
  47. {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/releasing.md +0 -0
  48. {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/schema.json +0 -0
  49. {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/schema.md +0 -0
  50. {vulnctl-0.2.2 → vulnctl-0.3.0}/docs/trees.md +0 -0
  51. {vulnctl-0.2.2 → vulnctl-0.3.0}/examples/app.cdx.json +0 -0
  52. {vulnctl-0.2.2 → vulnctl-0.3.0}/examples/ci/vulnctl-gate.yml +0 -0
  53. {vulnctl-0.2.2 → vulnctl-0.3.0}/examples/context.yaml +0 -0
  54. {vulnctl-0.2.2 → vulnctl-0.3.0}/scripts/build_exploit_index.py +0 -0
  55. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/__init__.py +0 -0
  56. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/__init__.py +0 -0
  57. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/epss.py +0 -0
  58. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/kev.py +0 -0
  59. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/adapters/osv.py +0 -0
  60. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/cache.py +0 -0
  61. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/context.py +0 -0
  62. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/data/__init__.py +0 -0
  63. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/data/epss_snapshot.csv.gz +0 -0
  64. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/data/exploit_index.json.gz +0 -0
  65. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/data/kev_snapshot.json.gz +0 -0
  66. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ingest/__init__.py +0 -0
  67. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ingest/cve_list.py +0 -0
  68. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ingest/cyclonedx.py +0 -0
  69. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ingest/grype.py +0 -0
  70. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/__init__.py +0 -0
  71. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/explain.py +0 -0
  72. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/json_out.py +0 -0
  73. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/markdown.py +0 -0
  74. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/progress.py +0 -0
  75. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/render.py +0 -0
  76. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/sarif.py +0 -0
  77. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/output/table.py +0 -0
  78. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/py.typed +0 -0
  79. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ssvc/__init__.py +0 -0
  80. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ssvc/engine.py +0 -0
  81. {vulnctl-0.2.2 → vulnctl-0.3.0}/src/vulnctl/ssvc/trees/cisa-deployer-v1.yaml +0 -0
  82. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/epss/batch.json +0 -0
  83. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/epss/malformed.json +0 -0
  84. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/epss/missing.json +0 -0
  85. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/advisory-by-cve.json +0 -0
  86. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/advisory-by-ghsa-id.json +0 -0
  87. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/malformed.json +0 -0
  88. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/not-found-empty-list.json +0 -0
  89. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/ghsa/rate-limited.json +0 -0
  90. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/golden/enrich.json +0 -0
  91. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/golden/enrich.md +0 -0
  92. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/grype/duplicate-layers.json +0 -0
  93. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/grype/ghsa-only.json +0 -0
  94. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/grype/npm-app.json +0 -0
  95. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/kev/catalog.json +0 -0
  96. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/cve-2021-44228.json +0 -0
  97. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/multiple-cvss.json +0 -0
  98. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/not-found.json +0 -0
  99. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/rejected.json +0 -0
  100. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/nvd/v2-only.json +0 -0
  101. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/cve-2021-44228.json +0 -0
  102. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/malformed.json +0 -0
  103. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/not-found.json +0 -0
  104. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/querybatch-npm-app.json +0 -0
  105. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/querybatch.json +0 -0
  106. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-cve-record.json +0 -0
  107. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-ghsa-no-cve.json +0 -0
  108. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-ghsa-with-cve.json +0 -0
  109. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-go.json +0 -0
  110. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/osv/vuln-pypi.json +0 -0
  111. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/sarif/sarif-schema-2.1.0.json +0 -0
  112. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/sbom/npm-app-1.6.cdx.json +0 -0
  113. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/sbom/npm-app.cdx.json +0 -0
  114. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/sbom/py-app.cdx.json +0 -0
  115. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/fixtures/trees/toy.yaml +0 -0
  116. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/live/test_live_smoke.py +0 -0
  117. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_epss.py +0 -0
  118. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_exploits.py +0 -0
  119. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_ghsa.py +0 -0
  120. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_kev.py +0 -0
  121. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_adapter_osv.py +0 -0
  122. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_cache.py +0 -0
  123. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_cli.py +0 -0
  124. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_context.py +0 -0
  125. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ingest_cve_list.py +0 -0
  126. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ingest_cyclonedx.py +0 -0
  127. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ingest_grype.py +0 -0
  128. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_models.py +0 -0
  129. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_explain.py +0 -0
  130. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_json.py +0 -0
  131. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_markdown.py +0 -0
  132. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_progress.py +0 -0
  133. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_sarif.py +0 -0
  134. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_output_table.py +0 -0
  135. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ssvc_cisa_tree.py +0 -0
  136. {vulnctl-0.2.2 → vulnctl-0.3.0}/tests/test_ssvc_engine.py +0 -0
  137. {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@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
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.2.2...HEAD
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, spdx.py, grype.py, trivy.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 only. Scrub any recorded fixtures of keys before committing.
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: vulnctl
3
- Version: 0.2.2
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; an NVD
67
- API key (optional, for higher rate limits) is read from the
68
- `VULNCTL_NVD_API_KEY` environment variable only.
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; an NVD
41
- API key (optional, for higher rate limits) is read from the
42
- `VULNCTL_NVD_API_KEY` environment variable only.
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