Keep local safety checks effective on large staged changes

Admit the complete index and patch reads, and drain pattern matches before interpreting their result under pipefail. Preserve sensitive-file and fallback-secret blocks and required docs, governance and lint admission; distinguish an empty filtered patch from a failed read. Exercise the actual hook with bounded synthetic large inputs and retained failure exits.

Contract-Neutral: Local hook input handling only; existing secret patterns, required checks and production contracts are unchanged.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-25 03:33:27 +00:00
parent 11f8ea81c5
commit 7adfe647fe
2 changed files with 247 additions and 12 deletions
+19 -12
View File
@@ -2,13 +2,22 @@
set -euo pipefail
# Admit the complete index read before interpreting any match. With pipefail,
# grep -q can close a large producer early and turn a real match into SIGPIPE
# (a false no-match), silently skipping both safety blocks and required checks.
STAGED_FILES="$(git diff --cached --name-only)"
staged_files_match() {
printf '%s\n' "$STAGED_FILES" | grep -E "$1" >/dev/null
}
# Pre-commit hook to prevent committing restricted or sensitive data
RESTRICTED_FILES="active_subs.json charges.json customers.json subscriptions.json"
echo "Running sensitivity check..."
for file in $RESTRICTED_FILES; do
if git diff --cached --name-only | grep -q "^${file}$"; then
if staged_files_match "^${file}$"; then
echo "BLOCKED: restricted file pattern matched: ${file}"
exit 1
fi
@@ -16,9 +25,9 @@ done
# Block sensitive file types from ever being committed
SENSITIVE_EXTENSIONS="\.pem$|\.p12$|\.pfx$|\.key$|\.keystore$|\.jks$|\.enc$|id_rsa$|id_ed25519$|id_ecdsa$"
if git diff --cached --name-only | grep -qE "$SENSITIVE_EXTENSIONS"; then
if staged_files_match "$SENSITIVE_EXTENSIONS"; then
echo "BLOCKED: sensitive file type staged for commit:"
git diff --cached --name-only | grep -E "$SENSITIVE_EXTENSIONS"
printf '%s\n' "$STAGED_FILES" | grep -E "$SENSITIVE_EXTENSIONS"
echo "If this is intentional (e.g. a template), use: git commit --no-verify"
exit 1
fi
@@ -35,9 +44,13 @@ if command -v gitleaks >/dev/null 2>&1; then
else
# Fallback: broader pattern check when gitleaks is not installed
# Covers Stripe, AWS, GCP, OpenAI, private keys, and generic high-entropy tokens
STAGED_DIFF=$(git diff --cached | grep -E "^\+" | grep -v ".husky/pre-commit" | grep -v "_test\.go")
# A failed Git read is not an empty patch. Filtering can legitimately find
# no added lines (for example a deletion-only change), so use a consuming
# filter whose no-match result is distinct from an operational failure.
STAGED_DIFF="$(git diff --cached)"
STAGED_DIFF=$(printf '%s\n' "$STAGED_DIFF" | awk '/^\+/ && !/.husky\/pre-commit/ && !/_test\.go/')
if echo "$STAGED_DIFF" | grep -qE "(cus_|sub_|ch_|pi_|pm_|sk_live_|sk_test_|rk_live_|rk_test_|whsec_)"; then
if printf '%s\n' "$STAGED_DIFF" | grep -E "(cus_|sub_|ch_|pi_|pm_|sk_live_|sk_test_|rk_live_|rk_test_|whsec_)" >/dev/null; then
echo "WARNING: Potential Stripe identifiers found in staged changes."
echo " Use 'git diff --cached' to review before proceeding."
if [ -t 0 ]; then
@@ -52,7 +65,7 @@ else
fi
fi
if echo "$STAGED_DIFF" | grep -qE "(AKIA[0-9A-Z]{16}|-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----|sk-[a-zA-Z0-9]{20,}|AIza[0-9A-Za-z_-]{35})"; then
if printf '%s\n' "$STAGED_DIFF" | grep -E "(AKIA[0-9A-Z]{16}|-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----|sk-[a-zA-Z0-9]{20,}|AIza[0-9A-Za-z_-]{35})" >/dev/null; then
echo "BLOCKED: Likely secret detected in staged changes (AWS key, private key, or API key)."
echo " Use 'git diff --cached' to review."
echo " Install gitleaks for more precise scanning: brew install gitleaks"
@@ -61,12 +74,6 @@ else
fi
echo "Sensitivity check passed."
STAGED_FILES="$(git diff --cached --name-only)"
staged_files_match() {
printf '%s\n' "$STAGED_FILES" | grep -qE "$1"
}
# Normalize staged source before any guard hashes or audits it. The browser
# verification receipt binds to exact frontend bytes and is named by their
# fingerprint, so formatting after its guard can produce a commit that passed
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""Run the real hook with synthetic index input and recording tool doubles.
These controls establish hook admission and failure propagation, not a secret
scanner, browser, linter or governance verdict. No repository is created and
no host hook, Git configuration or enforcement is changed.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
HOOK = ROOT / ".husky/pre-commit"
BASH = shutil.which("bash")
# Well beyond a pipe's capacity, with the only match before the neutral tail.
TAIL = [f"z-fixture/path-{i:05d}.txt" for i in range(16000)]
class PrecommitAdmissionTest(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="pulse-hook-admission-")
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
self.bin = self.root / "bin"
self.bin.mkdir()
self.calls = self.root / "calls.jsonl"
self.names = self.root / "names.txt"
self.patch = self.root / "patch.txt"
self.names.write_text("README.md\n")
self.patch.write_text("+ordinary synthetic content\n")
self.hook = self.root / "pre-commit"
shutil.copyfile(HOOK, self.hook)
# A closed PATH makes scanner-present and fallback cases independent
# of whatever happens to be installed on the developer/CI host.
for tool in ("grep", "awk", "dirname", "sed", "sort", "xargs"):
target = shutil.which(tool)
self.assertIsNotNone(target, tool)
(self.bin / tool).symlink_to(target)
self.env = {
**os.environ,
"PATH": str(self.bin),
"HOOK_FIXTURE_CALLS": str(self.calls),
"HOOK_FIXTURE_NAMES": str(self.names),
"HOOK_FIXTURE_PATCH": str(self.patch),
}
# Do not inherit local hook overrides from the caller.
for key in ("HOOK_FIXTURE_FAIL", "HOOK_FIXTURE_GIT_FAIL", "GOLANGCI_LINT_GOTOOLCHAIN"):
self.env.pop(key, None)
self.tool("git", """
args = sys.argv[1:]
kind = 'names' if args == ['diff', '--cached', '--name-only'] else 'patch'
if args not in (['diff', '--cached', '--name-only'], ['diff', '--cached']):
raise SystemExit(64)
if os.environ.get('HOOK_FIXTURE_GIT_FAIL') == kind:
raise SystemExit(31)
path = os.environ['HOOK_FIXTURE_NAMES' if kind == 'names' else 'HOOK_FIXTURE_PATCH']
sys.stdout.buffer.write(Path(path).read_bytes())
if os.environ.get('HOOK_FIXTURE_GIT_FAIL') == 'partial-' + kind:
raise SystemExit(31)
""")
for name in ("gitleaks", "python3", "go", "npm"):
self.recording_tool(name)
def tool(self, name, body):
target = self.bin / name
target.write_text(f"#!{sys.executable}\nimport json, os, sys\nfrom pathlib import Path\n" + body)
target.chmod(0o700)
return target
def recording_tool(self, name):
return self.tool(name, r"""
with Path(os.environ['HOOK_FIXTURE_CALLS']).open('a') as output:
output.write(json.dumps([Path(sys.argv[0]).name, *sys.argv[1:]]) + '\n')
selector = ' '.join([Path(sys.argv[0]).name, *sys.argv[1:]])
raise SystemExit(23 if os.environ.get('HOOK_FIXTURE_FAIL') == selector else 0)
""")
def staged(self, match=None, *, position=0):
paths = list(TAIL)
if match is not None:
paths.insert(position, match)
self.names.write_text("\n".join(paths) + "\n")
def observed(self):
return [json.loads(line) for line in self.calls.read_text().splitlines()] if self.calls.exists() else []
def run_hook(self, *, fail=None, git_fail=None):
env = {**self.env}
if fail:
env['HOOK_FIXTURE_FAIL'] = fail
if git_fail:
env['HOOK_FIXTURE_GIT_FAIL'] = git_fail
return subprocess.run([BASH, str(self.hook)], cwd=self.root, env=env,
capture_output=True, text=True, timeout=20)
def test_every_restricted_file_blocks_before_scanner_or_other_checks(self):
for name in ('active_subs.json', 'charges.json', 'customers.json', 'subscriptions.json'):
with self.subTest(name=name):
self.staged(name)
result = self.run_hook()
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn('BLOCKED: restricted file pattern matched:', result.stdout)
self.assertEqual(self.observed(), [])
def test_sensitive_extension_at_either_end_blocks_before_scanner(self):
for position in (0, len(TAIL)):
with self.subTest(position=position):
self.staged('fixture.pem', position=position)
result = self.run_hook()
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn('BLOCKED: sensitive file type', result.stdout)
self.assertEqual(self.observed(), [])
def test_docs_match_runs_mirror_and_preserves_its_failure(self):
self.staged('docs/fixture.md')
result = self.run_hook(fail='python3 scripts/check_docs_mirror.py --staged')
self.assertEqual(result.returncode, 23, result.stdout + result.stderr)
self.assertIn(['python3', 'scripts/check_docs_mirror.py', '--staged'], self.observed())
self.assertNotIn('Pre-commit checks passed!', result.stdout)
def test_governance_match_runs_guard_and_preserves_its_failure(self):
status = self.root / 'docs/release-control/v6/internal/status.json'
status.parent.mkdir(parents=True)
status.write_text('{}')
self.staged('internal/api/fixture.go')
result = self.run_hook(fail='python3 scripts/release_control/governance_stage_guard.py')
self.assertEqual(result.returncode, 23, result.stdout + result.stderr)
self.assertIn(['python3', 'scripts/release_control/governance_stage_guard.py'], self.observed())
self.assertNotIn('Pre-commit checks passed!', result.stdout)
def test_frontend_match_runs_lint_and_preserves_its_failure(self):
frontend = self.root / 'frontend-modern'
frontend.mkdir()
(frontend / 'package.json').write_text('{}')
self.staged('frontend-modern/src/fixture.ts')
result = self.run_hook(fail='npm run lint')
self.assertEqual(result.returncode, 23, result.stdout + result.stderr)
self.assertIn(['npm', 'run', 'lint'], self.observed())
self.assertNotIn('Pre-commit checks passed!', result.stdout)
def test_go_match_runs_lint_and_preserves_its_failure(self):
self.recording_tool('golangci-lint')
source = self.root / 'internal/api/fixture.go'
source.parent.mkdir(parents=True)
source.write_text('package api\n')
(self.root / 'go.mod').write_text('module fixture\n\ngo 1.26.10\n')
self.staged('internal/api/fixture.go')
self.env['GOLANGCI_LINT_GOTOOLCHAIN'] = 'go1.26.10'
result = self.run_hook(fail='golangci-lint run --concurrency 2 --timeout 20m --new-from-rev=HEAD ./internal/api')
self.assertEqual(result.returncode, 23, result.stdout + result.stderr)
self.assertTrue(any(call[0] == 'golangci-lint' for call in self.observed()))
self.assertNotIn('Pre-commit checks passed!', result.stdout)
def test_large_nonmatching_list_does_not_invent_checks(self):
(self.root / 'frontend-modern').mkdir()
(self.root / 'frontend-modern/package.json').write_text('{}')
self.recording_tool('golangci-lint')
self.staged()
result = self.run_hook()
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertFalse(any(call[0] in ('npm', 'golangci-lint', 'go') for call in self.observed()))
self.assertIn(['python3', 'scripts/release_control/browser_verification_guard.py'], self.observed())
self.assertIn('Pre-commit checks passed!', result.stdout)
def test_index_read_failure_is_not_a_clean_or_nonmatching_index(self):
self.staged('active_subs.json')
for mode in ('names', 'partial-names'):
with self.subTest(mode=mode):
result = self.run_hook(git_fail=mode)
self.assertEqual(result.returncode, 31, result.stdout + result.stderr)
self.assertEqual(self.observed(), [])
self.assertNotIn('Pre-commit checks passed!', result.stdout)
def test_scanner_rejection_stays_gating_before_other_checks(self):
result = self.run_hook(fail='gitleaks protect --staged --config .gitleaks.toml --no-banner')
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn('BLOCKED: gitleaks detected secrets', result.stdout)
self.assertEqual(len(self.observed()), 1)
self.assertNotIn('Pre-commit checks passed!', result.stdout)
def fallback(self, text):
(self.bin / 'gitleaks').unlink()
self.patch.write_text(text)
return self.run_hook()
def test_large_fallback_stripe_match_cannot_disappear_on_pipe_close(self):
result = self.fallback('+cus_synthetic_fixture\n' + '+ordinary fixture line\n' * 50000)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn('WARNING: Potential Stripe identifiers', result.stdout)
self.assertEqual(self.observed(), [])
def test_large_fallback_key_match_cannot_disappear_on_pipe_close(self):
marker = '-----BEGIN ' + 'OPENSSH PRIVATE ' + 'KEY-----'
result = self.fallback('+' + marker + '\n' + '+ordinary fixture line\n' * 50000)
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
self.assertIn('BLOCKED: Likely secret detected', result.stdout)
self.assertEqual(self.observed(), [])
def test_fallback_empty_filtered_patch_is_a_valid_no_match(self):
for patch in ('', '-deleted content\n', '+fixture_test.go\n', '+.husky/pre-commit\n'):
with self.subTest(patch=patch):
if not (self.bin / 'gitleaks').exists():
self.recording_tool('gitleaks')
result = self.fallback(patch)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn('Pre-commit checks passed!', result.stdout)
def test_fallback_patch_read_failure_stays_gating(self):
(self.bin / 'gitleaks').unlink()
for mode in ('patch', 'partial-patch'):
with self.subTest(mode=mode):
result = self.run_hook(git_fail=mode)
self.assertEqual(result.returncode, 31, result.stdout + result.stderr)
self.assertEqual(self.observed(), [])
self.assertNotIn('Pre-commit checks passed!', result.stdout)
if __name__ == '__main__':
unittest.main()