diff --git a/src/domain/cve-enrichment.ts b/src/domain/cve-enrichment.ts index ac20c26..1970a0c 100644 --- a/src/domain/cve-enrichment.ts +++ b/src/domain/cve-enrichment.ts @@ -83,6 +83,12 @@ 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.'); diff --git a/src/features/project-import/canonical-model-to-dataset.ts b/src/features/project-import/canonical-model-to-dataset.ts index 17add01..b790b33 100644 --- a/src/features/project-import/canonical-model-to-dataset.ts +++ b/src/features/project-import/canonical-model-to-dataset.ts @@ -5,7 +5,7 @@ import { makeSheet } from '../../importers/sheet-factory'; import { stableSheetId } from '../../shared/identifiers'; import { groupProjectImportFiles } from './datahub-import-groups'; import type { CveAffectedConfiguration, CveConfigurationMatch, CveEnrichmentRecord } from '../../domain/cve-enrichment'; -import { cveIdFromRuleId } from '../../domain/cve-enrichment'; +import { cveIdFromRuleId, cweDetailUrl } from '../../domain/cve-enrichment'; import { canonicalSecurityScores } from '../../scoring/canonical-security-score'; import { defaultSecurityScoringPolicy } from '../../domain/security-scoring'; @@ -111,9 +111,11 @@ export function cveFieldsForDisplay(record: CveEnrichmentRecord): OriginalField[ '| --- | --- | ---: | --- | --- |', ...record.metrics.map(metric => `| ${markdownCell(metric.version)} | ${markdownCell(metric.source)} | ${metric.baseScore} | ${markdownCell(metric.baseSeverity ?? '')} | ${markdownCell(metric.vector)} |`) ].join('\n') : ''); - add('nvd-weaknesses', 'NVD Weaknesses', record.weaknesses.map(item => - `- ${item.id}${item.description ? ` — ${item.description}` : ''} (${item.source})` - ).join('\n')); + add('nvd-weaknesses', 'NVD Weaknesses', record.weaknesses.map(item => { + const url = cweDetailUrl(item.id); + const label = url ? `[${markdownCell(item.id)}](${url})` : markdownCell(item.id); + return `- ${label}${item.description ? ` — ${markdownCell(item.description)}` : ''} (${markdownCell(item.source)})`; + }).join('\n')); add('nvd-references', 'NVD References', record.references.length ? [ '| Reference | Source | Tags |', '| --- | --- | --- |', diff --git a/src/features/results-browser/ResultsBrowser.tsx b/src/features/results-browser/ResultsBrowser.tsx index af3f363..e3198d1 100644 --- a/src/features/results-browser/ResultsBrowser.tsx +++ b/src/features/results-browser/ResultsBrowser.tsx @@ -8,6 +8,7 @@ import { browserDownloadAdapter } from '../../platform/browser/browser-download- import { downloadText } from '../../exports/text-download'; import { buildUnifiedResultJsonl, queryAllUnifiedResults, unifiedResultJsonlFileName } from '../../exports/unified-result-jsonl'; import { hasSelectedTimeslotScope } from '../../app/work-scope'; +import { cweDetailUrl } from '../../domain/cve-enrichment'; const assessorApi = new AssessorApiClient(); const PAGE_SIZE = 50; @@ -273,7 +274,13 @@ function ResultRow({ item, expanded, onToggle, onUpdated }: {
  • Metrics: {item.cveEnrichment.metrics.map(metric => `CVSS ${metric.version} ${metric.baseScore}${metric.baseSeverity ? ` ${metric.baseSeverity}` : ''} (${metric.source})` ).join(' · ') || 'Not supplied'}
  • -
  • Weaknesses: {item.cveEnrichment.weaknesses.map(weakness => weakness.id).join(', ') || 'Not supplied'}
  • +
  • Weaknesses: {item.cveEnrichment.weaknesses.length ? item.cveEnrichment.weaknesses.map((weakness, index) => { + const url = cweDetailUrl(weakness.id); + return + {index > 0 ? ', ' : ''} + {url ? {weakness.id} : weakness.id} + ; + }) : 'Not supplied'}
  • {item.cveEnrichment.references.length > 0 &&
    Advisories, solutions, and tools ({item.cveEnrichment.references.length.toLocaleString('en-US')}) diff --git a/src/scoring/canonical-security-score.ts b/src/scoring/canonical-security-score.ts index cd9a302..047869a 100644 --- a/src/scoring/canonical-security-score.ts +++ b/src/scoring/canonical-security-score.ts @@ -19,7 +19,7 @@ function scoreObservation(item: EnvironmentObservation): EnvironmentObservation export function securityToolId(value: string): string { const normalized = value.toLowerCase().replace(/[^a-z0-9]/g, ''); - return ({ purpleknight: 'purple-knight', pingcastle: 'pingcastle', ciscat: 'cis-cat', ciscatpro: 'cis-cat', maester: 'maester', gpos: 'group-policy', gpoanalysis: 'group-policy', grouppolicy: 'group-policy' } as Record)[normalized] ?? value.toLowerCase(); + return ({ purpleknight: 'purple-knight', pingcastle: 'pingcastle', ciscat: 'cis-cat', ciscatpro: 'cis-cat', maester: 'maester', gpos: 'group-policy', gpoanalysis: 'group-policy', grouppolicy: 'group-policy', defenderforiotvulnerabilities: 'defender-for-iot', microsoftdefenderforiot: 'defender-for-iot' } as Record)[normalized] ?? value.toLowerCase(); } function valueOf(item: EnvironmentObservation): JsonValue { diff --git a/tests/domain/cwe-detail-url.test.ts b/tests/domain/cwe-detail-url.test.ts new file mode 100644 index 0000000..e7c28cc --- /dev/null +++ b/tests/domain/cwe-detail-url.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; +import { cweDetailUrl } from '../../src/domain/cve-enrichment'; + +describe('CWE detail URL derivation', () => { + it('links a CWE- id to its MITRE definition page', () => { + expect(cweDetailUrl('CWE-119')).toBe('https://cwe.mitre.org/data/definitions/119.html'); + expect(cweDetailUrl('cwe-79')).toBe('https://cwe.mitre.org/data/definitions/79.html'); + expect(cweDetailUrl(' CWE-89 ')).toBe('https://cwe.mitre.org/data/definitions/89.html'); + }); + + it('returns null for non-CWE weakness ids', () => { + expect(cweDetailUrl('NVD-CWE-noinfo')).toBeNull(); + expect(cweDetailUrl('NVD-CWE-Other')).toBeNull(); + expect(cweDetailUrl('CWE-119 extra')).toBeNull(); + expect(cweDetailUrl('')).toBeNull(); + }); +});