Skip to content

Commit d41831c

Browse files
committed
chore: refactor licenseCheck to each provider and make backendUrl usage consistent
Signed-off-by: Ruben Romero Montes <rromerom@redhat.com>
1 parent b41be0d commit d41831c

14 files changed

Lines changed: 149 additions & 173 deletions

src/analysis.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
5656
body: provided.content
5757
}, opts);
5858

59-
const finalUrl = new URL(`${url}/api/v4/analysis`);
59+
const finalUrl = new URL(`${url}/api/v5/analysis`);
6060
if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
6161
finalUrl.searchParams.append('recommend', 'false');
6262
}
@@ -118,7 +118,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
118118
body: provided.content
119119
}, opts);
120120

121-
const finalUrl = new URL(`${url}/api/v4/analysis`);
121+
const finalUrl = new URL(`${url}/api/v5/analysis`);
122122
if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
123123
finalUrl.searchParams.append('recommend', 'false');
124124
}
@@ -167,7 +167,7 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
167167
imageSboms[parsedImageRef.getPackageURL().toString()] = generateImageSBOM(parsedImageRef, opts)
168168
}
169169

170-
const finalUrl = new URL(`${url}/api/v4/batch-analysis`);
170+
const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
171171
if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
172172
finalUrl.searchParams.append('recommend', 'false');
173173
}
@@ -218,7 +218,7 @@ async function validateToken(url, opts = {}) {
218218
}
219219
}, opts);
220220

221-
let resp = await fetch(`${url}/api/v4/token`, fetchOptions)
221+
let resp = await fetch(`${url}/api/v5/token`, fetchOptions)
222222
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
223223
let exRequestId = resp.headers.get("ex-request-id");
224224
if (exRequestId) {

src/cli.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { hideBin } from 'yargs/helpers'
77

88
import { getProjectLicense, getLicenseDetails } from './license/index.js'
99

10-
import client from './index.js'
10+
import client, { selectTrustifyDABackend } from './index.js'
1111

1212

1313
// command for component analysis take manifest type and content
@@ -34,9 +34,8 @@ const validateToken = {
3434
builder: yargs => yargs.positional(
3535
'token-provider',
3636
{
37-
desc: 'the token provider',
38-
type: 'string',
39-
choices: ['snyk','oss-index'],
37+
desc: 'the token provider name',
38+
type: 'string'
4039
}
4140
).options({
4241
tokenValue: {
@@ -50,7 +49,7 @@ const validateToken = {
5049
let opts={}
5150
if(args['tokenValue'] !== undefined && args['tokenValue'].trim() !=="" ) {
5251
let tokenValue = args['tokenValue'].trim()
53-
opts[`TRUSTIFY_DA_${tokenProvider}_TOKEN`] = tokenValue
52+
opts[`TRUSTIFY_DA_PROVIDER_${tokenProvider}_TOKEN`] = tokenValue
5453
}
5554
let res = await client.validateToken(opts)
5655
console.log(res)
@@ -185,9 +184,11 @@ const license = {
185184
handler: async args => {
186185
let manifestPath = args['/path/to/manifest']
187186

188-
const backendUrl = process.env.TRUSTIFY_DA_BACKEND_URL
189-
if (!backendUrl) {
190-
console.error(JSON.stringify({ error: 'TRUSTIFY_DA_BACKEND_URL environment variable is required for the license command' }, null, 2))
187+
const opts = {} // CLI options can be extended in the future
188+
try {
189+
selectTrustifyDABackend(opts)
190+
} catch (err) {
191+
console.error(JSON.stringify({ error: err.message }, null, 2))
191192
process.exit(1)
192193
}
193194

@@ -200,7 +201,6 @@ const license = {
200201
}
201202

202203
const errors = []
203-
const opts = {} // CLI options can be extended in the future
204204

205205
// Build LicenseInfo objects
206206
const buildLicenseInfo = async (spdxId) => {
@@ -209,7 +209,7 @@ const license = {
209209
const licenseInfo = { spdxId }
210210

211211
try {
212-
const details = await getLicenseDetails(spdxId, backendUrl, opts)
212+
const details = await getLicenseDetails(spdxId, opts)
213213
if (details) {
214214
// Check if backend recognized the license as valid
215215
if (details.category === 'UNKNOWN') {

src/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as url from 'url';
99

1010
export { parseImageRef } from "./oci_image/utils.js";
1111
export { ImageRef } from "./oci_image/images.js";
12-
export { getProjectLicense, getProjectLicenseFromManifest as getProjectLicenseFromManifestOnly, findLicenseFilePath, identifyLicenseViaBackend, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";
12+
export { getProjectLicense, findLicenseFilePath, identifyLicenseViaBackend, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";
1313

1414
export default { componentAnalysis, stackAnalysis, imageAnalysis, validateToken }
1515

@@ -40,7 +40,6 @@ export default { componentAnalysis, stackAnalysis, imageAnalysis, validateToken
4040
* TRUSTIFY_DA_SYFT_PATH?: string | undefined,
4141
* TRUSTIFY_DA_YARN_PATH?: string | undefined,
4242
* TRUSTIFY_DA_LICENSE_CHECK?: string | undefined,
43-
* TRUSTIFY_DA_LICENSES_API_PATH?: string | undefined,
4443
* MATCH_MANIFEST_VERSIONS?: string | undefined,
4544
* TRUSTIFY_DA_SOURCE?: string | undefined,
4645
* TRUSTIFY_DA_TOKEN?: string | undefined,

src/license/index.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { getProjectLicense, findLicenseFilePath, identifyLicense } from './proje
66
import { licensesFromReport, getLicenseDetails } from './licenses_api.js';
77
import { getCompatibility } from './compatibility.js';
88

9-
export { getProjectLicense, getProjectLicenseFromManifest, findLicenseFilePath, identifyLicense as identifyLicenseViaBackend } from './project_license.js';
10-
export { licensesFromReport as licensesFromReport, normalizeLicensesResponse, getLicenseDetails } from './licenses_api.js';
9+
export { getProjectLicense, findLicenseFilePath, identifyLicense as identifyLicenseViaBackend } from './project_license.js';
10+
export { licensesFromReport, normalizeLicensesResponse, getLicenseDetails } from './licenses_api.js';
1111
export { getCompatibility } from './compatibility.js';
1212

1313
/**
@@ -16,21 +16,21 @@ export { getCompatibility } from './compatibility.js';
1616
*
1717
* @param {string} sbomContent - CycloneDX SBOM JSON string (the one sent for component analysis)
1818
* @param {string} manifestPath - path to manifest
19-
* @param {string} backendUrl - Trustify DA backend base URL
19+
* @param {string} url - the backend url to send the request to
2020
* @param {import('../index.js').Options} [opts={}]
2121
* @param {import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport} [analysisResult] - analysis result that includes licenses array from backend
2222
* @returns {Promise<{ projectLicense: { manifest: Object|null, file: Object|null, mismatch: boolean }, incompatibleDependencies: Array<{ purl: string, licenses: string[], category?: string, reason: string }>, error?: string }>}
2323
*/
24-
export async function runLicenseCheck(sbomContent, manifestPath, backendUrl, opts = {}, analysisResult = null) {
24+
export async function runLicenseCheck(sbomContent, manifestPath, url, opts = {}, analysisResult = null) {
2525
// Resolve project license from manifest and LICENSE file
2626
const projectLicense = getProjectLicense(manifestPath, opts);
2727

2828
// Try backend identification for LICENSE file (more accurate than local pattern matching)
2929
const licenseFilePath = findLicenseFilePath(manifestPath);
3030
let backendFileId = null;
31-
if (licenseFilePath && backendUrl) {
31+
if (licenseFilePath) {
3232
try {
33-
backendFileId = await identifyLicense(licenseFilePath, backendUrl, opts);
33+
backendFileId = await identifyLicense(licenseFilePath, { ...opts, TRUSTIFY_DA_BACKEND_URL: url });
3434
} catch {
3535
// Fall back to local detection
3636
}
@@ -45,11 +45,11 @@ export async function runLicenseCheck(sbomContent, manifestPath, backendUrl, opt
4545
const licenseDetailsCache = new Map();
4646

4747
async function getDetails(spdxId) {
48-
if (!spdxId || !backendUrl) return null;
48+
if (!spdxId || !url) return null;
4949
if (licenseDetailsCache.has(spdxId)) return licenseDetailsCache.get(spdxId);
5050

5151
try {
52-
const details = await getLicenseDetails(spdxId, backendUrl, opts);
52+
const details = await getLicenseDetails(spdxId, { ...opts, TRUSTIFY_DA_BACKEND_URL: url });
5353
licenseDetailsCache.set(spdxId, details);
5454
return details;
5555
} catch {

src/license/licenses_api.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,22 @@
77

88
import { HttpsProxyAgent } from 'https-proxy-agent';
99

10+
import { selectTrustifyDABackend } from '../index.js';
1011
import { getCustom , getTokenHeaders } from '../tools.js';
1112

1213
/**
1314
* Fetch license details by SPDX identifier from the backend GET /api/v5/licenses/{spdx}.
1415
* Returns detailed information about a specific license including category, name, and text.
1516
*
1617
* @param {string} spdxId - SPDX identifier (e.g., "Apache-2.0", "MIT")
17-
* @param {string} backendUrl - base URL of the Trustify DA backend (no trailing slash)
18-
* @param {import('../index.js').Options} [opts={}] - options (proxy, token, etc.)
18+
* @param {import('../index.js').Options} [opts={}] - options (proxy, token, TRUSTIFY_DA_BACKEND_URL, etc.)
1919
* @returns {Promise<Object|null>} License details or null if not found
2020
*/
21-
export async function getLicenseDetails(spdxId, backendUrl, opts = {}) {
21+
export async function getLicenseDetails(spdxId, opts = {}) {
2222
if (!spdxId) {return null;}
2323

24-
const url = `${backendUrl.replace(/\/$/, '')}/api/v5/licenses/${encodeURIComponent(spdxId)}`;
24+
const url = selectTrustifyDABackend(opts);
25+
const finalUrl = new URL(`${url}/api/v5/licenses/${encodeURIComponent(spdxId)}`);
2526

2627
const fetchOptions = {
2728
method: 'GET',
@@ -37,7 +38,7 @@ export async function getLicenseDetails(spdxId, backendUrl, opts = {}) {
3738
}
3839

3940
try {
40-
const resp = await fetch(url, fetchOptions);
41+
const resp = await fetch(finalUrl, fetchOptions);
4142
if (!resp.ok) {
4243
const errorText = await resp.text().catch(() => '');
4344
throw new Error(`HTTP ${resp.status}: ${errorText || resp.statusText}`);

src/license/project_license.js

Lines changed: 9 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,12 @@
66
import fs from 'node:fs';
77
import path from 'node:path';
88

9-
import { XMLParser } from 'fast-xml-parser';
10-
9+
import { selectTrustifyDABackend } from '../index.js';
10+
import { matchForLicense, availableProviders } from '../provider.js';
1111
import { getCustom, getTokenHeaders } from '../tools.js';
1212

1313
const LICENSE_FILES = ['LICENSE', 'LICENSE.md', 'LICENSE.txt'];
1414

15-
/**
16-
* Resolve project license from the manifest file only (no LICENSE file).
17-
* @param {string} manifestPath - path to manifest (e.g. package.json, pom.xml)
18-
* @param {{}} [opts={}] - options (for getCustom, etc.)
19-
* @returns {{ fromManifest: string|null, fromFile: null, mismatch: false }}
20-
*/
21-
export function getProjectLicenseFromManifest(manifestPath, opts = {}) {
22-
const fromManifest = readLicenseFromManifest(manifestPath, opts);
23-
return {
24-
fromManifest: fromManifest || null,
25-
fromFile: null,
26-
mismatch: false
27-
};
28-
}
29-
3015
/**
3116
* Resolve project license from manifest and from LICENSE / LICENSE.md in manifest dir or git root.
3217
* Uses local pattern matching for LICENSE file identification (synchronous).
@@ -35,8 +20,10 @@ export function getProjectLicenseFromManifest(manifestPath, opts = {}) {
3520
* @returns {{ fromManifest: string|null, fromFile: string|null, mismatch: boolean }}
3621
*/
3722
export function getProjectLicense(manifestPath) {
38-
const fromManifest = readLicenseFromManifest(manifestPath);
39-
const fromFile = readLicenseFromFile(manifestPath);
23+
const resolved = path.resolve(manifestPath);
24+
const provider = matchForLicense(resolved, availableProviders);
25+
const fromManifest = provider.readLicenseFromManifest(resolved);
26+
const fromFile = readLicenseFromFile(resolved);
4027
const mismatch = Boolean(
4128
fromManifest && fromFile && normalizeSpdx(fromManifest) !== normalizeSpdx(fromFile)
4229
);
@@ -47,71 +34,6 @@ export function getProjectLicense(manifestPath) {
4734
};
4835
}
4936

50-
/**
51-
* Read license from manifest (package.json, pom.xml). Returns null if not present or unsupported manifest.
52-
* @param {string} manifestPath
53-
* @returns {string|null}
54-
*/
55-
function readLicenseFromManifest(manifestPath) {
56-
const base = path.basename(manifestPath);
57-
if (base === 'package.json') {
58-
return readLicenseFromPackageJson(manifestPath);
59-
}
60-
if (base === 'pom.xml') {
61-
return readLicenseFromPomXml(manifestPath);
62-
}
63-
// build.gradle, go.mod, requirements.txt: no standard license field
64-
return null;
65-
}
66-
67-
/**
68-
* @param {string} manifestPath
69-
* @returns {string|null}
70-
*/
71-
function readLicenseFromPackageJson(manifestPath) {
72-
try {
73-
const content = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
74-
if (typeof content.license === 'string') {
75-
return content.license.trim() || null;
76-
}
77-
if (Array.isArray(content.licenses) && content.licenses.length > 0) {
78-
const first = content.licenses[0];
79-
const name = first.type || first.name;
80-
return typeof name === 'string' ? name.trim() : null;
81-
}
82-
return null;
83-
} catch {
84-
return null;
85-
}
86-
}
87-
88-
/**
89-
* @param {string} manifestPath
90-
* @returns {string|null}
91-
*/
92-
function readLicenseFromPomXml(manifestPath) {
93-
try {
94-
const xml = fs.readFileSync(manifestPath, 'utf-8');
95-
const parser = new XMLParser({ ignoreAttributes: false });
96-
const obj = parser.parse(xml);
97-
const project = obj?.project;
98-
if (!project?.licenses?.license) {
99-
return null;
100-
}
101-
const license = Array.isArray(project.licenses.license)
102-
? project.licenses.license[0]
103-
: project.licenses.license;
104-
const name = (license?.name && license.name.trim()) || null;
105-
if (!name) {
106-
return null;
107-
}
108-
109-
return name;
110-
} catch {
111-
return null;
112-
}
113-
}
114-
11537
/**
11638
* Find LICENSE file path in the same directory as the manifest.
11739
* @param {string} manifestPath
@@ -136,14 +58,14 @@ export function findLicenseFilePath(manifestPath) {
13658
/**
13759
* Call backend /licenses/identify endpoint to identify license from file.
13860
* @param {string} licenseFilePath - path to LICENSE file
139-
* @param {string} backendUrl - backend base URL
14061
* @param {{}} [opts={}] - options (proxy, token, etc.)
14162
* @returns {Promise<string|null>} - SPDX identifier or null
14263
*/
143-
export async function identifyLicense(licenseFilePath, backendUrl, opts = {}) {
64+
export async function identifyLicense(licenseFilePath, opts = {}) {
14465
try {
14566
const fileContent = fs.readFileSync(licenseFilePath);
146-
const url = `${backendUrl.replace(/\/$/, '')}/licenses/identify`;
67+
const backendUrl = selectTrustifyDABackend(opts);
68+
const url = new URL(`${backendUrl}/licenses/identify`);
14769
const tokenHeaders = getTokenHeaders(opts);
14870
const fetchOptions = {
14971
method: 'POST',

src/provider.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Javascript_yarn from './providers/javascript_yarn.js';
1010
import pythonPipProvider from './providers/python_pip.js'
1111

1212
/** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
13-
/** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>}} Provider */
13+
/** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>, readLicenseFromManifest: function(string): string | null}} Provider */
1414

1515
/**
1616
* MUST include all providers here.
@@ -26,6 +26,22 @@ export const availableProviders = [
2626
golangGomodulesProvider,
2727
pythonPipProvider]
2828

29+
/**
30+
* Match a provider by manifest type only (no lock file check). Used for license reading.
31+
* @param {string} manifestPath - path or name of the manifest
32+
* @param {[Provider]} providers - list of providers to iterate over
33+
* @returns {Provider}
34+
* @throws {Error} when the manifest is not supported and no provider was matched
35+
*/
36+
export function matchForLicense(manifestPath, providers) {
37+
const base = path.parse(manifestPath).base
38+
const provider = providers.find(prov => prov.isSupported(base))
39+
if (!provider) {
40+
throw new Error(`${base} is not supported`)
41+
}
42+
return provider
43+
}
44+
2945
/**
3046
* Match a provider from a list or providers based on file type.
3147
* Each provider MUST export 'isSupported' taking a file name-type and returning true if supported.

src/providers/base_java.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@ import { PackageURL } from 'packageurl-js'
55

66
import { getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js"
77

8-
9-
/** @typedef {import('../provider').Provider} */
10-
11-
/** @typedef {import('../provider').Provided} Provided */
12-
138
/** @typedef {{name: string, version: string}} Package */
149

1510
/** @typedef {{groupId: string, artifactId: string, version: string, scope: string, ignore: boolean}} Dependency */

0 commit comments

Comments
 (0)