395 Commits
Author SHA1 Message Date
autonomic-bot ab5d11ea2a Merge pull request 'advisory-scan: NVD by CPE, so mattermost and mumble stop scanning as '?'' (#11) from feat/nvd-cpe-source into main 2026-08-11 22:17:13 +00:00
autonomic-bot 74117c2260 cve-check: record the remedy for a blind recipe, not just the symptom
The skill said to render a sourceless recipe as '?'. It now says how to stop it
being sourceless: declare an NVD CPE in the registry. That is what took the fleet
from two blind recipes to zero, and it is the first thing to try when the sweep
flags another.
2026-08-11 22:16:52 +00:00
autonomic-bot 985dc06e47 advisory-scan: NVD by CPE, so mattermost and mumble stop scanning as '?'
Two recipes could not see CVEs at all. mattermost-lts has an empty GitHub advisory
feed and renders its security bulletins client-side, so a text sweep finds nothing;
mumble publishes nothing anywhere the registry points. Both returned '?' - nothing
measured - which is honest but useless.

NVD is CPE-indexed and carries structured version ranges, so it answers where the
vendor does not. Declared per recipe as 'nvd-cpe: <image> = <cpe:2.3:...>'.

  mattermost-lts 10.5.0  -> 10.12.4   165 CVEs
  mattermost-lts 10.11.22 -> 10.12.4    0 CVEs  (measured, not unknown)
  mumble         1.3.0   -> 1.6.870      2 CVEs

Both NVD range forms are used: versionEndExcluding is a patched version;
versionEndIncluding means the fix version is unpublished but the upgrade delivers
it whenever it crosses X.

That 0 for the actual mattermost upgrade is the interesting one, and it needed a
new rule to be correct: a fix on the line you upgrade FROM was already yours.
mattermost patches every maintained line at once, so 10.11.22 -> 10.12.4 crosses
10.12.1 while 10.11.22 already had the 10.11.4 backport. Without the rule the scan
claimed 12 CVEs the upgrade did not deliver.

The rule is skipped for placeholders: '7.4.X' parses to a bare 7.4 and would read
as 'already fixed at 7.4', which silently dropped redis CVE-2024-46981 and took
discourse 140 -> 139 before I caught it.

79 tests. discourse 140 / gitea 2 / mailu 2 / keycloak 12 / plausible 6 unchanged.
Fleet sweep: 0 recipes with no usable CVE source, down from 2.
2026-08-11 22:16:37 +00:00
autonomic-bot 4b9978ac02 Merge pull request 'audit-sources --security-sources: find the recipes that cannot see CVEs at all' (#10) from feat/audit-security-sources into main 2026-08-11 20:02:24 +00:00
autonomic-bot 46ace30b4d audit-sources --security-sources: find the recipes that cannot see CVEs at all
Follow-up to the nginx blind spot. Sweeping all 22 recipes for sources whose CVEs
are USABLE (structured advisory feed, or a changelog attributable to releases)
rather than merely visible.

Before the changelog-attribution fix: 20 unusable sources. After: 5, and all five
are redundant - the same project also publishes an advisory feed (redis, gitea,
minio, clickhouse), so nothing is actually lost.

One real find, same shape as nginx: ONLYOFFICE/DocumentServer publishes NO GitHub
advisories, and the registry pointed its CHANGELOG.md at the GitHub *blob* page -
636KB of markup in which the release headings do not survive HTML-stripping, so 24
CVEs were visible and NONE attributable. The raw URL attributes all 24. Rather than
fix one registry line, advisory-scan now normalises github.com/../blob/.. to
raw.githubusercontent.com, which fixes every entry present and future.
lasuite-drive bumps documentserver, so this was live.

Genuinely blind after all that: mattermost-lts and mumble - no advisory feed, no
attributable changelog, no CVE data anywhere the registry points. mattermost is the
notable one: its bulletins are client-side rendered, so a regex sweep sees nothing.
Their scans can report 0 while nothing was measured, so /cve-check now renders those
recipes as ? and says why.

The audit output distinguishes a blind RECIPE from an unparseable PAGE, because
conflating them made 5 harmless redundancies look like 5 gaps.
2026-08-11 20:02:06 +00:00
autonomic-bot dab3edf3c2 Merge pull request 'advisory-scan: attribute vendor-changelog CVEs to the release that fixed them' (#9) from feat/changelog-version-attribution into main 2026-08-11 19:50:00 +00:00
autonomic-bot db37f1618b advisory-scan: attribute vendor-changelog CVEs to the release that fixed them
nginx publishes NO GitHub security advisories. Every nginx CVE we can see comes
from nginx.org/en/CHANGES, and the scan scraped ids out of it without attributing
them to a release - so they had no patched version, could never be classified, and
every nginx bump in the fleet reported 0 CVEs. nginx is a sidecar in most recipes,
so this was a fleet-wide blind spot.

Measured on the two PRs that prompted the question:
  lasuite-docs#7  nginx 1.31.1 -> 1.31.3   0 -> 6 CVEs
  lasuite-drive#6 nginx 1.31.2 -> 1.31.3   0 -> 3 CVEs
matching a hand count of the changelog exactly (three fixed in 1.31.2, three in
1.31.3; the narrower window correctly counts only the latter).

How: when a vendor page is organised by release, each CVE is attributed to the
nearest preceding release heading ('Changes with nginx 1.31.3', '## v1.31.3'),
and that becomes its fixed-in version. The CVE is tied to a window by the image
name appearing in the page URL (window 'nginx' <-> nginx.org/...). A changelog
lists the project's whole history, so only releases the window actually crosses
count - asserted by a test that the 2013 entries stay out.

76 tests. discourse 140 / gitea 2 / mailu 2 / keycloak 12 unchanged.
2026-08-11 19:49:43 +00:00
autonomic-bot 4bad1ea6db Merge pull request 'advisory-scan: derive the scan windows from a compose diff (--compose-to)' (#8) from feat/advisory-scan-compose into main 2026-08-11 19:24:46 +00:00
autonomic-bot ef58e33102 advisory-scan: derive windows from a compose diff (--compose-to)
Typing --from/--to/--image by hand means someone has to remember the recipe also
bumped its redis. That is how sidecar CVEs went uncounted for months. Point this
at a PR's compose.yml and it reads the windows off the diff instead.

  advisory-scan.py plausible --compose-to <.../branch/<pr>/compose.yml>
    -> community-edition: v2.0.0 -> v3.2.1
    -> clickhouse-server: 23.4.2.11-alpine -> 24.12-alpine
    -> 6 CVEs, identical to the hand-specified args

Details that mattered:

- keyed by SERVICE, not image repo. plausible moved plausible/analytics ->
  ghcr.io/plausible/community-edition; keyed by repo that reads as one image
  vanishing and an unrelated one appearing, and the app window - the one carrying
  the critical - is lost entirely.
- the baseline is the repo's DEFAULT BRANCH resolved from the API, never assumed
  to be main, because several recipes keep a stale main beside a live master.
- image names are matched against advisory sources BOTH ways: an image name is
  often longer than its source repo (clickhouse/clickhouse-server vs
  ClickHouse/ClickHouse) and sometimes shorter (redis vs redis/redis). One
  direction silently dropped the clickhouse window.
- credentials go in an Authorization header, never the URL: in-URL creds leak
  into shell history and process lists, and urllib mis-parses a password
  containing a colon.

--from/--to/--image remain for finer-grained checks (scanning a window that is
not a literal compose diff). 71 tests; discourse 140 / gitea 2 / mailu 2
unchanged.
2026-08-11 19:24:24 +00:00
autonomic-bot c352ea9058 Merge pull request 'add /cc-ci-cleanup — reconcile, close dead PRs, report what blocks the rest' (#7) from skill/cc-ci-cleanup into main 2026-08-11 19:17:19 +00:00
autonomic-bot fae2fbe21b add /cc-ci-cleanup — reconcile, close dead PRs, report what actually blocks the rest
25 open recipe PRs had accumulated, and the list had stopped being readable: CI
sweep artifacts that were never meant to merge sat next to genuine CVE fixes, and
three PRs the operator had been told were outstanding were in fact already merged
upstream (discourse #6 with 140 CVEs, keycloak #6 with 12, n8n #5) — visible only
once the mirrors were reconciled.

The skill: reconcile every mirror from true upstream FIRST (that step alone closed
those three), survey every open PR deterministically, close the ones that cannot
merge or were never meant to, and report prioritised action items — CVE-carrying
first — for the ones that should land. It never merges a recipe PR.

pr-survey.py gathers the facts and decides nothing: behind_main, mergeable,
diff_files, which images the PR adds vs which are already pinned in main, the
newest !testme verdict, branch kind, age.

One correctness detail worth the extra state: a FAILED diff fetch is reported as
unknown, never as an empty diff. gitea #4 reads that way (force-pushed branch)
while being a verified green fix, and 'empty diff' is a close signal — so the
tool says DIFF-UNREADABLE(do not close on this) instead.
2026-08-11 19:17:00 +00:00
autonomic-bot 6ebc35bc18 Merge pull request 'AGENTS.md: ship infra work as PRs, self-merge, operator reviews retrospectively' (#6) from policy/pr-then-merge into main 2026-08-11 19:08:55 +00:00
autonomic-bot 96c536f543 AGENTS.md: ship infra work as PRs, self-merge, operator reviews retrospectively
Operator policy (2026-08-11). For cc-ci-orchestrator and cc-ci: branch, open a PR
whose description is written to be read AFTER the fact, merge it yourself once
verified, and let the operator review retrospectively. The PR is not a gate — it
is how the work stays legible — so a description that says 'fix scanner' has
failed at its only job.

Explicitly does NOT extend to recipe repos: those are created and verified but
never agent-merged, because they change what deploys on other people's
infrastructure.

Also records what to do when work has already landed on main without a PR: pin a
branch at the pre-work commit and PR against that, rather than rewriting
published history.
2026-08-11 19:08:39 +00:00
autonomic-bot 94ce5c4de2 cc-ci-status: correct the ENOSPC note — inode recreation was coincidence
I recorded that recreating the runs directory with a fresh inode preceded
recovery. It recurred afterwards (build 1252), so that was not the fix.

The real signal is that it is INTERMITTENT and tracks concurrent activity: every
failure landed while a second run or manual deploy was in flight, and every build
on a quiet host passed (1243, 1250, 1251, 1253). Free space never moves during a
failing build. Practical guidance is therefore to wait for the host to go quiet
and re-trigger before calling it a recipe failure, and DRONE_RUNNER_CAPACITY=2 is
the obvious knob to test if it becomes disruptive.

Root cause still not established, and the note now says so plainly rather than
presenting a coincidence as understood.
2026-08-11 18:54:58 +00:00
autonomic-bot bb7ebb4a27 reconcile-upstream.sh: one deterministic entry point, mandated before PR work
Working against a stale mirror has cost us three different ways:

- mailu #6 was linked as the fix for two internet-facing Roundcube CVEs while
  upstream had already merged AND released it (3.1.3+2024.06.57). The work was
  done; only our mirror was behind. Reconciling closed the PR automatically.
- a stale mirror makes a survey report 'no upgrades available', so the recipe
  silently drops out of the weekly run.
- reading the wrong branch: several coopcloud recipes keep a stale 'main' beside
  the real default 'master'. gitea's main is 1.24.2-rootless while master has
  1.27.1-rootless and the merged PRs, so reading main manufactures a false
  'three releases behind, missing two CVSS-9.8 RCEs' finding.

The reconcile logic already existed inside open-recipe-pr.sh --reconcile-only and
already resolves the default branch itself. What was missing was a single obvious
entry point and a rule saying to run it. reconcile-upstream.sh takes recipes or
--all, and is idempotent — recipe work lives in branches, never on mirror main, so
force-syncing main discards nothing.

/ci-test-review and /cc-ci-tests-update had NO reconcile step at all; both now
require it. /cve-check, /recipe-upgrade and /upgrade-all already reconciled and now
point at the shared script.
2026-08-11 18:38:09 +00:00
autonomic-bot ecf126d98d cc-ci-status: record the ENOSPC-with-free-disk failure and what recovered it
Builds 1244-1249 died on mkdir of the run dir with 110GB free and 16% inodes.
Ruled out: actual disk (df sampled every 2s across a failing build never moved),
inodes, quotas, a poisoned parent directory (61/61 stress creations succeeded),
runner sandboxing (namespaces identical to the host), and a wedged runner
(restart changed nothing). The same harness with the same numeric run id, run by
hand outside drone, worked every time.

Recreating the runs directory with a fresh inode preceded recovery; builds have
run normally since. The root cause is NOT established, so the note says so rather
than presenting a fix that might be coincidence.
2026-08-11 17:39:56 +00:00
autonomic-bot ab88e59c21 cc-ci: prune unused images in the sweep; catch a starving host before CI dies
The CI server filled up and every !testme from build 1236 to 1242 died at harness
startup with ENOSPC on /var/lib/cc-ci-runs/<build>. Because the harness never got
far enough to write results.json, the PR badges just said 'failure' — so it read
as recipe regressions, and plausible's genuinely-fixed suite looked still-broken.

Cause: every run pulls each recipe's images and nothing ever removed the old ones.
72GB of images, 63GB of it unused. Reclaimed 69.8GB; the host went 73% -> 22%.

Two changes so it does not recur:

- sweep-orphans.sh (runs at the start AND end of every /upgrade-all) now prunes
  unused images when the disk is >=60% (DISK_PRUNE_PCT). Below that it keeps the
  layer cache so runs stay fast. 'docker image prune -a' spares anything a container
  references, so infra and warm-* canonicals are safe. Volumes are still NOT
  pruned — warm-* canonical volumes are data-warm and legitimately dangling.

- /cc-ci-status flags server disk at >65% rather than >80%, because this is not a
  steady-state measure: the host was at 73% when runs started failing. It also now
  checks that recent builds actually produced results.json — an empty run dir is
  the fingerprint of a host problem masquerading as a recipe failure — and records
  how to read a drone step log out of its sqlite when the API token is unreachable.
2026-08-11 15:41:40 +00:00
autonomic-bot e89da2d842 audit-sources: check we are still looking where releases actually happen
A recipe tracks an image repo and a set of registry URLs. When upstream moves,
nothing errors — the old repo just stops receiving tags and the recipe looks
'up to date' forever. plausible is the case: it tracked plausible/analytics on
Docker Hub while upstream moved to ghcr.io/plausible/community-edition. Every
survey said 'no upgrades available' while v3 shipped elsewhere.

audit-sources.py reports the signals that catch it, per image and per registry
URL: image gone quiet (newest tag older than --quiet-days), deprecation wording
in the registry description, and GitHub repos that are archived, renamed or
gone. Signals, not verdicts — a stable image can be quiet for good reason — so
each finding says what was measured.

First run over 22 recipes, 11 findings, 4 alerts. It independently re-derived
the plausible case (analytics quiet 1126 days), and found:
  - drone: harness/drone now answers as harness/harness (the image is fine)
  - lasuite-docs, lasuite-drive: minio/minio is ARCHIVED on GitHub
  - lasuite-docs: docspecio/api is ARCHIVED
  - matrix-synapse: halfshot/matrix-appservice-discord image quiet 2078 days
  - mumble: NO cc-ci-plan/upstream/mumble.md at all

That last one exposed a scanner bug. With no registry file there is no source to
query, yet the scan still printed '0 identified by the deterministic scan' — and
that 0 was published as a clean count in the 2026-08-11 CVE check. A scan with no
usable source has measured nothing and must not report a number, least of all 0.
It now returns UNKNOWN and says the registry file is missing.

upstream/mumble.md added; mumble now scans 6 sources for a genuine 0.
2026-08-11 15:02:06 +00:00
autonomic-bot 6c91373357 skills: point every test-editing path at tests/STYLE.md
/recipe-upgrade --with-tests, /ci-test-review and /cc-ci-tests-update all author
test changes, and all three had only 'never weaken a test' as guidance. That did
not stop the plausible failure: the fixture INSERTed rows into the app's database,
which was correct for v2 and silently wrong for v3, where a site must belong to a
team. Events were acked 202 and discarded; the recipe sat RED for six weeks.

The rule that would have prevented it — set state up through the app's own
interface, not its database — now lives in tests/STYLE.md in the cc-ci repo, and
each of the three paths is told to read it before editing a test.
2026-08-11 14:41:10 +00:00
autonomic-bot 1db85a7e77 resolve-images: abra-independent version resolver; fix release-line over-count
immich pins two images with BOTH a tag and a digest, which makes abra FATA and
abandon the WHOLE recipe. It therefore contributed no version data at all and
silently dropped out of every survey — indistinguishable from 'up to date'. The
standing answer was prose in three skills telling an agent to check registries by
hand. This replaces it with a tool.

resolve-images.py reads the compose files and queries registries itself:
  - Docker Hub, ghcr, and any OCI registry via its own auth challenge (lscr.io
    and dock.mau.dev advertise different realms; assuming ghcr's shape 401'd).
  - tag SHAPES (digits -> '#') so -alpine stays on -alpine and 'latest' is never
    proposed as an upgrade.
  - reports newest_within_major AND newest_same_shape, and refuses to choose:
    immich's postgres tag encodes the pg major plus the vectorchord/pgvectors
    build immich-server expects, so taking the newest breaks the deploy.
  - integrity check: if the CURRENT pin is absent from the listing, the listing
    was truncated and any 'newest' is a guess. ghcr caps out past 40k tags, so
    that falls back to the project's GitHub releases.
  - per-repo cache + backoff + Docker Hub auth: a fleet sweep re-reads nginx,
    redis and postgres many times and was getting 429s reported as 'unresolved'.

21/21 recipes now resolve. It found upgrades abra missed entirely in five:
mumble (abra said 'no new versions'; four patches behind), plausible's
clickhouse, lasuite-drive's collabora, gitea's mariadb, immich's postgres.
plausible's carried four CVEs, three high.

Also fixes a real over-count found while validating that: a fix inside the
numeric window is not a fix on the branch you land on. ClickHouse patched
CVE-2023-48704 in 23.9.6.20 AND 23.10.5.20 — landing on 23.10.4.25 crosses the
23.9 fix but sits below its own line's, so it does NOT have it. A fix named on
the target's own line and above the target is now proof of absence.

70 tests (64 offline + 6 live). keycloak's live expectation moves 7 -> 12 and
mailu's 0 -> 2: both are the release-note source finding real fixes that were
never filed as advisories.
2026-08-11 04:56:39 +00:00
autonomic-bot 8df32edfcf cve-check: reconcile is mandatory; do not assume the default branch is main
Skipping the reconcile to keep the sweep 'read-only' was wrong. It researches a
stale checkout, and on the first real run left two recipes with no survey output
at all — indistinguishable from 'no upgrades available' unless you look. The
reconcile is safe precisely because recipe work lives in branches, not on main.

Also documents the trap that produced a false finding in that run: several
coopcloud recipes keep a stale 'main' beside the real default 'master'. gitea's
main is at 1.24.2-rootless while master has 1.27.1-rootless and the merged PRs,
so reading main reports a recipe three releases behind and missing two CVSS-9.8
RCE fixes. Resolve default_branch from the API before reading any file.

And: no output is not 'no upgrade'. It is a third outcome, and only becomes '?'
after the direct registry check has also failed.
2026-08-11 04:32:49 +00:00
autonomic-bot 18caf047bf advisory-scan: two more cases decided in pass 1, found by the first real /cve-check
1. Release-note resolution now covers vendor pages on the same repo. It required
   a github-advisories: source, so mailu's Roundcube CVEs — announced only on
   github.com/Mailu/Mailu/releases — went to pass 2 even though the answer was
   sitting in the release notes. mailu now reports 2 deterministically, matching
   what previously took an agent reading the notes.

2. 'All known fix versions predate the version we were on' is now a DECISION,
   not an unknown. mailu's redis 8.8.0 -> 8.10.0 crosses 12 advisories all fixed
   by 8.6.3 or earlier; reporting them as 'could not judge' overstated the
   uncertainty. Recorded as outside-window with the naming tags as evidence.
   A fix landing ABOVE the window still stays indeterminate on purpose: that is
   an open vulnerability and must stay visible.

60 offline tests (was 58). discourse 140 / gitea 2 unchanged.
2026-08-11 04:24:15 +00:00
autonomic-bot b0bdce2c15 add /cve-check and /cve-check-and-upgrade
/cve-check answers 'what are we exposed to that an upgrade would fix?' without
running an upgrade: per-recipe, resolve the available window for EVERY image
(sidecars included), run the advisory scan over it, adjudicate whatever pass 1
could not decide, publish a report. Read-only — no PRs, no CI, no merges.

/cve-check-and-upgrade does that sweep, then runs /recipe-upgrade only on the
recipes whose upgrade actually closes a CVE, worst severity first, and reports
on both. --min-severity high for just the urgent ones; --dry-run prints the
queue and stops. Never merges.

Deliberate choices, each written into the skills:
- externals are SWEPT but never upgraded here — a security sweep that skipped
  deployed software would misreport exposure, but we don't maintain them.
- an unknown count never justifies an upgrade AND is never treated as clean;
  it goes to the Addendum.
- no upgrade available means 0 CVEs, not '?'.
- subagents are told which CVEs justify their upgrade, so the PR says why it
  exists — a PR naming the RCE it closes gets reviewed sooner.

recipe-report.py grows a page kind: 'cve' files as cve-DATE.html so a sweep
can't overwrite a weekly edition, while BOTH appear in the same archive index,
suffixed 'full report' / 'CVE check'.

/help and /cc-ci-status updated to route to them.
2026-08-11 04:04:59 +00:00
autonomic-bot 44cb9b6704 advisory-scan: tests, audit, and two real undercounts they found
Adds test-advisory-scan.py (58 offline tests on fixtures + 6 live regressions
against the week-2026-08-07 report) and audit-advisory-scan.py, which re-derives
every count with a SEPARATE semver implementation and its own release fetch and
diffs against the scanner. Both found real defects:

1. Window membership was compared on ragged tuples, so (18,) < (18,0) — a CVE
   patched in 18.0 fell OUTSIDE a window ending at 18. Bare major tags are the
   norm for sidecars (postgres:18, redis:8-alpine). Now zero-padded, which also
   keeps the upper bound conservative (18.5 stays out of a window ending at 18).

2. Advisories with no knowable fix version were silently counted as 'not fixed'.
   Twelve redis advisories say patched_versions 'TBD' or '7.4.X' with an
   open-ended range — six of them high severity. They are now INDETERMINATE:
   not counted, not dismissed, and surfaced in the output.

   All twelve turned out to be genuinely fixed: redis names each in the release
   notes of every branch that got the fix (CVE-2025-32023 -> 6.2.19, 7.2.10,
   7.4.5, 8.0.3, 8.2.0). So a third deterministic method resolves them from
   release notes, with the naming tags recorded as the citation. discourse's
   redis contribution goes 5 -> 17, and its total 128 -> 140.

Pass 2 (--adjudicate) is the model-judged stage for what arithmetic cannot
settle: it hands over each open case's full evidence, plus every verdict pass 1
reached, and takes FIXED/NOT-FIXED/STILL-UNKNOWN with a reason citing that
evidence. It may only raise a count. Vendor-page-only CVEs — the shape of both
gitea CVSS-9.8 RCEs — now reach it instead of being dropped.

Tests cover pass 1 only, by design; pass 2's judgement is a model's. What is
tested there is deterministic: which cases it selects, and that truncation is
announced rather than silent.

SPEC.md rewritten around the two passes.
2026-08-11 01:29:11 +00:00
autonomic-bot 46c4fff1a6 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.
2026-08-11 00:52:40 +00:00
autonomic-bot 65bf3c095b advisory-scan: --image NAME FROM TO replaces --window KEY=FROM:TO
The image name was packed into the value, so the flag needed a hand-rolled
KEY=FROM:TO parser with its own malformed-input branch, and 'window' named the
wrong thing — the tool has two kinds of window (version ranges and, on the date
fallback, real date windows) and the flag meant only the first.

Now each part is its own argument: --image redis 7.4 8.10, repeatable, all in
one call. argparse enforces the arity, so the string parsing and its error path
are deleted. 'windows' survives internally as the computed-range concept.

Counts unchanged: discourse 128 with redis / 123 without, gitea 2.
2026-08-11 00:51:12 +00:00
autonomic-bot 8d7320f32e recipe-report: scheme changes and sidecars no longer mean '?'
The skill still told the reporter to publish '?' whenever the scan hit a
version-scheme change. The scan now resolves those by advisory publish date, so
that instruction would have re-introduced a '?' for a count it can determine.

Also documents that counts are a union across per-image windows, and that a
sidecar-sourced critical must name its image in the bulletin.
2026-08-10 22:06:38 +00:00
autonomic-bot b5f8543a9b 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.
2026-08-10 22:01:28 +00:00
autonomic-bot 78ae2be8ae docs: spec for the advisory scanner's CVE detection
Step-by-step specification of cc-ci-plan/advisory-scan.py: inputs, the three source
classes and why each is ranked where it is, the union, both classification paths
(patched-version ranges, and the advisory-publish-date fallback for version-scheme
changes), the output contract, and how /recipe-report must read it.

Each rule records the production wrong answer that motivated it — the false 133 from
cross-image counting, the n8n misclassification from reading only vulnerabilities[0],
the '?' sprawl from url punctuation and benign-404s, and the 'never emit 0 for an
undetermined count' rule. Claims cross-checked against the implementation.
Keep this file in the same commit as any behaviour change.
2026-08-10 21:55:37 +00:00
autonomic-bot 98a624a13a advisory-scan: paginate, and count by advisory DATE when versions can't be ordered
Answers 'how can the weekly run produce counts like the hand count?' — by doing
exactly what the hand count did, deterministically. Two changes:

1. PAGINATION. The scanner requested per_page=100 and stopped. This endpoint caps at
   100 AND ignores ?page= (it re-returns the same rows — which is how a manual count
   first produced exact triplicates and a bogus 300). Busy projects were silently
   truncated: discourse has 286 advisories, so a single page could not see the window
   at all. Now follows the Link rel=next cursor to exhaustion.

2. DATE-BASED FALLBACK. Version strings cannot be ordered across a scheme change
   (discourse semver 3.5.3 -> calver 2026.7.1), which is why the scan first reported a
   false 133, then correctly refused. Release DATES always order. When the version path
   refuses, the scan now resolves both versions to their git tag dates on the primary
   repo and counts advisories PUBLISHED in that window, labelling the method in the
   output. The version path is still preferred when usable — it is exact rather than
   temporal.

Verified: discourse 3.5.3 -> 2026.7.1 now reports 123, matching the hand count
(1 critical, 16 high, 91 medium, 16 low; window 2025-12-30 -> 2026-07-31); gitea
1.27.0 -> 1.27.1 still reports 2 via the version path.
2026-08-10 21:02:39 +00:00
autonomic-bot fc36d0e10f advisory-scan: report UNKNOWN, never 0, when a count could not be determined
Operator: 'the scanner should not say 0 when it was not able to scan.' Correct — the
previous patch still led with '0 identified' and relegated the caveat to a footnote,
so the headline number was wrong even though the prose was right. A 0 in a security
column is an assertion of safety; it must never be emitted for an undetermined result.

Now: cve_count_fixed is null (not 0) in JSON, a count_known flag distinguishes
'counted zero' from 'could not count', and the markdown headline reads
'CVEs fixed by this upgrade: UNKNOWN — the scan could NOT determine a count' with an
explicit 'This is NOT zero' and instructions to render '?'.

Verified: discourse 3.5.3 -> 2026.7.1 (semver->calver) now reports UNKNOWN; gitea
1.27.0 -> 1.27.1 still reports 2.
2026-08-10 20:52:51 +00:00
autonomic-bot 1daf0fa616 advisory-scan: stop cross-image and cross-scheme miscounting (discourse's false 133)
Operator disbelieved discourse's '133 CVEs fixed' — correctly. Two defects made it
confidently wrong:

1. ONE WINDOW APPLIED TO EVERY IMAGE. The scan queries all source repos in the
   recipe's registry (app + redis/postgres/nginx sidecars) but judged them all with
   the APP's version window. 34 of the 133 were redis advisories, including
   CVE-2021-21309 — patched in redis 6.0.11 back in 2021 — scored as 'fixed by this
   upgrade' purely because 6.0.11 sits numerically inside discourse's 3.5.3 ->
   2026.7.1 range. Only the PRIMARY app repo is now classified; other sources are
   reported as unclassified so they stay visible without inflating the count.

2. VERSION-SCHEME CHANGES BREAK ORDERING. discourse moved semver -> calver
   (3.5.3 -> 2026.7.1), so 2025.12.2 compares 'newer' than 3.5.3 while shipping
   earlier. Numeric comparison cannot order that. The scan now detects a leading-
   component jump >= 100, refuses to classify, and says so in the block: the count
   is '0 by refusal, not by evidence — read the vendor's release notes'.

Refusing to answer beats answering wrongly: a fabricated 133 in a public security
report is worse than an explicit 'cannot determine'.

Verified after the fix: discourse 133 -> 0 (with the refusal caveat), gitea still
exactly 2 (both criticals, patched 1.27.1), keycloak 7 all genuinely from
keycloak/keycloak patched in 26.7.1, plausible 1. No other count changed.
2026-08-10 20:44:27 +00:00
autonomic-bot 318d09bdab advisory-scan: eliminate spurious '?' — url punctuation, stale URL, and ? semantics
The 2026-08-07 regeneration rendered '?' for 5 of 21 recipes. '?' is meant to be a
rare 'we tried and could not tell'; at that rate it is indistinguishable from noise
and hides the real unknowns. Three causes, none of them genuine uncertainty:

1. URL EXTRACTION BUG (mine). The registry is markdown, so urls appear inside
   `backticks` and 'quotes'. The extractor captured the trailing punctuation, so
   it fetched https://docs.n8n.io/release-notes/` and https://git.autonomic.zone'`
   — both 404 on the malformed url, both 200 when clean. Trailing markdown
   punctuation is now stripped. Fixed immich + n8n.
2. STALE REGISTRY URL. mattermost-lts pointed at
   docs.mattermost.com/about/mattermost-changelog.html, which 404s; the page moved
   to /deploy/. Corrected (same class as the pgautoupgrade fix).
3. WRONG SEMANTICS FOR 'NO UPGRADE'. lasuite-docs and custom-html-tiny were
   up-to-date this run, so no scan block existed and the report fell back to '?'.
   But a recipe with no upgrade has nothing an upgrade could have fixed — that is
   0, not unknown. The report skill now says so explicitly, restricts '?' to scans
   that RAN and reported genuinely failed sources, states that benign notes
   (no-advisories-published / template URL) never trigger '?', and instructs that
   many '?' is itself a bug to raise in the Addendum.

Result across all 16 scanned recipes of that run: 0 failed sources (was 5).
Counts also improved with the classifier fix: discourse 130->133, keycloak ->7.
2026-08-10 20:23:10 +00:00
autonomic-bot 3e59924450 advisory-scan: fix version classification (multi-line patches + range expressions)
Exposed by asking whether the scan catches the n8n CVEs (CVE-2026-42231/42232). It
did not — the advisories were fetched correctly but both misclassified as
out-of-window. Two bugs:

1. Only vulnerabilities[0] was read. An advisory carries ONE ENTRY PER PATCHED
   RELEASE LINE: n8n patches three (1.123.32, 2.17.4, 2.18.1), so whichever line
   the deployment is actually on was silently dropped. gitea passed only because it
   patches a single line. Now all entries are kept.
2. patched_versions is a RANGE EXPRESSION ('>= 2.18.1'), not a bare version. Naive
   parsing produced (18,1) instead of (2,18,1), so no comparison could ever match.
   Version tokens are now extracted with a regex and the advisory counts as
   fixed-by-this-upgrade if ANY patched line falls in (from, to].

Verified: n8n 2.17.0 -> 2.18.1 now reports 12 CVEs including both criticals
(CVE-2026-42231 GHSA-q5f4-99jv-pgg5, CVE-2026-42232); gitea 1.27.0 -> 1.27.1 still
reports exactly 2. Note our deployed n8n (2.27.2+) is already past all three patched
lines, so these were never outstanding for us — the bug was in detection, not
exposure.
2026-08-10 18:50:07 +00:00
autonomic-bot 3307bdb0fe advisory-scan: separate benign source absence from real failures; fix pgautoupgrade URLs
Two refinements found by running the scan across all 14 recipes of the 2026-08-07 run:

1. A repo with no advisory feed returns HTTP 404 on /security-advisories (e.g. the
   pgautoupgrade sidecar image). That is a BENIGN ABSENCE, not a failed check.
   Likewise registry entries that are TEMPLATE urls for humans
   (…/changelog/v<VERSION>/, …/<vX.Y.Z>/…) are documentation, not fetchable.
   Counting either as a failure pushed most recipes to '?', which would make the
   unknown-vs-clean distinction meaningless again — the exact signal the ? exists to
   preserve. Both are now recorded in sources_benign; only genuine errors (rate
   limit, network, 5xx, wrong URL) land in sources_failed.

2. upstream/*.md pointed at github.com/pgautoupgrade/pgautoupgrade, which 404s —
   the repo is pgautoupgrade/docker-pgautoupgrade. Corrected in n8n, lasuite-docs,
   lasuite-drive, lasuite-meet. A 404ing registry URL means we were not scanning a
   source we believed we were.

Effect on the 2026-08-07 data: recipes with genuine failed sources 5 -> 3 (the
remainder are really unreachable vendor pages). CVE counts unchanged where they
were already sound: discourse 130, gitea 2, plausible 1.
2026-08-10 18:45:45 +00:00
autonomic-bot cf26ef863a advisory-scan: read-only GitHub token from env or file (rate limit only)
Anonymous GitHub API is 60 req/hr — a full weekly sweep across ~20 recipes exhausts
it and the scan then reports sources as failed (visible, but degraded coverage). A
token lifts it to 5000/hr.

_github_token(): GITHUB_TOKEN env wins, else GITHUB_TOKEN_FILE (default
/srv/cc-ci/.github-token, 0600, gitignored). Reading PUBLIC advisories needs NO
scopes — a classic PAT with nothing ticked, or fine-grained limited to 'Public
repositories: read'. The tool only ever GETs advisories; do not grant write scopes.
A missing token is not an error: the scan runs anonymously and surfaces failures.

Also gitignores .github-token and .hcloud-token.
2026-08-10 18:38:45 +00:00
autonomic-bot 5775fe23f8 security: deterministic advisory scan as an ADDITIVE pre-step
Why: gitea 1.27.1 fixed CVE-2026-60004 + CVE-2026-59774 (both CVSS 9.8). The
2026-08-03 report printed gitea's CVE count as '1', the 2026-08-07 report as
'none'. Cause chain: the upgrade subagent read the GitHub release notes, which
name NEITHER cve (they are announced only in the vendor blog's security section),
so it recorded one unrelated minor item; the report then derived security content
from those notes plus model knowledge, and the model's training predates the CVEs.
Nothing in the pipeline ever queried an advisory source.

cc-ci-plan/advisory-scan.py — deterministic, per recipe, per upgrade window:
  1. GitHub Security Advisories API for every source repo in the upstream registry.
     PRIMARY: CVE + GHSA + severity + vulnerable/patched ranges, so 'fixed by THIS
     upgrade' is computed. Needs no new per-recipe config (134 registry URLs are
     already github.com).
  2. Vendor release/security pages — every registry URL, fetched + regex-scanned.
     This is the source that actually had the gitea CVEs.
  3. OSV where a package mapping exists — supplementary.
Each source reports its own status so 'checked, none found' is never confused with
'not checked'. Source selection was measured, not assumed: for these two CVEs OSV
404'd and NVD's API had them by neither CPE, id, nor keyword — advisory DBs lag the
vendor, hence 1+2 lead.

Wiring is strictly ADDITIVE:
- /recipe-upgrade gains step 2a: run the scan, paste the block into the per-recipe
  log, and report the UNION of it and the existing release-note reading. The scan
  may never lower a count established by reading.
- /recipe-report treats the block as a FURTHER source, prefers its advisory ids /
  severities / fixed-in versions for citation, and must render '?' (not 'none')
  when a scan is absent or has failed sources — the false-clean 'none' is exactly
  what happened on 2026-08-07.
- upstream/gitea.md records blog.gitea.com as the security-announcement URL.

Verified on the real regression: 1.27.0 -> 1.27.1 now yields exactly the 2 missed
criticals with their GHSA ids; the wider 1.26.2 -> 1.27.1 window yields 62.
2026-08-10 18:25:58 +00:00
autonomic-bot 02dbd71b49 recipe-upgrade: stop upgrade-PR branches drifting behind upstream
The extend path grafts HEAD^{tree} WHOLESALE onto the existing upgrade-* branch
(commit-tree -p <branch tip>). Reconcile force-syncs the MIRROR's main to upstream
but never brought the branch — or the local checkout — forward, so each week the
PR base drifted further back and upstream changes made since the branch was cut
were silently absent from the pushed tree. CI then verified a tree that would
never deploy.

Found on gitea PR #5 (2026-08-10): base 0ab323d predated upstream's
'BREAKING CHANGE: remove forgejo' (37ebd22), so the 1.27.1 bump fixing
CVE-2026-60004 + CVE-2026-59774 was !testme-GREEN against a forgejo-bearing tree.

Two changes:
1. Before pushing, if the local work does not contain the freshly-synced upstream
   main, merge upstream in — and FAIL LOUDLY (exit 1, naming the checkout) if that
   cannot auto-merge, rather than pushing a tree that omits upstream changes.
2. The extend commit now also parents on upstream main when the branch predated it,
   so the recorded history matches the pushed tree. Without it the merge-base stays
   stale and a later merge can REVERT upstream's changes. Still no force-push.

Verified against the real gitea drift: detection fires, merge is clean, resulting
tree keeps forgejo removed AND the 1.27.1 pin, history contains upstream.
2026-08-10 16:24:37 +00:00
autonomic-bot 8f85a238cc journal: 2026-08-07 run finished; supervisor-gate, report-pin, and subagent-model bugs fixed 2026-08-10 15:57:25 +00:00
autonomic-bot 80008da80d opencode: drop the misplaced project config
It lived in the orchestrator repo (the parent session's project) and so never
governed the task-tool subagents, which resolve their parent session's directory —
for launcher-started runs that is /srv/cc-ci-orch/cc-ci. The real config now lives
there (cc-ci repo, operator-approved). Extension was never the problem: .jsonc
parsed and resolved fine; the LOCATION was wrong.
2026-08-10 15:56:55 +00:00
autonomic-bot 242a6d9659 report: clear+re-pin the session id at launch (fixes un-watchdogged report runs)
Regression from the 2026-08-04 session-pinning work: lu._session_id() prefers the
pin file, but launch-report.py never cleared or re-established it. A surviving pin
from a PREVIOUS report run points at a session whose last message already carries
RECIPE REPORT COMPLETE, so the shared watchdog evaluates _completed()=True and
exits within one poll ('run completed — exiting'), leaving the CURRENT run
unwatched. Observed live: the 2026-08-07 finish-run's report step was watched by a
watchdog that quit after 3 minutes against an Aug-4 pin, then the report session
ended early with nothing to resume it.

start() now archives stale titles, clears the pin, snapshots existing ids, and
re-pins the new session after launch — the same contract launch-upgrader.start()
already follows. Scopes the shared helpers via UPGRADER_SESSION=<report session>.
2026-08-10 15:41:48 +00:00
autonomic-bot d441c6caaf supervisor: fix 3-day progress-gate deadlock (2026-08-07 run)
Root cause chain, all confirmed on the live system:
1. _run_pids() substring-matched the WHOLE cmdline for the session name. An agent's
   kickoff PROMPT is an argv element, and the supervisor's prompt text contains the
   literal 'cc-ci-upgrader' — so the supervisor's OWN billing-hung agent matched as a
   live upgrader run. Now matches FLAG VALUES only (--title <SESSION> / -s <sid>).
2. The gate treated 'a live proc exists' as progress. A provider-walled run keeps its
   process alive and SPINNING while emitting nothing (verified: 3 days, zero session
   output, still burning CPU). Progress now REQUIRES the session tree to have advanced
   within STALL_MIN; a live-but-idle proc is explicitly logged as stalled.
Result was ~60 consecutive false 'run progressing — leaving it' no-ops while the weekly
run sat unfinished and unreported.

Billing-walled runs are REPORTED, never killed (operator policy 2026-08-10): they may
resume when the wall lifts and their context is the run's state. New _billing_blocked()
detects the wall from the log tail and the gate surfaces
'run BLOCKED on a provider billing/usage wall — NOT killing; operator action required'.

Verified live: _run_pids no longer matches the hung Aug-7 supervisor (1497561) while
still matching the real finisher; gate now reasons 'session advanced Nm ago'.
2026-08-10 15:17:02 +00:00
autonomic-bot d101147b93 upstream(plausible): note v3.2.1 ships with clickhouse 24.12-alpine 2026-08-07 05:14:27 +00:00
autonomic-bot fb1dc7af9c upstream(mattermost-lts): 2026-08-07 re-check (11.10.0 pre-release; ESR=11.7.8) 2026-08-07 04:47:40 +00:00
autonomic-bot 0347511a84 upstream(n8n): release-notes sources 2026-08-07 04:44:40 +00:00
autonomic-bot f8888b2082 upstream(lasuite-drive): fix collabora release-notes URL + note minio AIStor move 2026-08-07 04:21:19 +00:00
autonomic-bot a56734de0b upstream(immich): 2026-08-07 — upstream main at v3.1.0; runner clone-token stale 2026-08-07 03:51:02 +00:00
autonomic-bot 5424954b3f upstream(gitea): note 1.27.1 patch release 2026-08-07 03:15:22 +00:00
autonomic-bot 51b067770c upstream(discourse): re-confirm pg18 newest (2026-08-07) 2026-08-07 02:53:05 +00:00
autonomic-bot 34d62fa049 journal: three pending weekly-run PRs unblocked (keycloak/mailu re-verified, discourse basefloor fix merged) 2026-08-04 17:58:02 +00:00