Skip to content

Commit d6c4b54

Browse files
soul2zimateclaude
andauthored
feat: implement license resolution and identification (#356)
feat: implement license resolution and identification Add license analysis features that detect the project license, check dependency license compatibility, and include license information in generated SBOMs. This mirrors the JavaScript client implementation. resolve #355 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 07abddb commit d6c4b54

50 files changed

Lines changed: 2025 additions & 106 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,11 @@ Code example
134134

135135
```java
136136
import io.github.guacsec.trustifyda.Api.MixedReport;
137+
import io.github.guacsec.trustifyda.ComponentAnalysisResult;
137138
import io.github.guacsec.trustifyda.impl.ExhortApi;
138-
import io.github.guacsec.trustifyda.AnalysisReport;
139+
import io.github.guacsec.trustifyda.api.v5.AnalysisReport;
139140
import java.nio.file.Files;
140-
import java.nio.file.Paths;
141+
import java.nio.file.Path;
141142
import java.util.concurrent.CompletableFuture;
142143

143144
public class TrustifyExample {
@@ -159,6 +160,12 @@ public class TrustifyExample {
159160
// get a AnalysisReport future holding a deserialized Component Analysis report
160161
var manifestContent = Files.readAllBytes(Path.of("/path/to/pom.xml"));
161162
CompletableFuture<AnalysisReport> componentReport = exhortApi.componentAnalysis("/path/to/pom.xml", manifestContent);
163+
164+
// get a ComponentAnalysisResult with license compatibility checking
165+
CompletableFuture<ComponentAnalysisResult> componentWithLicense = exhortApi.componentAnalysisWithLicense("/path/to/pom.xml");
166+
var result = componentWithLicense.get();
167+
var report = result.report(); // standard AnalysisReport
168+
var licenseSummary = result.licenseSummary(); // license compatibility summary (may be null)
162169
}
163170
}
164171
```
@@ -331,6 +338,16 @@ regex = "1.5.4" # trustify-da-ignore
331338

332339
</ul>
333340

341+
#### License Resolution and Compliance
342+
343+
The Java client includes built-in license analysis that detects your project's license, checks dependency license compatibility, and includes license information in generated SBOMs.
344+
345+
- License checking runs automatically during **component analysis**
346+
- Supports reading licenses from `pom.xml`, `package.json`, `Cargo.toml`, and LICENSE files
347+
- Set `TRUSTIFY_DA_LICENSE_CHECK=false` to disable
348+
349+
For full documentation, see [License Resolution and Compliance](docs/license-resolution-and-compliance.md).
350+
334351
#### Ignore Strategies - experimental
335352

336353
You can specify the method to ignore dependencies in manifest (globally), by setting the environment variable `TRUSTIFY_DA_IGNORE_METHOD` to one of the following values:
@@ -623,11 +640,17 @@ Options:
623640
```shell
624641
java -jar trustify-da-java-client-cli.jar component <file_path> [--summary]
625642
```
626-
Perform component analysis on the specified manifest file.
643+
Perform component analysis on the specified manifest file. License compatibility checking is included by default.
627644

628645
Options:
629646
- `--summary` - Output summary in JSON format
630-
- (default) - Output full report in JSON format
647+
- (default) - Output full report in JSON format (includes license summary)
648+
649+
**License Information**
650+
```shell
651+
java -jar trustify-da-java-client-cli.jar license <file_path>
652+
```
653+
Display project license information from manifest and LICENSE file in JSON format.
631654

632655
**Image Analysis**
633656
```shell
@@ -676,6 +699,9 @@ java -jar trustify-da-java-client-cli.jar component /path/to/go.mod --summary
676699
# Rust Cargo analysis
677700
java -jar trustify-da-java-client-cli.jar stack /path/to/Cargo.toml --summary
678701

702+
# License information
703+
java -jar trustify-da-java-client-cli.jar license /path/to/pom.xml
704+
679705
# Container image analysis with JSON output (default)
680706
java -jar trustify-da-java-client-cli.jar image nginx:latest
681707

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
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., `pom.xml`, `package.json`, `Cargo.toml`) 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 with **automatic fallback**:
19+
20+
1. **Primary: Manifest file** — Reads the license field from:
21+
- `pom.xml`: `<licenses><license><name>` element
22+
- `package.json`: `license` field (or legacy `licenses` array)
23+
- `Cargo.toml`: `package.license` field
24+
- `build.gradle` / `build.gradle.kts`: No standard license field (falls back to LICENSE file)
25+
- `go.mod`: No standard license field (falls back to LICENSE file)
26+
- `requirements.txt`: No standard license field (falls back to LICENSE file)
27+
28+
2. **Fallback: LICENSE file** — If no license is found in the manifest, searches for `LICENSE`, `LICENSE.md`, or `LICENSE.txt` in the same directory as your manifest
29+
30+
**How the fallback works:**
31+
- **Ecosystems with manifest license support** (Maven, JavaScript, Cargo): Uses manifest license if present, otherwise falls back to LICENSE file
32+
- **Ecosystems without manifest license support** (Gradle, Go, Python): Automatically reads from LICENSE file
33+
- **SPDX detection**: Common licenses (Apache-2.0, MIT, GPL-2.0/3.0, LGPL-2.1/3.0, AGPL-3.0, BSD-2-Clause/3-Clause) are automatically detected from LICENSE file content
34+
35+
The backend's license identification API (`POST /api/v5/licenses/identify`) is used for more accurate LICENSE file detection when available.
36+
37+
### Compatibility Checking
38+
39+
The client checks if dependency licenses are compatible with your project license using a restrictiveness hierarchy:
40+
41+
```
42+
PERMISSIVE (1) < WEAK_COPYLEFT (2) < STRONG_COPYLEFT (3)
43+
```
44+
45+
- If a dependency's license is **more restrictive** than the project → **INCOMPATIBLE**
46+
- If a dependency's license is **equal or less restrictive****COMPATIBLE**
47+
- If either license category is **UNKNOWN****UNKNOWN**
48+
49+
Examples:
50+
- Permissive project (MIT) + permissive dependency (Apache-2.0) → Compatible
51+
- Permissive project (MIT) + strong copyleft dependency (GPL-3.0) → Incompatible
52+
- Strong copyleft project (GPL-3.0) + permissive dependency (MIT) → Compatible
53+
54+
## Configuration
55+
56+
### Disable License Checking
57+
58+
License analysis runs automatically during **component analysis only** (not stack analysis). To disable it:
59+
60+
**Environment variable:**
61+
```bash
62+
export TRUSTIFY_DA_LICENSE_CHECK=false
63+
```
64+
65+
**Java property:**
66+
```java
67+
System.setProperty("TRUSTIFY_DA_LICENSE_CHECK", "false");
68+
```
69+
70+
## CLI Usage
71+
72+
### License Command
73+
74+
Display project license information from manifest and LICENSE file:
75+
76+
```bash
77+
java -jar trustify-da-java-client-cli.jar license /path/to/pom.xml
78+
```
79+
80+
**Example output:**
81+
```json
82+
{
83+
"manifestLicense": {
84+
"spdxId": "Apache-2.0",
85+
"details": {
86+
"identifiers": [
87+
{
88+
"id": "Apache-2.0",
89+
"name": "Apache License 2.0",
90+
"isDeprecated": false,
91+
"isOsiApproved": true,
92+
"isFsfLibre": true,
93+
"category": "PERMISSIVE"
94+
}
95+
],
96+
"expression": "Apache-2.0",
97+
"name": "Apache License 2.0",
98+
"category": "PERMISSIVE",
99+
"source": "SPDX",
100+
"sourceUrl": "https://spdx.org"
101+
}
102+
},
103+
"mismatch": false
104+
}
105+
```
106+
107+
> Note: `fileLicense` is omitted when null. The `license` command shows only your project's license. For dependency license compatibility, use component analysis.
108+
109+
### Component Analysis with License Summary
110+
111+
When running component analysis, the license summary is automatically included in the output:
112+
113+
```bash
114+
java -jar trustify-da-java-client-cli.jar component /path/to/pom.xml
115+
```
116+
117+
## Programmatic Usage
118+
119+
```java
120+
import io.github.guacsec.trustifyda.ComponentAnalysisResult;
121+
import io.github.guacsec.trustifyda.impl.ExhortApi;
122+
import io.github.guacsec.trustifyda.license.LicenseCheck.LicenseSummary;
123+
124+
ExhortApi api = new ExhortApi();
125+
126+
// Run component analysis with license check
127+
ComponentAnalysisResult result = api.componentAnalysisWithLicense("/path/to/pom.xml").get();
128+
129+
// Access the analysis report
130+
var report = result.report();
131+
132+
// Access the license summary
133+
LicenseSummary licenseSummary = result.licenseSummary();
134+
if (licenseSummary != null) {
135+
// Project license info
136+
var projectLicense = licenseSummary.projectLicense();
137+
System.out.println("Mismatch: " + projectLicense.mismatch());
138+
139+
// Incompatible dependencies
140+
for (var dep : licenseSummary.incompatibleDependencies()) {
141+
System.out.println("Incompatible: " + dep.purl() + " - " + dep.licenses());
142+
}
143+
}
144+
145+
// Or use componentAnalysis() without license check (original API, unchanged)
146+
var reportOnly = api.componentAnalysis("/path/to/pom.xml").get();
147+
```
148+
149+
## License Summary Fields
150+
151+
The `LicenseSummary` returned in `ComponentAnalysisResult.licenseSummary()` contains:
152+
153+
| Field | Type | Description |
154+
|-------|------|-------------|
155+
| `projectLicense` | `ProjectLicenseSummary` | Project license from manifest and LICENSE file |
156+
| `incompatibleDependencies` | `List<IncompatibleDependency>` | Dependencies with incompatible licenses |
157+
| `error` | `String` | Error message if license check partially failed |
158+
159+
**ProjectLicenseSummary:**
160+
161+
| Field | Type | Description |
162+
|-------|------|-------------|
163+
| `manifest` | `JsonNode` | Full license details from backend for the manifest license (includes identifiers, category, name, source) |
164+
| `file` | `JsonNode` | Full license details from backend for the LICENSE file license |
165+
| `mismatch` | `boolean` | True if manifest and file licenses differ |
166+
167+
**IncompatibleDependency:**
168+
169+
| Field | Type | Description |
170+
|-------|------|-------------|
171+
| `purl` | `String` | Package URL of the dependency |
172+
| `licenses` | `List<LicenseIdentifier>` | Full license identifier objects (id, name, category, isDeprecated, isOsiApproved, isFsfLibre) |
173+
| `category` | `LicenseCategory` | License category |
174+
| `reason` | `String` | Explanation of the incompatibility |
175+
176+
## SBOM Integration
177+
178+
Project license information is automatically included in generated CycloneDX SBOMs on the root component:
179+
180+
```json
181+
{
182+
"metadata": {
183+
"component": {
184+
"type": "application",
185+
"name": "my-project",
186+
"version": "1.0.0",
187+
"licenses": [
188+
{ "license": { "id": "Apache-2.0" } }
189+
]
190+
}
191+
}
192+
}
193+
```
194+
195+
- **All ecosystems** include license information in the SBOM when available
196+
- License names are resolved to valid SPDX identifiers using the CycloneDX license resolver
197+
- If neither manifest nor LICENSE file contains a license, the SBOM root component will have no `licenses` field
198+
199+
## Common Scenarios
200+
201+
### Mismatch Between Manifest and LICENSE File
202+
203+
If your `pom.xml` says `Apache-2.0` but your LICENSE file contains MIT text:
204+
205+
```json
206+
{
207+
"projectLicense": {
208+
"manifest": {
209+
"expression": "Apache-2.0",
210+
"category": "PERMISSIVE"
211+
},
212+
"file": {
213+
"expression": "MIT",
214+
"category": "PERMISSIVE"
215+
},
216+
"mismatch": true
217+
}
218+
}
219+
```
220+
221+
**Action:** Update your manifest or LICENSE file to match.
222+
223+
### Incompatible Dependencies
224+
225+
If you have a permissive-licensed project (e.g., Apache-2.0) but depend on copyleft-licensed libraries:
226+
227+
```json
228+
{
229+
"incompatibleDependencies": [
230+
{
231+
"purl": "pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4",
232+
"licenses": [
233+
{
234+
"id": "LGPL-2.1",
235+
"name": "GNU Lesser General Public License v2.1 only",
236+
"isDeprecated": true,
237+
"isOsiApproved": true,
238+
"isFsfLibre": true,
239+
"category": "WEAK_COPYLEFT"
240+
}
241+
],
242+
"category": "WEAK_COPYLEFT",
243+
"reason": "Dependency license(s) are incompatible with the project license."
244+
}
245+
]
246+
}
247+
```
248+
249+
**Action:** Review the flagged dependencies and consider finding alternatives with compatible licenses.

src/main/java/io/github/guacsec/trustifyda/Api.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ CompletableFuture<AnalysisReport> componentAnalysis(String manifest, byte[] mani
110110

111111
CompletableFuture<AnalysisReport> componentAnalysis(String manifest) throws IOException;
112112

113+
/**
114+
* Use for creating a component analysis with license compatibility checking.
115+
*
116+
* @param manifest the path of the manifest, example {@code /path/to/pom.xml}
117+
* @return a ComponentAnalysisResult containing the analysis report and license summary
118+
* @throws IOException when failed to load the manifest content
119+
*/
120+
CompletableFuture<ComponentAnalysisResult> componentAnalysisWithLicense(String manifest)
121+
throws IOException;
122+
113123
CompletableFuture<Map<ImageRef, AnalysisReport>> imageAnalysis(Set<ImageRef> imageRefs)
114124
throws IOException;
115125

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2023-2025 Trustify Dependency Analytics Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
*
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package io.github.guacsec.trustifyda;
18+
19+
import io.github.guacsec.trustifyda.api.v5.AnalysisReport;
20+
import io.github.guacsec.trustifyda.license.LicenseCheck.LicenseSummary;
21+
22+
public record ComponentAnalysisResult(AnalysisReport report, LicenseSummary licenseSummary) {}

src/main/java/io/github/guacsec/trustifyda/Provider.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.github.guacsec.trustifyda;
1818

1919
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import io.github.guacsec.trustifyda.license.LicenseUtils;
2021
import io.github.guacsec.trustifyda.tools.Ecosystem;
2122
import java.io.IOException;
2223
import java.nio.file.Path;
@@ -71,6 +72,16 @@ protected Provider(Ecosystem.Type ecosystem, Path manifest) {
7172
*/
7273
public abstract Content provideComponent() throws IOException;
7374

75+
/**
76+
* Read the project license from the manifest file. Each provider must decide how to extract the
77+
* license from its manifest (e.g., pom.xml {@code <licenses>}, package.json {@code license},
78+
* Cargo.toml {@code license}). Providers without a manifest-level license field should fall back
79+
* to {@link LicenseUtils#readLicenseFile(Path)}.
80+
*
81+
* @return SPDX identifier or license name from the manifest, or null if not available
82+
*/
83+
public abstract String readLicenseFromManifest();
84+
7485
/**
7586
* If a package manager requires having a lock file it must exist in the provided path
7687
*

0 commit comments

Comments
 (0)