From ab7a421bde06ecc61cd47d92b9fb4807edcc65c3 Mon Sep 17 00:00:00 2001 From: saraeloop Date: Tue, 7 Apr 2026 20:23:29 -0700 Subject: [PATCH] refactor(classification): centralize security deprecation detection - add shared security deprecation classifier in domain - update scorer logic to use the shared classifier - update evaluation readiness logic to use the shared classifier - remove duplicated security deprecation matching logic - keep presentation logic unchanged so it continues to consume signal types only - add focused classifier tests for: - security vulnerability wording - structured CVE identifiers - rejecting unrelated bare cve mentions verification: - pnpm test - pnpm run build --- src/application/evaluation-readiness.ts | 11 ++--------- src/domain/security-deprecation.ts | 8 ++++++++ src/domain/value-objects.ts | 6 ++---- test/security-deprecation.test.ts | 25 +++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 src/domain/security-deprecation.ts create mode 100644 test/security-deprecation.test.ts 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, + ) +})