From 15918aea1f06b1500cf6877612e547c3bce5c613 Mon Sep 17 00:00:00 2001 From: Venancio Orozco <4390221+v3nant@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:25:24 -0600 Subject: [PATCH] fix(eol): classify unknown-metadata components as UNKNOWN eol-api now always returns a metadata object per component. Unknown/ unresolvable packages return a partial object with only a reason ({ unknownReason }) instead of the historical null. deriveComponentStatus only treated falsy metadata as UNKNOWN, so the new truthy { unknownReason } object fell through to the isEol/eolAt logic and was silently classified as OK ("Not EOL"). Detect the unknown reason before the isEol/eolAt logic and widen the type surface to model the new shape: - add UNKNOWN_REASONS / UnknownReason / UnknownComponentMetadata and the ComponentMetadata union to src/types/eol-scan.ts, plus an isUnknownReason guard; EolScanComponent.metadata becomes ComponentMetadata | null (| null kept so legacy null records still resolve to UNKNOWN) - deriveComponentStatus takes ComponentMetadata | null and returns UNKNOWN for null or any present unknownReason, narrowing to the known metadata shape for the existing EOL/EOL_UPCOMING/OK logic - export UNKNOWN_REASONS and isUnknownReason from the package root - cover the four reasons, an unrecognized reason, null, and isUnknownReason --- src/eol/utils.test.ts | 37 +++++++++++++++++++++++++++++++++++++ src/eol/utils.ts | 13 ++++++++++--- src/index.ts | 1 + src/types/eol-scan.ts | 24 +++++++++++++++++++++++- 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/eol/utils.test.ts b/src/eol/utils.test.ts index e6aad79..1ae75ef 100644 --- a/src/eol/utils.test.ts +++ b/src/eol/utils.test.ts @@ -6,6 +6,7 @@ import { createPurlIdentity, canonicalizeVersionFilter, } from './utils.ts'; +import { isUnknownReason, UNKNOWN_REASONS } from '../types/eol-scan.ts'; import type { EolScanComponentMetadata } from '../types/eol-scan.ts'; // These are required for the object but not used to derive the status @@ -84,6 +85,42 @@ describe('deriveComponentStatus', () => { const result = deriveComponentStatus(metadata); assert.equal(result, 'OK'); }); + + for (const unknownReason of UNKNOWN_REASONS) { + test(`should return UNKNOWN for unknownReason "${unknownReason}"`, () => { + const result = deriveComponentStatus({ unknownReason }); + assert.equal(result, 'UNKNOWN'); + }); + } + + test('should return UNKNOWN for an unrecognized unknownReason value', () => { + const result = deriveComponentStatus({ + unknownReason: 'something_else', + } as never); + assert.equal(result, 'UNKNOWN'); + }); +}); + +describe('isUnknownReason', () => { + for (const unknownReason of UNKNOWN_REASONS) { + test(`returns true for "${unknownReason}"`, () => { + assert.equal(isUnknownReason(unknownReason), true); + }); + } + + test('returns false for null', () => { + assert.equal(isUnknownReason(null), false); + }); + + test('returns false for an unrecognized string', () => { + assert.equal(isUnknownReason('something_else'), false); + }); + + test('returns false for non-string garbage', () => { + assert.equal(isUnknownReason(42), false); + assert.equal(isUnknownReason(undefined), false); + assert.equal(isUnknownReason({}), false); + }); }); describe('createPurlIdentity', () => { diff --git a/src/eol/utils.ts b/src/eol/utils.ts index aedd571..da27eab 100644 --- a/src/eol/utils.ts +++ b/src/eol/utils.ts @@ -1,8 +1,9 @@ import { PackageURL } from 'packageurl-js'; import type { CdxBom } from '../types/index.js'; import type { + ComponentMetadata, ComponentStatus, - EolScanComponentMetadata, + UnknownComponentMetadata, } from '../types/eol-scan.js'; // Maps non-canonical PURL type tokens to their canonical equivalents. @@ -243,10 +244,16 @@ export function canonicalizeVersionFilter< } as TFilter; } +function isUnknownComponentMetadata( + metadata: ComponentMetadata, +): metadata is UnknownComponentMetadata { + return 'unknownReason' in metadata && metadata.unknownReason != null; +} + export function deriveComponentStatus( - metadata: EolScanComponentMetadata | null, + metadata: ComponentMetadata | null, ): ComponentStatus { - if (!metadata) { + if (!metadata || isUnknownComponentMetadata(metadata)) { return 'UNKNOWN'; } diff --git a/src/index.ts b/src/index.ts index a7f0557..aa24d41 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ export type { } from './eol/utils.js'; export type * from './types/eol-scan.js'; +export { UNKNOWN_REASONS, isUnknownReason } from './types/eol-scan.js'; export type * from './types/index.js'; export { ComponentScope } from './types/index.js'; diff --git a/src/types/eol-scan.ts b/src/types/eol-scan.ts index c3144a1..e514f2f 100644 --- a/src/types/eol-scan.ts +++ b/src/types/eol-scan.ts @@ -25,6 +25,28 @@ export interface EolScanNextSupportedVersion { releasedAt: Date; } +export const UNKNOWN_REASONS = [ + 'not_identifiable', + 'no_listed_versions', + 'unsupported_ecosystem', + 'queued', +] as const; +export type UnknownReason = (typeof UNKNOWN_REASONS)[number]; + +export interface UnknownComponentMetadata { + unknownReason: UnknownReason; +} + +export type ComponentMetadata = + | EolScanComponentMetadata + | UnknownComponentMetadata; + +export function isUnknownReason(v: unknown): v is UnknownReason { + return ( + typeof v === 'string' && (UNKNOWN_REASONS as readonly string[]).includes(v) + ); +} + export interface NesRemediation { remediations: { purls: { nes: string; oss: string }; @@ -67,7 +89,7 @@ export interface Remediation { } export interface EolScanComponent { - metadata: EolScanComponentMetadata | null; + metadata: ComponentMetadata | null; purl: string; nesRemediation?: NesRemediation | null; remediations?: Remediation[] | null;