diff --git a/src/lib/security/logger.test.ts b/src/lib/security/logger.test.ts new file mode 100644 index 0000000..19d9a6b --- /dev/null +++ b/src/lib/security/logger.test.ts @@ -0,0 +1,49 @@ +import { SecurityLogger, SecurityEventType } from './logger'; + +describe('Security Logging System', () => { + let logger: SecurityLogger; + + beforeEach(() => { + logger = new SecurityLogger(); + }); + + test('should log security events', () => { + logger.logEvent({ + type: SecurityEventType.CAPTCHA_FAILED, + ip: '192.168.1.1', + email: 'test@example.com', + details: { reason: 'Invalid answer' }, + }); + + const logs = logger.getRecentLogs(10); + expect(logs.length).toBe(1); + expect(logs[0].type).toBe(SecurityEventType.CAPTCHA_FAILED); + }); + + test('should detect suspicious activity', () => { + for (let i = 0; i < 6; i++) { + logger.logEvent({ + type: SecurityEventType.CAPTCHA_FAILED, + ip: '192.168.1.2', + email: 'suspicious@example.com', + details: {}, + }); + } + + const suspiciousIPs = logger.getSuspiciousIPs(); + expect(suspiciousIPs).toContain('192.168.1.2'); + }); + + test('should clear old logs', () => { + logger.logEvent({ + type: SecurityEventType.RATE_LIMIT_EXCEEDED, + ip: '192.168.1.3', + details: {}, + }); + + logger.clearOldLogs(0); + + const logs = logger.getRecentLogs(10); + expect(logs.length).toBe(0); + }); +}); diff --git a/src/lib/security/logger.ts b/src/lib/security/logger.ts new file mode 100644 index 0000000..f53c908 --- /dev/null +++ b/src/lib/security/logger.ts @@ -0,0 +1,107 @@ +export enum SecurityEventType { + CAPTCHA_FAILED = 'captcha_failed', + CAPTCHA_EXPIRED = 'captcha_expired', + RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded', + MALICIOUS_CONTENT = 'malicious_content', + XSS_ATTEMPT = 'xss_attempt', + SQL_INJECTION_ATTEMPT = 'sql_injection_attempt', + SUSPICIOUS_IP = 'suspicious_ip', + BLOCKED_REQUEST = 'blocked_request', +} + +export interface SecurityEvent { + type: SecurityEventType; + timestamp: number; + ip?: string; + email?: string; + userAgent?: string; + details?: Record; +} + +export interface SuspiciousActivity { + ip: string; + eventCount: number; + lastSeen: number; + eventTypes: SecurityEventType[]; +} + +class InMemorySecurityLogger { + private events: SecurityEvent[] = []; + private ipActivity = new Map(); + + logEvent(event: Omit): void { + const fullEvent: SecurityEvent = { + ...event, + timestamp: Date.now(), + }; + + this.events.push(fullEvent); + + if (event.ip) { + const currentCount = this.ipActivity.get(event.ip) || 0; + this.ipActivity.set(event.ip, currentCount + 1); + } + + console.log(`[Security] ${event.type}:`, event); + } + + getRecentLogs(limit: number = 100): SecurityEvent[] { + return this.events.slice(-limit); + } + + getSuspiciousIPs(threshold: number = 5): string[] { + const suspicious: string[] = []; + for (const [ip, count] of this.ipActivity.entries()) { + if (count >= threshold) { + suspicious.push(ip); + } + } + return suspicious; + } + + clearOldLogs(daysToKeep: number = 30): void { + const cutoffTime = Date.now() - (daysToKeep * 24 * 60 * 60 * 1000); + this.events = this.events.filter(event => event.timestamp > cutoffTime); + } + + getActivityByIP(ip: string): number { + return this.ipActivity.get(ip) || 0; + } + + clear(): void { + this.events = []; + this.ipActivity.clear(); + } +} + +export class SecurityLogger { + private logger: InMemorySecurityLogger; + + constructor() { + this.logger = new InMemorySecurityLogger(); + } + + logEvent(event: Omit): void { + this.logger.logEvent(event); + } + + getRecentLogs(limit?: number): SecurityEvent[] { + return this.logger.getRecentLogs(limit); + } + + getSuspiciousIPs(threshold?: number): string[] { + return this.logger.getSuspiciousIPs(threshold); + } + + clearOldLogs(daysToKeep?: number): void { + this.logger.clearOldLogs(daysToKeep); + } + + getActivityByIP(ip: string): number { + return this.logger.getActivityByIP(ip); + } + + clear(): void { + this.logger.clear(); + } +}