diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9d66d22..1eac59a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -106,6 +106,23 @@ jobs: uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-checksums: dist/checksums.txt + id: attest + - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + with: + cosign-release: v3.1.3 + # Attach a keyless signature of checksums.txt and the SLSA provenance bundle + # to the GitHub release so both can be verified from the release page alone. + - name: Sign checksums and attach signature and provenance + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + BUNDLE: ${{ steps.attest.outputs.bundle-path }} + run: | + cosign sign-blob --yes --bundle dist/checksums.txt.sigstore.json dist/checksums.txt + cp "$BUNDLE" "dist/notation-aws-verifier_${TAG#v}.intoto.jsonl" + gh release upload "$TAG" --repo "$GITHUB_REPOSITORY" --clobber \ + dist/checksums.txt.sigstore.json \ + "dist/notation-aws-verifier_${TAG#v}.intoto.jsonl" image: name: image diff --git a/CHANGELOG.md b/CHANGELOG.md index bd6f84d..69c1ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Security + +- Base images are pinned by digest directly in `FROM`, so Dependabot keeps tag and digest current. +- Releases attach a keyless cosign signature of `checksums.txt` and the SLSA provenance bundle. +- Added a fuzz test for `/checkimages` request parsing. +- Added `osv-scanner.toml` documenting advisories that do not affect this service. + ## [1.0.0] - 2026-09-24 First release of `notation-aws-verifier`, forked from diff --git a/Dockerfile b/Dockerfile index f8b983e..117d5bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,7 @@ # syntax=docker/dockerfile:1 -ARG BUILDER_IMAGE="golang:1.26.8-alpine3.24" -# gcr.io/distroless/static:nonroot, pinned by digest (multi-arch index). -ARG BASE_IMAGE="gcr.io/distroless/static:nonroot@sha256:e2e927ec666bae08560abb3c55d0659eceabb657f56b6782ab500a9fc7f555e3" - -FROM --platform=$BUILDPLATFORM $BUILDER_IMAGE AS builder +# Base images are pinned by multi-arch index digest directly in FROM so that +# Dependabot can bump tag and digest together. +FROM --platform=$BUILDPLATFORM golang:1.26.8-alpine3.24@sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c AS builder ARG TARGETOS ARG TARGETARCH @@ -37,7 +35,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \ -ldflags="-s -w -X main.version=${VERSION}" \ -o /out/notation-aws-verifier . -FROM $BASE_IMAGE +FROM gcr.io/distroless/static:nonroot@sha256:e2e927ec666bae08560abb3c55d0659eceabb657f56b6782ab500a9fc7f555e3 ARG VERSION=dev LABEL org.opencontainers.image.title="notation-aws-verifier" \ diff --git a/SECURITY.md b/SECURITY.md index 92c98d9..92432a3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -61,3 +61,15 @@ Verify a downloaded release binary archive: gh attestation verify notation-aws-verifier_1.0.0_linux_amd64.tar.gz \ --repo krax1337/notation-aws-verifier ``` + +Each release attaches `notation-aws-verifier_.intoto.jsonl` (the SLSA +provenance bundle, usable with `gh attestation verify --bundle`). Releases after +v1.0.0 also attach `checksums.txt.sigstore.json`, a keyless cosign signature of +`checksums.txt`, so archives can be verified offline from the release page: + +```sh +cosign verify-blob checksums.txt --bundle checksums.txt.sigstore.json \ + --certificate-identity-regexp '^https://github.com/krax1337/notation-aws-verifier/.github/workflows/release.yaml@refs/tags/v' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com +sha256sum --ignore-missing -c checksums.txt +``` diff --git a/internal/verifier/request_fuzz_test.go b/internal/verifier/request_fuzz_test.go new file mode 100644 index 0000000..df4497a --- /dev/null +++ b/internal/verifier/request_fuzz_test.go @@ -0,0 +1,48 @@ +package verifier + +import ( + "encoding/json" + "testing" + + "github.com/krax1337/notation-aws-verifier/internal/types" +) + +// FuzzProcessRequestData feeds arbitrary /checkimages request bodies through the +// same decode and validation path as the HTTP handler. The body is supplied by +// the caller (a Kyverno policy), so malformed input must be rejected with an +// error, never a panic, and an accepted request must carry at least one image. +func FuzzProcessRequestData(f *testing.F) { + seeds := []string{ + `{}`, + `{"images":{}}`, + `{"images":{"containers":{"app":{"registry":"123456789012.dkr.ecr.us-west-2.amazonaws.com","path":"demo","name":"demo","tag":"v1","jsonPointer":"/spec/containers/0/image"}}}}`, + `{"images":{"initContainers":{"init":{"registry":"public.ecr.aws","path":"a/b","name":"b","digest":"sha256:00"}}},"trustPolicy":"tp-default"}`, + `{"images":{"containers":{"a":{}}},"attestations":[{"imageReference":"*","type":[{"name":"sbom/cyclone-dx","conditions":{"all":[{"key":"{{a}}","operator":"Equals","value":"1"}]}}]}]}`, + `{"images":{"containers":{"a":{}}},"attestations":[{"imageReference":"","type":[]}]}`, + `{"images":{"containers":{"a":{}}},"attestations":[{"imageReference":"*","type":[{"name":"x","conditions":{"any":[{"operator":"Equals"}]}}]}]}`, + `{"images":null,"attestations":null,"imageReferences":["*"],"insecure":true}`, + } + for _, s := range seeds { + f.Add([]byte(s)) + } + + f.Fuzz(func(t *testing.T, body []byte) { + var req types.RequestData + if err := json.Unmarshal(body, &req); err != nil { + return + } + got, err := processRequestData(&req) + if err != nil { + if got != nil { + t.Fatalf("non-nil request returned with error %v", err) + } + return + } + if got == nil { + t.Fatal("nil request returned without error") + } + if len(got.Images.Containers)+len(got.Images.InitContainers)+len(got.Images.EphemeralContainers) == 0 { + t.Fatal("request without images was accepted") + } + }) +} diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 0000000..0ee7e4b --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,40 @@ +# Advisories reported by OSV-based scanners (osv-scanner, OpenSSF Scorecard) that +# do not affect this service. govulncheck, which checks reachability, reports none +# of them as called; see .govulncheck-ignore and .trivyignore.yaml for the same +# decisions in CI. Review on every dependency bump; remove entries once fixed. + +# github.com/kyverno/kyverno: module-wide advisories without a fixed version. +# They concern the Kyverno admission controller (apiCall, service calls, policy +# context loading). This service only imports condition evaluation, JMESPath and +# image helpers and never makes Kyverno service or API calls. +[[IgnoredVulns]] +id = "GO-2025-3615" +reason = "Kyverno SSRF via service calls; not used, no fixed version" + +[[IgnoredVulns]] +id = "GO-2026-5337" +reason = "Kyverno cross-namespace read in policy context; not used, no fixed version" + +[[IgnoredVulns]] +id = "GO-2026-5594" +reason = "Kyverno apiCall SSRF; apiCall not used, no fixed version" + +# Pulled in transitively by Kyverno's API package (via cosign CLI options). +# The ACR credential helper is linked but never invoked: registry credentials come +# from the ECR helper and image pull secrets only. No fixed version exists. +[[IgnoredVulns]] +id = "GO-2026-6225" +reason = "docker-credential-acr-env credential leakage; ACR helper never invoked, no fixed version" + +# Advisory against the deprecated golang.org/x/crypto/openpgp package as a whole. +# Only x/crypto/cryptobyte and other packages are used; openpgp is not imported. +[[IgnoredVulns]] +id = "GO-2026-5932" +reason = "x/crypto/openpgp unmaintained; openpgp is not imported" + +# gRPC-Go server panic on missing :authority/Host. grpc is indirect and this +# binary runs no gRPC server. Fixed only in a 1.85.0 development build. +[[IgnoredVulns]] +id = "GO-2026-6443" +ignoreUntil = 2026-12-31 +reason = "grpc server panic; no gRPC server runs; fix only in 1.85.0-dev"