ktg-plugin-marketplace/plugins/config-audit/scanners/lib/output.mjs

121 lines
3.5 KiB
JavaScript

/**
* Finding and result builders for config-audit scanners.
* Finding IDs: CA-{SCANNER}-{NNN} (e.g. CA-CML-001)
* Zero external dependencies.
*/
import { riskScore, riskBand, verdict } from './severity.mjs';
let findingCounter = 0;
/** Reset the finding counter. Call in beforeEach of tests and before each scanner run. */
export function resetCounter() {
findingCounter = 0;
}
/**
* Create a finding object with auto-incremented ID.
* @param {object} opts
* @param {string} opts.scanner - 3-letter scanner prefix (CML, SET, HKV, RUL, etc.)
* @param {string} opts.severity - critical | high | medium | low | info
* @param {string} opts.title
* @param {string} opts.description
* @param {string} [opts.file] - file path where finding was detected
* @param {number} [opts.line] - line number
* @param {string} [opts.evidence] - relevant snippet
* @param {string} [opts.category] - quality category
* @param {string} [opts.recommendation] - suggested fix
* @param {boolean} [opts.autoFixable] - can be auto-fixed
* @returns {object}
*/
export function finding(opts) {
findingCounter++;
const id = `CA-${opts.scanner}-${String(findingCounter).padStart(3, '0')}`;
return {
id,
scanner: opts.scanner,
severity: opts.severity,
title: opts.title,
description: opts.description,
file: opts.file || null,
line: opts.line || null,
evidence: opts.evidence || null,
category: opts.category || null,
recommendation: opts.recommendation || null,
autoFixable: opts.autoFixable || false,
};
}
/**
* Create a scanner result envelope.
* @param {string} scannerName - 3-letter prefix
* @param {'ok' | 'error' | 'skipped'} status
* @param {object[]} findings
* @param {number} filesScanned
* @param {number} durationMs
* @param {string} [errorMsg]
* @returns {object}
*/
export function scannerResult(scannerName, status, findings, filesScanned, durationMs, errorMsg) {
const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
for (const f of findings) {
if (counts[f.severity] !== undefined) {
counts[f.severity]++;
}
}
const result = {
scanner: scannerName,
status,
files_scanned: filesScanned,
duration_ms: durationMs,
findings,
counts,
};
if (errorMsg) result.error = errorMsg;
return result;
}
/**
* Create the top-level output envelope combining all scanner results.
* @param {string} targetPath
* @param {object[]} scannerResults
* @param {number} totalDurationMs
* @returns {object}
*/
export function envelope(targetPath, scannerResults, totalDurationMs) {
const aggregate = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
let totalFindings = 0;
let scannersOk = 0;
let scannersError = 0;
let scannersSkipped = 0;
for (const r of scannerResults) {
for (const sev of Object.keys(aggregate)) {
aggregate[sev] += (r.counts[sev] || 0);
}
totalFindings += r.findings.length;
if (r.status === 'ok') scannersOk++;
else if (r.status === 'error') scannersError++;
else if (r.status === 'skipped') scannersSkipped++;
}
return {
meta: {
target: targetPath,
timestamp: new Date().toISOString(),
version: '2.2.0',
tool: 'config-audit',
},
scanners: scannerResults,
aggregate: {
total_findings: totalFindings,
counts: aggregate,
risk_score: riskScore(aggregate),
risk_band: riskBand(riskScore(aggregate)),
verdict: verdict(aggregate),
scanners_ok: scannersOk,
scanners_error: scannersError,
scanners_skipped: scannersSkipped,
},
};
}