import { writeFileSync } from "node:fs"; import { setTimeout as sleep } from "node:timers/promises"; import { apiFetch } from "../api.mjs"; import { emit } from "../output.mjs"; import { t } from "../i18n.mjs"; function truncate(v, len = 40) { if (v == null) return "-"; const s = String(v); return s.length > len ? s.slice(0, len - 1) + "…" : s; } function fmtTs(v) { if (!v) return "-"; try { return new Date(v).toLocaleString(); } catch { return String(v); } } function maskActor(v) { if (!v) return "-"; const s = String(v); if (s.length <= 8) return s; return `${s.slice(0, 4)}****${s.slice(-4)}`; } const auditSchema = [ { key: "timestamp", header: "Time", width: 22, formatter: fmtTs }, { key: "source", header: "Source", width: 10 }, { key: "actor", header: "Actor", width: 16, formatter: maskActor }, { key: "action", header: "Action", width: 28 }, { key: "resource", header: "Resource", width: 32, formatter: truncate }, { key: "result", header: "Result", formatter: (v) => (v === "success" ? "✓" : "✗") }, { key: "details", header: "Details", formatter: truncate }, ]; function endpointFor(source) { return source === "mcp" ? "/api/mcp/audit" : "/api/compliance/audit-log"; } async function fetchAuditEntries(sources, params) { const entries = []; for (const src of sources) { const endpoint = endpointFor(src); const res = await apiFetch(`${endpoint}?${params}`); if (!res.ok) continue; const data = await res.json(); for (const e of data.items ?? data) { entries.push({ ...e, source: src }); } } return entries; } function resolveSources(source) { if (source === "all") return ["compliance", "mcp"]; return [source ?? "compliance"]; } export async function runAuditTail(opts, cmd) { const globalOpts = cmd.optsWithGlobals(); const sources = resolveSources(opts.source); const params = new URLSearchParams({ limit: String(opts.limit ?? 100) }); const entries = await fetchAuditEntries(sources, params); entries.sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? ""))); emit(entries.slice(0, opts.limit ?? 100), globalOpts, auditSchema); if (opts.follow) { process.stderr.write("\n[following — Ctrl+C to exit]\n"); let lastTs = entries[0]?.timestamp ?? new Date().toISOString(); const loop = async () => { while (true) { await sleep(2000); for (const src of sources) { const endpoint = endpointFor(src); const res = await apiFetch(`${endpoint}?since=${encodeURIComponent(lastTs)}&limit=50`); if (!res.ok) continue; const data = await res.json(); const newEntries = (data.items ?? data) .map((e) => ({ ...e, source: src })) .filter((e) => String(e.timestamp ?? "") > String(lastTs)); for (const e of newEntries) { if (String(e.timestamp ?? "") > String(lastTs)) lastTs = e.timestamp; emit([e], globalOpts, auditSchema); } } } }; process.on("SIGINT", () => process.exit(0)); await loop(); } } export async function runAuditSearch(query, opts, cmd) { const globalOpts = cmd.optsWithGlobals(); const sources = resolveSources(opts.source); const params = new URLSearchParams({ q: query, limit: String(opts.limit ?? 200) }); if (opts.since) params.set("since", opts.since); if (opts.until) params.set("until", opts.until); if (opts.actor) params.set("actor", opts.actor); if (opts.action) params.set("action", opts.action); const entries = await fetchAuditEntries(sources, params); entries.sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? ""))); emit(entries, globalOpts, auditSchema); } export async function runAuditExport(file, opts, cmd) { const sources = resolveSources(opts.source === "all" ? "compliance" : opts.source); const format = opts.format ?? "jsonl"; const params = new URLSearchParams({ format }); if (opts.since) params.set("since", opts.since); if (opts.until) params.set("until", opts.until); const allLines = []; for (const src of sources) { const endpoint = endpointFor(src); const res = await apiFetch(`${endpoint}?${params}`); if (!res.ok) { process.stderr.write(`Error fetching ${src}: ${res.status}\n`); continue; } const body = await res.text(); allLines.push(body); } const combined = allLines.join("\n"); writeFileSync(file, combined); process.stdout.write(`Exported to ${file} (${combined.length} bytes)\n`); } export async function runAuditStats(opts, cmd) { const globalOpts = cmd.optsWithGlobals(); const source = opts.source ?? "mcp"; const params = new URLSearchParams({ period: opts.period ?? "7d" }); const endpoint = source === "mcp" ? "/api/mcp/audit/stats" : "/api/compliance/audit-log/stats"; const res = await apiFetch(`${endpoint}?${params}`); if (!res.ok) { process.stderr.write(`Error: ${res.status}\n`); process.exit(1); } const data = await res.json(); emit(data, globalOpts); } export async function runAuditGet(id, opts, cmd) { const globalOpts = cmd.optsWithGlobals(); const source = opts.source ?? "compliance"; const endpoint = endpointFor(source); const res = await apiFetch(`${endpoint}/${id}`); if (!res.ok) { process.stderr.write(`Not found: ${id}\n`); process.exit(1); } const data = await res.json(); emit(data, globalOpts, auditSchema); } export function registerAudit(program) { const audit = program.command("audit").description(t("audit.description")); audit .command("tail") .description(t("audit.tail.description")) .option("--source ", t("audit.source"), "all") .option("--follow", t("audit.tail.follow")) .option("--limit ", t("audit.tail.limit"), parseInt, 100) .action(runAuditTail); audit .command("search ") .description(t("audit.search.description")) .option("--source ", t("audit.source"), "all") .option("--since ", t("audit.since")) .option("--until ", t("audit.until")) .option("--limit ", t("audit.search.limit"), parseInt, 200) .option("--actor ", t("audit.search.actor")) .option("--action ", t("audit.search.action")) .action(runAuditSearch); audit .command("export ") .description(t("audit.export.description")) .option("--source ", t("audit.source"), "all") .option("--format ", t("audit.export.format"), "jsonl") .option("--since ", t("audit.since")) .option("--until ", t("audit.until")) .action(runAuditExport); audit .command("stats") .description(t("audit.stats.description")) .option("--source ", t("audit.source"), "mcp") .option("--period

", t("audit.stats.period"), "7d") .action(runAuditStats); audit .command("get ") .description(t("audit.get.description")) .option("--source ", t("audit.source"), "compliance") .action(runAuditGet); }