#!/usr/bin/env node // Hook: pre-edit-secrets.mjs (consolidated) // Event: PreToolUse (Edit|Write) // Purpose: Detect secrets/credentials in file content before writing. // Consolidates patterns from global, kiur, llm-security, and ms-ai-architect. // // Protocol: // - Read JSON from stdin: { tool_name, tool_input } // - tool_input.file_path — destination path // - tool_input.content — full content (Write) // - tool_input.new_string — replacement text (Edit) // - Block: stderr + exit 2 // - Allow: exit 0 import { readFileSync } from 'node:fs'; import { normalize } from 'node:path'; // --------------------------------------------------------------------------- // Secret detection patterns (union of global, kiur, llm-security, ms-ai-architect) // --------------------------------------------------------------------------- const SECRET_PATTERNS = [ { name: 'AWS Access Key ID', pattern: /AKIA[0-9A-Z]{16}/ }, { name: 'AWS Secret Access Key', pattern: /(?:aws_secret(?:_access)?_key|AWS_SECRET(?:_ACCESS)?_KEY)\s*[=:]\s*['"]?[0-9a-zA-Z/+=]{40}['"]?/i }, { name: 'Azure Connection String (AccountKey/SharedAccessKey/sig)', pattern: /(?:AccountKey|SharedAccessKey|sig)=[A-Za-z0-9+/=]{20,}/ }, { name: 'Azure AD ClientSecret', pattern: /(?:client[_-]?secret|ClientSecret)\s*[=:]\s*['"][^'"]{8,}['"]/i }, { name: 'Azure AI Services Key', pattern: /Ocp-Apim-Subscription-Key\s*[=:]\s*['"]?[0-9a-f]{32}['"]?/i }, { name: 'GitHub Token', pattern: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/ }, { name: 'npm Token', pattern: /npm_[A-Za-z0-9]{36}/ }, { name: 'Private Key PEM Block', pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/ }, { name: 'JWT Secret', pattern: /JWT[_-]?SECRET\s*[=:]\s*['"][^'"]{8,}['"]/i }, { name: 'Slack/Discord Webhook URL', pattern: /https:\/\/(?:hooks\.slack\.com\/services|discord(?:app)?\.com\/api\/webhooks)\// }, { name: 'Generic credential assignment', pattern: /(?:password|passwd|secret|token|api[_-]?key)\s*[=:]\s*['"][^'"]{8,}['"]/i }, { name: 'Authorization header with token', pattern: /[Bb]earer [A-Za-z0-9\-._~+/]{20,}/ }, { name: 'Database connection string', pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^\s]+@[^\s]+/i }, ]; // --------------------------------------------------------------------------- // Exclusions: files that may contain example patterns for documentation // --------------------------------------------------------------------------- function isExcluded(filePath) { if (!filePath) return false; const n = normalize(filePath); if (/[\\/]knowledge[\\/].+\.md$/i.test(n)) return true; if (/[\\/]references[\\/].+\.md$/i.test(n)) return true; if (/\.(test|spec|mock)\.[jt]sx?$/.test(n)) return true; if (/\.(example|template|sample)(\.|$)/.test(n)) return true; return false; } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- let input; try { const raw = readFileSync(0, 'utf-8'); input = JSON.parse(raw); } catch { process.exit(0); } const toolInput = input?.tool_input ?? {}; const filePath = toolInput.file_path ?? ''; if (isExcluded(filePath)) process.exit(0); const contentToCheck = [toolInput.content ?? '', toolInput.new_string ?? ''].join('\n'); if (!contentToCheck.trim()) process.exit(0); for (const { name, pattern } of SECRET_PATTERNS) { if (pattern.test(contentToCheck)) { process.stderr.write( `BLOCKED: Potential secret detected — ${name}\n` + ` File: ${filePath || '(unknown)'}\n` + ` Remove the credential before writing. Use or .env.\n` ); process.exit(2); } } process.exit(0);