Assessor_2.0/src/domain/cve-enrichment.ts

107 lines
3.6 KiB
TypeScript

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<CveAffectedConfiguration | LegacyCveAffectedConfiguration>;
/** Preserves small NVD additions that ASSESSOR does not yet normalize. */
attributes: Record<string, JsonValue>;
}
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-<number>` 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;
}