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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ Use as CLI Script
```shell
$ npx @trustify-da/trustify-da-javascript-client help

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

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

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

# specify architecture using ^^ notation (e.g., httpd:2.4.49^^amd64)
$ npx @trustify-da/trustify-da-javascript-client image httpd:2.4.49^^amd64

# get project license information
$ npx @trustify-da/trustify-da-javascript-client license /path/to/package.json
```
</li>

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

# specify architecture using ^^ notation (e.g., httpd:2.4.49^^amd64)
$ trustify-da-javascript-client image httpd:2.4.49^^amd64

# get project license information
$ trustify-da-javascript-client license /path/to/package.json
```
</li>
</ul>
Expand Down Expand Up @@ -372,6 +379,11 @@ const options = {
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.
</p>

<h4>License resolution and dependency license compliance</h4>
<p>
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.
</p>

<h4>Customizing Executables</h4>
<p>
This project uses each ecosystem's executable for creating dependency trees. These executables are expected to be
Expand Down
158 changes: 158 additions & 0 deletions docs/license-resolution-and-compliance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# License Resolution and Compliance

This document describes the license analysis features that help you understand your project’s license and check compatibility with your dependencies.

## Overview

License analysis is **enabled by default** and provides:

1. **Project license detection** from your manifest file (e.g., `package.json`, `pom.xml`) and LICENSE files
2. **Dependency license information** from the Trustify DA backend
3. **Compatibility checking** to identify potential license conflicts
4. **Mismatch detection** when your manifest and LICENSE file declare different licenses

## How It Works

### Project License Detection

The client looks for your project’s license in two places:

1. **Manifest file** — Reads the license field from:
- `package.json`: `license` field
- `pom.xml`: `<licenses><license><name>` element
- Other ecosystems: varies by ecosystem (some don’t have standard license fields)

2. **LICENSE file** — Searches for `LICENSE`, `LICENSE.md`, or `LICENSE.txt` in the same directory as your manifest

The backend’s license identification API is used for accurate LICENSE file detection.

### Compatibility Checking

The client checks if dependency licenses are compatible with your project license. For example:
- Permissive project (MIT) + permissive dependencies → ✅ Compatible
- Permissive project (MIT) + strong copyleft dependency (GPL) → ⚠️ Potentially incompatible

Compatibility results are included in the analysis report’s `licenseSummary`.

## Configuration

### Disable License Checking

License analysis runs automatically during **component analysis only** (not stack analysis). To disable it:

**Environment variable:**
```bash
export TRUSTIFY_DA_LICENSE_CHECK=false
```

**Programmatic option:**
```javascript
await componentAnalysis(‘pom.xml’, { licenseCheck: false });
```

## CLI Usage

### Get License Information

```bash
exhort license path/to/pom.xml
```

**Example output:**
```json
{
"manifestLicense": {
"spdxId": "Apache-2.0",
"category": "PERMISSIVE",
"name": "Apache License 2.0",
"identifiers": ["Apache-2.0"]
},
"fileLicense": {
"spdxId": "Apache-2.0",
"category": "PERMISSIVE",
"name": "Apache License 2.0",
"identifiers": ["Apache-2.0"]
},
"mismatch": false
}
```

Note: The `license` command shows only your project's license. For dependency license information, use component analysis.

## Analysis Report Fields

When license checking is enabled, component analysis includes a `licenseSummary` field:

```javascript
{
// ... standard analysis fields (providers, etc.) ...
"licenseSummary": {
"projectLicense": {
"manifest": {
"spdxId": "Apache-2.0",
"category": "PERMISSIVE",
"name": "Apache License 2.0",
"identifiers": ["Apache-2.0"]
},
"file": {
"spdxId": "Apache-2.0",
"category": "PERMISSIVE",
"name": "Apache License 2.0",
"identifiers": ["Apache-2.0"]
},
"mismatch": false
},
"incompatibleDependencies": [
{
"purl": "pkg:maven/org.example/gpl-lib@1.0.0",
"licenses": ["GPL-3.0"],
"category": "STRONG_COPYLEFT",
"reason": "Dependency license(s) are incompatible with the project license."
}
]
}
}
```

**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.

## Common Scenarios

### Mismatch Between Manifest and LICENSE File

If your `package.json` says `"license": "MIT"` but your LICENSE file contains Apache-2.0 text, the component analysis report will show:
```json
{
"licenseSummary": {
"projectLicense": {
"manifest": {
"spdxId": "MIT",
"category": "PERMISSIVE",
"name": "MIT License"
},
"file": {
"spdxId": "Apache-2.0",
"category": "PERMISSIVE",
"name": "Apache License 2.0"
},
"mismatch": true
},
"incompatibleDependencies": []
}
}
```

**Action:** Update your manifest or LICENSE file to match.

### Incompatible Dependencies

If you have a permissive-licensed project (MIT, Apache) but depend on GPL-licensed libraries, they’ll appear in `incompatibleDependencies`.

**Action:** Review the flagged dependencies and consider:
- Finding alternative libraries with compatible licenses
- Consulting legal counsel if the dependency is necessary
- Understanding how you’re using the dependency (linking, distribution, etc.)

## SBOM Integration

Project license information is automatically included in generated SBOMs (CycloneDX format) in the root component’s `licenses` field.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
},
"devDependencies": {
"@babel/core": "^7.23.2",
"@trustify-da/trustify-da-api-model": "^2.0.1",
"@trustify-da/trustify-da-api-model": "^2.0.7",
"@types/node": "^20.17.30",
"@types/which": "^3.0.4",
"babel-plugin-rewire": "^1.2.0",
Expand Down
88 changes: 18 additions & 70 deletions src/analysis.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,12 @@ import fs from "node:fs";
import path from "node:path";
import { EOL } from "os";

import { HttpsProxyAgent } from "https-proxy-agent";

import { runLicenseCheck } from "./license/index.js";
import { generateImageSBOM, parseImageRef } from "./oci_image/utils.js";
import { RegexNotToBeLogged, getCustom } from "./tools.js";
import { addProxyAgent, getCustom, getTokenHeaders , TRUSTIFY_DA_OPERATION_TYPE_HEADER, TRUSTIFY_DA_PACKAGE_MANAGER_HEADER } from "./tools.js";

export default { requestComponent, requestStack, requestImages, validateToken }

const rhdaTokenHeader = "trust-da-token";
const rhdaTelemetryId = "telemetry-anonymous-id";
const rhdaSourceHeader = "trust-da-source"
const rhdaOperationTypeHeader = "trust-da-operation-type"
const rhdaPackageManagerHeader = "trust-da-pkg-manager"

/**
* Adds proxy agent configuration to fetch options if a proxy URL is specified
* @param {RequestInit} options - The base fetch options
* @param {import("index.js").Options} opts - The trustify DA options that may contain proxy configuration
* @returns {RequestInit} The fetch options with proxy agent if applicable
*/
function addProxyAgent(options, opts) {
const proxyUrl = getCustom('TRUSTIFY_DA_PROXY_URL', null, opts);
if (proxyUrl) {
options.agent = new HttpsProxyAgent(proxyUrl);
}
return options;
}

/**
* Send a stack analysis request and get the report as 'text/html' or 'application/json'.
* @param {import('./provider').Provider} provider - the provided data for constructing the request
Expand All @@ -43,13 +22,13 @@ async function requestStack(provider, manifest, url, html = false, opts = {}) {
opts["manifest-type"] = path.parse(manifest).base
let provided = await provider.provideStack(manifest, opts) // throws error if content providing failed
opts["source-manifest"] = ""
opts[rhdaOperationTypeHeader.toUpperCase().replaceAll("-", "_")] = "stack-analysis"
opts[TRUSTIFY_DA_OPERATION_TYPE_HEADER.toUpperCase().replaceAll("-", "_")] = "stack-analysis"
let startTime = new Date()
let endTime
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
console.log("Starting time of sending stack analysis request to the dependency analytics server= " + startTime)
}
opts[rhdaPackageManagerHeader.toUpperCase().replaceAll("-", "_")] = provided.ecosystem
opts[TRUSTIFY_DA_PACKAGE_MANAGER_HEADER.toUpperCase().replaceAll("-", "_")] = provided.ecosystem

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

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

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

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

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


}
const licenseCheckEnabled = getCustom('TRUSTIFY_DA_LICENSE_CHECK', 'true', opts) !== 'false' && opts.licenseCheck !== false
if (licenseCheckEnabled) {
try {
result.licenseSummary = await runLicenseCheck(provided.content, manifest, url, opts, result)
} catch (licenseErr) {
result.licenseSummary = { error: licenseErr.message }
}
}
} else {
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()}`)
}
Expand All @@ -164,7 +151,7 @@ async function requestImages(imageRefs, url, html = false, opts = {}) {
imageSboms[parsedImageRef.getPackageURL().toString()] = generateImageSBOM(parsedImageRef, opts)
}

const finalUrl = new URL(`${url}/api/v4/batch-analysis`);
const finalUrl = new URL(`${url}/api/v5/batch-analysis`);
if (opts['TRUSTIFY_DA_RECOMMENDATIONS_ENABLED'] === 'false') {
finalUrl.searchParams.append('recommend', 'false');
}
Expand Down Expand Up @@ -215,7 +202,7 @@ async function validateToken(url, opts = {}) {
}
}, opts);

let resp = await fetch(`${url}/api/v4/token`, fetchOptions)
let resp = await fetch(`${url}/api/v5/token`, fetchOptions)
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
let exRequestId = resp.headers.get("ex-request-id");
if (exRequestId) {
Expand All @@ -224,42 +211,3 @@ async function validateToken(url, opts = {}) {
}
return resp.status
}

/**
*
* @param {string} headerName - the header name to populate in request
* @param headers
* @param {string} optsKey - key in the options object to use the value for
* @param {import("index.js").Options} [opts={}] - options input object to fetch header values from
* @private
*/
function setRhdaHeader(headerName, headers, optsKey, opts) {
let rhdaHeaderValue = getCustom(optsKey, null, opts);
if (rhdaHeaderValue) {
headers[headerName] = rhdaHeaderValue
}
}

/**
* Utility function for fetching vendor tokens
* @param {import("index.js").Options} [opts={}] - optional various options to pass along the application
* @returns {{}}
*/
export function getTokenHeaders(opts = {}) {
let headers = {}
setRhdaHeader(rhdaTokenHeader, headers, 'TRUSTIFY_DA_TOKEN', opts);
setRhdaHeader(rhdaSourceHeader, headers, 'TRUSTIFY_DA_SOURCE', opts);
setRhdaHeader(rhdaOperationTypeHeader, headers, rhdaOperationTypeHeader.toUpperCase().replaceAll("-", "_"), opts);
setRhdaHeader(rhdaPackageManagerHeader, headers, rhdaPackageManagerHeader.toUpperCase().replaceAll("-", "_"), opts)
setRhdaHeader(rhdaTelemetryId, headers, 'TRUSTIFY_DA_TELEMETRY_ID', opts);

if (getCustom("TRUSTIFY_DA_DEBUG", null, opts) === "true") {
console.log("Headers Values to be sent to Trustify DA backend:" + EOL)
for (const headerKey in headers) {
if (!headerKey.match(RegexNotToBeLogged)) {
console.log(`${headerKey}: ${headers[headerKey]}`)
}
}
}
return headers
}
Loading
Loading