package main import ( "os" "regexp" "strings" "testing" ) // R-1105: restic is pinned OUTSIDE the OS. Until v0.323.0 it came from the runtime's apt list, so the // version was „whatever Debian ships" — a Debian 12 → 13 base move would have swapped restic 0.14.0 for // 0.18.0 under the off-site code with nobody choosing it. The consequences pinned here: // - the version is one explicit ARG, and each architecture's checksum is pinned beside it; // - the fetch checks restic's signature (vendored key, no keyserver) AND both checksums; // - the runtime takes restic ONLY from that stage — no apt package can bring a second one; // - every base image is pinned by digest, and the Docker repo's codename is read from the image. func TestR1105_ResticIsPinnedOutsideTheOS(t *testing.T) { raw, err := os.ReadFile("../../Dockerfile") if err != nil { t.Fatal(err) } src := string(raw) // Stages, split on FROM, with `\`-continued lines joined so an apt list reads as one command. joined := strings.ReplaceAll(src, "\\\n", " ") var stages []string for _, part := range regexp.MustCompile(`(?m)^FROM `).Split(joined, -1)[1:] { stages = append(stages, "FROM "+part) } if len(stages) < 3 { t.Fatalf("expected a builder, a restic and a runtime stage, found %d stages", len(stages)) } var fetch string for _, s := range stages { if regexp.MustCompile(`^FROM \S+ AS restic\b`).MatchString(s) { fetch = s } } if fetch == "" { t.Fatal("no `AS restic` stage: restic is no longer fetched as a pinned release binary") } runtime := stages[len(stages)-1] if m := regexp.MustCompile(`ARG RESTIC_VERSION=(\d+\.\d+\.\d+)\s`).FindStringSubmatch(fetch); m == nil { t.Error("ARG RESTIC_VERSION=X.Y.Z is missing — the version must be an explicit decision") } for _, arch := range []string{"AMD64", "ARM64"} { if !regexp.MustCompile(`ARG RESTIC_SHA256_` + arch + `=[0-9a-f]{64}\s`).MatchString(fetch) { t.Errorf("no pinned sha256 for %s", arch) } } for _, want := range []string{ "COPY third_party/restic/restic-release-key.asc", "gpgv --keyring", // SHA256SUMS is restic's, signed `SHA256SUMS | sha256sum -c -`, // the download matches the published sum `echo "${want} ${f}" | sha256sum -c -`, // ...and the sum pinned in this file } { if !strings.Contains(fetch, want) { t.Errorf("the restic stage no longer does %q", want) } } if _, err := os.Stat("../../third_party/restic/restic-release-key.asc"); err != nil { t.Errorf("the vendored release key is missing: %v", err) } if !strings.Contains(runtime, "COPY --from=restic /usr/local/bin/restic /usr/local/bin/restic") { t.Error("the runtime does not take restic from the pinned stage") } aptRe := regexp.MustCompile(`apt-get install[^&]*`) for _, inst := range aptRe.FindAllString(runtime, -1) { if regexp.MustCompile(`\srestic(\s|$)`).MatchString(inst) { t.Errorf("the runtime's apt list installs restic — a second, OS-chosen restic: %q", strings.Join(strings.Fields(inst), " ")) } } for _, from := range regexp.MustCompile(`(?m)^FROM (\S+)`).FindAllStringSubmatch(src, -1) { if !strings.Contains(from[1], "@sha256:") { t.Errorf("base image %s is not pinned by digest", from[1]) } } if regexp.MustCompile(`download\.docker\.com/linux/debian (bookworm|trixie|forky)`).MatchString(src) { t.Error("the Docker apt repo codename is hard-coded — it must come from /etc/os-release (VERSION_CODENAME)") } if !strings.Contains(runtime, "${VERSION_CODENAME}") { t.Error("the Docker apt repo line does not read VERSION_CODENAME") } }