advisory-scan: count sidecar CVEs via per-image windows

A recipe upgrades several images, each through its own version range. The scan
previously classified only the app repo, so sidecar bumps contributed nothing — the
alternative to the earlier bug where sidecars were judged by the APP's window and
produced a false 133.

Now: --window KEY=FROM:TO (repeatable) gives any other source its own range; each
window is classified independently (one may use patched-version ranges while another
falls back to advisory dates) and the count is the UNION. An image with no window is
still not counted — the scan will not guess a range it was not given. If ANY requested
window cannot be ordered, the total is UNKNOWN rather than a partial number.

/recipe-upgrade now instructs passing a --window per bumped sidecar.

Verified on discourse app 3.5.3->2026.7.1 + redis 7.4->8.10: 128 = 123 (app, by
publish date) + 5 (redis, by version range). The redis five are genuine for that bump
(patched 7.4.1 / 7.4.6 / 8.2.3) and include CVE-2025-49844, CRITICAL — previously
invisible. Regressions clean: gitea still 2, discourse without the sidecar window
still 123.
This commit is contained in:
autonomic-bot
2026-08-10 22:01:28 +00:00
parent 78ae2be8ae
commit b5f8543a9b
3 changed files with 133 additions and 81 deletions
+16 -9
View File
@@ -26,6 +26,7 @@ advisory-scan.py <recipe> [--from <version>] [--to <version>] [--json] [--regist
|---|---|
| `<recipe>` | Recipe name; selects `cc-ci-plan/upstream/<recipe>.md` (the per-recipe URL registry) |
| `--from` / `--to` | The **primary app image's** version window being upgraded across |
| `--window KEY=FROM:TO` | A **sidecar's own** window (repeatable). `KEY` matches a source repo name, e.g. `--window redis=7.4:8.10`. Without it that image's advisories stay unclassified. |
| `--registry` | Registry dir; also `CCCI_UPSTREAM_REGISTRY` |
| `GITHUB_TOKEN` / `GITHUB_TOKEN_FILE` | Read-only token; **rate limit only** (60/hr anonymous → 5000/hr). Default file `/srv/cc-ci/.github-token`, mode 600. Public advisories need **no scopes**. |
@@ -100,12 +101,17 @@ published_at, context}`. A CVE seen by several sources keeps them all.
Two invariants govern this step, both learned from a wrong answer in production.
> **A. The window belongs to ONE image.** Only advisories from the **primary** source (the first
> `github-advisories:` source, i.e. the app repo the registry lists first) are classified. Sidecar
> advisories are recorded as unclassified so they stay visible without inflating the count.
> **A. Every image is judged by its OWN window.** The app repo uses `--from/--to`; each sidecar uses
> its own `--window KEY=FROM:TO`. An image with no window is **not** classified — its advisories are
> listed as unclassified so they stay visible without inflating the count. The reported count is the
> **union across windows**, and each window is classified independently (so one may use version
> ranges while another falls back to dates).
> *Why:* discourse once reported **133**, of which **34 were redis CVEs** — including
> `CVE-2021-21309`, patched in redis 6.0.11 in 2021 — counted purely because 6.0.11 sits numerically
> inside discourse's `3.5.3 → 2026.7.1` range.
> inside discourse's `3.5.3 → 2026.7.1` range. The fix is not to ignore sidecars but to give each one
> the window it actually moved through: with `--window redis=7.4:8.10`, discourse scores
> **128 = 123 (app, by date) + 5 (redis, by version range)** — and the redis five include
> `CVE-2025-49844`, **critical**, which was invisible while sidecars went uncounted.
>
> **B. Never emit a number you cannot justify.** If neither method below can order the window, the
> count is `null` / `UNKNOWN`, never `0`. A `0` in a security column asserts safety.
@@ -147,10 +153,11 @@ Markdown (default) for pasting into the per-recipe upgrade log, or `--json`.
| Field | Meaning |
|---|---|
| `cve_count_fixed` | Count, or **`null`** when undeterminable |
| `cve_count_fixed` | Union across all windows, or **`null`** if ANY requested window could not be ordered (a partial number would understate) |
| `count_known` | Distinguishes "counted zero" from "could not count" |
| `classified_by` | `patched version ranges` or `advisory publish date (version scheme changed)` |
| `date_window` | The resolved date window, when 4b was used |
| `windows` | Every source classified, with its from/to |
| `classified_by` | **Per source**: `patched version ranges` or `advisory publish date (version scheme changed)` |
| `date_window` | **Per source**, when 4b was used |
| `fixed_by_this_upgrade[]` | CVE ids, with severity / GHSA / fixed-in per id |
| `unclassified[]` | Seen but not attributable to this window (incl. other images) |
| `sources[]` | Every source with its own status |
@@ -175,8 +182,8 @@ in this tool or stale registry data.
## Known limits
1. **One window per scan.** Sidecar bumps (redis, postgres) are not counted; they appear as
unclassified. Per-image windows would be the natural extension.
1. **Windows must be supplied.** An image with no `--window` is not counted — the scan will not
guess a version range it was not told. `/recipe-upgrade` passes one per image it bumped.
2. **Date-based counts are temporal**, not exact — they assume publish-at-fix-time.
3. **Registry-bound.** Unlisted vendor security pages are invisible; the scan cannot know what it was
never pointed at.