From 57133acfba88f4d8007f444fc4db998acb8dc2a4 Mon Sep 17 00:00:00 2001 From: Michael Gilardi <11632652+mgilardi@users.noreply.github.com> Date: Sat, 7 Nov 2026 22:09:21 -0700 Subject: [PATCH] Add a leak check to CI Every public repo should run a leak check. In CI it fails on email addresses that aren't noreply or placeholders and on committed binaries or build output. Run locally, it also checks for private terms read from a file outside the repo (LEAK_PRIVATE_TERMS, default ~/.leak-private-terms), and reports only file and line, never the term. It passes on the current tree without a terms file. The private-terms path was exercised with a stand-in file only; no terms file at the default path exists yet. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 2 ++ scripts/leak-check.sh | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100755 scripts/leak-check.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 230dfeb..9e849c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Leak check (emails and build output; private terms are only checked locally) + run: bash scripts/leak-check.sh - uses: actions/setup-java@v4 with: distribution: temurin diff --git a/scripts/leak-check.sh b/scripts/leak-check.sh new file mode 100755 index 0000000..f0dec9a --- /dev/null +++ b/scripts/leak-check.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Blocks publishing private details. Exit 1 means something was found. +# Always checks for email addresses that aren't noreply or placeholders (example.com, the reserved .example and .test), and for committed binaries or build output. +# Locally it also checks for private terms. They live outside the repo, one per line, matched as whole words +# without regard to case, in the file named by LEAK_PRIVATE_TERMS (default ~/.leak-private-terms). +# The repo's own public names (its org, package scope, owner handle) don't belong in that file. +# CI has no terms file, so there it runs the other checks only. +# Lines containing a phrase from scripts/leak-allow.txt are skipped. Keep that list short and public. +set -u +cd "$(dirname "$0")/.." +TERMS="${LEAK_PRIVATE_TERMS:-$HOME/.leak-private-terms}" +ALLOW=scripts/leak-allow.txt +if git rev-parse --git-dir >/dev/null 2>&1; then + FILES=$(git ls-files --cached --others --exclude-standard) +else + FILES=$(find . -type f -not -path './.git/*' -not -path '*/node_modules/*' | sed 's|^\./||') +fi +FILES=$(echo "$FILES" | grep -v -E '(^|/)scripts/leak-(check\.sh|allow\.txt)$' || true) +[[ -n "$FILES" ]] || { echo "Leak check: nothing to scan"; exit 0; } +allowed() { + if [[ -f "$ALLOW" ]]; then grep -v -F -f <(grep -v -E '^\s*(#|$)' "$ALLOW") || true; else cat; fi +} +scan() { echo "$FILES" | tr '\n' '\0' | xargs -0 grep -I -n "$@" 2>/dev/null; } +fail=0 +if [[ -f "$TERMS" ]]; then + pats=() + while IFS= read -r term; do pats+=(-e "$term"); done < <(grep -v -E '^\s*(#|$)' "$TERMS") + if [[ ${#pats[@]} -gt 0 ]]; then + # Report places only, never the term itself, so the output is safe to paste anywhere. + hits=$(scan -i -w -F "${pats[@]}" | allowed | cut -d: -f1,2) + names=$(echo "$FILES" | grep -i -w -F "${pats[@]}" | allowed) + if [[ -n "$hits$names" ]]; then echo "Private terms found at:"; echo "$names"; echo "$hits" | head -40; fail=1; fi + fi +else + echo "No private terms file, so only checking for email addresses and build output." +fi +emails=$(scan -o -E '[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}' \ + | grep -v -E '@(users\.noreply\.github\.com|example\.(com|org|net)|anthropic\.com)$|\.(example|test|invalid|localhost)$|@[0-9]+x\.(png|jpe?g|gif|webp|svg)$' \ + | allowed) +[[ -n "$emails" ]] && { echo "Email addresses found:"; echo "$emails" | head -20; fail=1; } +built=$(echo "$FILES" | grep -E '\.(jar|class|zip|tgz|dmg|pkg|exe|dll|so|dylib|o|a)$|(^|/)(node_modules|dist|build|out|target|\.build|\.next|\.gradle|DerivedData)/' \ + | grep -v -E '(^|/)gradle-wrapper\.jar$' | allowed) +[[ -n "$built" ]] && { echo "Binaries or build output committed:"; echo "$built" | head -20; fail=1; } +[[ $fail == 0 ]] && echo "Leak check passed ($(echo "$FILES" | wc -l | tr -d ' ') files)" +exit $fail