vulnctl 0.1.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.1.0/.github/workflows/ci.yml +36 -0
- vulnctl-0.1.0/.github/workflows/live-smoke.yml +59 -0
- vulnctl-0.1.0/.github/workflows/publish-testpypi.yml +29 -0
- vulnctl-0.1.0/.github/workflows/release.yml +237 -0
- vulnctl-0.1.0/.gitignore +22 -0
- vulnctl-0.1.0/.pre-commit-config.yaml +27 -0
- vulnctl-0.1.0/.python-version +1 -0
- vulnctl-0.1.0/CHANGELOG.md +61 -0
- vulnctl-0.1.0/CLAUDE.md +79 -0
- vulnctl-0.1.0/FRAMEWORK.md +197 -0
- vulnctl-0.1.0/LICENSE +202 -0
- vulnctl-0.1.0/PKG-INFO +175 -0
- vulnctl-0.1.0/README.md +149 -0
- vulnctl-0.1.0/ROADMAP.md +198 -0
- vulnctl-0.1.0/SPEC.md +113 -0
- vulnctl-0.1.0/docs/announcement.md +46 -0
- vulnctl-0.1.0/docs/case-study.md +95 -0
- vulnctl-0.1.0/docs/context.md +126 -0
- vulnctl-0.1.0/docs/demo.tape +43 -0
- vulnctl-0.1.0/docs/exit-codes.md +33 -0
- vulnctl-0.1.0/docs/releasing.md +164 -0
- vulnctl-0.1.0/docs/schema.json +614 -0
- vulnctl-0.1.0/docs/schema.md +78 -0
- vulnctl-0.1.0/docs/trees.md +135 -0
- vulnctl-0.1.0/examples/app.cdx.json +1 -0
- vulnctl-0.1.0/examples/ci/vulnctl-gate.yml +48 -0
- vulnctl-0.1.0/examples/context.yaml +33 -0
- vulnctl-0.1.0/pyproject.toml +84 -0
- vulnctl-0.1.0/scripts/build_exploit_index.py +128 -0
- vulnctl-0.1.0/scripts/case_study_stats.py +139 -0
- vulnctl-0.1.0/src/vulnctl/__init__.py +5 -0
- vulnctl-0.1.0/src/vulnctl/adapters/__init__.py +5 -0
- vulnctl-0.1.0/src/vulnctl/adapters/base.py +150 -0
- vulnctl-0.1.0/src/vulnctl/adapters/epss.py +158 -0
- vulnctl-0.1.0/src/vulnctl/adapters/exploits.py +83 -0
- vulnctl-0.1.0/src/vulnctl/adapters/ghsa.py +220 -0
- vulnctl-0.1.0/src/vulnctl/adapters/kev.py +139 -0
- vulnctl-0.1.0/src/vulnctl/adapters/nvd.py +218 -0
- vulnctl-0.1.0/src/vulnctl/adapters/osv.py +437 -0
- vulnctl-0.1.0/src/vulnctl/cache.py +133 -0
- vulnctl-0.1.0/src/vulnctl/cli.py +160 -0
- vulnctl-0.1.0/src/vulnctl/context.py +140 -0
- vulnctl-0.1.0/src/vulnctl/data/__init__.py +9 -0
- vulnctl-0.1.0/src/vulnctl/data/epss_snapshot.csv.gz +0 -0
- vulnctl-0.1.0/src/vulnctl/data/exploit_index.json.gz +0 -0
- vulnctl-0.1.0/src/vulnctl/data/kev_snapshot.json.gz +0 -0
- vulnctl-0.1.0/src/vulnctl/ingest/__init__.py +5 -0
- vulnctl-0.1.0/src/vulnctl/ingest/cve_list.py +33 -0
- vulnctl-0.1.0/src/vulnctl/ingest/cyclonedx.py +135 -0
- vulnctl-0.1.0/src/vulnctl/ingest/grype.py +166 -0
- vulnctl-0.1.0/src/vulnctl/models.py +263 -0
- vulnctl-0.1.0/src/vulnctl/output/__init__.py +32 -0
- vulnctl-0.1.0/src/vulnctl/output/json_out.py +52 -0
- vulnctl-0.1.0/src/vulnctl/output/markdown.py +189 -0
- vulnctl-0.1.0/src/vulnctl/output/render.py +59 -0
- vulnctl-0.1.0/src/vulnctl/output/sarif.py +174 -0
- vulnctl-0.1.0/src/vulnctl/output/table.py +167 -0
- vulnctl-0.1.0/src/vulnctl/pipeline.py +334 -0
- vulnctl-0.1.0/src/vulnctl/py.typed +0 -0
- vulnctl-0.1.0/src/vulnctl/ssvc/__init__.py +1 -0
- vulnctl-0.1.0/src/vulnctl/ssvc/engine.py +100 -0
- vulnctl-0.1.0/src/vulnctl/ssvc/tree.py +341 -0
- vulnctl-0.1.0/src/vulnctl/ssvc/trees/cisa-deployer-v1.yaml +164 -0
- vulnctl-0.1.0/tests/conftest.py +39 -0
- vulnctl-0.1.0/tests/fixtures/epss/batch.json +1 -0
- vulnctl-0.1.0/tests/fixtures/epss/malformed.json +28 -0
- vulnctl-0.1.0/tests/fixtures/epss/missing.json +1 -0
- vulnctl-0.1.0/tests/fixtures/ghsa/advisory-by-cve.json +231 -0
- vulnctl-0.1.0/tests/fixtures/ghsa/advisory-by-ghsa-id.json +74 -0
- vulnctl-0.1.0/tests/fixtures/ghsa/malformed.json +1 -0
- vulnctl-0.1.0/tests/fixtures/ghsa/not-found-empty-list.json +3 -0
- vulnctl-0.1.0/tests/fixtures/ghsa/rate-limited.json +4 -0
- vulnctl-0.1.0/tests/fixtures/golden/enrich.json +252 -0
- vulnctl-0.1.0/tests/fixtures/golden/enrich.md +35 -0
- vulnctl-0.1.0/tests/fixtures/grype/duplicate-layers.json +394 -0
- vulnctl-0.1.0/tests/fixtures/grype/npm-app.json +1 -0
- vulnctl-0.1.0/tests/fixtures/kev/catalog.json +70 -0
- vulnctl-0.1.0/tests/fixtures/nvd/cve-2021-44228.json +3373 -0
- vulnctl-0.1.0/tests/fixtures/nvd/multiple-cvss.json +1029 -0
- vulnctl-0.1.0/tests/fixtures/nvd/not-found.json +9 -0
- vulnctl-0.1.0/tests/fixtures/nvd/rejected.json +28 -0
- vulnctl-0.1.0/tests/fixtures/nvd/v2-only.json +145 -0
- vulnctl-0.1.0/tests/fixtures/osv/cve-2021-44228.json +1 -0
- vulnctl-0.1.0/tests/fixtures/osv/malformed.json +1 -0
- vulnctl-0.1.0/tests/fixtures/osv/not-found.json +1 -0
- vulnctl-0.1.0/tests/fixtures/osv/querybatch-npm-app.json +14 -0
- vulnctl-0.1.0/tests/fixtures/osv/querybatch.json +37 -0
- vulnctl-0.1.0/tests/fixtures/osv/vuln-cve-record.json +1 -0
- vulnctl-0.1.0/tests/fixtures/osv/vuln-ghsa-no-cve.json +1 -0
- vulnctl-0.1.0/tests/fixtures/osv/vuln-ghsa-with-cve.json +1 -0
- vulnctl-0.1.0/tests/fixtures/osv/vuln-go.json +1 -0
- vulnctl-0.1.0/tests/fixtures/osv/vuln-pypi.json +1 -0
- vulnctl-0.1.0/tests/fixtures/sarif/sarif-schema-2.1.0.json +3389 -0
- vulnctl-0.1.0/tests/fixtures/sbom/npm-app-1.6.cdx.json +1 -0
- vulnctl-0.1.0/tests/fixtures/sbom/npm-app.cdx.json +1 -0
- vulnctl-0.1.0/tests/fixtures/sbom/py-app.cdx.json +1 -0
- vulnctl-0.1.0/tests/fixtures/trees/toy.yaml +26 -0
- vulnctl-0.1.0/tests/live/test_live_smoke.py +116 -0
- vulnctl-0.1.0/tests/test_adapter_epss.py +173 -0
- vulnctl-0.1.0/tests/test_adapter_exploits.py +112 -0
- vulnctl-0.1.0/tests/test_adapter_ghsa.py +270 -0
- vulnctl-0.1.0/tests/test_adapter_kev.py +125 -0
- vulnctl-0.1.0/tests/test_adapter_nvd.py +279 -0
- vulnctl-0.1.0/tests/test_adapter_osv.py +466 -0
- vulnctl-0.1.0/tests/test_adapters_base.py +99 -0
- vulnctl-0.1.0/tests/test_cache.py +112 -0
- vulnctl-0.1.0/tests/test_cli.py +223 -0
- vulnctl-0.1.0/tests/test_context.py +102 -0
- vulnctl-0.1.0/tests/test_ingest_cve_list.py +30 -0
- vulnctl-0.1.0/tests/test_ingest_cyclonedx.py +204 -0
- vulnctl-0.1.0/tests/test_ingest_grype.py +176 -0
- vulnctl-0.1.0/tests/test_models.py +140 -0
- vulnctl-0.1.0/tests/test_output_json.py +100 -0
- vulnctl-0.1.0/tests/test_output_markdown.py +98 -0
- vulnctl-0.1.0/tests/test_output_sarif.py +231 -0
- vulnctl-0.1.0/tests/test_output_table.py +242 -0
- vulnctl-0.1.0/tests/test_pipeline.py +383 -0
- vulnctl-0.1.0/tests/test_ssvc_cisa_tree.py +177 -0
- vulnctl-0.1.0/tests/test_ssvc_engine.py +186 -0
- vulnctl-0.1.0/tests/test_ssvc_tree.py +393 -0
- vulnctl-0.1.0/uv.lock +1108 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
checks:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout
|
|
16
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
20
|
+
with:
|
|
21
|
+
enable-cache: true
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: uv sync --locked
|
|
25
|
+
|
|
26
|
+
- name: Ruff lint
|
|
27
|
+
run: uv run ruff check .
|
|
28
|
+
|
|
29
|
+
- name: Ruff format
|
|
30
|
+
run: uv run ruff format --check .
|
|
31
|
+
|
|
32
|
+
- name: Mypy
|
|
33
|
+
run: uv run mypy src/
|
|
34
|
+
|
|
35
|
+
- name: Pytest (with 100% branch-coverage gate on ssvc/)
|
|
36
|
+
run: uv run pytest --cov=vulnctl.ssvc --cov-branch --cov-report=term-missing --cov-fail-under=100
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Weekly live-smoke: exercise every adapter against real upstream APIs to catch
|
|
2
|
+
# schema drift before it silently degrades production runs. This is the ONLY
|
|
3
|
+
# workflow that hits the network — PR/push CI stays fixture-only. On failure it
|
|
4
|
+
# opens a tracking issue. Never triggered by pull_request.
|
|
5
|
+
name: live-smoke
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: "17 6 * * 1" # Mondays 06:17 UTC
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
issues: write # open a tracking issue when an upstream source drifts/breaks
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
live-smoke:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout
|
|
21
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
25
|
+
|
|
26
|
+
- name: Sync dependencies
|
|
27
|
+
run: uv sync --locked
|
|
28
|
+
|
|
29
|
+
- name: Live adapter smoke (real APIs)
|
|
30
|
+
env:
|
|
31
|
+
VULNCTL_LIVE: "1"
|
|
32
|
+
# Optional secrets: raise NVD/GitHub rate limits. Anonymous also works.
|
|
33
|
+
VULNCTL_NVD_API_KEY: ${{ secrets.VULNCTL_NVD_API_KEY }}
|
|
34
|
+
VULNCTL_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
35
|
+
run: uv run pytest tests/live -v
|
|
36
|
+
|
|
37
|
+
- name: Open an issue on failure
|
|
38
|
+
if: failure()
|
|
39
|
+
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
|
|
40
|
+
with:
|
|
41
|
+
script: |
|
|
42
|
+
const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
|
43
|
+
await github.rest.issues.create({
|
|
44
|
+
owner: context.repo.owner,
|
|
45
|
+
repo: context.repo.repo,
|
|
46
|
+
title: `live-smoke failed — upstream drift or outage (${new Date().toISOString().slice(0, 10)})`,
|
|
47
|
+
body: [
|
|
48
|
+
`The weekly [live-smoke run](${run}) failed.`,
|
|
49
|
+
'',
|
|
50
|
+
'An adapter returned no data for a known-good CVE. Because every adapter',
|
|
51
|
+
'parses upstream responses through strict Pydantic models, this usually',
|
|
52
|
+
'means an upstream API changed its schema (the adapter degraded to',
|
|
53
|
+
'`Unavailable`) — or the source is temporarily down.',
|
|
54
|
+
'',
|
|
55
|
+
'Check the run logs to see which source failed, then update the adapter',
|
|
56
|
+
'and its recorded fixtures if the schema changed.',
|
|
57
|
+
].join('\n'),
|
|
58
|
+
labels: ['live-smoke', 'upstream-drift'],
|
|
59
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish to TestPyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: testpypi
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
id-token: write # OIDC trusted publishing — no stored token
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout
|
|
18
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
22
|
+
|
|
23
|
+
- name: Build sdist and wheel
|
|
24
|
+
run: uv build
|
|
25
|
+
|
|
26
|
+
- name: Publish to TestPyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
28
|
+
with:
|
|
29
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Cut a signed release of vulnctl.
|
|
4
|
+
#
|
|
5
|
+
# Pipeline (in order, each stage gates the next):
|
|
6
|
+
# build → build sdist+wheel (uv), install the wheel into a clean env,
|
|
7
|
+
# SBOM that runtime closure (Syft, CycloneDX JSON), scan it (Grype),
|
|
8
|
+
# and gate: (1) no critical-with-fix vuln, (2) dogfood vulnctl's own
|
|
9
|
+
# --fail-on act against the Grype report.
|
|
10
|
+
# sign → cosign keyless (OIDC) signatures for the wheel, sdist, and SBOM.
|
|
11
|
+
# publish → PyPI via trusted publishing (OIDC, no stored token).
|
|
12
|
+
# release → GitHub Release with wheel, sdist, SBOM, and signatures attached.
|
|
13
|
+
#
|
|
14
|
+
# Triggers:
|
|
15
|
+
# - push of a `v*` tag → the full pipeline, including publish + GitHub Release.
|
|
16
|
+
# - workflow_dispatch → DRY RUN: build, SBOM, scan, gates, and signing all
|
|
17
|
+
# run (so the release path is fully exercised), but publish + release are
|
|
18
|
+
# skipped. Every produced artifact is downloadable from the run.
|
|
19
|
+
#
|
|
20
|
+
# Security posture: every action is pinned to a full commit SHA; permissions are
|
|
21
|
+
# least-privilege per job; `id-token: write` (OIDC) is granted only to the jobs
|
|
22
|
+
# that actually sign or publish.
|
|
23
|
+
|
|
24
|
+
on:
|
|
25
|
+
push:
|
|
26
|
+
tags: ["v*"]
|
|
27
|
+
workflow_dispatch:
|
|
28
|
+
|
|
29
|
+
# Deny everything by default; each job opts in to exactly what it needs.
|
|
30
|
+
permissions: {}
|
|
31
|
+
|
|
32
|
+
concurrency:
|
|
33
|
+
group: release-${{ github.ref }}
|
|
34
|
+
cancel-in-progress: false
|
|
35
|
+
|
|
36
|
+
jobs:
|
|
37
|
+
build:
|
|
38
|
+
name: Build, SBOM, scan & gate
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
permissions:
|
|
41
|
+
contents: read
|
|
42
|
+
steps:
|
|
43
|
+
- name: Checkout
|
|
44
|
+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
45
|
+
|
|
46
|
+
- name: Install uv
|
|
47
|
+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
48
|
+
|
|
49
|
+
- name: Verify tag matches package version
|
|
50
|
+
if: github.event_name == 'push'
|
|
51
|
+
run: |
|
|
52
|
+
set -euo pipefail
|
|
53
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
54
|
+
pkg="$(grep -m1 -E '^version\s*=' pyproject.toml | sed -E 's/.*=\s*"([^"]+)".*/\1/')"
|
|
55
|
+
echo "tag=$tag pyproject=$pkg"
|
|
56
|
+
if [ "$tag" != "$pkg" ]; then
|
|
57
|
+
echo "::error::tag v$tag does not match pyproject version $pkg — bump pyproject first"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
- name: Build sdist and wheel
|
|
62
|
+
run: uv build
|
|
63
|
+
|
|
64
|
+
- name: Resolve built wheel path
|
|
65
|
+
run: echo "WHEEL=$(ls dist/*.whl)" >> "$GITHUB_ENV"
|
|
66
|
+
|
|
67
|
+
- name: Install the release artifact into a clean environment
|
|
68
|
+
# Installs exactly what a `pipx install vulnctl` user receives, so the
|
|
69
|
+
# SBOM and scan below describe the real shipped runtime closure.
|
|
70
|
+
run: |
|
|
71
|
+
set -euo pipefail
|
|
72
|
+
uv venv .relenv
|
|
73
|
+
uv pip install --python .relenv "$WHEEL"
|
|
74
|
+
|
|
75
|
+
- name: Generate CycloneDX SBOM of the runtime closure (Syft)
|
|
76
|
+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
|
|
77
|
+
with:
|
|
78
|
+
path: .relenv
|
|
79
|
+
format: cyclonedx-json
|
|
80
|
+
output-file: sbom.cdx.json
|
|
81
|
+
upload-artifact: false
|
|
82
|
+
|
|
83
|
+
- name: Install Grype
|
|
84
|
+
id: grype
|
|
85
|
+
uses: anchore/scan-action/download-grype@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
|
|
86
|
+
|
|
87
|
+
- name: Scan the SBOM (Grype → JSON)
|
|
88
|
+
run: ${{ steps.grype.outputs.cmd }} sbom:sbom.cdx.json -o json=grype.json
|
|
89
|
+
|
|
90
|
+
- name: "Gate 1 — no critical vulnerability that already has a fix"
|
|
91
|
+
run: |
|
|
92
|
+
set -euo pipefail
|
|
93
|
+
count="$(jq '[.matches[]
|
|
94
|
+
| select(.vulnerability.severity == "Critical"
|
|
95
|
+
and .vulnerability.fix.state == "fixed")] | length' grype.json)"
|
|
96
|
+
if [ "$count" -gt 0 ]; then
|
|
97
|
+
echo "::error::$count critical vulnerability(ies) with an available fix — release blocked"
|
|
98
|
+
jq -r '.matches[]
|
|
99
|
+
| select(.vulnerability.severity == "Critical" and .vulnerability.fix.state == "fixed")
|
|
100
|
+
| " - \(.artifact.name) \(.artifact.version): \(.vulnerability.id) → fix \(.vulnerability.fix.versions | join(", "))"' grype.json
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
echo "OK: no critical-with-fix findings."
|
|
104
|
+
|
|
105
|
+
- name: "Gate 2 — dogfood vulnctl on its own Grype report (fail on ACT)"
|
|
106
|
+
env:
|
|
107
|
+
# Optional: raises NVD rate limits. vulnctl fails open if unset/down.
|
|
108
|
+
VULNCTL_NVD_API_KEY: ${{ secrets.VULNCTL_NVD_API_KEY }}
|
|
109
|
+
run: |
|
|
110
|
+
# Run the exact wheel we're about to ship against its own scan. A
|
|
111
|
+
# KEV-listed dependency resolves to exploitation=active → ACT → exit 2.
|
|
112
|
+
./.relenv/bin/vulnctl enrich --grype grype.json --fail-on act
|
|
113
|
+
|
|
114
|
+
- name: Upload distributions
|
|
115
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
116
|
+
with:
|
|
117
|
+
name: dist
|
|
118
|
+
path: dist/
|
|
119
|
+
if-no-files-found: error
|
|
120
|
+
|
|
121
|
+
- name: Upload SBOM and scan report
|
|
122
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
123
|
+
with:
|
|
124
|
+
name: reports
|
|
125
|
+
path: |
|
|
126
|
+
sbom.cdx.json
|
|
127
|
+
grype.json
|
|
128
|
+
if-no-files-found: error
|
|
129
|
+
|
|
130
|
+
sign:
|
|
131
|
+
name: Sign artifacts (cosign keyless)
|
|
132
|
+
runs-on: ubuntu-latest
|
|
133
|
+
needs: build
|
|
134
|
+
permissions:
|
|
135
|
+
contents: read
|
|
136
|
+
id-token: write # OIDC identity for keyless signing (Fulcio); no private key
|
|
137
|
+
steps:
|
|
138
|
+
- name: Download distributions
|
|
139
|
+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
|
140
|
+
with:
|
|
141
|
+
name: dist
|
|
142
|
+
path: dist
|
|
143
|
+
|
|
144
|
+
- name: Download SBOM
|
|
145
|
+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
|
146
|
+
with:
|
|
147
|
+
name: reports
|
|
148
|
+
path: .
|
|
149
|
+
|
|
150
|
+
- name: Install cosign
|
|
151
|
+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
|
152
|
+
with:
|
|
153
|
+
cosign-release: "v3.0.6"
|
|
154
|
+
|
|
155
|
+
- name: Sign wheel, sdist, and SBOM (keyless)
|
|
156
|
+
run: |
|
|
157
|
+
set -euo pipefail
|
|
158
|
+
mkdir -p sigs
|
|
159
|
+
# cosign v3 defaults to the Sigstore "new bundle format": one bundle
|
|
160
|
+
# file per artifact carrying the signature, certificate, and Rekor
|
|
161
|
+
# entry. (The old --output-signature/--output-certificate flags are
|
|
162
|
+
# deprecated and ignored under this format.)
|
|
163
|
+
for f in dist/*.whl dist/*.tar.gz sbom.cdx.json; do
|
|
164
|
+
base="$(basename "$f")"
|
|
165
|
+
echo "signing $f"
|
|
166
|
+
cosign sign-blob --yes \
|
|
167
|
+
--bundle "sigs/$base.sigstore.json" \
|
|
168
|
+
"$f"
|
|
169
|
+
done
|
|
170
|
+
|
|
171
|
+
- name: Upload signatures
|
|
172
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
173
|
+
with:
|
|
174
|
+
name: signatures
|
|
175
|
+
path: sigs/
|
|
176
|
+
if-no-files-found: error
|
|
177
|
+
|
|
178
|
+
publish:
|
|
179
|
+
name: Publish to PyPI (trusted publishing)
|
|
180
|
+
runs-on: ubuntu-latest
|
|
181
|
+
needs: [build, sign]
|
|
182
|
+
if: github.event_name == 'push'
|
|
183
|
+
environment:
|
|
184
|
+
name: pypi
|
|
185
|
+
url: https://pypi.org/p/vulnctl
|
|
186
|
+
permissions:
|
|
187
|
+
id-token: write # OIDC for PyPI trusted publishing — no stored API token
|
|
188
|
+
steps:
|
|
189
|
+
- name: Download distributions
|
|
190
|
+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
|
191
|
+
with:
|
|
192
|
+
name: dist
|
|
193
|
+
path: dist
|
|
194
|
+
|
|
195
|
+
- name: Publish to PyPI
|
|
196
|
+
# Uploads only dist/*.whl and dist/*.tar.gz (signatures/SBOM live in
|
|
197
|
+
# separate artifacts). Generates PEP 740 attestations via the same OIDC
|
|
198
|
+
# identity by default.
|
|
199
|
+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
200
|
+
|
|
201
|
+
release:
|
|
202
|
+
name: GitHub Release
|
|
203
|
+
runs-on: ubuntu-latest
|
|
204
|
+
needs: [build, sign, publish]
|
|
205
|
+
if: github.event_name == 'push'
|
|
206
|
+
permissions:
|
|
207
|
+
contents: write # create the release and upload assets
|
|
208
|
+
steps:
|
|
209
|
+
- name: Download distributions
|
|
210
|
+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
|
211
|
+
with:
|
|
212
|
+
name: dist
|
|
213
|
+
path: dist
|
|
214
|
+
|
|
215
|
+
- name: Download SBOM and scan report
|
|
216
|
+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
|
217
|
+
with:
|
|
218
|
+
name: reports
|
|
219
|
+
path: .
|
|
220
|
+
|
|
221
|
+
- name: Download signatures
|
|
222
|
+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
|
223
|
+
with:
|
|
224
|
+
name: signatures
|
|
225
|
+
path: signatures
|
|
226
|
+
|
|
227
|
+
- name: Create GitHub Release
|
|
228
|
+
uses: softprops/action-gh-release@2bb465e97f322d3cb2a965294d483e0d26a67aa9 # v3.0.1
|
|
229
|
+
with:
|
|
230
|
+
tag_name: ${{ github.ref_name }}
|
|
231
|
+
generate_release_notes: true
|
|
232
|
+
fail_on_unmatched_files: true
|
|
233
|
+
files: |
|
|
234
|
+
dist/*.whl
|
|
235
|
+
dist/*.tar.gz
|
|
236
|
+
sbom.cdx.json
|
|
237
|
+
signatures/*
|
vulnctl-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
|
|
8
|
+
# Virtual environments / uv
|
|
9
|
+
.venv/
|
|
10
|
+
.python-version.local
|
|
11
|
+
|
|
12
|
+
# Tooling caches
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# OS / editor
|
|
20
|
+
.DS_Store
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Runs the same four checks CI runs, via the project venv (uv), so hook and CI
|
|
2
|
+
# tool versions can never drift apart.
|
|
3
|
+
repos:
|
|
4
|
+
- repo: local
|
|
5
|
+
hooks:
|
|
6
|
+
- id: ruff-check
|
|
7
|
+
name: ruff check
|
|
8
|
+
entry: uv run ruff check --force-exclude
|
|
9
|
+
language: system
|
|
10
|
+
types_or: [python, pyi]
|
|
11
|
+
- id: ruff-format
|
|
12
|
+
name: ruff format
|
|
13
|
+
entry: uv run ruff format --check --force-exclude
|
|
14
|
+
language: system
|
|
15
|
+
types_or: [python, pyi]
|
|
16
|
+
- id: mypy
|
|
17
|
+
name: mypy
|
|
18
|
+
entry: uv run mypy src/
|
|
19
|
+
language: system
|
|
20
|
+
pass_filenames: false
|
|
21
|
+
types_or: [python, pyi]
|
|
22
|
+
- id: pytest
|
|
23
|
+
name: pytest
|
|
24
|
+
entry: uv run pytest
|
|
25
|
+
language: system
|
|
26
|
+
pass_filenames: false
|
|
27
|
+
always_run: true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to vulnctl are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-07-07
|
|
11
|
+
|
|
12
|
+
First public release. `vulnctl` turns CVE lists, SBOMs, and scanner output into
|
|
13
|
+
auditable, SSVC-based remediation verdicts — each with the full decision path
|
|
14
|
+
that produced it.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`enrich` command** accepting three input modes (exactly one per run): one or
|
|
19
|
+
more CVE IDs, a CycloneDX 1.4–1.6 SBOM (`--sbom`, components resolved to CVEs
|
|
20
|
+
via OSV), or Grype JSON (`--grype <file>` or `-` for stdin).
|
|
21
|
+
- **Fused enrichment** from six public intelligence sources: FIRST EPSS
|
|
22
|
+
(exploit probability + percentile), CISA KEV (known-exploited + ransomware
|
|
23
|
+
flag), NVD (CVSS vector/score, CWE), OSV and GHSA (affected/fixed versions,
|
|
24
|
+
advisories), and exploit presence (Exploit-DB, Metasploit, nuclei).
|
|
25
|
+
- **SSVC decision engine**: a pure, deterministic tree-walker bundling the
|
|
26
|
+
CISA-style deployer tree `cisa-deployer-v1`. Every verdict carries a full
|
|
27
|
+
`DecisionPath` — each node visited, its value, and the source that supplied
|
|
28
|
+
it — and flags when a degraded input fell back to a tree default. Bring your
|
|
29
|
+
own tree with `--tree`.
|
|
30
|
+
- **Organizational context** via `--context context.yaml`: `exposure`,
|
|
31
|
+
`mission_impact`, `asset_tier`, and per-decision-point `overrides`, with
|
|
32
|
+
conservative defaults when absent and hard errors on unknown keys.
|
|
33
|
+
- **Offline mode** (`--offline`): runs from cached data and bundled EPSS/KEV/
|
|
34
|
+
exploit snapshots with zero network access.
|
|
35
|
+
- **Output formats**: rich terminal table (with `--show-path`), machine-readable
|
|
36
|
+
`--format json` (versioned, schema-documented), `--format sarif` (SARIF 2.1.0
|
|
37
|
+
for GitHub code scanning), and `--format md` (stakeholder report).
|
|
38
|
+
- **CI gating**: `--fail-on track|track*|attend|act` exits `2` when any finding
|
|
39
|
+
meets or exceeds the threshold; output is written before the gate is applied.
|
|
40
|
+
- **Response cache**: SQLite with per-source TTLs; `cache stats` and
|
|
41
|
+
`cache purge` subcommands.
|
|
42
|
+
- **`--version`** sourced from installed package metadata (single source of
|
|
43
|
+
truth in `pyproject.toml`).
|
|
44
|
+
- **Documentation**: README with quickstart, plus `docs/` references for the
|
|
45
|
+
context file, tree format, JSON schema, exit codes, and the release runbook;
|
|
46
|
+
a `vhs` demo tape; and an example CI gate under `examples/ci/`.
|
|
47
|
+
|
|
48
|
+
### Security
|
|
49
|
+
|
|
50
|
+
- All GitHub Actions pinned by full commit SHA; least-privilege token scopes per
|
|
51
|
+
workflow.
|
|
52
|
+
- Strict Pydantic validation of every external JSON payload before it becomes a
|
|
53
|
+
model; response- and file-size bounds on all inputs; no `eval`, no `pickle` of
|
|
54
|
+
untrusted data, no shelling out to parse files.
|
|
55
|
+
- Signed release pipeline: build with `uv`, generate a CycloneDX SBOM (Syft),
|
|
56
|
+
scan the artifact (Grype, dogfooded through vulnctl's own `--fail-on` gate),
|
|
57
|
+
sign with keyless cosign (OIDC), and publish to PyPI via trusted publishing
|
|
58
|
+
(no stored token).
|
|
59
|
+
|
|
60
|
+
[Unreleased]: https://github.com/NokiGuard/vulnctl/compare/v0.1.0...HEAD
|
|
61
|
+
[0.1.0]: https://github.com/NokiGuard/vulnctl/releases/tag/v0.1.0
|
vulnctl-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# CLAUDE.md — vulnctl
|
|
2
|
+
|
|
3
|
+
## What this project is
|
|
4
|
+
|
|
5
|
+
`vulnctl` is a Python CLI that ingests CVE identifiers, SBOMs (CycloneDX/SPDX), or scanner output (Grype/Trivy JSON), enriches each finding from multiple threat-intelligence sources (EPSS, CISA KEV, NVD, OSV, GHSA, exploit-presence feeds), evaluates it against a declarative SSVC decision tree with user-supplied organizational context, and emits a prioritized, **auditable** ranking. The differentiator is that every verdict ships with its full decision path — not just a score.
|
|
6
|
+
|
|
7
|
+
Read `SPEC.md` for the product spec and `FRAMEWORK.md` for architecture before making structural changes.
|
|
8
|
+
|
|
9
|
+
## Tech stack & conventions
|
|
10
|
+
|
|
11
|
+
- **Python 3.11+**, packaged with `pyproject.toml` (hatchling or setuptools), managed with `uv`
|
|
12
|
+
- **CLI:** Typer. Entry point: `vulnctl` console script
|
|
13
|
+
- **HTTP:** `httpx` with async batching for source adapters; respect per-source rate limits
|
|
14
|
+
- **Models:** Pydantic v2 for ALL data structures crossing module boundaries. No bare dicts across interfaces.
|
|
15
|
+
- **Cache:** SQLite via `sqlite3` stdlib (no ORM), single file at `~/.cache/vulnctl/cache.db`, per-source TTLs
|
|
16
|
+
- **Terminal output:** `rich` tables
|
|
17
|
+
- **Config/trees:** YAML via `ruamel.yaml` (round-trip safe, preserves comments in user files)
|
|
18
|
+
- **Tests:** pytest + pytest-asyncio; adapters tested against recorded fixtures (JSON files in `tests/fixtures/`), never live APIs in CI
|
|
19
|
+
- **Lint/format:** ruff (lint + format), mypy --strict on `src/`
|
|
20
|
+
- **Line length 100. Google-style docstrings.**
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv sync # install deps
|
|
26
|
+
uv run pytest # run tests
|
|
27
|
+
uv run pytest -k adapter # adapter tests only
|
|
28
|
+
uv run ruff check --fix . # lint
|
|
29
|
+
uv run ruff format . # format
|
|
30
|
+
uv run mypy src/ # type check
|
|
31
|
+
uv run vulnctl --help # smoke-test the CLI
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
All four (pytest, ruff check, ruff format, mypy) must pass before any commit.
|
|
35
|
+
|
|
36
|
+
## Repository layout
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
src/vulnctl/
|
|
40
|
+
├── cli.py # Typer app; thin — no business logic here
|
|
41
|
+
├── models.py # Pydantic: Finding, Enrichment, Verdict, DecisionPath
|
|
42
|
+
├── ingest/ # cve_list.py, cyclonedx.py, spdx.py, grype.py, trivy.py
|
|
43
|
+
├── adapters/ # one module per intel source; all implement SourceAdapter ABC
|
|
44
|
+
│ ├── base.py # SourceAdapter ABC + registry
|
|
45
|
+
│ ├── epss.py, kev.py, nvd.py, osv.py, ghsa.py, exploits.py
|
|
46
|
+
├── cache.py # SQLite cache: get/set with TTL, per-source namespacing
|
|
47
|
+
├── ssvc/ # engine.py (tree walker), trees/ (bundled YAML trees)
|
|
48
|
+
├── context.py # org context loading + validation (context.yaml)
|
|
49
|
+
├── output/ # table.py, json_out.py, sarif.py, markdown.py
|
|
50
|
+
└── __init__.py
|
|
51
|
+
tests/
|
|
52
|
+
├── fixtures/ # recorded API responses, sample SBOMs, scanner JSON
|
|
53
|
+
└── ...mirrors src layout
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Architecture rules (do not violate without discussion)
|
|
57
|
+
|
|
58
|
+
1. **Adapters are isolated.** An adapter may import from `base.py`, `cache.py`, and `models.py` only. Adapters never import each other or the SSVC engine.
|
|
59
|
+
2. **The SSVC engine is pure.** `ssvc/engine.py` takes `(Enrichment, OrgContext, tree)` and returns a `Verdict` with a `DecisionPath`. No I/O, no network, no cache access. Fully deterministic and unit-testable.
|
|
60
|
+
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.
|
|
61
|
+
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`.
|
|
62
|
+
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.
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
## Security posture (this is a security tool — hold the bar)
|
|
66
|
+
|
|
67
|
+
- Pin all GitHub Actions by full SHA
|
|
68
|
+
- Dependabot/renovate on; lockfile committed
|
|
69
|
+
- No `eval`, no `pickle` for untrusted data, no shelling out to parse files
|
|
70
|
+
- Validate and bound all external JSON before model construction (Pydantic strict mode)
|
|
71
|
+
- SARIF output must validate against the 2.1.0 schema (test enforces this)
|
|
72
|
+
|
|
73
|
+
## Style notes for Claude Code
|
|
74
|
+
|
|
75
|
+
- Prefer small PRs/commits scoped to one adapter or one output format
|
|
76
|
+
- When adding a source adapter: implement ABC → add fixtures → add tests → register in `base.py` registry → update `SPEC.md` source table
|
|
77
|
+
- Never mock `httpx` inline in tests; use the fixture-loading helper in `tests/conftest.py`
|
|
78
|
+
- Update `FRAMEWORK.md` diagrams when module boundaries change
|
|
79
|
+
- Keep `cli.py` under ~150 lines; push logic down into modules
|