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

468 lines (404 loc) 13.2 kB
/** * Auditable Decision Wrapper * * Wraps any SSVCDecision (generated or runtime) to add audit trail capabilities * without modifying the underlying decision implementation. */ // Using browser-compatible crypto.randomUUID() instead of uuid package import { SSVCDecision, SSVCOutcome, AuditableDecision } from '../core'; import { DecisionPointEvidence } from '../evidence/types'; import { DataSource } from '../mapping/types'; import { AuditEntry, ForensicReport, AuditEventType, AuditEvent, TimelineEntry } from '../audit/types'; import { createRuntimeDecision } from '../runtime'; import * as yaml from 'yaml'; /** * Universal wrapper that makes any SSVCDecision auditable */ export class AuditableDecisionWrapper implements AuditableDecision { public readonly evaluationId: string; private wrappedDecision: SSVCDecision; private methodology: string; private version: string; private evidenceMap: Map<string, DecisionPointEvidence> = new Map(); private dataSourceMap: Map<string, DataSource> = new Map(); private auditEvents: AuditEvent[] = []; private timeline: TimelineEntry[] = []; private startTime: number; constructor(decision: SSVCDecision, methodology: string, version: string) { this.evaluationId = crypto.randomUUID(); this.wrappedDecision = decision; this.methodology = methodology; this.version = version; this.startTime = Date.now(); // Set evaluationId on wrapped decision if it supports it if (this.wrappedDecision.evaluationId !== undefined) { this.wrappedDecision.evaluationId = this.evaluationId; } this.recordEvent(AuditEventType.EVALUATION_STARTED, { methodology: this.methodology, version: this.version, wrappedType: decision.constructor.name }); } /** * Delegate evaluation to wrapped decision with audit tracking */ evaluate(): SSVCOutcome { this.recordEvent(AuditEventType.EVALUATION_STARTED, { evaluationId: this.evaluationId }); const outcome = this.wrappedDecision.evaluate(); this.recordEvent(AuditEventType.EVALUATION_COMPLETED, { action: outcome.action, priority: outcome.priority, duration: Date.now() - this.startTime }); return outcome; } /** * Get cached outcome from wrapped decision */ get outcome(): SSVCOutcome | undefined { return this.wrappedDecision.outcome; } /** * Delegate vector generation to wrapped decision */ toVector(): string | undefined { if (this.wrappedDecision.toVector) { return this.wrappedDecision.toVector(); } return undefined; } /** * Attach evidence to a decision point */ attachEvidence(decisionPoint: string, evidence: DecisionPointEvidence): void { this.evidenceMap.set(decisionPoint, evidence); this.recordEvent(AuditEventType.EVIDENCE_COLLECTED, { decisionPoint, evidenceId: evidence.verifications[0]?.evidence?.evidenceId || 'unknown', verificationsCount: evidence.verifications.length }); } /** * Map a data source to a decision point */ mapDataSource(decisionPoint: string, dataSource: DataSource): void { this.dataSourceMap.set(decisionPoint, dataSource); this.recordEvent(AuditEventType.DATA_SOURCE_ACCESSED, { decisionPoint, sourceId: dataSource.sourceId, sourceName: dataSource.name, sourceType: dataSource.type }); } /** * Generate complete audit entry */ getAuditEntry(): AuditEntry { const outcome = this.outcome || { action: 'UNKNOWN', priority: 'UNKNOWN' }; return { auditId: crypto.randomUUID(), timestamp: Date.now(), methodology: this.methodology, methodologyVersion: this.version, evaluationId: this.evaluationId, parameters: this.extractParameters(), outcome, decisionEvidence: Array.from(this.evidenceMap.values()), dataMappings: [], // Would be populated if available checksum: this.generateChecksum() }; } /** * Generate forensic report */ generateForensicReport(): ForensicReport { const outcome = this.outcome || { action: 'UNKNOWN', priority: 'UNKNOWN' }; return { reportId: crypto.randomUUID(), generatedAt: Date.now(), evaluationId: this.evaluationId, decisionPath: this.buildDecisionPath(), finalOutcome: outcome, dataSourcesUsed: this.buildDataSourceUsage(), dataSourceSummary: this.buildDataSourceSummary(), timeline: this.timeline, totalDuration: Date.now() - this.startTime, verificationSummary: this.buildVerificationSummary() }; } /** * Get the wrapped decision (for advanced use cases) */ getWrappedDecision(): SSVCDecision { return this.wrappedDecision; } /** * Get all collected evidence */ getEvidence(): Record<string, DecisionPointEvidence> { return Object.fromEntries(this.evidenceMap); } /** * Get all mapped data sources */ getDataSources(): Record<string, DataSource> { return Object.fromEntries(this.dataSourceMap); } /** * Get audit timeline */ getTimeline(): TimelineEntry[] { return [...this.timeline]; } /** * Record an audit event */ private recordEvent(type: AuditEventType, details: Record<string, any>): void { const event: AuditEvent = { type, timestamp: Date.now(), details, evaluationId: this.evaluationId }; this.auditEvents.push(event); // Add to timeline this.timeline.push({ timestamp: event.timestamp, event: type, details }); } /** * Extract parameters from wrapped decision (best effort) */ private extractParameters(): Record<string, any> { // This is a best-effort extraction // Different decision types may store parameters differently const decision = this.wrappedDecision as any; // Try common property names if (decision.parameters) { return decision.parameters; } if (decision.options) { return decision.options; } // For runtime decisions, try to extract from the decision object if (decision.getParameters && typeof decision.getParameters === 'function') { return decision.getParameters(); } // Extract from direct properties (for generated decisions) const params: Record<string, any> = {}; for (const [key, value] of Object.entries(decision)) { if (typeof value !== 'function' && !key.startsWith('_') && key !== 'outcome' && key !== 'evaluationId') { params[key] = value; } } return params; } /** * Build decision path from collected evidence */ private buildDecisionPath(): ForensicReport['decisionPath'] { const decisionPath = []; let order = 0; for (const [decisionPoint, evidence] of this.evidenceMap) { decisionPath.push({ decisionPoint, value: evidence.value, evidence, order: order++ }); } return decisionPath; } /** * Build data source usage information */ private buildDataSourceUsage(): ForensicReport['dataSourcesUsed'] { const usage = []; for (const [decisionPoint, dataSource] of this.dataSourceMap) { usage.push({ source: dataSource, accessed: true, // Assume accessed if mapped result: 'Data extracted successfully', accessTime: this.startTime, }); } return usage; } /** * Build data source usage summary */ private buildDataSourceSummary(): ForensicReport['dataSourceSummary'] { const sourceTypes: Record<string, number> = {}; for (const dataSource of this.dataSourceMap.values()) { sourceTypes[dataSource.type] = (sourceTypes[dataSource.type] || 0) + 1; } return { totalSources: this.dataSourceMap.size, successfulAccesses: this.dataSourceMap.size, failedAccesses: 0, sourcesByType: sourceTypes }; } /** * Build verification summary */ private buildVerificationSummary(): ForensicReport['verificationSummary'] { let totalVerifications = 0; let successfulVerifications = 0; const bySource: Record<string, number> = {}; for (const evidence of this.evidenceMap.values()) { totalVerifications += evidence.verifications.length; for (const verification of evidence.verifications) { successfulVerifications++; // Assume all are successful for now bySource[verification.source] = (bySource[verification.source] || 0) + 1; } } return { totalVerifications, successfulVerifications, failedVerifications: totalVerifications - successfulVerifications, verificationsBySource: bySource }; } /** * Generate checksum for audit entry integrity */ private generateChecksum(): string { // Simple checksum based on key audit data // In production, use a proper cryptographic hash const data = { evaluationId: this.evaluationId, methodology: this.methodology, version: this.version, outcome: this.outcome, evidenceCount: this.evidenceMap.size }; return Buffer.from(JSON.stringify(data)).toString('base64'); } } /** * Factory functions for creating auditable decisions */ export class AuditableDecisionFactory { /** * Wrap any existing decision to make it auditable */ static wrapDecision( decision: SSVCDecision, methodology: string, version: string ): AuditableDecision { return new AuditableDecisionWrapper(decision, methodology, version); } /** * Create an auditable decision from a generated plugin */ static fromGeneratedPlugin( plugin: any, parameters: Record<string, any> ): AuditableDecision { const decision = plugin.createDecision(parameters); return new AuditableDecisionWrapper(decision, plugin.name, plugin.version); } /** * Create an auditable decision from runtime YAML */ static fromRuntimeYAML( yamlContent: string, parameters: Record<string, any> ): AuditableDecision { try { // Parse YAML to extract methodology info const methodologyData = yaml.parse(yamlContent); const methodology = methodologyData.name || 'unknown'; const version = methodologyData.version || '1.0'; // Create runtime decision const decision = createRuntimeDecision(yamlContent, parameters); // Wrap with audit capabilities return new AuditableDecisionWrapper(decision, methodology, version); } catch (error) { throw new Error(`Failed to create auditable decision from YAML: ${error instanceof Error ? error.message : String(error)}`); } } /** * Wrap an existing decision with pre-configured evidence and data sources */ static wrapWithEvidence( decision: SSVCDecision, methodology: string, version: string, evidence: Map<string, DecisionPointEvidence>, dataSources: Map<string, DataSource> ): AuditableDecision { const wrapper = new AuditableDecisionWrapper(decision, methodology, version); // Attach all evidence for (const [decisionPoint, evidenceData] of evidence) { wrapper.attachEvidence(decisionPoint, evidenceData); } // Map all data sources for (const [decisionPoint, dataSource] of dataSources) { wrapper.mapDataSource(decisionPoint, dataSource); } return wrapper; } } /** * Utility functions for working with auditable decisions */ export class AuditableDecisionUtils { /** * Check if a decision is auditable */ static isAuditable(decision: SSVCDecision): decision is AuditableDecision { return decision.evaluationId !== undefined && typeof (decision as any).attachEvidence === 'function'; } /** * Get audit information from a decision if available */ static getAuditInfo(decision: SSVCDecision): { isAuditable: boolean; evaluationId?: string; evidenceCount?: number; dataSourceCount?: number; } { if (this.isAuditable(decision)) { const wrapper = decision as AuditableDecisionWrapper; return { isAuditable: true, evaluationId: decision.evaluationId, evidenceCount: wrapper.getEvidence ? Object.keys(wrapper.getEvidence()).length : 0, dataSourceCount: wrapper.getDataSources ? Object.keys(wrapper.getDataSources()).length : 0 }; } return { isAuditable: false, evaluationId: decision.evaluationId }; } /** * Compare two decisions for audit purposes */ static compareDecisions( decision1: SSVCDecision, decision2: SSVCDecision ): { sameEvaluationId: boolean; sameOutcome: boolean; bothAuditable: boolean; } { return { sameEvaluationId: decision1.evaluationId === decision2.evaluationId, sameOutcome: JSON.stringify(decision1.outcome) === JSON.stringify(decision2.outcome), bothAuditable: this.isAuditable(decision1) && this.isAuditable(decision2) }; } }