#!/usr/bin/env bash # # Refuse to publish anything that looks like a credential. # # This repository is PUBLIC, and a public git history is # not something you can take back, a leaked key is leaked the moment the push # lands, regardless of what the next commit does. So this runs as a gate before # a push, not as a report afterwards. # # scripts/secret-sweep.sh [dir] # defaults to the package root # # Exits non-zero on the first credible hit. False positives are expected and are # the correct trade here: the cost of one is a person reading a line of output, # and the cost of a miss is a rotation and an incident. set -euo pipefail ROOT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" cd "$ROOT" # Build products and vendored upstream are excluded: .build is compiler output, # and vendor/ is checked separately by the supply-chain digest test. This script # excludes ITSELF by filename rather than by one hardcoded path -- it is the file # with every pattern in it, so a copy living anywhere else reports itself as a # find, and a repo with a `Scripts/` rather than a `scripts/` did exactly that. # `git ls-files .` and not plain `git ls-files`: this subtree may be untracked # in the repo that contains it, and an unscoped call there returns the WHOLE # repository. An empty result means untracked, not empty, fall back to find, # or the sweep silently passes by looking at nothing, which is worse than not # running it at all. # `--others` matters as much as `--cached`. Plain `git ls-files` lists # only TRACKED files, so a brand-new one, which is exactly what a dropped # credential looks like, was invisible to this sweep while `git add -A` staged # it happily. A planted `sk_live_…` in a new file passed the gate. This set is # the one `git add -A` would stage: tracked, plus untracked and not ignored. FILES=$(git ls-files --cached --others --exclude-standard . 2>/dev/null || true) if [ -z "$FILES" ]; then FILES=$(find . -type f -not -path '*/.build/*' -not -path '*/node_modules/*' -not -path '*/.git/*') fi hits=0 probe() { local label="$1" pattern="$2" local found found=$(printf '%s\n' "$FILES" \ | grep -vE '(^|/)vendor/|\.build/|secret-sweep\.sh$' \ | xargs -I{} grep -HnE "$pattern" {} 2>/dev/null || true) if [ -n "$found" ]; then echo "── $label" printf '%s\n' "$found" | head -8 hits=$((hits + 1)) fi } # Shapes, not names: a variable called API_KEY holding "" is fine, and a # variable called `x` holding a live token is not. probe "Stripe live key" 'sk_live_[0-9A-Za-z]{16,}' probe "GitLab token" 'glpat-[0-9A-Za-z_-]{20,}' probe "AWS access key id" 'AKIA[0-9A-Z]{16}' probe "Anthropic key" 'sk-ant-[0-9A-Za-z_-]{20,}' probe "Google API key" 'AIza[0-9A-Za-z_-]{35}' probe "Slack token" 'xox[baprs]-[0-9A-Za-z-]{10,}' probe "private key block" '-----BEGIN (RSA |EC |OPENSSH |PGP )?PRIVATE KEY' probe "JWT" 'eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.' probe "Supabase service role" 'service_role.{0,40}eyJ' probe "assigned password" '(password|passwd|secret|token)[[:space:]]*[:=][[:space:]]*["'"'"'][^"'"'"'{}$][^"'"'"']{7,}["'"'"']' if [ "$hits" -gt 0 ]; then echo echo "secret sweep FAILED: $hits pattern(s) matched. Nothing was published." exit 1 fi echo "secret sweep clean ($(printf '%s\n' "$FILES" | wc -l | tr -d ' ') files)"