From 9fc3cbadf67c5e72c89f9192a1b604433597fec6 Mon Sep 17 00:00:00 2001 From: "H.E. Pennypacker" <115990865+pennypacker-he@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:58:44 +0000 Subject: [PATCH] [patch] Harden verified image publication --- .github/workflows/build-push.yml | 68 ++++++++++++++++++++++++++++++- .trivyignore | 4 ++ Dockerfile | 6 ++- ci/workflow_contract_test.go | 69 ++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 .trivyignore create mode 100644 ci/workflow_contract_test.go diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 2eb7786..6b5233d 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -1,8 +1,16 @@ name: build-push on: + pull_request: push: + branches: + - main + tags: + - "*" workflow_dispatch: +permissions: + contents: read + jobs: lint-test: runs-on: ubuntu-24.04 @@ -46,10 +54,66 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + image-check: + if: github.event_name == 'pull_request' + strategy: + fail-fast: false + matrix: + architecture: + - runner: ubuntu-24.04 + platform: linux/amd64 + suffix: amd64 + - runner: ubuntu-24.04-arm + platform: linux/arm64 + suffix: arm64 + runs-on: ${{ matrix.architecture.runner }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + persist-credentials: false + + - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 + + - name: Build native image without credentials + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 + with: + context: . + load: true + platforms: ${{ matrix.architecture.platform }} + provenance: false + push: false + tags: cap:ci-${{ matrix.architecture.suffix }} + + - name: Scan native image + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: cap:ci-${{ matrix.architecture.suffix }} + format: table + exit-code: "1" + ignore-unfixed: true + severity: HIGH,CRITICAL + vuln-type: os,library + run: + name: run + if: ${{ always() && github.event_name == 'pull_request' }} + needs: [lint-test, image-check] + permissions: {} + uses: libops/.github/.github/workflows/pr-status.yaml@057262171193c6d563e7b08ae93d0c4aebfce2bd # credential-free-required-status + with: + needs-json: ${{ toJSON(needs) }} + + publish: + if: github.ref == 'refs/heads/main' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) needs: [lint-test] - uses: libops/.github/.github/workflows/build-push.yaml@main + uses: libops/.github/.github/workflows/build-push.yaml@a86300fb8020d0f7141bb9f833d89b5dbd7aa4d7 # guarded-signed-image-publisher + with: + ref: ${{ github.sha }} + expected-main-sha: ${{ github.ref == 'refs/heads/main' && github.sha || '' }} + scan: true + sign: true + certificate-identity: https://github.com/libops/.github/.github/workflows/build-push.yaml@a86300fb8020d0f7141bb9f833d89b5dbd7aa4d7 permissions: contents: read packages: write - secrets: inherit + id-token: write diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..5aa8523 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,4 @@ +# Cap links the Prometheus parser/export client, not Prometheus's remote-read +# HTTP handler where this decompression issue exists. Remove this exception +# when prometheus-engine exposes a supported exporter on the patched line. +CVE-2026-42154 exp:2026-10-01 diff --git a/Dockerfile b/Dockerfile index 757ebda..96cf8f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.26.3-alpine3.22@sha256:be93003ee861b3b91b6ebcb22678524947e0cd786c2df3f32af520006b1e54f5 AS builder +FROM golang:1.25.12-alpine@sha256:56961d79ea8129efddcc0b8643fd8a5416b4e6228cfd477e3fd61deb2672c587 AS builder SHELL ["/bin/ash", "-o", "pipefail", "-ex", "-c"] @@ -15,7 +15,9 @@ COPY scraper ./scraper RUN --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/binary . -FROM golang:1.26.3-alpine3.22@sha256:be93003ee861b3b91b6ebcb22678524947e0cd786c2df3f32af520006b1e54f5 +FROM alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b + +RUN apk add --no-cache ca-certificates COPY --from=builder /app/binary /app/binary diff --git a/ci/workflow_contract_test.go b/ci/workflow_contract_test.go new file mode 100644 index 0000000..a3c3fdc --- /dev/null +++ b/ci/workflow_contract_test.go @@ -0,0 +1,69 @@ +package ci + +import ( + "os" + "strings" + "testing" +) + +const sharedPublisherSHA = "a86300fb8020d0f7141bb9f833d89b5dbd7aa4d7" + +func TestImagePublicationWorkflowContract(t *testing.T) { + workflow, err := os.ReadFile("../.github/workflows/build-push.yml") + if err != nil { + t.Fatal(err) + } + contents := string(workflow) + + required := []string{ + "pull_request:", + "if: github.event_name == 'pull_request'", + "if: github.ref == 'refs/heads/main' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))", + "Build native image without credentials", + "libops/.github/.github/workflows/build-push.yaml@" + sharedPublisherSHA, + "ref: ${{ github.sha }}", + "expected-main-sha: ${{ github.ref == 'refs/heads/main' && github.sha || '' }}", + "scan: true", + "sign: true", + "certificate-identity: https://github.com/libops/.github/.github/workflows/build-push.yaml@" + sharedPublisherSHA, + "packages: write", + "id-token: write", + } + for _, value := range required { + if !strings.Contains(contents, value) { + t.Errorf("image workflow must contain %q", value) + } + } + + forbidden := []string{ + "build-push.yaml@main", + "build-push-ghcr.yaml", + "secrets: inherit", + "docker-registry:", + "additional-gar-registry:", + } + for _, value := range forbidden { + if strings.Contains(contents, value) { + t.Errorf("image workflow must not contain %q", value) + } + } +} + +func TestTrivyExceptionIsNarrowAndExpiring(t *testing.T) { + ignore, err := os.ReadFile("../.trivyignore") + if err != nil { + t.Fatal(err) + } + + lines := strings.Split(strings.TrimSpace(string(ignore)), "\n") + var rules []string + for _, line := range lines { + line = strings.TrimSpace(line) + if line != "" && !strings.HasPrefix(line, "#") { + rules = append(rules, line) + } + } + if len(rules) != 1 || rules[0] != "CVE-2026-42154 exp:2026-10-01" { + t.Fatalf("unexpected Trivy exception rules: %q", rules) + } +}