diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 829604b4ce..0a965febb1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -111,3 +111,16 @@ updates: - minor - patch open-pull-requests-limit: 5 + + # Docker base images + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + groups: + docker-images: + patterns: + - "*" + open-pull-requests-limit: 5 diff --git a/Dockerfile b/Dockerfile index 2ead5c51fc..1b40c2da35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-bookworm +FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 # Install Bun (required for build scripts) RUN curl -fsSL https://bun.sh/install | bash diff --git a/Dockerfile.sandbox b/Dockerfile.sandbox index 21fd321a49..a463d4a102 100644 --- a/Dockerfile.sandbox +++ b/Dockerfile.sandbox @@ -1,4 +1,4 @@ -FROM debian:bookworm-slim +FROM debian:bookworm-slim@sha256:98f4b71de414932439ac6ac690d7060df1f27161073c5036a7553723881bffbe ENV DEBIAN_FRONTEND=noninteractive diff --git a/Dockerfile.sandbox-browser b/Dockerfile.sandbox-browser index 4eccbc9a1a..ec9faf7111 100644 --- a/Dockerfile.sandbox-browser +++ b/Dockerfile.sandbox-browser @@ -1,4 +1,4 @@ -FROM debian:bookworm-slim +FROM debian:bookworm-slim@sha256:98f4b71de414932439ac6ac690d7060df1f27161073c5036a7553723881bffbe ENV DEBIAN_FRONTEND=noninteractive diff --git a/scripts/docker/cleanup-smoke/Dockerfile b/scripts/docker/cleanup-smoke/Dockerfile index 683dfbea9d..1d9288b0df 100644 --- a/scripts/docker/cleanup-smoke/Dockerfile +++ b/scripts/docker/cleanup-smoke/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim +FROM node:22-bookworm-slim@sha256:3cfe526ec8dd62013b8843e8e5d4877e297b886e5aace4a59fec25dc20736e45 RUN apt-get update \ && apt-get install -y --no-install-recommends \ diff --git a/scripts/docker/install-sh-e2e/Dockerfile b/scripts/docker/install-sh-e2e/Dockerfile index 3b887c4203..7b4908f7fa 100644 --- a/scripts/docker/install-sh-e2e/Dockerfile +++ b/scripts/docker/install-sh-e2e/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim +FROM node:22-bookworm-slim@sha256:3cfe526ec8dd62013b8843e8e5d4877e297b886e5aace4a59fec25dc20736e45 RUN apt-get update \ && apt-get install -y --no-install-recommends \ diff --git a/scripts/docker/install-sh-nonroot/Dockerfile b/scripts/docker/install-sh-nonroot/Dockerfile index f7c378281c..9691b0bbcb 100644 --- a/scripts/docker/install-sh-nonroot/Dockerfile +++ b/scripts/docker/install-sh-nonroot/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:24.04 +FROM ubuntu:24.04@sha256:cd1dba651b3080c3686ecf4e3c4220f026b521fb76978881737d24f200828b2b RUN set -eux; \ for attempt in 1 2 3; do \ diff --git a/scripts/docker/install-sh-smoke/Dockerfile b/scripts/docker/install-sh-smoke/Dockerfile index ee806c7b60..29bf8e8486 100644 --- a/scripts/docker/install-sh-smoke/Dockerfile +++ b/scripts/docker/install-sh-smoke/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim +FROM node:22-bookworm-slim@sha256:3cfe526ec8dd62013b8843e8e5d4877e297b886e5aace4a59fec25dc20736e45 RUN set -eux; \ for attempt in 1 2 3; do \ diff --git a/scripts/e2e/Dockerfile b/scripts/e2e/Dockerfile index fcad225bed..4451de617c 100644 --- a/scripts/e2e/Dockerfile +++ b/scripts/e2e/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-bookworm +FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 RUN corepack enable diff --git a/scripts/e2e/Dockerfile.qr-import b/scripts/e2e/Dockerfile.qr-import index c2370044db..60f601566f 100644 --- a/scripts/e2e/Dockerfile.qr-import +++ b/scripts/e2e/Dockerfile.qr-import @@ -1,4 +1,4 @@ -FROM node:22-bookworm +FROM node:22-bookworm@sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935 RUN corepack enable diff --git a/src/docker-image-digests.test.ts b/src/docker-image-digests.test.ts new file mode 100644 index 0000000000..ab721e5abe --- /dev/null +++ b/src/docker-image-digests.test.ts @@ -0,0 +1,61 @@ +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { parse } from "yaml"; + +const repoRoot = resolve(fileURLToPath(new URL(".", import.meta.url)), ".."); + +const DIGEST_PINNED_DOCKERFILES = [ + "Dockerfile", + "Dockerfile.sandbox", + "Dockerfile.sandbox-browser", + "scripts/docker/cleanup-smoke/Dockerfile", + "scripts/docker/install-sh-e2e/Dockerfile", + "scripts/docker/install-sh-nonroot/Dockerfile", + "scripts/docker/install-sh-smoke/Dockerfile", + "scripts/e2e/Dockerfile", + "scripts/e2e/Dockerfile.qr-import", +] as const; + +type DependabotDockerGroup = { + patterns?: string[]; +}; + +type DependabotUpdate = { + "package-ecosystem"?: string; + directory?: string; + schedule?: { interval?: string }; + groups?: Record; +}; + +type DependabotConfig = { + updates?: DependabotUpdate[]; +}; + +describe("docker base image pinning", () => { + it("pins selected Dockerfile FROM lines to immutable sha256 digests", async () => { + for (const dockerfilePath of DIGEST_PINNED_DOCKERFILES) { + const dockerfile = await readFile(resolve(repoRoot, dockerfilePath), "utf8"); + const fromLine = dockerfile + .split(/\r?\n/) + .find((line) => line.trimStart().startsWith("FROM ")); + expect(fromLine, `${dockerfilePath} should define a FROM line`).toBeDefined(); + expect(fromLine, `${dockerfilePath} FROM must be digest-pinned`).toMatch( + /^FROM\s+\S+@sha256:[a-f0-9]{64}$/, + ); + } + }); + + it("keeps Dependabot Docker updates enabled for root Dockerfiles", async () => { + const raw = await readFile(resolve(repoRoot, ".github/dependabot.yml"), "utf8"); + const config = parse(raw) as DependabotConfig; + const dockerUpdate = config.updates?.find( + (update) => update["package-ecosystem"] === "docker" && update.directory === "/", + ); + + expect(dockerUpdate).toBeDefined(); + expect(dockerUpdate?.schedule?.interval).toBe("weekly"); + expect(dockerUpdate?.groups?.["docker-images"]?.patterns).toContain("*"); + }); +});