From 46c4fff1a6d890f1c996d39aa9f50e5aac69635c Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Tue, 11 Aug 2026 00:52:40 +0000 Subject: [PATCH] advisory-scan: --image takes NAME=FROM:TO Restores the single-value form (operator preference) under the --image name. Repeat the flag per image, all in one call. Malformed values warn on stderr and are skipped rather than aborting the scan, since it is an additive pre-step. Counts unchanged: discourse 128 with redis / 123 without, gitea 2. --- .claude/skills/recipe-upgrade/SKILL.md | 4 ++-- cc-ci-plan/advisory-scan.SPEC.md | 8 ++++---- cc-ci-plan/advisory-scan.py | 19 +++++++++++++------ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.claude/skills/recipe-upgrade/SKILL.md b/.claude/skills/recipe-upgrade/SKILL.md index 2e033e1..7806050 100644 --- a/.claude/skills/recipe-upgrade/SKILL.md +++ b/.claude/skills/recipe-upgrade/SKILL.md @@ -164,7 +164,7 @@ into the per-recipe log**: ``` python3 /srv/cc-ci/cc-ci-plan/advisory-scan.py --from --to \ - [--image ]... + [--image =:]... ``` **Pass an `--image` for EVERY sidecar you upgraded** (redis, postgres, nginx …), not just the app — @@ -173,7 +173,7 @@ the flag for each one and pass them **all in a single call** (the count is a uni e.g. discourse moving app 3.5.3→2026.7.1 *and* redis 7.4→8.10: ``` -... --from 3.5.3 --to 2026.7.1 --image redis 7.4 8.10 +... --from 3.5.3 --to 2026.7.1 --image redis=7.4:8.10 ``` → 128 CVEs (123 app + 5 redis), where the redis five include a **critical** (CVE-2025-49844) that is diff --git a/cc-ci-plan/advisory-scan.SPEC.md b/cc-ci-plan/advisory-scan.SPEC.md index 4884ee5..bd0aad8 100644 --- a/cc-ci-plan/advisory-scan.SPEC.md +++ b/cc-ci-plan/advisory-scan.SPEC.md @@ -20,14 +20,14 @@ Nothing in the pipeline queried an advisory source. This scan closes that hole. ``` advisory-scan.py [--from ] [--to ] - [--image ]... [--json] [--registry DIR] + [--image =:]... [--json] [--registry DIR] ``` | Input | Meaning | |---|---| | `` | Recipe name; selects `cc-ci-plan/upstream/.md` (the per-recipe URL registry) | | `--from` / `--to` | The **primary app image's** version window being upgraded across | -| `--image NAME FROM TO` | A **sidecar image and the versions it moved between** (repeatable). `NAME` is matched as a substring against source repo names, e.g. `--image redis 7.4 8.10`. Without it that image's advisories stay unclassified. | +| `--image NAME=FROM:TO` | A **sidecar image and the versions it moved between** (repeatable). `NAME` is matched as a substring against source repo names, e.g. `--image redis=7.4:8.10`. Malformed values are warned about on stderr and skipped. 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**. | @@ -103,7 +103,7 @@ 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. Every image is judged by its OWN versions.** The app repo uses `--from/--to`; each sidecar uses -> its own `--image NAME FROM TO`. **Pass them all in ONE invocation** — the count is a union across +> its own `--image NAME=FROM:TO`. **Pass them all in ONE invocation** — the count is a union across > images, and the UNKNOWN guarantee in B only holds when a single run sees every one. 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 @@ -111,7 +111,7 @@ Two invariants govern this step, both learned from a wrong answer in production. > *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. The fix is not to ignore sidecars but to give each one -> the versions it actually moved through: with `--image redis 7.4 8.10`, discourse scores +> the versions it actually moved through: with `--image 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. > diff --git a/cc-ci-plan/advisory-scan.py b/cc-ci-plan/advisory-scan.py index 9718ea0..bdb436a 100755 --- a/cc-ci-plan/advisory-scan.py +++ b/cc-ci-plan/advisory-scan.py @@ -353,8 +353,8 @@ def scan(recipe: str, v_from: str | None, v_to: str | None, registry_dir: str, # is classified against ITS OWN window, and the count is the union across windows. # # --from/--to → the PRIMARY app repo (first github source in the registry) - # --image NAME FROM TO → any other source whose name contains NAME (repeatable), - # e.g. --image redis 7.4 8.10 + # --image NAME=FROM:TO → any other source whose name contains NAME (repeatable), + # e.g. --image redis=7.4:8.10 # # A source with no window is not classified: its advisories are listed as unclassified so they # stay visible without inflating the count. @@ -506,14 +506,21 @@ def main() -> int: ap.add_argument("--to", dest="v_to", default=None) ap.add_argument("--json", action="store_true", help="emit raw JSON instead of markdown") ap.add_argument("--registry", default=REGISTRY_DIR) - ap.add_argument("--image", action="append", default=[], nargs=3, - metavar=("NAME", "FROM", "TO"), + ap.add_argument("--image", action="append", default=[], metavar="NAME=FROM:TO", help="a sidecar image and the versions it moved between, e.g. " - "--image redis 7.4 8.10 (repeatable). NAME matches a source repo name; " + "--image redis=7.4:8.10 (repeatable). NAME matches a source repo name; " "its advisories are then counted against ITS OWN versions instead of " "being left unclassified.") a = ap.parse_args() - rep = scan(a.recipe, a.v_from, a.v_to, a.registry, [tuple(x) for x in a.image]) + images = [] + for spec in a.image: + name, _, rng = spec.partition('=') + vf, _, vt = rng.partition(':') + if name and vf and vt: + images.append((name, vf, vt)) + else: + print(f'ignoring malformed --image {spec!r} (expected NAME=FROM:TO)', file=sys.stderr) + rep = scan(a.recipe, a.v_from, a.v_to, a.registry, images) print(json.dumps(rep, indent=2) if a.json else markdown(rep)) return 0