feat: enforce live supply chain provenance gates
This commit is contained in:
@@ -135,6 +135,8 @@ run "phase-sec23-registry-crypt" bash "$TESTS/phase-sec23-registry-crypt-tests.s
|
||||
# Plan-16 SEC-24/25 supply-chain (offline slice)
|
||||
run "phase-sec24-supplychain" bash "$TESTS/phase-sec24-tests.sh"
|
||||
run "phase-sec25-attestation" bash "$TESTS/phase-sec25-tests.sh"
|
||||
run "phase-production-handoff" bash "$TESTS/phase-production-preflight-tests.sh"
|
||||
run "phase-release-provenance" bash "$TESTS/phase-release-provenance-tests.sh"
|
||||
|
||||
# Plan-17 loop engineering (Agentic Loop Governance) — each primitive fail-closed.
|
||||
run "phase-loop-governor" bash "$TESTS/phase-loop-governor-tests.sh"
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Live CI supply-chain gate. Unlike the deterministic local denylist gate, this
|
||||
# obtains current advisory data and scans the built images. Missing scanners are
|
||||
# an error in strict CI; no "scanner unavailable => green" path exists.
|
||||
#
|
||||
# Usage: live-supply-chain-scan.sh <report-dir> [image-ref ...]
|
||||
# Required tools: npm, osv-scanner, trivy (when CASAN_REQUIRE_LIVE_SCANNERS=1)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
ROOT="$CASAN_APP_ROOT"
|
||||
OUT="${1:-}"; shift || true
|
||||
[[ -n "$OUT" ]] || { echo "LIVE_SCAN_USAGE report-dir [image-ref ...]" >&2; exit 64; }
|
||||
mkdir -p "$OUT"
|
||||
|
||||
fail() { echo "LIVE_SUPPLY_CHAIN_FAIL $*" >&2; exit 1; }
|
||||
require() { command -v "$1" >/dev/null 2>&1 || fail "tool_missing name=$1"; }
|
||||
require npm
|
||||
if [[ "${CASAN_REQUIRE_LIVE_SCANNERS:-1}" == "1" ]]; then
|
||||
require osv-scanner
|
||||
require trivy
|
||||
fi
|
||||
|
||||
[[ -f "$ROOT/package-lock.json" ]] || fail "package_lock_missing"
|
||||
cd "$ROOT"
|
||||
|
||||
# npm audit exits non-zero for findings at/above the requested level. Preserve
|
||||
# JSON evidence even on failure without converting a failed scan into success.
|
||||
set +e
|
||||
npm audit --omit=dev --audit-level=high --json > "$OUT/npm-audit.json" 2> "$OUT/npm-audit.stderr"
|
||||
audit_rc=$?
|
||||
set -e
|
||||
[[ "$audit_rc" -eq 0 ]] || fail "npm_audit_high_or_critical rc=$audit_rc report=$OUT/npm-audit.json"
|
||||
|
||||
npm sbom --sbom-format cyclonedx --package-lock-only --omit=dev > "$OUT/npm.cyclonedx.json" \
|
||||
|| fail "npm_sbom_failed"
|
||||
python3 - "$OUT/npm.cyclonedx.json" <<'PY' || exit 1
|
||||
import json, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
assert d.get("bomFormat") == "CycloneDX", d
|
||||
PY
|
||||
|
||||
if command -v osv-scanner >/dev/null 2>&1; then
|
||||
osv-scanner scan source --recursive --format json --output-file "$OUT/osv-source.json" "$ROOT" \
|
||||
|| fail "osv_source_findings_or_scan_error report=$OUT/osv-source.json"
|
||||
fi
|
||||
if command -v trivy >/dev/null 2>&1; then
|
||||
trivy fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1 --format json --output "$OUT/trivy-source.json" "$ROOT" \
|
||||
|| fail "trivy_source_findings_or_scan_error report=$OUT/trivy-source.json"
|
||||
fi
|
||||
|
||||
for image in "$@"; do
|
||||
safe="$(printf '%s' "$image" | tr '/:@' '___')"
|
||||
if command -v osv-scanner >/dev/null 2>&1; then
|
||||
osv-scanner scan image --format json --output-file "$OUT/osv-image-${safe}.json" "$image" \
|
||||
|| fail "osv_image_findings_or_scan_error image=$image"
|
||||
fi
|
||||
if command -v trivy >/dev/null 2>&1; then
|
||||
trivy image --severity HIGH,CRITICAL --exit-code 1 --format json --output "$OUT/trivy-image-${safe}.json" "$image" \
|
||||
|| fail "trivy_image_findings_or_scan_error image=$image"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "LIVE_SUPPLY_CHAIN_PASS sbom=$OUT/npm.cyclonedx.json images=$#"
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Create/verify a signed release provenance statement. The statement binds the
|
||||
# exact commit, SBOM, test evidence and immutable image digests. The signing key
|
||||
# is injected by CI from the secret manager and never lives in the repository.
|
||||
#
|
||||
# release-provenance.sh create <statement.json> <private.pem> <sbom.json> <test-evidence> <image@sha256...>...
|
||||
# release-provenance.sh verify <statement.json> <public.pem> [expected-commit]
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
ROOT="$CASAN_APP_ROOT"
|
||||
|
||||
hash_of() { openssl dgst -sha256 "$1" | awk '{print $NF}'; }
|
||||
fail() { echo "RELEASE_PROVENANCE_FAIL $*" >&2; exit 1; }
|
||||
cmd="${1:-}"; shift || true
|
||||
|
||||
case "$cmd" in
|
||||
create)
|
||||
statement="${1:-}"; key="${2:-}"; sbom="${3:-}"; evidence="${4:-}"; shift 4 || true
|
||||
[[ -n "$statement" && -f "$key" && -s "$sbom" && -s "$evidence" && "$#" -ge 1 ]] || fail "create_usage_or_missing_input"
|
||||
for image in "$@"; do [[ "$image" =~ @sha256:[a-f0-9]{64}$ ]] || fail "image_not_digest_pinned image=$image"; done
|
||||
commit="$(git -C "$ROOT" rev-parse HEAD)"
|
||||
python3 - "$statement" "$commit" "$sbom" "$evidence" "$@" <<'PY'
|
||||
import hashlib, json, sys
|
||||
out, commit, sbom, evidence, *images = sys.argv[1:]
|
||||
def sha(path):
|
||||
h=hashlib.sha256()
|
||||
with open(path,'rb') as f:
|
||||
for c in iter(lambda:f.read(65536), b''): h.update(c)
|
||||
return h.hexdigest()
|
||||
payload = {
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"predicateType": "https://slsa.dev/provenance/v1",
|
||||
"subject": [{"name": image, "digest": {"sha256": image.rsplit("@sha256:",1)[1]}} for image in images],
|
||||
"predicate": {"buildDefinition": {"resolvedDependencies": [{"uri": "git+casan", "digest": {"sha1": commit}}]},
|
||||
"runDetails": {"builder": {"id": "casan-gitea-ci"},
|
||||
"byproducts": [{"name": "npm.cyclonedx.json", "sha256": sha(sbom)}, {"name": "test-evidence", "sha256": sha(evidence)}]}}
|
||||
}
|
||||
with open(out, 'w', encoding='utf-8') as f: json.dump(payload, f, sort_keys=True, separators=(',', ':'))
|
||||
PY
|
||||
openssl dgst -sha256 -sign "$key" -out "$statement.sig" "$statement" || fail "sign_failed"
|
||||
echo "RELEASE_PROVENANCE_CREATED statement=$statement commit=$commit images=$# sbom_sha256=$(hash_of "$sbom")"
|
||||
;;
|
||||
verify)
|
||||
statement="${1:-}"; key="${2:-}"; expected="${3:-$(git -C "$ROOT" rev-parse HEAD)}"
|
||||
[[ -s "$statement" && -s "$statement.sig" && -f "$key" ]] || fail "verify_missing_statement_signature_or_key"
|
||||
openssl dgst -sha256 -verify "$key" -signature "$statement.sig" "$statement" >/dev/null || fail "signature_invalid"
|
||||
python3 - "$statement" "$expected" <<'PY' || exit 1
|
||||
import json, re, sys
|
||||
d=json.load(open(sys.argv[1])); expected=sys.argv[2]
|
||||
assert d.get('_type') == 'https://in-toto.io/Statement/v1'
|
||||
assert d.get('predicateType') == 'https://slsa.dev/provenance/v1'
|
||||
deps=d.get('predicate',{}).get('buildDefinition',{}).get('resolvedDependencies',[])
|
||||
assert deps and deps[0].get('digest',{}).get('sha1') == expected
|
||||
subjects=d.get('subject',[]); assert subjects
|
||||
for s in subjects:
|
||||
digest=s.get('digest',{}).get('sha256','')
|
||||
assert re.fullmatch(r'[a-f0-9]{64}', digest), digest
|
||||
PY
|
||||
echo "RELEASE_PROVENANCE_VERIFIED statement=$statement commit=$expected"
|
||||
;;
|
||||
*) fail "usage create|verify" ;;
|
||||
esac
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Verify every commit in a release range has a cryptographically good Git
|
||||
# signature. This is deliberately strict for release/main enforcement: unsigned
|
||||
# or merely unknown signatures cannot become production provenance.
|
||||
# Usage: verify-signed-commits.sh [range] (default: HEAD)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/casan-paths.sh"
|
||||
ROOT="$CASAN_APP_ROOT"; RANGE="${1:-HEAD}"
|
||||
commits=()
|
||||
while IFS= read -r commit; do [[ -n "$commit" ]] && commits+=("$commit"); done < <(git -C "$ROOT" rev-list --reverse "$RANGE")
|
||||
[[ "${#commits[@]}" -gt 0 ]] || { echo "SIGNED_COMMITS_FAIL empty_range=$RANGE" >&2; exit 1; }
|
||||
for commit in "${commits[@]}"; do
|
||||
status="$(git -C "$ROOT" log -1 --format='%G?' "$commit")"
|
||||
[[ "$status" == G ]] || { echo "SIGNED_COMMITS_FAIL commit=$commit signature_status=$status" >&2; exit 1; }
|
||||
done
|
||||
echo "SIGNED_COMMITS_VERIFIED range=$RANGE count=${#commits[@]}"
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
|
||||
SCAN="$CASAN_HARNESS_ROOT/scripts/bash/live-supply-chain-scan.sh"
|
||||
W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT
|
||||
PASS=0; FAIL=0
|
||||
pass() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
||||
fail() { echo "FAIL: $1"; FAIL=$((FAIL+1)); }
|
||||
rc() { set +e; "$@" >/dev/null 2>&1; local c=$?; set -e; echo "$c"; }
|
||||
mkdir -p "$W/bin"
|
||||
cat > "$W/bin/npm" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >> "$FAKE_LOG"
|
||||
if [[ "$1" == sbom ]]; then printf '%s\n' '{"bomFormat":"CycloneDX"}'; fi
|
||||
exit 0
|
||||
SH
|
||||
cat > "$W/bin/osv-scanner" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "osv $*" >> "$FAKE_LOG"
|
||||
for ((i=1; i<=$#; i++)); do [[ "${!i}" == --output-file ]] && { j=$((i+1)); printf '{}' > "${!j}"; }; done
|
||||
exit 0
|
||||
SH
|
||||
cat > "$W/bin/trivy" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "trivy $*" >> "$FAKE_LOG"
|
||||
for ((i=1; i<=$#; i++)); do [[ "${!i}" == --output ]] && { j=$((i+1)); printf '{}' > "${!j}"; }; done
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$W/bin/"*
|
||||
FAKE_LOG="$W/log" PATH="$W/bin:$PATH" CASAN_REQUIRE_LIVE_SCANNERS=1 bash "$SCAN" "$W/out" casan-test:image >/dev/null \
|
||||
&& [[ -s "$W/out/npm.cyclonedx.json" ]] && grep -q 'osv scan image' "$W/log" && grep -q 'trivy image' "$W/log" \
|
||||
&& pass "live gate invokes SBOM, OSV and Trivy source/image scans" || fail "live scanner contract failed"
|
||||
mkdir -p "$W/npm-only"; cp "$W/bin/npm" "$W/npm-only/npm"
|
||||
[[ "$(rc env PATH="$W/npm-only:/usr/bin:/bin" CASAN_REQUIRE_LIVE_SCANNERS=1 bash "$SCAN" "$W/missing")" -ne 0 ]] \
|
||||
&& pass "missing required scanner fails closed" || fail "missing scanner accepted"
|
||||
echo "===== LIVE SUPPLY CHAIN SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
|
||||
P="$CASAN_HARNESS_ROOT/scripts/bash/release-provenance.sh"
|
||||
W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT
|
||||
PASS=0; FAIL=0
|
||||
pass() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
||||
fail() { echo "FAIL: $1"; FAIL=$((FAIL+1)); }
|
||||
rc() { set +e; "$@" >/dev/null 2>&1; local c=$?; set -e; echo "$c"; }
|
||||
openssl genrsa -out "$W/key.pem" 2048 2>/dev/null
|
||||
openssl rsa -in "$W/key.pem" -pubout -out "$W/pub.pem" 2>/dev/null
|
||||
openssl genrsa -out "$W/other.pem" 2048 2>/dev/null
|
||||
printf '{"bomFormat":"CycloneDX"}\n' > "$W/sbom.json"; printf 'tests passed\n' > "$W/evidence.txt"
|
||||
DIGEST="sha256:$(printf 'a%.0s' {1..64})"
|
||||
bash "$P" create "$W/provenance.json" "$W/key.pem" "$W/sbom.json" "$W/evidence.txt" "registry.local/casan/api@$DIGEST" >/dev/null \
|
||||
&& bash "$P" verify "$W/provenance.json" "$W/pub.pem" >/dev/null && pass "signed provenance binds commit and image digest" || fail "valid provenance rejected"
|
||||
printf 'tamper' >> "$W/provenance.json"
|
||||
[[ "$(rc bash "$P" verify "$W/provenance.json" "$W/pub.pem")" -ne 0 ]] && pass "tampered provenance is refused" || fail "tampered provenance accepted"
|
||||
[[ "$(rc bash "$P" create "$W/bad.json" "$W/key.pem" "$W/sbom.json" "$W/evidence.txt" 'registry.local/casan/api:latest')" -ne 0 ]] && pass "floating image tag is refused" || fail "floating image accepted"
|
||||
bash "$P" create "$W/other.json" "$W/other.pem" "$W/sbom.json" "$W/evidence.txt" "registry.local/casan/api@$DIGEST" >/dev/null
|
||||
[[ "$(rc bash "$P" verify "$W/other.json" "$W/pub.pem")" -ne 0 ]] && pass "provenance signed by another key is refused" || fail "wrong signing key accepted"
|
||||
echo "===== RELEASE PROVENANCE SUMMARY: PASS=$PASS FAIL=$FAIL ====="
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
Reference in New Issue
Block a user