UNPKG

@ordojs/security

Version:

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

403 lines (342 loc) 12.4 kB
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { RuntimeSecurityMonitor } from './runtime-monitor'; describe('RuntimeSecurityMonitor', () => { let monitor: RuntimeSecurityMonitor; beforeEach(() => { monitor = new RuntimeSecurityMonitor({ enableLogging: false, // Disable logging for tests maxEvents: 100, }); }); describe('recordEvent', () => { it('should record a security event', () => { const event = { type: 'xss_attempt' as const, severity: 'high' as const, source: { ip: '192.168.1.1', userAgent: 'test-agent' }, details: { payload: '<script>alert("xss")</script>' }, blocked: true, }; monitor.recordEvent(event); const events = monitor.getEvents(); expect(events).toHaveLength(1); expect(events[0]).toMatchObject(event); expect(events[0].id).toBeDefined(); expect(events[0].timestamp).toBeInstanceOf(Date); }); it('should maintain max events limit', () => { const smallMonitor = new RuntimeSecurityMonitor({ maxEvents: 2, enableLogging: false }); // Add 3 events for (let i = 0; i < 3; i++) { smallMonitor.recordEvent({ type: 'xss_attempt', severity: 'low', source: { ip: '192.168.1.1' }, details: { index: i }, blocked: false, }); } const events = smallMonitor.getEvents(); expect(events).toHaveLength(2); // The events array should contain the last 2 events (indices 1 and 2) // getEvents() returns them in reverse chronological order (most recent first) const indices = events.map(e => e.details.index); expect(indices).toContain(1); expect(indices).toContain(2); expect(indices).not.toContain(0); // First event should be removed }); }); describe('specialized recording methods', () => { it('should record XSS attempts', () => { monitor.recordXSSAttempt({ payload: '<script>alert("xss")</script>', source: { ip: '192.168.1.1' }, blocked: true, context: 'user input field', }); const events = monitor.getEvents(); expect(events).toHaveLength(1); expect(events[0].type).toBe('xss_attempt'); expect(events[0].severity).toBe('high'); expect(events[0].blocked).toBe(true); expect(events[0].details.payload).toBe('<script>alert("xss")</script>'); expect(events[0].details.context).toBe('user input field'); }); it('should record CSRF violations', () => { monitor.recordCSRFViolation({ expectedToken: 'abc123', receivedToken: 'def456', source: { ip: '192.168.1.1', sessionId: 'session123' }, endpoint: '/api/transfer', }); const events = monitor.getEvents(); expect(events).toHaveLength(1); expect(events[0].type).toBe('csrf_violation'); expect(events[0].severity).toBe('high'); expect(events[0].blocked).toBe(true); expect(events[0].details.endpoint).toBe('/api/transfer'); }); it('should record injection attempts', () => { monitor.recordInjectionAttempt({ type: 'sql', payload: "'; DROP TABLE users; --", source: { ip: '192.168.1.1' }, blocked: true, query: 'SELECT * FROM users WHERE id = ?', }); const events = monitor.getEvents(); expect(events).toHaveLength(1); expect(events[0].type).toBe('injection_attempt'); expect(events[0].severity).toBe('critical'); expect(events[0].details.injectionType).toBe('sql'); expect(events[0].details.payload).toBe("'; DROP TABLE users; --"); }); it('should record rate limit exceeded', () => { monitor.recordRateLimitExceeded({ limit: 100, current: 150, window: '1h', source: { ip: '192.168.1.1' }, endpoint: '/api/data', }); const events = monitor.getEvents(); expect(events).toHaveLength(1); expect(events[0].type).toBe('rate_limit_exceeded'); expect(events[0].severity).toBe('medium'); expect(events[0].details.limit).toBe(100); expect(events[0].details.current).toBe(150); }); it('should record auth failures', () => { monitor.recordAuthFailure({ reason: 'invalid_credentials', source: { ip: '192.168.1.1' }, username: 'testuser', endpoint: '/login', }); const events = monitor.getEvents(); expect(events).toHaveLength(1); expect(events[0].type).toBe('auth_failure'); expect(events[0].severity).toBe('high'); expect(events[0].details.reason).toBe('invalid_credentials'); expect(events[0].details.username).toBe('testuser'); }); it('should record suspicious activity with risk-based severity', () => { // High risk activity monitor.recordSuspiciousActivity({ activity: 'Multiple failed login attempts from different IPs', riskScore: 85, source: { ip: '192.168.1.1' }, context: { attempts: 10, timeWindow: '5m' }, }); // Low risk activity monitor.recordSuspiciousActivity({ activity: 'Unusual user agent string', riskScore: 25, source: { ip: '192.168.1.2' }, }); const events = monitor.getEvents(); expect(events).toHaveLength(2); const highRiskEvent = events.find(e => e.details.riskScore === 85); const lowRiskEvent = events.find(e => e.details.riskScore === 25); expect(highRiskEvent?.severity).toBe('critical'); expect(highRiskEvent?.blocked).toBe(true); expect(lowRiskEvent?.severity).toBe('low'); expect(lowRiskEvent?.blocked).toBe(false); }); }); describe('getMetrics', () => { beforeEach(() => { // Add some test events monitor.recordXSSAttempt({ payload: '<script>alert(1)</script>', source: { ip: '192.168.1.1' }, blocked: true, }); monitor.recordCSRFViolation({ source: { ip: '192.168.1.1' }, endpoint: '/api/test', }); monitor.recordInjectionAttempt({ type: 'sql', payload: 'DROP TABLE', source: { ip: '192.168.1.2' }, blocked: false, }); }); it('should calculate correct metrics', () => { const metrics = monitor.getMetrics(); expect(metrics.totalEvents).toBe(3); expect(metrics.blockedEvents).toBe(2); expect(metrics.eventsByType).toEqual({ xss_attempt: 1, csrf_violation: 1, injection_attempt: 1, }); expect(metrics.eventsBySeverity).toEqual({ high: 2, critical: 1, }); expect(metrics.topSources).toHaveLength(2); expect(metrics.topSources[0].ip).toBe('192.168.1.1'); expect(metrics.topSources[0].count).toBe(2); }); it('should include time range in metrics', () => { const metrics = monitor.getMetrics(); expect(metrics.timeRange.start).toBeInstanceOf(Date); expect(metrics.timeRange.end).toBeInstanceOf(Date); expect(metrics.timeRange.end.getTime()).toBeGreaterThanOrEqual( metrics.timeRange.start.getTime() ); }); }); describe('getEvents with filters', () => { beforeEach(() => { monitor.recordXSSAttempt({ payload: 'test1', source: { ip: '192.168.1.1' }, blocked: true, }); monitor.recordCSRFViolation({ source: { ip: '192.168.1.2' }, endpoint: '/test', }); monitor.recordInjectionAttempt({ type: 'sql', payload: 'test2', source: { ip: '192.168.1.3' }, blocked: false, }); }); it('should filter by event type', () => { const xssEvents = monitor.getEvents({ type: 'xss_attempt' }); expect(xssEvents).toHaveLength(1); expect(xssEvents[0].type).toBe('xss_attempt'); }); it('should filter by severity', () => { const criticalEvents = monitor.getEvents({ severity: 'critical' }); expect(criticalEvents).toHaveLength(1); expect(criticalEvents[0].type).toBe('injection_attempt'); }); it('should filter by date', () => { const now = new Date(); const oneMinuteAgo = new Date(now.getTime() - 60000); const recentEvents = monitor.getEvents({ since: oneMinuteAgo }); expect(recentEvents).toHaveLength(3); // All events are recent const futureEvents = monitor.getEvents({ since: new Date(now.getTime() + 60000) }); expect(futureEvents).toHaveLength(0); }); it('should limit results', () => { const limitedEvents = monitor.getEvents({ limit: 2 }); expect(limitedEvents).toHaveLength(2); }); it('should return events in reverse chronological order', () => { const events = monitor.getEvents(); expect(events).toHaveLength(3); // Events should be ordered by timestamp descending for (let i = 1; i < events.length; i++) { expect(events[i - 1].timestamp.getTime()).toBeGreaterThanOrEqual( events[i].timestamp.getTime() ); } }); }); describe('clearEvents', () => { it('should clear all events', () => { monitor.recordXSSAttempt({ payload: 'test', source: { ip: '192.168.1.1' }, blocked: true, }); expect(monitor.getEvents()).toHaveLength(1); monitor.clearEvents(); expect(monitor.getEvents()).toHaveLength(0); expect(monitor.getMetrics().totalEvents).toBe(0); }); }); describe('exportEvents', () => { beforeEach(() => { monitor.recordXSSAttempt({ payload: '<script>test</script>', source: { ip: '192.168.1.1', userAgent: 'test-agent' }, blocked: true, }); }); it('should export events as JSON', () => { const jsonExport = monitor.exportEvents('json'); const events = JSON.parse(jsonExport); expect(Array.isArray(events)).toBe(true); expect(events).toHaveLength(1); expect(events[0].type).toBe('xss_attempt'); }); it('should export events as CSV', () => { const csvExport = monitor.exportEvents('csv'); const lines = csvExport.split('\n'); expect(lines[0]).toContain('id,type,severity,timestamp,blocked,source_ip,user_agent,details'); expect(lines[1]).toContain('xss_attempt,high'); expect(lines[1]).toContain('192.168.1.1'); expect(lines[1]).toContain('test-agent'); }); }); describe('alert thresholds', () => { it('should trigger alerts when thresholds are exceeded', () => { const alertCallback = vi.fn(); const alertMonitor = new RuntimeSecurityMonitor({ alertThresholds: { critical: 1, high: 2, medium: 3 }, onAlert: alertCallback, }); // First critical event should trigger alert alertMonitor.recordInjectionAttempt({ type: 'sql', payload: 'DROP TABLE', source: { ip: '192.168.1.1' }, blocked: true, }); expect(alertCallback).toHaveBeenCalledTimes(1); expect(alertCallback).toHaveBeenCalledWith( expect.objectContaining({ type: 'injection_attempt', severity: 'critical', }) ); }); it('should not trigger alerts below threshold', () => { const alertCallback = vi.fn(); const alertMonitor = new RuntimeSecurityMonitor({ alertThresholds: { critical: 1, high: 2, medium: 3 }, onAlert: alertCallback, }); // First high severity event should not trigger alert (threshold is 2) alertMonitor.recordXSSAttempt({ payload: 'test', source: { ip: '192.168.1.1' }, blocked: true, }); expect(alertCallback).not.toHaveBeenCalled(); }); }); describe('metrics updates', () => { it('should call metrics update callback when configured', () => { const metricsCallback = vi.fn(); const metricsMonitor = new RuntimeSecurityMonitor({ onMetricsUpdate: metricsCallback, }); metricsMonitor.recordXSSAttempt({ payload: 'test', source: { ip: '192.168.1.1' }, blocked: true, }); expect(metricsCallback).toHaveBeenCalledTimes(1); expect(metricsCallback).toHaveBeenCalledWith( expect.objectContaining({ totalEvents: 1, blockedEvents: 1, }) ); }); }); });