#!/usr/bin/env bash set -euo pipefail repo_root="$(git rev-parse --show-toplevel)" cd "$repo_root" artifact_path_regex='(^|/)(docs/baselines/|baselines/|snapshots?/|exports?/|dumps?/|backups?/|diagnostics?/|artifacts?/|tmp/|temp/)|(^|/).*(baseline|snapshot|export|dump|diagnostic).*(\.json|\.ya?ml|\.txt|\.conf)?$' placeholder_regex='(REDACTED|CHANGE_ME||example|placeholder|your[_ -]?token|your[_ -]?key)' empty_tree='4b825dc642cb6eb9a060e54bf8d69288fbee4904' usage() { cat >&2 <<'EOF' usage: scripts/scan-secret-bearing-artifacts.sh --staged scripts/scan-secret-bearing-artifacts.sh --tracked scripts/scan-secret-bearing-artifacts.sh --files ... scripts/scan-secret-bearing-artifacts.sh --git-range scripts/scan-secret-bearing-artifacts.sh --unpublished EOF exit 2 } mode="${1:---staged}" shift || true declare -a files=() declare -a commit_specs=() case "$mode" in --staged) while IFS= read -r -d '' f; do files+=("$f") done < <(git diff --cached --name-only --diff-filter=ACMR -z) ;; --tracked) while IFS= read -r -d '' f; do files+=("$f") done < <(git ls-files -z) ;; --files) [[ $# -ge 1 ]] || usage files=("$@") ;; --git-range) [[ $# -eq 1 ]] || usage while IFS= read -r commit; do [[ -n "$commit" ]] && commit_specs+=("$commit") done < <(git rev-list --reverse "$1") ;; --unpublished) [[ $# -eq 1 ]] || usage while IFS= read -r commit; do [[ -n "$commit" ]] && commit_specs+=("$commit") done < <(git rev-list --reverse "$1" --not --remotes) ;; *) usage ;; esac if [[ ${#files[@]} -eq 0 && ${#commit_specs[@]} -eq 0 ]]; then exit 0 fi python_scan() { local display_name="$1" local source_mode="$2" local source_value="$3" python3 - "$display_name" "$placeholder_regex" "$source_mode" "$source_value" <<'PY' from pathlib import Path import re import subprocess import sys label = sys.argv[1] placeholder = re.compile(sys.argv[2], re.I) source_mode = sys.argv[3] source_value = sys.argv[4] if source_mode == 'file': text = Path(source_value).read_text(errors='ignore') else: text = subprocess.check_output( ['git', 'show', source_value], text=True, errors='ignore', ) lines = text.splitlines() patterns = [ (re.compile(r'"x_wireguard_private_key"\s*:\s*"([^"]+)"', re.I), 'embedded WireGuard private key'), (re.compile(r'"wireguard_client_configuration_file"\s*:\s*"([^"\\]|\\.)*PrivateKey = [A-Za-z0-9+/=]{20,}', re.I), 'embedded WireGuard client config with private key'), (re.compile(r'PrivateKey\s*=\s*([A-Za-z0-9+/=]{20,})'), 'WireGuard private key line'), (re.compile(r'BEGIN [A-Z0-9 ]*PRIVATE KEY', re.I), 'private key block header'), (re.compile(r'Authorization\s*[:=]\s*(Bearer\s+)?[A-Za-z0-9._~+/=-]{16,}', re.I), 'authorization header/token material'), (re.compile(r'"(token|api[_-]?key|client[_-]?secret|password|secret|cookie)"\s*:\s*"([^"]{12,})"', re.I), 'suspicious secret-like JSON field'), ] for idx, line in enumerate(lines, start=1): for regex, finding in patterns: match = regex.search(line) if not match: continue if placeholder.search(line): continue print(f'{label}:{idx}: {finding}') print(f' {line[:220]}') sys.exit(1) sys.exit(0) PY } scan_worktree_file() { local file="$1" [[ -f "$file" ]] || return 0 [[ "$file" =~ $artifact_path_regex ]] || return 0 python_scan "$file" file "$file" } scan_commit_file() { local commit="$1" local file="$2" git cat-file -e "$commit:$file" 2>/dev/null || return 0 [[ "$file" =~ $artifact_path_regex ]] || return 0 python_scan "$commit:$file" git_object "$commit:$file" } failures=0 if [[ ${#files[@]} -gt 0 ]]; then for file in "${files[@]}"; do if ! scan_worktree_file "$file"; then failures=1 fi done fi if [[ ${#commit_specs[@]} -gt 0 ]]; then declare -A seen=() for commit in "${commit_specs[@]}"; do parent="$empty_tree" if git rev-parse --verify "$commit^" >/dev/null 2>&1; then parent="$commit^" fi while IFS= read -r -d '' file; do key="$commit:$file" [[ -n "$file" ]] || continue [[ -n "${seen[$key]:-}" ]] && continue seen[$key]=1 if ! scan_commit_file "$commit" "$file"; then failures=1 fi done < <(git diff --name-only --diff-filter=ACMR -z "$parent" "$commit") done fi if [[ "$failures" -ne 0 ]]; then cat >&2 <<'EOF' Secret-bearing artifact scan failed. This guardrail is intentionally narrow: it is meant to stop raw controller/export/baseline artifacts from being committed or pushed with embedded keys or tokens. Fix options: - remove the raw artifact from the commit - replace it with a sanitized markdown summary - explicitly redact the secret-bearing fields If raw retention is required, store it outside the main repo or in the encrypted secrets repo. EOF exit 1 fi