#!/usr/bin/env node /** * Config-Audit Posture Assessment CLI * Runs all scanners + scoring in a single Node.js process. * Usage: node posture.mjs [--json] [--global] [--output-file path] * Zero external dependencies. */ import { resolve } from 'node:path'; import { writeFile } from 'node:fs/promises'; import { runAllScanners } from './scan-orchestrator.mjs'; import { calculateUtilization, determineMaturityLevel, determineSegment, scoreByArea, topActions, generateScorecard, generateHealthScorecard, } from './lib/scoring.mjs'; /** * Run posture assessment and return structured result. * @param {string} targetPath * @param {object} [opts] * @param {boolean} [opts.includeGlobal=false] * @param {boolean} [opts.fullMachine=false] - Scan all known locations across the machine * @returns {Promise} */ export async function runPosture(targetPath, opts = {}) { const envelope = await runAllScanners(targetPath, opts); // Extract GAP scanner results const gapScanner = envelope.scanners.find(s => s.scanner === 'GAP'); const gapFindings = gapScanner ? gapScanner.findings : []; // Calculate scores const utilization = calculateUtilization(gapFindings); const maturity = determineMaturityLevel(gapFindings, { files: [] }); const segment = determineSegment(utilization.score); const areaScores = scoreByArea(envelope.scanners); const actions = topActions(gapFindings); return { utilization, maturity, segment, areas: areaScores.areas, overallGrade: areaScores.overallGrade, topActions: actions, opportunityCount: gapFindings.length, scannerEnvelope: envelope, }; } // --- CLI entry point --- async function main() { const args = process.argv.slice(2); let targetPath = '.'; let outputFile = null; let jsonMode = false; let rawMode = false; let includeGlobal = false; let fullMachine = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--output-file' && args[i + 1]) { outputFile = args[++i]; } else if (args[i] === '--json') { jsonMode = true; } else if (args[i] === '--raw') { rawMode = true; } else if (args[i] === '--global') { includeGlobal = true; } else if (args[i] === '--full-machine') { fullMachine = true; } else if (args[i] === '--include-fixtures') { // handled below } else if (!args[i].startsWith('-')) { targetPath = args[i]; } } const filterFixtures = !args.includes('--include-fixtures'); const humanizedProgress = !jsonMode && !rawMode; const result = await runPosture(targetPath, { includeGlobal, fullMachine, filterFixtures, humanizedProgress, }); // stdout JSON path: --json and --raw both write the v5.0.0-shape result // (byte-identical). Default mode writes nothing to stdout. if (jsonMode || rawMode) { const json = JSON.stringify(result, null, 2); process.stdout.write(json + '\n'); } // stderr scorecard path: --json suppresses; --raw renders v5.0.0 verbatim // (humanized=false); default renders humanized scorecard. if (!jsonMode) { const scorecard = generateHealthScorecard( { areas: result.areas, overallGrade: result.overallGrade }, result.opportunityCount, { humanized: !rawMode }, ); process.stderr.write('\n' + scorecard + '\n'); } if (outputFile) { const json = JSON.stringify(result, null, 2); await writeFile(outputFile, json, 'utf-8'); process.stderr.write(`\nResults written to ${outputFile}\n`); } } // 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(1); }); }