import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = __dirname; const SCAN_ROOTS = ['src', 'scripts', 'schema', 'docs', '.github', 'package.json', 'tsconfig.json', '.gitleaks.toml'] as const; const TEXT_FILE_EXTENSIONS = new Set([ '.ts', '.tsx', '.js', '.mjs', '.cjs', '.json', '.md', '.yml', '.yaml', '.toml', '.sql', '.txt' ]); const EXCLUDED_PATH_SEGMENTS = ['node_modules', 'dist', '.git', '.vite']; const PLACEHOLDER_HINTS = ['your_', 'your-', 'example', 'changeme', 'placeholder', '', 'xxxx']; const SECRET_ASSIGNMENT_PATTERNS: Array<{ name: string; regex: RegExp }> = [ { name: 'env_secret_assignment', regex: /\b(?:SUPABASE_(?:KEY|SERVICE_ROLE_KEY)|ALPACA_API_KEY|ALPACA_SECRET_KEY|REAL_ALPACA_API_KEY|REAL_ALPACA_SECRET_KEY|OPENAI_API_KEY|GEMINI_API_KEY|PERPLEXITY_API_KEY)\s*=\s*([^\s#'"]{12,})/gi }, { name: 'openai_like_secret', regex: /\bsk-[a-z0-9]{20,}\b/gi }, { name: 'aws_access_key_like', regex: /\bAKIA[0-9A-Z]{16}\b/g } ]; function shouldScan(filePath: string): boolean { const normalized = filePath.replace(/\\/g, '/'); if (EXCLUDED_PATH_SEGMENTS.some((segment) => normalized.includes(`/${segment}/`) || normalized.startsWith(`${segment}/`))) { return false; } const ext = path.extname(filePath).toLowerCase(); if (TEXT_FILE_EXTENSIONS.has(ext)) return true; const base = path.basename(filePath).toLowerCase(); return base.startsWith('.env'); } function isLikelyPlaceholder(value: string): boolean { const normalized = value.trim().toLowerCase(); return PLACEHOLDER_HINTS.some((hint) => normalized.includes(hint)); } function main() { const files: string[] = []; const walk = (dirPath: string) => { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { const absolute = path.join(dirPath, entry.name); const relative = path.relative(repoRoot, absolute); if (!relative) continue; if (!shouldScan(relative) && entry.isFile()) continue; if (entry.isDirectory()) { if (EXCLUDED_PATH_SEGMENTS.includes(entry.name)) continue; walk(absolute); } else { if (shouldScan(relative)) { files.push(relative); } } } }; for (const rootEntry of SCAN_ROOTS) { const absolutePath = path.join(repoRoot, rootEntry); if (!fs.existsSync(absolutePath)) continue; const stat = fs.statSync(absolutePath); if (stat.isDirectory()) { walk(absolutePath); } else { files.push(path.relative(repoRoot, absolutePath)); } } const findings: string[] = []; for (const relativePath of files) { const absolutePath = path.join(repoRoot, relativePath); let content = ''; try { content = fs.readFileSync(absolutePath, 'utf8'); } catch { continue; } for (const pattern of SECRET_ASSIGNMENT_PATTERNS) { let match: RegExpExecArray | null = null; while ((match = pattern.regex.exec(content)) !== null) { const rawValue = match[1] || match[0]; if (isLikelyPlaceholder(rawValue)) continue; findings.push(`[${pattern.name}] ${relativePath}: ${match[0].slice(0, 120)}`); } pattern.regex.lastIndex = 0; } } assert.equal( findings.length, 0, `Potential plaintext secrets detected:\n${findings.join('\n')}\n\nRotate credentials and scrub artifacts before release.` ); console.log(`[secret-hygiene] OK: scanned ${files.length} tracked text files, no plaintext secret patterns found`); } main();