Initial commit of massive v2 rewrite
This commit is contained in:
parent
1025f3b523
commit
dc120fe78a
55 changed files with 21733 additions and 0 deletions
411
.tests/behavioral-middleware.test.js
Normal file
411
.tests/behavioral-middleware.test.js
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue