vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
392 lines (391 loc) • 16.3 kB
JavaScript
import fs from 'fs-extra';
import path from 'path';
import crypto from 'crypto';
import { AppError } from '../../../utils/errors.js';
import logger from '../../../logger.js';
export class SecurityAuditLogger {
static instance = null;
config;
auditEvents = [];
suspiciousPatterns = new Map();
eventCounter = 0;
lastEventId = null;
currentLogFile = null;
logFileHandle = null;
constructor(config) {
this.config = {
enabled: true,
logDirectory: path.join(process.cwd(), 'data', 'audit-logs'),
maxLogFileSize: 10 * 1024 * 1024,
maxLogFiles: 100,
enableIntegrityProtection: true,
enableSuspiciousActivityDetection: true,
enableComplianceReporting: true,
retentionPeriodDays: 365,
encryptLogs: false,
...config
};
this.initializeAuditSystem();
this.initializeSuspiciousActivityPatterns();
logger.info({ config: this.config }, 'Security Audit Logger initialized');
}
static getInstance(config) {
if (!SecurityAuditLogger.instance) {
SecurityAuditLogger.instance = new SecurityAuditLogger(config);
}
return SecurityAuditLogger.instance;
}
async logSecurityEvent(eventType, severity, source, action, outcome, description, options) {
if (!this.config.enabled) {
return;
}
try {
const eventId = `audit_${++this.eventCounter}_${Date.now()}`;
const auditEvent = {
id: eventId,
timestamp: new Date(),
eventType,
severity,
source,
actor: {
userId: options?.actor?.userId,
sessionId: options?.actor?.sessionId,
ipAddress: options?.actor?.ipAddress,
userAgent: options?.actor?.userAgent
},
resource: {
type: options?.resource?.type || 'unknown',
id: options?.resource?.id,
path: options?.resource?.path
},
action,
outcome,
details: {
description,
metadata: options?.metadata,
errorCode: options?.errorCode,
stackTrace: options?.stackTrace
},
integrity: {
checksum: '',
previousEventId: this.lastEventId || undefined
}
};
if (this.config.enableIntegrityProtection) {
auditEvent.integrity.checksum = this.calculateEventChecksum(auditEvent);
}
this.auditEvents.push(auditEvent);
this.lastEventId = eventId;
if (this.auditEvents.length > 10000) {
this.auditEvents = this.auditEvents.slice(-10000);
}
await this.writeToLogFile(auditEvent);
if (this.config.enableSuspiciousActivityDetection) {
await this.detectSuspiciousActivity(auditEvent);
}
const logData = {
eventId,
eventType,
severity,
source,
action,
outcome,
description: description.substring(0, 200)
};
switch (severity) {
case 'critical':
logger.error(logData, 'Critical security event');
break;
case 'high':
logger.warn(logData, 'High severity security event');
break;
case 'medium':
logger.info(logData, 'Medium severity security event');
break;
default:
logger.debug(logData, 'Security event logged');
}
}
catch (error) {
logger.error({ err: error }, 'Failed to log security audit event');
}
}
async initializeAuditSystem() {
try {
await fs.ensureDir(this.config.logDirectory);
await this.initializeLogFile();
await this.cleanupOldLogFiles();
}
catch (error) {
logger.error({ err: error }, 'Failed to initialize audit system');
throw new AppError('Failed to initialize security audit system');
}
}
async initializeLogFile() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
this.currentLogFile = path.join(this.config.logDirectory, `audit-${timestamp}.log`);
this.logFileHandle = fs.createWriteStream(this.currentLogFile, { flags: 'a' });
await this.logSecurityEvent('system_event', 'info', 'audit-logger', 'startup', 'success', 'Security audit system initialized');
}
async writeToLogFile(event) {
if (!this.logFileHandle) {
return;
}
try {
let logData = JSON.stringify(event) + '\n';
if (this.config.encryptLogs && this.config.encryptionKey) {
logData = this.encryptLogData(logData);
}
const stats = await fs.stat(this.currentLogFile);
if (stats.size > this.config.maxLogFileSize) {
await this.rotateLogFile();
}
this.logFileHandle.write(logData);
}
catch (error) {
logger.error({ err: error }, 'Failed to write to audit log file');
}
}
async rotateLogFile() {
if (this.logFileHandle) {
this.logFileHandle.end();
}
await this.initializeLogFile();
}
calculateEventChecksum(event) {
const eventData = {
...event,
integrity: { ...event.integrity, checksum: '' }
};
const eventString = JSON.stringify(eventData);
return crypto.createHash('sha256').update(eventString).digest('hex').substring(0, 8);
}
encryptLogData(data) {
if (!this.config.encryptionKey) {
return data;
}
try {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(this.config.encryptionKey.substring(0, 32)), iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted + '\n';
}
catch (error) {
logger.error({ err: error }, 'Failed to encrypt log data');
return data;
}
}
initializeSuspiciousActivityPatterns() {
const patterns = [
{
id: 'multiple_failed_auth',
name: 'Multiple Failed Authentication Attempts',
description: 'Multiple failed authentication attempts from same source',
pattern: {
eventTypes: ['authentication'],
timeWindow: 300000,
threshold: 5,
conditions: { outcome: 'failure' }
},
severity: 'high',
enabled: true
},
{
id: 'rapid_data_access',
name: 'Rapid Data Access',
description: 'Unusually rapid data access patterns',
pattern: {
eventTypes: ['data_access'],
timeWindow: 60000,
threshold: 50,
conditions: { outcome: 'success' }
},
severity: 'medium',
enabled: true
},
{
id: 'security_violations',
name: 'Multiple Security Violations',
description: 'Multiple security violations in short time',
pattern: {
eventTypes: ['security_violation'],
timeWindow: 600000,
threshold: 3
},
severity: 'critical',
enabled: true
},
{
id: 'privilege_escalation',
name: 'Potential Privilege Escalation',
description: 'Attempts to access unauthorized resources',
pattern: {
eventTypes: ['authorization'],
timeWindow: 300000,
threshold: 10,
conditions: { outcome: 'blocked' }
},
severity: 'high',
enabled: true
}
];
for (const pattern of patterns) {
this.suspiciousPatterns.set(pattern.id, pattern);
}
}
async detectSuspiciousActivity(newEvent) {
const now = Date.now();
for (const pattern of this.suspiciousPatterns.values()) {
if (!pattern.enabled || !pattern.pattern.eventTypes.includes(newEvent.eventType)) {
continue;
}
const recentEvents = this.auditEvents.filter(event => {
const eventTime = event.timestamp.getTime();
const withinTimeWindow = now - eventTime <= pattern.pattern.timeWindow;
const matchesType = pattern.pattern.eventTypes.includes(event.eventType);
let matchesConditions = true;
if (pattern.pattern.conditions) {
for (const [key, value] of Object.entries(pattern.pattern.conditions)) {
if (event[key] !== value) {
matchesConditions = false;
break;
}
}
}
return withinTimeWindow && matchesType && matchesConditions;
});
if (recentEvents.length >= pattern.pattern.threshold) {
await this.logSecurityEvent('suspicious_activity', pattern.severity, 'audit-logger', 'pattern_detection', 'warning', `Suspicious activity detected: ${pattern.description}`, {
metadata: {
patternId: pattern.id,
patternName: pattern.name,
eventCount: recentEvents.length,
threshold: pattern.pattern.threshold,
timeWindow: pattern.pattern.timeWindow,
triggeringEvents: recentEvents.slice(-5).map(e => e.id)
}
});
}
}
}
async generateComplianceReport(startDate, endDate) {
const reportId = `compliance_${Date.now()}`;
const periodEvents = this.auditEvents.filter(event => event.timestamp >= startDate && event.timestamp <= endDate);
const eventsByType = {};
const eventsBySeverity = {};
for (const event of periodEvents) {
eventsByType[event.eventType] = (eventsByType[event.eventType] || 0) + 1;
eventsBySeverity[event.severity] = (eventsBySeverity[event.severity] || 0) + 1;
}
const violations = periodEvents.filter(event => event.eventType === 'security_violation' ||
event.eventType === 'suspicious_activity');
const recommendations = this.generateRecommendations(periodEvents, violations);
const report = {
id: reportId,
generatedAt: new Date(),
period: { start: startDate, end: endDate },
summary: {
totalEvents: periodEvents.length,
eventsByType,
eventsBySeverity,
securityViolations: violations.length,
suspiciousActivities: periodEvents.filter(e => e.eventType === 'suspicious_activity').length
},
violations,
recommendations
};
await this.logSecurityEvent('compliance_event', 'info', 'audit-logger', 'report_generation', 'success', `Compliance report generated for period ${startDate.toISOString()} to ${endDate.toISOString()}`, {
metadata: {
reportId,
totalEvents: periodEvents.length,
violations: violations.length
}
});
return report;
}
generateRecommendations(events, violations) {
const recommendations = [];
const authFailures = events.filter(e => e.eventType === 'authentication' && e.outcome === 'failure').length;
if (authFailures > 100) {
recommendations.push('Consider implementing account lockout policies due to high authentication failure rate');
}
if (violations.length > 10) {
recommendations.push('Review and strengthen security policies due to multiple violations');
}
const suspiciousEvents = events.filter(e => e.eventType === 'suspicious_activity').length;
if (suspiciousEvents > 5) {
recommendations.push('Investigate suspicious activity patterns and consider additional monitoring');
}
const criticalEvents = events.filter(e => e.severity === 'critical').length;
if (criticalEvents > 0) {
recommendations.push('Address all critical security events immediately');
}
return recommendations;
}
async cleanupOldLogFiles() {
try {
const files = await fs.readdir(this.config.logDirectory);
const logFiles = files
.filter(file => file.startsWith('audit-') && file.endsWith('.log'))
.map(file => ({
name: file,
path: path.join(this.config.logDirectory, file),
stats: fs.statSync(path.join(this.config.logDirectory, file))
}))
.sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime());
if (logFiles.length > this.config.maxLogFiles) {
const filesToRemove = logFiles.slice(this.config.maxLogFiles);
for (const file of filesToRemove) {
await fs.remove(file.path);
logger.debug({ file: file.name }, 'Removed old audit log file');
}
}
const retentionCutoff = Date.now() - (this.config.retentionPeriodDays * 24 * 60 * 60 * 1000);
for (const file of logFiles) {
if (file.stats.mtime.getTime() < retentionCutoff) {
await fs.remove(file.path);
logger.debug({ file: file.name }, 'Removed expired audit log file');
}
}
}
catch (error) {
logger.warn({ err: error }, 'Failed to cleanup old audit log files');
}
}
getAuditStatistics() {
const eventsByType = {};
const eventsBySeverity = {};
for (const event of this.auditEvents) {
eventsByType[event.eventType] = (eventsByType[event.eventType] || 0) + 1;
eventsBySeverity[event.severity] = (eventsBySeverity[event.severity] || 0) + 1;
}
const recentViolations = this.auditEvents
.filter(event => event.eventType === 'security_violation' ||
event.eventType === 'suspicious_activity')
.slice(-20);
return {
totalEvents: this.auditEvents.length,
eventsByType,
eventsBySeverity,
recentViolations,
suspiciousPatterns: this.suspiciousPatterns.size
};
}
async shutdown() {
await this.logSecurityEvent('system_event', 'info', 'audit-logger', 'shutdown', 'success', 'Security audit system shutdown');
if (this.logFileHandle) {
this.logFileHandle.end();
}
this.auditEvents = [];
this.suspiciousPatterns.clear();
logger.info('Security Audit Logger shutdown');
}
}
export async function logSecurityEvent(eventType, severity, source, action, outcome, description, options) {
const auditLogger = SecurityAuditLogger.getInstance();
return auditLogger.logSecurityEvent(eventType, severity, source, action, outcome, description, options);
}
export function getSecurityAuditLogger() {
return SecurityAuditLogger.getInstance();
}