import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { SecurityManager } from '../SecurityManager'; import { SecurityConfigBuilder, DEFAULT_SECURITY_CONFIG } from '../SecurityConfig'; import { CryptoUtils } from '../CryptoUtils'; describe('SecurityManager - Comprehensive Tests', () => { let securityManager: SecurityManager; let consoleSpy: any; beforeEach(() => { const config = new SecurityConfigBuilder().build(); securityManager = new SecurityManager(config); // Mock console methods to capture log output consoleSpy = { info: vi.spyOn(console, 'info').mockImplementation(() => {}), warn: vi.spyOn(console, 'warn').mockImplementation(() => {}), error: vi.spyOn(console, 'error').mockImplementation(() => {}), debug: vi.spyOn(console, 'debug').mockImplementation(() => {}) }; }); afterEach(() => { vi.restoreAllMocks(); }); describe('Input Validation', () => { it('should validate input with security policies', () => { const result = securityManager.validateInput('test@example.com', ['email']); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should reject invalid input and audit the failure', () => { const result = securityManager.validateInput('', ['noEmpty']); expect(result.valid).toBe(false); expect(result.errors).toContain('Value cannot be empty'); // Check that the failure was audited const auditLog = securityManager.getAuditLog(); expect(auditLog.length).toBeGreaterThan(0); expect(auditLog[auditLog.length - 1].event).toBe('input_validation_failed'); }); it('should validate complex input with multiple rules', () => { const validResult = securityManager.validateInput('user123', ['alphanumericOnly', 'noEmpty']); expect(validResult.valid).toBe(true); const invalidResult = securityManager.validateInput('user@domain', ['alphanumericOnly', 'noEmpty']); expect(invalidResult.valid).toBe(false); }); it('should handle SQL injection attempts', () => { const maliciousInput = "'; DROP TABLE users; --"; const result = securityManager.validateInput(maliciousInput, ['noSqlInjection']); expect(result.valid).toBe(false); expect(result.errors).toContain('Potentially dangerous SQL patterns detected'); const auditLog = securityManager.getAuditLog(); const lastEntry = auditLog[auditLog.length - 1]; expect(lastEntry.event).toBe('input_validation_failed'); expect(lastEntry.details.errors).toContain('Potentially dangerous SQL patterns detected'); }); it('should handle XSS attempts', () => { const xssInput = ''; const result = securityManager.validateInput(xssInput, ['noXss']); expect(result.valid).toBe(false); expect(result.errors).toContain('Potentially dangerous XSS patterns detected'); }); it('should ensure constant-time validation', async () => { const startTime = process.hrtime.bigint(); securityManager.validateInput('test', ['email']); const endTime = process.hrtime.bigint(); const elapsed = endTime - startTime; expect(elapsed).toBeGreaterThan(BigInt(0)); }); }); describe('Cryptographic Operations', () => { it('should perform constant-time string comparison', () => { expect(securityManager.constantTimeCompare('test', 'test')).toBe(true); expect(securityManager.constantTimeCompare('test', 'fail')).toBe(false); expect(securityManager.constantTimeCompare('', '')).toBe(true); expect(securityManager.constantTimeCompare('short', 'verylongstring')).toBe(false); }); it('should generate secure tokens', () => { const token1 = securityManager.generateSecureToken(); const token2 = securityManager.generateSecureToken(); expect(token1).not.toBe(token2); expect(typeof token1).toBe('string'); expect(token1.length).toBeGreaterThan(0); }); it('should verify HMAC signatures with timing protection', async () => { const data = 'test data'; const secret = 'test secret'; const signature = CryptoUtils.hmac(data, secret); const startTime = process.hrtime.bigint(); const result = securityManager.verifyHmac(data, signature, secret); const endTime = process.hrtime.bigint(); expect(result).toBe(true); const elapsed = endTime - startTime; expect(elapsed).toBeGreaterThan(BigInt(0)); }); it('should reject invalid HMAC signatures', () => { const data = 'test data'; const secret = 'test secret'; const validSignature = CryptoUtils.hmac(data, secret); expect(securityManager.verifyHmac('wrong data', validSignature, secret)).toBe(false); expect(securityManager.verifyHmac(data, 'wrong signature', secret)).toBe(false); expect(securityManager.verifyHmac(data, validSignature, 'wrong secret')).toBe(false); }); }); describe('Security Event Auditing', () => { it('should audit security events with sanitized data', () => { const eventData = { userId: 'user123', action: 'login_attempt', password: 'secret123', ip: '192.168.1.1' }; securityManager.auditSecurityEvent('authentication_event', eventData); const auditLog = securityManager.getAuditLog(); expect(auditLog.length).toBe(1); const auditEntry = auditLog[0]; expect(auditEntry.event).toBe('authentication_event'); expect(auditEntry.details.userId).toBe('user123'); expect(auditEntry.details.action).toBe('login_attempt'); expect(auditEntry.details.password).toBe('[REDACTED]'); expect(auditEntry.details.ip).toBe('192.168.1.1'); expect(auditEntry.timestamp).toBeInstanceOf(Date); }); it('should handle nested sensitive data in audit logs', () => { const nestedEventData = { user: { id: 'user123', credentials: { password: 'secret123', token: 'jwt-token-here' } }, session: { id: 'session123', secret: 'session-secret' } }; securityManager.auditSecurityEvent('complex_event', nestedEventData); const auditLog = securityManager.getAuditLog(); const auditEntry = auditLog[auditLog.length - 1]; expect(auditEntry.details.user.id).toBe('user123'); expect(auditEntry.details.user.credentials.password).toBe('[REDACTED]'); expect(auditEntry.details.user.credentials.token).toBe('[REDACTED]'); expect(auditEntry.details.session.id).toBe('session123'); expect(auditEntry.details.session.secret).toBe('[REDACTED]'); }); it('should respect audit configuration', () => { const disabledAuditConfig = new SecurityConfigBuilder() .withAudit({ enabled: false }) .build(); const noAuditManager = new SecurityManager(disabledAuditConfig); noAuditManager.auditSecurityEvent('test_event', { data: 'test' }); expect(noAuditManager.getAuditLog()).toHaveLength(0); }); it('should limit audit log size', () => { const limitedConfig = new SecurityConfigBuilder() .withAudit({ maxEntries: 3 }) .build(); const limitedManager = new SecurityManager(limitedConfig); // Add more entries than the limit for (let i = 0; i < 5; i++) { limitedManager.auditSecurityEvent(`event_${i}`, { index: i }); } const auditLog = limitedManager.getAuditLog(); expect(auditLog.length).toBe(3); expect(auditLog[0].event).toBe('event_2'); // Oldest entries removed expect(auditLog[2].event).toBe('event_4'); }); it('should clear audit log', () => { securityManager.auditSecurityEvent('test_event', { data: 'test' }); expect(securityManager.getAuditLog()).toHaveLength(1); securityManager.clearAuditLog(); expect(securityManager.getAuditLog()).toHaveLength(0); }); }); describe('Timing Attack Detection', () => { it('should detect consistent timing patterns', () => { // Simulate consistent timing (potential timing attack) const consistentTimes = [100, 101, 99, 100, 102, 101, 100, 99, 101, 100]; const isAttack = securityManager.detectTimingAttack(consistentTimes); expect(isAttack).toBe(true); }); it('should not flag normal timing variations', () => { // Simulate normal timing variations const normalTimes = [50, 150, 75, 200, 125, 80, 300, 90, 175, 110]; const isAttack = securityManager.detectTimingAttack(normalTimes); expect(isAttack).toBe(false); }); it('should require minimum samples for detection', () => { const fewSamples = [100, 101, 99]; const isAttack = securityManager.detectTimingAttack(fewSamples); expect(isAttack).toBe(false); }); it('should handle edge cases in timing detection', () => { expect(securityManager.detectTimingAttack([])).toBe(false); expect(securityManager.detectTimingAttack([100])).toBe(false); }); it('should use configurable timing attack threshold', () => { const customConfig = new SecurityConfigBuilder() .withSecurityPolicy({ timingAttackThreshold: 5, timingVarianceThreshold: 0.05 }) .build(); const customManager = new SecurityManager(customConfig); const consistentTimes = [100, 101, 99, 100, 102]; expect(customManager.detectTimingAttack(consistentTimes)).toBe(true); }); }); describe('Security Metrics', () => { it('should track security metrics', () => { // Generate some security events securityManager.auditSecurityEvent('input_validation_failed', { field: 'email' }); securityManager.auditSecurityEvent('timing_attack_detected', { endpoint: '/api/login' }); securityManager.auditSecurityEvent('input_validation_failed', { field: 'password' }); securityManager.auditSecurityEvent('timing_attack_detected', { endpoint: '/api/auth' }); securityManager.auditSecurityEvent('other_event', { data: 'test' }); const metrics = securityManager.getSecurityMetrics(); expect(metrics.auditLogSize).toBe(5); expect(metrics.inputValidationFailures).toBe(2); expect(metrics.timingAttackDetections).toBe(2); }); it('should handle empty audit log metrics', () => { const metrics = securityManager.getSecurityMetrics(); expect(metrics.auditLogSize).toBe(0); expect(metrics.inputValidationFailures).toBe(0); expect(metrics.timingAttackDetections).toBe(0); }); }); describe('Configuration Integration', () => { it('should use custom sensitive field configuration', () => { const customConfig = new SecurityConfigBuilder() .addSensitiveFields(['customSecret', 'internalKey']) .build(); const customManager = new SecurityManager(customConfig); const eventData = { normalField: 'visible', customSecret: 'hidden123', internalKey: 'internal456', password: 'alsoHidden' }; customManager.auditSecurityEvent('test_event', eventData); const auditLog = customManager.getAuditLog(); const auditEntry = auditLog[0]; expect(auditEntry.details.normalField).toBe('visible'); expect(auditEntry.details.customSecret).toBe('[REDACTED]'); expect(auditEntry.details.internalKey).toBe('[REDACTED]'); expect(auditEntry.details.password).toBe('[REDACTED]'); }); it('should use custom timing thresholds', () => { const customConfig = new SecurityConfigBuilder() .withSecurityPolicy({ minValidationTime: BigInt(5000000), // 5ms minVerificationTime: BigInt(10000000) // 10ms }) .build(); const customManager = new SecurityManager(customConfig); // These operations should be padded to meet minimum time requirements const start1 = process.hrtime.bigint(); customManager.validateInput('test', ['email']); const end1 = process.hrtime.bigint(); const start2 = process.hrtime.bigint(); const signature = CryptoUtils.hmac('data', 'secret'); customManager.verifyHmac('data', signature, 'secret'); const end2 = process.hrtime.bigint(); // Timing should meet minimum requirements (though setTimeout may not be exact) expect(end1 - start1).toBeGreaterThan(BigInt(0)); expect(end2 - start2).toBeGreaterThan(BigInt(0)); }); }); describe('Error Handling and Edge Cases', () => { it('should handle null and undefined inputs gracefully', () => { expect(() => securityManager.validateInput(null, ['email'])).not.toThrow(); expect(() => securityManager.validateInput(undefined, ['email'])).not.toThrow(); const nullResult = securityManager.validateInput(null, ['email']); expect(nullResult.valid).toBe(false); }); it('should handle empty rule arrays', () => { const result = securityManager.validateInput('test', []); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); }); it('should handle constant-time comparison with null values', () => { expect(() => securityManager.constantTimeCompare(null as any, 'test')).not.toThrow(); expect(securityManager.constantTimeCompare(null as any, 'test')).toBe(false); }); it('should handle HMAC verification with invalid inputs', () => { expect(securityManager.verifyHmac('', '', '')).toBe(false); expect(securityManager.verifyHmac(null as any, null as any, null as any)).toBe(false); }); it('should sanitize audit data with circular references', () => { const circularData: any = { name: 'test' }; circularData.self = circularData; circularData.password = 'secret123'; expect(() => { securityManager.auditSecurityEvent('circular_test', circularData); }).not.toThrow(); const auditLog = securityManager.getAuditLog(); const auditEntry = auditLog[auditLog.length - 1]; expect(auditEntry.details.name).toBe('test'); expect(auditEntry.details.password).toBe('[REDACTED]'); }); it('should handle primitive values in audit data', () => { securityManager.auditSecurityEvent('primitive_test', 'simple string'); securityManager.auditSecurityEvent('number_test', 12345); securityManager.auditSecurityEvent('boolean_test', true); securityManager.auditSecurityEvent('null_test', null); const auditLog = securityManager.getAuditLog(); expect(auditLog.length).toBe(4); expect(auditLog[auditLog.length - 4].details).toBe('simple string'); expect(auditLog[auditLog.length - 3].details).toBe(12345); expect(auditLog[auditLog.length - 2].details).toBe(true); expect(auditLog[auditLog.length - 1].details).toBe(null); }); }); describe('Integration with Security Components', () => { it('should integrate with SecureLogger for audit events', () => { securityManager.auditSecurityEvent('integration_test', { userId: 'user123', action: 'test_action' }); // Check that the logger was called (via console spy) expect(consoleSpy.warn).toHaveBeenCalled(); const logOutput = consoleSpy.warn.mock.calls[0][0]; expect(logOutput).toContain('Security Event: integration_test'); }); it('should use InputValidator for input validation', () => { // Test that the security manager properly delegates to InputValidator const htmlInput = ''; const result = securityManager.validateInput(htmlInput, ['noXss']); expect(result.valid).toBe(false); expect(result.errors).toContain('Potentially dangerous XSS patterns detected'); }); it('should use CryptoUtils for cryptographic operations', () => { // Test that constant-time comparison delegates to CryptoUtils const cryptoSpy = vi.spyOn(CryptoUtils, 'constantTimeCompare'); securityManager.constantTimeCompare('test1', 'test2'); expect(cryptoSpy).toHaveBeenCalledWith('test1', 'test2'); cryptoSpy.mockRestore(); }); }); describe('Performance and Scalability', () => { it('should handle large audit logs efficiently', () => { const startTime = Date.now(); // Add many audit entries for (let i = 0; i < 1000; i++) { securityManager.auditSecurityEvent(`batch_event_${i}`, { index: i, data: `test_data_${i}`, sensitive: `secret_${i}` }); } const endTime = Date.now(); const duration = endTime - startTime; // Should complete in reasonable time (adjust threshold as needed) expect(duration).toBeLessThan(5000); // 5 seconds const metrics = securityManager.getSecurityMetrics(); expect(metrics.auditLogSize).toBe(1000); }); it('should maintain performance with complex audit data', () => { const complexData = { level1: { level2: { level3: { level4: { level5: { deepSecret: 'hidden_value', normalData: 'visible_value' } } } } }, array: [ { password: 'secret1', username: 'user1' }, { password: 'secret2', username: 'user2' }, { password: 'secret3', username: 'user3' } ] }; const startTime = Date.now(); securityManager.auditSecurityEvent('complex_audit', complexData); const endTime = Date.now(); expect(endTime - startTime).toBeLessThan(100); // Should be fast const auditLog = securityManager.getAuditLog(); const auditEntry = auditLog[auditLog.length - 1]; // Verify deep sanitization worked expect(auditEntry.details.level1.level2.level3.level4.level5.deepSecret).toBe('[REDACTED]'); expect(auditEntry.details.level1.level2.level3.level4.level5.normalData).toBe('visible_value'); expect(auditEntry.details.array[0].password).toBe('[REDACTED]'); expect(auditEntry.details.array[0].username).toBe('user1'); }); }); });