#!/usr/bin/env bash set -uo pipefail # CASAN Plan-16 SEC-24 (SC-05/06, offline) — supply-chain integrity for build files. # # Two harness-side controls that need no network: # * image-pin: Dockerfiles / CI workflow files must pin container images by DIGEST # (`@sha256:...`), never a floating tag (`:latest`, `:20`, or no tag) — a floating # tag lets a malicious image be swapped in under the same name. # * sign / verify: a CI workflow (or any build file) is signed and verified on load; # a tampered, forged, or UNSIGNED file is REFUSED (fail-closed). # # (Live CVE/OSV scanning and real image scanning need infra and remain planned.) # # Usage: # supply-chain-integrity.sh image-pin [ ...] # supply-chain-integrity.sh sign # supply-chain-integrity.sh verify # Exit: 0 ok · 2 violation (unpinned image / tampered-forged sig) · 3 missing/unsigned/openssl · 64 usage. CMD="${1:-}"; shift || true case "$CMD" in image-pin) [[ "$#" -ge 1 ]] || { echo "usage: supply-chain-integrity.sh image-pin ..." >&2; exit 64; } for f in "$@"; do [[ -f "$f" ]] || { echo "IMAGE_PIN_MISSING file=$f" >&2; exit 3; }; done python3 - "$@" <<'PY' import re, sys bad = [] # image refs from Dockerfile `FROM x` and workflow/compose `image: x` pat = re.compile(r'^\s*(?:FROM\s+|image:\s*["\']?)([^\s"\']+)', re.IGNORECASE) for path in sys.argv[1:]: for i, line in enumerate(open(path, encoding="utf-8", errors="replace"), 1): m = pat.match(line) if not m: continue ref = m.group(1).strip() low = ref.lower() if low in ("scratch",): continue # bare single token with no registry path and no tag = local build stage -> ok if "/" not in ref and ":" not in ref and "." not in ref and "@" not in ref: continue if "@sha256:" in ref: continue # digest-pinned -> ok bad.append(f"{path}:{i} unpinned image '{ref}' (use @sha256:)") if bad: for b in bad: sys.stderr.write("IMAGE_UNPINNED " + b + "\n") raise SystemExit(2) print("IMAGE_PIN_OK all images digest-pinned") PY ;; sign) F="${1:-}"; KEY="${2:-}" command -v openssl >/dev/null 2>&1 || { echo "OPENSSL_UNAVAILABLE" >&2; exit 3; } [[ -f "$F" && -f "$KEY" ]] || { echo "usage: supply-chain-integrity.sh sign " >&2; exit 64; } openssl dgst -sha256 -sign "$KEY" -out "$F.sig" "$F" 2>/dev/null \ && { echo "WORKFLOW_SIGNED file=$F"; exit 0; } echo "WORKFLOW_SIGN_FAILED file=$F" >&2; exit 2 ;; verify) F="${1:-}"; KEY="${2:-}" command -v openssl >/dev/null 2>&1 || { echo "OPENSSL_UNAVAILABLE" >&2; exit 3; } [[ -f "$F" ]] || { echo "WORKFLOW_MISSING file=$F" >&2; exit 3; } [[ -f "$KEY" ]] || { echo "WORKFLOW_PUBKEY_MISSING key=$KEY" >&2; exit 3; } [[ -f "$F.sig" ]] || { echo "WORKFLOW_UNSIGNED file=$F — refusing (fail-closed)" >&2; exit 3; } if openssl dgst -sha256 -verify "$KEY" -signature "$F.sig" "$F" >/dev/null 2>&1; then echo "WORKFLOW_VERIFIED file=$F"; exit 0 fi echo "WORKFLOW_INVALID file=$F — tampered or forged" >&2; exit 2 ;; *) echo "Usage: supply-chain-integrity.sh {image-pin ...|sign |verify }" >&2 exit 64 ;; esac