diff --git a/src/application/evaluation-readiness.ts b/src/application/evaluation-readiness.ts index da9cbf5..98a59b4 100644 --- a/src/application/evaluation-readiness.ts +++ b/src/application/evaluation-readiness.ts @@ -19,6 +19,7 @@ import type { SignalFrequency, } from '../domain/contracts.js' import type { PackageNode, RiskSignal } from '../domain/entities.js' +import { isSecurityRelatedDeprecation } from '../domain/security-deprecation.js' // Export-readiness exclusions are single-reason buckets. This precedence keeps // counts deterministic even when one row fails multiple readiness checks. @@ -30,14 +31,6 @@ const EXPORT_EXCLUSION_ORDER = [ 'unavailable_field_dependency', ] as const -const SECURITY_DEPRECATION_PATTERNS = [ - /security vulnerability/i, - /security issue/i, - /critical vulnerability/i, - /cve-/i, - /cve /i, -] as const - interface EvaluationDatasetSummary { signal_frequency: SignalFrequency[] metadata_coverage: MetadataCoverageSummary @@ -319,7 +312,7 @@ export function buildEvaluationDatasetSummary(scanRecords: ScanReviewRecord[]): * @returns `true` when the message matches the evaluation security patterns. */ export function isSecurityRelatedDeprecationMessage(message: string): boolean { - return SECURITY_DEPRECATION_PATTERNS.some((pattern) => pattern.test(message)) + return isSecurityRelatedDeprecation(message) } function determineExportBlockingReasons({ diff --git a/src/domain/security-deprecation.ts b/src/domain/security-deprecation.ts new file mode 100644 index 0000000..152e53a --- /dev/null +++ b/src/domain/security-deprecation.ts @@ -0,0 +1,8 @@ +// Prefer structured CVE identifiers over bare "cve" so incidental references +// do not count as security-related deprecation evidence without an actual id. +const SECURITY_RELATED_DEPRECATION_PATTERN = + /\b(?:security|vulnerab(?:ility|ilities)|cve-\d{4}-\d+)\b/i + +export function isSecurityRelatedDeprecation(message: string): boolean { + return SECURITY_RELATED_DEPRECATION_PATTERN.test(message) +} diff --git a/src/domain/value-objects.ts b/src/domain/value-objects.ts index 01e81eb..7333d92 100644 --- a/src/domain/value-objects.ts +++ b/src/domain/value-objects.ts @@ -3,10 +3,10 @@ import { basename } from 'node:path' import type { BaselineIdentity, PackageSpec, ResolvedPackage, ScanMode } from './contracts.js' import type { Recommendation, RiskLevel, RiskSignal } from './entities.js' import { InvalidUsageError } from './errors.js' +import { isSecurityRelatedDeprecation } from './security-deprecation.js' export const DEFAULT_MAX_DEPTH = 3 export const DEFAULT_THRESHOLD = 0.4 -export const SECURITY_DEPRECATION_KEYWORDS = ['security', 'vulnerability', 'cve'] as const export const RISK_SIGNAL_WEIGHTS = { low: 0.08, medium: 0.16, @@ -187,7 +187,5 @@ export function hasSecurityDeprecationLanguage(message: string | null): boolean return false } - const normalizedMessage = message.toLowerCase() - - return SECURITY_DEPRECATION_KEYWORDS.some((keyword) => normalizedMessage.includes(keyword)) + return isSecurityRelatedDeprecation(message) } diff --git a/test/security-deprecation.test.ts b/test/security-deprecation.test.ts new file mode 100644 index 0000000..6c98366 --- /dev/null +++ b/test/security-deprecation.test.ts @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { isSecurityRelatedDeprecation } from '../src/domain/security-deprecation.js' + +test('shared security-deprecation classifier matches security vulnerability language', () => { + assert.equal( + isSecurityRelatedDeprecation('This version has a security vulnerability. Please upgrade.'), + true, + ) +}) + +test('shared security-deprecation classifier matches structured CVE identifiers', () => { + assert.equal( + isSecurityRelatedDeprecation('See CVE-2025-12345 for details.'), + true, + ) +}) + +test('shared security-deprecation classifier does not match bare cve references', () => { + assert.equal( + isSecurityRelatedDeprecation('See the cve guidance page for more information.'), + false, + ) +})