Files
2026-07-13 13:39:12 +08:00

204 lines
6.9 KiB
JavaScript

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 <s>", t("audit.source"), "all")
.option("--follow", t("audit.tail.follow"))
.option("--limit <n>", t("audit.tail.limit"), parseInt, 100)
.action(runAuditTail);
audit
.command("search <query>")
.description(t("audit.search.description"))
.option("--source <s>", t("audit.source"), "all")
.option("--since <ts>", t("audit.since"))
.option("--until <ts>", t("audit.until"))
.option("--limit <n>", t("audit.search.limit"), parseInt, 200)
.option("--actor <id>", t("audit.search.actor"))
.option("--action <a>", t("audit.search.action"))
.action(runAuditSearch);
audit
.command("export <file>")
.description(t("audit.export.description"))
.option("--source <s>", t("audit.source"), "all")
.option("--format <f>", t("audit.export.format"), "jsonl")
.option("--since <ts>", t("audit.since"))
.option("--until <ts>", t("audit.until"))
.action(runAuditExport);
audit
.command("stats")
.description(t("audit.stats.description"))
.option("--source <s>", t("audit.source"), "mcp")
.option("--period <p>", t("audit.stats.period"), "7d")
.action(runAuditStats);
audit
.command("get <id>")
.description(t("audit.get.description"))
.option("--source <s>", t("audit.source"), "compliance")
.action(runAuditGet);
}