#!/usr/bin/env node /** * Config-Audit Self-Audit * Runs the plugin's own scanners on its own configuration. * CLI: node self-audit.mjs [--json] [--fix] * Exit codes: 0=PASS (no critical/high), 1=WARN (high findings), 2=FAIL (critical findings) * Zero external dependencies. */ import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { runAllScanners } from './scan-orchestrator.mjs'; import { scan as scanPluginHealth } from './plugin-health-scanner.mjs'; import { scoreByArea } from './lib/scoring.mjs'; import { gradeFromPassRate } from './lib/severity.mjs'; import { loadSuppressions, applySuppressions } from './lib/suppression.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PLUGIN_ROOT = resolve(__dirname, '..'); /** * Run self-audit on this plugin. * @param {object} [opts] * @param {boolean} [opts.fix=false] - Run fix-engine on auto-fixable findings * @returns {Promise} Combined result */ export async function runSelfAudit(opts = {}) { const pluginDir = PLUGIN_ROOT; // 1. Run all config scanners on plugin root // Fixture filtering is handled automatically by runAllScanners (filterFixtures defaults to true) const configEnvelope = await runAllScanners(pluginDir); // 2. Run plugin health scanner + apply suppressions const pluginHealthResult = await scanPluginHealth(pluginDir); const { suppressions } = await loadSuppressions(pluginDir); if (suppressions.length > 0) { const { active, suppressed } = applySuppressions(pluginHealthResult.findings, suppressions); pluginHealthResult.findings = active; pluginHealthResult.suppressedFindings = suppressed; } // 3. Score config quality const areaScores = scoreByArea(configEnvelope.scanners); const avgScore = areaScores.areas.length > 0 ? Math.round(areaScores.areas.reduce((s, a) => s + a.score, 0) / areaScores.areas.length) : 0; const configGrade = gradeFromPassRate(avgScore); // 4. Score plugin health const pluginIssueCount = pluginHealthResult.findings.length; const pluginScore = Math.max(0, 100 - pluginIssueCount * 10); const pluginGrade = gradeFromPassRate(pluginScore); // 5. Determine overall result const allFindings = [ ...configEnvelope.scanners.flatMap(s => s.findings), ...pluginHealthResult.findings, ]; const hasCritical = allFindings.some(f => f.severity === 'critical'); const hasHigh = allFindings.some(f => f.severity === 'high'); let exitCode = 0; let verdict = 'PASS'; if (hasCritical) { exitCode = 2; verdict = 'FAIL'; } else if (hasHigh) { exitCode = 1; verdict = 'WARN'; } // 6. Optionally fix let fixResult = null; if (opts.fix && allFindings.some(f => f.autoFixable)) { try { const { planFixes, applyFixes } = await import('./fix-engine.mjs'); const plan = planFixes(configEnvelope); if (plan.length > 0) { fixResult = await applyFixes(plan); } } catch { // Fix engine unavailable or failed — non-fatal } } return { pluginDir, configGrade, configScore: avgScore, pluginGrade, pluginScore, configEnvelope, pluginHealthResult, allFindings, exitCode, verdict, fixResult, }; } /** * Format self-audit result for terminal display. * @param {object} result - From runSelfAudit() * @returns {string} */ export function formatSelfAudit(result) { const lines = []; lines.push('\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501'); lines.push(' Config-Audit Self-Audit'); lines.push('\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501'); lines.push(''); lines.push(` Plugin health: ${result.pluginGrade} (${result.pluginScore})`); lines.push(` Config quality: ${result.configGrade} (${result.configScore})`); lines.push(''); // Issues summary const nonInfo = result.allFindings.filter(f => f.severity !== 'info'); if (nonInfo.length > 0) { lines.push(` Issues (${nonInfo.length}):`); for (const f of nonInfo.slice(0, 10)) { lines.push(` - [${f.severity}] ${f.title}`); } if (nonInfo.length > 10) { lines.push(` ...and ${nonInfo.length - 10} more`); } } else { lines.push(' Issues (0)'); } lines.push(''); // Fix results if (result.fixResult) { const applied = result.fixResult.filter(r => r.status === 'applied').length; lines.push(` Auto-fix: ${applied} fix(es) applied`); lines.push(''); } // Verdict if (result.verdict === 'PASS') { lines.push(' Self-audit: PASS'); lines.push(' (No critical or high findings)'); } else if (result.verdict === 'WARN') { lines.push(' Self-audit: WARN'); lines.push(' (High-severity findings detected)'); } else { lines.push(' Self-audit: FAIL'); lines.push(' (Critical findings detected)'); } lines.push(''); lines.push('\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501'); return lines.join('\n'); } // --- CLI entry point --- async function main() { const args = process.argv.slice(2); const jsonMode = args.includes('--json'); const fixMode = args.includes('--fix'); const result = await runSelfAudit({ fix: fixMode }); if (jsonMode) { const json = JSON.stringify(result, null, 2) + '\n'; await new Promise(resolve => process.stdout.write(json, resolve)); } else { process.stderr.write('\n' + formatSelfAudit(result) + '\n'); } process.exitCode = result.exitCode; } const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url)); if (isDirectRun) { main().catch(err => { process.stderr.write(`Fatal: ${err.message}\n`); process.exit(3); }); }