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,250 @@
|
||||
import { createWriteStream, existsSync, mkdirSync, promises as fs } from 'fs';
|
||||
import { dirname } from 'path';
|
||||
import type {
|
||||
AuditEvent,
|
||||
AuditConfig,
|
||||
ToolStartEvent,
|
||||
ToolSuccessEvent,
|
||||
ToolErrorEvent,
|
||||
ToolDeniedEvent,
|
||||
SessionCreateEvent,
|
||||
SessionMessageEvent,
|
||||
SessionDeleteEvent,
|
||||
SessionCompactEvent,
|
||||
CronTriggerEvent,
|
||||
WebhookReceiveEvent,
|
||||
HeartbeatCycleEvent,
|
||||
HeartbeatCheckEvent,
|
||||
HeartbeatFailEvent,
|
||||
HeartbeatRecoverEvent,
|
||||
GmailPollEvent,
|
||||
GmailNewEmailEvent,
|
||||
} from './types.js';
|
||||
import { AuditRotator } from './rotation.js';
|
||||
|
||||
export class AuditLogger {
|
||||
private writeStream: import('fs').WriteStream | null = null;
|
||||
private config: AuditConfig;
|
||||
private rotator: AuditRotator;
|
||||
|
||||
constructor(config: AuditConfig) {
|
||||
this.config = config;
|
||||
this.rotator = new AuditRotator(config);
|
||||
|
||||
if (!this.config.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureLogDirectory();
|
||||
this.rotator.checkRotation();
|
||||
this.writeStream = createWriteStream(config.path, { flags: 'a' });
|
||||
}
|
||||
|
||||
private ensureLogDirectory(): void {
|
||||
const logDir = dirname(this.config.path);
|
||||
if (!existsSync(logDir)) {
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
private write(event: Omit<AuditEvent, 'timestamp'>): void {
|
||||
if (!this.config.enabled || !this.writeStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rotator.checkRotation();
|
||||
|
||||
const fullEvent: AuditEvent = { ...event, timestamp: Date.now() };
|
||||
this.writeStream!.write(JSON.stringify(fullEvent) + '\n');
|
||||
}
|
||||
|
||||
private shouldLog(category: 'tools' | 'sessions' | 'automation', level: string): boolean {
|
||||
const levelOrder = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
const configLevel = this.config.levels[category];
|
||||
return levelOrder[level as keyof typeof levelOrder] >= levelOrder[configLevel];
|
||||
}
|
||||
|
||||
// ── Tool Events ───────────────────────────────────────────────
|
||||
|
||||
toolStart(event: ToolStartEvent): void {
|
||||
if (!this.shouldLog('tools', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'tool.start', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
toolSuccess(event: ToolSuccessEvent): void {
|
||||
if (!this.shouldLog('tools', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'tool.success', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
toolError(event: ToolErrorEvent): void {
|
||||
if (!this.shouldLog('tools', 'error')) return;
|
||||
this.write({ level: 'error', event_type: 'tool.error', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
toolDenied(event: ToolDeniedEvent): void {
|
||||
if (!this.shouldLog('tools', 'warn')) return;
|
||||
this.write({ level: 'warn', event_type: 'tool.denied', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
// ── Session Events ───────────────────────────────────────────
|
||||
|
||||
sessionCreate(event: SessionCreateEvent): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'session.create', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
sessionMessage(event: SessionMessageEvent): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'session.message', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
sessionDelete(event: SessionDeleteEvent): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'session.delete', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
sessionCompact(event: SessionCompactEvent): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'session.compact', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
sessionTransfer(from: string, to: string, messageCount: number): void {
|
||||
if (!this.shouldLog('sessions', 'debug')) return;
|
||||
this.write({
|
||||
level: 'debug',
|
||||
event_type: 'session.transfer',
|
||||
event: { from_session: from, to_session: to, message_count: messageCount },
|
||||
});
|
||||
}
|
||||
|
||||
// ── Automation Events ───────────────────────────────────────
|
||||
|
||||
// Cron
|
||||
cronTrigger(event: CronTriggerEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'cron.trigger', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
cronAdd(jobName: string, schedule: string): void {
|
||||
if (!this.shouldLog('automation', 'info')) return;
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'cron.add',
|
||||
event: { job_name: jobName, schedule },
|
||||
});
|
||||
}
|
||||
|
||||
cronRemove(jobName: string): void {
|
||||
if (!this.shouldLog('automation', 'info')) return;
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'cron.remove',
|
||||
event: { job_name: jobName },
|
||||
});
|
||||
}
|
||||
|
||||
// Webhook
|
||||
webhookReceive(event: WebhookReceiveEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'webhook.receive', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
webhookNotFound(webhookName: string): void {
|
||||
if (!this.shouldLog('automation', 'warn')) return;
|
||||
this.write({
|
||||
level: 'warn',
|
||||
event_type: 'webhook.not_found',
|
||||
event: { webhook_name: webhookName },
|
||||
});
|
||||
}
|
||||
|
||||
webhookDenied(webhookName: string, reason: string): void {
|
||||
if (!this.shouldLog('automation', 'warn')) return;
|
||||
this.write({
|
||||
level: 'warn',
|
||||
event_type: 'webhook.denied',
|
||||
event: { webhook_name: webhookName, reason },
|
||||
});
|
||||
}
|
||||
|
||||
// Heartbeat
|
||||
heartbeatCycle(event: HeartbeatCycleEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'heartbeat.cycle', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
heartbeatCheck(event: HeartbeatCheckEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'heartbeat.check', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
heartbeatFail(event: HeartbeatFailEvent): void {
|
||||
if (!this.shouldLog('automation', 'warn')) return;
|
||||
this.write({ level: 'warn', event_type: 'heartbeat.fail', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
heartbeatRecover(event: HeartbeatRecoverEvent): void {
|
||||
if (!this.shouldLog('automation', 'info')) return;
|
||||
this.write({ level: 'info', event_type: 'heartbeat.recover', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
// Gmail
|
||||
gmailPoll(event: GmailPollEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'gmail.poll', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
gmailNewEmail(event: GmailNewEmailEvent): void {
|
||||
if (!this.shouldLog('automation', 'debug')) return;
|
||||
this.write({ level: 'debug', event_type: 'gmail.new_email', event: event as unknown as Record<string, unknown> });
|
||||
}
|
||||
|
||||
gmailError(error: string, context?: string): void {
|
||||
if (!this.shouldLog('automation', 'error')) return;
|
||||
this.write({
|
||||
level: 'error',
|
||||
event_type: 'gmail.error',
|
||||
event: { error, context },
|
||||
});
|
||||
}
|
||||
|
||||
// ── System Events ────────────────────────────────────────────
|
||||
|
||||
systemStart(component: string, config?: Record<string, unknown>): void {
|
||||
if (!this.config.enabled) return;
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'system.start',
|
||||
event: { component, config },
|
||||
});
|
||||
}
|
||||
|
||||
systemStop(component: string, reason?: string): void {
|
||||
if (!this.config.enabled) return;
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'system.stop',
|
||||
event: { component, reason },
|
||||
});
|
||||
}
|
||||
|
||||
systemConfig(component: string, action: string, config: Record<string, unknown>): void {
|
||||
if (!this.config.enabled) return;
|
||||
this.write({
|
||||
level: 'info',
|
||||
event_type: 'system.config',
|
||||
event: { component, action, config },
|
||||
});
|
||||
}
|
||||
|
||||
// ── Lifecycle ───────────────────────────────────────────────
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.writeStream) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.writeStream!.end(() => resolve());
|
||||
});
|
||||
this.writeStream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user