124 lines
3.3 KiB
JavaScript
124 lines
3.3 KiB
JavaScript
/**
|
|
* Baseline manager for config-audit.
|
|
* Stores and retrieves scanner envelopes as named baselines.
|
|
* Zero external dependencies.
|
|
*/
|
|
|
|
import { readFile, writeFile, readdir, unlink, mkdir, stat } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
import { homedir } from 'node:os';
|
|
|
|
const BASELINES_DIR = join(homedir(), '.config-audit', 'baselines');
|
|
|
|
/**
|
|
* Get the baselines directory path.
|
|
* @returns {string}
|
|
*/
|
|
export function getBaselinesDir() {
|
|
return BASELINES_DIR;
|
|
}
|
|
|
|
/**
|
|
* Save a scanner envelope as a named baseline.
|
|
* @param {object} envelope - Full envelope from scan-orchestrator
|
|
* @param {string} [name='default'] - Baseline name
|
|
* @returns {Promise<{ path: string, name: string }>}
|
|
*/
|
|
export async function saveBaseline(envelope, name = 'default') {
|
|
await mkdir(BASELINES_DIR, { recursive: true });
|
|
|
|
const enriched = {
|
|
...envelope,
|
|
_baseline: {
|
|
saved_at: new Date().toISOString(),
|
|
target_path: envelope.meta?.target || '',
|
|
finding_count: envelope.aggregate?.total_findings || 0,
|
|
score: avgScore(envelope),
|
|
},
|
|
};
|
|
|
|
const filePath = join(BASELINES_DIR, `${name}.json`);
|
|
await writeFile(filePath, JSON.stringify(enriched, null, 2), 'utf-8');
|
|
|
|
return { path: filePath, name };
|
|
}
|
|
|
|
/**
|
|
* Load a named baseline.
|
|
* @param {string} [name='default'] - Baseline name
|
|
* @returns {Promise<object|null>} Envelope or null if not found
|
|
*/
|
|
export async function loadBaseline(name = 'default') {
|
|
const filePath = join(BASELINES_DIR, `${name}.json`);
|
|
try {
|
|
const content = await readFile(filePath, 'utf-8');
|
|
return JSON.parse(content);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all saved baselines.
|
|
* @returns {Promise<{ baselines: Array<{ name: string, savedAt: string, targetPath: string, findingCount: number, score: number }> }>}
|
|
*/
|
|
export async function listBaselines() {
|
|
try {
|
|
await stat(BASELINES_DIR);
|
|
} catch {
|
|
return { baselines: [] };
|
|
}
|
|
|
|
const entries = await readdir(BASELINES_DIR);
|
|
const baselines = [];
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.endsWith('.json')) continue;
|
|
const name = entry.replace(/\.json$/, '');
|
|
const filePath = join(BASELINES_DIR, entry);
|
|
|
|
try {
|
|
const content = await readFile(filePath, 'utf-8');
|
|
const data = JSON.parse(content);
|
|
const meta = data._baseline || {};
|
|
baselines.push({
|
|
name,
|
|
savedAt: meta.saved_at || '',
|
|
targetPath: meta.target_path || '',
|
|
findingCount: meta.finding_count || 0,
|
|
score: meta.score || 0,
|
|
});
|
|
} catch {
|
|
// Skip corrupt baselines
|
|
baselines.push({ name, savedAt: '', targetPath: '', findingCount: 0, score: 0 });
|
|
}
|
|
}
|
|
|
|
return { baselines };
|
|
}
|
|
|
|
/**
|
|
* Delete a named baseline.
|
|
* @param {string} name - Baseline name
|
|
* @returns {Promise<{ deleted: boolean }>}
|
|
*/
|
|
export async function deleteBaseline(name) {
|
|
const filePath = join(BASELINES_DIR, `${name}.json`);
|
|
try {
|
|
await unlink(filePath);
|
|
return { deleted: true };
|
|
} catch {
|
|
return { deleted: false };
|
|
}
|
|
}
|
|
|
|
// --- Internal helpers ---
|
|
|
|
function avgScore(envelope) {
|
|
const scanners = envelope.scanners || [];
|
|
if (scanners.length === 0) return 0;
|
|
// Simple: count findings as proxy for score
|
|
const total = envelope.aggregate?.total_findings || 0;
|
|
// Lower findings = higher score. Cap at 100.
|
|
return Math.max(0, 100 - total * 3);
|
|
}
|