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 + + + + + + + + +
+ +
+
+

Forensic Audit Trail

+

Immutable audit logs with cryptographic integrity verification

+
+
+ + +
+
+ + +
+
+
+ +
+
+ +

0

+
+
+
+
+ +
+
+ +

0

+
+
+
+
+ +
+
+ +

0

+
+
+
+
+ +
+
+ +

Verified

+
+
+
+ + +
+
+

Filters

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+

Audit Logs

+
+ + Page 1 of 1 + +
+
+
+ + + + + + + + + + + + + + + + +
TimestampUserActionEntitySeverityCategoryIP AddressDetails
+
+
+ + +
+
+
+

Activity by Action

+
+
+ +
+
+ +
+
+

Severity Distribution

+
+
+ +
+
+
+ + +
+
+

Recent Critical Events

+
+
+ +
+
+
+ + + + + + + + + + + + \ 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.severity} + ${new Date(event.timestamp).toLocaleString()} +
+
+ ${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();