UNPKG

ssvc

Version:

TypeScript implementation of SSVC (Stakeholder-Specific Vulnerability Categorization). A prioritization framework to triage CVE vulnerabilities as an alternative or compliment to CVSS

364 lines (290 loc) 11.6 kB
import { PluginRegistry, SSVCPlugin, Decision, SSVCDecision, SSVCOutcome } from './core'; // Mock plugin for testing class MockPlugin extends SSVCPlugin { readonly name = 'MockPlugin'; readonly description = 'Mock plugin for testing'; readonly version = '1.0.0'; createDecision(options: Record<string, any>): SSVCDecision { return new MockDecision(options); } fromVector(vector: string): SSVCDecision { return new MockDecision({}); } } class MockDecision implements SSVCDecision { private options: Record<string, any>; public outcome?: SSVCOutcome; constructor(options: Record<string, any>) { this.options = options; } evaluate(): SSVCOutcome { const mockOutcome = { action: this.options.severity === 'high' ? 'immediate' : 'track', priority: this.options.severity === 'high' ? 'high' : 'low' }; this.outcome = mockOutcome; return mockOutcome; } } // Another mock plugin to test multiple plugins class AnotherMockPlugin extends SSVCPlugin { readonly name = 'AnotherMock'; readonly description = 'Another mock plugin'; readonly version = '2.0.0'; createDecision(options: Record<string, any>): SSVCDecision { return new MockDecision(options); } fromVector(vector: string): SSVCDecision { return new MockDecision({}); } } describe('PluginRegistry', () => { let registry: PluginRegistry; beforeEach(() => { // Create a fresh registry for each test registry = new (PluginRegistry as any)(); (PluginRegistry as any).instance = registry; }); afterEach(() => { // Clean up singleton (PluginRegistry as any).instance = undefined; }); describe('singleton behavior', () => { it('should return the same instance', () => { const registry1 = PluginRegistry.getInstance(); const registry2 = PluginRegistry.getInstance(); expect(registry1).toBe(registry2); }); }); describe('plugin registration', () => { it('should register a plugin', () => { const mockPlugin = new MockPlugin(); registry.register(mockPlugin); expect(registry.has('mockplugin')).toBe(true); expect(registry.get('mockplugin')).toBe(mockPlugin); }); it('should handle case-insensitive plugin names', () => { const mockPlugin = new MockPlugin(); registry.register(mockPlugin); expect(registry.has('MOCKPLUGIN')).toBe(true); expect(registry.has('mockplugin')).toBe(true); expect(registry.get('MOCKPLUGIN')).toBe(mockPlugin); }); it('should list all registered plugins', () => { const mockPlugin1 = new MockPlugin(); const mockPlugin2 = new AnotherMockPlugin(); registry.register(mockPlugin1); registry.register(mockPlugin2); const plugins = registry.list(); expect(plugins).toHaveLength(2); expect(plugins).toContain(mockPlugin1); expect(plugins).toContain(mockPlugin2); }); it('should return undefined for non-existent plugins', () => { expect(registry.get('nonexistent')).toBeUndefined(); expect(registry.has('nonexistent')).toBe(false); }); it('should overwrite existing plugin with same name', () => { const mockPlugin1 = new MockPlugin(); const mockPlugin2 = new AnotherMockPlugin(); // Override the name to test overwriting (mockPlugin2 as any).name = 'MockPlugin'; registry.register(mockPlugin1); registry.register(mockPlugin2); expect(registry.get('mockplugin')).toBe(mockPlugin2); expect(registry.list()).toHaveLength(1); }); }); }); describe('Decision', () => { let registry: PluginRegistry; beforeEach(() => { registry = new (PluginRegistry as any)(); (PluginRegistry as any).instance = registry; }); afterEach(() => { (PluginRegistry as any).instance = undefined; }); describe('plugin-based decisions', () => { it('should create and evaluate decisions using registered plugins', () => { const mockPlugin = new MockPlugin(); registry.register(mockPlugin); const decision = new Decision('MockPlugin', { severity: 'high' }); const outcome = decision.evaluate(); expect(outcome.action).toBe('immediate'); expect(outcome.priority).toBe('high'); expect(decision.outcome).toBe(outcome); }); it('should handle different plugin options', () => { const mockPlugin = new MockPlugin(); registry.register(mockPlugin); const decision = new Decision('MockPlugin', { severity: 'low' }); const outcome = decision.evaluate(); expect(outcome.action).toBe('track'); expect(outcome.priority).toBe('low'); }); it('should throw error for unknown methodology', () => { expect(() => { const decision = new Decision('UnknownMethodology', {}); decision.evaluate(); }).toThrow('Unknown methodology: UnknownMethodology. Available methodologies: '); }); it('should include available methodologies in error message', () => { const mockPlugin1 = new MockPlugin(); const mockPlugin2 = new AnotherMockPlugin(); registry.register(mockPlugin1); registry.register(mockPlugin2); expect(() => { const decision = new Decision('UnknownMethodology', {}); decision.evaluate(); }).toThrow('Unknown methodology: UnknownMethodology. Available methodologies: MockPlugin, AnotherMock'); }); it('should handle empty options', () => { const mockPlugin = new MockPlugin(); registry.register(mockPlugin); const decision = new Decision('MockPlugin'); const outcome = decision.evaluate(); expect(outcome.action).toBe('track'); // default behavior expect(outcome.priority).toBe('low'); }); }); describe('static factory method', () => { it('should create decision using static method', () => { const mockPlugin = new MockPlugin(); registry.register(mockPlugin); const decision = Decision.createDecision('MockPlugin', { severity: 'high' }); expect(decision).toBeInstanceOf(Decision); const outcome = decision.evaluate(); expect(outcome.action).toBe('immediate'); }); it('should create decision with empty options using static method', () => { const mockPlugin = new MockPlugin(); registry.register(mockPlugin); const decision = Decision.createDecision('MockPlugin'); expect(decision).toBeInstanceOf(Decision); const outcome = decision.evaluate(); expect(outcome.action).toBe('track'); }); }); }); describe('SSVCPlugin abstract class', () => { it('should require implementation of abstract properties and methods', () => { class TestPlugin extends SSVCPlugin { readonly name = 'TestPlugin'; readonly description = 'Test plugin'; readonly version = '1.0.0'; createDecision(options: Record<string, any>): SSVCDecision { return new MockDecision(options); } fromVector(vector: string): SSVCDecision { return new MockDecision({}); } } const plugin = new TestPlugin(); expect(plugin.name).toBe('TestPlugin'); expect(plugin.description).toBe('Test plugin'); expect(plugin.version).toBe('1.0.0'); expect(typeof plugin.createDecision).toBe('function'); }); }); describe('Integration tests', () => { let registry: PluginRegistry; beforeEach(() => { registry = new (PluginRegistry as any)(); (PluginRegistry as any).instance = registry; }); afterEach(() => { (PluginRegistry as any).instance = undefined; }); it('should support multiple plugins with different outcomes', () => { const plugin1 = new MockPlugin(); const plugin2 = new AnotherMockPlugin(); registry.register(plugin1); registry.register(plugin2); const decision1 = new Decision('MockPlugin', { severity: 'high' }); const outcome1 = decision1.evaluate(); const decision2 = new Decision('AnotherMock', { severity: 'low' }); const outcome2 = decision2.evaluate(); expect(outcome1.action).toBe('immediate'); expect(outcome2.action).toBe('track'); }); it('should maintain state between plugin calls', () => { const mockPlugin = new MockPlugin(); registry.register(mockPlugin); const decision1 = new Decision('MockPlugin', { severity: 'high' }); const decision2 = new Decision('MockPlugin', { severity: 'low' }); const outcome1 = decision1.evaluate(); const outcome2 = decision2.evaluate(); // Each decision should maintain its own outcome expect(decision1.outcome?.action).toBe('immediate'); expect(decision2.outcome?.action).toBe('track'); expect(outcome1.action).toBe('immediate'); expect(outcome2.action).toBe('track'); }); describe('Error handling edge cases', () => { it('should handle invalid vector string format in fromVector', () => { const mockPlugin = new MockPlugin(); registry.register(mockPlugin); // Test invalid vector format (line 104 in core.ts) - use a string that fails the regex expect(() => { Decision.fromVector('invalid-vector-123'); }).toThrow('Invalid vector string format'); }); it('should handle toVector when plugin does not support vectors', () => { const pluginWithoutVector = new (class extends SSVCPlugin { readonly name = 'NoVector'; readonly description = 'Plugin without vector support'; readonly version = '1.0.0'; createDecision(options: Record<string, any>): SSVCDecision { return new (class implements SSVCDecision { evaluate(): SSVCOutcome { return { action: 'track', priority: 'low' }; } // No toVector method })(); } fromVector(vector: string): SSVCDecision { return new MockDecision({}); } })(); registry.register(pluginWithoutVector); const decision = new Decision('NoVector', {}); // Test when toVector is not supported (line 146 in core.ts) expect(() => { decision.toVector(); }).toThrow('Vector string generation not supported for methodology: NoVector'); }); it('should handle toVector when plugin toVector returns undefined', () => { const pluginWithUndefinedVector = new (class extends SSVCPlugin { readonly name = 'UndefinedVector'; readonly description = 'Plugin with undefined vector'; readonly version = '1.0.0'; createDecision(options: Record<string, any>): SSVCDecision { return new (class implements SSVCDecision { evaluate(): SSVCOutcome { return { action: 'track', priority: 'low' }; } toVector(): string | undefined { return undefined; // This should trigger the error path } })(); } fromVector(vector: string): SSVCDecision { return new MockDecision({}); } })(); registry.register(pluginWithUndefinedVector); const decision = new Decision('UndefinedVector', {}); // Test when toVector returns undefined (line 146 in core.ts) expect(() => { decision.toVector(); }).toThrow('Vector string generation not supported for methodology: UndefinedVector'); }); }); });