From 8c56d13e336a8852bcc669babe9231663b40a638 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 12 Oct 2026 19:00:06 +0200 Subject: [PATCH 1/7] test(scripts): add git-secrets allowlist self-test (#2170) Ported from cloud-commitments-mcp, plus a structural check that every .gitallowed entry is anchored and must-catch cases with a placeholder and a secret on one line. Co-Authored-By: Claude Sonnet 5.5 --- scripts/test-git-secrets-allowlist.sh | 191 ++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100755 scripts/test-git-secrets-allowlist.sh diff --git a/scripts/test-git-secrets-allowlist.sh b/scripts/test-git-secrets-allowlist.sh new file mode 100755 index 000000000..712a31ab6 --- /dev/null +++ b/scripts/test-git-secrets-allowlist.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# Self-test for the git-secrets allowlist (#1972). Exercises the real +# scripts/setup-git-secrets.sh and .gitallowed in a throwaway repo, both +# directly via `git secrets --scan --cached` and through the installed +# pre-commit hook, in both directions: fixtures that must be caught, and +# fixtures that must scan clean. +# +# Fixture safety: every positive fixture below is a secret-shaped string +# that this script itself, and the repo's detect-private-key / git-secrets +# pre-commit hooks, would flag if it appeared as a contiguous literal. Each +# is assembled at runtime from adjacent string literals so this file's own +# source never contains a scannable token. Where a fixture shows \n below, +# it is the two literal characters backslash and n, as in a Go string +# literal, never an actual newline. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if ! command -v git-secrets >/dev/null 2>&1; then + echo "git-secrets not found on PATH; install it before running this test" >&2 + exit 1 +fi + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +mkdir -p "$tmp/home" +# Isolate HOME: --register-aws reads ~/.aws/credentials, and git-secrets +# echoes the whole joined pattern list on a regex error, which would print +# real credentials into this log. +export HOME="$tmp/home" + +git init -q "$tmp/repo" +mkdir -p "$tmp/repo/scripts" +cp "$REPO_ROOT/scripts/setup-git-secrets.sh" "$tmp/repo/scripts/setup-git-secrets.sh" +cp "$REPO_ROOT/.gitallowed" "$tmp/repo/.gitallowed" +cd "$tmp/repo" + +if ! bash scripts/setup-git-secrets.sh > "$tmp/setup.log" 2>&1; then + echo "scripts/setup-git-secrets.sh failed in the throwaway repo:" >&2 + cat "$tmp/setup.log" >&2 + exit 1 +fi +# Same HOME-isolation reason: drop the provider before any further scan. +git config --unset-all secrets.providers + +# The pre-commit hook ignores the args git would pass it and recomputes its +# own file list from the index (diff against HEAD, or the empty tree if +# there is no HEAD yet, which is always true in this never-committed +# throwaway repo). So it scans every currently staged path, not just +# $relpath -- calling it with no args, as git itself does, is correct. +HOOK_DIR="$(git rev-parse --git-dir)/hooks" + +pass=0 +fail=0 + +# Records one pass/fail against $expected, printing a FAIL line labeled +# $label on mismatch. Shared by both scan paths in run_case/run_staged_case +# so a direct-scan/hook disagreement surfaces as its own labeled failure +# instead of being silently reconciled. +check_result() { + local label=$1 expected=$2 actual=$3 + if [ "$actual" -eq "$expected" ]; then + pass=$((pass + 1)) + else + fail=$((fail + 1)) + echo "FAIL: $label (expected exit $expected, got $actual)" >&2 + fi +} + +# Writes $content to $relpath, stages it, scans it two ways -- directly via +# `git secrets --scan --cached` (exact file argument) and via the installed +# pre-commit hook (the real path a developer's commit takes) -- compares +# each exit code to $expected, then unstages and removes it. +run_case() { + local label=$1 expected=$2 relpath=$3 content=$4 + mkdir -p "$(dirname "$relpath")" + printf '%s\n' "$content" >"$relpath" + git add "$relpath" + local actual=0 + git secrets --scan --cached "$relpath" >/dev/null 2>&1 || actual=$? + check_result "$label (direct scan)" "$expected" "$actual" + local hook_actual=0 + "$HOOK_DIR/pre-commit" >/dev/null 2>&1 || hook_actual=$? + check_result "$label (installed hook)" "$expected" "$hook_actual" + git rm -q --cached "$relpath" + rm -f "$relpath" +} + +# Same as run_case, for a file already on disk (the copied setup script and +# .gitallowed), left staged afterward like any other tracked file would be. +run_staged_case() { + local label=$1 expected=$2 relpath=$3 + git add "$relpath" + local actual=0 + git secrets --scan --cached "$relpath" >/dev/null 2>&1 || actual=$? + check_result "$label (direct scan)" "$expected" "$actual" + local hook_actual=0 + "$HOOK_DIR/pre-commit" >/dev/null 2>&1 || hook_actual=$? + check_result "$label (installed hook)" "$expected" "$hook_actual" +} + +# Fixtures, assembled from adjacent literals so this file scans clean. +KEY="AKIA""0123456789ABCDEF" +PEM="-----BEGIN ""PRIVATE KEY-----" +AIZA="AIza$(head -c 35 /dev/zero | tr '\0' X)" + +# Must be caught (exit 1): the #1972 hole and the coverage this PR adds. +run_case "terraform resource with a real-shaped key" 1 a.tf \ + 'resource "aws_iam_access_key" "x" { key = "'"$KEY"'" }' +run_case "terraform var reference alongside a key" 1 b.tf \ + 'secret = var.x # '"$KEY" +run_case "go test const with a key" 1 c_test.go \ + 'const k = "'"$KEY"'"' +run_case "testdata fixture with a key" 1 testdata/creds.txt \ + 'key='"$KEY" +run_case "placeholder line with a key" 1 d.txt \ + 'placeholder '"$KEY" +run_case "untruncated PKCS8 body" 1 e.json \ + '"private_key": "'"$PEM"'\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC\n"' +run_case "corrected GCP API key range" 1 f.txt \ + "$AIZA" +run_case "password with real spaces around the separator" 1 n.txt \ + 'password = "hunter2hunter2"' +run_case "secret before an otherwise-allowed truncated PEM, same line" 1 l.go \ + 'k := "'"$KEY"'"; PrivateKey: "'"$PEM"'\n...",' +run_case "secret after an otherwise-allowed truncated PEM, same line" 1 m.go \ + '"private_key": "'"$PEM"'\nMIIEvQIBADANBg...\n'"$KEY"'"' + +# Negative controls for the setup-script allowlist entries below, each +# closing one instance of the same class of hole (#1972 reintroduced at +# the scale of one file) that a narrower-but-still-wrong anchor leaves +# open. Both mutate the tracked copy of the setup script in place at its +# real path, since the allowlist entries are anchored on that path, and +# restore the pristine copy afterward so the later self-scan case below +# sees it intact. + +# A secret appended to a DIFFERENT git-secrets registration line in that +# file (the GCP one, not one of the three that legitimately self-match) +# must still be caught. A prefix-only entry anchored on just "git secrets +# --add '" would hide this. +run_case "secret appended to an unrelated registration line stays caught" 1 \ + scripts/setup-git-secrets.sh \ + "$(sed "s/# GCP API Key\$/# GCP API Key ${KEY}/" "$REPO_ROOT/scripts/setup-git-secrets.sh")" +cp "$REPO_ROOT/scripts/setup-git-secrets.sh" scripts/setup-git-secrets.sh + +# A secret appended to the END of one of the three self-matching lines +# THEMSELVES must also still be caught. An entry anchored on the path and +# pattern but missing a trailing $ would stop matching at the end of the +# pattern it names and let anything appended after it through, which is a +# narrower version of the same hole the entry above closes. +run_case "secret appended to a self-matching line itself stays caught" 1 \ + scripts/setup-git-secrets.sh \ + "$(sed "s/# PostgreSQL\$/# PostgreSQL ${KEY}/" "$REPO_ROOT/scripts/setup-git-secrets.sh")" +cp "$REPO_ROOT/scripts/setup-git-secrets.sh" scripts/setup-git-secrets.sh + +# Must scan clean (exit 0): what the tree contains, and what the allowlist +# must keep clean. +run_staged_case "setup script's own self-matching lines" 0 scripts/setup-git-secrets.sh +run_case "GCP type marker alone, no key material" 0 g.json \ + '{"type": "service_account", "project_id": "p"}' +run_case "truncated PEM in a struct literal" 0 i.go \ + 'PrivateKey: "'"$PEM"'\n...",' +run_case "truncated PEM in a JSON literal" 0 j.go \ + '"private_key": "'"$PEM"'\nMIIEvQIBADANBg...\n",' +run_case "PEM word fragments are not a real header" 0 k.txt \ + 'this is PRIVATE data +algo EC here +-----BEGIN CERTIFICATE-----' +run_staged_case ".gitallowed self-scan" 0 .gitallowed + +# Structural check (#2170): every active .gitallowed entry must be anchored +# on the scanner's "path:line:content" output with ^ and $, so no entry can +# whitelist a whole line by containing a bare token. +unanchored=$(grep -vE '^[[:space:]]*(#|$)' "$REPO_ROOT/.gitallowed" | grep -vE '^\^.*\$$' || true) +if [ -n "$unanchored" ]; then + fail=$((fail + 1)) + echo "FAIL: unanchored .gitallowed entries:" >&2 + echo "$unanchored" >&2 +else + pass=$((pass + 1)) +fi + +# Placeholder-shaped account IDs next to a real-shaped key must be caught. +ACCT="1234""56789012" +run_case "account-id placeholder with a key on the same line" 1 o.go \ + 'accountId := "'"$ACCT"'"; k := "'"$KEY"'"' +run_case "UUID placeholder with a password on the same line" 1 p.go \ + 'id := "11111111-1111-1111-1111-111111111111"; password = "hunter2hunter2"' + +echo "$pass PASS, $fail FAIL" +[ "$fail" -eq 0 ] From 52ecf67d7bdcf27a2352c3857f77bdcd3c9fbf0c Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 12 Oct 2026 19:00:08 +0200 Subject: [PATCH 2/7] fix(scripts): drop keyword allowlist from git-secrets setup, fix install hang (#2170) Port of the mcp fix. The keyword --allowed entries matched whole path:line:content lines. A no-op say() shim stops git-secrets calling macOS /usr/bin/say, which hung the install; hooks are verified after install, and the PEM and regex detectors are fixed. Co-Authored-By: Claude Sonnet 5.5 --- scripts/setup-git-secrets.sh | 94 +++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 39 deletions(-) diff --git a/scripts/setup-git-secrets.sh b/scripts/setup-git-secrets.sh index 16cbf7634..f27fa86a3 100755 --- a/scripts/setup-git-secrets.sh +++ b/scripts/setup-git-secrets.sh @@ -30,6 +30,26 @@ echo -e "${GREEN}✓ git-secrets is installed${NC}" echo "" # Install git hooks +# +# git-secrets 1.3.0's install_hook() writes and chmods the hook file, then +# reports success via a `say` call. `say` was never its own function: +# git-secrets sources git's git-sh-setup and relied on `say` being defined +# there (introduced in git-sh-setup.sh in 2009). Git removed it in commit +# 5b893f7d81 ("git-sh-setup.sh: remove 'say' function, change last users"), +# first shipped in Git 2.38 (2022), because it was undocumented and unused +# within git's own tree, breaking git-secrets as an unintended side effect +# of a git upgrade rather than a git-secrets regression. On macOS the bare +# `say` call resolves to /usr/bin/say (the text-to-speech binary) instead +# and exits 0 by accident; on Linux (or on a new-enough git anywhere) there +# is no such fallback, so it's "command not found" and `git secrets +# --install -f` returns non-zero even though every hook file was already +# written correctly. Define `say` as a no-op here and export it so the +# exported function is visible in the git-secrets child process on both +# platforms, making the real hook-writing exit status the one that reaches +# the check below. +say() { :; } +export -f say + echo "Installing git-secrets hooks..." if git secrets --install -f; then echo -e "${GREEN}✓ Git hooks installed${NC}" @@ -38,6 +58,32 @@ else exit 1 fi +# The no-op say() above means "git secrets --install -f" returning success no +# longer proves anything: with say() removed the real signal was its exit +# status, but say() itself is a no-op that returns success regardless of +# whether install_hook actually wrote the file (e.g. a permissions error on +# chmod). Verify what the command was supposed to do instead: each hook file +# it writes exists, is executable, and contains the git-secrets invocation. +git_dir="$(git rev-parse --git-dir)" +hooks_ok=1 +for hook_spec in "commit-msg:commit_msg_hook" "pre-commit:pre_commit_hook" "prepare-commit-msg:prepare_commit_msg_hook"; do + hook_name="${hook_spec%%:*}" + hook_cmd="${hook_spec##*:}" + hook_path="${git_dir}/hooks/${hook_name}" + if [ -d "${git_dir}/hooks/${hook_name}.d" ]; then + hook_path="${git_dir}/hooks/${hook_name}.d/git-secrets" + fi + if [ ! -x "${hook_path}" ] || ! grep -qF "git secrets --${hook_cmd} -- \"\$@\"" "${hook_path}"; then + echo -e "${RED}✗ ${hook_name} hook missing, not executable, or doesn't invoke git-secrets: ${hook_path}${NC}" + hooks_ok=0 + fi +done +if [ "${hooks_ok}" -ne 1 ]; then + echo -e "${RED}✗ Hook verification failed; git-secrets would not actually run on commit${NC}" + exit 1 +fi +echo -e "${GREEN}✓ Verified all three hooks are installed and executable${NC}" + # Register AWS secret patterns echo "" echo "Registering AWS secret patterns..." @@ -53,56 +99,26 @@ git secrets --add '[^A-Za-z0-9/+=]{40}[^A-Za-z0-9/+=]' # AWS Sec git secrets --add 'aws(.{0,20})?['\''"][0-9a-zA-Z/+]{40}['\''"]' # AWS Credentials # GCP patterns -git secrets --add 'type.*service_account' # GCP Service Account JSON -git secrets --add 'AIza[0-9A-Za-z-_]{35}' # GCP API Key +git secrets --add 'AIza[0-9A-Za-z_-]{35}' # GCP API Key # Azure patterns git secrets --add 'DefaultEndpointsProtocol=https' # Azure Connection String # Generic secrets (require quoted values to avoid matching variable declarations) -git secrets --add 'password\s*[=:]\s*['\''"][^'\''"]{8,}' # Password with quoted value -git secrets --add 'api[_-]?key\s*[=:]\s*['\''"][^'\''"]{8,}' # API key with quoted value -git secrets --add 'secret[_-]?key\s*[=:]\s*['\''"][^'\''"]{8,}' # Secret key with quoted value -git secrets --add '-----BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY-----' # PEM private keys +git secrets --add 'password[[:space:]]*[=:][[:space:]]*['\''"][^'\''"]{8,}' # Password with quoted value +git secrets --add 'api[_-]?key[[:space:]]*[=:][[:space:]]*['\''"][^'\''"]{8,}' # API key with quoted value +git secrets --add 'secret[_-]?key[[:space:]]*[=:][[:space:]]*['\''"][^'\''"]{8,}' # Secret key with quoted value +git secrets --add 'BEGIN[[:space:]]((RSA|DSA|EC|OPENSSH|ENCRYPTED)[[:space:]])?PRIVATE[[:space:]]KEY-----' # PEM private keys # Database connection strings git secrets --add 'postgres://[^:]+:[^@]+@' # PostgreSQL git secrets --add 'mysql://[^:]+:[^@]+@' # MySQL git secrets --add 'mongodb(\+srv)?://[^:]+:[^@]+@' # MongoDB -# Add allowed patterns (things that look like secrets but aren't) -echo "" -echo "Adding allowed patterns (false positives)..." - -# Terraform variables and outputs -git secrets --add --allowed 'var\.' -git secrets --add --allowed 'local\.' -git secrets --add --allowed 'output\.' -git secrets --add --allowed 'data\.' - -# Test files -git secrets --add --allowed '_test\.go' -git secrets --add --allowed 'testdata/' -git secrets --add --allowed 'test_password' -git secrets --add --allowed 'test_secret' - -# Documentation and examples -git secrets --add --allowed 'example\.com' -git secrets --add --allowed 'YOUR_' -git secrets --add --allowed ' Date: Mon, 12 Oct 2026 19:00:10 +0200 Subject: [PATCH 3/7] sec(scripts): remove bare placeholder entries from .gitallowed (#2170) Bare account-ID/UUID entries exempted any line containing them from every detector. Keep only path-and-line anchored entries. Closes #2170 Co-Authored-By: Claude Sonnet 5.5 --- .gitallowed | 89 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/.gitallowed b/.gitallowed index 6ff918dde..d0e04e6e0 100644 --- a/.gitallowed +++ b/.gitallowed @@ -10,39 +10,60 @@ # DO NOT add a real account ID here. If a real account ID lands in the repo, # rotate it and treat the leak seriously instead of silencing the scanner. # -# Format: one regex per line, matched against each line of file content -# (path-scoping is not supported by .gitallowed). +# Format: one regex per line, matched against the scanner's whole +# "path:line:content" output line (not just the file content), so an entry +# may anchor on the path with "^path:[0-9]+:". Every entry must still be a +# synthetic literal or one known benign line, never a bare keyword: a +# keyword whitelists every line that happens to contain it (#1972). -# Sequential test placeholders (ascending and descending). -123456789012 -210987654321 - -# All-same-digit blocks of 12 — used as table-row distinctions in fixtures. -# Listed explicitly rather than via a regex back-reference because not every -# git-secrets build supports back-refs in its allowlist regexes. -000000000000 -111111111111 -222222222222 -333333333333 -444444444444 -555555555555 -666666666666 -777777777777 -888888888888 -999999999999 - -# Group-block placeholders used in cmd/helpers_test.go variants and -# cmd/multi_service_helpers_test.go. -111222333444 -555666777888 -999888777666 -# Descending-step fixture added in #956 tests (store_postgres_pgxmock_test.go, -# handler_analytics_test.go, handler_history_test.go, handler_dashboard_test.go). -999988887777 -# Repeating-pair-block fixture used in handler_analytics_test.go, -# handler_inventory_test.go, store_postgres_pgxmock_test.go. -111122223333 +# scripts/setup-git-secrets.sh registers its detectors as literal regex text +# and three of them (Azure connection string, PostgreSQL and MySQL DSN +# detectors, verified empirically -- MongoDB's does not) match their own +# registration line. Each entry below is anchored on the path, the line +# number, AND the entire line as it appears in that file (including the +# trailing comment), with a trailing $ -- not just the "git secrets --add +# '" command prefix, and not just the added pattern with the anchor left +# open at the end. A prefix-only anchor whitelists a secret appended to +# ANY registration line in this file; an anchor missing the trailing $ +# whitelists a secret appended to the END of one of these three specific +# lines. Both are the #1972 hole reintroduced at a smaller scale, and both +# were caught only by a fixture exercising exactly that mutation -- add +# one for any future entry in this block that isn't already covered. +# +# Each entry below also replaces one letter of its own trigger substring +# with a single-character bracket expression, e.g. "Protoco[l]" for +# "Protocol". This is a no-op for matching (as a regex, [l] matches the +# literal letter l exactly like l would) but it breaks the contiguous +# trigger span each entry is built from, so .gitallowed does not need to +# allow-list ITSELF against the very patterns these entries exist to +# suppress (confirmed empirically: without this, the .gitallowed +# self-scan case below fails -- and note this comment must not spell any +# of those spans out literally either, for the same reason). +^scripts/setup-git-secrets\.sh:[0-9]+:git secrets --add 'DefaultEndpointsProtoco[l]=https' # Azure Connection String$ +^scripts/setup-git-secrets\.sh:[0-9]+:git secrets --add 'postgre[s]://\[\^:\]\+:\[\^@\]\+@' # PostgreSQL$ +^scripts/setup-git-secrets\.sh:[0-9]+:git secrets --add 'mysq[l]://\[\^:\]\+:\[\^@\]\+@' # MySQL$ -# UUID-shaped account ID used in handler_accounts_test.go (synthetic, not a -# real subscription/account). -11111111-1111-1111-1111-111111111111 +# Truncated PEM placeholder used by scripts/test-git-secrets-allowlist.sh +# (and by any fixture in this repo needing a non-secret PEM-shaped string): +# a PEM header immediately followed by "\n..." (optionally with a short +# base64 prefix) is never a real key body, so a line consisting of EXACTLY +# a "PrivateKey:" or "private_key" assignment to that shape is safe. +# +# Anchored on the WHOLE scanner output line, not just the quoted value: +# .gitallowed suppresses a line if ANY allowed pattern matches anywhere in +# it, regardless of which prohibited pattern fired, so a real secret in an +# earlier, unrelated statement on the same physical line as this entry's +# fixture would otherwise be suppressed too, even though the entry has +# nothing to do with that secret (a fixture below exercises exactly this: +# a key assigned before the fixture, on the same scanner line, must still +# be caught -- note this comment must not spell the fixture out as a +# literal contiguous string either, for the same self-scan reason). +# ^[^:]+:[0-9]+: pins the match to the start of git-secrets' own +# "path:line:content" format, so the key assignment must be the first thing +# on the line (only leading whitespace allowed), and the trailing $ pins the +# tail to only the closing quote/comma this repo's fixtures actually use. +# Both ends are load-bearing -- do not loosen either back to an open match. +# +# One letter of "PRIVATE" is bracketed for the same self-scan reason the +# block above explains -- do not tidy it back to a bare word. +^[^:]+:[0-9]+:[[:space:]]*("private_key"|PrivateKey):[[:space:]]*"-----BEGIN PRIVAT[E] KEY-----\\n(MIIEvQIBADANBg)?\.\.\.(\\n)?",?$ From 89ec263b370b1ef51193742add2f0f940521f961 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 12 Oct 2026 19:11:17 +0200 Subject: [PATCH 4/7] ci: pin git-secrets and run the allowlist self-test (#2170) Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/pre-commit.yml | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index b5fddfa29..b933056e4 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -83,14 +83,25 @@ jobs: sh /tmp/trivy-install.sh -b /usr/local/bin "${TRIVY_VERSION}" - name: Install git-secrets - # Pinned to a release tag rather than master HEAD. After install - # we register the AWS pattern set and ASSERT at least one pattern - # was registered — without the assert, a registration failure - # produces a patternless scanner that exits 0 unconditionally, - # leaving the gate silently downgraded. + # Pinned to a release tag rather than master HEAD. The tag is + # mutable in principle, so also assert HEAD resolves to the exact + # commit verified at the time this was written (`git ls-remote + # --tags` against the upstream repo) before trusting it enough to + # `sudo make install`. After install we register the AWS pattern + # set and ASSERT at least one pattern was registered — without the + # assert, a registration failure produces a patternless scanner + # that exits 0 unconditionally, leaving the gate silently + # downgraded. + env: + GIT_SECRETS_SHA: ad82d68ee924906a0401dfd48de5057731a9bc84 run: | set -euo pipefail git clone --depth 1 --branch 1.3.0 https://github.com/awslabs/git-secrets.git /tmp/git-secrets + resolved="$(git -C /tmp/git-secrets rev-parse HEAD)" + if [ "${resolved}" != "${GIT_SECRETS_SHA}" ]; then + echo "git-secrets 1.3.0 tag resolved to ${resolved}, expected ${GIT_SECRETS_SHA}" >&2 + exit 1 + fi sudo make -C /tmp/git-secrets install git secrets --register-aws --global git secrets --list --global | grep -q '.' || { @@ -98,6 +109,12 @@ jobs: exit 1 } + - name: Run git-secrets allowlist self-test + # Asserts, through the real pre-commit hook scan path, that + # secret-shaped fixtures are still caught and that the documented + # false positives still scan clean (#2170). + run: bash scripts/test-git-secrets-allowlist.sh + # Note: the local `hadolint` hook in .pre-commit-config.yaml (search for # `id: hadolint`; no line number, because this comment has already gone # stale twice as that file shifted) runs ghcr.io/hadolint/hadolint pinned From 5806c3c375570dce9a03255bf6451884e74ef3ce Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 12 Oct 2026 19:12:10 +0200 Subject: [PATCH 5/7] fix(scripts): anchor the remaining benign git-secrets hits (#2170) The repo-wide scan in setup-git-secrets.sh flags separator lines and truncated PEM literals once the bare entries are gone. Each is anchored on path and whole line. Fixtures in the self-test no longer spell a password assignment literally. Co-Authored-By: Claude Sonnet 5.5 --- .gitallowed | 13 +++++++++++++ scripts/test-git-secrets-allowlist.sh | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.gitallowed b/.gitallowed index d0e04e6e0..7303d31d4 100644 --- a/.gitallowed +++ b/.gitallowed @@ -67,3 +67,16 @@ # One letter of "PRIVATE" is bracketed for the same self-scan reason the # block above explains -- do not tidy it back to a bare word. ^[^:]+:[0-9]+:[[:space:]]*("private_key"|PrivateKey):[[:space:]]*"-----BEGIN PRIVAT[E] KEY-----\\n(MIIEvQIBADANBg)?\.\.\.(\\n)?",?$ + +# Benign lines the setup script's 40-character-run detector and PEM +# detector flag in this repo's tree: separator rules and box-drawing log +# banners (no letters or digits), and truncated or placeholder PEM literals +# in two test files. Each is anchored on path and the entire line, with a +# bracketed letter in PRIVAT[E] for the same self-scan reason as above. +^(SECURITY\.md|\.github/workflows/README\.md):[0-9]+:\|[-| ]+\|$ +^cmd/[a-z_]+\.go:[0-9]+:[[:space:]]*(fmt|AppLogger)\.Println\("-+"\)$ +^cmd/multi_service(_helpers)?\.go:[0-9]+:[[:space:]]*AppLogger\.Printf\("(\\n)?(━)+\\n"\)$ +^cmd/configure_gcp_test\.go:[0-9]+:// --- addMemberToPolicyBinding -+$ +^cmd/configure_test\.go:[0-9]+:[[:space:]]*assert\.Equal\(t, "-----BEGIN PRIVAT[E] KEY-----\\n\.\.\.", creds\.PrivateKey\)$ +^cmd/configure_test\.go:[0-9]+:[[:space:]]*"private_key": "-----BEGIN PRIVAT[E] KEY-----\\nMIIEvQIBADANBg\.\.\.\\n-----END PRIVATE KEY-----\\n",$ +^cmd/configure_gcp_test\.go:[0-9]+:[[:space:]]*keyMaterial := \[\]byte\(`\{"type":"service_account","project_id":"proj","client_email":"sa@proj\.iam\.gserviceaccount\.com","private_key":"-----BEGIN PRIVAT[E] KEY-----\\nx\\n-----END PRIVATE KEY-----\\n"\}`\)$ diff --git a/scripts/test-git-secrets-allowlist.sh b/scripts/test-git-secrets-allowlist.sh index 712a31ab6..86702ef14 100755 --- a/scripts/test-git-secrets-allowlist.sh +++ b/scripts/test-git-secrets-allowlist.sh @@ -120,7 +120,7 @@ run_case "untruncated PKCS8 body" 1 e.json \ run_case "corrected GCP API key range" 1 f.txt \ "$AIZA" run_case "password with real spaces around the separator" 1 n.txt \ - 'password = "hunter2hunter2"' + 'pass''word = "hunter2hunter2"' run_case "secret before an otherwise-allowed truncated PEM, same line" 1 l.go \ 'k := "'"$KEY"'"; PrivateKey: "'"$PEM"'\n...",' run_case "secret after an otherwise-allowed truncated PEM, same line" 1 m.go \ @@ -185,7 +185,7 @@ ACCT="1234""56789012" run_case "account-id placeholder with a key on the same line" 1 o.go \ 'accountId := "'"$ACCT"'"; k := "'"$KEY"'"' run_case "UUID placeholder with a password on the same line" 1 p.go \ - 'id := "11111111-1111-1111-1111-111111111111"; password = "hunter2hunter2"' + 'id := "11111111-1111-1111-1111-111111111111"; pass''word = "hunter2hunter2"' echo "$pass PASS, $fail FAIL" [ "$fail" -eq 0 ] From 1f4d6f7f51d30149bcccc8f8426e532a2b4b3a46 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 12 Oct 2026 19:21:50 +0200 Subject: [PATCH 6/7] fix(scripts): anchor the account-ID fixtures the CI scanner flags (#2170) The AWS account-ID detector in CI flags placeholder IDs in Go test tables once the bare entries are gone. Each is anchored on path, placeholder digits and whole line; a self-test case checks an appended key is still caught. Co-Authored-By: Claude Sonnet 5.5 --- .gitallowed | 9 +++++++++ scripts/test-git-secrets-allowlist.sh | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/.gitallowed b/.gitallowed index 7303d31d4..e9056c6d9 100644 --- a/.gitallowed +++ b/.gitallowed @@ -80,3 +80,12 @@ ^cmd/configure_test\.go:[0-9]+:[[:space:]]*assert\.Equal\(t, "-----BEGIN PRIVAT[E] KEY-----\\n\.\.\.", creds\.PrivateKey\)$ ^cmd/configure_test\.go:[0-9]+:[[:space:]]*"private_key": "-----BEGIN PRIVAT[E] KEY-----\\nMIIEvQIBADANBg\.\.\.\\n-----END PRIVATE KEY-----\\n",$ ^cmd/configure_gcp_test\.go:[0-9]+:[[:space:]]*keyMaterial := \[\]byte\(`\{"type":"service_account","project_id":"proj","client_email":"sa@proj\.iam\.gserviceaccount\.com","private_key":"-----BEGIN PRIVAT[E] KEY-----\\nx\\n-----END PRIVATE KEY-----\\n"\}`\)$ + +# Synthetic 12-digit account-ID fixtures (sequential or repeated-digit +# blocks, none a real account) that the AWS account-ID detector flags in Go +# test tables. Each entry pins the path, the placeholder digits and the +# whole line, so anything else on the line is still scanned. +^cmd/(helpers_test|multi_service_filters_test|multi_service_test)\.go:[0-9]+:[[:space:]]*(accountID|Account):[[:space:]]*"(123456789012|999888777666|111222333444)",$ +^cmd/multi_service_helpers_test\.go:[0-9]+:[[:space:]]*\{Account: "(123456789012|210987654321|111222333444|555666777888)", AccountName: ""\},( // Same account - should use cache)?$ +^cmd/savings_plan_purchase_test\.go:[0-9]+:[[:space:]]*(Service: common\.ServiceSavingsPlansCompute, )?(Count: 1, )?Account: "111111111111", Term: "1yr", PaymentOption: "no-upfront",$ +^cmd/savings_plan_purchase_test\.go:[0-9]+:[[:space:]]*"account": func\(r \*common\.Recommendation\) \{ r\.Account = "222222222222" \},$ diff --git a/scripts/test-git-secrets-allowlist.sh b/scripts/test-git-secrets-allowlist.sh index 86702ef14..f9950a188 100755 --- a/scripts/test-git-secrets-allowlist.sh +++ b/scripts/test-git-secrets-allowlist.sh @@ -186,6 +186,10 @@ run_case "account-id placeholder with a key on the same line" 1 o.go \ 'accountId := "'"$ACCT"'"; k := "'"$KEY"'"' run_case "UUID placeholder with a password on the same line" 1 p.go \ 'id := "11111111-1111-1111-1111-111111111111"; pass''word = "hunter2hunter2"' +# A secret appended to an otherwise-allowed account-ID fixture line stays caught. +run_case "key appended to an allowed account-ID fixture line" 1 \ + cmd/helpers_test.go \ + ' accountID: "123456789012", k := "'"$KEY"'"' echo "$pass PASS, $fail FAIL" [ "$fail" -eq 0 ] From a29d9ce42d6df89d2acb0c373f82a5f481f57214 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 12 Oct 2026 19:30:54 +0200 Subject: [PATCH 7/7] fix(scripts): bracket placeholder digits in allowlist, add file-argument scan to self-test (#2170) The new account-ID entries and one self-test fixture were themselves flagged by the AWS account-ID pattern under the file-argument scan the pre-commit framework uses. Bracket one digit per entry, build the fixture from $ACCT, and make the self-test run that CI-equivalent scan over all tracked files. Correct the say() comment: on macOS /usr/bin/say can hang on audio output. Co-Authored-By: Claude Sonnet 5.5 --- .gitallowed | 4 ++-- scripts/setup-git-secrets.sh | 5 +++-- scripts/test-git-secrets-allowlist.sh | 12 +++++++++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.gitallowed b/.gitallowed index e9056c6d9..1ea968622 100644 --- a/.gitallowed +++ b/.gitallowed @@ -87,5 +87,5 @@ # whole line, so anything else on the line is still scanned. ^cmd/(helpers_test|multi_service_filters_test|multi_service_test)\.go:[0-9]+:[[:space:]]*(accountID|Account):[[:space:]]*"(123456789012|999888777666|111222333444)",$ ^cmd/multi_service_helpers_test\.go:[0-9]+:[[:space:]]*\{Account: "(123456789012|210987654321|111222333444|555666777888)", AccountName: ""\},( // Same account - should use cache)?$ -^cmd/savings_plan_purchase_test\.go:[0-9]+:[[:space:]]*(Service: common\.ServiceSavingsPlansCompute, )?(Count: 1, )?Account: "111111111111", Term: "1yr", PaymentOption: "no-upfront",$ -^cmd/savings_plan_purchase_test\.go:[0-9]+:[[:space:]]*"account": func\(r \*common\.Recommendation\) \{ r\.Account = "222222222222" \},$ +^cmd/savings_plan_purchase_test\.go:[0-9]+:[[:space:]]*(Service: common\.ServiceSavingsPlansCompute, )?(Count: 1, )?Account: "11111111111[1]", Term: "1yr", PaymentOption: "no-upfront",$ +^cmd/savings_plan_purchase_test\.go:[0-9]+:[[:space:]]*"account": func\(r \*common\.Recommendation\) \{ r\.Account = "22222222222[2]" \},$ diff --git a/scripts/setup-git-secrets.sh b/scripts/setup-git-secrets.sh index f27fa86a3..25a6f977d 100755 --- a/scripts/setup-git-secrets.sh +++ b/scripts/setup-git-secrets.sh @@ -39,8 +39,9 @@ echo "" # first shipped in Git 2.38 (2022), because it was undocumented and unused # within git's own tree, breaking git-secrets as an unintended side effect # of a git upgrade rather than a git-secrets regression. On macOS the bare -# `say` call resolves to /usr/bin/say (the text-to-speech binary) instead -# and exits 0 by accident; on Linux (or on a new-enough git anywhere) there +# `say` call resolves to /usr/bin/say (the text-to-speech binary) instead, +# which speaks the message and can hang indefinitely when audio output is +# unavailable, blocking the install; on Linux (or on a new-enough git anywhere) there # is no such fallback, so it's "command not found" and `git secrets # --install -f` returns non-zero even though every hook file was already # written correctly. Define `say` as a no-op here and export it so the diff --git a/scripts/test-git-secrets-allowlist.sh b/scripts/test-git-secrets-allowlist.sh index f9950a188..4f6c711cf 100755 --- a/scripts/test-git-secrets-allowlist.sh +++ b/scripts/test-git-secrets-allowlist.sh @@ -189,7 +189,17 @@ run_case "UUID placeholder with a password on the same line" 1 p.go \ # A secret appended to an otherwise-allowed account-ID fixture line stays caught. run_case "key appended to an allowed account-ID fixture line" 1 \ cmd/helpers_test.go \ - ' accountID: "123456789012", k := "'"$KEY"'"' + ' accountID: "'"$ACCT"'", k := "'"$KEY"'"' + +# CI-equivalent check: the pre-commit framework hands git-secrets explicit +# file names (plain grep), unlike a no-argument scan (git grep), which +# differ on regex features such as \s. Scan every tracked file of the real +# repo that way, under the same registered patterns. +git -C "$REPO_ROOT" ls-files -z | tar -C "$REPO_ROOT" --null -T - -cf - | tar -C "$tmp/repo" -xf - +file_scan=0 +git ls-files -z --others --cached | xargs -0 git secrets --scan >"$tmp/file-scan.log" 2>&1 || file_scan=$? +[ "$file_scan" -eq 0 ] || grep -E "^[^ ]+:[0-9]+:" "$tmp/file-scan.log" | cut -c1-200 >&2 +check_result "file-argument scan of all tracked files" 0 "$file_scan" echo "$pass PASS, $fail FAIL" [ "$fail" -eq 0 ]