Skip to main content
Glama
security-audit.ts13.5 kB
/** * Security audit logging and monitoring */ import { logger } from '../utils/logger'; export interface SecurityEvent { type: | 'input_validation' | 'rate_limit' | 'resource_abuse' | 'suspicious_activity' | 'access_denied'; severity: 'low' | 'medium' | 'high' | 'critical'; operation?: string; clientId?: string; details: Record<string, unknown>; timestamp: Date; userAgent?: string; ipAddress?: string; } export interface SecurityMetrics { totalEvents: number; eventsByType: Record<string, number>; eventsBySeverity: Record<string, number>; recentEvents: SecurityEvent[]; suspiciousClients: Array<{ clientId: string; eventCount: number; lastEvent: Date; riskScore: number; }>; } export class SecurityAuditor { private static instance: SecurityAuditor; private events: SecurityEvent[] = []; private maxEvents = 10000; // Keep last 10k events private clientRiskScores: Map<string, number> = new Map(); private cleanupInterval: ReturnType<typeof setInterval> | null = null; private suspiciousPatterns: RegExp[] = [ /script/i, /javascript/i, /vbscript/i, /onload/i, /onerror/i, /eval\(/i, /expression\(/i, /<iframe/i, /<object/i, /<embed/i, ]; private constructor() { // Only start periodic cleanup in non-test environments const isTestEnvironment = process.env['NODE_ENV'] === 'test' || process.env['JEST_WORKER_ID'] !== undefined || process.env['CI'] === 'true' || typeof jest !== 'undefined' || (typeof global !== 'undefined' && 'jest' in global); if (!isTestEnvironment) { this.cleanupInterval = setInterval( () => { this.cleanupOldEvents(); }, 60 * 60 * 1000 ); // Every hour } } public static getInstance(): SecurityAuditor { if (!SecurityAuditor.instance) { SecurityAuditor.instance = new SecurityAuditor(); } return SecurityAuditor.instance; } public logSecurityEvent(event: Omit<SecurityEvent, 'timestamp'>): void { const fullEvent: SecurityEvent = { ...event, timestamp: new Date(), }; this.events.push(fullEvent); // Update client risk score if (event.clientId) { this.updateClientRiskScore(event.clientId, event.severity, event.type); } // Log based on severity const logMessage = `Security event: ${event.type}`; const logContext: Record<string, unknown> = { severity: event.severity, details: event.details, }; if (event.operation) { logContext['tool'] = event.operation; } if (event.clientId) { logContext['clientId'] = event.clientId; } switch (event.severity) { case 'critical': logger.error(logMessage, logContext); break; case 'high': logger.warn(logMessage, logContext); break; case 'medium': logger.info(logMessage, logContext); break; case 'low': logger.debug(logMessage, logContext); break; } // Trim events if we have too many if (this.events.length > this.maxEvents) { this.events = this.events.slice(-this.maxEvents); } // Check for immediate threats this.checkForImmediateThreats(fullEvent); } private updateClientRiskScore( clientId: string, severity: string, eventType: string ): void { const currentScore = this.clientRiskScores.get(clientId) || 0; let scoreIncrease = 0; // Score increases based on severity switch (severity) { case 'critical': scoreIncrease = 50; break; case 'high': scoreIncrease = 20; break; case 'medium': scoreIncrease = 5; break; case 'low': scoreIncrease = 1; break; } // Additional score for certain event types if (eventType === 'suspicious_activity') { scoreIncrease *= 2; } const newScore = Math.min(100, currentScore + scoreIncrease); this.clientRiskScores.set(clientId, newScore); // Log high-risk clients if (newScore >= 80) { logger.warn(`High-risk client detected: ${clientId}`, { riskScore: newScore, eventType, severity, }); } } private checkForImmediateThreats(event: SecurityEvent): void { // Prevent infinite recursion by not checking threats for threat events if (event.type === 'suspicious_activity' && event.details['threat']) { return; } // Check for patterns that indicate immediate threats const threatPatterns = [ { condition: event.type === 'input_validation' && event.severity === 'critical', action: 'Block client temporarily', }, { condition: event.type === 'resource_abuse' && event.severity === 'high', action: 'Implement aggressive rate limiting', }, { condition: this.getClientEventCount(event.clientId || '', 60000) > 100, // 100 events in 1 minute action: 'Potential DoS attack detected', }, ]; for (const pattern of threatPatterns) { if (pattern.condition) { const logContext: Record<string, unknown> = { eventType: event.type, severity: event.severity, }; if (event.clientId) { logContext['clientId'] = event.clientId; } logger.error( `Immediate threat detected: ${pattern.action}`, logContext ); // Create threat event directly without triggering recursive check const threatEvent: SecurityEvent = { type: 'suspicious_activity', severity: 'critical', operation: event.operation || 'unknown', clientId: event.clientId || 'unknown', timestamp: new Date(), details: { threat: pattern.action, triggeringEvent: event, }, }; // Add to events without calling logSecurityEvent to prevent recursion this.events.push(threatEvent); // Update client risk score directly without triggering more events if (threatEvent.clientId) { const currentScore = this.clientRiskScores.get(threatEvent.clientId) || 0; const newScore = Math.min(100, currentScore + 50); // Critical = +50 this.clientRiskScores.set(threatEvent.clientId, newScore); } } } } public analyzeInput( input: string, operation: string, clientId?: string ): { isSuspicious: boolean; suspiciousPatterns: string[]; riskScore: number; } { const suspiciousPatterns: string[] = []; let riskScore = 0; // Check against known suspicious patterns for (const pattern of this.suspiciousPatterns) { if (pattern.test(input)) { suspiciousPatterns.push(pattern.source); riskScore += 10; } } // Check for excessive length if (input.length > 10000) { suspiciousPatterns.push('excessive_length'); riskScore += 5; } // Check for unusual characters const unusualChars = /[^\x20-\x7E\s]/g; const unusualMatches = input.match(unusualChars); if (unusualMatches && unusualMatches.length > 10) { suspiciousPatterns.push('unusual_characters'); riskScore += 5; } // Check for repeated patterns (potential injection) const repeatedPattern = /(.{3,})\1{3,}/g; if (repeatedPattern.test(input)) { suspiciousPatterns.push('repeated_patterns'); riskScore += 5; } const isSuspicious = suspiciousPatterns.length > 0 || riskScore > 15; if (isSuspicious) { this.logSecurityEvent({ type: 'suspicious_activity', severity: riskScore > 30 ? 'high' : riskScore > 15 ? 'medium' : 'low', operation, clientId: clientId || 'unknown', details: { suspiciousPatterns, riskScore, inputLength: input.length, inputPreview: input.substring(0, 100), // First 100 chars for analysis }, }); } return { isSuspicious, suspiciousPatterns, riskScore, }; } public getClientEventCount(clientId: string, timeWindowMs: number): number { const cutoff = new Date(Date.now() - timeWindowMs); return this.events.filter( event => event.clientId === clientId && event.timestamp >= cutoff ).length; } public getClientRiskScore(clientId: string): number { return this.clientRiskScores.get(clientId) || 0; } public getMetrics(): SecurityMetrics { const now = new Date(); const last24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000); // Filter recent events const recentEvents = this.events.filter( event => event.timestamp >= last24Hours ); // Count by type const eventsByType: Record<string, number> = {}; const eventsBySeverity: Record<string, number> = {}; for (const event of recentEvents) { eventsByType[event.type] = (eventsByType[event.type] || 0) + 1; eventsBySeverity[event.severity] = (eventsBySeverity[event.severity] || 0) + 1; } // Get suspicious clients const clientEventCounts: Record< string, { count: number; lastEvent: Date } > = {}; for (const event of recentEvents) { if (event.clientId) { if (!clientEventCounts[event.clientId]) { clientEventCounts[event.clientId] = { count: 0, lastEvent: event.timestamp, }; } const clientData = clientEventCounts[event.clientId]; if (clientData) { clientData.count++; if (event.timestamp > clientData.lastEvent) { clientData.lastEvent = event.timestamp; } } } } const suspiciousClients = Object.entries(clientEventCounts) .map(([clientId, data]) => ({ clientId, eventCount: data.count, lastEvent: data.lastEvent, riskScore: this.clientRiskScores.get(clientId) || 0, })) .filter(client => client.riskScore > 20 || client.eventCount > 50) .sort((a, b) => b.riskScore - a.riskScore); return { totalEvents: recentEvents.length, eventsByType, eventsBySeverity, recentEvents: recentEvents.slice(-100), // Last 100 events suspiciousClients, }; } public generateSecurityReport(): { summary: string; recommendations: string[]; criticalIssues: SecurityEvent[]; trends: Record<string, number>; } { const metrics = this.getMetrics(); const criticalIssues = this.events .filter(event => event.severity === 'critical') .slice(-10); // Calculate trends (compare last 24h vs previous 24h) const now = new Date(); const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1000); const previous24h = new Date(now.getTime() - 48 * 60 * 60 * 1000); const recent = this.events.filter(e => e.timestamp >= last24h).length; const previous = this.events.filter( e => e.timestamp >= previous24h && e.timestamp < last24h ).length; const trends = { totalEvents: recent - previous, criticalEvents: criticalIssues.length, suspiciousClients: metrics.suspiciousClients.length, }; // Generate recommendations const recommendations: string[] = []; if ((metrics.eventsBySeverity['critical'] || 0) > 0) { recommendations.push('Investigate critical security events immediately'); } if (metrics.suspiciousClients.length > 5) { recommendations.push('Consider implementing stricter rate limiting'); } if ((metrics.eventsByType['input_validation'] || 0) > 100) { recommendations.push( 'Review input validation rules - high validation failure rate' ); } if (trends.totalEvents > 50) { recommendations.push( 'Security event volume increasing - monitor closely' ); } const summary = `Security Report: ${metrics.totalEvents} events in last 24h, ${criticalIssues.length} critical issues, ${metrics.suspiciousClients.length} suspicious clients`; return { summary, recommendations, criticalIssues, trends, }; } private cleanupOldEvents(): void { const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // 7 days const originalLength = this.events.length; this.events = this.events.filter(event => event.timestamp >= cutoff); const removed = originalLength - this.events.length; if (removed > 0) { logger.debug(`Cleaned up ${removed} old security events`); } // Also cleanup old client risk scores const activeClients = new Set( this.events.filter(e => e.clientId).map(e => e.clientId!) ); for (const [clientId] of this.clientRiskScores.entries()) { if (!activeClients.has(clientId)) { this.clientRiskScores.delete(clientId); } } } public reset(): void { this.events = []; this.clientRiskScores.clear(); logger.info('Security audit data reset'); } public destroy(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.events = []; this.clientRiskScores.clear(); // @ts-ignore - We need to reset the instance for testing SecurityAuditor.instance = undefined; } } // Export singleton instance export const securityAuditor = SecurityAuditor.getInstance();

Latest Blog Posts

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/keyurgolani/ColorMcp'

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