Skip to main content
Glama

Jira MCP Server

by sespinosa
MIT License
15
2
  • Apple
  • Linux
auditLogger.ts12.7 kB
export interface AuditLogEntry { timestamp: string; operation: string; resource: string; resourceId?: string; details: Record<string, any>; riskLevel: 'low' | 'medium' | 'high' | 'critical'; success: boolean; error?: string; metadata?: Record<string, any>; } export interface AuditLoggerOptions { enableConsoleLogging?: boolean; enableFileLogging?: boolean; logFilePath?: string; maxLogEntries?: number; retentionDays?: number; cleanupIntervalMs?: number; maxMemoryUsageMB?: number; batchCleanupSize?: number; } export class AuditLogger { private logs: AuditLogEntry[] = []; private options: Required<AuditLoggerOptions>; private cleanupTimer: NodeJS.Timeout | null = null; private lastCleanup: number = Date.now(); private cleanupInProgress: boolean = false; constructor(options: AuditLoggerOptions = {}) { this.options = { enableConsoleLogging: options.enableConsoleLogging ?? true, enableFileLogging: options.enableFileLogging ?? false, logFilePath: options.logFilePath ?? './audit.log', maxLogEntries: options.maxLogEntries ?? 10000, retentionDays: options.retentionDays ?? 90, cleanupIntervalMs: options.cleanupIntervalMs ?? 300000, // 5 minutes maxMemoryUsageMB: options.maxMemoryUsageMB ?? 50, batchCleanupSize: options.batchCleanupSize ?? 1000, }; // Start periodic cleanup this.startPeriodicCleanup(); } log(entry: Omit<AuditLogEntry, 'timestamp'>): void { const fullEntry: AuditLogEntry = { timestamp: new Date().toISOString(), ...entry, }; // Add to in-memory logs this.logs.push(fullEntry); // Trigger cleanup if memory usage is high or if it's time const now = Date.now(); if (this.shouldTriggerCleanup(now)) { this.performCleanup(); } // Console logging if (this.options.enableConsoleLogging) { this.logToConsole(fullEntry); } // File logging (if enabled) if (this.options.enableFileLogging) { this.logToFile(fullEntry); } } logOperation( operation: string, resource: string, details: Record<string, any>, options: { resourceId?: string; riskLevel?: AuditLogEntry['riskLevel']; success?: boolean; error?: string; metadata?: Record<string, any>; } = {} ): void { this.log({ operation, resource, resourceId: options.resourceId, details, riskLevel: options.riskLevel ?? this.determineRiskLevel(operation, resource), success: options.success ?? true, error: options.error, metadata: options.metadata, }); } logSuccess(operation: string, resource: string, details: Record<string, any>): void { this.logOperation(operation, resource, details, { success: true }); } logFailure( operation: string, resource: string, error: string, details: Record<string, any> = {} ): void { this.logOperation(operation, resource, details, { success: false, error, riskLevel: 'high', }); } logSecurityEvent( operation: string, details: Record<string, any>, severity: 'medium' | 'high' | 'critical' = 'high' ): void { this.logOperation(operation, 'security', details, { riskLevel: severity, success: false, }); } private determineRiskLevel(operation: string, resource: string): AuditLogEntry['riskLevel'] { // Critical operations const criticalOps = ['delete', 'remove', 'destroy', 'purge']; const criticalResources = ['user', 'project', 'board']; if ( criticalOps.some((op) => operation.toLowerCase().includes(op)) || criticalResources.some((res) => resource.toLowerCase().includes(res)) ) { return 'critical'; } // High risk operations const highRiskOps = ['bulk', 'mass', 'complete', 'close', 'resolve', 'transition']; const highRiskResources = ['sprint', 'attachment', 'permission']; if ( highRiskOps.some((op) => operation.toLowerCase().includes(op)) || highRiskResources.some((res) => resource.toLowerCase().includes(res)) ) { return 'high'; } // Medium risk operations const mediumRiskOps = ['update', 'modify', 'edit', 'move', 'assign']; const mediumRiskResources = ['issue', 'comment', 'worklog']; if ( mediumRiskOps.some((op) => operation.toLowerCase().includes(op)) || mediumRiskResources.some((res) => resource.toLowerCase().includes(res)) ) { return 'medium'; } return 'low'; } private logToConsole(entry: AuditLogEntry): void { const prefix = `[AUDIT:${entry.riskLevel.toUpperCase()}]`; const message = `${entry.operation} on ${entry.resource}${entry.resourceId ? `:${entry.resourceId}` : ''}`; const status = entry.success ? 'SUCCESS' : 'FAILED'; const logLine = `${prefix} ${entry.timestamp} - ${message} - ${status}`; switch (entry.riskLevel) { case 'critical': console.error(logLine, entry.details); break; case 'high': console.warn(logLine, entry.details); break; case 'medium': console.info(logLine, entry.details); break; default: console.log(logLine, entry.details); } } private logToFile(_entry: AuditLogEntry): void { // In a real implementation, this would write to a file // For now, we'll just store it in memory // You could implement this with fs.appendFileSync or a logging library } private startPeriodicCleanup(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.cleanupTimer = setInterval(() => { try { this.performCleanup(); } catch (error) { console.error('AuditLogger cleanup error:', error); } }, this.options.cleanupIntervalMs); // Don't keep Node.js alive just for cleanup this.cleanupTimer.unref(); } private shouldTriggerCleanup(now: number): boolean { // Always cleanup if we've exceeded max entries if (this.logs.length > this.options.maxLogEntries) { return true; } // Cleanup if it's been too long since last cleanup if (now - this.lastCleanup > this.options.cleanupIntervalMs) { return true; } // Cleanup if estimated memory usage is high if (this.getEstimatedMemoryUsage() > this.options.maxMemoryUsageMB) { return true; } return false; } private performCleanup(): void { if (this.cleanupInProgress) { return; } this.cleanupInProgress = true; try { const initialLength = this.logs.length; // Step 1: Remove entries older than retention period const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - this.options.retentionDays); this.logs = this.logs.filter((log) => new Date(log.timestamp) > cutoffDate); // Step 2: If still too many entries, remove oldest ones in batches if (this.logs.length > this.options.maxLogEntries) { // Keep the most recent entries this.logs = this.logs.slice(-this.options.maxLogEntries); } // Step 3: If memory usage is still high, remove more entries while ( this.getEstimatedMemoryUsage() > this.options.maxMemoryUsageMB && this.logs.length > 100 ) { const removeCount = Math.min(this.options.batchCleanupSize, this.logs.length - 100); this.logs.splice(0, removeCount); } const finalLength = this.logs.length; const removedEntries = initialLength - finalLength; if (removedEntries > 0) { console.log( `AuditLogger cleanup: removed ${removedEntries} entries, ${finalLength} remaining` ); } this.lastCleanup = Date.now(); } catch (error) { console.error('AuditLogger cleanup failed:', error); } finally { this.cleanupInProgress = false; } } private getEstimatedMemoryUsage(): number { if (this.logs.length === 0) return 0; // Estimate memory usage based on average entry size const sampleSize = Math.min(10, this.logs.length); const sampleEntries = this.logs.slice(-sampleSize); const avgEntrySize = sampleEntries.reduce((sum, entry) => { return sum + JSON.stringify(entry).length; }, 0) / sampleSize; // Convert to MB (rough estimate including object overhead) return (this.logs.length * avgEntrySize * 2) / (1024 * 1024); } getLogs(filter?: { operation?: string; resource?: string; riskLevel?: AuditLogEntry['riskLevel']; success?: boolean; since?: Date; limit?: number; }): AuditLogEntry[] { let filtered = [...this.logs]; if (filter) { if (filter.operation) { filtered = filtered.filter((log) => log.operation.includes(filter.operation!)); } if (filter.resource) { filtered = filtered.filter((log) => log.resource.includes(filter.resource!)); } if (filter.riskLevel) { filtered = filtered.filter((log) => log.riskLevel === filter.riskLevel); } if (filter.success !== undefined) { filtered = filtered.filter((log) => log.success === filter.success); } if (filter.since) { filtered = filtered.filter((log) => new Date(log.timestamp) > filter.since!); } } // Sort by timestamp (most recent first) filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); // Apply limit if (filter?.limit) { filtered = filtered.slice(0, filter.limit); } return filtered; } getSecurityEvents(limit = 100): AuditLogEntry[] { return this.getLogs({ resource: 'security', limit, }); } getFailedOperations(limit = 100): AuditLogEntry[] { return this.getLogs({ success: false, limit, }); } getHighRiskOperations(limit = 100): AuditLogEntry[] { return this.getLogs({ riskLevel: 'high', limit, }).concat( this.getLogs({ riskLevel: 'critical', limit, }) ); } clear(): void { this.logs = []; this.lastCleanup = Date.now(); } destroy(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } this.logs = []; this.cleanupInProgress = false; } getStats(): { totalLogs: number; byRiskLevel: Record<AuditLogEntry['riskLevel'], number>; bySuccess: { success: number; failure: number }; recentActivity: number; // last 24 hours } { const byRiskLevel = { low: 0, medium: 0, high: 0, critical: 0, }; const bySuccess = { success: 0, failure: 0, }; const last24Hours = new Date(); last24Hours.setHours(last24Hours.getHours() - 24); let recentActivity = 0; for (const log of this.logs) { byRiskLevel[log.riskLevel]++; if (log.success) { bySuccess.success++; } else { bySuccess.failure++; } if (new Date(log.timestamp) > last24Hours) { recentActivity++; } } return { totalLogs: this.logs.length, byRiskLevel, bySuccess, recentActivity, }; } } // Global audit logger instance export const auditLogger = new AuditLogger({ enableConsoleLogging: true, enableFileLogging: false, maxLogEntries: 5000, retentionDays: 30, cleanupIntervalMs: 300000, // 5 minutes maxMemoryUsageMB: 25, batchCleanupSize: 500, }); // Convenience functions for common operations export const logFileOperation = ( operation: string, filePath: string, success = true, error?: string ) => { auditLogger.logOperation(operation, 'file', { filePath }, { success, error }); }; export const logJQLSearch = (jql: string, resultCount: number, success = true, error?: string) => { auditLogger.logOperation('jql_search', 'search', { jql, resultCount }, { success, error }); }; export const logBulkOperation = ( operation: string, resource: string, count: number, success = true, error?: string ) => { auditLogger.logOperation( `bulk_${operation}`, resource, { count }, { success, error, riskLevel: 'high', } ); }; export const logDestructiveOperation = ( operation: string, resource: string, resourceId?: string, success = true, error?: string ) => { auditLogger.logOperation( operation, resource, { destructive: true }, { resourceId, success, error, riskLevel: 'critical', } ); }; export const logSecurityViolation = (violation: string, details: Record<string, any>) => { auditLogger.logSecurityEvent(`security_violation_${violation}`, details, 'critical'); };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sespinosa/jira-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server