learning_ai_invt_trdg/backend/verifySecretHygiene.ts

112 lines
4.0 KiB
TypeScript

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', '<redacted>', '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();