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:
@@ -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');
|
||||
}
|
||||
Reference in New Issue
Block a user