This commit introduces a complete transparency infrastructure including: Core Transparency Modules: - AuditLog: Immutable, cryptographically-linked audit trail for all actions - EventStream: Real-time SSE streaming and webhook support - TransparencyDashboard: Aggregated metrics and system health monitoring - DigitalSignatures: Cryptographic verification for handoffs and certificates API Endpoints: - /api/transparency/dashboard - Full platform metrics - /api/transparency/audit - Query and log audit entries - /api/transparency/events - SSE stream and event history - /api/transparency/webhooks - Webhook management - /api/transparency/signatures - Digital signature operations - /api/transparency/certificate/[plantId] - Plant authenticity certificates - /api/transparency/export - Multi-format data export - /api/transparency/report - Compliance reporting - /api/transparency/health - System health checks Features: - Immutable audit logging with chain integrity verification - Real-time event streaming via Server-Sent Events - Webhook support with HMAC signature verification - Digital signatures for transport handoffs and ownership transfers - Certificate of Authenticity generation for plants - Multi-format data export (JSON, CSV, summary) - Public transparency portal at /transparency - System health monitoring for all components Documentation: - Comprehensive TRANSPARENCY.md guide with API examples
720 lines
20 KiB
TypeScript
720 lines
20 KiB
TypeScript
/**
|
|
* Comprehensive Audit Logging System for LocalGreenChain
|
|
*
|
|
* Provides complete transparency through immutable audit trails
|
|
* tracking all system actions, data modifications, and user activities.
|
|
*/
|
|
|
|
import * as crypto from 'crypto';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
// Audit Entry Types
|
|
export type AuditAction =
|
|
| 'CREATE' | 'UPDATE' | 'DELETE' | 'READ' | 'QUERY'
|
|
| 'TRANSFER' | 'VERIFY' | 'SIGN' | 'EXPORT'
|
|
| 'LOGIN' | 'LOGOUT' | 'API_CALL' | 'ERROR'
|
|
| 'PLANT_REGISTER' | 'PLANT_CLONE' | 'PLANT_TRANSFER'
|
|
| 'TRANSPORT_EVENT' | 'TRANSPORT_VERIFY'
|
|
| 'DEMAND_SIGNAL' | 'SUPPLY_COMMIT'
|
|
| 'FARM_UPDATE' | 'BATCH_CREATE' | 'BATCH_HARVEST'
|
|
| 'AGENT_TASK' | 'AGENT_ALERT'
|
|
| 'SYSTEM_EVENT' | 'CONFIG_CHANGE';
|
|
|
|
export type AuditSeverity = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';
|
|
|
|
export type AuditCategory =
|
|
| 'PLANT' | 'TRANSPORT' | 'DEMAND' | 'SUPPLY'
|
|
| 'FARM' | 'ENVIRONMENT' | 'USER' | 'SYSTEM'
|
|
| 'AGENT' | 'BLOCKCHAIN' | 'API' | 'SECURITY';
|
|
|
|
export interface AuditActor {
|
|
id: string;
|
|
type: 'USER' | 'SYSTEM' | 'AGENT' | 'API' | 'ANONYMOUS';
|
|
name?: string;
|
|
ip?: string;
|
|
userAgent?: string;
|
|
sessionId?: string;
|
|
}
|
|
|
|
export interface AuditResource {
|
|
type: string;
|
|
id: string;
|
|
name?: string;
|
|
previousState?: any;
|
|
newState?: any;
|
|
}
|
|
|
|
export interface AuditEntry {
|
|
id: string;
|
|
timestamp: string;
|
|
action: AuditAction;
|
|
category: AuditCategory;
|
|
severity: AuditSeverity;
|
|
actor: AuditActor;
|
|
resource?: AuditResource;
|
|
description: string;
|
|
metadata?: Record<string, any>;
|
|
correlationId?: string;
|
|
parentId?: string;
|
|
hash: string;
|
|
previousHash: string;
|
|
}
|
|
|
|
export interface AuditQuery {
|
|
startDate?: string;
|
|
endDate?: string;
|
|
actions?: AuditAction[];
|
|
categories?: AuditCategory[];
|
|
severities?: AuditSeverity[];
|
|
actorId?: string;
|
|
actorType?: AuditActor['type'];
|
|
resourceType?: string;
|
|
resourceId?: string;
|
|
correlationId?: string;
|
|
searchTerm?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
|
|
export interface AuditStats {
|
|
totalEntries: number;
|
|
entriesByAction: Record<AuditAction, number>;
|
|
entriesByCategory: Record<AuditCategory, number>;
|
|
entriesBySeverity: Record<AuditSeverity, number>;
|
|
entriesByActorType: Record<AuditActor['type'], number>;
|
|
entriesLast24h: number;
|
|
entriesLast7d: number;
|
|
entriesLast30d: number;
|
|
topActors: Array<{ id: string; count: number }>;
|
|
topResources: Array<{ type: string; count: number }>;
|
|
errorRate24h: number;
|
|
lastEntryAt: string | null;
|
|
}
|
|
|
|
export interface AuditReport {
|
|
generatedAt: string;
|
|
period: { start: string; end: string };
|
|
stats: AuditStats;
|
|
highlights: string[];
|
|
anomalies: AuditEntry[];
|
|
complianceStatus: {
|
|
dataIntegrity: boolean;
|
|
chainValid: boolean;
|
|
noTampering: boolean;
|
|
};
|
|
}
|
|
|
|
class AuditLog {
|
|
private entries: AuditEntry[] = [];
|
|
private dataDir: string;
|
|
private dataFile: string;
|
|
private autoSaveInterval: NodeJS.Timeout | null = null;
|
|
private genesisHash = '0';
|
|
|
|
constructor() {
|
|
this.dataDir = path.join(process.cwd(), 'data');
|
|
this.dataFile = path.join(this.dataDir, 'audit-log.json');
|
|
this.load();
|
|
this.startAutoSave();
|
|
}
|
|
|
|
private load(): void {
|
|
try {
|
|
if (fs.existsSync(this.dataFile)) {
|
|
const data = JSON.parse(fs.readFileSync(this.dataFile, 'utf-8'));
|
|
this.entries = data.entries || [];
|
|
console.log(`[AuditLog] Loaded ${this.entries.length} audit entries`);
|
|
} else {
|
|
this.entries = [];
|
|
this.logSystemEvent('Audit log initialized', 'INFO');
|
|
}
|
|
} catch (error) {
|
|
console.error('[AuditLog] Error loading audit log:', error);
|
|
this.entries = [];
|
|
}
|
|
}
|
|
|
|
private save(): void {
|
|
try {
|
|
if (!fs.existsSync(this.dataDir)) {
|
|
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(this.dataFile, JSON.stringify({
|
|
entries: this.entries,
|
|
exportedAt: new Date().toISOString(),
|
|
version: '1.0.0'
|
|
}, null, 2));
|
|
} catch (error) {
|
|
console.error('[AuditLog] Error saving audit log:', error);
|
|
}
|
|
}
|
|
|
|
private startAutoSave(): void {
|
|
// Auto-save every 30 seconds
|
|
this.autoSaveInterval = setInterval(() => this.save(), 30000);
|
|
}
|
|
|
|
private generateId(): string {
|
|
return `audit_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
|
|
}
|
|
|
|
private calculateHash(entry: Omit<AuditEntry, 'hash'>): string {
|
|
const data = JSON.stringify({
|
|
id: entry.id,
|
|
timestamp: entry.timestamp,
|
|
action: entry.action,
|
|
category: entry.category,
|
|
severity: entry.severity,
|
|
actor: entry.actor,
|
|
resource: entry.resource,
|
|
description: entry.description,
|
|
metadata: entry.metadata,
|
|
correlationId: entry.correlationId,
|
|
parentId: entry.parentId,
|
|
previousHash: entry.previousHash
|
|
});
|
|
return crypto.createHash('sha256').update(data).digest('hex');
|
|
}
|
|
|
|
private getPreviousHash(): string {
|
|
if (this.entries.length === 0) {
|
|
return this.genesisHash;
|
|
}
|
|
return this.entries[this.entries.length - 1].hash;
|
|
}
|
|
|
|
/**
|
|
* Log an audit entry
|
|
*/
|
|
log(
|
|
action: AuditAction,
|
|
category: AuditCategory,
|
|
description: string,
|
|
options: {
|
|
severity?: AuditSeverity;
|
|
actor?: Partial<AuditActor>;
|
|
resource?: AuditResource;
|
|
metadata?: Record<string, any>;
|
|
correlationId?: string;
|
|
parentId?: string;
|
|
} = {}
|
|
): AuditEntry {
|
|
const entry: Omit<AuditEntry, 'hash'> = {
|
|
id: this.generateId(),
|
|
timestamp: new Date().toISOString(),
|
|
action,
|
|
category,
|
|
severity: options.severity || 'INFO',
|
|
actor: {
|
|
id: options.actor?.id || 'system',
|
|
type: options.actor?.type || 'SYSTEM',
|
|
name: options.actor?.name,
|
|
ip: options.actor?.ip,
|
|
userAgent: options.actor?.userAgent,
|
|
sessionId: options.actor?.sessionId
|
|
},
|
|
resource: options.resource,
|
|
description,
|
|
metadata: options.metadata,
|
|
correlationId: options.correlationId,
|
|
parentId: options.parentId,
|
|
previousHash: this.getPreviousHash()
|
|
};
|
|
|
|
const hash = this.calculateHash(entry);
|
|
const fullEntry: AuditEntry = { ...entry, hash };
|
|
|
|
this.entries.push(fullEntry);
|
|
|
|
// Log critical events immediately
|
|
if (fullEntry.severity === 'CRITICAL' || fullEntry.severity === 'ERROR') {
|
|
console.log(`[AuditLog] ${fullEntry.severity}: ${fullEntry.description}`);
|
|
this.save();
|
|
}
|
|
|
|
return fullEntry;
|
|
}
|
|
|
|
/**
|
|
* Log a system event
|
|
*/
|
|
logSystemEvent(description: string, severity: AuditSeverity = 'INFO', metadata?: Record<string, any>): AuditEntry {
|
|
return this.log('SYSTEM_EVENT', 'SYSTEM', description, { severity, metadata });
|
|
}
|
|
|
|
/**
|
|
* Log an API call
|
|
*/
|
|
logApiCall(
|
|
endpoint: string,
|
|
method: string,
|
|
actor: Partial<AuditActor>,
|
|
options: {
|
|
statusCode?: number;
|
|
responseTime?: number;
|
|
error?: string;
|
|
requestBody?: any;
|
|
queryParams?: any;
|
|
} = {}
|
|
): AuditEntry {
|
|
const severity: AuditSeverity = options.error ? 'ERROR' :
|
|
(options.statusCode && options.statusCode >= 400) ? 'WARNING' : 'INFO';
|
|
|
|
return this.log('API_CALL', 'API', `${method} ${endpoint}`, {
|
|
severity,
|
|
actor,
|
|
metadata: {
|
|
endpoint,
|
|
method,
|
|
statusCode: options.statusCode,
|
|
responseTime: options.responseTime,
|
|
error: options.error,
|
|
requestBody: options.requestBody,
|
|
queryParams: options.queryParams
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Log a plant action
|
|
*/
|
|
logPlantAction(
|
|
action: 'PLANT_REGISTER' | 'PLANT_CLONE' | 'PLANT_TRANSFER',
|
|
plantId: string,
|
|
actor: Partial<AuditActor>,
|
|
options: {
|
|
previousState?: any;
|
|
newState?: any;
|
|
metadata?: Record<string, any>;
|
|
} = {}
|
|
): AuditEntry {
|
|
const descriptions: Record<string, string> = {
|
|
PLANT_REGISTER: `Plant ${plantId} registered`,
|
|
PLANT_CLONE: `Plant ${plantId} cloned`,
|
|
PLANT_TRANSFER: `Plant ${plantId} transferred`
|
|
};
|
|
|
|
return this.log(action, 'PLANT', descriptions[action], {
|
|
actor,
|
|
resource: {
|
|
type: 'plant',
|
|
id: plantId,
|
|
previousState: options.previousState,
|
|
newState: options.newState
|
|
},
|
|
metadata: options.metadata
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Log a transport event
|
|
*/
|
|
logTransportEvent(
|
|
eventType: string,
|
|
plantId: string,
|
|
actor: Partial<AuditActor>,
|
|
options: {
|
|
fromLocation?: { lat: number; lng: number };
|
|
toLocation?: { lat: number; lng: number };
|
|
distance?: number;
|
|
carbonFootprint?: number;
|
|
metadata?: Record<string, any>;
|
|
} = {}
|
|
): AuditEntry {
|
|
return this.log('TRANSPORT_EVENT', 'TRANSPORT', `Transport event: ${eventType} for plant ${plantId}`, {
|
|
actor,
|
|
resource: {
|
|
type: 'transport',
|
|
id: plantId
|
|
},
|
|
metadata: {
|
|
eventType,
|
|
...options
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Log an agent action
|
|
*/
|
|
logAgentAction(
|
|
agentName: string,
|
|
taskType: string,
|
|
success: boolean,
|
|
options: {
|
|
executionTime?: number;
|
|
error?: string;
|
|
metadata?: Record<string, any>;
|
|
} = {}
|
|
): AuditEntry {
|
|
return this.log('AGENT_TASK', 'AGENT', `Agent ${agentName} executed ${taskType}`, {
|
|
severity: success ? 'INFO' : 'WARNING',
|
|
actor: {
|
|
id: agentName,
|
|
type: 'AGENT',
|
|
name: agentName
|
|
},
|
|
metadata: {
|
|
taskType,
|
|
success,
|
|
executionTime: options.executionTime,
|
|
error: options.error,
|
|
...options.metadata
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Log a blockchain action
|
|
*/
|
|
logBlockchainAction(
|
|
action: 'CREATE' | 'VERIFY' | 'UPDATE',
|
|
blockIndex: number,
|
|
blockHash: string,
|
|
options: {
|
|
valid?: boolean;
|
|
error?: string;
|
|
metadata?: Record<string, any>;
|
|
} = {}
|
|
): AuditEntry {
|
|
const severity: AuditSeverity = options.error ? 'ERROR' :
|
|
options.valid === false ? 'WARNING' : 'INFO';
|
|
|
|
return this.log(action, 'BLOCKCHAIN', `Block ${blockIndex} ${action.toLowerCase()}d`, {
|
|
severity,
|
|
resource: {
|
|
type: 'block',
|
|
id: blockIndex.toString(),
|
|
name: blockHash.substring(0, 16)
|
|
},
|
|
metadata: {
|
|
blockHash,
|
|
valid: options.valid,
|
|
error: options.error,
|
|
...options.metadata
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Query audit entries
|
|
*/
|
|
query(queryParams: AuditQuery): { entries: AuditEntry[]; total: number } {
|
|
let filtered = [...this.entries];
|
|
|
|
if (queryParams.startDate) {
|
|
filtered = filtered.filter(e => e.timestamp >= queryParams.startDate!);
|
|
}
|
|
if (queryParams.endDate) {
|
|
filtered = filtered.filter(e => e.timestamp <= queryParams.endDate!);
|
|
}
|
|
if (queryParams.actions && queryParams.actions.length > 0) {
|
|
filtered = filtered.filter(e => queryParams.actions!.includes(e.action));
|
|
}
|
|
if (queryParams.categories && queryParams.categories.length > 0) {
|
|
filtered = filtered.filter(e => queryParams.categories!.includes(e.category));
|
|
}
|
|
if (queryParams.severities && queryParams.severities.length > 0) {
|
|
filtered = filtered.filter(e => queryParams.severities!.includes(e.severity));
|
|
}
|
|
if (queryParams.actorId) {
|
|
filtered = filtered.filter(e => e.actor.id === queryParams.actorId);
|
|
}
|
|
if (queryParams.actorType) {
|
|
filtered = filtered.filter(e => e.actor.type === queryParams.actorType);
|
|
}
|
|
if (queryParams.resourceType) {
|
|
filtered = filtered.filter(e => e.resource?.type === queryParams.resourceType);
|
|
}
|
|
if (queryParams.resourceId) {
|
|
filtered = filtered.filter(e => e.resource?.id === queryParams.resourceId);
|
|
}
|
|
if (queryParams.correlationId) {
|
|
filtered = filtered.filter(e => e.correlationId === queryParams.correlationId);
|
|
}
|
|
if (queryParams.searchTerm) {
|
|
const term = queryParams.searchTerm.toLowerCase();
|
|
filtered = filtered.filter(e =>
|
|
e.description.toLowerCase().includes(term) ||
|
|
e.actor.name?.toLowerCase().includes(term) ||
|
|
e.resource?.name?.toLowerCase().includes(term)
|
|
);
|
|
}
|
|
|
|
const total = filtered.length;
|
|
|
|
// Sort by timestamp descending (newest first)
|
|
filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
|
|
// Apply pagination
|
|
const offset = queryParams.offset || 0;
|
|
const limit = queryParams.limit || 100;
|
|
filtered = filtered.slice(offset, offset + limit);
|
|
|
|
return { entries: filtered, total };
|
|
}
|
|
|
|
/**
|
|
* Get audit statistics
|
|
*/
|
|
getStats(): AuditStats {
|
|
const now = new Date();
|
|
const day = 24 * 60 * 60 * 1000;
|
|
|
|
const entriesByAction: Record<string, number> = {};
|
|
const entriesByCategory: Record<string, number> = {};
|
|
const entriesBySeverity: Record<string, number> = {};
|
|
const entriesByActorType: Record<string, number> = {};
|
|
const actorCounts: Record<string, number> = {};
|
|
const resourceCounts: Record<string, number> = {};
|
|
|
|
let entriesLast24h = 0;
|
|
let entriesLast7d = 0;
|
|
let entriesLast30d = 0;
|
|
let errorsLast24h = 0;
|
|
|
|
for (const entry of this.entries) {
|
|
// Count by action
|
|
entriesByAction[entry.action] = (entriesByAction[entry.action] || 0) + 1;
|
|
|
|
// Count by category
|
|
entriesByCategory[entry.category] = (entriesByCategory[entry.category] || 0) + 1;
|
|
|
|
// Count by severity
|
|
entriesBySeverity[entry.severity] = (entriesBySeverity[entry.severity] || 0) + 1;
|
|
|
|
// Count by actor type
|
|
entriesByActorType[entry.actor.type] = (entriesByActorType[entry.actor.type] || 0) + 1;
|
|
|
|
// Count by actor
|
|
actorCounts[entry.actor.id] = (actorCounts[entry.actor.id] || 0) + 1;
|
|
|
|
// Count by resource type
|
|
if (entry.resource?.type) {
|
|
resourceCounts[entry.resource.type] = (resourceCounts[entry.resource.type] || 0) + 1;
|
|
}
|
|
|
|
// Time-based counts
|
|
const entryTime = new Date(entry.timestamp).getTime();
|
|
const age = now.getTime() - entryTime;
|
|
|
|
if (age <= day) {
|
|
entriesLast24h++;
|
|
if (entry.severity === 'ERROR' || entry.severity === 'CRITICAL') {
|
|
errorsLast24h++;
|
|
}
|
|
}
|
|
if (age <= 7 * day) {
|
|
entriesLast7d++;
|
|
}
|
|
if (age <= 30 * day) {
|
|
entriesLast30d++;
|
|
}
|
|
}
|
|
|
|
const topActors = Object.entries(actorCounts)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 10)
|
|
.map(([id, count]) => ({ id, count }));
|
|
|
|
const topResources = Object.entries(resourceCounts)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 10)
|
|
.map(([type, count]) => ({ type, count }));
|
|
|
|
return {
|
|
totalEntries: this.entries.length,
|
|
entriesByAction: entriesByAction as Record<AuditAction, number>,
|
|
entriesByCategory: entriesByCategory as Record<AuditCategory, number>,
|
|
entriesBySeverity: entriesBySeverity as Record<AuditSeverity, number>,
|
|
entriesByActorType: entriesByActorType as Record<AuditActor['type'], number>,
|
|
entriesLast24h,
|
|
entriesLast7d,
|
|
entriesLast30d,
|
|
topActors,
|
|
topResources,
|
|
errorRate24h: entriesLast24h > 0 ? errorsLast24h / entriesLast24h : 0,
|
|
lastEntryAt: this.entries.length > 0 ? this.entries[this.entries.length - 1].timestamp : null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Verify the integrity of the audit chain
|
|
*/
|
|
verifyIntegrity(): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = [];
|
|
|
|
for (let i = 0; i < this.entries.length; i++) {
|
|
const entry = this.entries[i];
|
|
|
|
// Verify hash
|
|
const { hash, ...entryWithoutHash } = entry;
|
|
const calculatedHash = this.calculateHash(entryWithoutHash as any);
|
|
|
|
if (calculatedHash !== hash) {
|
|
errors.push(`Entry ${entry.id} has invalid hash (index ${i})`);
|
|
}
|
|
|
|
// Verify chain linkage
|
|
if (i === 0) {
|
|
if (entry.previousHash !== this.genesisHash) {
|
|
errors.push(`First entry should have genesis hash as previousHash`);
|
|
}
|
|
} else {
|
|
if (entry.previousHash !== this.entries[i - 1].hash) {
|
|
errors.push(`Entry ${entry.id} has broken chain link at index ${i}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate an audit report
|
|
*/
|
|
generateReport(startDate: string, endDate: string): AuditReport {
|
|
const { entries } = this.query({ startDate, endDate, limit: 10000 });
|
|
const stats = this.getStats();
|
|
const integrity = this.verifyIntegrity();
|
|
|
|
// Find anomalies (errors, critical events)
|
|
const anomalies = entries.filter(e =>
|
|
e.severity === 'ERROR' || e.severity === 'CRITICAL'
|
|
).slice(0, 50);
|
|
|
|
// Generate highlights
|
|
const highlights: string[] = [];
|
|
|
|
if (stats.entriesLast24h > 0) {
|
|
highlights.push(`${stats.entriesLast24h} events logged in the last 24 hours`);
|
|
}
|
|
if (stats.errorRate24h > 0.05) {
|
|
highlights.push(`Warning: Error rate is ${(stats.errorRate24h * 100).toFixed(1)}% in last 24h`);
|
|
}
|
|
if (anomalies.length > 0) {
|
|
highlights.push(`${anomalies.length} anomalies detected in the reporting period`);
|
|
}
|
|
if (!integrity.valid) {
|
|
highlights.push(`ALERT: Audit chain integrity compromised - ${integrity.errors.length} issues found`);
|
|
}
|
|
|
|
return {
|
|
generatedAt: new Date().toISOString(),
|
|
period: { start: startDate, end: endDate },
|
|
stats,
|
|
highlights,
|
|
anomalies,
|
|
complianceStatus: {
|
|
dataIntegrity: integrity.valid,
|
|
chainValid: integrity.valid,
|
|
noTampering: integrity.errors.length === 0
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get recent entries
|
|
*/
|
|
getRecent(limit: number = 50): AuditEntry[] {
|
|
return this.entries.slice(-limit).reverse();
|
|
}
|
|
|
|
/**
|
|
* Get entries by correlation ID (for tracking related actions)
|
|
*/
|
|
getByCorrelation(correlationId: string): AuditEntry[] {
|
|
return this.entries.filter(e => e.correlationId === correlationId);
|
|
}
|
|
|
|
/**
|
|
* Export audit log to various formats
|
|
*/
|
|
export(format: 'json' | 'csv' | 'summary', query?: AuditQuery): string {
|
|
const { entries } = this.query(query || { limit: 10000 });
|
|
|
|
switch (format) {
|
|
case 'json':
|
|
return JSON.stringify(entries, null, 2);
|
|
|
|
case 'csv':
|
|
const headers = ['Timestamp', 'Action', 'Category', 'Severity', 'Actor', 'Actor Type', 'Resource', 'Description'];
|
|
const rows = entries.map(e => [
|
|
e.timestamp,
|
|
e.action,
|
|
e.category,
|
|
e.severity,
|
|
e.actor.id,
|
|
e.actor.type,
|
|
e.resource?.id || '',
|
|
`"${e.description.replace(/"/g, '""')}"`
|
|
]);
|
|
return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
|
|
|
case 'summary':
|
|
const stats = this.getStats();
|
|
return `
|
|
AUDIT LOG SUMMARY
|
|
=================
|
|
Generated: ${new Date().toISOString()}
|
|
|
|
Total Entries: ${stats.totalEntries}
|
|
Last 24 Hours: ${stats.entriesLast24h}
|
|
Last 7 Days: ${stats.entriesLast7d}
|
|
Last 30 Days: ${stats.entriesLast30d}
|
|
|
|
Error Rate (24h): ${(stats.errorRate24h * 100).toFixed(2)}%
|
|
|
|
Top Actions:
|
|
${Object.entries(stats.entriesByAction)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 5)
|
|
.map(([action, count]) => ` - ${action}: ${count}`)
|
|
.join('\n')}
|
|
|
|
Top Categories:
|
|
${Object.entries(stats.entriesByCategory)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 5)
|
|
.map(([cat, count]) => ` - ${cat}: ${count}`)
|
|
.join('\n')}
|
|
`.trim();
|
|
|
|
default:
|
|
return JSON.stringify(entries);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Force save (for shutdown)
|
|
*/
|
|
forceSave(): void {
|
|
this.save();
|
|
}
|
|
|
|
/**
|
|
* Cleanup on shutdown
|
|
*/
|
|
shutdown(): void {
|
|
if (this.autoSaveInterval) {
|
|
clearInterval(this.autoSaveInterval);
|
|
}
|
|
this.save();
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let auditLogInstance: AuditLog | null = null;
|
|
|
|
export function getAuditLog(): AuditLog {
|
|
if (!auditLogInstance) {
|
|
auditLogInstance = new AuditLog();
|
|
}
|
|
return auditLogInstance;
|
|
}
|
|
|
|
export default AuditLog;
|