cerberus-sca 1.0.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.
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ *.egg-info/
5
+ .venv/
6
+ venv/
7
+ *.json.bak
8
+ dist/
9
+ build/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shankar Adhikary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.5
2
+ Name: cerberus-sca
3
+ Version: 1.0.0
4
+ Summary: A CI gate that scans package-lock.json/requirements.txt/go.sum/Cargo.lock against OSV.dev and fails the build on vulnerabilities at or above a configurable severity threshold.
5
+ Project-URL: Repository, https://github.com/ShankarAdhikary/Cerberus
6
+ Author-email: Shankar Adhikary <adhikaryshankar04@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Security
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: cvss==3.6
14
+ Requires-Dist: requests==2.32.5
15
+ Requires-Dist: rich==15.0.0
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Dependency Vulnerability Gate
21
+
22
+ A minimal Software Composition Analysis (SCA) CLI that scans `package-lock.json`
23
+ or `requirements.txt` against the [OSV.dev](https://osv.dev) vulnerability
24
+ database and fails CI when findings meet a severity threshold.
25
+
26
+ ## How it works
27
+
28
+ 1. **Parse** the lockfile into a flat list of `{name, version, ecosystem}`.
29
+ 2. **Batch query** `POST /v1/querybatch` — cheap, returns only vulnerability
30
+ IDs per package, not full details.
31
+ 3. **Hydrate** each *unique* vulnerability ID via `GET /v1/vulns/{id}` to get
32
+ the full record (severity, description, fixed version), caching so the
33
+ same CVE isn't fetched twice even if it affects many packages.
34
+ 4. **Score** each finding: parse the CVSS vector if present, fall back to
35
+ the advisory's plain-text `database_specific.severity` rating, or mark
36
+ it `UNKNOWN` if neither exists.
37
+ 5. **Gate**: exit `1` if any finding is at or above `--fail-on` (default
38
+ `high`), else exit `0`.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install dep-vuln-gate
44
+ ```
45
+
46
+ From [PyPI](https://pypi.org/project/dep-vuln-gate/) — verified end-to-end
47
+ in a clean venv (install, `--help`, a real live scan against OSV.dev).
48
+ Note the PyPI/`pip install` name is `dep-vuln-gate`, not `dep-gate` —
49
+ that shorter name was already too similar to an existing, unrelated PyPI
50
+ package for PyPI's own upload validator to accept (see `docs/Tracker.md`
51
+ for the exact rejection and the availability checks that did and didn't
52
+ catch it up front). The **console command** installed either way is
53
+ `dep-gate` (short, what you actually type) — only the package name you
54
+ `pip install` differs.
55
+
56
+ Also installable from a tagged release without PyPI at all (equally
57
+ verified):
58
+ ```bash
59
+ pip install git+https://github.com/ShankarAdhikary/Cerberus.git@v1.0.0
60
+ ```
61
+
62
+ Alternatively, without installing the package at all:
63
+ ```bash
64
+ git clone https://github.com/ShankarAdhikary/Cerberus.git
65
+ cd Cerberus/dep-vuln-gate
66
+ pip install -r requirements.txt
67
+ python -m dep_gate.cli --file package-lock.json --fail-on high
68
+ ```
69
+
70
+ **In another repo's own CI**, pin to a released version rather than a
71
+ moving target:
72
+ ```yaml
73
+ - run: pip install dep-vuln-gate==1.0.0
74
+ - run: dep-gate --file package-lock.json --fail-on high
75
+ ```
76
+
77
+ ## Usage
78
+
79
+ ```bash
80
+ dep-gate --file package-lock.json --fail-on high
81
+ dep-gate --file requirements.txt --fail-on critical --json report.json
82
+
83
+ # --file is repeatable: scan multiple lockfiles in one invocation, with
84
+ # one combined JSON/SARIF/SBOM/PR-comment output instead of one per file.
85
+ dep-gate --file package-lock.json --file requirements.txt --fail-on high
86
+ ```
87
+
88
+ (`python -m dep_gate.cli` works identically to `dep-gate` — the console
89
+ script is `dep_gate.cli:run` with no wrapper, so both invoke the exact
90
+ same code path. Every example below uses the module form only because it
91
+ also works without installing the package; swap in `dep-gate` freely.)
92
+
93
+ Flags:
94
+ - `--file PATH` — required, **repeatable**. Path to a lockfile
95
+ (`package-lock.json`, `requirements.txt`, `go.sum`, `Cargo.lock`).
96
+ Repeat it to scan several in one run; a single `--file` behaves exactly
97
+ as it always has.
98
+ - `--fail-on {low,moderate,high,critical}` — minimum severity that blocks the build.
99
+ - `--json PATH` — also write a machine-readable report.
100
+ - `--fail-open` — exit `0` instead of `1` if OSV.dev is unreachable (off by
101
+ default; a broken security gate should fail loudly, not silently pass).
102
+ - `--diff-only --base-ref REF` — only scan dependencies that are new or
103
+ version-changed relative to `REF` (see "Delta scanning" below).
104
+ - `--verbose` — add a "Source" column/JSON field showing how each
105
+ severity was derived (e.g. `CVSS_V3 vector` vs.
106
+ `database_specific.severity`). Off by default; purely additive.
107
+ - `--sbom PATH` — write a CycloneDX JSON Software Bill of Materials
108
+ listing every dependency scanned (respects `--diff-only`). Independent
109
+ of findings — written even on a clean scan or an OSV.dev outage. For a
110
+ complete build inventory, run it *without* `--diff-only` — the PR-gate
111
+ workflow below only ever scans the changed subset, so a `--sbom` there
112
+ would be a partial bill of materials, not a full one.
113
+ - `--ignore-file PATH` — suppress accepted-risk findings via a
114
+ `.dep-gate-ignore.yml` file (see below). A suppressed finding stays
115
+ visible in the table/JSON (marked, never hidden) and just doesn't
116
+ count toward `--fail-on`.
117
+ - `--sarif PATH` — also write a SARIF 2.1.0 report for GitHub
118
+ code-scanning upload (`github/codeql-action/upload-sarif`). Same
119
+ findings as `--json`, different shape.
120
+ - `--cache-file PATH` — reuse a local JSON cache of hydrated OSV records
121
+ across runs, skipping re-fetches for a vuln ID already fetched within
122
+ the last 6 hours. Off by default. Persist the file across CI runs
123
+ (e.g. `actions/cache`) to get the actual benefit.
124
+ - `--pr-comment` — post/update an idempotent PR comment summarizing
125
+ blocking findings, via the GitHub API. Off by default — this is the
126
+ tool's *only other* network egress point besides `api.osv.dev`.
127
+ Requires `--github-repo OWNER/REPO`, `--github-pr-number N`, and a
128
+ `GITHUB_TOKEN` environment variable (the workflow's own token — never
129
+ pass it as a CLI argument). A comment-posting failure prints a warning
130
+ but never changes the exit code.
131
+
132
+ ```yaml
133
+ # in a pull_request-triggered job, with:
134
+ # permissions:
135
+ # pull-requests: write
136
+ - run: >
137
+ python -m dep_gate.cli --file package-lock.json --file requirements.txt
138
+ --fail-on high
139
+ --pr-comment
140
+ --github-repo ${{ github.repository }}
141
+ --github-pr-number ${{ github.event.pull_request.number }}
142
+ env:
143
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
144
+ ```
145
+
146
+ This is exactly what `security-scan.yml` below does: one combined
147
+ invocation across every lockfile present, so `--pr-comment` posts a
148
+ single comment covering all of them (grouped by file — see Design.md §4)
149
+ instead of one ecosystem's `--pr-comment` call finding-and-overwriting
150
+ another's. (An earlier version of this project ran one `dep_gate.cli`
151
+ invocation per ecosystem, which made `--pr-comment` impossible to wire in
152
+ safely — each invocation would've silently dropped every other
153
+ ecosystem's findings from the comment. Multi-file `--file` support fixed
154
+ that at the root, rather than working around it.)
155
+
156
+ ## Suppressing accepted-risk findings
157
+
158
+ ```yaml
159
+ # .dep-gate-ignore.yml
160
+ - vuln_id: GHSA-jf85-cpcp-j695
161
+ package: lodash # optional - omit to suppress this ID for any package
162
+ expires: 2026-12-31 # required
163
+ reason: "Accepted risk - vendor patch pending, tracked in JIRA-1234"
164
+ ```
165
+
166
+ `vuln_id`, `expires` (`YYYY-MM-DD`), and a non-blank `reason` are all
167
+ required per entry — a malformed file exits `2` rather than silently
168
+ ignoring the bad entry. Once `expires` passes, the entry stops
169
+ suppressing automatically and that finding blocks the build again.
170
+
171
+ ```bash
172
+ python -m dep_gate.cli --file package-lock.json --ignore-file .dep-gate-ignore.yml
173
+ ```
174
+
175
+ ## Delta scanning
176
+
177
+ By default the tool re-scans every dependency in the lockfile on every run.
178
+ With `--diff-only`, it instead reads the lockfile's content at `--base-ref`
179
+ via `git show` (no checkout needed), parses both versions, and only reports
180
+ on dependencies that are **new** or have a **different pinned version** —
181
+ a version bump counts as changed even if the old version had no known
182
+ vulnerabilities, since the new one might.
183
+
184
+ This mirrors how Dependabot avoids re-flagging pre-existing vulnerable
185
+ dependencies on every PR: a gate that only complains about risk *you just
186
+ introduced* is far less likely to get muted or bypassed by a frustrated team.
187
+
188
+ ```bash
189
+ python -m dep_gate.cli --file requirements.txt --diff-only --base-ref origin/main
190
+ ```
191
+
192
+ Requires running inside a git repo with the base ref actually fetched — in
193
+ GitHub Actions this means `actions/checkout@v4` with `fetch-depth: 0`
194
+ (shallow clones won't have the base branch's history available). If the
195
+ lockfile didn't exist at the base ref at all (e.g. it's brand new), every
196
+ dependency in it is treated as new — not an error.
197
+
198
+ ## Supported lockfiles
199
+
200
+ - `package-lock.json` (npm lockfile v1, v2, and v3 — including transitive deps)
201
+ - `requirements.txt` (only pinned `==` lines have a resolvable version;
202
+ unpinned/VCS lines are skipped with a warning)
203
+ - `go.sum` (Go modules — the `/go.mod`-hash and content-hash rows for the
204
+ same module collapse into one dependency)
205
+ - `Cargo.lock` (Rust — only registry (crates.io) packages are checked;
206
+ path/workspace and git dependencies are skipped, since they aren't
207
+ published to crates.io and have no OSV-resolvable version)
208
+
209
+ ## Testing
210
+
211
+ ```bash
212
+ pip install -r requirements-dev.txt
213
+ python -m pytest
214
+ ```
215
+
216
+ The suite is fully network-free (the OSV.dev boundary is mocked in
217
+ `tests/test_osv_client.py` and `tests/test_cli.py`); `tests/test_diff.py`
218
+ spins up real temporary git repositories rather than mocking `git`.
219
+
220
+ ## CI integration
221
+
222
+ See `.github/workflows/security-scan.yml`. Two triggers:
223
+ - **`pull_request`** — diff-only scan of whatever lockfile(s) the PR
224
+ touches; uploads the JSON report as a build artifact and the SARIF
225
+ report to code scanning.
226
+ - **`push` to `main`** — full (non-diff) scan on every merge. This isn't
227
+ just belt-and-suspenders: GitHub's code-scanning alerts only report a
228
+ PR's findings as "new" relative to the last analysis on the default
229
+ branch, so without this trigger a PR's SARIF upload silently never
230
+ shows up as a visible alert, even though it succeeds.
231
+
232
+ ## Known limitations / good next steps
233
+
234
+ - Four ecosystems supported (`npm`, `PyPI`, `Go`, `crates.io`). OSV also
235
+ covers Maven, RubyGems, Packagist, etc. — the same batch/hydrate client
236
+ works for those, you'd just need lockfile parsers for each.
237
+ - SPDX SBOM format not supported (CycloneDX is).
238
+
239
+ ## License
240
+
241
+ [MIT](LICENSE)
@@ -0,0 +1,222 @@
1
+ # Dependency Vulnerability Gate
2
+
3
+ A minimal Software Composition Analysis (SCA) CLI that scans `package-lock.json`
4
+ or `requirements.txt` against the [OSV.dev](https://osv.dev) vulnerability
5
+ database and fails CI when findings meet a severity threshold.
6
+
7
+ ## How it works
8
+
9
+ 1. **Parse** the lockfile into a flat list of `{name, version, ecosystem}`.
10
+ 2. **Batch query** `POST /v1/querybatch` — cheap, returns only vulnerability
11
+ IDs per package, not full details.
12
+ 3. **Hydrate** each *unique* vulnerability ID via `GET /v1/vulns/{id}` to get
13
+ the full record (severity, description, fixed version), caching so the
14
+ same CVE isn't fetched twice even if it affects many packages.
15
+ 4. **Score** each finding: parse the CVSS vector if present, fall back to
16
+ the advisory's plain-text `database_specific.severity` rating, or mark
17
+ it `UNKNOWN` if neither exists.
18
+ 5. **Gate**: exit `1` if any finding is at or above `--fail-on` (default
19
+ `high`), else exit `0`.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install dep-vuln-gate
25
+ ```
26
+
27
+ From [PyPI](https://pypi.org/project/dep-vuln-gate/) — verified end-to-end
28
+ in a clean venv (install, `--help`, a real live scan against OSV.dev).
29
+ Note the PyPI/`pip install` name is `dep-vuln-gate`, not `dep-gate` —
30
+ that shorter name was already too similar to an existing, unrelated PyPI
31
+ package for PyPI's own upload validator to accept (see `docs/Tracker.md`
32
+ for the exact rejection and the availability checks that did and didn't
33
+ catch it up front). The **console command** installed either way is
34
+ `dep-gate` (short, what you actually type) — only the package name you
35
+ `pip install` differs.
36
+
37
+ Also installable from a tagged release without PyPI at all (equally
38
+ verified):
39
+ ```bash
40
+ pip install git+https://github.com/ShankarAdhikary/Cerberus.git@v1.0.0
41
+ ```
42
+
43
+ Alternatively, without installing the package at all:
44
+ ```bash
45
+ git clone https://github.com/ShankarAdhikary/Cerberus.git
46
+ cd Cerberus/dep-vuln-gate
47
+ pip install -r requirements.txt
48
+ python -m dep_gate.cli --file package-lock.json --fail-on high
49
+ ```
50
+
51
+ **In another repo's own CI**, pin to a released version rather than a
52
+ moving target:
53
+ ```yaml
54
+ - run: pip install dep-vuln-gate==1.0.0
55
+ - run: dep-gate --file package-lock.json --fail-on high
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ ```bash
61
+ dep-gate --file package-lock.json --fail-on high
62
+ dep-gate --file requirements.txt --fail-on critical --json report.json
63
+
64
+ # --file is repeatable: scan multiple lockfiles in one invocation, with
65
+ # one combined JSON/SARIF/SBOM/PR-comment output instead of one per file.
66
+ dep-gate --file package-lock.json --file requirements.txt --fail-on high
67
+ ```
68
+
69
+ (`python -m dep_gate.cli` works identically to `dep-gate` — the console
70
+ script is `dep_gate.cli:run` with no wrapper, so both invoke the exact
71
+ same code path. Every example below uses the module form only because it
72
+ also works without installing the package; swap in `dep-gate` freely.)
73
+
74
+ Flags:
75
+ - `--file PATH` — required, **repeatable**. Path to a lockfile
76
+ (`package-lock.json`, `requirements.txt`, `go.sum`, `Cargo.lock`).
77
+ Repeat it to scan several in one run; a single `--file` behaves exactly
78
+ as it always has.
79
+ - `--fail-on {low,moderate,high,critical}` — minimum severity that blocks the build.
80
+ - `--json PATH` — also write a machine-readable report.
81
+ - `--fail-open` — exit `0` instead of `1` if OSV.dev is unreachable (off by
82
+ default; a broken security gate should fail loudly, not silently pass).
83
+ - `--diff-only --base-ref REF` — only scan dependencies that are new or
84
+ version-changed relative to `REF` (see "Delta scanning" below).
85
+ - `--verbose` — add a "Source" column/JSON field showing how each
86
+ severity was derived (e.g. `CVSS_V3 vector` vs.
87
+ `database_specific.severity`). Off by default; purely additive.
88
+ - `--sbom PATH` — write a CycloneDX JSON Software Bill of Materials
89
+ listing every dependency scanned (respects `--diff-only`). Independent
90
+ of findings — written even on a clean scan or an OSV.dev outage. For a
91
+ complete build inventory, run it *without* `--diff-only` — the PR-gate
92
+ workflow below only ever scans the changed subset, so a `--sbom` there
93
+ would be a partial bill of materials, not a full one.
94
+ - `--ignore-file PATH` — suppress accepted-risk findings via a
95
+ `.dep-gate-ignore.yml` file (see below). A suppressed finding stays
96
+ visible in the table/JSON (marked, never hidden) and just doesn't
97
+ count toward `--fail-on`.
98
+ - `--sarif PATH` — also write a SARIF 2.1.0 report for GitHub
99
+ code-scanning upload (`github/codeql-action/upload-sarif`). Same
100
+ findings as `--json`, different shape.
101
+ - `--cache-file PATH` — reuse a local JSON cache of hydrated OSV records
102
+ across runs, skipping re-fetches for a vuln ID already fetched within
103
+ the last 6 hours. Off by default. Persist the file across CI runs
104
+ (e.g. `actions/cache`) to get the actual benefit.
105
+ - `--pr-comment` — post/update an idempotent PR comment summarizing
106
+ blocking findings, via the GitHub API. Off by default — this is the
107
+ tool's *only other* network egress point besides `api.osv.dev`.
108
+ Requires `--github-repo OWNER/REPO`, `--github-pr-number N`, and a
109
+ `GITHUB_TOKEN` environment variable (the workflow's own token — never
110
+ pass it as a CLI argument). A comment-posting failure prints a warning
111
+ but never changes the exit code.
112
+
113
+ ```yaml
114
+ # in a pull_request-triggered job, with:
115
+ # permissions:
116
+ # pull-requests: write
117
+ - run: >
118
+ python -m dep_gate.cli --file package-lock.json --file requirements.txt
119
+ --fail-on high
120
+ --pr-comment
121
+ --github-repo ${{ github.repository }}
122
+ --github-pr-number ${{ github.event.pull_request.number }}
123
+ env:
124
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
125
+ ```
126
+
127
+ This is exactly what `security-scan.yml` below does: one combined
128
+ invocation across every lockfile present, so `--pr-comment` posts a
129
+ single comment covering all of them (grouped by file — see Design.md §4)
130
+ instead of one ecosystem's `--pr-comment` call finding-and-overwriting
131
+ another's. (An earlier version of this project ran one `dep_gate.cli`
132
+ invocation per ecosystem, which made `--pr-comment` impossible to wire in
133
+ safely — each invocation would've silently dropped every other
134
+ ecosystem's findings from the comment. Multi-file `--file` support fixed
135
+ that at the root, rather than working around it.)
136
+
137
+ ## Suppressing accepted-risk findings
138
+
139
+ ```yaml
140
+ # .dep-gate-ignore.yml
141
+ - vuln_id: GHSA-jf85-cpcp-j695
142
+ package: lodash # optional - omit to suppress this ID for any package
143
+ expires: 2026-12-31 # required
144
+ reason: "Accepted risk - vendor patch pending, tracked in JIRA-1234"
145
+ ```
146
+
147
+ `vuln_id`, `expires` (`YYYY-MM-DD`), and a non-blank `reason` are all
148
+ required per entry — a malformed file exits `2` rather than silently
149
+ ignoring the bad entry. Once `expires` passes, the entry stops
150
+ suppressing automatically and that finding blocks the build again.
151
+
152
+ ```bash
153
+ python -m dep_gate.cli --file package-lock.json --ignore-file .dep-gate-ignore.yml
154
+ ```
155
+
156
+ ## Delta scanning
157
+
158
+ By default the tool re-scans every dependency in the lockfile on every run.
159
+ With `--diff-only`, it instead reads the lockfile's content at `--base-ref`
160
+ via `git show` (no checkout needed), parses both versions, and only reports
161
+ on dependencies that are **new** or have a **different pinned version** —
162
+ a version bump counts as changed even if the old version had no known
163
+ vulnerabilities, since the new one might.
164
+
165
+ This mirrors how Dependabot avoids re-flagging pre-existing vulnerable
166
+ dependencies on every PR: a gate that only complains about risk *you just
167
+ introduced* is far less likely to get muted or bypassed by a frustrated team.
168
+
169
+ ```bash
170
+ python -m dep_gate.cli --file requirements.txt --diff-only --base-ref origin/main
171
+ ```
172
+
173
+ Requires running inside a git repo with the base ref actually fetched — in
174
+ GitHub Actions this means `actions/checkout@v4` with `fetch-depth: 0`
175
+ (shallow clones won't have the base branch's history available). If the
176
+ lockfile didn't exist at the base ref at all (e.g. it's brand new), every
177
+ dependency in it is treated as new — not an error.
178
+
179
+ ## Supported lockfiles
180
+
181
+ - `package-lock.json` (npm lockfile v1, v2, and v3 — including transitive deps)
182
+ - `requirements.txt` (only pinned `==` lines have a resolvable version;
183
+ unpinned/VCS lines are skipped with a warning)
184
+ - `go.sum` (Go modules — the `/go.mod`-hash and content-hash rows for the
185
+ same module collapse into one dependency)
186
+ - `Cargo.lock` (Rust — only registry (crates.io) packages are checked;
187
+ path/workspace and git dependencies are skipped, since they aren't
188
+ published to crates.io and have no OSV-resolvable version)
189
+
190
+ ## Testing
191
+
192
+ ```bash
193
+ pip install -r requirements-dev.txt
194
+ python -m pytest
195
+ ```
196
+
197
+ The suite is fully network-free (the OSV.dev boundary is mocked in
198
+ `tests/test_osv_client.py` and `tests/test_cli.py`); `tests/test_diff.py`
199
+ spins up real temporary git repositories rather than mocking `git`.
200
+
201
+ ## CI integration
202
+
203
+ See `.github/workflows/security-scan.yml`. Two triggers:
204
+ - **`pull_request`** — diff-only scan of whatever lockfile(s) the PR
205
+ touches; uploads the JSON report as a build artifact and the SARIF
206
+ report to code scanning.
207
+ - **`push` to `main`** — full (non-diff) scan on every merge. This isn't
208
+ just belt-and-suspenders: GitHub's code-scanning alerts only report a
209
+ PR's findings as "new" relative to the last analysis on the default
210
+ branch, so without this trigger a PR's SARIF upload silently never
211
+ shows up as a visible alert, even though it succeeds.
212
+
213
+ ## Known limitations / good next steps
214
+
215
+ - Four ecosystems supported (`npm`, `PyPI`, `Go`, `crates.io`). OSV also
216
+ covers Maven, RubyGems, Packagist, etc. — the same batch/hydrate client
217
+ works for those, you'd just need lockfile parsers for each.
218
+ - SPDX SBOM format not supported (CycloneDX is).
219
+
220
+ ## License
221
+
222
+ [MIT](LICENSE)
@@ -0,0 +1,3 @@
1
+ """Dependency Vulnerability Gate - a minimal SCA (Software Composition Analysis) CLI."""
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,57 @@
1
+ """
2
+ A flat-JSON, TTL-based cache of hydrated OSV vuln records
3
+ (GET /v1/vulns/{id} responses), keyed by vuln_id, so a CI job that runs
4
+ repeatedly against the same repo doesn't re-fetch a record it already
5
+ has fresh.
6
+
7
+ Why TTL instead of OSV's `modified` timestamp: `batch_query()`'s response
8
+ does carry a per-result `modified` field (see Schema.md §5.1), which
9
+ could in principle drive exact staleness checks - but wiring that through
10
+ would change `batch_query()`'s documented, already-tested return shape
11
+ (`Dict[str, set[str]]`) into something structurally different, for a
12
+ benefit (byte-exact staleness detection) that a bounded TTL already
13
+ delivers with far less risk to the existing, well-tested contract. A
14
+ security tool's cache staleness window should be small and explicit
15
+ either way, so `CACHE_TTL_SECONDS` is deliberately short.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+
22
+ CACHE_TTL_SECONDS = 6 * 60 * 60 # 6 hours
23
+
24
+
25
+ def load_cache(cache_path: str) -> dict[str, dict]:
26
+ """
27
+ Load the cache file. Returns `{}` if it doesn't exist (first run) or
28
+ is unreadable/corrupt - a broken cache file must never take down the
29
+ scan; it's just treated as an empty cache and rebuilt.
30
+ """
31
+ try:
32
+ with open(cache_path, "r", encoding="utf-8") as f:
33
+ data = json.load(f)
34
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
35
+ return {}
36
+ return data if isinstance(data, dict) else {}
37
+
38
+
39
+ def save_cache(cache_path: str, cache: dict[str, dict]) -> None:
40
+ """Write the cache file."""
41
+ with open(cache_path, "w", encoding="utf-8") as f:
42
+ json.dump(cache, f)
43
+
44
+
45
+ def get_fresh(cache: dict[str, dict], vuln_id: str, now: float) -> dict | None:
46
+ """Return the cached hydrated record for `vuln_id` if still within TTL, else None."""
47
+ entry = cache.get(vuln_id)
48
+ if entry is None:
49
+ return None
50
+ if now - entry["cached_at"] > CACHE_TTL_SECONDS:
51
+ return None
52
+ return entry["record"]
53
+
54
+
55
+ def put(cache: dict[str, dict], vuln_id: str, record: dict, now: float) -> None:
56
+ """Store a freshly-fetched hydrated record in `cache` (mutated in place)."""
57
+ cache[vuln_id] = {"record": record, "cached_at": now}