From c710c8172b704055b7ff7f923d924567e3e64b1a Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 27 Mar 2026 23:03:33 -0700 Subject: [PATCH] chore: add Husky pre-commit hooks + secret-scan scripts --- .husky/pre-commit | 13 +++++ .husky/pre-push | 13 +++++ package.json | 11 ++++- pnpm-lock.yaml | 13 ++++- scripts/secret-scan-repo.sh | 52 ++++++++++++++++++++ scripts/secret-scan-staged.sh | 89 +++++++++++++++++++++++++++++++++++ 6 files changed, 188 insertions(+), 3 deletions(-) create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100755 scripts/secret-scan-repo.sh create mode 100755 scripts/secret-scan-staged.sh diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..58be9cb --- /dev/null +++ b/.husky/pre-commit @@ -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 diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..640f96b --- /dev/null +++ b/.husky/pre-push @@ -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 diff --git a/package.json b/package.json index 4f9edde..b60f888 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,16 @@ "private": true, "packageManager": "pnpm@10.6.5", "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": { - "onlyBuiltDependencies": ["esbuild", "sharp"] + "onlyBuiltDependencies": [ + "esbuild", + "sharp" + ] + }, + "devDependencies": { + "husky": "^9.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 189f667..7ab7e3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,11 @@ settings: importers: - .: {} + .: + devDependencies: + husky: + specifier: ^9.0.0 + version: 9.1.7 backend: dependencies: @@ -2335,6 +2339,11 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + idb@8.0.3: resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} @@ -5852,6 +5861,8 @@ snapshots: transitivePeerDependencies: - supports-color + husky@9.1.7: {} + idb@8.0.3: {} ignore@5.3.2: {} diff --git a/scripts/secret-scan-repo.sh b/scripts/secret-scan-repo.sh new file mode 100755 index 0000000..efb9a5e --- /dev/null +++ b/scripts/secret-scan-repo.sh @@ -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)" diff --git a/scripts/secret-scan-staged.sh b/scripts/secret-scan-staged.sh new file mode 100755 index 0000000..23d935e --- /dev/null +++ b/scripts/secret-scan-staged.sh @@ -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}" +