diff --git a/.github/scripts/check-image-policies.sh b/.github/scripts/check-image-policies.sh index 3ef1b35..7edeade 100644 --- a/.github/scripts/check-image-policies.sh +++ b/.github/scripts/check-image-policies.sh @@ -19,15 +19,15 @@ run_scan() { for tool in HELM KUBECTL; do docker run --rm --entrypoint /usr/bin/cat "$image" "/licenses/$tool-PACKAGES.txt" > "dist/$tool-PACKAGES.txt" test -s "dist/$tool-PACKAGES.txt" - ! grep -Eq '^golang.org/x/crypto/openpgp(/|$)' "dist/$tool-PACKAGES.txt" + if grep -Eq '^golang.org/x/crypto/openpgp(/|$)' "dist/$tool-PACKAGES.txt"; then echo 'Forbidden OpenPGP dependency' >&2; exit 1; fi done for tool in DOCKER BUILDX TRIVY; do docker run --rm --entrypoint /usr/bin/cat "$DAPPER_IMAGE" "/licenses/$tool-PACKAGES.txt" > "dist/$tool-PACKAGES.txt" test -s "dist/$tool-PACKAGES.txt" - ! grep -Eq '^golang.org/x/crypto/openpgp(/|$)' "dist/$tool-PACKAGES.txt" + if grep -Eq '^golang.org/x/crypto/openpgp(/|$)' "dist/$tool-PACKAGES.txt"; then echo 'Forbidden OpenPGP dependency' >&2; exit 1; fi done -for binary in helm kubectl; do +for binary in helm kubectl kubectl-service; do jq -e --arg binary "$binary" 'any(.Results[]?; .Type == "gobinary" and (.Target | split("/") | last) == $binary and (.Packages | length > 0))' dist/kubectl-service.security-raw.json >/dev/null diff --git a/.github/scripts/test_negative_package_guards.py b/.github/scripts/test_negative_package_guards.py new file mode 100644 index 0000000..ec208a9 --- /dev/null +++ b/.github/scripts/test_negative_package_guards.py @@ -0,0 +1,46 @@ +"""Exercise the actual shell guards, including commands after a rejected match.""" +import pathlib +import re +import subprocess +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[2] + + +class NegativePackageGuards(unittest.TestCase): + def test_all_package_guards_fail_before_the_next_command(self): + count = 0 + for relative in ('Dockerfile.dapper', 'package/Dockerfile', + '.github/scripts/check-image-policies.sh'): + lines = (ROOT / relative).read_text().splitlines() + self.assertFalse(any(re.search(r'^\s*! grep', line) for line in lines)) + for line in lines: + if 'if grep' not in line: + continue + count += 1 + command = line.strip().removesuffix('\\').strip().rstrip(';') + command = re.sub(r'(/(?:licenses|out|tmp)/[^; ]+|"dist/\$tool-PACKAGES.txt")', + '"$fixture"', command) + forbidden = ('dep\tgithub.com/docker/docker\tv1\n' + if 'build metadata' in command else + 'github.com/docker/docker/client\n' + if 'upstream Docker dependency' in command else + 'golang.org/x/crypto/openpgp/packet\n') + with tempfile.TemporaryDirectory() as temp: + fixture = pathlib.Path(temp) / 'packages.txt' + for contents, expected in ((forbidden, 1), ('fmt\n', 0)): + fixture.write_text(contents) + result = subprocess.run(['bash', '-c', + 'set -euo pipefail; fixture="$1"; ' + command + + '; echo NEXT_COMMAND', 'guard-test', str(fixture)], + capture_output=True, text=True) + self.assertEqual(result.returncode, expected, result.stderr) + self.assertEqual('NEXT_COMMAND' in result.stdout, expected == 0) + if expected: + self.assertIn('Forbidden', result.stderr) + self.assertEqual(count, 9) + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef04d7d..fb86b4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,7 @@ jobs: test "$(git rev-list -n 1 "$RELEASE_TAG")" = "$(git rev-parse HEAD)" test -z "$(git status --porcelain)" python3 .github/scripts/test_runtime_vulnerability_policy.py + python3 .github/scripts/test_negative_package_guards.py - name: Build verification environment and vulnerability database shell: bash @@ -116,6 +117,7 @@ jobs: dist/dapper.raw.json dist/dapper.effective.json dist/dapper.cdx.json \ dist/dapper-policy.json dist/dapper.accepted-build-header-findings.json \ dist/runtime-openvex.json dist/*-PACKAGES.txt \ + dist/dapper-ubuntu-apt-packages.tsv dist/ubuntu-apt.lock dist/ubuntu-security-packages.lock \ dist/SHA256SUMS - name: Clean run-owned resources diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 4b030a9..42b6b37 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -42,6 +42,7 @@ jobs: docker version docker buildx version python3 .github/scripts/test_runtime_vulnerability_policy.py + python3 .github/scripts/test_negative_package_guards.py - name: Build the checksum-locked verification environment shell: bash diff --git a/Dockerfile.dapper b/Dockerfile.dapper index da6923c..bafb335 100644 --- a/Dockerfile.dapper +++ b/Dockerfile.dapper @@ -138,7 +138,7 @@ RUN set -eux; \ GOFLAGS=-mod=mod GOWORK=off GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org GOMODCACHE=/tmp/docker-go-mod go list -deps ./cmd/docker > /tmp/docker-resolved-packages.txt; \ GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/docker-go-mod go mod verify; \ GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/docker-go-mod go list -deps ./cmd/docker > /licenses/DOCKER-PACKAGES.txt; \ - ! grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /licenses/DOCKER-PACKAGES.txt; \ + if grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /licenses/DOCKER-PACKAGES.txt; then echo 'Forbidden OpenPGP dependency' >&2; exit 1; fi; \ docker_build_time="$(date -u --date="@${DOCKER_SOURCE_DATE_EPOCH}" +'%Y-%m-%dT%H:%M:%SZ')"; \ SOURCE_DATE_EPOCH="${DOCKER_SOURCE_DATE_EPOCH}" CGO_ENABLED=0 GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/docker-go-mod GOOS=linux GOARCH="${DAPPER_HOST_ARCH}" \ go build -trimpath -buildvcs=false -tags 'grpcnotrace osusergo netgo' \ @@ -180,8 +180,8 @@ RUN set -eux; \ test "$(GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/buildx-go-mod go list -m -f '{{.Version}}' golang.org/x/mod)" = "${BUILDX_X_MOD_VERSION}"; \ test "$(GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/buildx-go-mod go list -m -f '{{.Version}}' google.golang.org/grpc)" = "${BUILDX_GRPC_VERSION}"; \ GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/buildx-go-mod go list -deps ./cmd/buildx > /tmp/buildx-deps.txt; \ - ! grep '^github.com/docker/docker/' /tmp/buildx-deps.txt; \ - ! grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /tmp/buildx-deps.txt; \ + if grep '^github.com/docker/docker/' /tmp/buildx-deps.txt; then echo 'Forbidden upstream Docker dependency' >&2; exit 1; fi; \ + if grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /tmp/buildx-deps.txt; then echo 'Forbidden OpenPGP dependency' >&2; exit 1; fi; \ cp /tmp/buildx-deps.txt /licenses/BUILDX-PACKAGES.txt; \ SOURCE_DATE_EPOCH="${BUILDX_SOURCE_DATE_EPOCH}" CGO_ENABLED=0 GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/buildx-go-mod GOOS=linux GOARCH="${DAPPER_HOST_ARCH}" \ go build -trimpath -buildvcs=false \ @@ -191,7 +191,7 @@ RUN set -eux; \ docker buildx version | grep -F "github.com/docker/buildx v${BUILDX_VERSION} ${BUILDX_GIT_COMMIT}"; \ go version -m /usr/libexec/docker/cli-plugins/docker-buildx > /licenses/BUILDX-GO-VERSION.txt; \ test "$(awk 'NR == 1 { print $NF }' /licenses/BUILDX-GO-VERSION.txt)" = "go${GO_VERSION}"; \ - ! grep -F $'dep\tgithub.com/docker/docker\t' /licenses/BUILDX-GO-VERSION.txt; \ + if grep -F $'dep\tgithub.com/docker/docker\t' /licenses/BUILDX-GO-VERSION.txt; then echo 'Forbidden upstream Docker build metadata' >&2; exit 1; fi; \ grep -F $'dep\tgithub.com/moby/go-archive\t'"${BUILDX_GO_ARCHIVE_VERSION}"$'\t' /licenses/BUILDX-GO-VERSION.txt; \ grep -F $'dep\tgolang.org/x/mod\t'"${BUILDX_X_MOD_VERSION}"$'\t' /licenses/BUILDX-GO-VERSION.txt; \ grep -F $'dep\tgoogle.golang.org/grpc\t'"${BUILDX_GRPC_VERSION}"$'\t' /licenses/BUILDX-GO-VERSION.txt; \ @@ -232,7 +232,7 @@ RUN set -eux; \ GOFLAGS=-mod=mod GOWORK=off GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org GOMODCACHE=/tmp/trivy-go-mod go list -deps ./cmd/trivy > /tmp/trivy-resolved-packages.txt; \ GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/trivy-go-mod go mod verify; \ GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/trivy-go-mod go list -deps ./cmd/trivy > /licenses/TRIVY-PACKAGES.txt; \ - ! grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /licenses/TRIVY-PACKAGES.txt; \ + if grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /licenses/TRIVY-PACKAGES.txt; then echo 'Forbidden OpenPGP dependency' >&2; exit 1; fi; \ SOURCE_DATE_EPOCH="${TRIVY_SOURCE_DATE_EPOCH}" CGO_ENABLED=0 GOEXPERIMENT=jsonv2 GOFLAGS=-mod=mod GOWORK=off GOPROXY=off GOSUMDB=sum.golang.org GOMODCACHE=/tmp/trivy-go-mod GOOS=linux GOARCH="${DAPPER_HOST_ARCH}" \ go build -trimpath -buildvcs=false \ -ldflags "-s -w -buildid= -X=github.com/aquasecurity/trivy/pkg/version/app.ver=${TRIVY_VERSION}" \ diff --git a/README.md b/README.md index 59932e3..db55bce 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,9 @@ non-root read-only smoke run, a client-side Helm render, full-severity Trivy reports, and a CycloneDX SBOM. Release gates reject all available fixes, unreviewed findings, and High/Critical findings. Only exact Low/Medium entries with official no-fix evidence may remain. The sole OpenVEX statement covers -the absent OpenPGP package; source dependency graphs enforce that boundary. +the absent OpenPGP package; source dependency graphs enforce that boundary +with fail-closed checks. Raw scans must include package inventories for the +service executable, Helm, kubectl, and each bundled Go build tool. See [COMPATIBILITY.md](COMPATIBILITY.md), [SECURITY.md](SECURITY.md), and [ORIGIN.md](ORIGIN.md). diff --git a/package/Dockerfile b/package/Dockerfile index 013f9a5..0f6f32b 100644 --- a/package/Dockerfile +++ b/package/Dockerfile @@ -35,7 +35,7 @@ RUN set -eux; \ go get "golang.org/x/net@v${HELM_X_NET_VERSION}" "golang.org/x/crypto@v${HELM_X_CRYPTO_VERSION}"; \ go mod verify; \ go list -deps ./cmd/helm > /out/HELM-PACKAGES.txt; \ - ! grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /out/HELM-PACKAGES.txt; \ + if grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /out/HELM-PACKAGES.txt; then echo 'Forbidden OpenPGP dependency' >&2; exit 1; fi; \ test "$(go list -m -f '{{.Version}}' oras.land/oras-go/v2)" = "v${HELM_ORAS_GO_VERSION}"; \ test "$(go list -m -f '{{.Version}}' golang.org/x/crypto)" = "v${HELM_X_CRYPTO_VERSION}"; \ test "$(go list -m -f '{{.Version}}' golang.org/x/net)" = "v${HELM_X_NET_VERSION}"; \ @@ -82,7 +82,7 @@ RUN set -eux; \ GOWORK=off go mod vendor; \ test "$(GOWORK=off go list -m -f '{{.Version}}' golang.org/x/net)" = "${KUBERNETES_X_NET_VERSION}"; \ GOWORK=off go list -deps ./cmd/kubectl > /out/KUBECTL-PACKAGES.txt; \ - ! grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /out/KUBECTL-PACKAGES.txt; \ + if grep -Eq '^golang.org/x/crypto/openpgp(/|$)' /out/KUBECTL-PACKAGES.txt; then echo 'Forbidden OpenPGP dependency' >&2; exit 1; fi; \ GOWORK=off GO_VERSION=1.27.3 FORCE_HOST_GO=1 KUBE_BUILD_PLATFORMS=linux/amd64 \ KUBE_GIT_VERSION="${KUBERNETES_VERSION}" KUBE_GIT_COMMIT="${KUBERNETES_GIT_COMMIT}" \ KUBE_GIT_TREE_STATE=clean KUBE_GIT_MAJOR=1 KUBE_GIT_MINOR=36 \