Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/license/licenses_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* @see https://github.com/guacsec/trustify-da-api-spec/blob/main/api/v5/openapi.yaml
*/

import { PackageURL } from 'packageurl-js';

Check warning on line 8 in src/license/licenses_api.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

There should be at least one empty line between import groups

Check warning on line 8 in src/license/licenses_api.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

There should be at least one empty line between import groups
import { selectTrustifyDABackend } from '../index.js';
import { addProxyAgent, getTokenHeaders } from '../tools.js';

Expand Down Expand Up @@ -42,6 +43,11 @@
}
}

function normalizePurlString(purl) {
const parsed = PackageURL.fromString(purl);
return new PackageURL(parsed.type, parsed.namespace, parsed.name, parsed.version, null, null).toString();
}

/**
* Normalize the LicensesResponse shape (array of LicenseProviderResult) into a map of purl -> license info.
* Each provider result has { status, summary, packages } where packages is { [purl]: { concluded, evidence } }.
Expand All @@ -55,6 +61,8 @@
const map = new Map();
if (!data || !Array.isArray(data)) {return map;}

const normalizedPurlsSet = purls.length > 0 ? new Set(purls.map(normalizePurlString)) : null;

for (const providerResult of data) {
const packages = providerResult?.packages;
if (!packages || typeof packages !== 'object') {continue;}
Expand All @@ -64,8 +72,9 @@
const expression = concluded?.expression;
const licenses = identifiers.length > 0 ? identifiers : (expression ? [expression] : []);
const category = concluded?.category; // PERMISSIVE | WEAK_COPYLEFT | STRONG_COPYLEFT | UNKNOWN
if (purls.length === 0 || purls.includes(purl)) {
map.set(purl, { licenses: licenses.filter(Boolean), category });
const normalizedPurl = normalizePurlString(purl);
if (normalizedPurlsSet === null || normalizedPurlsSet.has(normalizedPurl)) {

Check warning on line 76 in src/license/licenses_api.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

Expected '==' and instead saw '==='

Check warning on line 76 in src/license/licenses_api.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

Expected '==' and instead saw '==='
map.set(normalizedPurl, { licenses: licenses.filter(Boolean), category });
}
}
// Use first provider that has packages; backend may return multiple (e.g. deps.dev)
Expand Down
12 changes: 0 additions & 12 deletions src/license/project_license.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,3 @@ export async function identifyLicense(licenseFilePath, opts = {}) {
return null; // Fallback to local detection on error
}
}

/**
* Get license for SBOM root component with fallback to LICENSE file.
* Priority: manifest license > LICENSE file > null
* This is used by providers to populate the license field in the SBOM.
* @param {string} manifestPath - path to manifest
* @returns {string|null} - SPDX identifier or null
*/
export function getLicenseForSbom(manifestPath) {
const { fromManifest, fromFile } = getProjectLicense(manifestPath);
return fromManifest || fromFile || null;
}
50 changes: 50 additions & 0 deletions test/providers/license.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,56 @@
import Javascript_pnpm from '../../src/providers/javascript_pnpm.js'
import Javascript_yarn from '../../src/providers/javascript_yarn.js'
import pythonPipProvider from '../../src/providers/python_pip.js'
import { normalizeLicensesResponse } from '../../src/license/licenses_api.js'

Check warning on line 13 in test/providers/license.test.js

View workflow job for this annotation

GitHub Actions / Lint and test project (24)

`../../src/license/licenses_api.js` import should occur before import of `../../src/providers/golang_gomodules.js`

Check warning on line 13 in test/providers/license.test.js

View workflow job for this annotation

GitHub Actions / Lint and test project (22)

`../../src/license/licenses_api.js` import should occur before import of `../../src/providers/golang_gomodules.js`

suite('normalizeLicensesResponse', () => {
const lgpl = { id: 'LGPL-2.1', name: 'GNU Lesser General Public License v2.1 only', category: 'WEAK_COPYLEFT' }
const apache = { id: 'Apache-2.0', name: 'Apache License 2.0', category: 'PERMISSIVE' }

const backendResponse = [
{
status: { ok: true, name: 'deps.dev' },
packages: {
// backend returns purls with ?scope=compile qualifier
'pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4?scope=compile': {
concluded: { identifiers: [lgpl], expression: 'LGPL-2.1', category: 'WEAK_COPYLEFT' }
},
'pkg:maven/javassist/javassist@3.12.1.GA?scope=compile': {
concluded: { identifiers: [lgpl], expression: 'LGPL-2.1', category: 'WEAK_COPYLEFT' }
},
'pkg:maven/commons-collections/commons-collections@3.2.1': {
concluded: { identifiers: [apache], expression: 'Apache-2.0', category: 'PERMISSIVE' }
}
}
}
]

// SBOM purls have no qualifier
const sbomPurls = [
'pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4',
'pkg:maven/javassist/javassist@3.12.1.GA',
'pkg:maven/commons-collections/commons-collections@3.2.1'
]

test('matches backend purls with ?scope=compile qualifier against plain SBOM purls', () => {
const map = normalizeLicensesResponse(backendResponse, sbomPurls)
expect(map.size).to.equal(3)
})

test('stores purl without qualifier as map key', () => {
const map = normalizeLicensesResponse(backendResponse, sbomPurls)
expect(map.has('pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4')).to.be.true
expect(map.has('pkg:maven/javassist/javassist@3.12.1.GA')).to.be.true
expect(map.has('pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4?scope=compile')).to.be.false
})

test('preserves correct license category for qualifier-stripped purls', () => {
const map = normalizeLicensesResponse(backendResponse, sbomPurls)
expect(map.get('pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4').category).to.equal('WEAK_COPYLEFT')
expect(map.get('pkg:maven/javassist/javassist@3.12.1.GA').category).to.equal('WEAK_COPYLEFT')
expect(map.get('pkg:maven/commons-collections/commons-collections@3.2.1').category).to.equal('PERMISSIVE')
})
})

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

Expand Down
Loading