The SBOM was generated and installed onto every server, and nothing read it. "Are we exposed to this advisory" was still a person going and looking, which meant it happened when somebody remembered to. scripts/scan_sbom.py queries OSV.dev by purl - no account, no API key, no agent, no vulnerability database to maintain on a box that cannot reach the internet. The SBOM is already the input OSV wants, so this is a lookup against what shipped rather than a second scan of the tree that could disagree with it. Three decisions worth recording: Shipped and build-only findings are both reported, but only shipped ones can fail a build. Two thirds of the components here are build tooling the SBOM already marks scope 'excluded', and the highest-severity match in the whole tree today is a CRITICAL against the test runner. A gate that fails a release over that trains everybody to bypass it, and then it catches nothing. Severity is taken from the advisory, never derived. Where there is only a CVSS vector the report says UNKNOWN, which is shown and never gates. A guessed score is worse than an honest gap because somebody would act on it. The allowlist is a record rather than a mute button: each entry carries the reason, the accepter, the date, and what would invalidate the reasoning. The first entry is a worked example - a PyJWT algorithm-confusion advisory that needs both mixed algorithm families and a raw JSON JWK as the key, and this application configures neither. The report ends with a per-component upgrade rollup, since that is the part somebody acts on. A target on a different major version is labelled rather than listed beside patch bumps as though the two cost the same. Current state: 8 shipped components have fixes available, none of them exploitable through a path this application opens today.
86 lines
3.7 KiB
Markdown
86 lines
3.7 KiB
Markdown
# Dependency security
|
|
|
|
Two scripts, one question each.
|
|
|
|
| question | script | output |
|
|
|---|---|---|
|
|
| what are we running | `scripts/generate_sbom.py` | `sbom.cdx.json`, CycloneDX 1.6, shipped with every release |
|
|
| are we exposed | `scripts/scan_sbom.py` | a report, and an exit code |
|
|
|
|
The SBOM was already generated and installed onto every server. Nothing read
|
|
it, so "are we exposed to this advisory" was still a person going and looking.
|
|
The scanner closes that.
|
|
|
|
## Running it
|
|
|
|
```bash
|
|
venv/bin/python scripts/scan_sbom.py # report, exit 0
|
|
venv/bin/python scripts/scan_sbom.py --gate # exit 1 on a shipped finding
|
|
venv/bin/python scripts/scan_sbom.py --json out.json # machine-readable too
|
|
```
|
|
|
|
With no `--sbom`, it generates one first, so there is never a stale input.
|
|
|
|
It needs outbound HTTPS to `api.osv.dev`. **Run it where the repository lives,
|
|
not on a customer's server** - a deployed instance is frequently air-gapped and
|
|
has nothing to query. That is also why the SBOM ships as a file: it is the
|
|
artefact a security team scans on their own terms.
|
|
|
|
## Why OSV
|
|
|
|
It takes a purl and returns the advisories that apply. No account, no API key,
|
|
no agent installed anywhere, no vulnerability database to keep current. The
|
|
SBOM is already the input it wants, so this is a lookup against what shipped
|
|
rather than a second scan of the tree that could disagree with it.
|
|
|
|
## Shipped and build-only are not the same finding
|
|
|
|
Two thirds of the components in the SBOM are build tooling - bundlers, test
|
|
runners, their transitive dependencies. They never reach a server, and the SBOM
|
|
already marks them `scope: excluded`.
|
|
|
|
Both are reported. **Only shipped components can fail a build.**
|
|
|
|
This is deliberate. At the time of writing, the highest-severity match in the
|
|
whole tree is a CRITICAL against the test runner, which is not installed
|
|
anywhere a release reaches. A gate that fails on that teaches everybody to pass
|
|
`--no-verify` within a month, and then it catches nothing at all.
|
|
|
|
## Severity is taken, not computed
|
|
|
|
Where an advisory records a plain word - GHSA gives LOW, MODERATE, HIGH,
|
|
CRITICAL - that word is used. Where there is only a CVSS vector, the report says
|
|
UNKNOWN rather than parsing the vector into a score it might get wrong.
|
|
|
|
UNKNOWN is shown and never gates. A guessed score is worse than an honest gap,
|
|
because somebody would act on it.
|
|
|
|
## The allowlist is a record, not a mute button
|
|
|
|
`scripts/vulnerability-allowlist.json` holds advisories that were reviewed and
|
|
accepted. An entry carries the advisory id, the reason, who accepted it, the
|
|
date, and what would invalidate the reasoning.
|
|
|
|
A good reason names the precondition that is absent. The first entry is the
|
|
worked example: a PyJWT algorithm-confusion advisory that needs a verifier
|
|
configured with both symmetric and asymmetric algorithms AND a raw JSON JWK
|
|
passed as the key. Neither is true here, and `invalidatedby` records the change
|
|
that would make it true.
|
|
|
|
That last field is the one that earns its keep. A reason stops being true when
|
|
the code that made it true changes, and nobody re-reads an allowlist looking for
|
|
that. Write down what would break it.
|
|
|
|
Upgrading is almost always cheaper than defending a decision to skip it. Do not
|
|
add an entry to make a build pass.
|
|
|
|
## What the report tells you to do
|
|
|
|
The report ends with a per-component upgrade rollup: the highest fix version
|
|
across that component's advisories, and how many it clears. Upgrading once to
|
|
the top of the list clears the ones below it.
|
|
|
|
A target on a different major version is labelled `MAJOR`. It is a different
|
|
job from a patch bump - it can be ESM-only, or drop an API - and it is marked
|
|
rather than listed as though the two cost the same.
|