130 lines
4.5 KiB
JavaScript
130 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Config-Audit Drift CLI
|
|
* Compare current configuration against a saved baseline.
|
|
* Usage:
|
|
* node drift-cli.mjs <path> --save [--name my-baseline]
|
|
* node drift-cli.mjs <path> [--baseline my-baseline] [--json]
|
|
* node drift-cli.mjs --list
|
|
* Zero external dependencies.
|
|
*/
|
|
|
|
import { resolve } from 'node:path';
|
|
import { runAllScanners } from './scan-orchestrator.mjs';
|
|
import { diffEnvelopes, formatDiffReport } from './lib/diff-engine.mjs';
|
|
import { saveBaseline, loadBaseline, listBaselines } from './lib/baseline.mjs';
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
let targetPath = '.';
|
|
let baselineName = 'default';
|
|
let save = false;
|
|
let list = false;
|
|
let jsonMode = false;
|
|
let includeGlobal = false;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--save') {
|
|
save = true;
|
|
} else if (args[i] === '--name' && args[i + 1]) {
|
|
baselineName = args[++i];
|
|
} else if (args[i] === '--baseline' && args[i + 1]) {
|
|
baselineName = args[++i];
|
|
} else if (args[i] === '--list') {
|
|
list = true;
|
|
} else if (args[i] === '--json') {
|
|
jsonMode = true;
|
|
} else if (args[i] === '--global') {
|
|
includeGlobal = true;
|
|
} else if (!args[i].startsWith('-')) {
|
|
targetPath = args[i];
|
|
}
|
|
}
|
|
|
|
// --- List mode ---
|
|
if (list) {
|
|
const result = await listBaselines();
|
|
if (jsonMode) {
|
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
} else {
|
|
if (result.baselines.length === 0) {
|
|
process.stderr.write('No baselines saved.\n');
|
|
process.stderr.write('Save one with: node drift-cli.mjs <path> --save\n');
|
|
} else {
|
|
process.stderr.write('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
process.stderr.write(' Saved Baselines\n');
|
|
process.stderr.write('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n');
|
|
for (const b of result.baselines) {
|
|
process.stderr.write(` ${b.name.padEnd(20)} ${b.findingCount} findings ${b.savedAt}\n`);
|
|
}
|
|
process.stderr.write('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
}
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
// --- Save mode ---
|
|
if (save) {
|
|
if (!jsonMode) {
|
|
process.stderr.write(`Config-Audit Drift CLI v2.1.0\n`);
|
|
process.stderr.write(`Saving baseline "${baselineName}" for ${resolve(targetPath)}\n\n`);
|
|
}
|
|
|
|
const envelope = await runAllScanners(targetPath, { includeGlobal });
|
|
const result = await saveBaseline(envelope, baselineName);
|
|
|
|
if (jsonMode) {
|
|
process.stdout.write(JSON.stringify({ saved: true, name: result.name, path: result.path }, null, 2) + '\n');
|
|
} else {
|
|
process.stderr.write(`\nBaseline "${result.name}" saved to ${result.path}\n`);
|
|
process.stderr.write(`Findings: ${envelope.aggregate.total_findings}\n`);
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
// --- Drift mode (default) ---
|
|
if (!jsonMode) {
|
|
process.stderr.write(`Config-Audit Drift CLI v2.1.0\n`);
|
|
process.stderr.write(`Target: ${resolve(targetPath)}\n`);
|
|
process.stderr.write(`Baseline: ${baselineName}\n\n`);
|
|
}
|
|
|
|
// Load baseline
|
|
const baseline = await loadBaseline(baselineName);
|
|
if (!baseline) {
|
|
if (jsonMode) {
|
|
process.stdout.write(JSON.stringify({ error: `Baseline "${baselineName}" not found. Save one with --save.` }, null, 2) + '\n');
|
|
} else {
|
|
process.stderr.write(`Baseline "${baselineName}" not found.\n`);
|
|
process.stderr.write(`Save one first: node drift-cli.mjs <path> --save\n`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
// Run current scan
|
|
const current = await runAllScanners(targetPath, { includeGlobal });
|
|
|
|
// Diff
|
|
const diff = diffEnvelopes(baseline, current);
|
|
|
|
if (jsonMode) {
|
|
process.stdout.write(JSON.stringify(diff, null, 2) + '\n');
|
|
} else {
|
|
const report = formatDiffReport(diff);
|
|
process.stderr.write('\n' + report + '\n');
|
|
}
|
|
|
|
// Exit code: 0=stable/improving, 1=degrading
|
|
if (diff.summary.trend === 'degrading') process.exit(1);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Only run CLI if invoked directly
|
|
const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
|
if (isDirectRun) {
|
|
main().catch(err => {
|
|
process.stderr.write(`Fatal: ${err.message}\n`);
|
|
process.exit(3);
|
|
});
|
|
}
|