// audit.js - Audit Logging System
const fs = require('fs').promises;
const path = require('path');
const config = require('./config');
class AuditLogger {
constructor() {
this.logFile = config.audit.logFile;
this.enabled = config.audit.enabled;
this.queue = [];
this.isWriting = false;
this.init();
}
async init() {
if (!this.enabled) {
console.error('[AUDIT] Audit logging disabled');
return;
}
try {
// Ensure log directory exists
const logDir = path.dirname(this.logFile);
await fs.mkdir(logDir, { recursive: true });
console.error(`[AUDIT] Audit logging enabled: ${this.logFile}`);
// Write startup entry
await this.log('SYSTEM', 'startup', {
version: config.server.version,
environment: config.server.environment
});
} catch (error) {
console.error(`[AUDIT] Failed to initialize audit log: ${error.message}`);
}
}
/**
* Log an audit event
*/
async log(action, resource, details = {}, userId = 'system', severity = 'info') {
if (!this.enabled) return;
const entry = {
timestamp: new Date().toISOString(),
action,
resource,
userId,
severity,
details: typeof details === 'object' ? details : { message: String(details) },
pid: process.pid
};
// Add to queue
this.queue.push(entry);
// Process queue
await this.processQueue();
}
/**
* Process the log queue
*/
async processQueue() {
if (this.isWriting || this.queue.length === 0) return;
this.isWriting = true;
try {
// Get all pending entries
const entries = [...this.queue];
this.queue = [];
// Format entries as JSON lines
const lines = entries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
// Append to log file
await fs.appendFile(this.logFile, lines, 'utf8');
// Also log to stderr for real-time monitoring
entries.forEach(entry => {
console.error(`[AUDIT] ${entry.severity.toUpperCase()} - ${entry.action}: ${entry.resource}`);
});
} catch (error) {
console.error(`[AUDIT] Failed to write audit log: ${error.message}`);
// Don't lose the entries, put them back
this.queue.unshift(...entries);
} finally {
this.isWriting = false;
// Process any new entries that arrived while writing
if (this.queue.length > 0) {
await this.processQueue();
}
}
}
/**
* Log successful tool execution
*/
async logToolExecution(toolName, args, userId = 'unknown', duration = 0) {
await this.log(
'TOOL_EXECUTION',
toolName,
{
arguments: args,
durationMs: duration,
success: true
},
userId,
'info'
);
}
/**
* Log failed tool execution
*/
async logToolError(toolName, args, error, userId = 'unknown') {
await this.log(
'TOOL_ERROR',
toolName,
{
arguments: args,
error: error.message,
errorCode: error.code
},
userId,
'error'
);
}
/**
* Log security events
*/
async logSecurityEvent(eventType, details, userId = 'unknown') {
await this.log(
'SECURITY_EVENT',
eventType,
details,
userId,
'warning'
);
}
/**
* Log rate limit violations
*/
async logRateLimitViolation(userId, resource) {
await this.log(
'RATE_LIMIT_VIOLATION',
resource,
{ message: 'Rate limit exceeded' },
userId,
'warning'
);
}
/**
* Log credential extraction (high severity)
*/
async logCredentialExtraction(userId, pcapPath, credentialCount) {
await this.log(
'CREDENTIAL_EXTRACTION',
pcapPath,
{
credentialsFound: credentialCount,
warning: 'Sensitive operation performed'
},
userId,
'high'
);
}
/**
* Get recent audit entries
*/
async getRecentEntries(count = 100) {
if (!this.enabled) {
return [];
}
try {
const content = await fs.readFile(this.logFile, 'utf8');
const lines = content.trim().split('\n');
const entries = lines
.slice(-count)
.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter(entry => entry !== null);
return entries.reverse();
} catch (error) {
console.error(`[AUDIT] Failed to read audit log: ${error.message}`);
return [];
}
}
/**
* Flush any pending log entries
*/
async flush() {
await this.processQueue();
}
}
// Export singleton instance
module.exports = new AuditLogger();