chore: add Husky pre-commit hooks + secret-scan scripts
This commit is contained in:
parent
d6cddb9641
commit
c710c8172b
13
.husky/pre-commit
Executable file
13
.husky/pre-commit
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
|
||||||
|
if [ -n "$ROOT" ]; then
|
||||||
|
cd "$ROOT" || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$HUSKY" = "0" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔐 Scanning staged changes for secrets..."
|
||||||
|
bash scripts/secret-scan-staged.sh
|
||||||
13
.husky/pre-push
Executable file
13
.husky/pre-push
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)"
|
||||||
|
if [ -n "$ROOT" ]; then
|
||||||
|
cd "$ROOT" || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$HUSKY" = "0" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔐 Scanning tracked files for secrets before push..."
|
||||||
|
bash scripts/secret-scan-repo.sh
|
||||||
11
package.json
11
package.json
@ -4,9 +4,16 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.6.5",
|
"packageManager": "pnpm@10.6.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"verify": "pnpm --filter @chronomind/backend run typecheck && pnpm --filter @chronomind/backend run test && pnpm --filter @chronomind/backend run build && pnpm --filter web run typecheck && pnpm --filter web run test && pnpm --filter web run build"
|
"verify": "pnpm --filter @chronomind/backend run typecheck && pnpm --filter @chronomind/backend run test && pnpm --filter @chronomind/backend run build && pnpm --filter web run typecheck && pnpm --filter web run test && pnpm --filter web run build",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": ["esbuild", "sharp"]
|
"onlyBuiltDependencies": [
|
||||||
|
"esbuild",
|
||||||
|
"sharp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"husky": "^9.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@ -6,7 +6,11 @@ settings:
|
|||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.: {}
|
.:
|
||||||
|
devDependencies:
|
||||||
|
husky:
|
||||||
|
specifier: ^9.0.0
|
||||||
|
version: 9.1.7
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2335,6 +2339,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
|
husky@9.1.7:
|
||||||
|
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
idb@8.0.3:
|
idb@8.0.3:
|
||||||
resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==}
|
resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==}
|
||||||
|
|
||||||
@ -5852,6 +5861,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
husky@9.1.7: {}
|
||||||
|
|
||||||
idb@8.0.3: {}
|
idb@8.0.3: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|||||||
52
scripts/secret-scan-repo.sh
Executable file
52
scripts/secret-scan-repo.sh
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Scans tracked files for common secret patterns.
|
||||||
|
# Intended for manual use (or as part of quick-check). Avoids printing matching lines.
|
||||||
|
#
|
||||||
|
# Note: This does not scan git history. Use a dedicated tool (e.g. gitleaks) for history scanning.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [[ -z "${ROOT}" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${ROOT}"
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local name="$1"
|
||||||
|
local pattern="$2"
|
||||||
|
|
||||||
|
# -l prints only filenames (no secret material in output)
|
||||||
|
if git grep -l -E "${pattern}" -- . >/dev/null 2>&1; then
|
||||||
|
echo "✗ ${name}: potential matches found in:"
|
||||||
|
git grep -l -E "${pattern}" -- . | sed 's/^/ - /'
|
||||||
|
echo
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check "Private key blocks" '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'
|
||||||
|
check "Azure storage AccountKey leaks" 'AccountKey=[A-Za-z0-9+/=_-]{20,}'
|
||||||
|
check "Azure SharedAccessKey leaks" 'SharedAccessKey=[A-Za-z0-9+/=_-]{20,}'
|
||||||
|
check "COSMOS_KEY assignment leaks" 'COSMOS_KEY[[:space:]]*=[[:space:]]*[A-Za-z0-9+/=_-]{20,}'
|
||||||
|
check "AZURE_OPENAI_KEY assignment leaks" 'AZURE_OPENAI_KEY[[:space:]]*=[[:space:]]*[A-Za-z0-9+/=_-]{20,}'
|
||||||
|
check "AZURE_SPEECH_KEY assignment leaks" 'AZURE_SPEECH_KEY[[:space:]]*=[[:space:]]*[A-Za-z0-9+/=_-]{20,}'
|
||||||
|
check "JWT_SECRET hex-like assignments" 'JWT_SECRET[[:space:]]*=[[:space:]]*[0-9a-fA-F]{32,}'
|
||||||
|
check "OpenAI API keys (sk-...)" 'sk-[A-Za-z0-9]{20,}'
|
||||||
|
check "Stripe secret keys (sk_live_/sk_test_)" 'sk_(live|test)_[A-Za-z0-9]{20,}'
|
||||||
|
check "Stripe webhook secrets (whsec_...)" 'whsec_[A-Za-z0-9]{20,}'
|
||||||
|
check "Perplexity API keys (pplx-...)" 'pplx-[A-Za-z0-9]{20,}'
|
||||||
|
check "AWS access key ids (AKIA...)" 'AKIA[0-9A-Z]{16}'
|
||||||
|
check "Google API keys (AIza...)" 'AIza[0-9A-Za-z\-_]{35}'
|
||||||
|
|
||||||
|
if [[ "${fail}" -ne 0 ]]; then
|
||||||
|
echo "Secret scan failed."
|
||||||
|
echo "Fix the files above (move values to Key Vault / env vars) and retry."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ Secret scan passed (tracked files)"
|
||||||
89
scripts/secret-scan-staged.sh
Executable file
89
scripts/secret-scan-staged.sh
Executable file
@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Blocks commits that introduce obvious secrets (Azure keys, connection strings, private keys, etc.).
|
||||||
|
# This scans only staged changes (the git index) to avoid false positives from unstaged work.
|
||||||
|
#
|
||||||
|
# Note: Avoid printing matched lines to keep secrets out of terminal scrollback/logs.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
||||||
|
if [[ -z "${ROOT}" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${ROOT}"
|
||||||
|
|
||||||
|
# Nothing staged -> nothing to scan.
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
DIFF="$(git diff --cached --no-color --unified=0)"
|
||||||
|
if [[ -z "${DIFF}" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
perl -ne '
|
||||||
|
our ($file, %hits, $exit);
|
||||||
|
|
||||||
|
if (/^\+\+\+ b\/(.*)$/) {
|
||||||
|
$file = $1;
|
||||||
|
next;
|
||||||
|
}
|
||||||
|
|
||||||
|
next unless defined $file;
|
||||||
|
next unless /^\+(?!\+\+\+)(.*)$/;
|
||||||
|
my $line = $1;
|
||||||
|
|
||||||
|
my @checks = (
|
||||||
|
["PRIVATE_KEY_BLOCK", qr/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i],
|
||||||
|
|
||||||
|
# Azure connection strings / keys (high signal, low false positives)
|
||||||
|
["AZURE_STORAGE_ACCOUNT_KEY", qr/AccountKey=[A-Za-z0-9+\/=_-]{20,}/],
|
||||||
|
["AZURE_SHARED_ACCESS_KEY", qr/SharedAccessKey=[A-Za-z0-9+\/=_-]{20,}/],
|
||||||
|
|
||||||
|
# Common env var leaks
|
||||||
|
["COSMOS_KEY_ASSIGNMENT", qr/\bCOSMOS_KEY\s*=\s*[A-Za-z0-9+\/=_-]{20,}/],
|
||||||
|
["AZURE_OPENAI_KEY_ASSIGNMENT", qr/\bAZURE_OPENAI_KEY\s*=\s*[A-Za-z0-9+\/=_-]{20,}/],
|
||||||
|
["AZURE_SPEECH_KEY_ASSIGNMENT", qr/\bAZURE_SPEECH_KEY\s*=\s*[A-Za-z0-9+\/=_-]{20,}/],
|
||||||
|
["JWT_SECRET_HEX_ASSIGNMENT", qr/\bJWT_SECRET\s*=\s*[0-9a-fA-F]{32,}\b/],
|
||||||
|
|
||||||
|
# OpenAI / Stripe / Perplexity
|
||||||
|
["OPENAI_API_KEY", qr/\bOPENAI_API_KEY\s*=\s*sk-[A-Za-z0-9]{20,}/],
|
||||||
|
["OPENAI_KEY_LIKE", qr/\bsk-[A-Za-z0-9]{20,}\b/],
|
||||||
|
["STRIPE_SECRET_KEY", qr/\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/],
|
||||||
|
["STRIPE_WEBHOOK_SECRET", qr/\bwhsec_[A-Za-z0-9]{20,}\b/],
|
||||||
|
["PERPLEXITY_API_KEY", qr/\bpplx-[A-Za-z0-9]{20,}\b/],
|
||||||
|
|
||||||
|
# Cloud provider patterns
|
||||||
|
["AWS_ACCESS_KEY_ID", qr/\bAKIA[0-9A-Z]{16}\b/],
|
||||||
|
["GOOGLE_API_KEY", qr/\bAIza[0-9A-Za-z\-_]{35}\b/],
|
||||||
|
);
|
||||||
|
|
||||||
|
for my $check (@checks) {
|
||||||
|
my ($name, $re) = @$check;
|
||||||
|
if ($line =~ $re) {
|
||||||
|
$hits{$name}{$file} = 1;
|
||||||
|
$exit = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
if (!$exit) {
|
||||||
|
exit 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
print STDERR "✗ Potential secrets detected in staged changes:\n";
|
||||||
|
for my $name (sort keys %hits) {
|
||||||
|
for my $path (sort keys %{ $hits{$name} }) {
|
||||||
|
print STDERR " - ${name}: ${path}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print STDERR "\nCommit aborted.\n";
|
||||||
|
print STDERR "Move secrets to Azure Key Vault (or env vars injected at deploy-time), then retry.\n";
|
||||||
|
print STDERR "If you believe this is a false positive, refactor the value into a placeholder.\n";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
' <<< "${DIFF}"
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user