lesavka/scripts/ci/supply_chain_gate.sh

145 lines
4.8 KiB
Bash
Raw Permalink Normal View History

#!/usr/bin/env bash
# Generate dependency/artifact security evidence and run available scanners.
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
REPORT_DIR="${ROOT_DIR}/target/supply-chain-gate"
SUMMARY_JSON="${REPORT_DIR}/summary.json"
SUMMARY_TXT="${REPORT_DIR}/summary.txt"
METRICS_FILE="${REPORT_DIR}/metrics.prom"
SBOM_JSON="${REPORT_DIR}/sbom.cargo-metadata.json"
TREE_TXT="${REPORT_DIR}/dependency-tree.txt"
SECRET_TXT="${REPORT_DIR}/secret-scan.txt"
PUSHGATEWAY_URL=${QUALITY_GATE_PUSHGATEWAY_URL:-}
ENFORCE_TOOLS=${LESAVKA_SUPPLY_CHAIN_ENFORCE_TOOLS:-0}
mkdir -p "${REPORT_DIR}"
cd "${ROOT_DIR}"
branch=${BRANCH_NAME:-${GIT_BRANCH:-}}
if [[ -z "${branch}" ]]; then
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)
fi
commit=${GIT_COMMIT:-}
if [[ -z "${commit}" ]]; then
commit=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
fi
status=0
cargo metadata --locked --format-version 1 >"${SBOM_JSON}"
cargo tree --locked --workspace >"${TREE_TXT}"
secret_status=ok
: >"${SECRET_TXT}"
while IFS= read -r path; do
case "$path" in
Cargo.lock|target/*|dist/*) continue ;;
esac
[[ -f "$path" ]] || continue
if file "$path" | grep -qi 'text\|json\|xml\|yaml\|toml\|script'; then
if grep -EHni \
-e 'AKIA[0-9A-Z]{16}' \
-e '-----BEGIN (RSA|EC|OPENSSH|PRIVATE) KEY-----' \
-e "(password|secret|token|api[_-]?key)[[:space:]]*[:=][[:space:]]*[\"'][A-Za-z0-9_+/=.-]{12,}[\"']" \
"$path" >>"${SECRET_TXT}" 2>/dev/null; then
secret_status=failed
status=1
fi
fi
done < <(git ls-files)
audit_status=not_applicable
if command -v cargo-audit >/dev/null 2>&1; then
audit_status=ok
if ! cargo audit --locked >"${REPORT_DIR}/cargo-audit.txt" 2>&1; then
audit_status=failed
status=1
fi
elif [[ "${ENFORCE_TOOLS}" == "1" ]]; then
audit_status=failed
status=1
fi
deny_status=not_applicable
if command -v cargo-deny >/dev/null 2>&1; then
deny_status=ok
if ! cargo deny check >"${REPORT_DIR}/cargo-deny.txt" 2>&1; then
deny_status=failed
status=1
fi
elif [[ "${ENFORCE_TOOLS}" == "1" ]]; then
deny_status=failed
status=1
fi
artifact_status=not_applicable
if compgen -G "dist/*.tar.gz" >/dev/null; then
artifact_status=ok
sha256sum dist/*.tar.gz >"${REPORT_DIR}/SHA256SUMS"
fi
python3 - "${SUMMARY_JSON}" "${SUMMARY_TXT}" "${METRICS_FILE}" "${branch}" "${commit}" "${secret_status}" "${audit_status}" "${deny_status}" "${artifact_status}" <<'PY'
import json
import pathlib
import sys
from datetime import datetime, timezone
summary_path = pathlib.Path(sys.argv[1])
text_path = pathlib.Path(sys.argv[2])
metrics_path = pathlib.Path(sys.argv[3])
branch = sys.argv[4]
commit = sys.argv[5]
secret_status = sys.argv[6]
audit_status = sys.argv[7]
deny_status = sys.argv[8]
artifact_status = sys.argv[9]
def esc(value: str) -> str:
return value.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')
checks = {
'secret_scan': secret_status,
'cargo_audit': audit_status,
'cargo_deny': deny_status,
'artifact_checksums': artifact_status,
'sbom': 'ok',
}
status = 'failed' if any(value == 'failed' for value in checks.values()) else 'ok'
summary = {
'suite': 'lesavka',
'check': 'supply_chain',
'status': status,
'branch': branch,
'commit': commit,
'generated_at': datetime.now(timezone.utc).isoformat(),
'checks': checks,
}
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + '\n', encoding='utf-8')
lines = ['supply chain gate report', f'status: {status}', f'branch: {branch}', f'commit: {commit}', '']
for name, value in checks.items():
lines.append(f'{name}: {value}')
text_path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
labels = f'suite="lesavka",branch="{esc(branch)}",commit="{esc(commit)}"'
metrics = [
'# HELP platform_quality_gate_checks_total Check outcomes from the latest lesavka gate run.',
'# TYPE platform_quality_gate_checks_total gauge',
]
for state in ('ok', 'failed', 'not_applicable'):
value = 1 if state == status else 0
metrics.append(f'platform_quality_gate_checks_total{{{labels},check="supply_chain",status="{state}"}} {value}')
metrics.append('# HELP lesavka_supply_chain_subcheck_info Supply-chain subcheck evidence status.')
metrics.append('# TYPE lesavka_supply_chain_subcheck_info gauge')
for name, value in checks.items():
metrics.append(f'lesavka_supply_chain_subcheck_info{{{labels},subcheck="{esc(name)}",status="{esc(value)}"}} 1')
metrics_path.write_text('\n'.join(metrics) + '\n', encoding='utf-8')
print(text_path.read_text(encoding='utf-8'))
PY
if [[ -n "${PUSHGATEWAY_URL}" ]]; then
curl --fail --silent --show-error \
--data-binary @"${METRICS_FILE}" \
"${PUSHGATEWAY_URL%/}/metrics/job/lesavka-supply-chain-gate/suite/lesavka" || status=$?
fi
exit "${status}"