Skip to content

Commit ef6b443

Browse files
soul2zimateclaude
andcommitted
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>
1 parent 29f6867 commit ef6b443

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

src/license/licenses_api.js

Lines changed: 14 additions & 1 deletion
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

@@ -51,10 +52,17 @@ export async function getLicenseDetails(spdxId, opts = {}) {
5152
* @param {string[]} [purls] - optional list of purls to restrict to (for consistency with getLicensesByPurl)
5253
* @returns {Map<string, { licenses: string[], category?: string }>}
5354
*/
55+
function getCorePurl(purl) {
56+
const parsed = PackageURL.fromString(purl);
57+
return new PackageURL(parsed.type, parsed.namespace, parsed.name, parsed.version, null, null).toString();
58+
}
59+
5460
export function normalizeLicensesResponse(data, purls = []) {
5561
const map = new Map();
5662
if (!data || !Array.isArray(data)) {return map;}
5763

64+
const allowed = purls.length > 0 ? new Set(purls.map(p => getCorePurl(p))) : null;
65+
5866
for (const providerResult of data) {
5967
const packages = providerResult?.packages;
6068
if (!packages || typeof packages !== 'object') {continue;}
@@ -64,8 +72,13 @@ 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)) {
75+
if (allowed === null) {
6876
map.set(purl, { licenses: licenses.filter(Boolean), category });
77+
} else {
78+
const corePurl = getCorePurl(purl);
79+
if (allowed.has(corePurl)) {
80+
map.set(corePurl, { licenses: licenses.filter(Boolean), category });
81+
}
6982
}
7083
}
7184
// Use first provider that has packages; backend may return multiple (e.g. deps.dev)

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)