| #!/usr/bin/env bash |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| set -uo pipefail |
|
|
| cd "$(dirname "$0")/.." |
| REQ="${1:-requirements.txt}" |
| OUT="security" |
| mkdir -p "$OUT" |
| STAMP="$(date -u +%Y%m%dT%H%M%SZ)" |
| STRICT="${SECURITY_SCAN_STRICT:-0}" |
|
|
| RC=0 |
| SKIPS=() |
|
|
| note() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } |
| fail() { printf '\033[31mFINDINGS:\033[0m %s\n' "$*"; RC=1; } |
| skip() { printf '\033[33mSKIP:\033[0m %s\n' "$*"; SKIPS+=("$*"); [[ "$STRICT" == "1" ]] && RC=1; return 0; } |
| ok() { printf '\033[32mOK:\033[0m %s\n' "$*"; } |
|
|
| [[ -f "$REQ" ]] || { echo "error: '$REQ' not found" >&2; exit 2; } |
|
|
| |
| |
| |
| note "SBOM (CycloneDX) -> $OUT/sbom.json" |
| if command -v uv >/dev/null 2>&1; then |
| if uvx --from cyclonedx-bom cyclonedx-py requirements "$REQ" --of JSON -o "$OUT/sbom.json"; then |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if python3 - "$OUT/sbom.json" <<'PY' |
| import json, sys, uuid |
|
|
| path = sys.argv[1] |
| with open(path) as fh: |
| bom = json.load(fh) |
|
|
| bom.get("metadata", {}).pop("timestamp", None) |
|
|
| purls = sorted( |
| c.get("purl") or f"{c.get('name')}@{c.get('version')}" |
| for c in bom.get("components", []) |
| ) |
| bom["serialNumber"] = "urn:uuid:" + str( |
| uuid.uuid5(uuid.NAMESPACE_URL, "cyclonedx-sbom:" + "\n".join(purls)) |
| ) |
|
|
| with open(path, "w") as fh: |
| json.dump(bom, fh, indent=2, sort_keys=True) |
| fh.write("\n") |
| PY |
| then ok "SBOM written (normalised β diffs only on real dependency changes)" |
| else fail "SBOM normalisation errored" |
| fi |
| else |
| fail "SBOM generation errored" |
| fi |
| else |
| skip "SBOM β uv not found (https://docs.astral.sh/uv/)" |
| fi |
|
|
| |
| |
| |
| |
| |
| note "pip-audit (known CVEs in $REQ; git dep excluded)" |
| REQ_NOGIT="$(mktemp)"; trap 'rm -f "$REQ_NOGIT"' EXIT |
| grep -v "git+" "$REQ" > "$REQ_NOGIT" |
|
|
| |
| |
| |
| |
| PYBIN="" |
| for cand in python3.13 python3.12 python3.11 python3; do |
| command -v "$cand" >/dev/null 2>&1 || continue |
| td="$(mktemp -d)"; if "$cand" -m venv "$td/probe" >/dev/null 2>&1; then PYBIN="$cand"; rm -rf "$td"; break; fi |
| rm -rf "$td" |
| done |
| if [[ -z "$PYBIN" ]]; then |
| skip "pip-audit β no Python >=3.11 with a working venv found" |
| else |
| VENV="$OUT/.audit-venv" |
| if [[ ! -x "$VENV/bin/pip-audit" ]]; then |
| echo " setting up audit venv ($PYBIN)" |
| "$PYBIN" -m venv "$VENV" && "$VENV/bin/python" -m pip install -q --upgrade pip pip-audit |
| fi |
| "$VENV/bin/python" -m pip_audit --requirement "$REQ_NOGIT" --no-deps \ |
| --format columns | tee "$OUT/pip-audit-$STAMP.txt" |
| AUDIT_RC=${PIPESTATUS[0]} |
| "$VENV/bin/python" -m pip_audit --requirement "$REQ_NOGIT" --no-deps \ |
| --format json --output "$OUT/pip-audit-$STAMP.json" >/dev/null 2>&1 || true |
| if [[ "$AUDIT_RC" -eq 0 ]]; then |
| ok "pip-audit β no known vulnerabilities" |
| else |
| fail "pip-audit reported vulnerabilities (see $OUT/pip-audit-$STAMP.txt). Some" |
| echo " pins (gradio, mcp) are constrained by the HF sdk_version; triage" |
| echo " each in security/ACCEPTED-FINDINGS.md." |
| fi |
| fi |
|
|
| |
| |
| |
| note "bandit (static analysis over src/)" |
| BASELINE="$OUT/bandit-baseline.json" |
| BANDIT=(); if command -v bandit >/dev/null 2>&1; then BANDIT=(bandit); \ |
| elif command -v uvx >/dev/null 2>&1; then BANDIT=(uvx bandit); fi |
| if [[ ${#BANDIT[@]} -eq 0 ]]; then |
| skip "bandit β not installed and uvx unavailable (pip install bandit)" |
| else |
| BARGS=(-r src/ -ll -c bandit.yaml) |
| [[ -f "$BASELINE" ]] && BARGS+=(-b "$BASELINE") |
| "${BANDIT[@]}" "${BARGS[@]}" -f json -o "$OUT/bandit-$STAMP.json" -q |
| BRC=$? |
| "${BANDIT[@]}" "${BARGS[@]}" -q 2>/dev/null | tail -n 20 || true |
| if [[ "$BRC" -eq 0 ]]; then |
| ok "bandit β no new findings beyond the accepted baseline" |
| else |
| fail "bandit reported NEW findings (see $OUT/bandit-$STAMP.json). Fix, or if" |
| echo " intended, annotate inline (# nosec Bxxx) and regenerate the" |
| echo " baseline: make security-baseline." |
| fi |
| fi |
|
|
| |
| |
| |
| note "gitleaks (secrets β working tree + full git history)" |
| if command -v gitleaks >/dev/null 2>&1; then |
| gitleaks detect --config .gitleaks.toml --redact --no-banner \ |
| --report-format json --report-path "$OUT/gitleaks-$STAMP.json" |
| if [[ $? -eq 0 ]]; then |
| ok "gitleaks β no secrets detected in tree or history" |
| else |
| fail "gitleaks detected potential secrets (see $OUT/gitleaks-$STAMP.json)." |
| echo " If a real secret leaked, ROTATE it (ADR-0012/0013) β removing" |
| echo " the commit is not enough." |
| fi |
| else |
| skip "gitleaks β not installed (brew install gitleaks)" |
| fi |
|
|
| |
| |
| |
| note "trivy (sandbox Dockerfile β ADR-0007)" |
| DOCKERFILE="docker/sandbox.Dockerfile" |
| if [[ ! -f "$DOCKERFILE" ]]; then |
| skip "trivy β $DOCKERFILE not found" |
| elif command -v trivy >/dev/null 2>&1; then |
| |
| |
| trivy config --severity HIGH,CRITICAL --exit-code 1 \ |
| --format json --output "$OUT/trivy-$STAMP.json" "$DOCKERFILE" |
| if [[ $? -eq 0 ]]; then |
| ok "trivy β no HIGH/CRITICAL Dockerfile misconfigurations" |
| else |
| fail "trivy flagged the sandbox Dockerfile (see $OUT/trivy-$STAMP.json)." |
| fi |
| else |
| skip "trivy β not installed (brew install trivy)" |
| fi |
|
|
| |
| |
| |
| note "summary" |
| if [[ ${#SKIPS[@]} -gt 0 ]]; then |
| echo "Skipped stages (install the tool to enable):" |
| for s in "${SKIPS[@]}"; do echo " - $s"; done |
| fi |
| if [[ "$RC" -eq 0 ]]; then |
| ok "security scan passed (all enabled stages clean)." |
| else |
| echo "Security scan FAILED (rc=$RC) β triage above. Artifacts in $OUT/." |
| fi |
| exit "$RC" |
|
|