Skip to content

Commit 0e9ba23

Browse files
ruromeroclaude
andauthored
feat: implement license resolution and identification (#403)
* feat: implement license resolution and identification * feat: calculate compatibility from backend information --------- Signed-off-by: Ruben Romero Montes <rromerom@redhat.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 63ae5c2 commit 0e9ba23

28 files changed

Lines changed: 1180 additions & 134 deletions

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,13 @@ Use as CLI Script
8383
```shell
8484
$ npx @trustify-da/trustify-da-javascript-client help
8585

86-
Usage: trustify-da-javascript-client {component|stack|image|validate-token}
86+
Usage: trustify-da-javascript-client {component|stack|image|validate-token|license}
8787

8888
Commands:
8989
trustify-da-javascript-client stack </path/to/manifest> [--html|--summary] produce stack report for manifest path
9090
trustify-da-javascript-client component <path/to/manifest> [--summary] produce component report for a manifest type and content
9191
trustify-da-javascript-client image <image-refs..> [--html|--summary] produce image analysis report for OCI image references
92+
trustify-da-javascript-client license </path/to/manifest> display project license information from manifest and LICENSE file in JSON format
9293

9394
Options:
9495
--help Show help [boolean]
@@ -123,6 +124,9 @@ $ npx @trustify-da/trustify-da-javascript-client image docker.io/library/node:18
123124

124125
# specify architecture using ^^ notation (e.g., httpd:2.4.49^^amd64)
125126
$ npx @trustify-da/trustify-da-javascript-client image httpd:2.4.49^^amd64
127+
128+
# get project license information
129+
$ npx @trustify-da/trustify-da-javascript-client license /path/to/package.json
126130
```
127131
</li>
128132

@@ -161,6 +165,9 @@ $ trustify-da-javascript-client image docker.io/library/node:18 docker.io/librar
161165

162166
# specify architecture using ^^ notation (e.g., httpd:2.4.49^^amd64)
163167
$ trustify-da-javascript-client image httpd:2.4.49^^amd64
168+
169+
# get project license information
170+
$ trustify-da-javascript-client license /path/to/package.json
164171
```
165172
</li>
166173
</ul>
@@ -372,6 +379,11 @@ const options = {
372379
The proxy URL should be in the format: `http://host:port` or `https://host:port`. The API will automatically use the appropriate protocol (HTTP or HTTPS) based on the proxy URL provided.
373380
</p>
374381

382+
<h4>License resolution and dependency license compliance</h4>
383+
<p>
384+
The client can resolve the <strong>project license</strong> from the manifest (e.g. <code>package.json</code> <code>license</code>, <code>pom.xml</code> <code>&lt;licenses&gt;</code>) and from a <code>LICENSE</code> or <code>LICENSE.md</code> file in the project, and report when they differ. For <strong>component analysis</strong>, you can optionally run a license check: the client fetches dependency licenses from the backend (by purl) and reports dependencies whose licenses are incompatible with the project license. See <a href="docs/license-resolution-and-compliance.md">License resolution and compliance</a> for design and behavior. To disable the check on component analysis, set <code>TRUSTIFY_DA_LICENSE_CHECK=false</code> or pass <code>licenseCheck: false</code> in the options.
385+
</p>
386+
375387
<h4>Customizing Executables</h4>
376388
<p>
377389
This project uses each ecosystem's executable for creating dependency trees. These executables are expected to be
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# License Resolution and Compliance
2+
3+
This document describes the license analysis features that help you understand your project’s license and check compatibility with your dependencies.
4+
5+
## Overview
6+
7+
License analysis is **enabled by default** and provides:
8+
9+
1. **Project license detection** from your manifest file (e.g., `package.json`, `pom.xml`) and LICENSE files
10+
2. **Dependency license information** from the Trustify DA backend
11+
3. **Compatibility checking** to identify potential license conflicts
12+
4. **Mismatch detection** when your manifest and LICENSE file declare different licenses
13+
14+
## How It Works
15+
16+
### Project License Detection
17+
18+
The client looks for your project’s license in two places:
19+
20+
1. **Manifest file** — Reads the license field from:
21+
- `package.json`: `license` field
22+
- `pom.xml`: `<licenses><license><name>` element
23+
- Other ecosystems: varies by ecosystem (some don’t have standard license fields)
24+
25+
2. **LICENSE file** — Searches for `LICENSE`, `LICENSE.md`, or `LICENSE.txt` in the same directory as your manifest
26+
27+
The backend’s license identification API is used for accurate LICENSE file detection.
28+
29+
### Compatibility Checking
30+
31+
The client checks if dependency licenses are compatible with your project license. For example:
32+
- Permissive project (MIT) + permissive dependencies → ✅ Compatible
33+
- Permissive project (MIT) + strong copyleft dependency (GPL) → ⚠️ Potentially incompatible
34+
35+
Compatibility results are included in the analysis report’s `licenseSummary`.
36+
37+
## Configuration
38+
39+
### Disable License Checking
40+
41+
License analysis runs automatically during **component analysis only** (not stack analysis). To disable it:
42+
43+
**Environment variable:**
44+
```bash
45+
export TRUSTIFY_DA_LICENSE_CHECK=false
46+
```
47+
48+
**Programmatic option:**
49+
```javascript
50+
await componentAnalysis(‘pom.xml’, { licenseCheck: false });
51+
```
52+
53+
## CLI Usage
54+
55+
### Get License Information
56+
57+
```bash
58+
exhort license path/to/pom.xml
59+
```
60+
61+
**Example output:**
62+
```json
63+
{
64+
"manifestLicense": {
65+
"spdxId": "Apache-2.0",
66+
"category": "PERMISSIVE",
67+
"name": "Apache License 2.0",
68+
"identifiers": ["Apache-2.0"]
69+
},
70+
"fileLicense": {
71+
"spdxId": "Apache-2.0",
72+
"category": "PERMISSIVE",
73+
"name": "Apache License 2.0",
74+
"identifiers": ["Apache-2.0"]
75+
},
76+
"mismatch": false
77+
}
78+
```
79+
80+
Note: The `license` command shows only your project's license. For dependency license information, use component analysis.
81+
82+
## Analysis Report Fields
83+
84+
When license checking is enabled, component analysis includes a `licenseSummary` field:
85+
86+
```javascript
87+
{
88+
// ... standard analysis fields (providers, etc.) ...
89+
"licenseSummary": {
90+
"projectLicense": {
91+
"manifest": {
92+
"spdxId": "Apache-2.0",
93+
"category": "PERMISSIVE",
94+
"name": "Apache License 2.0",
95+
"identifiers": ["Apache-2.0"]
96+
},
97+
"file": {
98+
"spdxId": "Apache-2.0",
99+
"category": "PERMISSIVE",
100+
"name": "Apache License 2.0",
101+
"identifiers": ["Apache-2.0"]
102+
},
103+
"mismatch": false
104+
},
105+
"incompatibleDependencies": [
106+
{
107+
"purl": "pkg:maven/org.example/gpl-lib@1.0.0",
108+
"licenses": ["GPL-3.0"],
109+
"category": "STRONG_COPYLEFT",
110+
"reason": "Dependency license(s) are incompatible with the project license."
111+
}
112+
]
113+
}
114+
}
115+
```
116+
117+
**Note:** Dependency license information (for all dependencies, not just incompatible ones) is available in the standard backend response under the `licenses` field. The `licenseSummary` only includes project license details and flagged incompatibilities.
118+
119+
## Common Scenarios
120+
121+
### Mismatch Between Manifest and LICENSE File
122+
123+
If your `package.json` says `"license": "MIT"` but your LICENSE file contains Apache-2.0 text, the component analysis report will show:
124+
```json
125+
{
126+
"licenseSummary": {
127+
"projectLicense": {
128+
"manifest": {
129+
"spdxId": "MIT",
130+
"category": "PERMISSIVE",
131+
"name": "MIT License"
132+
},
133+
"file": {
134+
"spdxId": "Apache-2.0",
135+
"category": "PERMISSIVE",
136+
"name": "Apache License 2.0"
137+
},
138+
"mismatch": true
139+
},
140+
"incompatibleDependencies": []
141+
}
142+
}
143+
```
144+
145+
**Action:** Update your manifest or LICENSE file to match.
146+
147+
### Incompatible Dependencies
148+
149+
If you have a permissive-licensed project (MIT, Apache) but depend on GPL-licensed libraries, they’ll appear in `incompatibleDependencies`.
150+
151+
**Action:** Review the flagged dependencies and consider:
152+
- Finding alternative libraries with compatible licenses
153+
- Consulting legal counsel if the dependency is necessary
154+
- Understanding how you’re using the dependency (linking, distribution, etc.)
155+
156+
## SBOM Integration
157+
158+
Project license information is automatically included in generated SBOMs (CycloneDX format) in the root component’s `licenses` field.

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
},
6060
"devDependencies": {
6161
"@babel/core": "^7.23.2",
62-
"@trustify-da/trustify-da-api-model": "^2.0.1",
62+
"@trustify-da/trustify-da-api-model": "^2.0.7",
6363
"@types/node": "^20.17.30",
6464
"@types/which": "^3.0.4",
6565
"babel-plugin-rewire": "^1.2.0",

src/analysis.js

Lines changed: 18 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,12 @@ import fs from "node:fs";
22
import path from "node:path";
33
import { EOL } from "os";
44

5-
import { HttpsProxyAgent } from "https-proxy-agent";
6-
5+
import { runLicenseCheck } from "./license/index.js";
76
import { generateImageSBOM, parseImageRef } from "./oci_image/utils.js";
8-
import { RegexNotToBeLogged, getCustom } from "./tools.js";
7+
import { addProxyAgent, getCustom, getTokenHeaders , TRUSTIFY_DA_OPERATION_TYPE_HEADER, TRUSTIFY_DA_PACKAGE_MANAGER_HEADER } from "./tools.js";
98

109
export default { requestComponent, requestStack, requestImages, validateToken }
1110

12-
const rhdaTokenHeader = "trust-da-token";
13-
const rhdaTelemetryId = "telemetry-anonymous-id";
14-
const rhdaSourceHeader = "trust-da-source"
15-
const rhdaOperationTypeHeader = "trust-da-operation-type"
16-
const rhdaPackageManagerHeader = "trust-da-pkg-manager"
17-
18-
/**
19-
* Adds proxy agent configuration to fetch options if a proxy URL is specified
20-
* @param {RequestInit} options - The base fetch options
21-
* @param {import("index.js").Options} opts - The trustify DA options that may contain proxy configuration
22-
* @returns {RequestInit} The fetch options with proxy agent if applicable
23-
*/
24-
function addProxyAgent(options, opts) {
25-
const proxyUrl = getCustom('TRUSTIFY_DA_PROXY_URL', null, opts);
26-
if (proxyUrl) {
27-
options.agent = new HttpsProxyAgent(proxyUrl);
28-
}
29-
return options;
30-
}
31-
3211
/**
3312
* Send a stack analysis request and get the report as 'text/html' or 'application/json'.
3413
* @param {import('./provider').Provider} provider - the provided data for constructing the request
@@ -43,13 +22,13 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
4322
opts["manifest-type"] = path.parse(manifest).base
4423
let provided = await provider.provideStack(manifest, opts) // throws error if content providing failed
4524
opts["source-manifest"] = ""
46-
opts[rhdaOperationTypeHeader.toUpperCase().replaceAll("-", "_")] = "stack-analysis"
25+
opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "stack-analysis"
4726
let startTime = new Date()
4827
let endTime
4928
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
5029
console.log("Starting time of sending stack analysis request to the dependency analytics server= " + startTime)
5130
}
52-
opts[rhdaPackageManagerHeader.toUpperCase().replaceAll("-", "_")] = provided.ecosystem
31+
opts[TRUSTIFY_DA_PACKAGE_MANAGER_HEADER.toUpperCase().replaceAll("-", "_")] = provided.ecosystem
5332

5433
const fetchOptions = addProxyAgent({
5534
method: 'POST',
@@ -61,7 +40,7 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
6140
body: provided.content
6241
}, opts);
6342

64-
const finalUrl = new URL(`${url}/api/v4/analysis`);
43+
const finalUrl = new URL(`${url}/api/v5/analysis`);
6544
if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
6645
finalUrl.searchParams.append('recommend', 'false');
6746
}
@@ -107,11 +86,11 @@ async function requestComponent(provider, manifest, url, opts = {}) {
10786

10887
let provided = await provider.provideComponent(manifest, opts) // throws error if content providing failed
10988
opts["source-manifest"] = ""
110-
opts[rhdaOperationTypeHeader.toUpperCase().replaceAll("-", "_")] = "component-analysis"
89+
opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "component-analysis"
11190
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
11291
console.log("Starting time of sending component analysis request to Trustify DA backend server= " + new Date())
11392
}
114-
opts[rhdaPackageManagerHeader.toUpperCase().replaceAll("-", "_")] = provided.ecosystem
93+
opts[TRUSTIFY_DA_PACKAGE_MANAGER_HEADER.toUpperCase().replaceAll("-", "_")] = provided.ecosystem
11594

11695
const fetchOptions = addProxyAgent({
11796
method: 'POST',
@@ -123,7 +102,7 @@ async function requestComponent(provider, manifest, url, opts = {}) {
123102
body: provided.content
124103
}, opts);
125104

126-
const finalUrl = new URL(`${url}/api/v4/analysis`);
105+
const finalUrl = new URL(`${url}/api/v5/analysis`);
127106
if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
128107
finalUrl.searchParams.append('recommend', 'false');
129108
}
@@ -143,6 +122,14 @@ async function requestComponent(provider, manifest, url, opts = {}) {
143122

144123

145124
}
125+
const licenseCheckEnabled = getCustom('TRUSTIFY_DA_LICENSE_CHECK', 'true', opts) !== 'false' && opts.licenseCheck !== false
126+
if (licenseCheckEnabled) {
127+
try {
128+
result.licenseSummary = await runLicenseCheck(provided.content, manifest, url, opts, result)
129+
} catch (licenseErr) {
130+
result.licenseSummary = { error: licenseErr.message }
131+
}
132+
}
146133
} else {
147134
throw new Error(`Got error response from Trustify DA backend - http return code : ${resp.status}, ex-request-id: ${resp.headers.get("ex-request-id")} error message => ${await resp.text()}`)
148135
}
@@ -164,7 +151,7 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
164151
imageSboms[parsedImageRef.getPackageURL().toString()] = generateImageSBOM(parsedImageRef, opts)
165152
}
166153

167-
const finalUrl = new URL(`${url}/api/v4/batch-analysis`);
154+
const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
168155
if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
169156
finalUrl.searchParams.append('recommend', 'false');
170157
}
@@ -215,7 +202,7 @@ async function validateToken(url, opts = {}) {
215202
}
216203
}, opts);
217204

218-
let resp = await fetch(`${url}/api/v4/token`, fetchOptions)
205+
let resp = await fetch(`${url}/api/v5/token`, fetchOptions)
219206
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
220207
let exRequestId = resp.headers.get("ex-request-id");
221208
if (exRequestId) {
@@ -224,42 +211,3 @@ async function validateToken(url, opts = {}) {
224211
}
225212
return resp.status
226213
}
227-
228-
/**
229-
*
230-
* @param {string} headerName - the header name to populate in request
231-
* @param headers
232-
* @param {string} optsKey - key in the options object to use the value for
233-
* @param {import("index.js").Options} [opts={}] - options input object to fetch header values from
234-
* @private
235-
*/
236-
function setRhdaHeader(headerName, headers, optsKey, opts) {
237-
let rhdaHeaderValue = getCustom(optsKey, null, opts);
238-
if (rhdaHeaderValue) {
239-
headers[headerName] = rhdaHeaderValue
240-
}
241-
}
242-
243-
/**
244-
* Utility function for fetching vendor tokens
245-
* @param {import("index.js").Options} [opts={}] - optional various options to pass along the application
246-
* @returns {{}}
247-
*/
248-
export function getTokenHeaders(opts = {}) {
249-
let headers = {}
250-
setRhdaHeader(rhdaTokenHeader, headers, 'TRUSTIFY_DA_TOKEN', opts);
251-
setRhdaHeader(rhdaSourceHeader, headers, 'TRUSTIFY_DA_SOURCE', opts);
252-
setRhdaHeader(rhdaOperationTypeHeader, headers, rhdaOperationTypeHeader.toUpperCase().replaceAll("-", "_"), opts);
253-
setRhdaHeader(rhdaPackageManagerHeader, headers, rhdaPackageManagerHeader.toUpperCase().replaceAll("-", "_"), opts)
254-
setRhdaHeader(rhdaTelemetryId, headers, 'TRUSTIFY_DA_TELEMETRY_ID', opts);
255-
256-
if (getCustom("TRUSTIFY_DA_DEBUG", null, opts) === "true") {
257-
console.log("Headers Values to be sent to Trustify DA backend:" + EOL)
258-
for (const headerKey in headers) {
259-
if (!headerKey.match(RegexNotToBeLogged)) {
260-
console.log(`${headerKey}: ${headers[headerKey]}`)
261-
}
262-
}
263-
}
264-
return headers
265-
}

0 commit comments

Comments
 (0)