Skip to content

Commit b0cca4f

Browse files
committed
fix(parser): treat SPDX NOASSERTION/NONE version & supplier as unknown
SPDX generators frequently emit the spec sentinels "NOASSERTION" (and "NONE") for fields they can't determine. parseSPDX stored these verbatim, so a package with versionInfo "NOASSERTION" in the old SBOM and a real version in the new one was reported as a spurious upgrade ("NOASSERTION -> 2.0.0"), corrupting the tool's headline output. Normalize these sentinels (and empty strings) to undefined for the version and supplier fields, so the diff's "both versions known" guard correctly skips packages whose prior version was never asserted. License normalization is intentionally left to PR #32. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HvD9bHmeu7mjeUpLNKzovy
1 parent cb28a82 commit b0cca4f

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

src/__tests__/parser.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect } from 'vitest';
22
import { parse, detectFormat } from '../parser.js';
3+
import { diff } from '../diff.js';
34

45
const cyclonedxFixture = {
56
bomFormat: 'CycloneDX',
@@ -119,6 +120,34 @@ describe('parse (SPDX)', () => {
119120
expect(sbom.components[0].version).toBe('2.28.0');
120121
expect(sbom.components[0].license).toBe('Apache-2.0');
121122
});
123+
124+
it('treats NOASSERTION/NONE version and supplier sentinels as undefined', () => {
125+
const sbom = parse({
126+
spdxVersion: 'SPDX-2.3',
127+
name: 'my-service',
128+
packages: [
129+
{ name: 'internal-lib', versionInfo: 'NOASSERTION', supplier: 'NOASSERTION' },
130+
{ name: 'other-lib', versionInfo: 'NONE', supplier: 'Organization: Acme' },
131+
],
132+
});
133+
expect(sbom.components[0].version).toBeUndefined();
134+
expect(sbom.components[0].supplier).toBeUndefined();
135+
expect(sbom.components[1].version).toBeUndefined();
136+
expect(sbom.components[1].supplier).toBe('Organization: Acme');
137+
});
138+
139+
it('does not report a spurious upgrade when the old version is NOASSERTION', () => {
140+
const old = parse({
141+
spdxVersion: 'SPDX-2.3',
142+
packages: [{ name: 'internal-lib', versionInfo: 'NOASSERTION' }],
143+
});
144+
const next = parse({
145+
spdxVersion: 'SPDX-2.3',
146+
packages: [{ name: 'internal-lib', versionInfo: '2.0.0' }],
147+
});
148+
const report = diff(old, next);
149+
expect(report.upgraded).toHaveLength(0);
150+
});
122151
});
123152

124153
describe('parse (JSON string input)', () => {

src/parser.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ export function parseSPDX(obj: Record<string, unknown>): SBOM {
6262
const components: Component[] = packages.map((pkg: Record<string, unknown>) => ({
6363
purl: extractSPDXPurl(pkg),
6464
name: typeof pkg.name === 'string' ? pkg.name : 'unknown',
65-
version: typeof pkg.versionInfo === 'string' ? pkg.versionInfo : undefined,
65+
version: normalizeSPDXValue(pkg.versionInfo),
6666
license: typeof pkg.licenseConcluded === 'string' ? pkg.licenseConcluded : undefined,
6767
ecosystem: extractEcosystemFromPurl(extractSPDXPurl(pkg) ?? ''),
68-
supplier: typeof pkg.supplier === 'string' ? pkg.supplier : undefined,
68+
supplier: normalizeSPDXValue(pkg.supplier),
6969
}));
7070

7171
return {
@@ -161,6 +161,26 @@ function extractCycloneDXTimestamp(metadata: Record<string, unknown>): string |
161161
return typeof metadata.timestamp === 'string' ? metadata.timestamp : undefined;
162162
}
163163

164+
/**
165+
* Normalize an SPDX string field, treating the spec's `NOASSERTION` and `NONE`
166+
* sentinels as "no value" (undefined) rather than real data.
167+
*
168+
* These sentinels are extremely common in generator output (e.g. `versionInfo:
169+
* "NOASSERTION"` when a version can't be determined). Storing them verbatim
170+
* corrupts the diff: a package whose version is "NOASSERTION" in the old SBOM
171+
* and "2.0.0" in the new one is reported as an upgrade `NOASSERTION → 2.0.0`,
172+
* which is meaningless. Collapsing the sentinel to undefined lets the diff's
173+
* "both versions known" guard correctly skip it.
174+
*
175+
* (License normalization is handled separately — see PR #32.)
176+
*/
177+
function normalizeSPDXValue(value: unknown): string | undefined {
178+
if (typeof value !== 'string') return undefined;
179+
const trimmed = value.trim();
180+
if (trimmed === '' || trimmed === 'NOASSERTION' || trimmed === 'NONE') return undefined;
181+
return trimmed;
182+
}
183+
164184
function extractSPDXPurl(pkg: Record<string, unknown>): string | undefined {
165185
const refs = pkg.externalRefs;
166186
if (!Array.isArray(refs)) return undefined;

0 commit comments

Comments
 (0)