107 lines
3.6 KiB
TypeScript
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;
|
|
}
|