From f62cd59789c15d9d53c49df4afeeacf52536d7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 2 Sep 2026 15:46:55 +0200 Subject: [PATCH 1/4] chore(deps): raise the stale advisory floors and pin valibot Three overrides had drifted behind the advisories they were written for, and the scan found the packages at versions the pins were supposed to exclude. dompurify moves from <=3.4.10 to <3.4.13. GHSA-55q2-fjhq-7xh7 is fixed in 3.4.13 and the tree carried 3.4.12. The copy is jspdf's optional HTML renderer, an optionalDependency at ^3.3.1, so the raise stays inside the range jspdf declares. postcss moves from <8.5.10 to <8.5.23 for CVE-2026-69153. next declares an exact postcss@8.4.31, so nothing but this bound moves it; the build has run on the 8.5 line since the first bump and stays there. valibot gets a floor it never had, <1.4.2, for CVE-2026-59952. Two copies arrive and neither is ours to call: an optional peer of @hookform/resolvers, which this app does not use because it validates with zod, and prisma's local dev server. 1.4.2 is the same major, so no consumer is moved onto a new one. @hono/node-server moves from <1.19.13 to <1.19.15 for GHSA-frvp-7c67-39w9. The app tree already resolved 1.19.17, so this changes nothing here; it exists so the bound states the real floor and so the Dockerfile mirror has something correct to match. The floor test in ci-determinism asserted the exact key and replacement string, which meant every legitimate raise failed it while a floor gone stale against a newer advisory passed it unchanged. It now reduces each key to a package name and asserts the pin is at or above the fixed version, with a case proving the comparison can return false. --- pnpm-lock.yaml | 43 +++++------- pnpm-workspace.yaml | 24 ++++++- scripts/__tests__/ci-determinism.test.ts | 83 ++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 36 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3423fa752..0993c6ce9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,10 +5,10 @@ settings: excludeLinksFromLockfile: false overrides: - dompurify@<=3.4.10: ^3.4.11 - postcss@<8.5.10: ^8.5.10 + dompurify@<3.4.13: ^3.4.13 + postcss@<8.5.23: ^8.5.23 nanoid@<3.3.18: ^3.3.18 - '@hono/node-server@<1.19.13': ^1.19.13 + '@hono/node-server@<1.19.15': ^1.19.15 hono@<4.12.25: ^4.12.25 picomatch@<4.0.4: ^4.0.4 body-parser@<2.3.0: ^2.3.0 @@ -21,6 +21,7 @@ overrides: sharp@<0.35.0: ^0.35.3 pdfjs-dist@>=5.6.83 <6.2.108: 6.2.108 deepmerge-ts@<8.0.0: ^8.0.0 + valibot@<1.4.2: ^1.4.2 importers: @@ -40,7 +41,7 @@ importers: version: 3.2.2(react@19.2.7) '@hookform/resolvers': specifier: ^5.9.1 - version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.2.0(typescript@6.0.3))(zod@4.4.3) + version: 5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3) '@modelcontextprotocol/sdk': specifier: ^1.30.0 version: 1.30.0(@cfworker/json-schema@4.1.1)(supports-color@7.2.0)(zod@4.4.3) @@ -864,7 +865,7 @@ packages: react-hook-form: ^7.55.0 superstruct: '>=0.12.0' typanion: ^3.3.2 - valibot: '>=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc' + valibot: ^1.4.2 vest: '>=6.0.0' yup: ^1.0.0 zod: ^3.25.0 || ^4.0.0 @@ -4204,8 +4205,8 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - dompurify@3.4.12: - resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} + dompurify@3.4.14: + resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==} dot-prop@6.0.1: resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} @@ -5961,10 +5962,6 @@ packages: resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} engines: {node: '>=4'} - postcss@8.5.20: - resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.25: resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} @@ -6923,8 +6920,8 @@ packages: utrie@1.0.2: resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} - valibot@1.2.0: - resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -7923,7 +7920,7 @@ snapshots: dependencies: hono: 4.13.3 - '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.2.0(typescript@6.0.3))(zod@4.4.3)': + '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(effect@3.20.0)(react-hook-form@7.83.0(react@19.2.7))(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)': dependencies: '@standard-schema/utils': 0.3.0 react-hook-form: 7.83.0(react@19.2.7) @@ -7932,7 +7929,7 @@ snapshots: ajv: 8.20.0 ajv-formats: 2.1.1(ajv@8.20.0) effect: 3.20.0 - valibot: 1.2.0(typescript@6.0.3) + valibot: 1.4.2(typescript@6.0.3) zod: 4.4.3 '@humanfs/core@0.19.1': {} @@ -8894,7 +8891,7 @@ snapshots: proper-lockfile: 4.1.2 remeda: 2.33.4 std-env: 3.10.0 - valibot: 1.2.0(typescript@6.0.3) + valibot: 1.4.2(typescript@6.0.3) zeptomatch: 2.1.0 transitivePeerDependencies: - typescript @@ -11068,7 +11065,7 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.4.12: + dompurify@3.4.14: optionalDependencies: '@types/trusted-types': 2.0.7 optional: true @@ -12253,7 +12250,7 @@ snapshots: optionalDependencies: canvg: 3.0.11 core-js: 3.49.0 - dompurify: 3.4.12 + dompurify: 3.4.14 html2canvas: 1.4.1 jsx-ast-utils@3.3.5: @@ -12603,7 +12600,7 @@ snapshots: '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.43 caniuse-lite: 1.0.30001805 - postcss: 8.5.20 + postcss: 8.5.26 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.7) @@ -12990,12 +12987,6 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.20: - dependencies: - nanoid: 3.3.18 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.25: dependencies: nanoid: 3.3.18 @@ -14169,7 +14160,7 @@ snapshots: base64-arraybuffer: 1.0.2 optional: true - valibot@1.2.0(typescript@6.0.3): + valibot@1.4.2(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c90da0443..5970d1ce6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,8 +1,20 @@ overrides: - "dompurify@<=3.4.10": "^3.4.11" - "postcss@<8.5.10": "^8.5.10" + # GHSA-55q2-fjhq-7xh7: a crafted document gets past the sanitiser and its + # markup survives into the output. The copy here is jspdf's optional HTML + # renderer, which the doctor-report PDF path reaches. + "dompurify@<3.4.13": "^3.4.13" + # CVE-2026-69153. Next declares an exact `postcss@8.4.31`, so nothing except + # this bound moves it. The build has run on the 8.5 line since the first + # bump; the fix landed in 8.5.23. + "postcss@<8.5.23": "^8.5.23" "nanoid@<3.3.18": "^3.3.18" - "@hono/node-server@<1.19.13": "^1.19.13" + # GHSA-frvp-7c67-39w9 raised the floor from 1.19.13 to 1.19.15. Mirrored for + # /opt/prisma-cli in the Dockerfile, which is where the flagged 1.19.11 + # actually landed: prisma pulls it through @prisma/dev, and that install + # reads npm `overrides`, not this file. The bound stays on the 1.x line + # because the advisory is fixed there too and 2.x is a major nothing asked + # for. + "@hono/node-server@<1.19.15": "^1.19.15" "hono@<4.12.25": "^4.12.25" "picomatch@<4.0.4": "^4.0.4" "body-parser@<2.3.0": "^2.3.0" @@ -39,6 +51,12 @@ overrides: # override is the only way through. It runs on the config-loading and CLI # path, not under a request, but a red audit blocks the whole release train. "deepmerge-ts@<8.0.0": "^8.0.0" + # CVE-2026-59952. Two copies arrive, neither of them ours to call: an + # optional peer of `@hookform/resolvers` (this app validates with zod) and + # prisma's local dev server. Mirrored for /opt/prisma-cli in the Dockerfile, + # where the flagged 1.2.0 landed. 1.4.2 is the same major, so no consumer is + # dragged onto a new one. + "valibot@<1.4.2": "^1.4.2" allowBuilds: "@prisma/engines": true diff --git a/scripts/__tests__/ci-determinism.test.ts b/scripts/__tests__/ci-determinism.test.ts index 34bac3b29..5d6714b18 100644 --- a/scripts/__tests__/ci-determinism.test.ts +++ b/scripts/__tests__/ci-determinism.test.ts @@ -150,17 +150,86 @@ describe("container dependency installation", () => { }); }); +/** + * `>= 0` comparison over a dotted version, enough for the floor checks below. + * Pre-release suffixes are not expected in an override replacement and are + * ignored rather than guessed at. + */ +function atLeast(version: string, floor: string): boolean { + const parts = (value: string) => + value + .replace(/^[\^~>=<\s]+/, "") + .split("-")[0] + .split(".") + .map((piece) => Number.parseInt(piece, 10) || 0); + const [a, b] = [parts(version), parts(floor)]; + for (let i = 0; i < Math.max(a.length, b.length); i += 1) { + const left = a[i] ?? 0; + const right = b[i] ?? 0; + if (left !== right) return left > right; + } + return true; +} + describe("production dependency advisory floors", () => { - it("pins vulnerable transitive ranges to compatible patched versions", () => { + /** + * The floor an advisory demands, by package. Raising an override past one of + * these must not break this test: an earlier version of it asserted the exact + * key and replacement string, so every legitimate floor raise failed it while + * a floor that had gone STALE against a newer advisory passed. It now asserts + * the property that matters, which is that the pin is at or above the fixed + * version. Update a floor here when a new advisory raises it. + */ + const floors: Record = { + // GHSA-55q2-fjhq-7xh7, reached through jspdf's optional HTML renderer. + dompurify: "3.4.13", + // CVE-2026-69153. + postcss: "8.5.23", + // GHSA-frvp-7c67-39w9 and CVE-2026-39406. Mirrored in the Dockerfile. + "@hono/node-server": "1.19.15", + hono: "4.12.25", + // CVE-2026-59952. Mirrored in the Dockerfile. + valibot: "1.4.2", + }; + + it("pins vulnerable transitive ranges at or above the fixed version", () => { const workspace = parse(readRepoFile("pnpm-workspace.yaml")) as { overrides?: Record; }; + const overrides = workspace.overrides ?? {}; - expect(workspace.overrides).toMatchObject({ - "dompurify@<=3.4.10": "^3.4.11", - "postcss@<8.5.10": "^8.5.10", - "@hono/node-server@<1.19.13": "^1.19.13", - "hono@<4.12.25": "^4.12.25", - }); + const byName = new Map(); + for (const [key, replacement] of Object.entries(overrides)) { + const selectorAt = key.startsWith("@") + ? key.indexOf("@", 1) + : key.indexOf("@"); + byName.set( + selectorAt === -1 ? key : key.slice(0, selectorAt), + replacement, + ); + } + expect(byName.size).toBeGreaterThan(0); + + for (const [name, floor] of Object.entries(floors)) { + const replacement = byName.get(name); + expect( + replacement, + `${name} has no override in pnpm-workspace.yaml`, + ).toBeDefined(); + expect( + atLeast(replacement as string, floor), + `${name} is pinned to ${replacement}, below the advisory floor ${floor}.`, + ).toBe(true); + } + }); + + it("compares versions rather than always agreeing", () => { + // The comparison is the whole test above; a helper that never returns + // false would make it vacuous. + expect(atLeast("^3.4.13", "3.4.13")).toBe(true); + expect(atLeast("^3.4.14", "3.4.13")).toBe(true); + expect(atLeast("^3.4.12", "3.4.13")).toBe(false); + expect(atLeast("^1.19.9", "1.19.15")).toBe(false); + expect(atLeast("^2.0.5", "1.19.15")).toBe(true); }); }); From c9f8aee4761411fac0070e481cb51d7a6609aee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 2 Sep 2026 15:47:13 +0200 Subject: [PATCH 2/4] build(docker): mirror the hono and valibot pins into /opt/prisma-cli The image carries two installs. pnpm builds the app tree from pnpm-workspace.yaml; npm builds /opt/prisma-cli and reads only its own overrides field. The scan reported @hono/node-server@1.19.11 (GHSA-frvp-7c67-39w9, CVE-2026-39406) and valibot@1.2.0 (CVE-2026-59952) under that path while both packages were pinned and clean in the app tree. Same shape as the deepmerge-ts finding, same cause. Rebuilt this exact install both ways to confirm rather than assume. Without the two fields npm resolves @hono/node-server 1.19.11 and valibot 1.2.0, which are the two versions the scan named. With them it resolves 1.19.17 and 1.4.2, and prisma --version still reports 7.8.0. Both arrive through prisma -> @prisma/dev. The lockstep guard could not have caught this. Its workspace matcher started a package name at [^"@], so every scoped key was invisible to it: @hono/node-server was in the workspace file and absent from the Dockerfile, the shared set never contained it, and the guard stayed green. Scoped keys parse now, a test pins the reduction itself so the blind spot cannot return quietly, and a second test asserts that the three packages the image scan has actually flagged under /opt/prisma-cli keep their npm-side mirror. Both fail when the pin is removed or moved out of step. --- Dockerfile | 10 ++++ ...dependency-override-lockstep-guard.test.ts | 59 +++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index f62b66d3b..1860926f2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -191,11 +191,21 @@ COPY --from=builder /app/src/generated ./src/generated # still reports 7.8.0. Keep the range in step with the entry in # `pnpm-workspace.yaml` — they are one decision applied to two package # managers, and a fix that lands in only one of them is the defect above. +# +# The same class recurred on 2026-09-02: the image scan reported +# `@hono/node-server@1.19.11` (GHSA-frvp-7c67-39w9, CVE-2026-39406) and +# `valibot@1.2.0` (CVE-2026-59952) under this path, both reached through +# `prisma -> @prisma/dev`, while the app tree carried 1.19.17 and a pin the +# npm install never reads. Rebuilding this install both ways confirms it: +# without the two fields below npm resolves 1.19.11 and 1.2.0, with them it +# resolves 1.19.17 and 1.4.2, and `prisma --version` still reports 7.8.0. RUN mkdir -p /opt/prisma-cli && \ cd /opt/prisma-cli && \ npm init -y && \ npm pkg set 'overrides.deepmerge-ts=^8.0.0' && \ npm pkg set 'overrides.mysql2=^3.22.0' && \ + npm pkg set 'overrides.@hono/node-server=^1.19.15' && \ + npm pkg set 'overrides.valibot=^1.4.2' && \ npm install --omit=dev prisma@7.8.0 @prisma/engines@7.8.0 tsx@4.23.1 dotenv@17.4.2 && \ ln -sfn /opt/prisma-cli/node_modules/.bin/tsx /usr/local/bin/healthlog-tsx && \ ln -sfn /opt/prisma-cli/node_modules/dotenv /app/node_modules/dotenv && \ diff --git a/src/__tests__/dependency-override-lockstep-guard.test.ts b/src/__tests__/dependency-override-lockstep-guard.test.ts index 0cd3c7b66..029e10fd6 100644 --- a/src/__tests__/dependency-override-lockstep-guard.test.ts +++ b/src/__tests__/dependency-override-lockstep-guard.test.ts @@ -20,6 +20,12 @@ * would encode noise. It asserts agreement where both files speak, which is * the failure that actually happened. * + * The matcher had a blind spot of its own until 2026-09-02: it refused any + * workspace key beginning with `@`, so every scoped package was invisible to + * it. `@hono/node-server` was pinned on one side and unpinned on the other and + * the guard stayed green, which is the same shape of green the guard exists to + * prevent. Scoped keys parse now, and a test below pins that they do. + * * Limit, written down so the next reader does not over-trust a green run: this * compares declared ranges. It cannot see a NEW vulnerable transitive that * neither file mentions. Only the image scan can, and that job runs on @@ -44,17 +50,30 @@ function dockerfileOverrides(): Map { return found; } +/** + * Reduces an overrides key to the bare package name. The key carries a version + * selector, so `deepmerge-ts@<8.0.0` and a bare `deepmerge-ts` both land on the + * same name. A scoped name opens with its own `@`, so the selector starts at + * the SECOND one, not the first. + */ +function packageNameOf(key: string): string { + const trimmed = key.trim(); + const selectorAt = trimmed.startsWith("@") + ? trimmed.indexOf("@", 1) + : trimmed.indexOf("@"); + return (selectorAt === -1 ? trimmed : trimmed.slice(0, selectorAt)).trim(); +} + /** * `"@": ""` entries under the workspace `overrides:` - * block. The key carries a version selector, so `deepmerge-ts@<8.0.0` and a - * bare `deepmerge-ts` both have to reduce to the same package name. + * block. */ function workspaceOverrides(): Map { const found = new Map(); - const pattern = /^\s+"([^"@][^"]*?)(?:@[^"]*)?"\s*:\s*"([^"]+)"/gm; + const pattern = /^\s+"([^"]+)"\s*:\s*"([^"]+)"/gm; const block = WORKSPACE.split(/^allowBuilds:/m)[0]; for (const match of block.matchAll(pattern)) { - found.set(match[1].trim(), match[2].trim()); + found.set(packageNameOf(match[1]), match[2].trim()); } return found; } @@ -74,6 +93,23 @@ describe("dependency overrides — pnpm workspace and the npm install agree", () expect(workspace.has("deepmerge-ts")).toBe(true); }); + it("reads scoped workspace keys rather than skipping them", () => { + // The old matcher started the name at `[^"@]`, so `@hono/node-server` was + // never in the shared set and could disagree across the two files without + // failing anything. Both halves are asserted: the reduction itself, and + // that a real scoped entry survives it. + expect(packageNameOf("@hono/node-server@<1.19.15")).toBe( + "@hono/node-server", + ); + expect(packageNameOf("dompurify@<3.4.13")).toBe("dompurify"); + expect(packageNameOf("js-yaml@>=4.0.0 <4.3.1")).toBe("js-yaml"); + expect(packageNameOf("deepmerge-ts")).toBe("deepmerge-ts"); + + const workspace = workspaceOverrides(); + const scoped = [...workspace.keys()].filter((name) => name.startsWith("@")); + expect(scoped.length).toBeGreaterThan(0); + }); + it("pins the same range wherever both files name the same package", () => { const docker = dockerfileOverrides(); const workspace = workspaceOverrides(); @@ -91,6 +127,21 @@ describe("dependency overrides — pnpm workspace and the npm install agree", () } }); + it("mirrors every pin the /opt/prisma-cli tree was scanned on", () => { + // These three were each reported by the image scan under + // `/opt/prisma-cli`, reached through `prisma`. Losing a mirror is not a + // style regression, it is the vulnerable version coming back. + const docker = dockerfileOverrides(); + for (const name of ["deepmerge-ts", "@hono/node-server", "valibot"]) { + expect( + docker.has(name), + `${name} was flagged under /opt/prisma-cli and needs an ` + + `\`npm pkg set 'overrides.${name}=...'\` line in the Dockerfile; ` + + `pnpm-workspace.yaml does not reach that install.`, + ).toBe(true); + } + }); + it("keeps the override ahead of the version Prisma's config asks for", () => { // `@prisma/config` requests deepmerge-ts 7.x; the advisory is fixed in // 8.0.0. Anything that lets a 7.x back in re-opens CVE-2026-40345. From c5fc707e592f8a51562d1ea3fc9de461cf28e5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 2 Sep 2026 15:47:14 +0200 Subject: [PATCH 3/4] ci: scope the dependabot auto-merge write token to its job The workflow granted contents: write and pull-requests: write at the top level, so every step in the run held them, including the checkout and the dependency install that execute code from the pull request being merged. Scorecard flags the shape, and the shape is the problem: a compromised transitive in a Dependabot branch would run with a token that can write to the repository. The workflow default is contents: read now, and the two write scopes sit on the auto-merge job, which is the only job there is and the only place they are spent. gh pr merge --auto needs contents: write to enable merge-on-green and pull-requests: write to act on the pull request; the major-bump comment needs pull-requests: write. The metadata fetch and the test run need neither. Nothing about which updates merge automatically changes. --- .github/workflows/dependabot-auto-merge.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 7a399726d..7995cc21f 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -2,14 +2,20 @@ name: Auto-merge Dependabot on: pull_request +# Nothing outside the one job needs more than a checkout, so the default for +# the workflow is read-only and the write scopes sit on the job that spends +# them: `gh pr merge --auto` writes to the repository's merge queue state, and +# both the merge and the review comment write to the pull request. permissions: - contents: write - pull-requests: write + contents: read jobs: auto-merge: runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' + permissions: + contents: write + pull-requests: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 From 6cac31403b7ac0d43e471feb98fa57cd7d492534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 2 Sep 2026 15:47:14 +0200 Subject: [PATCH 4/4] docs(changelog): record the dependency floors and the token scope --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2693f1f..86b96506e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [Unreleased] + +### Security + +- **The published image no longer ships four flagged dependency versions.** + Three floors in the override block had gone stale: the sanitiser behind the + doctor-report PDF sat below the fix for GHSA-55q2-fjhq-7xh7, the CSS + processor the build runs on sat below the fix for CVE-2026-69153, and a + schema library that arrives through two unrelated parents had no floor at + all (CVE-2026-59952). All three are raised and re-resolved, each inside its + existing major, so nothing changes shape for anyone. +- **The Prisma command line inside the image gets the same pins as the app.** + The image carries two separate installs, and only one of them reads the + project's override block. The other is an npm install of the Prisma CLI, and + it was quietly resolving an HTTP server (GHSA-frvp-7c67-39w9, + CVE-2026-39406) and a schema library (CVE-2026-59952) at the versions the + image scan flagged, while the same packages were pinned and clean in the + application tree. Both are now pinned on the npm side too, verified by + rebuilding that install both ways. This is the second time the same gap has + produced a finding, so the guard that compares the two files was widened: it + had been skipping every scoped package name, which is exactly the case that + went unnoticed here. +- The Dependabot auto-merge workflow no longer hands a write token to the + whole run. The workflow default is read-only and the write scopes sit on the + one job that spends them. Nothing about which updates merge automatically + changes. + ## [1.38.3] — 2026-09-02 An account with two-factor can be deleted from the phone, and every data