178 lines
6.2 KiB
JavaScript
178 lines
6.2 KiB
JavaScript
#!/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<object>} 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);
|
|
});
|
|
}
|