@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
403 lines (342 loc) • 12.4 kB
text/typescript
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,
})
);
});
});
});