UNPKG

@ordojs/security

Version:

Security package for OrdoJS with XSS, CSRF, and injection protection

360 lines (315 loc) 9.55 kB
export interface SecurityEvent { id: string; type: 'xss_attempt' | 'csrf_violation' | 'injection_attempt' | 'rate_limit_exceeded' | 'auth_failure' | 'suspicious_activity'; severity: 'low' | 'medium' | 'high' | 'critical'; timestamp: Date; source: { ip?: string; userAgent?: string; userId?: string; sessionId?: string; }; details: Record<string, any>; blocked: boolean; } export interface SecurityMetrics { totalEvents: number; blockedEvents: number; eventsByType: Record<string, number>; eventsBySeverity: Record<string, number>; topSources: Array<{ ip: string; count: number }>; timeRange: { start: Date; end: Date; }; } export interface RuntimeMonitorOptions { enableLogging?: boolean; logLevel?: 'debug' | 'info' | 'warn' | 'error'; maxEvents?: number; alertThresholds?: { critical: number; high: number; medium: number; }; onAlert?: (event: SecurityEvent) => void; onMetricsUpdate?: (metrics: SecurityMetrics) => void; } export class RuntimeSecurityMonitor { private events: SecurityEvent[] = []; private options: RuntimeMonitorOptions; private alertCounts: Record<string, number> = {}; private startTime: Date; constructor(options: RuntimeMonitorOptions = {}) { this.options = { enableLogging: true, logLevel: 'warn', maxEvents: 10000, alertThresholds: { critical: 1, high: 5, medium: 10, }, ...options, }; this.startTime = new Date(); } recordEvent(event: Omit<SecurityEvent, 'id' | 'timestamp'>): void { const securityEvent: SecurityEvent = { id: this.generateEventId(), timestamp: new Date(), ...event, }; this.events.push(securityEvent); // Maintain max events limit if (this.events.length > this.options.maxEvents!) { this.events.shift(); } // Log event if enabled if (this.options.enableLogging) { this.logEvent(securityEvent); } // Check alert thresholds this.checkAlertThresholds(securityEvent); // Update metrics if (this.options.onMetricsUpdate) { this.options.onMetricsUpdate(this.getMetrics()); } } recordXSSAttempt(details: { payload: string; source: SecurityEvent['source']; blocked: boolean; context?: string; }): void { this.recordEvent({ type: 'xss_attempt', severity: 'high', source: details.source, details: { payload: details.payload, context: details.context, }, blocked: details.blocked, }); } recordCSRFViolation(details: { expectedToken?: string; receivedToken?: string; source: SecurityEvent['source']; endpoint: string; }): void { this.recordEvent({ type: 'csrf_violation', severity: 'high', source: details.source, details: { endpoint: details.endpoint, hasExpectedToken: !!details.expectedToken, hasReceivedToken: !!details.receivedToken, }, blocked: true, }); } recordInjectionAttempt(details: { type: 'sql' | 'nosql' | 'ldap' | 'command'; payload: string; source: SecurityEvent['source']; blocked: boolean; query?: string; }): void { this.recordEvent({ type: 'injection_attempt', severity: 'critical', source: details.source, details: { injectionType: details.type, payload: details.payload, query: details.query, }, blocked: details.blocked, }); } recordRateLimitExceeded(details: { limit: number; current: number; window: string; source: SecurityEvent['source']; endpoint?: string; }): void { this.recordEvent({ type: 'rate_limit_exceeded', severity: 'medium', source: details.source, details: { limit: details.limit, current: details.current, window: details.window, endpoint: details.endpoint, }, blocked: true, }); } recordAuthFailure(details: { reason: 'invalid_credentials' | 'account_locked' | 'token_expired' | 'insufficient_permissions'; source: SecurityEvent['source']; username?: string; endpoint?: string; }): void { this.recordEvent({ type: 'auth_failure', severity: details.reason === 'insufficient_permissions' ? 'medium' : 'high', source: details.source, details: { reason: details.reason, username: details.username, endpoint: details.endpoint, }, blocked: true, }); } recordSuspiciousActivity(details: { activity: string; riskScore: number; source: SecurityEvent['source']; context?: Record<string, any>; }): void { const severity = details.riskScore >= 80 ? 'critical' : details.riskScore >= 60 ? 'high' : details.riskScore >= 40 ? 'medium' : 'low'; this.recordEvent({ type: 'suspicious_activity', severity, source: details.source, details: { activity: details.activity, riskScore: details.riskScore, context: details.context, }, blocked: details.riskScore >= 80, }); } getMetrics(): SecurityMetrics { const now = new Date(); const eventsByType: Record<string, number> = {}; const eventsBySeverity: Record<string, number> = {}; const sourceIpCounts: Record<string, number> = {}; let totalEvents = 0; let blockedEvents = 0; this.events.forEach(event => { totalEvents++; if (event.blocked) blockedEvents++; eventsByType[event.type] = (eventsByType[event.type] || 0) + 1; eventsBySeverity[event.severity] = (eventsBySeverity[event.severity] || 0) + 1; if (event.source.ip) { sourceIpCounts[event.source.ip] = (sourceIpCounts[event.source.ip] || 0) + 1; } }); const topSources = Object.entries(sourceIpCounts) .map(([ip, count]) => ({ ip, count })) .sort((a, b) => b.count - a.count) .slice(0, 10); return { totalEvents, blockedEvents, eventsByType, eventsBySeverity, topSources, timeRange: { start: this.startTime, end: now, }, }; } getEvents(filter?: { type?: SecurityEvent['type']; severity?: SecurityEvent['severity']; since?: Date; limit?: number; }): SecurityEvent[] { let filteredEvents = [...this.events]; if (filter?.type) { filteredEvents = filteredEvents.filter(e => e.type === filter.type); } if (filter?.severity) { filteredEvents = filteredEvents.filter(e => e.severity === filter.severity); } if (filter?.since) { filteredEvents = filteredEvents.filter(e => e.timestamp >= filter.since!); } if (filter?.limit) { filteredEvents = filteredEvents.slice(-filter.limit); } return filteredEvents.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); } clearEvents(): void { this.events = []; this.alertCounts = {}; } exportEvents(format: 'json' | 'csv' = 'json'): string { if (format === 'csv') { const headers = ['id', 'type', 'severity', 'timestamp', 'blocked', 'source_ip', 'user_agent', 'details']; const rows = this.events.map(event => [ event.id, event.type, event.severity, event.timestamp.toISOString(), event.blocked.toString(), event.source.ip || '', event.source.userAgent || '', JSON.stringify(event.details), ]); return [headers, ...rows].map(row => row.join(',')).join('\n'); } return JSON.stringify(this.events, null, 2); } private generateEventId(): string { return `sec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } private logEvent(event: SecurityEvent): void { const logLevel = this.getLogLevel(event.severity); if (!this.shouldLog(logLevel)) return; const message = `Security Event [${event.type}]: ${event.severity.toUpperCase()} - ${JSON.stringify(event.details)}`; switch (logLevel) { case 'error': console.error(message); break; case 'warn': console.warn(message); break; case 'info': console.info(message); break; case 'debug': console.debug(message); break; } } private getLogLevel(severity: SecurityEvent['severity']): 'debug' | 'info' | 'warn' | 'error' { switch (severity) { case 'critical': return 'error'; case 'high': return 'error'; case 'medium': return 'warn'; case 'low': return 'info'; default: return 'debug'; } } private shouldLog(level: 'debug' | 'info' | 'warn' | 'error'): boolean { const levels = ['debug', 'info', 'warn', 'error']; const currentLevelIndex = levels.indexOf(this.options.logLevel!); const eventLevelIndex = levels.indexOf(level); return eventLevelIndex >= currentLevelIndex; } private checkAlertThresholds(event: SecurityEvent): void { const thresholds = this.options.alertThresholds!; const threshold = event.severity === 'low' ? undefined : thresholds[event.severity as keyof typeof thresholds]; if (!threshold) return; const key = `${event.severity}_${event.type}`; this.alertCounts[key] = (this.alertCounts[key] || 0) + 1; if (this.alertCounts[key] >= threshold && this.options.onAlert) { this.options.onAlert(event); } } } // Singleton instance for global use export const runtimeMonitor = new RuntimeSecurityMonitor();