The hook carried ~50 lines of gitleaks logic and a comment explaining it was self-contained because "this repo has no Makefile". It has one now, so the reason is gone and the arrangement is backwards: a hook is a trigger, and logic belongs where it can be read, run by hand, and changed under review. .githooks/pre-push is now a byte-identical shim onto `make pre-push` in every repo in the workspace. The scan itself moves to ci/secrets.sh unchanged, and `make secrets` runs it on its own. The call surface is identical everywhere; what it runs is not, and should not be — each repo gates what it actually has. That is the point of standardising the name rather than the contents: nobody has to read a repo to find out how to check it. secrets runs first, deliberately. It is the only failure here that cannot be undone by fixing it afterwards — a failed lint costs another commit, a pushed credential is cached and indexed whether or not it is later deleted. Some of these gates fail today, on lint debt that predates them, and they are left wired anyway. The board was measured once and written down in T-56 instead of being worked around here. Narrowing each gate to whatever already passes would produce a gate that reports success for doing nothing, which is the failure this workspace keeps rediscovering. Co-Authored-By: Claude <noreply@anthropic.com>
51 lines
2.1 KiB
Bash
Executable File
51 lines
2.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Secret scan over the commits about to be pushed.
|
|
#
|
|
# Lives here rather than inside .githooks/pre-push so it can be read, run by
|
|
# hand (`make secrets`), and changed under review. A hook is a trigger; it is
|
|
# not a home for logic. Identical in every repo in this workspace (D-27).
|
|
set -euo pipefail
|
|
|
|
cd "$(git rev-parse --show-toplevel)"
|
|
|
|
# A non-login shell — which is what git gives a hook — skips /etc/profile.d
|
|
# and never sees ~/.local/bin, where the gitleaks release tarball lands.
|
|
# Without this the scan reports "not installed" on every push.
|
|
[ -d "$HOME/.local/bin" ] && PATH="$HOME/.local/bin:$PATH"
|
|
|
|
if ! command -v gitleaks >/dev/null 2>&1; then
|
|
echo "FAIL secrets — gitleaks not installed, so this check would be a no-op pretending to pass." >&2
|
|
echo " https://github.com/gitleaks/gitleaks/releases → ~/.local/bin/gitleaks" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Scan the outgoing range, not full history. History here carries findings
|
|
# that are settled — test fixtures and vendored third-party code — and a gate
|
|
# that fails on something unfixable gets bypassed within a week. What matters
|
|
# is what is about to leave this machine.
|
|
if upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null); then
|
|
range="$upstream..HEAD"
|
|
elif git rev-parse --verify --quiet origin/main >/dev/null; then
|
|
range="origin/main..HEAD"
|
|
else
|
|
range=""
|
|
fi
|
|
|
|
if [ -z "$range" ]; then
|
|
gitleaks dir . --redact --no-banner --exit-code 1 || {
|
|
echo "FAIL secrets — gitleaks found a credential in the working tree." >&2; exit 1; }
|
|
exit 0
|
|
fi
|
|
|
|
[ -n "$(git log --oneline "$range" 2>/dev/null)" ] || exit 0
|
|
|
|
gitleaks git . --log-opts="$range" --redact --no-banner --exit-code 1 >/dev/null 2>&1 || {
|
|
echo "FAIL secrets — gitleaks found a credential in the commits being pushed." >&2
|
|
echo " inspect (values redacted): gitleaks git . --log-opts=\"$range\" --redact" >&2
|
|
echo " then remove and rotate it, or suppress deliberately:" >&2
|
|
echo " inline '# gitleaks:allow <reason>'" >&2
|
|
echo " or add the fingerprint to .gitleaksignore WITH a reason" >&2
|
|
exit 1
|
|
}
|
|
echo " ok secrets"
|