diff --git a/.tests/behavioral-middleware.test.js b/.tests/behavioral-middleware.test.js deleted file mode 100644 index 491847b..0000000 --- a/.tests/behavioral-middleware.test.js +++ /dev/null @@ -1,411 +0,0 @@ -import { jest } from '@jest/globals'; - -// Mock dependencies -jest.unstable_mockModule('../dist/utils/behavioral-detection.js', () => ({ - behavioralDetection: { - config: { enabled: true, Responses: {} }, - isBlocked: jest.fn(), - getRateLimit: jest.fn(), - analyzeRequest: jest.fn() - } -})); - -jest.unstable_mockModule('../dist/utils/network.js', () => ({ - getRealIP: jest.fn() -})); - -jest.unstable_mockModule('../dist/utils/logs.js', () => ({ - plugin: jest.fn(), - error: jest.fn() -})); - -// Import the modules after mocking -const BehavioralDetectionMiddleware = (await import('../dist/utils/behavioral-middleware.js')).default; -const { behavioralDetection } = await import('../dist/utils/behavioral-detection.js'); -const { getRealIP } = await import('../dist/utils/network.js'); -const logs = await import('../dist/utils/logs.js'); - -describe('Behavioral Middleware', () => { - let mockReq, mockRes, mockNext; - let activeTimeouts = []; - let activeImmediates = []; - - // Track async operations for cleanup - const originalSetTimeout = global.setTimeout; - const originalSetImmediate = global.setImmediate; - - global.setTimeout = (fn, delay, ...args) => { - const id = originalSetTimeout(fn, delay, ...args); - activeTimeouts.push(id); - return id; - }; - - global.setImmediate = (fn, ...args) => { - const id = originalSetImmediate(fn, ...args); - activeImmediates.push(id); - return id; - }; - - beforeEach(() => { - jest.clearAllMocks(); - activeTimeouts = []; - activeImmediates = []; - - // Mock Express request object - mockReq = { - url: '/api/test', - method: 'GET', - headers: { - 'user-agent': 'test-agent', - 'x-forwarded-for': '192.168.1.1' - }, - ip: '192.168.1.1' - }; - - // Mock Express response object - mockRes = { - statusCode: 200, - status: jest.fn().mockReturnThis(), - setHeader: jest.fn().mockReturnThis(), - end: jest.fn(), - json: jest.fn(), - send: jest.fn(), - locals: {} - }; - - // Mock next function - mockNext = jest.fn(); - - // Default mock returns - getRealIP.mockReturnValue('192.168.1.1'); - behavioralDetection.isBlocked.mockResolvedValue({ blocked: false }); - behavioralDetection.getRateLimit.mockResolvedValue(null); - behavioralDetection.analyzeRequest.mockResolvedValue({ totalScore: 0, patterns: [] }); - }); - - afterEach(() => { - // Clear any pending timeouts and immediates - activeTimeouts.forEach(id => clearTimeout(id)); - activeImmediates.forEach(id => clearImmediate(id)); - activeTimeouts = []; - activeImmediates = []; - }); - - afterAll(() => { - // Restore original functions - global.setTimeout = originalSetTimeout; - global.setImmediate = originalSetImmediate; - }); - - describe('plugin creation', () => { - test('should create a behavioral detection middleware plugin', () => { - const plugin = BehavioralDetectionMiddleware(); - - expect(plugin.name).toBe('behavioral-detection'); - expect(plugin.priority).toBe(90); - expect(typeof plugin.middleware).toBe('function'); - }); - }); - - describe('middleware execution', () => { - test('should skip processing when behavioral detection is disabled', async () => { - behavioralDetection.config.enabled = false; - const plugin = BehavioralDetectionMiddleware(); - - await plugin.middleware(mockReq, mockRes, mockNext); - - expect(mockNext).toHaveBeenCalled(); - expect(behavioralDetection.isBlocked).not.toHaveBeenCalled(); - - // Restore enabled state - behavioralDetection.config.enabled = true; - }); - - test('should process request when behavioral detection is enabled', async () => { - const plugin = BehavioralDetectionMiddleware(); - - await plugin.middleware(mockReq, mockRes, mockNext); - - expect(mockNext).toHaveBeenCalled(); - expect(getRealIP).toHaveBeenCalledWith(mockReq); - }); - - test('should capture client IP correctly', async () => { - const plugin = BehavioralDetectionMiddleware(); - getRealIP.mockReturnValue('10.0.0.1'); - - await plugin.middleware(mockReq, mockRes, mockNext); - - expect(getRealIP).toHaveBeenCalledWith(mockReq); - }); - }); - - describe('blocking functionality', () => { - test('should block requests from blocked IPs', async () => { - behavioralDetection.isBlocked.mockResolvedValue({ - blocked: true, - reason: 'Malicious activity detected' - }); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - // Trigger response capture by calling end and wait for async processing - mockRes.end(); - - // Wait for setImmediate and async processing to complete - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - expect(mockRes.status).toHaveBeenCalledWith(403); - expect(mockRes.setHeader).toHaveBeenCalledWith('X-Behavioral-Block', 'true'); - expect(mockRes.setHeader).toHaveBeenCalledWith('X-Block-Reason', 'Malicious activity detected'); - expect(logs.plugin).toHaveBeenCalledWith('behavioral', expect.stringContaining('Blocked IP')); - }); - - test('should use default block message when none configured', async () => { - behavioralDetection.isBlocked.mockResolvedValue({ - blocked: true, - reason: 'Suspicious activity' - }); - behavioralDetection.config.Responses.BlockMessage = undefined; - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - expect(mockRes.status).toHaveBeenCalledWith(403); - }); - - test('should handle blocked IP without reason', async () => { - behavioralDetection.isBlocked.mockResolvedValue({ - blocked: true - }); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - expect(mockRes.setHeader).toHaveBeenCalledWith('X-Block-Reason', 'suspicious activity'); - }); - }); - - describe('rate limiting functionality', () => { - test('should apply rate limiting when limit exceeded', async () => { - // Make sure isBlocked returns false so rate limiting is checked - behavioralDetection.isBlocked.mockResolvedValue({ blocked: false }); - behavioralDetection.getRateLimit.mockResolvedValue({ - exceeded: true, - requests: 150, - limit: 100, - window: 60000, - resetTime: Date.now() + 60000 - }); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - expect(mockRes.status).toHaveBeenCalledWith(429); - expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Limit', '100'); - expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Remaining', '0'); - expect(mockRes.setHeader).toHaveBeenCalledWith('Retry-After', '60'); - expect(logs.plugin).toHaveBeenCalledWith('behavioral', expect.stringContaining('Rate limit exceeded')); - }); - - test('should set rate limit headers for non-exceeded limits', async () => { - behavioralDetection.isBlocked.mockResolvedValue({ blocked: false }); - behavioralDetection.getRateLimit.mockResolvedValue({ - exceeded: false, - requests: 50, - limit: 100, - resetTime: Date.now() + 30000 - }); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Limit', '100'); - expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Remaining', '50'); - expect(mockRes.status).not.toHaveBeenCalledWith(429); - }); - }); - - describe('behavioral analysis', () => { - test('should analyze request and set behavioral headers', async () => { - behavioralDetection.isBlocked.mockResolvedValue({ blocked: false }); - behavioralDetection.getRateLimit.mockResolvedValue(null); - behavioralDetection.analyzeRequest.mockResolvedValue({ - totalScore: 25, - patterns: [ - { name: 'rapid_requests', score: 15 }, - { name: 'suspicious_user_agent', score: 10 } - ] - }); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - expect(behavioralDetection.analyzeRequest).toHaveBeenCalledWith( - '192.168.1.1', - mockReq, - expect.objectContaining({ - status: 200, - responseTime: expect.any(Number) - }) - ); - - expect(mockRes.setHeader).toHaveBeenCalledWith('X-Behavioral-Score', '25'); - expect(mockRes.setHeader).toHaveBeenCalledWith('X-Behavioral-Patterns', 'rapid_requests, suspicious_user_agent'); - }); - - test('should store behavioral signals in response locals', async () => { - behavioralDetection.isBlocked.mockResolvedValue({ blocked: false }); - behavioralDetection.getRateLimit.mockResolvedValue(null); - const analysis = { - totalScore: 10, - patterns: [{ name: 'test_pattern', score: 10 }] - }; - behavioralDetection.analyzeRequest.mockResolvedValue(analysis); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - expect(mockRes.locals.behavioralSignals).toEqual(analysis); - }); - - test('should not set headers when no patterns detected', async () => { - behavioralDetection.isBlocked.mockResolvedValue({ blocked: false }); - behavioralDetection.getRateLimit.mockResolvedValue(null); - behavioralDetection.analyzeRequest.mockResolvedValue({ - totalScore: 0, - patterns: [] - }); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - expect(mockRes.setHeader).not.toHaveBeenCalledWith('X-Behavioral-Score', expect.anything()); - expect(mockRes.setHeader).not.toHaveBeenCalledWith('X-Behavioral-Patterns', expect.anything()); - }); - }); - - describe('response method interception', () => { - test('should intercept res.end() calls', async () => { - const originalEnd = mockRes.end; - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end('test data'); - - // Should have called the original method - expect(originalEnd).toHaveBeenCalledWith('test data'); - }); - - test('should intercept res.json() calls', async () => { - const originalJson = mockRes.json; - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - const testData = { test: 'data' }; - mockRes.json(testData); - - expect(originalJson).toHaveBeenCalledWith(testData); - }); - - test('should intercept res.send() calls', async () => { - const originalSend = mockRes.send; - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.send('test response'); - - expect(originalSend).toHaveBeenCalledWith('test response'); - }); - }); - - describe('error handling', () => { - test('should handle errors in behavioral analysis gracefully', async () => { - behavioralDetection.analyzeRequest.mockRejectedValue(new Error('Analysis failed')); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setTimeout(resolve, 10)); // Give error handling time - - expect(logs.error).toHaveBeenCalledWith('behavioral', expect.stringContaining('Error in behavioral analysis')); - expect(mockNext).toHaveBeenCalled(); // Should not block request flow - }); - - test('should handle errors in isBlocked check', async () => { - behavioralDetection.isBlocked.mockRejectedValue(new Error('Block check failed')); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setTimeout(resolve, 10)); - - expect(logs.error).toHaveBeenCalledWith('behavioral', expect.stringContaining('Error in behavioral analysis')); - }); - - test('should fail open for availability on errors', async () => { - behavioralDetection.isBlocked.mockRejectedValue(new Error('Service unavailable')); - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - expect(mockNext).toHaveBeenCalled(); - // Should not block the request even if behavioral detection fails - }); - }); - - describe('response locals handling', () => { - test('should handle missing response locals gracefully', async () => { - delete mockRes.locals; - - const plugin = BehavioralDetectionMiddleware(); - await plugin.middleware(mockReq, mockRes, mockNext); - - mockRes.end(); - await new Promise(resolve => setImmediate(resolve)); - await new Promise(resolve => setImmediate(resolve)); - - // Should not throw error even without locals - expect(mockNext).toHaveBeenCalled(); - }); - }); -}); \ No newline at end of file diff --git a/.tests/checkpoint.test.js b/.tests/checkpoint.test.js deleted file mode 100644 index a67cca0..0000000 --- a/.tests/checkpoint.test.js +++ /dev/null @@ -1,525 +0,0 @@ -import { jest } from '@jest/globals'; -import * as crypto from 'crypto'; - -// Mock the dependencies -jest.unstable_mockModule('../dist/index.js', () => ({ - registerPlugin: jest.fn(), - loadConfig: jest.fn().mockResolvedValue({ - Core: { - Enabled: true, - CookieName: '__checkpoint', - SanitizeURLs: true - }, - ThreatScoring: { - Enabled: true, - AllowThreshold: 20, - ChallengeThreshold: 60, - BlockThreshold: 80 - }, - ProofOfWork: { - Difficulty: 16, - SaltLength: 32, - ChallengeExpiration: '5m' - } - }), - rootDir: '/test/root' -})); - -jest.unstable_mockModule('../dist/utils/logs.js', () => ({ - plugin: jest.fn(), - warn: jest.fn(), - error: jest.fn() -})); - -jest.unstable_mockModule('../dist/utils/threat-scoring.js', () => ({ - threatScorer: { - calculateThreatScore: jest.fn() - }, - THREAT_THRESHOLDS: { - ALLOW: 20, - CHALLENGE: 60, - BLOCK: 80 - } -})); - -jest.unstable_mockModule('../dist/utils/proof.js', () => ({ - challengeStore: new Map(), - generateRequestID: jest.fn(() => 'test-request-id'), - getChallengeParams: jest.fn(), - deleteChallenge: jest.fn(), - verifyPoW: jest.fn(), - verifyPoS: jest.fn() -})); - -jest.unstable_mockModule('level', () => ({ - Level: jest.fn(() => ({ - open: jest.fn().mockResolvedValue(undefined), - put: jest.fn().mockResolvedValue(undefined), - get: jest.fn().mockResolvedValue(undefined), - del: jest.fn().mockResolvedValue(undefined), - close: jest.fn().mockResolvedValue(undefined), - iterator: jest.fn(() => []) - })) -})); - -jest.unstable_mockModule('level-ttl', () => ({ - default: jest.fn((db) => db) -})); - -jest.unstable_mockModule('fs', () => ({ - existsSync: jest.fn(() => true), - promises: { - mkdir: jest.fn().mockResolvedValue(undefined), - readFile: jest.fn().mockResolvedValue('
{{TargetPath}}') - } -})); - -// Import after mocking -const checkpoint = await import('../dist/checkpoint.js'); - -describe('Checkpoint Security System', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('Utility Functions', () => { - describe('sanitizePath', () => { - test('should sanitize basic paths', () => { - // This function isn't directly exported, so we'll test through integration - expect(typeof checkpoint).toBe('object'); - }); - - test('should handle invalid input types gracefully', () => { - // Testing integration behaviors since sanitizePath is internal - expect(checkpoint).toBeDefined(); - }); - }); - - describe('LimitedMap', () => { - test('should respect size limits', () => { - // LimitedMap is internal, testing through checkpoint behaviors - expect(checkpoint).toBeDefined(); - }); - }); - }); - - describe('Template System', () => { - test('should handle template data replacement', () => { - const templateStr = 'Hello {{name}}, your score is {{score}}'; - const data = { name: 'John', score: 85 }; - - const result = templateStr.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (_, key) => { - let value = data; - for (const part of key.trim().split('.')) { - if (value && typeof value === 'object' && part in value) { - value = value[part]; - } else { - value = undefined; - break; - } - } - return value != null ? String(value) : ''; - }); - - expect(result).toBe('Hello John, your score is 85'); - }); - - test('should handle nested template data', () => { - const templateStr = 'Request {{request.id}} to {{request.path}}'; - const data = { - request: { id: '123', path: '/test' } - }; - - const result = templateStr.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (_, key) => { - let value = data; - for (const part of key.trim().split('.')) { - if (value && typeof value === 'object' && part in value) { - value = value[part]; - } else { - value = undefined; - break; - } - } - return value != null ? String(value) : ''; - }); - - expect(result).toBe('Request 123 to /test'); - }); - - test('should handle missing template data gracefully', () => { - const templateStr = 'Hello {{missing.key}}'; - const data = {}; - - const result = templateStr.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (_, key) => { - let value = data; - for (const part of key.trim().split('.')) { - if (value && typeof value === 'object' && part in value) { - value = value[part]; - } else { - value = undefined; - break; - } - } - return value != null ? String(value) : ''; - }); - - expect(result).toBe('Hello '); - }); - }); - - describe('Response Generation', () => { - const mockRequest = { - url: '/test', - headers: { - host: 'example.com', - 'user-agent': 'Mozilla/5.0 Test Browser' - } - }; - - test('should generate threat level descriptions', () => { - const getThreatLevel = (score) => { - if (score >= 80) return 'critical'; - if (score >= 60) return 'high'; - if (score >= 40) return 'medium'; - if (score >= 20) return 'low'; - return 'minimal'; - }; - - expect(getThreatLevel(0)).toBe('minimal'); - expect(getThreatLevel(15)).toBe('minimal'); - expect(getThreatLevel(25)).toBe('low'); - expect(getThreatLevel(45)).toBe('medium'); - expect(getThreatLevel(65)).toBe('high'); - expect(getThreatLevel(85)).toBe('critical'); - }); - - test('should format signal names correctly', () => { - const formatSignalName = (signal) => { - const formatMap = { - 'sql_injection': 'SQL Injection Attempt', - 'xss_attempt': 'Cross-Site Scripting', - 'command_injection': 'Command Injection', - 'blacklisted_ip': 'Blacklisted IP Address', - 'tor_exit_node': 'Tor Exit Node', - 'attack_tool_ua': 'Attack Tool Detected' - }; - return formatMap[signal] || signal.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - }; - - expect(formatSignalName('sql_injection')).toBe('SQL Injection Attempt'); - expect(formatSignalName('xss_attempt')).toBe('Cross-Site Scripting'); - expect(formatSignalName('unknown_signal')).toBe('Unknown Signal'); - }); - - test('should generate appropriate challenge types based on threat score', () => { - const getChallengeType = (score) => score > 60 ? 'advanced' : 'standard'; - const getEstimatedTime = (score) => score > 60 ? '10-15' : '5-10'; - - expect(getChallengeType(30)).toBe('standard'); - expect(getEstimatedTime(30)).toBe('5-10'); - expect(getChallengeType(70)).toBe('advanced'); - expect(getEstimatedTime(70)).toBe('10-15'); - }); - }); - - describe('Client Identification', () => { - test('should hash IP addresses consistently', () => { - const ip = '192.168.1.100'; - const hash1 = crypto.createHash('sha256').update(ip).digest().slice(0, 8).toString('hex'); - const hash2 = crypto.createHash('sha256').update(ip).digest().slice(0, 8).toString('hex'); - - expect(hash1).toBe(hash2); - expect(hash1).toHaveLength(16); // 8 bytes = 16 hex chars - }); - - test('should generate different hashes for different IPs', () => { - const ip1 = '192.168.1.100'; - const ip2 = '192.168.1.101'; - const hash1 = crypto.createHash('sha256').update(ip1).digest().slice(0, 8).toString('hex'); - const hash2 = crypto.createHash('sha256').update(ip2).digest().slice(0, 8).toString('hex'); - - expect(hash1).not.toBe(hash2); - }); - - test('should hash user agents consistently', () => { - const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'; - const hash1 = crypto.createHash('sha256').update(ua).digest().slice(0, 8).toString('hex'); - const hash2 = crypto.createHash('sha256').update(ua).digest().slice(0, 8).toString('hex'); - - expect(hash1).toBe(hash2); - expect(hash1).toHaveLength(16); - }); - - test('should handle empty user agents', () => { - const emptyUA = ''; - const hash = emptyUA ? crypto.createHash('sha256').update(emptyUA).digest().slice(0, 8).toString('hex') : ''; - - expect(hash).toBe(''); - }); - - test('should extract browser fingerprint from headers', () => { - const headers = { - 'sec-ch-ua': '"Google Chrome";v="119"', - 'sec-ch-ua-platform': '"Windows"', - 'sec-ch-ua-mobile': '?0' - }; - - const headerNames = [ - 'sec-ch-ua', - 'sec-ch-ua-platform', - 'sec-ch-ua-mobile', - 'sec-ch-ua-platform-version', - 'sec-ch-ua-arch', - 'sec-ch-ua-model' - ]; - - const parts = headerNames - .map(h => headers[h]) - .filter(part => typeof part === 'string' && part.length > 0); - - expect(parts).toHaveLength(3); - - if (parts.length > 0) { - const fingerprint = crypto.createHash('sha256') - .update(Buffer.from(parts.join('|'))) - .digest() - .slice(0, 12) - .toString('hex'); - - expect(fingerprint).toHaveLength(24); // 12 bytes = 24 hex chars - } - }); - - test('should handle fetch-style headers', () => { - const fetchHeaders = { - get: jest.fn((name) => { - const headers = { - 'sec-ch-ua': '"Chrome";v="119"', - 'sec-ch-ua-platform': '"Windows"' - }; - return headers[name] || null; - }) - }; - - expect(fetchHeaders.get('sec-ch-ua')).toBe('"Chrome";v="119"'); - expect(fetchHeaders.get('nonexistent')).toBe(null); - }); - }); - - describe('Security Configuration', () => { - test('should validate threat score thresholds', () => { - const thresholds = { - ALLOW: 20, - CHALLENGE: 60, - BLOCK: 80 - }; - - expect(thresholds.ALLOW).toBeLessThan(thresholds.CHALLENGE); - expect(thresholds.CHALLENGE).toBeLessThan(thresholds.BLOCK); - expect(thresholds.ALLOW).toBeGreaterThanOrEqual(0); - expect(thresholds.BLOCK).toBeLessThanOrEqual(100); - }); - - test('should handle user-defined thresholds', () => { - const determineAction = (score, userThresholds = null) => { - if (userThresholds) { - const allowThreshold = userThresholds.ALLOW || userThresholds.AllowThreshold || 20; - const challengeThreshold = userThresholds.CHALLENGE || userThresholds.ChallengeThreshold || 60; - - if (score <= allowThreshold) return 'allow'; - if (score <= challengeThreshold) return 'challenge'; - return 'block'; - } - - if (score <= 20) return 'allow'; - if (score <= 60) return 'challenge'; - return 'block'; - }; - - const userThresholds = { AllowThreshold: 15, ChallengeThreshold: 50, BlockThreshold: 80 }; - - expect(determineAction(10, userThresholds)).toBe('allow'); - expect(determineAction(30, userThresholds)).toBe('challenge'); - expect(determineAction(80, userThresholds)).toBe('block'); - }); - - test('should validate configuration structure', () => { - const mockConfig = { - Core: { - Enabled: true, - CookieName: '__checkpoint', - SanitizeURLs: true - }, - ThreatScoring: { - Enabled: true, - AllowThreshold: 20, - ChallengeThreshold: 60, - BlockThreshold: 80 - }, - ProofOfWork: { - Difficulty: 16, - SaltLength: 32, - ChallengeExpiration: '5m' - } - }; - - expect(mockConfig.Core.Enabled).toBe(true); - expect(mockConfig.ThreatScoring.AllowThreshold).toBe(20); - expect(mockConfig.ProofOfWork.Difficulty).toBe(16); - }); - }); - - describe('Token Management', () => { - test('should generate consistent token signatures', () => { - const secret = 'test-secret'; - const token = 'test-token'; - - const signature1 = crypto.createHmac('sha256', secret).update(token).digest('hex'); - const signature2 = crypto.createHmac('sha256', secret).update(token).digest('hex'); - - expect(signature1).toBe(signature2); - expect(signature1).toHaveLength(64); // SHA256 hex = 64 chars - }); - - test('should generate different signatures for different tokens', () => { - const secret = 'test-secret'; - const token1 = 'test-token-1'; - const token2 = 'test-token-2'; - - const signature1 = crypto.createHmac('sha256', secret).update(token1).digest('hex'); - const signature2 = crypto.createHmac('sha256', secret).update(token2).digest('hex'); - - expect(signature1).not.toBe(signature2); - }); - - test('should handle token expiration logic', () => { - const now = Date.now(); - const oneHour = 60 * 60 * 1000; - const expiration = now + oneHour; - - expect(expiration).toBeGreaterThan(now); - expect(expiration - now).toBe(oneHour); - - // Test if token is expired - const isExpired = (expirationTime) => Date.now() > expirationTime; - expect(isExpired(expiration)).toBe(false); - expect(isExpired(now - 1000)).toBe(true); - }); - - test('should validate nonce uniqueness', () => { - const nonce1 = crypto.randomBytes(16).toString('hex'); - const nonce2 = crypto.randomBytes(16).toString('hex'); - - expect(nonce1).not.toBe(nonce2); - expect(nonce1).toHaveLength(32); // 16 bytes = 32 hex chars - expect(nonce2).toHaveLength(32); - }); - }); - - describe('Rate Limiting', () => { - test('should track request attempts per IP', () => { - const ipAttempts = new Map(); - const maxAttempts = 10; - - const recordAttempt = (ip) => { - const currentAttempts = ipAttempts.get(ip) || 0; - const newAttempts = currentAttempts + 1; - ipAttempts.set(ip, newAttempts); - return newAttempts <= maxAttempts; - }; - - expect(recordAttempt('192.168.1.100')).toBe(true); - expect(ipAttempts.get('192.168.1.100')).toBe(1); - - // Simulate many attempts - for (let i = 0; i < 10; i++) { - recordAttempt('192.168.1.100'); - } - - expect(recordAttempt('192.168.1.100')).toBe(false); // Should exceed limit - }); - - test('should handle time-based rate limiting', () => { - const now = Date.now(); - const oneHour = 60 * 60 * 1000; - const windowStart = now - oneHour; - - const isWithinWindow = (timestamp) => timestamp > windowStart; - - expect(isWithinWindow(now)).toBe(true); - expect(isWithinWindow(now - oneHour - 1000)).toBe(false); - }); - }); - - describe('Extension Handling', () => { - test('should handle file extension filtering', () => { - const path = '/static/style.css'; - const extension = path.substring(path.lastIndexOf('.')).toLowerCase(); - - expect(extension).toBe('.css'); - }); - - test('should identify static file extensions', () => { - const staticExtensions = new Set(['.css', '.js', '.png', '.jpg', '.gif', '.ico', '.svg']); - - expect(staticExtensions.has('.css')).toBe(true); - expect(staticExtensions.has('.js')).toBe(true); - expect(staticExtensions.has('.html')).toBe(false); - }); - - test('should handle paths without extensions', () => { - const pathWithoutExt = '/api/users'; - const lastDot = pathWithoutExt.lastIndexOf('.'); - - expect(lastDot).toBe(-1); - }); - }); - - describe('Security Validation', () => { - test('should sanitize URL paths', () => { - const sanitizePath = (inputPath) => { - if (typeof inputPath !== 'string') { - return '/'; - } - let pathOnly = inputPath.replace(/[\x00-\x1F\x7F]/g, ''); - pathOnly = pathOnly.replace(/[<>;"'`|]/g, ''); - const parts = pathOnly.split('/').filter(seg => seg && seg !== '.' && seg !== '..'); - return '/' + parts.map(seg => encodeURIComponent(seg)).join('/'); - }; - - expect(sanitizePath('/path/../../../etc/passwd')).toBe('/path/etc/passwd'); // .. filtered out - expect(sanitizePath('/path')).toBe('/pathscriptalert(xss)/script'); - expect(sanitizePath('/path\x00\x1F\x7F/file')).toBe('/path/file'); - expect(sanitizePath(null)).toBe('/'); - }); - - test('should handle extension filtering', () => { - const isStaticFile = (path) => { - const staticExtensions = new Set(['.css', '.js', '.png', '.jpg', '.gif', '.ico', '.svg']); - const ext = path.substring(path.lastIndexOf('.')).toLowerCase(); - return staticExtensions.has(ext); - }; - - expect(isStaticFile('/static/style.css')).toBe(true); - expect(isStaticFile('/app.js')).toBe(true); - expect(isStaticFile('/api/users')).toBe(false); - expect(isStaticFile('/index.html')).toBe(false); - }); - - test('should handle control characters in paths', () => { - const pathWithControlChars = '/path\x00\x1F\x7F/file'; - const sanitized = pathWithControlChars.replace(/[\x00-\x1F\x7F]/g, ''); - - expect(sanitized).toBe('/path/file'); - expect(sanitized).not.toMatch(/[\x00-\x1F\x7F]/); - }); - - test('should filter dangerous characters', () => { - const pathWithDangerousChars = '/path'; - const sanitized = pathWithDangerousChars.replace(/[<>;"'`|]/g, ''); - - expect(sanitized).toBe('/pathscriptalert(xss)/script'); // Correct expectation - expect(sanitized).not.toContain(' +