Spaces:
Runtime error
Runtime error
| import { describe, it, expect, vi, beforeEach } from 'vitest' | |
| const mockRun = vi.fn((): any => ({ lastInsertRowid: 1, changes: 1 })) | |
| const mockGet = vi.fn((): any => ({ | |
| auth_failures: 1, | |
| injection_attempts: 0, | |
| rate_limit_hits: 0, | |
| secret_exposures: 0, | |
| successful_tasks: 5, | |
| failed_tasks: 0, | |
| trust_score: 0.95, | |
| })) | |
| const mockPrepare = vi.fn(() => ({ run: mockRun, get: mockGet, all: vi.fn(() => []) })) | |
| vi.mock('@/lib/db', () => ({ | |
| getDatabase: () => ({ prepare: mockPrepare }), | |
| })) | |
| vi.mock('@/lib/event-bus', () => ({ | |
| eventBus: { broadcast: vi.fn() }, | |
| })) | |
| vi.mock('@/lib/logger', () => ({ | |
| logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, | |
| })) | |
| import { logSecurityEvent, updateAgentTrustScore, getSecurityPosture } from '@/lib/security-events' | |
| describe('logSecurityEvent', () => { | |
| beforeEach(() => { | |
| vi.clearAllMocks() | |
| mockRun.mockReturnValue({ lastInsertRowid: 42, changes: 1 }) | |
| }) | |
| it('inserts an event into the database', () => { | |
| const id = logSecurityEvent({ | |
| event_type: 'auth_failure', | |
| severity: 'warning', | |
| source: 'auth', | |
| detail: 'test detail', | |
| }) | |
| expect(mockPrepare).toHaveBeenCalled() | |
| expect(mockRun).toHaveBeenCalledWith( | |
| 'auth_failure', 'warning', 'auth', null, 'test detail', null, 1, 1 | |
| ) | |
| expect(id).toBe(42) | |
| }) | |
| it('defaults severity to info when not provided', () => { | |
| logSecurityEvent({ event_type: 'test_event' }) | |
| expect(mockRun).toHaveBeenCalledWith( | |
| 'test_event', 'info', null, null, null, null, 1, 1 | |
| ) | |
| }) | |
| it('uses provided workspace_id and tenant_id', () => { | |
| logSecurityEvent({ | |
| event_type: 'test_event', | |
| severity: 'critical', | |
| workspace_id: 5, | |
| tenant_id: 3, | |
| }) | |
| expect(mockRun).toHaveBeenCalledWith( | |
| 'test_event', 'critical', null, null, null, null, 5, 3 | |
| ) | |
| }) | |
| it('broadcasts via event bus', async () => { | |
| const { eventBus } = await import('@/lib/event-bus') | |
| logSecurityEvent({ event_type: 'injection_attempt', severity: 'critical' }) | |
| expect(eventBus.broadcast).toHaveBeenCalledWith( | |
| 'security.event', | |
| expect.objectContaining({ event_type: 'injection_attempt', severity: 'critical' }) | |
| ) | |
| }) | |
| }) | |
| describe('updateAgentTrustScore', () => { | |
| beforeEach(() => { | |
| vi.clearAllMocks() | |
| mockGet.mockReturnValue({ | |
| auth_failures: 1, | |
| injection_attempts: 0, | |
| rate_limit_hits: 0, | |
| secret_exposures: 0, | |
| successful_tasks: 5, | |
| failed_tasks: 0, | |
| trust_score: 0.95, | |
| }) | |
| }) | |
| it('creates a row if one does not exist (INSERT OR IGNORE)', () => { | |
| updateAgentTrustScore('test-agent', 'auth.failure', 1) | |
| // First call: INSERT OR IGNORE, second: UPDATE counter, third: SELECT, fourth: UPDATE score | |
| expect(mockPrepare).toHaveBeenCalled() | |
| expect(mockRun).toHaveBeenCalled() | |
| }) | |
| it('recalculates trust score clamped between 0 and 1', () => { | |
| mockGet.mockReturnValue({ | |
| auth_failures: 20, | |
| injection_attempts: 10, | |
| rate_limit_hits: 5, | |
| secret_exposures: 3, | |
| successful_tasks: 0, | |
| failed_tasks: 0, | |
| trust_score: 0, | |
| }) | |
| updateAgentTrustScore('bad-agent', 'injection.attempt', 1) | |
| // Score would go negative, should be clamped to 0 | |
| const calls = mockRun.mock.calls as any[][] | |
| const lastCall = calls[calls.length - 1] | |
| if (typeof lastCall[0] === 'number') { | |
| expect(lastCall[0]).toBeGreaterThanOrEqual(0) | |
| expect(lastCall[0]).toBeLessThanOrEqual(1) | |
| } | |
| }) | |
| }) | |
| describe('getSecurityPosture', () => { | |
| beforeEach(() => { | |
| vi.clearAllMocks() | |
| }) | |
| it('returns expected posture shape', () => { | |
| mockGet | |
| .mockReturnValueOnce({ total: 10, critical: 2, warning: 5 }) | |
| .mockReturnValueOnce({ count: 3 }) | |
| .mockReturnValueOnce({ avg_trust: 0.85 }) | |
| const posture = getSecurityPosture(1) | |
| expect(posture).toHaveProperty('score') | |
| expect(posture).toHaveProperty('totalEvents') | |
| expect(posture).toHaveProperty('criticalEvents') | |
| expect(posture).toHaveProperty('warningEvents') | |
| expect(posture).toHaveProperty('avgTrustScore') | |
| expect(posture).toHaveProperty('recentIncidents') | |
| expect(typeof posture.score).toBe('number') | |
| expect(posture.score).toBeGreaterThanOrEqual(0) | |
| expect(posture.score).toBeLessThanOrEqual(100) | |
| }) | |
| it('deducts points for critical and warning events', () => { | |
| mockGet | |
| .mockReturnValueOnce({ total: 5, critical: 5, warning: 0 }) | |
| .mockReturnValueOnce({ count: 5 }) | |
| .mockReturnValueOnce({ avg_trust: 1.0 }) | |
| const posture = getSecurityPosture(1) | |
| expect(posture.score).toBeLessThan(100) | |
| }) | |
| it('returns score of 100 with no events', () => { | |
| mockGet | |
| .mockReturnValueOnce({ total: 0, critical: 0, warning: 0 }) | |
| .mockReturnValueOnce({ count: 0 }) | |
| .mockReturnValueOnce({ avg_trust: 1.0 }) | |
| const posture = getSecurityPosture(1) | |
| expect(posture.score).toBe(100) | |
| }) | |
| }) | |
| describe('injection guard new rules', () => { | |
| let scanForInjection: typeof import('@/lib/injection-guard').scanForInjection | |
| beforeEach(async () => { | |
| const mod = await import('@/lib/injection-guard') | |
| scanForInjection = mod.scanForInjection | |
| }) | |
| it('detects SSRF targeting metadata endpoint', () => { | |
| const report = scanForInjection('curl http://169.254.169.254/latest/meta-data/', { context: 'shell' }) | |
| expect(report.safe).toBe(false) | |
| expect(report.matches.some(m => m.rule === 'cmd-ssrf')).toBe(true) | |
| }) | |
| it('detects SSRF targeting localhost', () => { | |
| const report = scanForInjection('wget http://localhost:8080/admin', { context: 'shell' }) | |
| expect(report.safe).toBe(false) | |
| expect(report.matches.some(m => m.rule === 'cmd-ssrf')).toBe(true) | |
| }) | |
| it('detects template injection (Jinja2)', () => { | |
| const report = scanForInjection('{{config.__class__.__init__.__globals__}}', { context: 'prompt' }) | |
| expect(report.safe).toBe(false) | |
| expect(report.matches.some(m => m.rule === 'cmd-template-injection')).toBe(true) | |
| }) | |
| it('detects SQL injection (UNION SELECT)', () => { | |
| const report = scanForInjection("' UNION SELECT * FROM users --", { context: 'shell' }) | |
| expect(report.safe).toBe(false) | |
| expect(report.matches.some(m => m.rule === 'cmd-sql-injection')).toBe(true) | |
| }) | |
| it('detects SQL injection (OR 1=1)', () => { | |
| const report = scanForInjection("' OR 1=1 --", { context: 'shell' }) | |
| expect(report.safe).toBe(false) | |
| expect(report.matches.some(m => m.rule === 'cmd-sql-injection')).toBe(true) | |
| }) | |
| it('does not false-positive on normal SQL mentions', () => { | |
| const report = scanForInjection('SELECT name FROM products WHERE id = 5', { context: 'shell' }) | |
| // This should not trigger because it lacks injection markers | |
| expect(report.matches.filter(m => m.rule === 'cmd-sql-injection')).toHaveLength(0) | |
| }) | |
| }) | |