diff --git a/CHANGELOG.md b/CHANGELOG.md index 0688c197..bc2f9965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,13 @@ hub answers 400 to both and nothing else changes. down, a changed reason, the wire reason, the wiring in main, the record across a reload. Red-proved with an inert `Tick` (all six behaviour tests failed) and with the scheduler line commented out (the wiring test failed). +- **R-1303 (tooling, no runtime change): every pin names where its security notices are published.** `internal/infra/ + upstream.txt` beside `infra.go` (one line per `*Image` const), and a `# upstream:` comment directly above every + `FROM` (`Dockerfile`, `infra-images/samba/Dockerfile`) and `ARG RESTIC_VERSION`. Gate `upstream` (`--fast`, SHARED: + felhom.eu `scripts/security_watch_inventory.py --gate=controller`) refuses a pin with no line; decoys in + `scripts/test_gate_decoys.py` (a new const with no line, the line only in a comment, the comment one line too high, + a new FROM with none), red-proved. The nightly security watch (felhom.eu `scripts/security_watch.py`) reads them. + ## v0.345.1 (2026-10-26) — SECURITY: cloudflared 2026.10.3 (Cloudflare's eMudhra notice) — built from v0.345.0 + one commit (R-1300) **MinAgent: 0.158.1** (unchanged). **This entry on `main` records the same change as the tag `v0.345.1`** (the v0.345.0 diff --git a/controller/Dockerfile b/controller/Dockerfile index 3f7ac988..d3dced7f 100644 --- a/controller/Dockerfile +++ b/controller/Dockerfile @@ -7,6 +7,7 @@ # --- Build stage --- # R-972 (v0.322.0): a supported, patched Go, pinned by version AND digest (no moving tag). Go 1.24 had no security fixes # left (go.dev/doc/devel/release#policy). The builder's Debian does not reach the runtime (a static CGO-free binary). +# upstream: osv:Go/stdlib FROM golang:1.27.3-trixie@sha256:fbb7ae40b62ff8afe31662df570d39b4f5052ac32b90dae8e60d7657a20a74f4 AS builder ARG TARGETOS=linux @@ -47,8 +48,10 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \ # in third_party/restic/, fingerprint CF8F 18F2 8445 7597 3F79 D4E1 91A6 868B D3F7 A907 — no keyserver at build # time); the download matches the published sum; and it matches the sum pinned HERE, so even a re-signed release # cannot swap the binary. Pinned by TestR1105_ResticIsPinnedOutsideTheOS. +# upstream: gap:Debian announces package fixes as DSAs on security-tracker.debian.org, which the watch does not read; BEHIND on the point release is the signal FROM debian:13.7-slim@sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f AS restic ARG TARGETARCH +# upstream: github:restic/restic ARG RESTIC_VERSION=0.19.1 ARG RESTIC_SHA256_AMD64=f415415624dcc452f2a02b8c33641791a8c6d6d3b65bbb3543fcf9a25151585c ARG RESTIC_SHA256_ARM64=a5f64aaab53d51e311fa3829124c5b703f2d14cf187d8640b6be3b2b49376465 @@ -74,6 +77,7 @@ RUN set -eu; \ # --- Runtime stage --- # Debian 13 (R-1105, v0.323.0), pinned by point release AND digest. It waited on Debian 12 until the off-site code's # restic assumptions were re-proven on 0.18 (07-backup-architecture.md §6.8); restic itself no longer comes from here. +# upstream: gap:Debian announces package fixes as DSAs on security-tracker.debian.org, which the watch does not read; BEHIND on the point release is the signal FROM debian:13.7-slim@sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f # Install runtime dependencies — each one is a program the controller binary runs (R-1105 re-checked every exec): diff --git a/controller/infra-images/samba/Dockerfile b/controller/infra-images/samba/Dockerfile index 4a74c701..d4f26def 100644 --- a/controller/infra-images/samba/Dockerfile +++ b/controller/infra-images/samba/Dockerfile @@ -24,6 +24,7 @@ # Evidence: felhom.eu/documentation/audits/DIAG-sharing-2026-07-20.md. # 1.2.0 (R-972): Alpine 3.24.2, pinned by digest — the same base as the hub. 3.21 carried 48 fixable high findings # (audits/security-review-2026-10-12/scanners/). Same packages; scanner before → after in the controller's v0.323.0 CHANGELOG. +# upstream: gap:Alpine's package fixes (Samba among them) are in its secdb, which the watch does not read; BEHIND on the point release is the signal FROM alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6 # samba = smbd + nmbd + smbpasswd/testparm (meta-package proven installable in the spike); diff --git a/controller/internal/infra/upstream.txt b/controller/internal/infra/upstream.txt new file mode 100644 index 00000000..b8466723 --- /dev/null +++ b/controller/internal/infra/upstream.txt @@ -0,0 +1,7 @@ +# upstream.txt — where each pin in infra.go publishes its security notices (the security watch, felhom.eu R-1303). +# One line per `*Image` const in infra.go: the const name, then its sources. The grammar lives in ONE place: +# felhom.eu/scripts/security_watch_inventory.py. A const with no line here refuses the push (gate `upstream`). +TraefikImage github:traefik/traefik +CloudflaredImage github:cloudflare/cloudflared feed:cloudflare-blog vulnnotes:cloudflared +FileBrowserImage github:gtsteffaniak/filebrowser +SambaImage self diff --git a/controller/scripts/controller_gates.py b/controller/scripts/controller_gates.py index aa8654cc..5381a1f3 100644 --- a/controller/scripts/controller_gates.py +++ b/controller/scripts/controller_gates.py @@ -21,6 +21,8 @@ Gates, in order (all must pass; **non-zero exit on any failure**): … security-scan govulncheck (called vulnerabilities) + gitleaks on the pushed commits, WARN-FIRST: it lists, it never refuses (R-986; shared, felhom.eu/scripts/security_scan_gate.py). Fast although it reads vuln.go.dev: offline it says NOT CHECKED, so it cannot stop a push + … upstream every infra.go pin and every Dockerfile FROM names where its security notices are + published (R-1303; shared, felhom.eu/scripts/security_watch_inventory.py) … gate-decoys the decoy suite (test_gate_decoys.py): every gate above still convicts its decoys (R-1116). ~22 s and it edits real files in this tree for a moment, so the full run only, never --fast (never the hook, never CI) @@ -65,6 +67,8 @@ SHARED_INSTRUCTIONS = os.path.join(SHARED_DIR, "instructions_gate.py") SHARED_OBSERVATIONS = os.path.join(SHARED_DIR, "observations_gate.py") # R-986 — shared too: the dependency and secret scanner, warn-first. SHARED_SECURITY = os.path.join(SHARED_DIR, "security_scan_gate.py") +# R-1303 — shared too: every pin names where its security notices are published (the nightly watch reads it). +SHARED_UPSTREAM = os.path.join(SHARED_DIR, "security_watch_inventory.py") # R-404 — the golden NOTICE. Lives here, beside the runner, because it is about THIS repo's # releases; it imports the felhom.eu gate rather than copying its comparison. @@ -114,6 +118,9 @@ GATES = [ # although it reads vuln.go.dev: it cannot refuse, so a network blink is a NOT CHECKED line. ~8 s here. CI fetches # the pinned gitleaks (FELHOM_SCAN_FETCH=1) and has no Go, so govulncheck says NOT CHECKED there. ("security-scan", SHARED_SECURITY, [REPO], True, True), + # R-1303 (2026-10-26) — every infra.go pin (upstream.txt beside it) and every Dockerfile FROM / restic ARG (a + # `# upstream:` comment directly above) names its notice source. Fast: stdlib file reads. + ("upstream", SHARED_UPSTREAM, ["--gate=controller", REPO], True, True), # R-404 — ADVISORY. Reports the golden debt where it is created; never refuses. ("golden-notice", GOLDEN_NOTICE, [REPO], True, False), # R-1116 (2026-10-17) — the gates' OWN decoy suite: every gate above, red-proved against its decoys. It was diff --git a/controller/scripts/test_gate_decoys.py b/controller/scripts/test_gate_decoys.py index f4646f7f..61b75254 100644 --- a/controller/scripts/test_gate_decoys.py +++ b/controller/scripts/test_gate_decoys.py @@ -55,6 +55,10 @@ COVERS = { "second secret on another line of an accepted file still listed; a called govulncheck " "finding listed, an imported-only one not; no tool is NOT CHECKED out loud), run from here; " "plus the gate on THIS repo: exit 0 and its summary line (warn-first)"), + "upstream": ("R-1303: a scratch controller tree - a NEW *Image const with no upstream.txt line, the line " + "only in a COMMENT, a Dockerfile `# upstream:` comment one line too high (not directly above " + "its FROM), a new FROM with none; controls: a FROM of an earlier STAGE needs no line, and the " + "real repo passes"), "offbox-rename": "R-425: NAS branding in a NEW backups*.html, and in a bundle value an offbox Go file " "names; control: the same token in the network-storage feature's copy is accepted", "docker-v": "R-426: an unallowlisted `-v /etc:/x` in a NEW .go file two directories down " @@ -533,6 +537,56 @@ def security_cases(): security_cases() + +def upstream_cases(): + """R-1303: the shared upstream rule (felhom.eu scripts/security_watch_inventory.py --gate=controller) on a SCRATCH + controller tree, then on this repo.""" + global ran + inv = os.path.join(SHARED, "security_watch_inventory.py") + if not os.path.isfile(inv): + fails.append("shared %s is MISSING beside this clone - a failure, never a skip" % inv) + return + GO = 'package infra\nconst (\n\tTraefikImage = "traefik:v3.7.13"\n\tFooImage = "foo/foo:1.0"\n)\n' + TABLE = "# header\nTraefikImage github:traefik/traefik\nFooImage github:foo/foo\n" + DOCKER = ("# upstream: osv:Go/stdlib\nFROM golang:1.27.3 AS builder\nRUN go build\n" + "# upstream: gap:Debian fixes are DSAs the watch does not read\nFROM debian:13.7-slim\n" + "COPY --from=builder /x /x\nFROM builder AS again\n") + ws = tempfile.mkdtemp(prefix="ctrl-upstream-") + try: + def run(name, go, table, docker, expect, must): + global ran + root = os.path.join(ws, "repo") + shutil.rmtree(root, ignore_errors=True) + os.makedirs(os.path.join(root, "controller", "internal", "infra")) + for rel, body in (("controller/internal/infra/infra.go", go), ("controller/internal/infra/upstream.txt", table), + ("controller/Dockerfile", docker)): + io.open(os.path.join(root, rel), "w", encoding="utf-8").write(body) + ran += 1 + rc, out = _run([sys.executable, inv, "--gate=controller", root], REPO) + if rc != expect or must not in out: + fails.append("upstream: %s: rc=%d (want %d), %r missing\n%s" % (name, rc, expect, must, out[-500:])) + else: + print(" ok upstream: %s" % name) + run("GENUINE: every const and FROM named (a stage FROM needs none)", GO, TABLE, DOCKER, 0, "upstream gate OK") + run("FACT: a new *Image const with no table line", GO.replace(")", '\tBarImage = "bar/bar:2.0"\n)'), TABLE, DOCKER, + 1, "BarImage") + run("FACT: the table line only in a COMMENT", GO, TABLE.replace("FooImage ", "# FooImage "), DOCKER, 1, "FooImage") + run("FACT: the upstream comment one line too high", GO, TABLE, + DOCKER.replace("# upstream: osv:Go/stdlib\nFROM golang", "# upstream: osv:Go/stdlib\n# builder\nFROM golang"), + 1, "golang:1.27.3") + run("FACT: a new FROM with no upstream comment", GO, TABLE, DOCKER + "FROM alpine:3.24.2\n", 1, "alpine:3.24.2") + ran += 1 + rc, out = _run([sys.executable, inv, "--gate=controller", REPO], REPO) + if rc != 0 or "upstream gate OK" not in out: + fails.append("upstream: GENUINE: this repo: rc=%d\n%s" % (rc, out[-500:])) + else: + print(" ok upstream: GENUINE: this repo") + finally: + shutil.rmtree(ws, ignore_errors=True) + + +upstream_cases() + print() if fails: for f in fails: