← back to Site Factory
critic/checks/security.js
110 lines
// security.js — secret scanner.
// * /sk_live_[A-Za-z0-9]{20,}/ → critical
// * /STRIPE_SECRET_KEY\s*=\s*sk_/ → critical
// * /password\s*[:=]\s*["'][^"']{6,}["']/ (skip .env.example/md) → high
const fs = require('fs');
const path = require('path');
const { glob } = require('glob');
const SOURCE = 'security';
// scan only "text-ish" files; avoid binaries
const TEXT_EXTS = new Set([
'.js', '.ts', '.tsx', '.jsx',
'.json', '.yml', '.yaml', '.toml',
'.html', '.css', '.scss',
'.env', '.sh', '.py', '.rb', '.go',
'.txt', '.cfg', '.ini', '.conf',
]);
async function expandPaths(paths) {
const files = new Set();
for (const p of paths || []) {
if (!p) continue;
let stat;
try { stat = fs.statSync(p); } catch { continue; }
if (stat.isFile()) {
files.add(p);
} else if (stat.isDirectory()) {
const matches = await glob('**/*', {
cwd: p,
ignore: ['**/node_modules/**', '**/.next/**', '**/dist/**', '**/build/**', '**/.git/**'],
absolute: true,
nodir: true,
});
for (const m of matches) files.add(m);
}
}
return [...files];
}
function isPasswordSkipFile(file) {
const base = path.basename(file).toLowerCase();
if (base === '.env.example') return true;
if (file.toLowerCase().endsWith('.md')) return true;
if (file.includes('/node_modules/')) return true;
return false;
}
const RX_STRIPE_LIVE = /sk_live_[A-Za-z0-9]{20,}/g;
const RX_STRIPE_HARDCODED = /STRIPE_SECRET_KEY\s*=\s*sk_[A-Za-z0-9_]+/g;
const RX_PASSWORD = /password\s*[:=]\s*["'][^"']{6,}["']/gi;
function lineOf(src, idx) {
return src.slice(0, idx).split('\n').length;
}
async function run({ paths /* , screenshots, domain */ }) {
const findings = [];
const files = await expandPaths(paths);
for (const file of files) {
const ext = path.extname(file).toLowerCase();
if (ext && !TEXT_EXTS.has(ext)) continue;
let src;
try { src = fs.readFileSync(file, 'utf8'); } catch { continue; }
let m;
while ((m = RX_STRIPE_LIVE.exec(src)) != null) {
findings.push({
source: SOURCE,
severity: 'critical',
title: `Live Stripe key in source — ${path.basename(file)}`,
detail: `${file}:${lineOf(src, m.index)} matches sk_live_… — never commit live secrets.`,
suggested_fix: 'ROTATE the key in Stripe dashboard immediately, remove from source, store in env (Vercel envs / 1Password), purge git history.',
});
}
RX_STRIPE_LIVE.lastIndex = 0;
while ((m = RX_STRIPE_HARDCODED.exec(src)) != null) {
findings.push({
source: SOURCE,
severity: 'critical',
title: `Hardcoded Stripe secret — ${path.basename(file)}`,
detail: `${file}:${lineOf(src, m.index)} hardcodes STRIPE_SECRET_KEY=sk_…`,
suggested_fix: 'Move to environment variable (process.env.STRIPE_SECRET_KEY); rotate the key if it ever hit git.',
});
}
RX_STRIPE_HARDCODED.lastIndex = 0;
if (!isPasswordSkipFile(file)) {
while ((m = RX_PASSWORD.exec(src)) != null) {
findings.push({
source: SOURCE,
severity: 'high',
title: `Hardcoded password — ${path.basename(file)}`,
detail: `${file}:${lineOf(src, m.index)} matches password = "…".`,
suggested_fix: 'Move secret to env var or secret manager; rotate if it has been committed.',
});
}
RX_PASSWORD.lastIndex = 0;
}
}
return findings;
}
module.exports = { run };