feat(audit): Add core audit logging infrastructure

- Add AuditLogger class with rotation support
- Add audit configuration to config schema
- Instrument tool execution with full audit logging
- Instrument session lifecycle (create, message, delete, transfer, compact)
- Add audit logger initialization in daemon
- Add cron scheduler audit logging

Audit events captured:
- tool.start/success/error/denied
- session.create/message/delete/transfer/compact
- cron.trigger/add/remove

All logs go to ~/.local/share/flynn/audit.log (JSON lines)
with rotation (10MB files, 30-day retention)
This commit is contained in:
William Valentin
2026-02-11 15:58:07 -08:00
parent fae3565480
commit d62e836b5d
12 changed files with 732 additions and 1 deletions
+75
View File
@@ -0,0 +1,75 @@
import { createReadStream, promises as fs } from 'fs';
import { dirname, basename } from 'path';
import type { AuditEvent, AuditQuery } from './types.js';
export async function queryAuditLogs(logPath: string, query: AuditQuery): Promise<AuditEvent[]> {
const logs: AuditEvent[] = [];
try {
const content = await fs.readFile(logPath, 'utf-8');
const lines = content.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
try {
const event: AuditEvent = JSON.parse(line);
if (matchesQuery(event, query)) {
logs.push(event);
}
} catch {
// Skip malformed lines
}
}
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
throw err;
}
}
return logs.sort((a, b) => a.timestamp - b.timestamp);
}
function matchesQuery(event: AuditEvent, query: AuditQuery): boolean {
if (query.start_time && event.timestamp < query.start_time) {
return false;
}
if (query.end_time && event.timestamp > query.end_time) {
return false;
}
if (query.event_types && !query.event_types.includes(event.event_type)) {
return false;
}
if (query.level && event.level !== query.level) {
return false;
}
if (query.session_id) {
const eventSessionId = (event.event as Record<string, unknown>).session_id as string | undefined;
if (eventSessionId !== query.session_id) {
return false;
}
}
if (query.tool_name) {
const eventToolName = (event.event as Record<string, unknown>).tool_name as string | undefined;
if (eventToolName !== query.tool_name) {
return false;
}
}
return true;
}
export async function exportAuditLogs(logPath: string, query: AuditQuery, format: 'json' | 'csv'): Promise<string> {
const logs = await queryAuditLogs(logPath, query);
if (format === 'json') {
return JSON.stringify(logs, null, 2);
}
// CSV format
const headers = ['timestamp', 'level', 'event_type', 'event'];
const rows = logs.map(log => {
const eventStr = JSON.stringify(log.event).replace(/"/g, '""');
return `${log.timestamp},${log.level},${log.event_type},"${eventStr}"`;
});
return [headers.join(','), ...rows].join('\n');
}