main was 16 days and 259 commits behind the line the product is actually built on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It was neither current nor curated - just stale. This brings it forward and, at the same time, makes it mean something: main is what a receiving site installs, and nothing else. WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides, knowledgebase, machines, network, notifications, printers, slides, tools, warranty - plus applications, which is manifest-less core and always ships. The only manifest dependency inside that set is endpoints on computers, and both are in it, so the closure is self-contained. WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb stay on the site line. So do the Windows installer tree, the VM rehearsal helpers, the agent tooling config, one site's data loader, the internal audit and findings records, and the planner spreadsheet. 1,135 tracked files against 1,438. A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped: deploy/site-profile-universal.json still named all seventeen plugins. It is the file docs/DEPLOY.md tells a site to provision from, so a fresh install would have failed at `flask plugin apply-profile` with PluginError - the first thing a receiving site does, broken by the branch meant to serve them. It now names the twelve that exist, and its comment says the two must agree. agent/ was never refreshed. Whoever populated this branch copied shopdb/, plugins/, frontend/ and tests/ from the site line and left the agent tree at its September state: 22 C# and PowerShell sources missing, including AgentCredentials, EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the site line had deleted still present. The old tree is internally consistent, so it compiles, and nothing in CI builds it - every gate was green over a server that speaks enrollment and per-PC credentials to a bundled agent source that cannot. Resynced whole. DECISIONS THIS ENCODES, rather than defers: - The plugin-authoring lab builds printedparts, whose manifest depends on employees, so it could not stay. The lab remains on the site line and main ships no build-along tutorial. That is a real loss for an adopting developer and is recorded here rather than discovered later. - Recognition, Training and Recertification notifications are no longer seeded. Each splits one notification into a card per employee and renders a photo, which needs a plugin this branch does not bundle. Seeding them would offer an author a type that cannot resolve anybody. A site that installs employees gets them from that plugin's own seed. - Windows/IIS is documented as the legacy path throughout, and the README leads with Docker, because that is what a receiving site gets. The API description is regenerated for THIS branch - 347 paths and 483 operations, down from 415 and 573 - so the published contract describes what is here rather than what the site line serves. docs/CONFIG.md lost the two measuring-tool label keys the code no longer declares, which two lanes had decided opposite ways. Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run rather than skip, and every index line count is derived from the file it names.
109 lines
4.7 KiB
Markdown
109 lines
4.7 KiB
Markdown
<!-- docmeta
|
|
purpose: howto
|
|
audience: developer
|
|
status: current
|
|
summary: Answer "are we exposed to this advisory" from the shipped SBOM, and what can and cannot fail a build.
|
|
loadwith:
|
|
-->
|
|
|
|
# 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 file has held exactly
|
|
one entry so far, and how it ended is the point.
|
|
|
|
A PyJWT advisory rated HIGH - algorithm confusion, forged HS256 tokens - needs a
|
|
verifier configured with both symmetric and asymmetric algorithms AND a raw JSON
|
|
JWK passed as the key. This application configures neither: there is no
|
|
`JWT_ALGORITHM` or `JWT_DECODE_ALGORITHMS` anywhere in the tree, so
|
|
flask-jwt-extended runs HS256-only against a string secret. That was written
|
|
down, with `invalidatedby` recording what would make it untrue - SSO issuing
|
|
tokens verified against a JWK.
|
|
|
|
Then PyJWT was upgraded, the advisory stopped matching, and the entry was
|
|
deleted. The allowlist is empty again, which is the state to aim for.
|
|
|
|
`invalidatedby` is the field 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 smallest upgrade that
|
|
clears every advisory against that component, and how many it clears.
|
|
|
|
"Smallest" is doing real work there. An advisory patched on several release
|
|
branches lists a fix per branch - nanoid's is fixed in both 3.3.18 and 5.1.16 -
|
|
and taking the highest recommends a major upgrade when a patch on the current
|
|
branch would do. Where the package is transitive that major may not even be
|
|
reachable: nanoid arrives under postcss, pinned `^3.3.11`, a range 5.x cannot
|
|
satisfy. So the target is the lowest published fix above the version in use.
|
|
Fixes on older branches are ignored rather than offered as a downgrade.
|
|
|
|
A target on a different major version is still labelled `MAJOR`, but now that
|
|
label means something: no fix exists on the current branch, so the upgrade is
|
|
genuinely a different job - it can be ESM-only, or drop an API.
|