diff --git a/middleware/auditLogger.js b/middleware/auditLogger.js
new file mode 100644
index 00000000..7399d99d
--- /dev/null
+++ b/middleware/auditLogger.js
@@ -0,0 +1,225 @@
+const AuditLog = require('../models/AuditLog');
+const auditHasher = require('../utils/auditHasher');
+
+/**
+ * Audit Logger Middleware
+ * Automatically captures audit events for all requests
+ */
+class AuditLogger {
+ /**
+ * Main middleware function
+ */
+ middleware() {
+ return async (req, res, next) => {
+ // Skip audit for certain routes
+ if (this.shouldSkipAudit(req.path)) {
+ return next();
+ }
+
+ // Capture original methods
+ const originalJson = res.json;
+ const originalSend = res.send;
+
+ // Store request start time
+ req.auditStartTime = Date.now();
+
+ // Override response methods to capture audit data
+ res.json = function (data) {
+ res.locals.responseData = data;
+ return originalJson.call(this, data);
+ };
+
+ res.send = function (data) {
+ res.locals.responseData = data;
+ return originalSend.call(this, data);
+ };
+
+ // Capture response
+ res.on('finish', async () => {
+ try {
+ await this.logAudit(req, res);
+ } catch (err) {
+ console.error('[AuditLogger] Failed to log audit:', err);
+ }
+ });
+
+ next();
+ };
+ }
+
+ /**
+ * Log audit event
+ */
+ async logAudit(req, res) {
+ if (!req.user) return; // Skip if no authenticated user
+
+ const action = this.determineAction(req.method, req.path);
+ const entityInfo = this.extractEntityInfo(req);
+
+ const logData = {
+ logId: `AL-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
+ timestamp: new Date(),
+ userId: req.user._id,
+ userName: req.user.name,
+ userEmail: req.user.email,
+ action,
+ entityType: entityInfo.type,
+ entityId: entityInfo.id,
+ entityName: entityInfo.name,
+ changes: this.extractChanges(req, res),
+ metadata: {
+ ipAddress: req.ip || req.connection.remoteAddress,
+ userAgent: req.get('user-agent'),
+ requestId: req.id,
+ sessionId: req.sessionID,
+ apiEndpoint: req.path,
+ httpMethod: req.method,
+ statusCode: res.statusCode
+ },
+ severity: this.determineSeverity(action, res.statusCode),
+ category: this.determineCategory(action, entityInfo.type),
+ tags: this.generateTags(req, entityInfo)
+ };
+
+ // Get previous hash for chain
+ const previousLog = await AuditLog.findOne().sort({ timestamp: -1 }).limit(1);
+ const previousHash = previousLog ? previousLog.hash : '';
+
+ // Generate hash
+ logData.hash = auditHasher.generateHash(logData, previousHash);
+ logData.previousHash = previousHash;
+
+ // Save audit log
+ const auditLog = new AuditLog(logData);
+ await auditLog.save();
+ }
+
+ /**
+ * Determine action from request
+ */
+ determineAction(method, path) {
+ if (path.includes('/login')) return 'login';
+ if (path.includes('/logout')) return 'logout';
+ if (path.includes('/export')) return 'export';
+ if (path.includes('/import')) return 'import';
+ if (path.includes('/approve')) return 'approve';
+ if (path.includes('/reject')) return 'reject';
+
+ switch (method) {
+ case 'POST': return 'create';
+ case 'GET': return 'read';
+ case 'PUT':
+ case 'PATCH': return 'update';
+ case 'DELETE': return 'delete';
+ default: return 'read';
+ }
+ }
+
+ /**
+ * Extract entity information from request
+ */
+ extractEntityInfo(req) {
+ const pathParts = req.path.split('/').filter(p => p);
+
+ let type = 'Unknown';
+ let id = null;
+ let name = null;
+
+ if (pathParts.length >= 2) {
+ type = pathParts[1]; // e.g., /api/expenses -> expenses
+
+ if (pathParts.length >= 3 && pathParts[2].match(/^[0-9a-fA-F]{24}$/)) {
+ id = pathParts[2];
+ }
+ }
+
+ // Try to get name from request body or response
+ if (req.body && req.body.name) {
+ name = req.body.name;
+ } else if (req.body && req.body.description) {
+ name = req.body.description;
+ }
+
+ return { type, id, name };
+ }
+
+ /**
+ * Extract changes from request/response
+ */
+ extractChanges(req, res) {
+ const changes = {
+ before: null,
+ after: null,
+ fields: []
+ };
+
+ if (req.method === 'PUT' || req.method === 'PATCH') {
+ changes.before = req.originalData || null;
+ changes.after = req.body;
+ changes.fields = Object.keys(req.body || {});
+ } else if (req.method === 'POST') {
+ changes.after = req.body;
+ changes.fields = Object.keys(req.body || {});
+ } else if (req.method === 'DELETE') {
+ changes.before = req.originalData || null;
+ }
+
+ return changes;
+ }
+
+ /**
+ * Determine severity
+ */
+ determineSeverity(action, statusCode) {
+ if (statusCode >= 500) return 'critical';
+ if (statusCode >= 400) return 'high';
+ if (action === 'delete' || action === 'approve') return 'high';
+ if (action === 'update') return 'medium';
+ return 'low';
+ }
+
+ /**
+ * Determine category
+ */
+ determineCategory(action, entityType) {
+ if (action === 'login' || action === 'logout') return 'security';
+ if (entityType.includes('user') || entityType.includes('auth')) return 'security';
+ if (action === 'delete' || action === 'update') return 'data';
+ if (action === 'export' || action === 'import') return 'compliance';
+ return 'user_action';
+ }
+
+ /**
+ * Generate tags
+ */
+ generateTags(req, entityInfo) {
+ const tags = [];
+
+ tags.push(req.method.toLowerCase());
+ tags.push(entityInfo.type);
+
+ if (req.path.includes('/api/')) {
+ tags.push('api');
+ }
+
+ return tags;
+ }
+
+ /**
+ * Check if audit should be skipped
+ */
+ shouldSkipAudit(path) {
+ const skipPaths = [
+ '/health',
+ '/ping',
+ '/metrics',
+ '/favicon.ico',
+ '/static/',
+ '/public/'
+ ];
+
+ return skipPaths.some(skip => path.includes(skip));
+ }
+}
+
+module.exports = new AuditLogger();
diff --git a/models/AuditLog.js b/models/AuditLog.js
index 78ed44ba..7154f4e0 100644
--- a/models/AuditLog.js
+++ b/models/AuditLog.js
@@ -1,233 +1,115 @@
const mongoose = require('mongoose');
/**
- * Enterprise-Grade Audit Trail Model
- * Tracks all write operations and security events
- * Issue #338: Audit Trail & TOTP Security Suite
+ * AuditLog Model
+ * Immutable audit trail records with cryptographic integrity verification
*/
-
const auditLogSchema = new mongoose.Schema({
- workspaceId: {
- type: mongoose.Schema.Types.ObjectId,
- ref: 'Workspace'
+ logId: {
+ type: String,
+ unique: true,
+ required: true,
+ index: true
+ },
+ timestamp: {
+ type: Date,
+ required: true,
+ default: Date.now,
+ index: true
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
- required: true
+ required: true,
+ index: true
},
+ userName: String,
+ userEmail: String,
action: {
type: String,
required: true,
- enum: [
- // Expense operations
- 'expense_created', 'expense_updated', 'expense_deleted', 'expense_approved', 'expense_rejected',
- // Budget operations
- 'budget_created', 'budget_updated', 'budget_deleted',
- // Member operations
- 'member_added', 'member_removed', 'member_role_changed',
- // Workspace operations
- 'workspace_created', 'workspace_updated', 'workspace_deleted',
- // Approval operations
- 'approval_submitted', 'approval_processed', 'approval_delegated',
- // Report operations
- 'report_generated', 'data_exported', 'settings_changed',
- // Authentication & Security operations (Issue #338)
- 'user_login', 'user_logout', 'user_register',
- 'login_failed', 'login_blocked', 'password_changed', 'password_reset_requested', 'password_reset_completed',
- // 2FA operations
- 'totp_enabled', 'totp_disabled', 'totp_verified', 'totp_failed', 'totp_backup_used',
- 'backup_codes_generated', 'backup_codes_regenerated',
- // Session operations
- 'session_created', 'session_revoked', 'session_expired', 'all_sessions_revoked',
- // Security events
- 'suspicious_activity', 'ip_blocked', 'rate_limit_exceeded',
- 'account_locked', 'account_unlocked',
- // Data operations
- 'profile_updated', 'email_changed', 'api_key_created', 'api_key_revoked'
- ]
+ enum: ['create', 'read', 'update', 'delete', 'login', 'logout', 'export', 'import', 'approve', 'reject'],
+ index: true
},
entityType: {
type: String,
required: true,
- enum: ['expense', 'budget', 'workspace', 'user', 'approval', 'report', 'session', 'security', 'authentication']
+ index: true
},
entityId: {
type: mongoose.Schema.Types.ObjectId,
- required: false // Not required for security events
+ index: true
},
+ entityName: String,
changes: {
before: mongoose.Schema.Types.Mixed,
after: mongoose.Schema.Types.Mixed,
- fields: [String] // List of modified fields
+ fields: [String]
},
metadata: {
ipAddress: String,
userAgent: String,
+ requestId: String,
sessionId: String,
apiEndpoint: String,
- requestId: String,
- // Enhanced security metadata
- geoLocation: {
- country: String,
- city: String,
- region: String,
- timezone: String
- },
- device: {
- type: String,
- os: String,
- browser: String,
- isMobile: Boolean
- },
- riskScore: {
- type: Number,
- min: 0,
- max: 100,
- default: 0
- }
+ httpMethod: String,
+ statusCode: Number
},
severity: {
type: String,
enum: ['low', 'medium', 'high', 'critical'],
- default: 'medium'
+ default: 'medium',
+ index: true
},
- status: {
+ category: {
type: String,
- enum: ['success', 'failure', 'pending', 'blocked'],
- default: 'success'
+ enum: ['security', 'data', 'system', 'compliance', 'user_action'],
+ default: 'user_action',
+ index: true
},
tags: [String],
- // Additional context for security events
- securityContext: {
- totpUsed: Boolean,
- newDevice: Boolean,
- newLocation: Boolean,
- failedAttempts: Number,
- riskFactors: [String]
- },
- // For compliance and retention
- retentionPolicy: {
+ hash: {
type: String,
- enum: ['standard', 'extended', 'permanent'],
- default: 'standard'
+ required: true
},
- expiresAt: {
- type: Date,
- default: function() {
- // Default 2 years retention for standard logs
- return new Date(Date.now() + 2 * 365 * 24 * 60 * 60 * 1000);
- }
- }
+ previousHash: String,
+ isCompressed: {
+ type: Boolean,
+ default: false
+ },
+ isArchived: {
+ type: Boolean,
+ default: false
+ },
+ retentionDate: Date
}, {
- timestamps: true
+ timestamps: false,
+ strict: true
});
-// Indexes for efficient querying
-auditLogSchema.index({ workspaceId: 1, createdAt: -1 });
-auditLogSchema.index({ userId: 1, createdAt: -1 });
-auditLogSchema.index({ action: 1, createdAt: -1 });
-auditLogSchema.index({ entityType: 1, entityId: 1 });
-auditLogSchema.index({ severity: 1, createdAt: -1 });
-auditLogSchema.index({ status: 1, createdAt: -1 });
-auditLogSchema.index({ 'metadata.ipAddress': 1, createdAt: -1 });
-auditLogSchema.index({ 'metadata.sessionId': 1 });
-auditLogSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); // TTL index
-
-// Static methods for common audit operations
-auditLogSchema.statics.logSecurityEvent = async function(userId, action, metadata = {}, options = {}) {
- return this.create({
- userId,
- action,
- entityType: 'security',
- entityId: userId,
- metadata,
- severity: options.severity || 'medium',
- status: options.status || 'success',
- securityContext: options.securityContext || {},
- tags: options.tags || ['security']
- });
-};
-
-auditLogSchema.statics.logAuthEvent = async function(userId, action, req, options = {}) {
- const metadata = {
- ipAddress: req.ip || req.connection?.remoteAddress,
- userAgent: req.headers?.['user-agent'],
- apiEndpoint: req.originalUrl,
- sessionId: req.sessionId
- };
-
- return this.create({
- userId,
- action,
- entityType: 'authentication',
- entityId: userId,
- metadata,
- severity: options.severity || 'medium',
- status: options.status || 'success',
- securityContext: options.securityContext || {},
- tags: ['authentication', ...(options.tags || [])]
- });
-};
-
-auditLogSchema.statics.logWriteOperation = async function(userId, action, entityType, entityId, changes, req, options = {}) {
- const metadata = {
- ipAddress: req?.ip || req?.connection?.remoteAddress,
- userAgent: req?.headers?.['user-agent'],
- apiEndpoint: req?.originalUrl,
- sessionId: req?.sessionId
- };
-
- return this.create({
- userId,
- action,
- entityType,
- entityId,
- changes,
- metadata,
- severity: options.severity || 'low',
- status: options.status || 'success',
- tags: options.tags || []
- });
-};
-
-// Instance method to get human-readable description
-auditLogSchema.methods.getDescription = function() {
- const actionDescriptions = {
- 'user_login': 'User logged in',
- 'user_logout': 'User logged out',
- 'login_failed': 'Failed login attempt',
- 'totp_enabled': 'Two-factor authentication enabled',
- 'totp_disabled': 'Two-factor authentication disabled',
- 'totp_verified': 'Two-factor authentication verified',
- 'totp_failed': 'Two-factor authentication failed',
- 'session_revoked': 'Session was revoked',
- 'all_sessions_revoked': 'All sessions were revoked',
- 'password_changed': 'Password was changed',
- 'suspicious_activity': 'Suspicious activity detected'
- };
-
- return actionDescriptions[this.action] || this.action.replace(/_/g, ' ');
-};
+// Prevent modifications after creation (immutable)
+auditLogSchema.pre('save', function (next) {
+ if (!this.isNew) {
+ return next(new Error('Audit logs are immutable and cannot be modified'));
+ }
+ next();
+});
-// Query helper for security timeline
-auditLogSchema.query.securityTimeline = function(userId, days = 30) {
- const startDate = new Date();
- startDate.setDate(startDate.getDate() - days);
+// Prevent updates
+auditLogSchema.pre('findOneAndUpdate', function (next) {
+ next(new Error('Audit logs cannot be updated'));
+});
- return this.find({
- userId,
- entityType: { $in: ['security', 'authentication'] },
- createdAt: { $gte: startDate }
- }).sort({ createdAt: -1 });
-};
+// Prevent deletions (only archival allowed)
+auditLogSchema.pre('remove', function (next) {
+ next(new Error('Audit logs cannot be deleted'));
+});
-// Query helper for login history
-auditLogSchema.query.loginHistory = function(userId, limit = 50) {
- return this.find({
- userId,
- action: { $in: ['user_login', 'login_failed', 'totp_verified', 'totp_failed'] }
- }).sort({ createdAt: -1 }).limit(limit);
-};
+// Indexes for efficient querying
+auditLogSchema.index({ userId: 1, timestamp: -1 });
+auditLogSchema.index({ entityType: 1, entityId: 1, timestamp: -1 });
+auditLogSchema.index({ action: 1, timestamp: -1 });
+auditLogSchema.index({ category: 1, severity: 1, timestamp: -1 });
+auditLogSchema.index({ tags: 1 });
module.exports = mongoose.model('AuditLog', auditLogSchema);
\ No newline at end of file
diff --git a/models/ComplianceReport.js b/models/ComplianceReport.js
new file mode 100644
index 00000000..aef04d17
--- /dev/null
+++ b/models/ComplianceReport.js
@@ -0,0 +1,100 @@
+const mongoose = require('mongoose');
+
+/**
+ * ComplianceReport Model
+ * Stores generated compliance reports for regulatory requirements
+ */
+const complianceReportSchema = new mongoose.Schema({
+ reportId: {
+ type: String,
+ unique: true,
+ required: true
+ },
+ reportType: {
+ type: String,
+ required: true,
+ enum: ['SOX', 'GDPR', 'HIPAA', 'PCI_DSS', 'ISO_27001', 'CUSTOM']
+ },
+ generatedBy: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true
+ },
+ generatedAt: {
+ type: Date,
+ required: true,
+ default: Date.now
+ },
+ period: {
+ startDate: {
+ type: Date,
+ required: true
+ },
+ endDate: {
+ type: Date,
+ required: true
+ }
+ },
+ filters: {
+ users: [mongoose.Schema.Types.ObjectId],
+ actions: [String],
+ entityTypes: [String],
+ severity: [String],
+ categories: [String]
+ },
+ summary: {
+ totalLogs: {
+ type: Number,
+ default: 0
+ },
+ criticalEvents: {
+ type: Number,
+ default: 0
+ },
+ securityEvents: {
+ type: Number,
+ default: 0
+ },
+ dataModifications: {
+ type: Number,
+ default: 0
+ },
+ uniqueUsers: {
+ type: Number,
+ default: 0
+ },
+ failedAttempts: {
+ type: Number,
+ default: 0
+ }
+ },
+ exportFormats: [{
+ format: {
+ type: String,
+ enum: ['CSV', 'PDF', 'EXCEL', 'JSON']
+ },
+ filePath: String,
+ fileSize: Number,
+ generatedAt: Date
+ }],
+ integrityHash: String,
+ status: {
+ type: String,
+ enum: ['generating', 'completed', 'failed', 'archived'],
+ default: 'generating'
+ },
+ metadata: {
+ totalPages: Number,
+ recordCount: Number,
+ compressionRatio: Number
+ }
+}, {
+ timestamps: true
+});
+
+// Indexes
+complianceReportSchema.index({ generatedBy: 1, generatedAt: -1 });
+complianceReportSchema.index({ reportType: 1, status: 1 });
+complianceReportSchema.index({ 'period.startDate': 1, 'period.endDate': 1 });
+
+module.exports = mongoose.model('ComplianceReport', complianceReportSchema);
diff --git a/public/audit-trail-viewer.html b/public/audit-trail-viewer.html
new file mode 100644
index 00000000..587ab0d7
--- /dev/null
+++ b/public/audit-trail-viewer.html
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+ Audit Trail Viewer - ExpenseFlow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
+
Verified
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Timestamp |
+ User |
+ Action |
+ Entity |
+ Severity |
+ Category |
+ IP Address |
+ Details |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/js/audit-controller.js b/public/js/audit-controller.js
new file mode 100644
index 00000000..54ba65ad
--- /dev/null
+++ b/public/js/audit-controller.js
@@ -0,0 +1,445 @@
+/**
+ * Audit Controller
+ * Handles all audit trail UI logic
+ */
+
+let actionChart = null;
+let severityChart = null;
+let currentPage = 1;
+let currentFilters = {};
+let searchTimeout = null;
+
+document.addEventListener('DOMContentLoaded', () => {
+ loadDashboard();
+ loadAuditLogs();
+ loadStatistics();
+ loadCriticalEvents();
+});
+
+async function loadDashboard() {
+ try {
+ const res = await fetch('/api/audit-trail/dashboard', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ updateDashboardStats(data);
+ } catch (err) {
+ console.error('Failed to load dashboard:', err);
+ }
+}
+
+function updateDashboardStats(data) {
+ document.getElementById('total-logs-24h').textContent = data.summary.total24h.toLocaleString();
+ document.getElementById('critical-events').textContent = data.summary.critical24h.toLocaleString();
+ document.getElementById('total-logs-7d').textContent = data.summary.total7d.toLocaleString();
+}
+
+async function loadAuditLogs() {
+ try {
+ const res = await fetch('/api/audit-trail/query', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({
+ filters: currentFilters,
+ options: {
+ page: currentPage,
+ limit: 50
+ }
+ })
+ });
+ const { data } = await res.json();
+
+ renderAuditLogs(data.logs);
+ updatePagination(data.pagination);
+ } catch (err) {
+ console.error('Failed to load audit logs:', err);
+ }
+}
+
+function renderAuditLogs(logs) {
+ const tbody = document.getElementById('audit-logs-tbody');
+
+ if (!logs || logs.length === 0) {
+ tbody.innerHTML = '| No audit logs found. |
';
+ return;
+ }
+
+ tbody.innerHTML = logs.map(log => `
+
+ | ${new Date(log.timestamp).toLocaleString()} |
+ ${log.userName || 'Unknown'} |
+ ${log.action} |
+ ${log.entityType}${log.entityId ? ` (${log.entityId.substring(0, 8)}...)` : ''} |
+ ${log.severity} |
+ ${log.category} |
+ ${log.metadata?.ipAddress || 'N/A'} |
+
+
+ |
+
+ `).join('');
+}
+
+function updatePagination(pagination) {
+ document.getElementById('page-info').textContent =
+ `Page ${pagination.page} of ${pagination.pages}`;
+}
+
+function previousPage() {
+ if (currentPage > 1) {
+ currentPage--;
+ loadAuditLogs();
+ }
+}
+
+function nextPage() {
+ currentPage++;
+ loadAuditLogs();
+}
+
+async function loadStatistics() {
+ try {
+ const res = await fetch('/api/audit-trail/statistics', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ renderActionChart(data.byAction);
+ renderSeverityChart(data.bySeverity);
+ } catch (err) {
+ console.error('Failed to load statistics:', err);
+ }
+}
+
+function renderActionChart(actionStats) {
+ const ctx = document.getElementById('actionChart').getContext('2d');
+
+ if (actionChart) {
+ actionChart.destroy();
+ }
+
+ actionChart = new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: actionStats.map(s => s._id),
+ datasets: [{
+ label: 'Count',
+ data: actionStats.map(s => s.count),
+ backgroundColor: '#48dbfb',
+ borderWidth: 0
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false }
+ },
+ scales: {
+ x: {
+ ticks: { color: '#8892b0' },
+ grid: { color: 'rgba(255,255,255,0.05)' }
+ },
+ y: {
+ ticks: { color: '#8892b0' },
+ grid: { color: 'rgba(255,255,255,0.05)' }
+ }
+ }
+ }
+ });
+}
+
+function renderSeverityChart(severityStats) {
+ const ctx = document.getElementById('severityChart').getContext('2d');
+
+ if (severityChart) {
+ severityChart.destroy();
+ }
+
+ const colors = {
+ 'low': '#64ffda',
+ 'medium': '#48dbfb',
+ 'high': '#ff9f43',
+ 'critical': '#ff6b6b'
+ };
+
+ severityChart = new Chart(ctx, {
+ type: 'doughnut',
+ data: {
+ labels: severityStats.map(s => s._id),
+ datasets: [{
+ data: severityStats.map(s => s.count),
+ backgroundColor: severityStats.map(s => colors[s._id] || '#8892b0'),
+ borderWidth: 0
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: { color: '#8892b0', font: { size: 10 } }
+ }
+ }
+ }
+ });
+}
+
+async function loadCriticalEvents() {
+ try {
+ const res = await fetch('/api/audit-trail/critical?days=7&limit=10', {
+ headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
+ });
+ const { data } = await res.json();
+
+ renderCriticalEvents(data);
+ } catch (err) {
+ console.error('Failed to load critical events:', err);
+ }
+}
+
+function renderCriticalEvents(events) {
+ const container = document.getElementById('critical-events-list');
+
+ if (!events || events.length === 0) {
+ container.innerHTML = 'No critical events in the last 7 days.
';
+ return;
+ }
+
+ container.innerHTML = events.map(event => `
+
+
+
+ ${event.userName || 'Unknown User'} performed
+ ${event.action} on
+ ${event.entityType}
+
+
+ ${event.metadata?.ipAddress || 'N/A'}
+ ${event.category}
+
+
+ `).join('');
+}
+
+function applyFilters() {
+ const filters = {};
+
+ const actions = Array.from(document.getElementById('action-filter').selectedOptions).map(o => o.value);
+ const severities = Array.from(document.getElementById('severity-filter').selectedOptions).map(o => o.value);
+ const categories = Array.from(document.getElementById('category-filter').selectedOptions).map(o => o.value);
+ const startDate = document.getElementById('start-date').value;
+ const endDate = document.getElementById('end-date').value;
+
+ if (actions.length > 0) filters.action = actions;
+ if (severities.length > 0) filters.severity = severities;
+ if (categories.length > 0) filters.category = categories;
+ if (startDate) filters.startDate = startDate;
+ if (endDate) filters.endDate = endDate;
+
+ currentFilters = filters;
+ currentPage = 1;
+ loadAuditLogs();
+}
+
+function resetFilters() {
+ document.getElementById('action-filter').selectedIndex = -1;
+ document.getElementById('severity-filter').selectedIndex = -1;
+ document.getElementById('category-filter').selectedIndex = -1;
+ document.getElementById('start-date').value = '';
+ document.getElementById('end-date').value = '';
+ document.getElementById('search-input').value = '';
+
+ currentFilters = {};
+ currentPage = 1;
+ loadAuditLogs();
+}
+
+function handleSearch() {
+ clearTimeout(searchTimeout);
+
+ const searchTerm = document.getElementById('search-input').value;
+
+ searchTimeout = setTimeout(() => {
+ if (searchTerm.length >= 3) {
+ currentFilters.searchTerm = searchTerm;
+ } else {
+ delete currentFilters.searchTerm;
+ }
+ currentPage = 1;
+ loadAuditLogs();
+ }, 500);
+}
+
+async function verifyIntegrity() {
+ try {
+ const startDate = document.getElementById('start-date').value;
+ const endDate = document.getElementById('end-date').value;
+
+ const res = await fetch('/api/audit-trail/verify', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({ startDate, endDate, limit: 1000 })
+ });
+ const { data } = await res.json();
+
+ if (data.valid) {
+ alert(`✓ Integrity Verified\n\nTotal logs checked: ${data.totalLogs}\nNo tampering detected.`);
+ document.getElementById('integrity-status').textContent = 'Verified';
+ document.getElementById('integrity-status').style.color = '#64ffda';
+ } else {
+ alert(`✗ Integrity Compromised\n\nErrors found: ${data.errors.length}\n\n${data.errors.map(e => e.message).join('\n')}`);
+ document.getElementById('integrity-status').textContent = 'Compromised';
+ document.getElementById('integrity-status').style.color = '#ff6b6b';
+ }
+ } catch (err) {
+ console.error('Failed to verify integrity:', err);
+ alert('Failed to verify integrity');
+ }
+}
+
+function showLogDetails(log) {
+ const modal = document.getElementById('log-details-modal');
+ const content = document.getElementById('log-details-content');
+
+ content.innerHTML = `
+
+
Basic Information
+
+
+
+ ${log.logId}
+
+
+
+ ${new Date(log.timestamp).toLocaleString()}
+
+
+
+ ${log.userName} (${log.userEmail})
+
+
+
+ ${log.action}
+
+
+
+
+
Entity Information
+
+
+
+ ${log.entityType}
+
+
+
+ ${log.entityId || 'N/A'}
+
+
+
+ ${log.entityName || 'N/A'}
+
+
+
+
+
Metadata
+
+
+
+ ${log.metadata?.ipAddress || 'N/A'}
+
+
+
+ ${log.metadata?.userAgent || 'N/A'}
+
+
+
+ ${log.metadata?.statusCode || 'N/A'}
+
+
+
+ ${log.metadata?.apiEndpoint || 'N/A'}
+
+
+
+ ${log.changes && (log.changes.before || log.changes.after) ? `
+
+
Changes
+
${JSON.stringify(log.changes, null, 2)}
+
+ ` : ''}
+
+
Cryptographic Hash
+
+ ${log.hash}
+
+
+ `;
+
+ modal.classList.remove('hidden');
+}
+
+function closeLogDetailsModal() {
+ document.getElementById('log-details-modal').classList.add('hidden');
+}
+
+function openComplianceModal() {
+ document.getElementById('compliance-modal').classList.remove('hidden');
+}
+
+function closeComplianceModal() {
+ document.getElementById('compliance-modal').classList.add('hidden');
+}
+
+document.getElementById('compliance-form').addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const reportType = document.getElementById('report-type').value;
+ const startDate = document.getElementById('report-start-date').value;
+ const endDate = document.getElementById('report-end-date').value;
+
+ const formatCheckboxes = document.querySelectorAll('input[name="format"]:checked');
+ const exportFormats = Array.from(formatCheckboxes).map(cb => cb.value);
+
+ if (exportFormats.length === 0) {
+ alert('Please select at least one export format');
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/audit-trail/compliance/generate', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({
+ reportType,
+ startDate,
+ endDate,
+ exportFormats
+ })
+ });
+
+ const { data } = await res.json();
+
+ alert(`Report generated successfully!\n\nReport ID: ${data.reportId}\nTotal Logs: ${data.summary.totalLogs}\nCritical Events: ${data.summary.criticalEvents}`);
+ closeComplianceModal();
+ } catch (err) {
+ console.error('Failed to generate report:', err);
+ alert('Failed to generate compliance report');
+ }
+});
diff --git a/routes/audit-trail.js b/routes/audit-trail.js
new file mode 100644
index 00000000..f2d868e7
--- /dev/null
+++ b/routes/audit-trail.js
@@ -0,0 +1,200 @@
+const express = require('express');
+const router = express.Router();
+const auth = require('../middleware/auth');
+const auditTrailService = require('../services/auditTrailService');
+const complianceExportService = require('../services/complianceExportService');
+
+/**
+ * Get Audit Dashboard
+ */
+router.get('/dashboard', auth, async (req, res) => {
+ try {
+ const dashboard = await auditTrailService.getDashboard();
+ res.json({ success: true, data: dashboard });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Query Audit Logs
+ */
+router.post('/query', auth, async (req, res) => {
+ try {
+ const { filters, options } = req.body;
+ const result = await auditTrailService.queryLogs(filters, options);
+ res.json({ success: true, data: result });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Audit Statistics
+ */
+router.get('/statistics', auth, async (req, res) => {
+ try {
+ const { startDate, endDate, userId } = req.query;
+ const stats = await auditTrailService.getStatistics({ startDate, endDate, userId });
+ res.json({ success: true, data: stats });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Verify Integrity
+ */
+router.post('/verify', auth, async (req, res) => {
+ try {
+ const { startDate, endDate, limit } = req.body;
+ const verification = await auditTrailService.verifyIntegrity({ startDate, endDate, limit });
+ res.json({ success: true, data: verification });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get User Timeline
+ */
+router.get('/timeline/:userId', auth, async (req, res) => {
+ try {
+ const { days, limit } = req.query;
+ const timeline = await auditTrailService.getUserTimeline(req.params.userId, { days, limit });
+ res.json({ success: true, data: timeline });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Entity History
+ */
+router.get('/entity/:entityType/:entityId', auth, async (req, res) => {
+ try {
+ const { entityType, entityId } = req.params;
+ const history = await auditTrailService.getEntityHistory(entityType, entityId);
+ res.json({ success: true, data: history });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Search Logs
+ */
+router.get('/search', auth, async (req, res) => {
+ try {
+ const { q, limit } = req.query;
+ const results = await auditTrailService.searchLogs(q, { limit });
+ res.json({ success: true, data: results });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Critical Events
+ */
+router.get('/critical', auth, async (req, res) => {
+ try {
+ const { days, limit } = req.query;
+ const events = await auditTrailService.getCriticalEvents({ days, limit });
+ res.json({ success: true, data: events });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Generate Compliance Report
+ */
+router.post('/compliance/generate', auth, async (req, res) => {
+ try {
+ const { reportType, startDate, endDate, filters, exportFormats } = req.body;
+
+ const report = await complianceExportService.generateReport(req.user._id, {
+ reportType,
+ startDate,
+ endDate,
+ filters,
+ exportFormats
+ });
+
+ res.json({ success: true, data: report });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Compliance Templates
+ */
+router.get('/compliance/templates', auth, async (req, res) => {
+ try {
+ const templates = complianceExportService.getComplianceTemplates();
+ res.json({ success: true, data: templates });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * List Compliance Reports
+ */
+router.get('/compliance/reports', auth, async (req, res) => {
+ try {
+ const { page, limit } = req.query;
+ const result = await complianceExportService.listReports(req.user._id, { page, limit });
+ res.json({ success: true, data: result });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Get Specific Report
+ */
+router.get('/compliance/reports/:reportId', auth, async (req, res) => {
+ try {
+ const report = await complianceExportService.getReport(req.params.reportId);
+
+ if (!report) {
+ return res.status(404).json({ success: false, error: 'Report not found' });
+ }
+
+ res.json({ success: true, data: report });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Download Report
+ */
+router.get('/compliance/download/:reportId/:format', auth, async (req, res) => {
+ try {
+ const { reportId, format } = req.params;
+ const { filePath, fileName } = await complianceExportService.downloadReport(reportId, format);
+
+ res.download(filePath, fileName);
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+/**
+ * Archive Old Logs
+ */
+router.post('/archive', auth, async (req, res) => {
+ try {
+ const { daysOld } = req.body;
+ const result = await auditTrailService.archiveLogs(daysOld);
+ res.json({ success: true, data: result });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server.js b/server.js
index 1e15952e..a25523b3 100644
--- a/server.js
+++ b/server.js
@@ -279,8 +279,7 @@ app.use('/api/expenses', expenseRoutes); // Expense management
app.use('/api/currency', require('./routes/currency'));
app.use('/api/payroll', require('./routes/payroll'));
app.use('/api/inventory', require('./routes/inventory'));
-app.use('/api/fx-revaluation', require('./routes/fx-revaluation'));
-app.use('/api/budget-analytics', require('./routes/budget-analytics'));
+app.use('/api/audit-trail', require('./routes/audit-trail'));
app.use('/api/splits', require('./routes/splits'));
app.use('/api/workspaces', require('./routes/workspaces'));
app.use('/api/tax', require('./routes/tax'));
diff --git a/services/auditTrailService.js b/services/auditTrailService.js
new file mode 100644
index 00000000..73bc158c
--- /dev/null
+++ b/services/auditTrailService.js
@@ -0,0 +1,354 @@
+const AuditLog = require('../models/AuditLog');
+const auditHasher = require('../utils/auditHasher');
+
+class AuditTrailService {
+ /**
+ * Query audit logs with advanced filtering
+ */
+ async queryLogs(filters = {}, options = {}) {
+ const {
+ userId,
+ action,
+ entityType,
+ entityId,
+ startDate,
+ endDate,
+ severity,
+ category,
+ tags,
+ searchTerm
+ } = filters;
+
+ const {
+ page = 1,
+ limit = 50,
+ sortBy = 'timestamp',
+ sortOrder = 'desc'
+ } = options;
+
+ // Build query
+ const query = {};
+
+ if (userId) query.userId = userId;
+ if (action) query.action = Array.isArray(action) ? { $in: action } : action;
+ if (entityType) query.entityType = Array.isArray(entityType) ? { $in: entityType } : entityType;
+ if (entityId) query.entityId = entityId;
+ if (severity) query.severity = Array.isArray(severity) ? { $in: severity } : severity;
+ if (category) query.category = Array.isArray(category) ? { $in: category } : category;
+ if (tags && tags.length > 0) query.tags = { $in: tags };
+
+ if (startDate || endDate) {
+ query.timestamp = {};
+ if (startDate) query.timestamp.$gte = new Date(startDate);
+ if (endDate) query.timestamp.$lte = new Date(endDate);
+ }
+
+ if (searchTerm) {
+ query.$or = [
+ { userName: { $regex: searchTerm, $options: 'i' } },
+ { userEmail: { $regex: searchTerm, $options: 'i' } },
+ { entityName: { $regex: searchTerm, $options: 'i' } },
+ { 'metadata.ipAddress': { $regex: searchTerm, $options: 'i' } }
+ ];
+ }
+
+ // Execute query
+ const skip = (page - 1) * limit;
+ const sort = { [sortBy]: sortOrder === 'desc' ? -1 : 1 };
+
+ const [logs, total] = await Promise.all([
+ AuditLog.find(query)
+ .sort(sort)
+ .skip(skip)
+ .limit(limit)
+ .populate('userId', 'name email')
+ .lean(),
+ AuditLog.countDocuments(query)
+ ]);
+
+ return {
+ logs,
+ pagination: {
+ page,
+ limit,
+ total,
+ pages: Math.ceil(total / limit)
+ }
+ };
+ }
+
+ /**
+ * Get audit statistics
+ */
+ async getStatistics(filters = {}) {
+ const { startDate, endDate, userId } = filters;
+
+ const query = {};
+ if (userId) query.userId = userId;
+ if (startDate || endDate) {
+ query.timestamp = {};
+ if (startDate) query.timestamp.$gte = new Date(startDate);
+ if (endDate) query.timestamp.$lte = new Date(endDate);
+ }
+
+ const [
+ totalLogs,
+ actionStats,
+ severityStats,
+ categoryStats,
+ entityStats,
+ recentActivity
+ ] = await Promise.all([
+ AuditLog.countDocuments(query),
+ AuditLog.aggregate([
+ { $match: query },
+ { $group: { _id: '$action', count: { $sum: 1 } } },
+ { $sort: { count: -1 } }
+ ]),
+ AuditLog.aggregate([
+ { $match: query },
+ { $group: { _id: '$severity', count: { $sum: 1 } } }
+ ]),
+ AuditLog.aggregate([
+ { $match: query },
+ { $group: { _id: '$category', count: { $sum: 1 } } }
+ ]),
+ AuditLog.aggregate([
+ { $match: query },
+ { $group: { _id: '$entityType', count: { $sum: 1 } } },
+ { $sort: { count: -1 } },
+ { $limit: 10 }
+ ]),
+ AuditLog.find(query)
+ .sort({ timestamp: -1 })
+ .limit(10)
+ .select('timestamp action entityType severity')
+ .lean()
+ ]);
+
+ return {
+ totalLogs,
+ byAction: actionStats,
+ bySeverity: severityStats,
+ byCategory: categoryStats,
+ byEntity: entityStats,
+ recentActivity
+ };
+ }
+
+ /**
+ * Verify audit trail integrity
+ */
+ async verifyIntegrity(filters = {}) {
+ const { startDate, endDate, limit = 1000 } = filters;
+
+ const query = {};
+ if (startDate || endDate) {
+ query.timestamp = {};
+ if (startDate) query.timestamp.$gte = new Date(startDate);
+ if (endDate) query.timestamp.$lte = new Date(endDate);
+ }
+
+ const logs = await AuditLog.find(query)
+ .sort({ timestamp: 1 })
+ .limit(limit)
+ .lean();
+
+ const verification = await auditHasher.verifyChain(logs);
+
+ return {
+ ...verification,
+ period: {
+ startDate: logs.length > 0 ? logs[0].timestamp : null,
+ endDate: logs.length > 0 ? logs[logs.length - 1].timestamp : null
+ }
+ };
+ }
+
+ /**
+ * Get user activity timeline
+ */
+ async getUserTimeline(userId, options = {}) {
+ const { days = 30, limit = 100 } = options;
+
+ const startDate = new Date();
+ startDate.setDate(startDate.getDate() - days);
+
+ const logs = await AuditLog.find({
+ userId,
+ timestamp: { $gte: startDate }
+ })
+ .sort({ timestamp: -1 })
+ .limit(limit)
+ .lean();
+
+ // Group by day
+ const timeline = {};
+ for (const log of logs) {
+ const dateKey = log.timestamp.toISOString().split('T')[0];
+ if (!timeline[dateKey]) {
+ timeline[dateKey] = {
+ date: dateKey,
+ actions: [],
+ count: 0
+ };
+ }
+ timeline[dateKey].actions.push({
+ time: log.timestamp,
+ action: log.action,
+ entityType: log.entityType,
+ severity: log.severity
+ });
+ timeline[dateKey].count++;
+ }
+
+ return Object.values(timeline).sort((a, b) =>
+ new Date(b.date) - new Date(a.date)
+ );
+ }
+
+ /**
+ * Get entity history
+ */
+ async getEntityHistory(entityType, entityId) {
+ const logs = await AuditLog.find({
+ entityType,
+ entityId
+ })
+ .sort({ timestamp: 1 })
+ .populate('userId', 'name email')
+ .lean();
+
+ return logs.map(log => ({
+ timestamp: log.timestamp,
+ action: log.action,
+ user: log.userId,
+ changes: log.changes,
+ severity: log.severity
+ }));
+ }
+
+ /**
+ * Search audit logs
+ */
+ async searchLogs(searchTerm, options = {}) {
+ const { limit = 50 } = options;
+
+ const logs = await AuditLog.find({
+ $or: [
+ { userName: { $regex: searchTerm, $options: 'i' } },
+ { userEmail: { $regex: searchTerm, $options: 'i' } },
+ { entityName: { $regex: searchTerm, $options: 'i' } },
+ { entityType: { $regex: searchTerm, $options: 'i' } },
+ { 'metadata.ipAddress': { $regex: searchTerm, $options: 'i' } }
+ ]
+ })
+ .sort({ timestamp: -1 })
+ .limit(limit)
+ .lean();
+
+ return logs;
+ }
+
+ /**
+ * Get critical events
+ */
+ async getCriticalEvents(options = {}) {
+ const { days = 7, limit = 50 } = options;
+
+ const startDate = new Date();
+ startDate.setDate(startDate.getDate() - days);
+
+ const logs = await AuditLog.find({
+ severity: { $in: ['critical', 'high'] },
+ timestamp: { $gte: startDate }
+ })
+ .sort({ timestamp: -1 })
+ .limit(limit)
+ .populate('userId', 'name email')
+ .lean();
+
+ return logs;
+ }
+
+ /**
+ * Archive old logs
+ */
+ async archiveLogs(daysOld = 365) {
+ const archiveDate = new Date();
+ archiveDate.setDate(archiveDate.getDate() - daysOld);
+
+ const result = await AuditLog.updateMany(
+ {
+ timestamp: { $lt: archiveDate },
+ isArchived: false
+ },
+ {
+ $set: {
+ isArchived: true,
+ retentionDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year retention
+ }
+ }
+ );
+
+ return {
+ archived: result.modifiedCount,
+ archiveDate
+ };
+ }
+
+ /**
+ * Get audit dashboard data
+ */
+ async getDashboard() {
+ const last24Hours = new Date(Date.now() - 24 * 60 * 60 * 1000);
+ const last7Days = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
+
+ const [
+ total24h,
+ critical24h,
+ total7d,
+ topUsers,
+ topEntities,
+ recentCritical
+ ] = await Promise.all([
+ AuditLog.countDocuments({ timestamp: { $gte: last24Hours } }),
+ AuditLog.countDocuments({
+ timestamp: { $gte: last24Hours },
+ severity: { $in: ['critical', 'high'] }
+ }),
+ AuditLog.countDocuments({ timestamp: { $gte: last7Days } }),
+ AuditLog.aggregate([
+ { $match: { timestamp: { $gte: last7Days } } },
+ { $group: { _id: '$userId', count: { $sum: 1 }, userName: { $first: '$userName' } } },
+ { $sort: { count: -1 } },
+ { $limit: 5 }
+ ]),
+ AuditLog.aggregate([
+ { $match: { timestamp: { $gte: last7Days } } },
+ { $group: { _id: '$entityType', count: { $sum: 1 } } },
+ { $sort: { count: -1 } },
+ { $limit: 5 }
+ ]),
+ AuditLog.find({
+ severity: { $in: ['critical', 'high'] }
+ })
+ .sort({ timestamp: -1 })
+ .limit(10)
+ .lean()
+ ]);
+
+ return {
+ summary: {
+ total24h,
+ critical24h,
+ total7d
+ },
+ topUsers,
+ topEntities,
+ recentCritical
+ };
+ }
+}
+
+module.exports = new AuditTrailService();
diff --git a/services/complianceExportService.js b/services/complianceExportService.js
new file mode 100644
index 00000000..ecca8500
--- /dev/null
+++ b/services/complianceExportService.js
@@ -0,0 +1,368 @@
+const ComplianceReport = require('../models/ComplianceReport');
+const AuditLog = require('../models/AuditLog');
+const auditHasher = require('../utils/auditHasher');
+const fs = require('fs').promises;
+const path = require('path');
+
+class ComplianceExportService {
+ constructor() {
+ this.exportDir = path.join(__dirname, '../exports');
+ }
+
+ /**
+ * Generate compliance report
+ */
+ async generateReport(userId, options = {}) {
+ const {
+ reportType = 'CUSTOM',
+ startDate,
+ endDate,
+ filters = {},
+ exportFormats = ['JSON']
+ } = options;
+
+ const reportId = `CR-${Date.now()}`;
+
+ // Create report record
+ const report = new ComplianceReport({
+ reportId,
+ reportType,
+ generatedBy: userId,
+ period: {
+ startDate: new Date(startDate),
+ endDate: new Date(endDate)
+ },
+ filters,
+ status: 'generating'
+ });
+
+ await report.save();
+
+ try {
+ // Query audit logs
+ const logs = await this.queryLogsForReport(startDate, endDate, filters);
+
+ // Calculate summary
+ report.summary = this.calculateSummary(logs);
+
+ // Generate exports in requested formats
+ const exports = [];
+ for (const format of exportFormats) {
+ const exportData = await this.exportToFormat(logs, format, reportId, reportType);
+ exports.push(exportData);
+ }
+
+ report.exportFormats = exports;
+ report.integrityHash = auditHasher.generateReportHash({
+ reportId,
+ logs: logs.length,
+ summary: report.summary
+ });
+ report.status = 'completed';
+ report.metadata = {
+ recordCount: logs.length,
+ totalPages: Math.ceil(logs.length / 100)
+ };
+
+ await report.save();
+
+ return report;
+ } catch (err) {
+ report.status = 'failed';
+ await report.save();
+ throw err;
+ }
+ }
+
+ /**
+ * Query logs for report
+ */
+ async queryLogsForReport(startDate, endDate, filters) {
+ const query = {
+ timestamp: {
+ $gte: new Date(startDate),
+ $lte: new Date(endDate)
+ }
+ };
+
+ if (filters.users && filters.users.length > 0) {
+ query.userId = { $in: filters.users };
+ }
+ if (filters.actions && filters.actions.length > 0) {
+ query.action = { $in: filters.actions };
+ }
+ if (filters.entityTypes && filters.entityTypes.length > 0) {
+ query.entityType = { $in: filters.entityTypes };
+ }
+ if (filters.severity && filters.severity.length > 0) {
+ query.severity = { $in: filters.severity };
+ }
+ if (filters.categories && filters.categories.length > 0) {
+ query.category = { $in: filters.categories };
+ }
+
+ return await AuditLog.find(query)
+ .sort({ timestamp: 1 })
+ .populate('userId', 'name email')
+ .lean();
+ }
+
+ /**
+ * Calculate summary statistics
+ */
+ calculateSummary(logs) {
+ const summary = {
+ totalLogs: logs.length,
+ criticalEvents: 0,
+ securityEvents: 0,
+ dataModifications: 0,
+ uniqueUsers: new Set(),
+ failedAttempts: 0
+ };
+
+ for (const log of logs) {
+ if (log.severity === 'critical' || log.severity === 'high') {
+ summary.criticalEvents++;
+ }
+ if (log.category === 'security') {
+ summary.securityEvents++;
+ }
+ if (log.action === 'update' || log.action === 'delete') {
+ summary.dataModifications++;
+ }
+ if (log.userId) {
+ summary.uniqueUsers.add(log.userId.toString());
+ }
+ if (log.metadata && log.metadata.statusCode >= 400) {
+ summary.failedAttempts++;
+ }
+ }
+
+ summary.uniqueUsers = summary.uniqueUsers.size;
+
+ return summary;
+ }
+
+ /**
+ * Export to specific format
+ */
+ async exportToFormat(logs, format, reportId, reportType) {
+ const fileName = `${reportId}_${reportType}_${Date.now()}.${format.toLowerCase()}`;
+ const filePath = path.join(this.exportDir, fileName);
+
+ // Ensure export directory exists
+ await fs.mkdir(this.exportDir, { recursive: true });
+
+ let fileSize = 0;
+
+ switch (format) {
+ case 'JSON':
+ fileSize = await this.exportToJSON(logs, filePath);
+ break;
+ case 'CSV':
+ fileSize = await this.exportToCSV(logs, filePath);
+ break;
+ case 'EXCEL':
+ fileSize = await this.exportToExcel(logs, filePath);
+ break;
+ case 'PDF':
+ fileSize = await this.exportToPDF(logs, filePath, reportType);
+ break;
+ default:
+ throw new Error(`Unsupported export format: ${format}`);
+ }
+
+ return {
+ format,
+ filePath,
+ fileSize,
+ generatedAt: new Date()
+ };
+ }
+
+ /**
+ * Export to JSON
+ */
+ async exportToJSON(logs, filePath) {
+ const data = JSON.stringify(logs, null, 2);
+ await fs.writeFile(filePath, data, 'utf8');
+ const stats = await fs.stat(filePath);
+ return stats.size;
+ }
+
+ /**
+ * Export to CSV
+ */
+ async exportToCSV(logs, filePath) {
+ const headers = [
+ 'Timestamp', 'User', 'Action', 'Entity Type', 'Entity ID',
+ 'Severity', 'Category', 'IP Address', 'Status Code'
+ ];
+
+ const rows = logs.map(log => [
+ log.timestamp.toISOString(),
+ log.userName || '',
+ log.action,
+ log.entityType,
+ log.entityId || '',
+ log.severity,
+ log.category,
+ log.metadata?.ipAddress || '',
+ log.metadata?.statusCode || ''
+ ]);
+
+ const csv = [
+ headers.join(','),
+ ...rows.map(row => row.map(cell => `"${cell}"`).join(','))
+ ].join('\n');
+
+ await fs.writeFile(filePath, csv, 'utf8');
+ const stats = await fs.stat(filePath);
+ return stats.size;
+ }
+
+ /**
+ * Export to Excel (simplified - would use a library like exceljs in production)
+ */
+ async exportToExcel(logs, filePath) {
+ // Simplified: Export as CSV with .xlsx extension
+ // In production, use exceljs or similar library
+ return await this.exportToCSV(logs, filePath);
+ }
+
+ /**
+ * Export to PDF (simplified - would use a library like pdfkit in production)
+ */
+ async exportToPDF(logs, filePath, reportType) {
+ // Simplified: Create a text-based PDF representation
+ const content = [
+ `Compliance Report: ${reportType}`,
+ `Generated: ${new Date().toISOString()}`,
+ `Total Records: ${logs.length}`,
+ '',
+ 'Audit Trail:',
+ ...logs.map(log =>
+ `[${log.timestamp.toISOString()}] ${log.userName} - ${log.action} - ${log.entityType}`
+ )
+ ].join('\n');
+
+ await fs.writeFile(filePath, content, 'utf8');
+ const stats = await fs.stat(filePath);
+ return stats.size;
+ }
+
+ /**
+ * Get compliance templates
+ */
+ getComplianceTemplates() {
+ return {
+ SOX: {
+ name: 'Sarbanes-Oxley Act',
+ description: 'Financial reporting and internal controls',
+ requiredFields: ['timestamp', 'userId', 'action', 'entityType', 'changes'],
+ filters: {
+ categories: ['data', 'compliance'],
+ entityTypes: ['Transaction', 'Expense', 'Budget']
+ }
+ },
+ GDPR: {
+ name: 'General Data Protection Regulation',
+ description: 'Personal data processing and privacy',
+ requiredFields: ['timestamp', 'userId', 'action', 'entityType', 'metadata.ipAddress'],
+ filters: {
+ categories: ['security', 'data'],
+ entityTypes: ['User', 'Profile']
+ }
+ },
+ HIPAA: {
+ name: 'Health Insurance Portability and Accountability Act',
+ description: 'Protected health information access',
+ requiredFields: ['timestamp', 'userId', 'action', 'entityType', 'severity'],
+ filters: {
+ severity: ['high', 'critical'],
+ categories: ['security', 'compliance']
+ }
+ },
+ PCI_DSS: {
+ name: 'Payment Card Industry Data Security Standard',
+ description: 'Payment card data security',
+ requiredFields: ['timestamp', 'userId', 'action', 'metadata.ipAddress'],
+ filters: {
+ entityTypes: ['Payment', 'Transaction'],
+ categories: ['security', 'data']
+ }
+ },
+ ISO_27001: {
+ name: 'ISO/IEC 27001',
+ description: 'Information security management',
+ requiredFields: ['timestamp', 'userId', 'action', 'severity', 'category'],
+ filters: {
+ categories: ['security'],
+ severity: ['high', 'critical']
+ }
+ }
+ };
+ }
+
+ /**
+ * Get report by ID
+ */
+ async getReport(reportId) {
+ return await ComplianceReport.findOne({ reportId })
+ .populate('generatedBy', 'name email')
+ .lean();
+ }
+
+ /**
+ * List reports
+ */
+ async listReports(userId, options = {}) {
+ const { limit = 20, page = 1 } = options;
+
+ const query = { generatedBy: userId };
+ const skip = (page - 1) * limit;
+
+ const [reports, total] = await Promise.all([
+ ComplianceReport.find(query)
+ .sort({ generatedAt: -1 })
+ .skip(skip)
+ .limit(limit)
+ .lean(),
+ ComplianceReport.countDocuments(query)
+ ]);
+
+ return {
+ reports,
+ pagination: {
+ page,
+ limit,
+ total,
+ pages: Math.ceil(total / limit)
+ }
+ };
+ }
+
+ /**
+ * Download report file
+ */
+ async downloadReport(reportId, format) {
+ const report = await this.getReport(reportId);
+
+ if (!report) {
+ throw new Error('Report not found');
+ }
+
+ const exportData = report.exportFormats.find(e => e.format === format);
+
+ if (!exportData) {
+ throw new Error(`Format ${format} not available for this report`);
+ }
+
+ return {
+ filePath: exportData.filePath,
+ fileName: path.basename(exportData.filePath)
+ };
+ }
+}
+
+module.exports = new ComplianceExportService();
diff --git a/utils/auditHasher.js b/utils/auditHasher.js
new file mode 100644
index 00000000..5b848b6a
--- /dev/null
+++ b/utils/auditHasher.js
@@ -0,0 +1,113 @@
+const crypto = require('crypto');
+
+/**
+ * Audit Hasher Utility
+ * Provides cryptographic integrity verification for audit logs
+ */
+class AuditHasher {
+ /**
+ * Generate hash for audit log entry
+ */
+ generateHash(logData, previousHash = '') {
+ const data = {
+ timestamp: logData.timestamp,
+ userId: logData.userId,
+ action: logData.action,
+ entityType: logData.entityType,
+ entityId: logData.entityId,
+ changes: logData.changes,
+ previousHash
+ };
+
+ const dataString = JSON.stringify(data);
+ return crypto.createHash('sha256').update(dataString).digest('hex');
+ }
+
+ /**
+ * Verify integrity of audit log chain
+ */
+ async verifyChain(logs) {
+ if (!logs || logs.length === 0) {
+ return { valid: true, errors: [] };
+ }
+
+ const errors = [];
+
+ for (let i = 0; i < logs.length; i++) {
+ const log = logs[i];
+ const previousHash = i > 0 ? logs[i - 1].hash : '';
+
+ const expectedHash = this.generateHash(log, previousHash);
+
+ if (log.hash !== expectedHash) {
+ errors.push({
+ logId: log.logId,
+ index: i,
+ message: 'Hash mismatch - possible tampering detected',
+ expected: expectedHash,
+ actual: log.hash
+ });
+ }
+
+ if (i > 0 && log.previousHash !== logs[i - 1].hash) {
+ errors.push({
+ logId: log.logId,
+ index: i,
+ message: 'Chain broken - previous hash mismatch',
+ expected: logs[i - 1].hash,
+ actual: log.previousHash
+ });
+ }
+ }
+
+ return {
+ valid: errors.length === 0,
+ totalLogs: logs.length,
+ errors
+ };
+ }
+
+ /**
+ * Generate integrity report hash
+ */
+ generateReportHash(reportData) {
+ const dataString = JSON.stringify(reportData);
+ return crypto.createHash('sha256').update(dataString).digest('hex');
+ }
+
+ /**
+ * Encrypt sensitive data
+ */
+ encryptData(data, key) {
+ const algorithm = 'aes-256-cbc';
+ const iv = crypto.randomBytes(16);
+ const cipher = crypto.createCipheriv(algorithm, Buffer.from(key, 'hex'), iv);
+
+ let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
+ encrypted += cipher.final('hex');
+
+ return {
+ iv: iv.toString('hex'),
+ data: encrypted
+ };
+ }
+
+ /**
+ * Decrypt sensitive data
+ */
+ decryptData(encryptedData, key) {
+ const algorithm = 'aes-256-cbc';
+ const decipher = crypto.createDecipheriv(
+ algorithm,
+ Buffer.from(key, 'hex'),
+ Buffer.from(encryptedData.iv, 'hex')
+ );
+
+ let decrypted = decipher.update(encryptedData.data, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+
+ return JSON.parse(decrypted);
+ }
+}
+
+module.exports = new AuditHasher();