Skip to content

Commit cb4ae28

Browse files
soul2zimateclaude
andauthored
fix: use PackageURL to canonicalize purls when matching deps.dev license response to SBOM purls (#414)
* fix: use PackageURL to canonicalize purls when matching deps.dev license response to SBOM purls The deps.dev backend returns package purls with qualifiers like ?scope=compile (e.g. pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4?scope=compile), while the SBOM generated by the client uses plain purls without qualifiers. The strict purls.includes(purl) check in normalizeLicensesResponse never matched qualifier-suffixed purls, causing incompatibleDependencies to always be empty even when LGPL/copyleft dependencies were present. Fix by using PackageURL.fromString() to parse purls and reconstructing them without qualifiers or subpath via new PackageURL(..., null, null). This produces canonical core purls for both the allowed set and the backend keys, making the match resilient to any qualifiers the backend may return. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove unused getLicenseForSbom function Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 29f6867 commit cb4ae28

3 files changed

Lines changed: 61 additions & 14 deletions

File tree

src/license/licenses_api.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* @see https://github.com/guacsec/trustify-da-api-spec/blob/main/api/v5/openapi.yaml
66
*/
77

8+
import { PackageURL } from 'packageurl-js';
89
import { selectTrustifyDABackend } from '../index.js';
910
import { addProxyAgent, getTokenHeaders } from '../tools.js';
1011

@@ -42,6 +43,11 @@ export async function getLicenseDetails(spdxId, opts = {}) {
4243
}
4344
}
4445

46+
function normalizePurlString(purl) {
47+
const parsed = PackageURL.fromString(purl);
48+
return new PackageURL(parsed.type, parsed.namespace, parsed.name, parsed.version, null, null).toString();
49+
}
50+
4551
/**
4652
* Normalize the LicensesResponse shape (array of LicenseProviderResult) into a map of purl -> license info.
4753
* Each provider result has { status, summary, packages } where packages is { [purl]: { concluded, evidence } }.
@@ -55,6 +61,8 @@ export function normalizeLicensesResponse(data, purls = []) {
5561
const map = new Map();
5662
if (!data || !Array.isArray(data)) {return map;}
5763

64+
const normalizedPurlsSet = purls.length > 0 ? new Set(purls.map(normalizePurlString)) : null;
65+
5866
for (const providerResult of data) {
5967
const packages = providerResult?.packages;
6068
if (!packages || typeof packages !== 'object') {continue;}
@@ -64,8 +72,9 @@ export function normalizeLicensesResponse(data, purls = []) {
6472
const expression = concluded?.expression;
6573
const licenses = identifiers.length > 0 ? identifiers : (expression ? [expression] : []);
6674
const category = concluded?.category; // PERMISSIVE | WEAK_COPYLEFT | STRONG_COPYLEFT | UNKNOWN
67-
if (purls.length === 0 || purls.includes(purl)) {
68-
map.set(purl, { licenses: licenses.filter(Boolean), category });
75+
const normalizedPurl = normalizePurlString(purl);
76+
if (normalizedPurlsSet === null || normalizedPurlsSet.has(normalizedPurl)) {
77+
map.set(normalizedPurl, { licenses: licenses.filter(Boolean), category });
6978
}
7079
}
7180
// Use first provider that has packages; backend may return multiple (e.g. deps.dev)

src/license/project_license.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,3 @@ export async function identifyLicense(licenseFilePath, opts = {}) {
7272
return null; // Fallback to local detection on error
7373
}
7474
}
75-
76-
/**
77-
* Get license for SBOM root component with fallback to LICENSE file.
78-
* Priority: manifest license > LICENSE file > null
79-
* This is used by providers to populate the license field in the SBOM.
80-
* @param {string} manifestPath - path to manifest
81-
* @returns {string|null} - SPDX identifier or null
82-
*/
83-
export function getLicenseForSbom(manifestPath) {
84-
const { fromManifest, fromFile } = getProjectLicense(manifestPath);
85-
return fromManifest || fromFile || null;
86-
}

test/providers/license.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,56 @@ import Javascript_npm from '../../src/providers/javascript_npm.js'
1010
import Javascript_pnpm from '../../src/providers/javascript_pnpm.js'
1111
import Javascript_yarn from '../../src/providers/javascript_yarn.js'
1212
import pythonPipProvider from '../../src/providers/python_pip.js'
13+
import { normalizeLicensesResponse } from '../../src/license/licenses_api.js'
14+
15+
suite('normalizeLicensesResponse', () => {
16+
const lgpl = { id: 'LGPL-2.1', name: 'GNU Lesser General Public License v2.1 only', category: 'WEAK_COPYLEFT' }
17+
const apache = { id: 'Apache-2.0', name: 'Apache License 2.0', category: 'PERMISSIVE' }
18+
19+
const backendResponse = [
20+
{
21+
status: { ok: true, name: 'deps.dev' },
22+
packages: {
23+
// backend returns purls with ?scope=compile qualifier
24+
'pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4?scope=compile': {
25+
concluded: { identifiers: [lgpl], expression: 'LGPL-2.1', category: 'WEAK_COPYLEFT' }
26+
},
27+
'pkg:maven/javassist/javassist@3.12.1.GA?scope=compile': {
28+
concluded: { identifiers: [lgpl], expression: 'LGPL-2.1', category: 'WEAK_COPYLEFT' }
29+
},
30+
'pkg:maven/commons-collections/commons-collections@3.2.1': {
31+
concluded: { identifiers: [apache], expression: 'Apache-2.0', category: 'PERMISSIVE' }
32+
}
33+
}
34+
}
35+
]
36+
37+
// SBOM purls have no qualifier
38+
const sbomPurls = [
39+
'pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4',
40+
'pkg:maven/javassist/javassist@3.12.1.GA',
41+
'pkg:maven/commons-collections/commons-collections@3.2.1'
42+
]
43+
44+
test('matches backend purls with ?scope=compile qualifier against plain SBOM purls', () => {
45+
const map = normalizeLicensesResponse(backendResponse, sbomPurls)
46+
expect(map.size).to.equal(3)
47+
})
48+
49+
test('stores purl without qualifier as map key', () => {
50+
const map = normalizeLicensesResponse(backendResponse, sbomPurls)
51+
expect(map.has('pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4')).to.be.true
52+
expect(map.has('pkg:maven/javassist/javassist@3.12.1.GA')).to.be.true
53+
expect(map.has('pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4?scope=compile')).to.be.false
54+
})
55+
56+
test('preserves correct license category for qualifier-stripped purls', () => {
57+
const map = normalizeLicensesResponse(backendResponse, sbomPurls)
58+
expect(map.get('pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4').category).to.equal('WEAK_COPYLEFT')
59+
expect(map.get('pkg:maven/javassist/javassist@3.12.1.GA').category).to.equal('WEAK_COPYLEFT')
60+
expect(map.get('pkg:maven/commons-collections/commons-collections@3.2.1').category).to.equal('PERMISSIVE')
61+
})
62+
})
1363

1464
suite('testing readLicenseFromManifest with existing test manifests', () => {
1565

0 commit comments

Comments
 (0)