/** * 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} 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); }