UNPKG

@noony-serverless/core

Version:

A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript

398 lines 14.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.securityEventTracker = exports.SecurityAuditPresets = exports.securityAudit = exports.SecurityAuditMiddleware = void 0; const core_1 = require("../core"); const logger_1 = require("../core/logger"); const DEFAULT_EXCLUDE_HEADERS = [ 'authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-auth-token', ]; const DEFAULT_SUSPICIOUS_PATTERNS = { sqlInjection: [ /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC(UTE)?|UNION|SCRIPT)\b)/i, /(('%27)|('))(('%6F)|o|('%4F'))(('%72)|r|('%52'))/i, /(((')|('))\s*(('%6F)|o|('%4F'))(('%72)|r|('%52')))/i, /((')|('))union/i, ], xss: [ /<script[^>]*>.*?<\/script>/gi, /javascript:/gi, /vbscript:/gi, /on\w+\s*=/gi, /<iframe[^>]*>.*?<\/iframe>/gi, ], pathTraversal: [ /\.\.[/\\]/g, /%2e%2e[/\\]/gi, /%252e%252e[/\\]/gi, /\.\.[%2f%5c]/gi, ], commandInjection: [ /[;&|`$()]/g, /%[0-9a-f]{2}/gi, /\b(cat|ls|ps|id|pwd|uname|whoami|curl|wget)\b/i, ], }; /** * Security event tracking for anomaly detection */ class SecurityEventTracker { events = new Map(); maxEventsPerClient = 100; timeWindow = 60 * 60 * 1000; // 1 hour addEvent(event) { const clientKey = event.clientIP; const clientEvents = this.events.get(clientKey) || []; // Remove old events outside time window const cutoff = Date.now() - this.timeWindow; const recentEvents = clientEvents.filter((e) => new Date(e.timestamp).getTime() > cutoff); // Add new event recentEvents.push(event); // Limit number of events stored per client if (recentEvents.length > this.maxEventsPerClient) { recentEvents.splice(0, recentEvents.length - this.maxEventsPerClient); } this.events.set(clientKey, recentEvents); } getClientEvents(clientIP, minutes = 60) { const cutoff = Date.now() - minutes * 60 * 1000; const events = this.events.get(clientIP) || []; return events.filter((e) => new Date(e.timestamp).getTime() > cutoff); } detectAnomalies(clientIP) { const recentEvents = this.getClientEvents(clientIP, 10); // Last 10 minutes const anomalies = []; // Multiple failed authentication attempts const authFailures = recentEvents.filter((e) => e.type === 'AUTHENTICATION_FAILURE'); if (authFailures.length >= 5) { anomalies.push({ type: 'UNUSUAL_BEHAVIOR', severity: 'HIGH', timestamp: new Date().toISOString(), requestId: 'anomaly-detection', clientIP, endpoint: 'multiple-endpoints', method: 'MULTIPLE', details: { anomalyType: 'multiple_auth_failures', count: authFailures.length, timeWindow: '10 minutes', }, }); } // High rate of suspicious requests const suspiciousEvents = recentEvents.filter((e) => ['INJECTION_ATTEMPT', 'MALFORMED_REQUEST', 'SUSPICIOUS_REQUEST'].includes(e.type)); if (suspiciousEvents.length >= 10) { anomalies.push({ type: 'UNUSUAL_BEHAVIOR', severity: 'CRITICAL', timestamp: new Date().toISOString(), requestId: 'anomaly-detection', clientIP, endpoint: 'multiple-endpoints', method: 'MULTIPLE', details: { anomalyType: 'high_suspicious_activity', count: suspiciousEvents.length, timeWindow: '10 minutes', }, }); } return anomalies; } } const securityEventTracker = new SecurityEventTracker(); exports.securityEventTracker = securityEventTracker; /** * Check for suspicious patterns in request data */ const detectSuspiciousPatterns = (data, patterns = DEFAULT_SUSPICIOUS_PATTERNS) => { const detected = []; for (const [type, regexList] of Object.entries(patterns)) { for (const regex of regexList || []) { if (regex.test(data)) { detected.push({ type, pattern: regex.source }); } } } return detected; }; /** * Sanitize data for logging (remove sensitive information) */ const sanitizeForLogging = (data, maxSize = 1024) => { if (typeof data === 'string') { return data.length > maxSize ? data.substring(0, maxSize) + '...[truncated]' : data; } try { const jsonStr = JSON.stringify(data); return jsonStr.length > maxSize ? jsonStr.substring(0, maxSize) + '...[truncated]' : jsonStr; } catch { return '[unserializable data]'; } }; /** * Extract client information from request */ const extractClientInfo = (context) => ({ clientIP: context.req.ip || (Array.isArray(context.req.headers?.['x-forwarded-for']) ? context.req.headers['x-forwarded-for'][0] : context.req.headers?.['x-forwarded-for']) || 'unknown', userAgent: context.req.headers?.['user-agent'], userId: context.user && typeof context.user === 'object' && 'sub' in context.user ? context.user.sub : undefined, }); /** * Security Audit Middleware * Provides comprehensive security event logging and monitoring * * @template TBody - The type of the request body payload (preserves type chain) * @template TUser - The type of the authenticated user (preserves type chain) */ class SecurityAuditMiddleware { options; constructor(options = {}) { this.options = { logRequests: options.logRequests ?? false, logResponses: options.logResponses ?? false, logBodies: options.logBodies ?? false, maxBodyLogSize: options.maxBodyLogSize ?? 1024, excludeHeaders: [ ...DEFAULT_EXCLUDE_HEADERS, ...(options.excludeHeaders || []), ], enableAnomalyDetection: options.enableAnomalyDetection ?? true, onSecurityEvent: options.onSecurityEvent, suspiciousPatterns: { ...DEFAULT_SUSPICIOUS_PATTERNS, ...options.suspiciousPatterns, }, }; } async before(context) { const startTime = Date.now(); const { clientIP, userAgent, userId } = extractClientInfo(context); // Store start time for performance monitoring context.businessData.set('audit_start_time', startTime); context.businessData.set('audit_client_info', { clientIP, userAgent, userId, }); // Log incoming request if enabled if (this.options.logRequests) { const requestData = { method: context.req.method, url: context.req.url || context.req.path, headers: this.sanitizeHeaders(context.req.headers || {}), clientIP, userAgent, userId, }; if (this.options.logBodies && context.req.body) { requestData.body = sanitizeForLogging(context.req.body, this.options.maxBodyLogSize); } logger_1.logger.info('Incoming request', requestData); } // Check for suspicious patterns in URL and headers const url = context.req.url || context.req.path || ''; const suspiciousInUrl = detectSuspiciousPatterns(url, this.options.suspiciousPatterns); if (suspiciousInUrl.length > 0) { await this.logSecurityEvent({ type: 'INJECTION_ATTEMPT', severity: 'HIGH', timestamp: new Date().toISOString(), requestId: context.requestId, clientIP, userAgent, userId, endpoint: url, method: context.req.method || 'UNKNOWN', details: { suspiciousPatterns: suspiciousInUrl, location: 'url', }, }); } // Check request body for suspicious patterns if (context.req.body && typeof context.req.body === 'string') { const suspiciousInBody = detectSuspiciousPatterns(context.req.body, this.options.suspiciousPatterns); if (suspiciousInBody.length > 0) { await this.logSecurityEvent({ type: 'INJECTION_ATTEMPT', severity: 'HIGH', timestamp: new Date().toISOString(), requestId: context.requestId, clientIP, userAgent, userId, endpoint: url, method: context.req.method || 'UNKNOWN', details: { suspiciousPatterns: suspiciousInBody, location: 'body', }, }); } } // Run anomaly detection if (this.options.enableAnomalyDetection) { const anomalies = securityEventTracker.detectAnomalies(clientIP); for (const anomaly of anomalies) { await this.logSecurityEvent(anomaly); } } } async after(context) { const startTime = context.businessData.get('audit_start_time'); const clientInfo = context.businessData.get('audit_client_info'); const duration = Date.now() - startTime; // Log response if enabled if (this.options.logResponses) { const responseData = { statusCode: context.res.statusCode, duration: `${duration}ms`, ...clientInfo, }; if (this.options.logBodies && context.responseData) { responseData.responseBody = sanitizeForLogging(context.responseData, this.options.maxBodyLogSize); } logger_1.logger.info('Outgoing response', responseData); } } async onError(error, context) { const clientInfo = context.businessData.get('audit_client_info'); if (!clientInfo) return; const { clientIP, userAgent, userId } = clientInfo; const url = context.req.url || context.req.path || ''; let eventType = 'SUSPICIOUS_REQUEST'; let severity = 'MEDIUM'; // Classify error types if (error instanceof core_1.HttpError) { switch (error.status) { case 401: eventType = 'AUTHENTICATION_FAILURE'; severity = 'MEDIUM'; break; case 403: eventType = 'AUTHORIZATION_FAILURE'; severity = 'HIGH'; break; case 400: eventType = 'INVALID_INPUT'; severity = 'LOW'; break; case 429: eventType = 'RATE_LIMIT_EXCEEDED'; severity = 'HIGH'; break; default: eventType = 'SUSPICIOUS_REQUEST'; severity = 'MEDIUM'; } } await this.logSecurityEvent({ type: eventType, severity, timestamp: new Date().toISOString(), requestId: context.requestId, clientIP, userAgent, userId, endpoint: url, method: context.req.method || 'UNKNOWN', details: { error: error.message, errorType: error.constructor.name, statusCode: error instanceof core_1.HttpError ? error.status : undefined, }, }); } async logSecurityEvent(event) { // Add to tracker for anomaly detection if (this.options.enableAnomalyDetection) { securityEventTracker.addEvent(event); } // Log the security event logger_1.logger.warn('Security event detected', event); // Call custom handler if provided if (this.options.onSecurityEvent) { try { await this.options.onSecurityEvent(event); } catch (handlerError) { logger_1.logger.error('Security event handler failed', { error: handlerError instanceof Error ? handlerError.message : 'Unknown error', originalEvent: event, }); } } } sanitizeHeaders(headers) { const sanitized = {}; for (const [key, value] of Object.entries(headers)) { if (this.options.excludeHeaders.includes(key.toLowerCase())) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = value; } } return sanitized; } } exports.SecurityAuditMiddleware = SecurityAuditMiddleware; /** * Security Audit Middleware Factory * @param options Security audit configuration * @returns BaseMiddleware */ const securityAudit = (options = {}) => new SecurityAuditMiddleware(options); exports.securityAudit = securityAudit; /** * Predefined security audit configurations */ exports.SecurityAuditPresets = { /** * Full monitoring with detailed logging */ COMPREHENSIVE: { logRequests: true, logResponses: true, logBodies: false, // Be careful with sensitive data enableAnomalyDetection: true, }, /** * Security events only */ SECURITY_ONLY: { logRequests: false, logResponses: false, logBodies: false, enableAnomalyDetection: true, }, /** * Development mode with full logging */ DEVELOPMENT: { logRequests: true, logResponses: true, logBodies: true, enableAnomalyDetection: false, }, }; //# sourceMappingURL=securityAuditMiddleware.js.map