4bnode
Version:
4bnode is a CLI-powered backend development platform with a built-in visual dashboard to generate, manage, and test Node.js/Express APIs faster.
154 lines (143 loc) • 6.97 kB
JavaScript
// Audit the /_dev dashboard exposure across 4bnode apps (read-only).
//
// node scripts/audit-dev-surface.mjs <dir> [--since=2026-06-07]
//
// Scans <dir> recursively for 4bnode apps (any folder containing
// .4bnode/dev-api.js) and reports, per app:
// • whether the /_dev mount is gated on NODE_ENV (production-safe)
// • whether dev-api.js carries the loopback network guard
// • whether a .passkey exists and whether it is a known-weak PIN
// • route/source files modified on/after --since, and any that contain
// code-execution / cryptominer signatures (possible injected backdoor)
//
// Nothing is modified. Rotate weak/shared passkeys with `npx 4bnode passkey`.
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
const args = process.argv.slice(2);
const root = path.resolve(args.find((a) => !a.startsWith('--')) || '.');
const sinceArg = (args.find((a) => a.startsWith('--since=')) || '').split('=')[1];
const since = sinceArg ? new Date(sinceArg) : null;
const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
// Common weak PINs operators reuse. Hashes compared against .4bnode/.passkey.
const WEAK_PINS = ['123456', '000000', '111111', '121212', '123123', '654321',
'666666', '999999', '112233', '159753', '147258', '101010', '696969', '777777',
'123321', '789456', '000001', '222222', '555555', '888888', '123654', '098765'];
const WEAK_HASHES = new Map(WEAK_PINS.map((p) => [sha(p), p]));
// Signatures of code execution / miners that should never appear in generated routes.
const BACKDOOR_PATTERNS = [
[/child_process|\brequire\(\s*['"]child_process['"]\s*\)/, 'child_process import'],
[/\bexec(Sync)?\s*\(/, 'exec() call'],
[/\bspawn(Sync)?\s*\(/, 'spawn() call'],
[/\beval\s*\(/, 'eval()'],
[/new\s+Function\s*\(/, 'new Function()'],
[/Buffer\.from\([^)]*,\s*['"]base64['"]\)/, 'base64-decoded payload'],
[/(curl|wget)\s+[^\n|]*\|\s*(sh|bash)/, 'pipe-to-shell download'],
[/xmrig|minerd|stratum\+tcp|coinhive|cryptonight|nanopool|supportxmr|nicehash/i, 'cryptominer signature'],
[/\/dev\/tcp\//, 'reverse-shell /dev/tcp'],
];
const C = { red: (s) => `\x1b[31m${s}\x1b[0m`, yel: (s) => `\x1b[33m${s}\x1b[0m`,
grn: (s) => `\x1b[32m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`, bold: (s) => `\x1b[1m${s}\x1b[0m` };
function findApps(dir, depth = 0, acc = []) {
if (depth > 4) return acc;
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return acc; }
if (entries.some((e) => e.isDirectory() && e.name === '.4bnode') &&
fs.existsSync(path.join(dir, '.4bnode', 'dev-api.js'))) {
acc.push(dir);
}
for (const e of entries) {
if (e.isDirectory() && e.name !== 'node_modules' && e.name !== '.git' && e.name !== '.4bnode') {
findApps(path.join(dir, e.name), depth + 1, acc);
}
}
return acc;
}
function scanSourceFiles(appDir) {
const targets = ['index.js', 'src'];
const files = [];
const walk = (p) => {
let st; try { st = fs.statSync(p); } catch { return; }
if (st.isDirectory()) {
if (path.basename(p) === 'node_modules' || path.basename(p) === '.git') return;
for (const c of fs.readdirSync(p)) walk(path.join(p, c));
} else if (p.endsWith('.js') || p.endsWith('.mjs') || p.endsWith('.cjs')) {
files.push(p);
}
};
for (const t of targets) walk(path.join(appDir, t));
const findings = [];
for (const f of files) {
let content, mtime;
try { content = fs.readFileSync(f, 'utf8'); mtime = fs.statSync(f).mtime; } catch { continue; }
const rel = path.relative(appDir, f);
const hits = [];
const lines = content.split('\n');
for (const [re, label] of BACKDOOR_PATTERNS) {
for (let i = 0; i < lines.length; i++) {
if (re.test(lines[i])) hits.push(`${rel}:${i + 1} — ${label}`);
}
}
const recent = since && mtime >= since;
if (hits.length || recent) findings.push({ rel, mtime, hits, recent });
}
return findings;
}
function auditApp(appDir) {
const name = path.basename(appDir);
const issues = [];
// 1. Mount gating
const idx = path.join(appDir, 'index.js');
if (fs.existsSync(idx)) {
const c = fs.readFileSync(idx, 'utf8');
const mountIdx = Math.max(c.indexOf('app.use("/_dev"'), c.indexOf("app.use('/_dev'"));
if (mountIdx !== -1) {
const gateIdx = c.search(/NODE_ENV\s*!==\s*['"]production['"]/);
const gated = gateIdx !== -1 && gateIdx < mountIdx;
if (!gated) issues.push([C.red('CRITICAL'), '/_dev is mounted WITHOUT a NODE_ENV production gate']);
}
}
// 2. Loopback guard in dev-api.js
const devApi = path.join(appDir, '.4bnode', 'dev-api.js');
const devApiSrc = fs.existsSync(devApi) ? fs.readFileSync(devApi, 'utf8') : '';
if (devApiSrc && !devApiSrc.includes('isLoopbackRequest')) {
issues.push([C.red('CRITICAL'), 'dev-api.js has NO loopback network guard (reachable off-host)']);
}
// 3. Passkey
const pkFile = path.join(appDir, '.4bnode', '.passkey');
if (!fs.existsSync(pkFile)) {
issues.push([C.red('CRITICAL'), 'NO .passkey file — dashboard auth is open (anyone gets in)']);
} else {
const h = fs.readFileSync(pkFile, 'utf8').trim();
if (WEAK_HASHES.has(h)) {
issues.push([C.red('CRITICAL'), `weak/guessable passkey "${WEAK_HASHES.get(h)}" — rotate with \`npx 4bnode passkey\``]);
}
}
// 4. Backdoor / recent-change scan
const findings = scanSourceFiles(appDir);
const backdoors = findings.filter((f) => f.hits.length);
const recents = findings.filter((f) => f.recent && !f.hits.length);
console.log(`\n${C.bold(name)} ${C.dim(appDir)}`);
if (!issues.length && !backdoors.length) {
console.log(` ${C.grn('✔')} no exposure issues found`);
}
for (const [sev, msg] of issues) console.log(` ${sev} ${msg}`);
for (const f of backdoors) {
console.log(` ${C.red('BACKDOOR?')} ${f.rel} ${C.dim('(modified ' + f.mtime.toISOString() + ')')}`);
for (const h of f.hits) console.log(` ${C.yel(h)}`);
}
if (recents.length) {
console.log(` ${C.yel('REVIEW')} ${recents.length} source file(s) modified since ${sinceArg}:`);
for (const f of recents) console.log(` ${f.rel} ${C.dim(f.mtime.toISOString())}`);
}
return { name, critical: issues.filter((i) => i[0].includes('CRITICAL')).length + backdoors.length };
}
console.log(C.bold(`\n4bnode /_dev exposure audit — ${root}`));
if (since) console.log(C.dim(`Flagging source changed on/after ${sinceArg}`));
const apps = findApps(root);
if (!apps.length) { console.log(C.yel('\nNo 4bnode apps (.4bnode/dev-api.js) found under that path.')); process.exit(0); }
let totalCritical = 0;
for (const app of apps) totalCritical += auditApp(app).critical;
console.log(`\n${C.bold('Summary:')} ${apps.length} app(s) scanned, ${totalCritical ? C.red(totalCritical + ' critical finding(s)') : C.grn('0 critical findings')}.`);
process.exit(totalCritical ? 1 : 0);