import type { JsonValue } from './canonical-model'; export type CveMetricVersion = '2.0' | '3.0' | '3.1' | '4.0'; export interface CveMetric { version: CveMetricVersion; source: string; type?: string; baseScore: number; baseSeverity?: string; vector: string; exploitabilityScore?: number; impactScore?: number; } export interface CveReference { url: string; source?: string; tags: string[]; } export interface CveWeakness { source: string; type?: string; id: string; description?: string; } export interface CveConfigurationMatch { criteria: string; matchCriteriaId?: string; versionStartIncluding?: string; versionStartExcluding?: string; versionEndIncluding?: string; versionEndExcluding?: string; } export interface CveAffectedConfiguration { operator: 'AND' | 'OR'; negate: boolean; vulnerableMatches: CveConfigurationMatch[]; runningOnWith: CveConfigurationMatch[]; } /** Version-1 records flattened NVD matches and remain readable until refreshed. */ export interface LegacyCveAffectedConfiguration extends CveConfigurationMatch { vulnerable: boolean; } /** * Tenant-independent public vulnerability knowledge. Scanner occurrences keep * their own source values; this record is a separately refreshable projection * of one external authority keyed only by the normalized CVE identifier. */ export interface CveEnrichmentRecord { version: 1 | 2; cveId: string; source: 'nvd'; sourceIdentifier: string; sourceUrl: string; apiVersion: string; vulnerabilityStatus: string; publishedAt: string; lastModifiedAt: string; fetchedAt: string; description?: string; metrics: CveMetric[]; weaknesses: CveWeakness[]; references: CveReference[]; affectedConfigurations: Array; /** Preserves small NVD additions that ASSESSOR does not yet normalize. */ attributes: Record; } const cvePattern = /^CVE-\d{4}-\d{4,}$/; export function normalizeCveId(value: string): string | null { const normalized = value.trim().toLocaleUpperCase('en-US'); return cvePattern.test(normalized) ? normalized : null; } export function cveIdFromRuleId(value: string | undefined): string | null { return value ? normalizeCveId(value) : null; } /** Derives the public MITRE CWE detail page for a `CWE-` weakness id, or null for non-CWE ids. */ export function cweDetailUrl(weaknessId: string): string | null { const match = /^CWE-(\d+)$/i.exec(weaknessId.trim()); return match ? `https://cwe.mitre.org/data/definitions/${match[1]}.html` : null; } export function cveEnrichmentValidationErrors(record: CveEnrichmentRecord): string[] { const errors: string[] = []; if (normalizeCveId(record.cveId) !== record.cveId) errors.push('CVE ID must be normalized.'); if (record.source !== 'nvd') errors.push('Only NVD enrichment is supported.'); if (!record.sourceIdentifier.trim()) errors.push('NVD source identifier is required.'); if (!record.sourceUrl.startsWith('https://nvd.nist.gov/vuln/detail/')) errors.push('NVD detail URL is invalid.'); if (![record.publishedAt, record.lastModifiedAt, record.fetchedAt].every(value => Number.isFinite(Date.parse(value)))) { errors.push('CVE timestamps must be valid ISO dates.'); } if (record.metrics.some(metric => !Number.isFinite(metric.baseScore) || metric.baseScore < 0 || metric.baseScore > 10 || !metric.vector.trim())) { errors.push('CVSS metrics contain an invalid score or vector.'); } if (record.references.some(reference => !/^https?:\/\//i.test(reference.url))) errors.push('CVE reference URL is invalid.'); return errors; }