145 lines
4.8 KiB
Bash
145 lines
4.8 KiB
Bash
|
|
#!/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}"
|