From 56897d63018c37cfadb17858e61012f1dd347e3c Mon Sep 17 00:00:00 2001 From: Chao Wang Date: Thu, 19 Mar 2026 10:27:06 +0800 Subject: [PATCH 1/3] 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. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 34 +- docs/license-resolution-and-compliance.md | 249 ++++++++++++++ .../io/github/guacsec/trustifyda/Api.java | 10 + .../trustifyda/ComponentAnalysisResult.java | 22 ++ .../github/guacsec/trustifyda/Provider.java | 13 + .../io/github/guacsec/trustifyda/cli/App.java | 171 ++++++---- .../guacsec/trustifyda/cli/Command.java | 3 +- .../guacsec/trustifyda/impl/ExhortApi.java | 183 +++++++++- .../trustifyda/license/LicenseCheck.java | 213 ++++++++++++ .../trustifyda/license/LicenseUtils.java | 316 ++++++++++++++++++ .../trustifyda/license/ProjectLicense.java | 45 +++ .../trustifyda/providers/CargoProvider.java | 22 +- .../providers/GoModulesProvider.java | 4 +- .../trustifyda/providers/GradleProvider.java | 2 +- .../providers/JavaMavenProvider.java | 53 ++- .../providers/JavaScriptProvider.java | 10 +- .../providers/JavaScriptProviderFactory.java | 5 +- .../providers/PythonPipProvider.java | 8 +- .../providers/javascript/model/Manifest.java | 35 ++ .../trustifyda/sbom/CycloneDXSbom.java | 12 + .../github/guacsec/trustifyda/sbom/Sbom.java | 2 + src/main/java/module-info.java | 1 + src/main/resources/cli_help.txt | 14 +- .../guacsec/trustifyda/cli/AppTest.java | 9 +- .../providers/CargoProviderLicenseTest.java | 57 ++++ .../JavaMavenProviderLicenseTest.java | 71 ++++ .../JavaScriptProviderLicenseTest.java | 65 ++++ .../providers/LicenseFallbackTest.java | 87 +++++ .../license/cargo_with_license/Cargo.toml | 7 + .../license/cargo_without_license/Cargo.toml | 6 + .../license/pom_with_empty_license/pom.xml | 24 ++ .../maven/license/pom_with_license/pom.xml | 25 ++ .../pom_with_multiple_licenses/pom.xml | 29 ++ .../maven/license/pom_without_license/pom.xml | 18 + .../expected_component_sbom.json | 22 ++ .../expected_component_sbom.json | 22 ++ .../deps_with_ignore/expected_stack_sbom.json | 22 ++ .../expected_stack_sbom.json | 22 ++ .../package-lock.json | 5 + .../package_with_legacy_licenses/package.json | 11 + .../package_with_license/package-lock.json | 5 + .../license/package_with_license/package.json | 8 + .../package_without_license/package-lock.json | 5 + .../package_without_license/package.json | 7 + .../deps_with_ignore/expected_stack_sbom.json | 22 ++ .../expected_stack_sbom.json | 22 ++ .../deps_with_ignore/expected_stack_sbom.json | 22 ++ .../expected_stack_sbom.json | 22 ++ .../deps_with_ignore/expected_stack_sbom.json | 22 ++ .../expected_stack_sbom.json | 22 ++ 50 files changed, 1984 insertions(+), 102 deletions(-) create mode 100644 docs/license-resolution-and-compliance.md create mode 100644 src/main/java/io/github/guacsec/trustifyda/ComponentAnalysisResult.java create mode 100644 src/main/java/io/github/guacsec/trustifyda/license/LicenseCheck.java create mode 100644 src/main/java/io/github/guacsec/trustifyda/license/LicenseUtils.java create mode 100644 src/main/java/io/github/guacsec/trustifyda/license/ProjectLicense.java create mode 100644 src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderLicenseTest.java create mode 100644 src/test/java/io/github/guacsec/trustifyda/providers/JavaMavenProviderLicenseTest.java create mode 100644 src/test/java/io/github/guacsec/trustifyda/providers/JavaScriptProviderLicenseTest.java create mode 100644 src/test/java/io/github/guacsec/trustifyda/providers/LicenseFallbackTest.java create mode 100644 src/test/resources/tst_manifests/cargo/license/cargo_with_license/Cargo.toml create mode 100644 src/test/resources/tst_manifests/cargo/license/cargo_without_license/Cargo.toml create mode 100644 src/test/resources/tst_manifests/maven/license/pom_with_empty_license/pom.xml create mode 100644 src/test/resources/tst_manifests/maven/license/pom_with_license/pom.xml create mode 100644 src/test/resources/tst_manifests/maven/license/pom_with_multiple_licenses/pom.xml create mode 100644 src/test/resources/tst_manifests/maven/license/pom_without_license/pom.xml create mode 100644 src/test/resources/tst_manifests/npm/license/package_with_legacy_licenses/package-lock.json create mode 100644 src/test/resources/tst_manifests/npm/license/package_with_legacy_licenses/package.json create mode 100644 src/test/resources/tst_manifests/npm/license/package_with_license/package-lock.json create mode 100644 src/test/resources/tst_manifests/npm/license/package_with_license/package.json create mode 100644 src/test/resources/tst_manifests/npm/license/package_without_license/package-lock.json create mode 100644 src/test/resources/tst_manifests/npm/license/package_without_license/package.json diff --git a/README.md b/README.md index 546c72b0..4dde423c 100644 --- a/README.md +++ b/README.md @@ -134,10 +134,11 @@ Code example ```java import io.github.guacsec.trustifyda.Api.MixedReport; +import io.github.guacsec.trustifyda.ComponentAnalysisResult; import io.github.guacsec.trustifyda.impl.ExhortApi; -import io.github.guacsec.trustifyda.AnalysisReport; +import io.github.guacsec.trustifyda.api.v5.AnalysisReport; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import java.util.concurrent.CompletableFuture; public class TrustifyExample { @@ -159,6 +160,12 @@ public class TrustifyExample { // get a AnalysisReport future holding a deserialized Component Analysis report var manifestContent = Files.readAllBytes(Path.of("/path/to/pom.xml")); CompletableFuture componentReport = exhortApi.componentAnalysis("/path/to/pom.xml", manifestContent); + + // get a ComponentAnalysisResult with license compatibility checking + CompletableFuture componentWithLicense = exhortApi.componentAnalysisWithLicense("/path/to/pom.xml"); + var result = componentWithLicense.get(); + var report = result.report(); // standard AnalysisReport + var licenseSummary = result.licenseSummary(); // license compatibility summary (may be null) } } ``` @@ -331,6 +338,16 @@ regex = "1.5.4" # trustify-da-ignore +#### License Resolution and Compliance + +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. + +- License checking runs automatically during **component analysis** +- Supports reading licenses from `pom.xml`, `package.json`, `Cargo.toml`, and LICENSE files +- Set `TRUSTIFY_DA_LICENSE_CHECK=false` to disable + +For full documentation, see [License Resolution and Compliance](docs/license-resolution-and-compliance.md). + #### Ignore Strategies - experimental 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: ```shell java -jar trustify-da-java-client-cli.jar component [--summary] ``` -Perform component analysis on the specified manifest file. +Perform component analysis on the specified manifest file. License compatibility checking is included by default. Options: - `--summary` - Output summary in JSON format -- (default) - Output full report in JSON format +- (default) - Output full report in JSON format (includes license summary) + +**License Information** +```shell +java -jar trustify-da-java-client-cli.jar license +``` +Display project license information from manifest and LICENSE file in JSON format. **Image Analysis** ```shell @@ -676,6 +699,9 @@ java -jar trustify-da-java-client-cli.jar component /path/to/go.mod --summary # Rust Cargo analysis java -jar trustify-da-java-client-cli.jar stack /path/to/Cargo.toml --summary +# License information +java -jar trustify-da-java-client-cli.jar license /path/to/pom.xml + # Container image analysis with JSON output (default) java -jar trustify-da-java-client-cli.jar image nginx:latest diff --git a/docs/license-resolution-and-compliance.md b/docs/license-resolution-and-compliance.md new file mode 100644 index 00000000..988b9906 --- /dev/null +++ b/docs/license-resolution-and-compliance.md @@ -0,0 +1,249 @@ +# 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., `pom.xml`, `package.json`, `Cargo.toml`) 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 with **automatic fallback**: + +1. **Primary: Manifest file** — Reads the license field from: + - `pom.xml`: `` element + - `package.json`: `license` field (or legacy `licenses` array) + - `Cargo.toml`: `package.license` field + - `build.gradle` / `build.gradle.kts`: No standard license field (falls back to LICENSE file) + - `go.mod`: No standard license field (falls back to LICENSE file) + - `requirements.txt`: No standard license field (falls back to LICENSE file) + +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 + +**How the fallback works:** +- **Ecosystems with manifest license support** (Maven, JavaScript, Cargo): Uses manifest license if present, otherwise falls back to LICENSE file +- **Ecosystems without manifest license support** (Gradle, Go, Python): Automatically reads from LICENSE file +- **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 + +The backend's license identification API (`POST /api/v5/licenses/identify`) is used for more accurate LICENSE file detection when available. + +### Compatibility Checking + +The client checks if dependency licenses are compatible with your project license using a restrictiveness hierarchy: + +``` +PERMISSIVE (1) < WEAK_COPYLEFT (2) < STRONG_COPYLEFT (3) +``` + +- If a dependency's license is **more restrictive** than the project → **INCOMPATIBLE** +- If a dependency's license is **equal or less restrictive** → **COMPATIBLE** +- If either license category is **UNKNOWN** → **UNKNOWN** + +Examples: +- Permissive project (MIT) + permissive dependency (Apache-2.0) → Compatible +- Permissive project (MIT) + strong copyleft dependency (GPL-3.0) → Incompatible +- Strong copyleft project (GPL-3.0) + permissive dependency (MIT) → Compatible + +## 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 +``` + +**Java property:** +```java +System.setProperty("TRUSTIFY_DA_LICENSE_CHECK", "false"); +``` + +## CLI Usage + +### License Command + +Display project license information from manifest and LICENSE file: + +```bash +java -jar trustify-da-java-client-cli.jar license /path/to/pom.xml +``` + +**Example output:** +```json +{ + "manifestLicense": { + "spdxId": "Apache-2.0", + "details": { + "identifiers": [ + { + "id": "Apache-2.0", + "name": "Apache License 2.0", + "isDeprecated": false, + "isOsiApproved": true, + "isFsfLibre": true, + "category": "PERMISSIVE" + } + ], + "expression": "Apache-2.0", + "name": "Apache License 2.0", + "category": "PERMISSIVE", + "source": "SPDX", + "sourceUrl": "https://spdx.org" + } + }, + "mismatch": false +} +``` + +> Note: `fileLicense` is omitted when null. The `license` command shows only your project's license. For dependency license compatibility, use component analysis. + +### Component Analysis with License Summary + +When running component analysis, the license summary is automatically included in the output: + +```bash +java -jar trustify-da-java-client-cli.jar component /path/to/pom.xml +``` + +## Programmatic Usage + +```java +import io.github.guacsec.trustifyda.ComponentAnalysisResult; +import io.github.guacsec.trustifyda.impl.ExhortApi; +import io.github.guacsec.trustifyda.license.LicenseCheck.LicenseSummary; + +ExhortApi api = new ExhortApi(); + +// Run component analysis with license check +ComponentAnalysisResult result = api.componentAnalysisWithLicense("/path/to/pom.xml").get(); + +// Access the analysis report +var report = result.report(); + +// Access the license summary +LicenseSummary licenseSummary = result.licenseSummary(); +if (licenseSummary != null) { + // Project license info + var projectLicense = licenseSummary.projectLicense(); + System.out.println("Mismatch: " + projectLicense.mismatch()); + + // Incompatible dependencies + for (var dep : licenseSummary.incompatibleDependencies()) { + System.out.println("Incompatible: " + dep.purl() + " - " + dep.licenses()); + } +} + +// Or use componentAnalysis() without license check (original API, unchanged) +var reportOnly = api.componentAnalysis("/path/to/pom.xml").get(); +``` + +## License Summary Fields + +The `LicenseSummary` returned in `ComponentAnalysisResult.licenseSummary()` contains: + +| Field | Type | Description | +|-------|------|-------------| +| `projectLicense` | `ProjectLicenseSummary` | Project license from manifest and LICENSE file | +| `incompatibleDependencies` | `List` | Dependencies with incompatible licenses | +| `error` | `String` | Error message if license check partially failed | + +**ProjectLicenseSummary:** + +| Field | Type | Description | +|-------|------|-------------| +| `manifest` | `JsonNode` | Full license details from backend for the manifest license (includes identifiers, category, name, source) | +| `file` | `JsonNode` | Full license details from backend for the LICENSE file license | +| `mismatch` | `boolean` | True if manifest and file licenses differ | + +**IncompatibleDependency:** + +| Field | Type | Description | +|-------|------|-------------| +| `purl` | `String` | Package URL of the dependency | +| `licenses` | `List` | Full license identifier objects (id, name, category, isDeprecated, isOsiApproved, isFsfLibre) | +| `category` | `LicenseCategory` | License category | +| `reason` | `String` | Explanation of the incompatibility | + +## SBOM Integration + +Project license information is automatically included in generated CycloneDX SBOMs on the root component: + +```json +{ + "metadata": { + "component": { + "type": "application", + "name": "my-project", + "version": "1.0.0", + "licenses": [ + { "license": { "id": "Apache-2.0" } } + ] + } + } +} +``` + +- **All ecosystems** include license information in the SBOM when available +- License names are resolved to valid SPDX identifiers using the CycloneDX license resolver +- If neither manifest nor LICENSE file contains a license, the SBOM root component will have no `licenses` field + +## Common Scenarios + +### Mismatch Between Manifest and LICENSE File + +If your `pom.xml` says `Apache-2.0` but your LICENSE file contains MIT text: + +```json +{ + "projectLicense": { + "manifest": { + "expression": "Apache-2.0", + "category": "PERMISSIVE" + }, + "file": { + "expression": "MIT", + "category": "PERMISSIVE" + }, + "mismatch": true + } +} +``` + +**Action:** Update your manifest or LICENSE file to match. + +### Incompatible Dependencies + +If you have a permissive-licensed project (e.g., Apache-2.0) but depend on copyleft-licensed libraries: + +```json +{ + "incompatibleDependencies": [ + { + "purl": "pkg:maven/org.mariadb.jdbc/mariadb-java-client@3.1.4", + "licenses": [ + { + "id": "LGPL-2.1", + "name": "GNU Lesser General Public License v2.1 only", + "isDeprecated": true, + "isOsiApproved": true, + "isFsfLibre": true, + "category": "WEAK_COPYLEFT" + } + ], + "category": "WEAK_COPYLEFT", + "reason": "Dependency license(s) are incompatible with the project license." + } + ] +} +``` + +**Action:** Review the flagged dependencies and consider finding alternatives with compatible licenses. diff --git a/src/main/java/io/github/guacsec/trustifyda/Api.java b/src/main/java/io/github/guacsec/trustifyda/Api.java index 63033110..fbfb704c 100644 --- a/src/main/java/io/github/guacsec/trustifyda/Api.java +++ b/src/main/java/io/github/guacsec/trustifyda/Api.java @@ -110,6 +110,16 @@ CompletableFuture componentAnalysis(String manifest, byte[] mani CompletableFuture componentAnalysis(String manifest) throws IOException; + /** + * Use for creating a component analysis with license compatibility checking. + * + * @param manifest the path of the manifest, example {@code /path/to/pom.xml} + * @return a ComponentAnalysisResult containing the analysis report and license summary + * @throws IOException when failed to load the manifest content + */ + CompletableFuture componentAnalysisWithLicense(String manifest) + throws IOException; + CompletableFuture> imageAnalysis(Set imageRefs) throws IOException; diff --git a/src/main/java/io/github/guacsec/trustifyda/ComponentAnalysisResult.java b/src/main/java/io/github/guacsec/trustifyda/ComponentAnalysisResult.java new file mode 100644 index 00000000..47d5f780 --- /dev/null +++ b/src/main/java/io/github/guacsec/trustifyda/ComponentAnalysisResult.java @@ -0,0 +1,22 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda; + +import io.github.guacsec.trustifyda.api.v5.AnalysisReport; +import io.github.guacsec.trustifyda.license.LicenseCheck.LicenseSummary; + +public record ComponentAnalysisResult(AnalysisReport report, LicenseSummary licenseSummary) {} diff --git a/src/main/java/io/github/guacsec/trustifyda/Provider.java b/src/main/java/io/github/guacsec/trustifyda/Provider.java index 14b56480..4f41bfae 100644 --- a/src/main/java/io/github/guacsec/trustifyda/Provider.java +++ b/src/main/java/io/github/guacsec/trustifyda/Provider.java @@ -17,6 +17,7 @@ package io.github.guacsec.trustifyda; import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.guacsec.trustifyda.license.LicenseUtils; import io.github.guacsec.trustifyda.tools.Ecosystem; import java.io.IOException; import java.nio.file.Path; @@ -71,6 +72,18 @@ protected Provider(Ecosystem.Type ecosystem, Path manifest) { */ public abstract Content provideComponent() throws IOException; + /** + * Read the project license from the manifest file. Providers that support manifest-level license + * declarations (e.g., pom.xml {@code }, package.json {@code license}, Cargo.toml {@code + * license}) should override this method. + * + * @return SPDX identifier or license name from the manifest, or null if not available + */ + public String readLicenseFromManifest() { + // Default: no manifest license field. Falls back to LICENSE file detection. + return LicenseUtils.readLicenseFile(manifest); + } + /** * If a package manager requires having a lock file it must exist in the provided path * diff --git a/src/main/java/io/github/guacsec/trustifyda/cli/App.java b/src/main/java/io/github/guacsec/trustifyda/cli/App.java index 99d31daf..4549118c 100644 --- a/src/main/java/io/github/guacsec/trustifyda/cli/App.java +++ b/src/main/java/io/github/guacsec/trustifyda/cli/App.java @@ -24,18 +24,23 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.github.guacsec.trustifyda.Api; +import io.github.guacsec.trustifyda.Provider; import io.github.guacsec.trustifyda.api.v5.AnalysisReport; import io.github.guacsec.trustifyda.api.v5.ProviderReport; import io.github.guacsec.trustifyda.api.v5.SourceSummary; import io.github.guacsec.trustifyda.image.ImageRef; import io.github.guacsec.trustifyda.image.ImageUtils; import io.github.guacsec.trustifyda.impl.ExhortApi; +import io.github.guacsec.trustifyda.license.ProjectLicense; +import io.github.guacsec.trustifyda.license.ProjectLicense.ProjectLicenseInfo; +import io.github.guacsec.trustifyda.tools.Ecosystem; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -86,15 +91,10 @@ private static CliArgs parseArgs(String[] args) { Command command = parseCommand(args[0]); - switch (command) { - case STACK: - case COMPONENT: - return parseFileBasedArgs(command, args); - case IMAGE: - return parseImageBasedArgs(command, args); - default: - throw new IllegalArgumentException("Unsupported command: " + command); - } + return switch (command) { + case STACK, COMPONENT, LICENSE -> parseFileBasedArgs(command, args); + case IMAGE -> parseImageBasedArgs(command, args); + }; } private static CliArgs parseFileBasedArgs(Command command, String[] args) { @@ -106,6 +106,9 @@ private static CliArgs parseFileBasedArgs(Command command, String[] args) { OutputFormat outputFormat = OutputFormat.JSON; if (args.length == 3) { + if (command == Command.LICENSE) { + throw new IllegalArgumentException("license command does not accept options"); + } outputFormat = parseOutputFormat(command, args[2]); } else if (args.length > 3) { throw new IllegalArgumentException("Too many arguments for " + command + " command"); @@ -151,33 +154,33 @@ private static CliArgs parseImageBasedArgs(Command command, String[] args) { } private static Command parseCommand(String commandStr) { - switch (commandStr) { - case "stack": - return Command.STACK; - case "component": - return Command.COMPONENT; - case "image": - return Command.IMAGE; - default: - throw new IllegalArgumentException( - "Unknown command: " + commandStr + ". Use 'stack', 'component', or 'image'"); - } + return switch (commandStr) { + case "stack" -> Command.STACK; + case "component" -> Command.COMPONENT; + case "image" -> Command.IMAGE; + case "license" -> Command.LICENSE; + default -> + throw new IllegalArgumentException( + "Unknown command: " + + commandStr + + ". Use 'stack', 'component', 'image', or 'license'"); + }; } private static OutputFormat parseOutputFormat(Command command, String formatArg) { - switch (formatArg) { - case "--summary": - return OutputFormat.SUMMARY; - case "--html": + return switch (formatArg) { + case "--summary" -> OutputFormat.SUMMARY; + case "--html" -> { if (command != Command.STACK && command != Command.IMAGE) { throw new IllegalArgumentException( "HTML format is only supported for stack and image commands"); } - return OutputFormat.HTML; - default: - throw new IllegalArgumentException( - "Unknown option for " + command + " command: " + formatArg); - } + yield OutputFormat.HTML; + } + default -> + throw new IllegalArgumentException( + "Unknown option for " + command + " command: " + formatArg); + }; } private static Path validateFile(String filePath) { @@ -192,45 +195,79 @@ private static Path validateFile(String filePath) { } private static CompletableFuture executeCommand(CliArgs args) throws IOException { - switch (args.command) { - case STACK: - return executeStackAnalysis(args.filePath.toAbsolutePath().toString(), args.outputFormat); - case COMPONENT: - return executeComponentAnalysis( - args.filePath.toAbsolutePath().toString(), args.outputFormat); - case IMAGE: - return executeImageAnalysis(args.imageRefs, args.outputFormat); - default: - throw new AssertionError(); - } + return switch (args.command) { + case STACK -> + executeStackAnalysis(args.filePath.toAbsolutePath().toString(), args.outputFormat); + case COMPONENT -> + executeComponentAnalysis(args.filePath.toAbsolutePath().toString(), args.outputFormat); + case IMAGE -> executeImageAnalysis(args.imageRefs, args.outputFormat); + case LICENSE -> executeLicenseCheck(args.filePath.toAbsolutePath()); + }; } private static CompletableFuture executeStackAnalysis( String filePath, OutputFormat outputFormat) throws IOException { Api api = new ExhortApi(); - switch (outputFormat) { - case JSON: - return api.stackAnalysis(filePath).thenApply(App::toJsonString); - case HTML: - return api.stackAnalysisHtml(filePath).thenApply(bytes -> new String(bytes)); - case SUMMARY: - return api.stackAnalysis(filePath) - .thenApply(App::extractSummary) - .thenApply(App::toJsonString); - default: - throw new AssertionError(); - } + return switch (outputFormat) { + case JSON -> api.stackAnalysis(filePath).thenApply(App::toJsonString); + case HTML -> api.stackAnalysisHtml(filePath).thenApply(bytes -> new String(bytes)); + case SUMMARY -> + api.stackAnalysis(filePath).thenApply(App::extractSummary).thenApply(App::toJsonString); + }; } private static CompletableFuture executeComponentAnalysis( String filePath, OutputFormat outputFormat) throws IOException { - Api api = new ExhortApi(); - CompletableFuture analysis = api.componentAnalysis(filePath); + ExhortApi api = new ExhortApi(); if (outputFormat.equals(OutputFormat.SUMMARY)) { - var summary = analysis.thenApply(App::extractSummary); - return summary.thenApply(App::toJsonString); + return api.componentAnalysis(filePath) + .thenApply(App::extractSummary) + .thenApply(App::toJsonString); + } + return api.componentAnalysisWithLicense(filePath).thenApply(App::toJsonString); + } + + private static CompletableFuture executeLicenseCheck(Path manifestPath) { + ExhortApi api = new ExhortApi(); + Provider provider = Ecosystem.getProvider(manifestPath); + ProjectLicenseInfo localResult = ProjectLicense.getProjectLicense(provider, manifestPath); + + CompletableFuture> manifestDetailsFuture = + buildLicenseInfo(api, localResult.fromManifest()); + CompletableFuture> fileDetailsFuture = + buildLicenseInfo(api, localResult.fromFile()); + + return manifestDetailsFuture.thenCombine( + fileDetailsFuture, + (manifestInfo, fileInfo) -> { + Map output = new LinkedHashMap<>(); + output.put("manifestLicense", manifestInfo); + output.put("fileLicense", fileInfo); + output.put("mismatch", localResult.mismatch()); + return toJsonString(output); + }); + } + + private static CompletableFuture> buildLicenseInfo( + ExhortApi api, String spdxId) { + if (spdxId == null) { + return CompletableFuture.completedFuture(null); } - return analysis.thenApply(App::toJsonString); + Map licenseInfo = new HashMap<>(); + licenseInfo.put("spdxId", spdxId); + return api.getLicenseDetails(spdxId) + .thenApply( + details -> { + if (details != null) { + licenseInfo.put("details", details); + } + return licenseInfo; + }) + .exceptionally( + ex -> { + licenseInfo.put("error", ex.getMessage()); + return licenseInfo; + }); } private static String toJsonString(Object obj) { @@ -244,18 +281,14 @@ private static String toJsonString(Object obj) { private static CompletableFuture executeImageAnalysis( Set imageRefs, OutputFormat outputFormat) throws IOException { Api api = new ExhortApi(); - switch (outputFormat) { - case JSON: - return api.imageAnalysis(imageRefs).thenApply(App::formatImageAnalysisResult); - case HTML: - return api.imageAnalysisHtml(imageRefs).thenApply(bytes -> new String(bytes)); - case SUMMARY: - return api.imageAnalysis(imageRefs) - .thenApply(App::extractImageSummary) - .thenApply(App::toJsonString); - default: - throw new AssertionError(); - } + return switch (outputFormat) { + case JSON -> api.imageAnalysis(imageRefs).thenApply(App::formatImageAnalysisResult); + case HTML -> api.imageAnalysisHtml(imageRefs).thenApply(bytes -> new String(bytes)); + case SUMMARY -> + api.imageAnalysis(imageRefs) + .thenApply(App::extractImageSummary) + .thenApply(App::toJsonString); + }; } private static String formatImageAnalysisResult(Map analysisResults) { diff --git a/src/main/java/io/github/guacsec/trustifyda/cli/Command.java b/src/main/java/io/github/guacsec/trustifyda/cli/Command.java index 2c3cb4d0..56d188a1 100644 --- a/src/main/java/io/github/guacsec/trustifyda/cli/Command.java +++ b/src/main/java/io/github/guacsec/trustifyda/cli/Command.java @@ -19,5 +19,6 @@ public enum Command { STACK, COMPONENT, - IMAGE + IMAGE, + LICENSE } diff --git a/src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java b/src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java index 16ee6950..a5f54d53 100644 --- a/src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java +++ b/src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java @@ -23,10 +23,12 @@ import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; import io.github.guacsec.trustifyda.Api; +import io.github.guacsec.trustifyda.ComponentAnalysisResult; import io.github.guacsec.trustifyda.Provider; import io.github.guacsec.trustifyda.api.v5.AnalysisReport; import io.github.guacsec.trustifyda.image.ImageRef; import io.github.guacsec.trustifyda.image.ImageUtils; +import io.github.guacsec.trustifyda.license.LicenseCheck; import io.github.guacsec.trustifyda.logging.LoggersFactory; import io.github.guacsec.trustifyda.tools.Ecosystem; import io.github.guacsec.trustifyda.utils.Environment; @@ -37,11 +39,13 @@ import java.net.InetSocketAddress; import java.net.ProxySelector; import java.net.URI; +import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpClient.Version; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @@ -75,6 +79,9 @@ public final class ExhortApi implements Api { public static final String TRUSTIFY_DA_REQUEST_ID_HEADER_NAME = "ex-request-id"; public static final String S_API_V_5_ANALYSIS = "%s/api/v5/analysis"; public static final String S_API_V_5_BATCH_ANALYSIS = "%s/api/v5/batch-analysis"; + public static final String S_API_V5_LICENSES = "%s/api/v5/licenses/%s"; + public static final String S_API_V5_LICENSES_IDENTIFY = "%s/api/v5/licenses/identify"; + private static final String TRUSTIFY_DA_LICENSE_CHECK = "TRUSTIFY_DA_LICENSE_CHECK"; private final String endpoint; @@ -272,7 +279,6 @@ public CompletableFuture stackAnalysis(final String manifestFile .sendAsync( this.buildStackRequest(manifestFile, MediaType.APPLICATION_JSON), HttpResponse.BodyHandlers.ofString()) - // .thenApply(HttpResponse::body) .thenApply( response -> getAnalysisReportFromResponse(response, "StackAnalysis", "json", exClientTraceId)) @@ -283,7 +289,6 @@ public CompletableFuture stackAnalysis(final String manifestFile "failed to invoke stackAnalysis for getting the json report, received" + " message= %s ", exception.getMessage())); - // LOG.log(System.Logger.Level.ERROR, "Exception Entity", exception); return new AnalysisReport(); }); } @@ -557,6 +562,137 @@ T getBatchAnalysisReportsFromResponse( return responseGenerator.apply(response); } + private static boolean isLicenseCheckEnabled() { + return Environment.getBoolean(TRUSTIFY_DA_LICENSE_CHECK, true); + } + + @Override + public CompletableFuture componentAnalysisWithLicense( + String manifestFile) throws IOException { + String exClientTraceId = commonHookBeginning(false); + var manifestPath = Path.of(manifestFile); + var provider = Ecosystem.getProvider(manifestPath); + var uri = URI.create(String.format(S_API_V_5_ANALYSIS, this.endpoint)); + var content = provider.provideComponent(); + String sbomJson = new String(content.buffer); + commonHookAfterProviderCreatedSbomAndBeforeExhort(); + return getAnalysisReportForComponent(uri, content, exClientTraceId) + .thenCompose( + report -> { + if (!isLicenseCheckEnabled()) { + return CompletableFuture.completedFuture(new ComponentAnalysisResult(report, null)); + } + return LicenseCheck.runLicenseCheck(this, provider, manifestPath, sbomJson, report) + .thenApply(summary -> new ComponentAnalysisResult(report, summary)) + .exceptionally( + ex -> { + LOG.warning( + String.format( + "License check failed, continuing without it: %s", + ex.getMessage())); + return new ComponentAnalysisResult(report, null); + }); + }); + } + + /** + * Fetch license details by SPDX identifier from the backend GET /api/v5/licenses/{spdx}. + * + * @param spdxId SPDX identifier (e.g., "Apache-2.0", "MIT") + * @return a CompletableFuture with license details as a JsonNode, or null if not found + */ + public CompletableFuture getLicenseDetails(String spdxId) { + String encodedId = URLEncoder.encode(spdxId, StandardCharsets.UTF_8); + URI uri = URI.create(String.format(S_API_V5_LICENSES, this.endpoint, encodedId)); + HttpRequest request = buildGetRequest(uri, "License Details"); + + return this.client + .sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply( + response -> { + if (response.statusCode() == 200) { + try { + return this.mapper.readTree(response.body()); + } catch (IOException e) { + LOG.warning( + String.format( + "Failed to parse license details for '%s': %s", spdxId, e.getMessage())); + return null; + } + } + LOG.warning( + String.format( + "Failed to fetch license details for '%s': HTTP %d", + spdxId, response.statusCode())); + return null; + }) + .exceptionally( + ex -> { + LOG.warning( + String.format( + "Failed to fetch license details for '%s': %s", spdxId, ex.getMessage())); + return null; + }); + } + + /** + * Call backend POST /api/v5/licenses/identify to identify license from file content. + * + * @param licenseFilePath path to LICENSE file + * @return a CompletableFuture with SPDX identifier or null + */ + public CompletableFuture identifyLicense(Path licenseFilePath) { + byte[] fileContent; + try { + fileContent = Files.readAllBytes(licenseFilePath); + } catch (IOException e) { + LOG.warning(String.format("Failed to read license file: %s", e.getMessage())); + return CompletableFuture.completedFuture(null); + } + URI uri = URI.create(String.format(S_API_V5_LICENSES_IDENTIFY, this.endpoint)); + HttpRequest request = + buildPostRequest( + uri, + "application/octet-stream", + HttpRequest.BodyPublishers.ofByteArray(fileContent), + "License Identify"); + + return this.client + .sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply( + response -> { + if (response.statusCode() == 200) { + try { + JsonNode data = this.mapper.readTree(response.body()); + JsonNode licenseNode = data.get("license"); + if (licenseNode != null && licenseNode.has("id")) { + String id = licenseNode.get("id").asText(); + return id.isBlank() ? null : id; + } + if (data.has("spdx_id")) { + String spdxId = data.get("spdx_id").asText(); + return spdxId.isBlank() ? null : spdxId; + } + if (data.has("identifier")) { + String identifier = data.get("identifier").asText(); + return identifier.isBlank() ? null : identifier; + } + } catch (IOException e) { + LOG.warning( + String.format( + "Failed to parse license identify response: %s", e.getMessage())); + } + } + return null; + }) + .exceptionally( + ex -> { + LOG.warning( + String.format("Failed to identify license from file: %s", ex.getMessage())); + return null; + }); + } + /** * Build an HTTP request for sending to the Backend API. * @@ -578,21 +714,50 @@ private HttpRequest buildRequest( .setHeader("Content-Type", content.type) .POST(HttpRequest.BodyPublishers.ofString(new String(content.buffer))); - // set trust-da-token - // Environment variable/property name = TRUST_DA_TOKEN + applyCommonHeaders(request, analysisType); + + return request.build(); + } + + private HttpRequest buildGetRequest(final URI uri, final String operationType) { + var request = + HttpRequest.newBuilder(uri) + .version(Version.HTTP_1_1) + .setHeader("Accept", MediaType.APPLICATION_JSON.toString()) + .GET(); + + applyCommonHeaders(request, operationType); + + return request.build(); + } + + private HttpRequest buildPostRequest( + final URI uri, + final String contentType, + final HttpRequest.BodyPublisher bodyPublisher, + final String operationType) { + var request = + HttpRequest.newBuilder(uri) + .version(Version.HTTP_1_1) + .setHeader("Accept", MediaType.APPLICATION_JSON.toString()) + .setHeader("Content-Type", contentType) + .POST(bodyPublisher); + + applyCommonHeaders(request, operationType); + + return request.build(); + } + + private void applyCommonHeaders(HttpRequest.Builder request, String operationType) { String trustDaToken = calculateHeaderValue(TRUST_DA_TOKEN_HEADER); if (trustDaToken != null) { request.setHeader(TRUST_DA_TOKEN_HEADER, trustDaToken); } - // set trust-da-source ( extension/plugin id/name) - // Environment variable/property name = TRUST_DA_SOURCE String trustDaSource = calculateHeaderValue(TRUST_DA_SOURCE_HEADER); if (trustDaSource != null) { request.setHeader(TRUST_DA_SOURCE_HEADER, trustDaSource); } - request.setHeader(TRUST_DA_OPERATION_TYPE_HEADER, analysisType); - - return request.build(); + request.setHeader(TRUST_DA_OPERATION_TYPE_HEADER, operationType); } private String calculateHeaderValue(String headerName) { diff --git a/src/main/java/io/github/guacsec/trustifyda/license/LicenseCheck.java b/src/main/java/io/github/guacsec/trustifyda/license/LicenseCheck.java new file mode 100644 index 00000000..2daed20b --- /dev/null +++ b/src/main/java/io/github/guacsec/trustifyda/license/LicenseCheck.java @@ -0,0 +1,213 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda.license; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.guacsec.trustifyda.Provider; +import io.github.guacsec.trustifyda.api.v5.AnalysisReport; +import io.github.guacsec.trustifyda.api.v5.LicenseCategory; +import io.github.guacsec.trustifyda.api.v5.LicenseIdentifier; +import io.github.guacsec.trustifyda.impl.ExhortApi; +import io.github.guacsec.trustifyda.license.LicenseUtils.Compatibility; +import io.github.guacsec.trustifyda.license.LicenseUtils.DependencyLicenseInfo; +import io.github.guacsec.trustifyda.license.ProjectLicense.ProjectLicenseInfo; +import io.github.guacsec.trustifyda.logging.LoggersFactory; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; + +/** + * Orchestrates the full license check: resolves the project license, fetches license details from + * the backend, extracts dependency licenses from the analysis report, and computes + * incompatibilities. + */ +public final class LicenseCheck { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final Logger LOG = LoggersFactory.getLogger(LicenseCheck.class.getName()); + + private LicenseCheck() {} + + /** + * Run full license check after a component analysis. + * + * @param api the ExhortApi instance (provides HTTP client, endpoint, and auth headers) + * @param manifestPath path to the project manifest + * @param sbomJson the CycloneDX SBOM JSON string that was sent for analysis + * @param analysisReport the analysis report returned by the backend + * @return a CompletableFuture with the license summary + */ + public static CompletableFuture runLicenseCheck( + ExhortApi api, + Provider provider, + Path manifestPath, + String sbomJson, + AnalysisReport analysisReport) { + + ProjectLicenseInfo projectLicense = ProjectLicense.getProjectLicense(provider, manifestPath); + + Path licenseFilePath = LicenseUtils.findLicenseFilePath(manifestPath); + CompletableFuture backendFileIdFuture; + if (licenseFilePath != null) { + backendFileIdFuture = + api.identifyLicense(licenseFilePath) + .exceptionally( + ex -> { + // Fall back to local detection + return null; + }); + } else { + backendFileIdFuture = CompletableFuture.completedFuture(null); + } + + return backendFileIdFuture.thenCompose( + backendFileId -> { + String manifestSpdx = projectLicense.fromManifest(); + String fileSpdx = backendFileId != null ? backendFileId : projectLicense.fromFile(); + boolean mismatch = + manifestSpdx != null + && fileSpdx != null + && !LicenseUtils.normalizeSpdx(manifestSpdx) + .equals(LicenseUtils.normalizeSpdx(fileSpdx)); + + CompletableFuture manifestDetailsFuture = + manifestSpdx != null + ? api.getLicenseDetails(manifestSpdx) + : CompletableFuture.completedFuture(null); + + CompletableFuture fileDetailsFuture; + if (fileSpdx != null + && (manifestSpdx == null + || !LicenseUtils.normalizeSpdx(manifestSpdx) + .equals(LicenseUtils.normalizeSpdx(fileSpdx)))) { + fileDetailsFuture = api.getLicenseDetails(fileSpdx); + } else if (fileSpdx != null) { + // Same license — reuse the manifest details future + fileDetailsFuture = manifestDetailsFuture; + } else { + fileDetailsFuture = CompletableFuture.completedFuture(null); + } + + return manifestDetailsFuture.thenCombine( + fileDetailsFuture, + (manifestDetails, fileDetails) -> { + ProjectLicenseSummary projectLicenseSummary = + new ProjectLicenseSummary(manifestDetails, fileDetails, mismatch); + + List purls = + extractPurls(sbomJson).stream().map(LicenseUtils::normalizePurlString).toList(); + if (purls.isEmpty()) { + return new LicenseSummary(projectLicenseSummary, Collections.emptyList(), null); + } + + Map licenseByPurl = + LicenseUtils.licensesFromReport(analysisReport, purls); + + if (licenseByPurl.isEmpty()) { + return new LicenseSummary( + projectLicenseSummary, + Collections.emptyList(), + "No license data available in analysis report"); + } + + LicenseCategory manifestCategory = LicenseUtils.extractCategory(manifestDetails); + LicenseCategory fileCategory = LicenseUtils.extractCategory(fileDetails); + LicenseCategory projectCategory = + manifestCategory != null ? manifestCategory : fileCategory; + List incompatible = new ArrayList<>(); + + for (String purl : purls) { + DependencyLicenseInfo entry = licenseByPurl.get(purl); + if (entry == null) { + continue; + } + Compatibility status = + LicenseUtils.getCompatibility(projectCategory, entry.category()); + if (status == Compatibility.INCOMPATIBLE) { + incompatible.add( + new IncompatibleDependency( + purl, + entry.licenses(), + entry.category(), + "Dependency license(s) are incompatible with the project license.")); + } + } + + return new LicenseSummary(projectLicenseSummary, incompatible, null); + }); + }); + } + + private static List extractPurls(String sbomJson) { + try { + JsonNode sbom = MAPPER.readTree(sbomJson); + + // Get root ref to exclude it + String rootRef = null; + JsonNode metadata = sbom.get("metadata"); + if (metadata != null) { + JsonNode component = metadata.get("component"); + if (component != null) { + if (component.has("bom-ref")) { + rootRef = component.get("bom-ref").asText(null); + } else if (component.has("purl")) { + rootRef = component.get("purl").asText(null); + } + } + } + + String normalizedRootRef = rootRef != null ? LicenseUtils.normalizePurlString(rootRef) : null; + + List purls = new ArrayList<>(); + JsonNode components = sbom.get("components"); + if (components != null && components.isArray()) { + for (JsonNode comp : components) { + String purl = comp.has("purl") ? comp.get("purl").asText(null) : null; + if (purl == null) { + purl = comp.has("bom-ref") ? comp.get("bom-ref").asText(null) : null; + } + if (purl != null + && (normalizedRootRef == null + || !LicenseUtils.normalizePurlString(purl).equals(normalizedRootRef))) { + purls.add(purl); + } + } + } + return purls; + } catch (IOException e) { + LOG.warning(String.format("Failed to extract PURLs from SBOM: %s", e.getMessage())); + return Collections.emptyList(); + } + } + + public record LicenseSummary( + ProjectLicenseSummary projectLicense, + List incompatibleDependencies, + String error) {} + + public record ProjectLicenseSummary(JsonNode manifest, JsonNode file, boolean mismatch) {} + + public record IncompatibleDependency( + String purl, List licenses, LicenseCategory category, String reason) {} +} diff --git a/src/main/java/io/github/guacsec/trustifyda/license/LicenseUtils.java b/src/main/java/io/github/guacsec/trustifyda/license/LicenseUtils.java new file mode 100644 index 00000000..17bf1d26 --- /dev/null +++ b/src/main/java/io/github/guacsec/trustifyda/license/LicenseUtils.java @@ -0,0 +1,316 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda.license; + +import static io.github.guacsec.trustifyda.impl.ExhortApi.debugLoggingIsNeeded; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.packageurl.MalformedPackageURLException; +import com.github.packageurl.PackageURL; +import io.github.guacsec.trustifyda.api.v5.AnalysisReport; +import io.github.guacsec.trustifyda.api.v5.LicenseCategory; +import io.github.guacsec.trustifyda.api.v5.LicenseIdentifier; +import io.github.guacsec.trustifyda.api.v5.LicenseInfo; +import io.github.guacsec.trustifyda.api.v5.LicenseProviderResult; +import io.github.guacsec.trustifyda.api.v5.PackageLicenseResult; +import io.github.guacsec.trustifyda.logging.LoggersFactory; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public final class LicenseUtils { + + private static final Logger LOG = LoggersFactory.getLogger(LicenseUtils.class.getName()); + private static final List LICENSE_FILES = List.of("LICENSE", "LICENSE.md", "LICENSE.txt"); + private static final Pattern APACHE_2_PATTERN = + Pattern.compile("Apache License,?\\s*Version 2\\.0", Pattern.CASE_INSENSITIVE); + private static final Pattern AGPL_3_PATTERN = + Pattern.compile("GNU AFFERO GENERAL PUBLIC LICENSE\\s+Version 3", Pattern.CASE_INSENSITIVE); + private static final Pattern LGPL_3_PATTERN = + Pattern.compile("GNU LESSER GENERAL PUBLIC LICENSE\\s+Version 3", Pattern.CASE_INSENSITIVE); + private static final Pattern LGPL_21_PATTERN = + Pattern.compile( + "GNU LESSER GENERAL PUBLIC LICENSE\\s+Version 2\\.1", Pattern.CASE_INSENSITIVE); + private static final Pattern GPL_2_PATTERN = + Pattern.compile("GNU GENERAL PUBLIC LICENSE\\s+Version 2", Pattern.CASE_INSENSITIVE); + private static final Pattern GPL_3_PATTERN = + Pattern.compile("GNU GENERAL PUBLIC LICENSE\\s+Version 3", Pattern.CASE_INSENSITIVE); + private static final Pattern BSD_2_PATTERN = + Pattern.compile("BSD 2-Clause", Pattern.CASE_INSENSITIVE); + private static final Pattern BSD_3_PATTERN = + Pattern.compile("BSD 3-Clause", Pattern.CASE_INSENSITIVE); + private static final Pattern MIT_LICENSE_PATTERN = + Pattern.compile("MIT License", Pattern.CASE_INSENSITIVE); + private static final Pattern MIT_PERMISSION_PATTERN = + Pattern.compile("Permission is hereby granted", Pattern.CASE_INSENSITIVE); + private static final Map LICENSE_PATTERNS = + Map.ofEntries( + Map.entry(APACHE_2_PATTERN, "Apache-2.0"), + Map.entry(AGPL_3_PATTERN, "AGPL-3.0-only"), + Map.entry(LGPL_3_PATTERN, "LGPL-3.0-only"), + Map.entry(LGPL_21_PATTERN, "LGPL-2.1-only"), + Map.entry(GPL_2_PATTERN, "GPL-2.0-only"), + Map.entry(GPL_3_PATTERN, "GPL-3.0-only"), + Map.entry(BSD_2_PATTERN, "BSD-2-Clause"), + Map.entry(BSD_3_PATTERN, "BSD-3-Clause")); + + public enum Compatibility { + COMPATIBLE, + INCOMPATIBLE, + UNKNOWN + } + + private LicenseUtils() {} + + /** + * Find LICENSE file path in the same directory as the manifest. + * + * @param manifestPath path to the manifest file + * @return path to LICENSE file or null if not found + */ + public static Path findLicenseFilePath(Path manifestPath) { + Path manifestDir = manifestPath.toAbsolutePath().getParent(); + for (String name : LICENSE_FILES) { + Path filePath = manifestDir.resolve(name); + if (Files.isRegularFile(filePath)) { + return filePath; + } + } + return null; + } + + /** + * Detect SPDX identifier from license text (first ~500 chars). + * + * @param text the license file text content + * @return SPDX identifier or null + */ + public static String detectSpdxFromText(String text) { + String head = text.length() > 500 ? text.substring(0, 500) : text; + if (MIT_LICENSE_PATTERN.matcher(head).find() && MIT_PERMISSION_PATTERN.matcher(head).find()) { + return "MIT"; + } + for (Map.Entry entry : LICENSE_PATTERNS.entrySet()) { + if (entry.getKey().matcher(head).find()) { + return entry.getValue(); + } + } + return null; + } + + /** + * Read LICENSE file and detect SPDX identifier. + * + * @param manifestPath path to manifest + * @return SPDX identifier from LICENSE file or null + */ + public static String readLicenseFile(Path manifestPath) { + Path licenseFilePath = findLicenseFilePath(manifestPath); + if (licenseFilePath == null) { + return null; + } + try { + String content = Files.readString(licenseFilePath, StandardCharsets.UTF_8); + String detected = detectSpdxFromText(content); + if (detected != null) { + return detected; + } + String firstLine = content.lines().findFirst().orElse("").trim(); + return firstLine.isEmpty() ? null : firstLine; + } catch (IOException e) { + if (debugLoggingIsNeeded()) { + LOG.warning("Failed reading LICENSE file: " + licenseFilePath); + } + return null; + } + } + + /** + * Get project license from manifest or LICENSE file. Returns manifestLicense if provided, + * otherwise tries LICENSE file. + * + * @param manifestLicense license from manifest (or null) + * @param manifestPath path to manifest + * @return SPDX identifier or null + */ + public static String getLicense(String manifestLicense, Path manifestPath) { + if (manifestLicense != null && !manifestLicense.isBlank()) { + return manifestLicense; + } + return readLicenseFile(manifestPath); + } + + /** + * Normalize SPDX identifier for comparison (lowercase, strip common suffixes). + * + * @param spdxOrName SPDX identifier or license name + * @return normalized string + */ + public static String normalizeSpdx(String spdxOrName) { + String s = spdxOrName.trim().toLowerCase(); + if (s.endsWith(" license")) { + return s.substring(0, s.length() - 8); + } + return s; + } + + /** + * Check if a dependency's license is compatible with the project license based on backend + * categories. + * + * @param projectCategory backend category for project license + * @param dependencyCategory backend category for dependency license + * @return compatibility result + */ + public static Compatibility getCompatibility( + LicenseCategory projectCategory, LicenseCategory dependencyCategory) { + if (projectCategory == null || dependencyCategory == null) { + return Compatibility.UNKNOWN; + } + if (projectCategory == LicenseCategory.UNKNOWN + || dependencyCategory == LicenseCategory.UNKNOWN) { + return Compatibility.UNKNOWN; + } + int projLevel = restrictiveness(projectCategory); + int depLevel = restrictiveness(dependencyCategory); + + if (projLevel < 0 || depLevel < 0) { + return Compatibility.UNKNOWN; + } + return depLevel > projLevel ? Compatibility.INCOMPATIBLE : Compatibility.COMPATIBLE; + } + + /** + * Extract category from a license details JSON response. + * + * @param licenseDetails JSON node from getLicenseDetails + * @return LicenseCategory or null + */ + public static LicenseCategory extractCategory(JsonNode licenseDetails) { + if (licenseDetails == null) { + return null; + } + JsonNode categoryNode = licenseDetails.get("category"); + if (categoryNode != null && !categoryNode.isNull()) { + try { + return LicenseCategory.fromValue(categoryNode.asText()); + } catch (IllegalArgumentException e) { + return null; + } + } + return null; + } + + private static int restrictiveness(LicenseCategory category) { + return switch (category) { + case PERMISSIVE -> 1; + case WEAK_COPYLEFT -> 2; + case STRONG_COPYLEFT -> 3; + default -> -1; + }; + } + + /** + * Build license map from an analysis report that already includes license data. Extracts + * dependency licenses from the report's licenses array. + * + * @param analysisReport the analysis report from the backend + * @param purls optional collection of purls to restrict to (empty means all) + * @return map of purl to DependencyLicenseInfo + */ + public static Map licensesFromReport( + AnalysisReport analysisReport, Collection purls) { + Map result = new HashMap<>(); + if (analysisReport == null || analysisReport.getLicenses() == null) { + return result; + } + + Set normalizedPurls = + (purls != null && !purls.isEmpty()) + ? purls.stream().map(LicenseUtils::normalizePurlString).collect(Collectors.toSet()) + : null; + + for (LicenseProviderResult providerResult : analysisReport.getLicenses()) { + Map packages = providerResult.getPackages(); + if (packages == null) { + continue; + } + + for (Map.Entry entry : packages.entrySet()) { + String purl = entry.getKey(); + String normalizedPurl = normalizePurlString(purl); + if (normalizedPurls != null && !normalizedPurls.contains(normalizedPurl)) { + continue; + } + + PackageLicenseResult pkgLicense = entry.getValue(); + LicenseInfo concluded = pkgLicense.getConcluded(); + List licenses = new ArrayList<>(); + LicenseCategory category = null; + + if (concluded != null) { + if (concluded.getIdentifiers() != null) { + licenses.addAll(concluded.getIdentifiers()); + } + category = concluded.getCategory(); + } + + result.put(normalizedPurl, new DependencyLicenseInfo(licenses, category)); + } + + if (!result.isEmpty()) { + break; + } + } + + return result; + } + + /** + * Canonicalize a PURL string by stripping qualifiers and subpath. Uses {@link PackageURL} for + * proper parsing. For example, {@code pkg:maven/log4j/log4j@1.2.17?scope=compile} becomes {@code + * pkg:maven/log4j/log4j@1.2.17}. + */ + static String normalizePurlString(String purl) { + try { + PackageURL normalizedPurl = new PackageURL(purl); + return new PackageURL( + normalizedPurl.getType(), + normalizedPurl.getNamespace(), + normalizedPurl.getName(), + normalizedPurl.getVersion(), + null, + null) + .canonicalize(); + } catch (MalformedPackageURLException e) { + LOG.warning("Unable to parse PackageURL " + purl); + return purl; + } + } + + public record DependencyLicenseInfo(List licenses, LicenseCategory category) {} +} diff --git a/src/main/java/io/github/guacsec/trustifyda/license/ProjectLicense.java b/src/main/java/io/github/guacsec/trustifyda/license/ProjectLicense.java new file mode 100644 index 00000000..c5a61215 --- /dev/null +++ b/src/main/java/io/github/guacsec/trustifyda/license/ProjectLicense.java @@ -0,0 +1,45 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda.license; + +import io.github.guacsec.trustifyda.Provider; +import java.nio.file.Path; + +public final class ProjectLicense { + + private ProjectLicense() {} + + /** + * Resolve project license from manifest and from LICENSE file in manifest directory. Uses local + * pattern matching for LICENSE file identification (synchronous). + * + * @param manifestPath path to manifest + * @return project license info with fromManifest, fromFile, and mismatch fields + */ + public static ProjectLicenseInfo getProjectLicense(Provider provider, Path manifestPath) { + String fromManifest = provider.readLicenseFromManifest(); + String fromFile = LicenseUtils.readLicenseFile(manifestPath); + if (fromManifest == null || fromFile == null) { + return new ProjectLicenseInfo(fromManifest, fromFile, false); + } + boolean mismatch = + !LicenseUtils.normalizeSpdx(fromManifest).equals(LicenseUtils.normalizeSpdx(fromFile)); + return new ProjectLicenseInfo(fromManifest, fromFile, mismatch); + } + + public record ProjectLicenseInfo(String fromManifest, String fromFile, boolean mismatch) {} +} diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java index 19cfc636..66c3118e 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java @@ -23,6 +23,7 @@ import com.github.packageurl.PackageURL; import io.github.guacsec.trustifyda.Api; import io.github.guacsec.trustifyda.Provider; +import io.github.guacsec.trustifyda.license.LicenseUtils; import io.github.guacsec.trustifyda.logging.LoggersFactory; import io.github.guacsec.trustifyda.providers.rust.model.CargoDep; import io.github.guacsec.trustifyda.providers.rust.model.CargoDepKind; @@ -609,6 +610,25 @@ public CargoProvider(Path manifest) { } } + @Override + public String readLicenseFromManifest() { + return readLicenseFromToml(null); + } + + private String readLicenseFromToml(TomlParseResult existingResult) { + try { + TomlParseResult tomlResult = existingResult != null ? existingResult : Toml.parse(manifest); + if (tomlResult.hasErrors()) { + return LicenseUtils.getLicense(null, manifest); + } + String license = tomlResult.getString("package.license"); + return LicenseUtils.getLicense(license, manifest); + } catch (IOException e) { + log.warning("Failed to read license from Cargo.toml: " + e.getMessage()); + return LicenseUtils.getLicense(null, manifest); + } + } + @Override public Content provideComponent() throws IOException { Sbom sbom = createSbom(AnalysisType.COMPONENT); @@ -639,7 +659,7 @@ private Sbom createSbom(AnalysisType analysisType) throws IOException { var root = new PackageURL( Type.CARGO.getType(), null, projectInfo.name(), projectInfo.version(), null, null); - sbom.addRoot(root); + sbom.addRoot(root, readLicenseFromToml(tomlResult)); String cargoContent = Files.readString(manifest, StandardCharsets.UTF_8); Set ignoredDeps = getIgnoredDependencies(tomlResult, cargoContent); diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java index 5fbe1d21..6951ae86 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java @@ -288,7 +288,7 @@ private Sbom buildSbomFromGraph( PackageURL root = toPurl(rootPackage, "@"); Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); - sbom.addRoot(root); + sbom.addRoot(root, readLicenseFromManifest()); edges.forEach( (key, value) -> { PackageURL source = toPurl(key, "@"); @@ -417,7 +417,7 @@ private Sbom buildSbomFromList(String golangDeps, List ignoredDeps) List deps = collectAllDirectDependencies(Arrays.asList(allModulesFlat), parentVertex); Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); - sbom.addRoot(root); + sbom.addRoot(root, readLicenseFromManifest()); deps.stream() .filter(dep -> !isGoToolchainEntry(dep)) .forEach( diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java index 8ca4ef61..f9cc79ca 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java @@ -281,7 +281,7 @@ private Sbom buildSbomFromTextFormat( String root = getRoot(textFormatFile, propertiesMap); PackageURL rootPurl = parseDep(root); - sbom.addRoot(rootPurl); + sbom.addRoot(rootPurl, readLicenseFromManifest()); List runtimeConfig = extractLines(textFormatFile, RUNTIME_CLASSPATH); List compileConfig = extractLines(textFormatFile, COMPILE_CLASSPATH); diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java index 8402a6d7..b400d243 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java @@ -22,6 +22,7 @@ import com.github.packageurl.PackageURL; import io.github.guacsec.trustifyda.Api; import io.github.guacsec.trustifyda.Provider; +import io.github.guacsec.trustifyda.license.LicenseUtils; import io.github.guacsec.trustifyda.logging.LoggersFactory; import io.github.guacsec.trustifyda.sbom.Sbom; import io.github.guacsec.trustifyda.sbom.SbomFactory; @@ -30,6 +31,7 @@ import io.github.guacsec.trustifyda.utils.Environment; import io.github.guacsec.trustifyda.utils.IgnorePatternDetector; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -66,6 +68,53 @@ public JavaMavenProvider(Path manifest) { this.mvnExecutable = selectMvnRuntime(manifest); } + @Override + public String readLicenseFromManifest() { + String manifestLicense = readLicenseFromPom(manifest); + return LicenseUtils.getLicense(manifestLicense, manifest); + } + + /** + * Parse the {@code } element from a pom.xml file. + * + * @param pomPath path to pom.xml + * @return the first license name found, or null + */ + private String readLicenseFromPom(Path pomPath) { + XMLInputFactory factory = XMLInputFactory.newInstance(); + try (InputStream is = Files.newInputStream(pomPath)) { + XMLStreamReader reader = factory.createXMLStreamReader(is); + boolean insideLicenses = false; + boolean insideLicense = false; + while (reader.hasNext()) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) { + String name = reader.getLocalName(); + if ("licenses".equals(name)) { + insideLicenses = true; + } else if (insideLicenses && "license".equals(name)) { + insideLicense = true; + } else if (insideLicense && "name".equals(name)) { + String license = reader.getElementText(); + if (license != null && !license.isBlank()) { + return license.trim(); + } + } + } else if (event == XMLStreamConstants.END_ELEMENT) { + String name = reader.getLocalName(); + if ("license".equals(name)) { + insideLicense = false; + } else if ("licenses".equals(name)) { + break; + } + } + } + } catch (IOException | XMLStreamException e) { + log.warning("Failed to read license from pom.xml: " + pomPath + " - " + e.getMessage()); + } + return null; + } + @Override public Content provideStack() throws IOException { var mvnCleanCmd = buildMvnCommandArgs("clean", "-f", manifest.toString(), "--batch-mode", "-q"); @@ -122,7 +171,7 @@ private Sbom buildSbomFromTextFormat(Path textFormatFile) throws IOException { List lines = Files.readAllLines(textFormatFile); var root = lines.get(0); var rootPurl = parseDep(root); - sbom.addRoot(rootPurl); + sbom.addRoot(rootPurl, readLicenseFromManifest()); lines.remove(0); String[] array = new String[lines.size()]; lines.toArray(array); @@ -171,7 +220,7 @@ private Content generateSbomFromEffectivePom() throws IOException { .filter(DependencyAggregator::isTestDependency) .collect(Collectors.toSet()); var deps = getDependencies(tmpEffPom); - var sbom = SbomFactory.newInstance().addRoot(getRoot(tmpEffPom)); + var sbom = SbomFactory.newInstance().addRoot(getRoot(tmpEffPom), readLicenseFromManifest()); deps.stream() .filter(dep -> !testsDeps.contains(dep)) .map(DependencyAggregator::toPurl) diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java index 85c968f9..1c005f26 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProvider.java @@ -24,6 +24,7 @@ import com.github.packageurl.PackageURL; import io.github.guacsec.trustifyda.Api; import io.github.guacsec.trustifyda.Provider; +import io.github.guacsec.trustifyda.license.LicenseUtils; import io.github.guacsec.trustifyda.logging.LoggersFactory; import io.github.guacsec.trustifyda.providers.javascript.model.Manifest; import io.github.guacsec.trustifyda.sbom.Sbom; @@ -68,6 +69,11 @@ public JavaScriptProvider(Path manifest, Ecosystem.Type ecosystem, String cmd) { } } + @Override + public String readLicenseFromManifest() { + return LicenseUtils.getLicense(manifest.license, manifest.path); + } + protected final String packageManager() { return cmd; } @@ -151,7 +157,7 @@ protected final String[] getExecEnvAsArgs() { private Sbom getDependencySbom() throws IOException { var depTree = buildDependencyTree(true); var sbom = SbomFactory.newInstance(); - sbom.addRoot(manifest.root); + sbom.addRoot(manifest.root, readLicenseFromManifest()); addDependenciesToSbom(sbom, depTree); sbom.filterIgnoredDeps(manifest.ignored); return sbom; @@ -175,7 +181,7 @@ protected void addDependenciesToSbom(Sbom sbom, JsonNode depTree) { private Sbom getDirectDependencySbom() throws IOException { var depTree = buildDependencyTree(false); var sbom = SbomFactory.newInstance(); - sbom.addRoot(manifest.root); + sbom.addRoot(manifest.root, readLicenseFromManifest()); // include only production dependencies for component analysis getRootDependencies(depTree).entrySet().stream() .filter(e -> manifest.dependencies.contains(e.getKey())) diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProviderFactory.java b/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProviderFactory.java index e40c7446..e7367071 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProviderFactory.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/JavaScriptProviderFactory.java @@ -16,7 +16,6 @@ */ package io.github.guacsec.trustifyda.providers; -import io.github.guacsec.trustifyda.Provider; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; @@ -24,13 +23,13 @@ public final class JavaScriptProviderFactory { - private static final Map> JS_PROVIDERS = + private static final Map> JS_PROVIDERS = Map.of( JavaScriptNpmProvider.LOCK_FILE, JavaScriptNpmProvider::new, JavaScriptYarnProvider.LOCK_FILE, JavaScriptYarnProvider::new, JavaScriptPnpmProvider.LOCK_FILE, JavaScriptPnpmProvider::new); - public static Provider create(final Path manifestPath) { + public static JavaScriptProvider create(final Path manifestPath) { var manifestDir = manifestPath.getParent(); for (var entry : JS_PROVIDERS.entrySet()) { var lockFilePath = manifestDir.resolve(entry.getKey()); diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java index c11c9bb5..8ff49e8b 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java @@ -67,7 +67,9 @@ public Content provideStack() throws IOException { pythonController.getDependencies(manifest.toString(), true); printDependenciesTree(dependencies); Sbom sbom = SbomFactory.newInstance(Sbom.BelongingCondition.PURL, "sensitive"); - sbom.addRoot(toPurl(DEFAULT_PIP_ROOT_COMPONENT_NAME, DEFAULT_PIP_ROOT_COMPONENT_VERSION)); + sbom.addRoot( + toPurl(DEFAULT_PIP_ROOT_COMPONENT_NAME, DEFAULT_PIP_ROOT_COMPONENT_VERSION), + readLicenseFromManifest()); for (Map component : dependencies) { addAllDependencies(sbom.getRoot(), component, sbom); } @@ -99,7 +101,9 @@ public Content provideComponent() throws IOException { pythonController.getDependencies(manifest.toString(), false); printDependenciesTree(dependencies); Sbom sbom = SbomFactory.newInstance(); - sbom.addRoot(toPurl(DEFAULT_PIP_ROOT_COMPONENT_NAME, DEFAULT_PIP_ROOT_COMPONENT_VERSION)); + sbom.addRoot( + toPurl(DEFAULT_PIP_ROOT_COMPONENT_NAME, DEFAULT_PIP_ROOT_COMPONENT_VERSION), + readLicenseFromManifest()); dependencies.forEach( (component) -> sbom.addDependency( diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/javascript/model/Manifest.java b/src/main/java/io/github/guacsec/trustifyda/providers/javascript/model/Manifest.java index 2be133d0..8099bea5 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/javascript/model/Manifest.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/javascript/model/Manifest.java @@ -32,6 +32,7 @@ public class Manifest { public final String name; public final String version; + public final String license; public final PackageURL root; public final Set dependencies; public final Set ignored; @@ -46,6 +47,7 @@ public Manifest(Path manifestPath) throws IOException { this.dependencies = loadDependencies(content); this.name = content.get("name").asText(); this.version = content.get("version").asText(); + this.license = loadLicense(content); this.root = JavaScriptProvider.toPurl(name, version); this.ignored = loadIgnored(content); } @@ -67,6 +69,39 @@ private Set loadDependencies(JsonNode content) { return Collections.unmodifiableSet(names); } + private String loadLicense(JsonNode content) { + if (content == null) { + return null; + } + // modern npm format + JsonNode licenseNode = content.get("license"); + if (licenseNode != null) { + if (licenseNode.isTextual()) { + String value = licenseNode.asText().trim(); + return value.isEmpty() ? null : value; + } + if (licenseNode.isObject()) { + JsonNode type = licenseNode.get("type"); + if (type != null && type.isTextual()) { + return type.asText().trim(); + } + } + } + + // legacy format + JsonNode licensesNode = content.get("licenses"); + if (licensesNode != null && licensesNode.isArray() && !licensesNode.isEmpty()) { + JsonNode first = licensesNode.get(0); + if (first.hasNonNull("type")) { + return first.get("type").asText().trim(); + } + if (first.hasNonNull("name")) { + return first.get("name").asText().trim(); + } + } + return null; + } + private Set loadIgnored(JsonNode content) { if (content == null) { return Collections.emptySet(); diff --git a/src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java b/src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java index 78f67775..e7b8f1fa 100644 --- a/src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java +++ b/src/main/java/io/github/guacsec/trustifyda/sbom/CycloneDXSbom.java @@ -41,7 +41,9 @@ import org.cyclonedx.model.Component; import org.cyclonedx.model.Component.Type; import org.cyclonedx.model.Dependency; +import org.cyclonedx.model.LicenseChoice; import org.cyclonedx.model.Metadata; +import org.cyclonedx.util.LicenseResolver; public class CycloneDXSbom implements Sbom { @@ -121,8 +123,18 @@ private BiPredicate, Component> getBelongingConditionByCoordinates } public Sbom addRoot(PackageURL rootRef) { + return addRoot(rootRef, null); + } + + public Sbom addRoot(PackageURL rootRef, String license) { this.root = rootRef; Component rootComponent = newRootComponent(rootRef); + if (license != null && !license.isBlank()) { + LicenseChoice licenseChoice = LicenseResolver.resolve(license); + if (licenseChoice != null) { + rootComponent.setLicenses(licenseChoice); + } + } bom.getMetadata().setComponent(rootComponent); bom.getComponents().add(rootComponent); bom.getDependencies().add(newDependency(rootRef)); diff --git a/src/main/java/io/github/guacsec/trustifyda/sbom/Sbom.java b/src/main/java/io/github/guacsec/trustifyda/sbom/Sbom.java index d15219b4..d882519f 100644 --- a/src/main/java/io/github/guacsec/trustifyda/sbom/Sbom.java +++ b/src/main/java/io/github/guacsec/trustifyda/sbom/Sbom.java @@ -23,6 +23,8 @@ public interface Sbom { public Sbom addRoot(PackageURL root); + public Sbom addRoot(PackageURL root, String license); + public PackageURL getRoot(); public Sbom filterIgnoredDeps(Collection ignoredDeps); diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8fc73303..6677376b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -35,6 +35,7 @@ exports io.github.guacsec.trustifyda.providers.rust.model; exports io.github.guacsec.trustifyda.logging; exports io.github.guacsec.trustifyda.image; + exports io.github.guacsec.trustifyda.license; opens io.github.guacsec.trustifyda.image to com.fasterxml.jackson.databind; diff --git a/src/main/resources/cli_help.txt b/src/main/resources/cli_help.txt index 710e6c5f..775576e2 100644 --- a/src/main/resources/cli_help.txt +++ b/src/main/resources/cli_help.txt @@ -13,9 +13,13 @@ COMMANDS: component [--summary] Perform component analysis on the specified manifest file + License compatibility checking is included by default in JSON output Options: - --summary Output summary in JSON format - (default) Output full report in JSON format + --summary Output summary in JSON format (without license check) + (default) Output full report in JSON format (includes license summary) + + license + Display project license information from manifest and LICENSE file in JSON format image [...] [--summary|--html] Perform security analysis on the specified container image(s) @@ -32,7 +36,8 @@ OPTIONS: -h, --help Show this help message ENVIRONMENT VARIABLES: - TRUSTIFY_DA_BACKEND_URL Backend URL for the Trustify Dependency Analytics service (required) + TRUSTIFY_DA_BACKEND_URL Backend URL for the Trustify Dependency Analytics service (required) + TRUSTIFY_DA_LICENSE_CHECK Enable/disable license check in component analysis (default: true) EXAMPLES: export TRUSTIFY_DA_BACKEND_URL=https://your-backend.url @@ -44,6 +49,9 @@ EXAMPLES: java -jar trustify-da-java-client-cli.jar component /path/to/requirements.txt java -jar trustify-da-java-client-cli.jar stack /path/to/Cargo.toml + # License information + java -jar trustify-da-java-client-cli.jar license /path/to/pom.xml + # Container image analysis java -jar trustify-da-java-client-cli.jar image nginx:latest java -jar trustify-da-java-client-cli.jar image nginx:latest docker.io/library/node:18 diff --git a/src/test/java/io/github/guacsec/trustifyda/cli/AppTest.java b/src/test/java/io/github/guacsec/trustifyda/cli/AppTest.java index aedaed5b..d8c4e59f 100644 --- a/src/test/java/io/github/guacsec/trustifyda/cli/AppTest.java +++ b/src/test/java/io/github/guacsec/trustifyda/cli/AppTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import io.github.guacsec.trustifyda.ComponentAnalysisResult; import io.github.guacsec.trustifyda.ExhortTest; import io.github.guacsec.trustifyda.api.v5.AnalysisReport; import io.github.guacsec.trustifyda.api.v5.ProviderReport; @@ -326,7 +327,7 @@ void executeCommand_with_ExecutionException_should_propagate_exception() throws CliArgs args = new CliArgs(Command.COMPONENT, TEST_FILE, OutputFormat.JSON); // Create a failed future to simulate ExecutionException - CompletableFuture failedFuture = new CompletableFuture<>(); + CompletableFuture failedFuture = new CompletableFuture<>(); failedFuture.completeExceptionally(new RuntimeException("Analysis failed")); // Mock ExhortApi constructor @@ -334,7 +335,7 @@ void executeCommand_with_ExecutionException_should_propagate_exception() throws mockConstruction( ExhortApi.class, (mock, context) -> { - when(mock.componentAnalysis(any(String.class))).thenReturn(failedFuture); + when(mock.componentAnalysisWithLicense(any(String.class))).thenReturn(failedFuture); })) { // Use reflection to access the private executeCommand method @@ -402,10 +403,12 @@ void command_enum_should_have_correct_values() { assertThat(Command.STACK).isNotNull(); assertThat(Command.COMPONENT).isNotNull(); assertThat(Command.IMAGE).isNotNull(); - assertThat(Command.values()).hasSize(3); + assertThat(Command.LICENSE).isNotNull(); + assertThat(Command.values()).hasSize(4); assertThat(Command.valueOf("STACK")).isEqualTo(Command.STACK); assertThat(Command.valueOf("COMPONENT")).isEqualTo(Command.COMPONENT); assertThat(Command.valueOf("IMAGE")).isEqualTo(Command.IMAGE); + assertThat(Command.valueOf("LICENSE")).isEqualTo(Command.LICENSE); } @Test diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderLicenseTest.java b/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderLicenseTest.java new file mode 100644 index 00000000..63bdaa31 --- /dev/null +++ b/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderLicenseTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda.providers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; + +import io.github.guacsec.trustifyda.ExhortTest; +import io.github.guacsec.trustifyda.tools.Operations; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CargoProviderLicenseTest extends ExhortTest { + + private CargoProvider createProvider(String testFolder) { + Path cargoTomlPath = resolveFile("tst_manifests/cargo/license/" + testFolder + "/Cargo.toml"); + try (MockedStatic mockedOperations = mockStatic(Operations.class)) { + mockedOperations + .when(() -> Operations.getExecutable(anyString(), anyString())) + .thenReturn("cargo"); + return new CargoProvider(cargoTomlPath); + } + } + + @Test + void readLicenseFromManifest_returns_license_from_cargo_toml() { + var provider = createProvider("cargo_with_license"); + String license = provider.readLicenseFromManifest(); + assertThat(license).isEqualTo("MIT"); + } + + @Test + void readLicenseFromManifest_returns_null_when_no_license() { + var provider = createProvider("cargo_without_license"); + String license = provider.readLicenseFromManifest(); + assertThat(license).isNull(); + } +} diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/JavaMavenProviderLicenseTest.java b/src/test/java/io/github/guacsec/trustifyda/providers/JavaMavenProviderLicenseTest.java new file mode 100644 index 00000000..70400a8d --- /dev/null +++ b/src/test/java/io/github/guacsec/trustifyda/providers/JavaMavenProviderLicenseTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda.providers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; + +import io.github.guacsec.trustifyda.ExhortTest; +import io.github.guacsec.trustifyda.tools.Operations; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class JavaMavenProviderLicenseTest extends ExhortTest { + + private JavaMavenProvider createProvider(String testFolder) { + Path pomPath = resolveFile("tst_manifests/maven/license/" + testFolder + "/pom.xml"); + try (MockedStatic mockedOperations = mockStatic(Operations.class)) { + mockedOperations + .when(() -> Operations.getExecutable(anyString(), anyString())) + .thenReturn("mvn"); + return new JavaMavenProvider(pomPath); + } + } + + @Test + void readLicenseFromManifest_returns_license_from_pom() { + var provider = createProvider("pom_with_license"); + String license = provider.readLicenseFromManifest(); + assertThat(license).isEqualTo("Apache-2.0"); + } + + @Test + void readLicenseFromManifest_returns_first_license_when_multiple() { + var provider = createProvider("pom_with_multiple_licenses"); + String license = provider.readLicenseFromManifest(); + assertThat(license).isEqualTo("MIT"); + } + + @Test + void readLicenseFromManifest_returns_null_when_no_licenses_section() { + var provider = createProvider("pom_without_license"); + String license = provider.readLicenseFromManifest(); + assertThat(license).isNull(); + } + + @Test + void readLicenseFromManifest_returns_null_when_license_name_is_blank() { + var provider = createProvider("pom_with_empty_license"); + String license = provider.readLicenseFromManifest(); + assertThat(license).isNull(); + } +} diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/JavaScriptProviderLicenseTest.java b/src/test/java/io/github/guacsec/trustifyda/providers/JavaScriptProviderLicenseTest.java new file mode 100644 index 00000000..595db802 --- /dev/null +++ b/src/test/java/io/github/guacsec/trustifyda/providers/JavaScriptProviderLicenseTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda.providers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; + +import io.github.guacsec.trustifyda.ExhortTest; +import io.github.guacsec.trustifyda.tools.Operations; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class JavaScriptProviderLicenseTest extends ExhortTest { + + private JavaScriptProvider createProvider(String testFolder) { + Path packageJsonPath = resolveFile("tst_manifests/npm/license/" + testFolder + "/package.json"); + try (MockedStatic mockedOperations = mockStatic(Operations.class)) { + mockedOperations + .when(() -> Operations.getExecutable(anyString(), anyString(), any())) + .thenReturn("npm"); + return JavaScriptProviderFactory.create(packageJsonPath); + } + } + + @Test + void readLicenseFromManifest_returns_license_field() { + var provider = createProvider("package_with_license"); + String license = provider.readLicenseFromManifest(); + assertThat(license).isEqualTo("MIT"); + } + + @Test + void readLicenseFromManifest_returns_first_from_legacy_licenses_array() { + var provider = createProvider("package_with_legacy_licenses"); + String license = provider.readLicenseFromManifest(); + assertThat(license).isEqualTo("Apache-2.0"); + } + + @Test + void readLicenseFromManifest_returns_null_when_no_license() { + var provider = createProvider("package_without_license"); + String license = provider.readLicenseFromManifest(); + assertThat(license).isNull(); + } +} diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/LicenseFallbackTest.java b/src/test/java/io/github/guacsec/trustifyda/providers/LicenseFallbackTest.java new file mode 100644 index 00000000..17eabf95 --- /dev/null +++ b/src/test/java/io/github/guacsec/trustifyda/providers/LicenseFallbackTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2023-2025 Trustify Dependency Analytics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.guacsec.trustifyda.providers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; + +import io.github.guacsec.trustifyda.tools.Operations; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests LICENSE file fallback for providers without manifest license support. Mirrors the + * JavaScript client's test/providers/license_fallback.test.js + */ +@ExtendWith(MockitoExtension.class) +class LicenseFallbackTest { + + @Test + void gradle_provider_should_read_license_file_when_present(@TempDir Path tempDir) + throws IOException { + Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }"); + Files.writeString(tempDir.resolve("LICENSE"), "Apache License, Version 2.0"); + + try (MockedStatic mock = mockStatic(Operations.class)) { + mock.when(() -> Operations.getExecutable(anyString(), anyString())).thenReturn("gradle"); + var provider = new GradleProvider(tempDir.resolve("build.gradle")); + assertThat(provider.readLicenseFromManifest()).isEqualTo("Apache-2.0"); + } + } + + @Test + void golang_provider_should_read_license_file_when_present(@TempDir Path tempDir) + throws IOException { + Files.writeString(tempDir.resolve("go.mod"), "module example.com/test"); + Files.writeString(tempDir.resolve("LICENSE"), "MIT License\n\nPermission is hereby granted"); + + try (MockedStatic mock = mockStatic(Operations.class)) { + mock.when(() -> Operations.getExecutable(anyString(), anyString())).thenReturn("go"); + var provider = new GoModulesProvider(tempDir.resolve("go.mod")); + assertThat(provider.readLicenseFromManifest()).isEqualTo("MIT"); + } + } + + @Test + void python_provider_should_read_license_file_when_present(@TempDir Path tempDir) + throws IOException { + Files.writeString(tempDir.resolve("requirements.txt"), "requests==2.28.0"); + Files.writeString(tempDir.resolve("LICENSE"), "BSD 3-Clause License"); + + var provider = new PythonPipProvider(tempDir.resolve("requirements.txt")); + assertThat(provider.readLicenseFromManifest()).isEqualTo("BSD-3-Clause"); + } + + @Test + void providers_should_return_null_when_no_license_file_exists(@TempDir Path tempDir) + throws IOException { + Files.writeString(tempDir.resolve("go.mod"), "module example.com/test"); + + try (MockedStatic mock = mockStatic(Operations.class)) { + mock.when(() -> Operations.getExecutable(anyString(), anyString())).thenReturn("go"); + var provider = new GoModulesProvider(tempDir.resolve("go.mod")); + assertThat(provider.readLicenseFromManifest()).isNull(); + } + } +} diff --git a/src/test/resources/tst_manifests/cargo/license/cargo_with_license/Cargo.toml b/src/test/resources/tst_manifests/cargo/license/cargo_with_license/Cargo.toml new file mode 100644 index 00000000..27bd5e7a --- /dev/null +++ b/src/test/resources/tst_manifests/cargo/license/cargo_with_license/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "test-project" +version = "1.0.0" +license = "MIT" + +[dependencies] +serde = "1.0" diff --git a/src/test/resources/tst_manifests/cargo/license/cargo_without_license/Cargo.toml b/src/test/resources/tst_manifests/cargo/license/cargo_without_license/Cargo.toml new file mode 100644 index 00000000..7b2160f6 --- /dev/null +++ b/src/test/resources/tst_manifests/cargo/license/cargo_without_license/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "test-project" +version = "1.0.0" + +[dependencies] +serde = "1.0" diff --git a/src/test/resources/tst_manifests/maven/license/pom_with_empty_license/pom.xml b/src/test/resources/tst_manifests/maven/license/pom_with_empty_license/pom.xml new file mode 100644 index 00000000..429f9996 --- /dev/null +++ b/src/test/resources/tst_manifests/maven/license/pom_with_empty_license/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.example + project-with-empty-license + 1.0.0 + + + + + + + + + + log4j + log4j + 1.2.17 + + + diff --git a/src/test/resources/tst_manifests/maven/license/pom_with_license/pom.xml b/src/test/resources/tst_manifests/maven/license/pom_with_license/pom.xml new file mode 100644 index 00000000..71d60d6b --- /dev/null +++ b/src/test/resources/tst_manifests/maven/license/pom_with_license/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + com.example + project-with-license + 1.0.0 + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + + log4j + log4j + 1.2.17 + + + diff --git a/src/test/resources/tst_manifests/maven/license/pom_with_multiple_licenses/pom.xml b/src/test/resources/tst_manifests/maven/license/pom_with_multiple_licenses/pom.xml new file mode 100644 index 00000000..c477eef4 --- /dev/null +++ b/src/test/resources/tst_manifests/maven/license/pom_with_multiple_licenses/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + com.example + project-with-multiple-licenses + 1.0.0 + + + + MIT + https://opensource.org/licenses/MIT + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + + log4j + log4j + 1.2.17 + + + diff --git a/src/test/resources/tst_manifests/maven/license/pom_without_license/pom.xml b/src/test/resources/tst_manifests/maven/license/pom_without_license/pom.xml new file mode 100644 index 00000000..fbee6885 --- /dev/null +++ b/src/test/resources/tst_manifests/maven/license/pom_without_license/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + com.example + project-without-license + 1.0.0 + + + + log4j + log4j + 1.2.17 + + + diff --git a/src/test/resources/tst_manifests/npm/common/deps_with_ignore/expected_component_sbom.json b/src/test/resources/tst_manifests/npm/common/deps_with_ignore/expected_component_sbom.json index 8949743c..660a1dbb 100644 --- a/src/test/resources/tst_manifests/npm/common/deps_with_ignore/expected_component_sbom.json +++ b/src/test/resources/tst_manifests/npm/common/deps_with_ignore/expected_component_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { diff --git a/src/test/resources/tst_manifests/npm/common/deps_with_no_ignore/expected_component_sbom.json b/src/test/resources/tst_manifests/npm/common/deps_with_no_ignore/expected_component_sbom.json index 967fe7d1..59713d73 100644 --- a/src/test/resources/tst_manifests/npm/common/deps_with_no_ignore/expected_component_sbom.json +++ b/src/test/resources/tst_manifests/npm/common/deps_with_no_ignore/expected_component_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { diff --git a/src/test/resources/tst_manifests/npm/deps_with_ignore/expected_stack_sbom.json b/src/test/resources/tst_manifests/npm/deps_with_ignore/expected_stack_sbom.json index 3dfc97d3..318aeb4c 100644 --- a/src/test/resources/tst_manifests/npm/deps_with_ignore/expected_stack_sbom.json +++ b/src/test/resources/tst_manifests/npm/deps_with_ignore/expected_stack_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { diff --git a/src/test/resources/tst_manifests/npm/deps_with_no_ignore/expected_stack_sbom.json b/src/test/resources/tst_manifests/npm/deps_with_no_ignore/expected_stack_sbom.json index 713950aa..6d9d4340 100644 --- a/src/test/resources/tst_manifests/npm/deps_with_no_ignore/expected_stack_sbom.json +++ b/src/test/resources/tst_manifests/npm/deps_with_no_ignore/expected_stack_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { diff --git a/src/test/resources/tst_manifests/npm/license/package_with_legacy_licenses/package-lock.json b/src/test/resources/tst_manifests/npm/license/package_with_legacy_licenses/package-lock.json new file mode 100644 index 00000000..31b40426 --- /dev/null +++ b/src/test/resources/tst_manifests/npm/license/package_with_legacy_licenses/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "test-legacy", + "version": "1.0.0", + "lockfileVersion": 2 +} diff --git a/src/test/resources/tst_manifests/npm/license/package_with_legacy_licenses/package.json b/src/test/resources/tst_manifests/npm/license/package_with_legacy_licenses/package.json new file mode 100644 index 00000000..c98c006c --- /dev/null +++ b/src/test/resources/tst_manifests/npm/license/package_with_legacy_licenses/package.json @@ -0,0 +1,11 @@ +{ + "name": "test-legacy", + "version": "1.0.0", + "licenses": [ + { "type": "Apache-2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0" }, + { "type": "MIT", "url": "https://opensource.org/licenses/MIT" } + ], + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/src/test/resources/tst_manifests/npm/license/package_with_license/package-lock.json b/src/test/resources/tst_manifests/npm/license/package_with_license/package-lock.json new file mode 100644 index 00000000..1bc6d933 --- /dev/null +++ b/src/test/resources/tst_manifests/npm/license/package_with_license/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "test-project", + "version": "1.0.0", + "lockfileVersion": 2 +} diff --git a/src/test/resources/tst_manifests/npm/license/package_with_license/package.json b/src/test/resources/tst_manifests/npm/license/package_with_license/package.json new file mode 100644 index 00000000..e647602f --- /dev/null +++ b/src/test/resources/tst_manifests/npm/license/package_with_license/package.json @@ -0,0 +1,8 @@ +{ + "name": "test-project", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/src/test/resources/tst_manifests/npm/license/package_without_license/package-lock.json b/src/test/resources/tst_manifests/npm/license/package_without_license/package-lock.json new file mode 100644 index 00000000..1c674b97 --- /dev/null +++ b/src/test/resources/tst_manifests/npm/license/package_without_license/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "test-no-license", + "version": "1.0.0", + "lockfileVersion": 2 +} diff --git a/src/test/resources/tst_manifests/npm/license/package_without_license/package.json b/src/test/resources/tst_manifests/npm/license/package_without_license/package.json new file mode 100644 index 00000000..e90465c8 --- /dev/null +++ b/src/test/resources/tst_manifests/npm/license/package_without_license/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-no-license", + "version": "1.0.0", + "dependencies": { + "express": "^4.17.1" + } +} diff --git a/src/test/resources/tst_manifests/pnpm/deps_with_ignore/expected_stack_sbom.json b/src/test/resources/tst_manifests/pnpm/deps_with_ignore/expected_stack_sbom.json index 89f93111..3c73dab7 100644 --- a/src/test/resources/tst_manifests/pnpm/deps_with_ignore/expected_stack_sbom.json +++ b/src/test/resources/tst_manifests/pnpm/deps_with_ignore/expected_stack_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { diff --git a/src/test/resources/tst_manifests/pnpm/deps_with_no_ignore/expected_stack_sbom.json b/src/test/resources/tst_manifests/pnpm/deps_with_no_ignore/expected_stack_sbom.json index 47e696f8..a33a02e0 100644 --- a/src/test/resources/tst_manifests/pnpm/deps_with_no_ignore/expected_stack_sbom.json +++ b/src/test/resources/tst_manifests/pnpm/deps_with_no_ignore/expected_stack_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { diff --git a/src/test/resources/tst_manifests/yarn-berry/deps_with_ignore/expected_stack_sbom.json b/src/test/resources/tst_manifests/yarn-berry/deps_with_ignore/expected_stack_sbom.json index 200cbdaa..9c59b375 100644 --- a/src/test/resources/tst_manifests/yarn-berry/deps_with_ignore/expected_stack_sbom.json +++ b/src/test/resources/tst_manifests/yarn-berry/deps_with_ignore/expected_stack_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { diff --git a/src/test/resources/tst_manifests/yarn-berry/deps_with_no_ignore/expected_stack_sbom.json b/src/test/resources/tst_manifests/yarn-berry/deps_with_no_ignore/expected_stack_sbom.json index 7c65091c..ed3617e6 100644 --- a/src/test/resources/tst_manifests/yarn-berry/deps_with_no_ignore/expected_stack_sbom.json +++ b/src/test/resources/tst_manifests/yarn-berry/deps_with_no_ignore/expected_stack_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { diff --git a/src/test/resources/tst_manifests/yarn-classic/deps_with_ignore/expected_stack_sbom.json b/src/test/resources/tst_manifests/yarn-classic/deps_with_ignore/expected_stack_sbom.json index 26d9b19c..35d0066f 100644 --- a/src/test/resources/tst_manifests/yarn-classic/deps_with_ignore/expected_stack_sbom.json +++ b/src/test/resources/tst_manifests/yarn-classic/deps_with_ignore/expected_stack_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { diff --git a/src/test/resources/tst_manifests/yarn-classic/deps_with_no_ignore/expected_stack_sbom.json b/src/test/resources/tst_manifests/yarn-classic/deps_with_no_ignore/expected_stack_sbom.json index 523b8eba..fe78dfcb 100644 --- a/src/test/resources/tst_manifests/yarn-classic/deps_with_no_ignore/expected_stack_sbom.json +++ b/src/test/resources/tst_manifests/yarn-classic/deps_with_no_ignore/expected_stack_sbom.json @@ -9,6 +9,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" } }, @@ -18,6 +29,17 @@ "bom-ref" : "pkg:npm/backend@1.0.0", "name" : "backend", "version" : "1.0.0", + "licenses" : [ { + "license" : { + "id" : "ISC", + "text" : { + "contentType" : "text/plain", + "encoding" : "base64", + "content" : "SVNDIExpY2Vuc2U6CgpDb3B5cmlnaHQgKGMpIDIwMDQtMjAxMCBieSBJbnRlcm5ldCBTeXN0ZW1zIENvbnNvcnRpdW0sIEluYy4gKCJJU0MiKQpDb3B5cmlnaHQgKGMpIDE5OTUtMjAwMyBieSBJbnRlcm5ldCBTb2Z0d2FyZSBDb25zb3J0aXVtCgpQZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnkgcHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLCBwcm92aWRlZCB0aGF0IHRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIGFwcGVhciBpbiBhbGwgY29waWVzLgoKVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEICJBUyBJUyIgQU5EIElTQyBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSCBSRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFkgQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIElTQyBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsIElORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTSBMT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUiBPVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SIFBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuCg==" + }, + "url" : "https://www.isc.org/licenses/" + } + } ], "purl" : "pkg:npm/backend@1.0.0" }, { From 178570d5f5be8a4844451d0b3d1f460e5e808c95 Mon Sep 17 00:00:00 2001 From: Chao Wang Date: Thu, 19 Mar 2026 11:10:48 +0800 Subject: [PATCH 2/3] fix: flatten component analysis CLI output to match JS client structure --- .../java/io/github/guacsec/trustifyda/cli/App.java | 11 ++++++++++- .../io/github/guacsec/trustifyda/cli/AppTest.java | 10 ++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/guacsec/trustifyda/cli/App.java b/src/main/java/io/github/guacsec/trustifyda/cli/App.java index 4549118c..b9051a69 100644 --- a/src/main/java/io/github/guacsec/trustifyda/cli/App.java +++ b/src/main/java/io/github/guacsec/trustifyda/cli/App.java @@ -224,7 +224,16 @@ private static CompletableFuture executeComponentAnalysis( .thenApply(App::extractSummary) .thenApply(App::toJsonString); } - return api.componentAnalysisWithLicense(filePath).thenApply(App::toJsonString); + return api.componentAnalysisWithLicense(filePath) + .thenApply( + result -> { + @SuppressWarnings("unchecked") + Map flat = MAPPER.convertValue(result.report(), Map.class); + if (result.licenseSummary() != null) { + flat.put("licenseSummary", result.licenseSummary()); + } + return toJsonString(flat); + }); } private static CompletableFuture executeLicenseCheck(Path manifestPath) { diff --git a/src/test/java/io/github/guacsec/trustifyda/cli/AppTest.java b/src/test/java/io/github/guacsec/trustifyda/cli/AppTest.java index d8c4e59f..42d21384 100644 --- a/src/test/java/io/github/guacsec/trustifyda/cli/AppTest.java +++ b/src/test/java/io/github/guacsec/trustifyda/cli/AppTest.java @@ -616,13 +616,14 @@ void main_with_valid_existing_file_should_work_with_mocked_api() { // Test with absolute path to pom.xml String absolutePomPath = System.getProperty("user.dir") + "/pom.xml"; + ComponentAnalysisResult mockResult = new ComponentAnalysisResult(mockReport, null); try (MockedStatic mockedAppUtils = mockStatic(AppUtils.class); MockedConstruction mockedExhortApi = mockConstruction( ExhortApi.class, (mock, context) -> { - when(mock.componentAnalysis(any(String.class))) - .thenReturn(CompletableFuture.completedFuture(mockReport)); + when(mock.componentAnalysisWithLicense(any(String.class))) + .thenReturn(CompletableFuture.completedFuture(mockResult)); })) { App.main(new String[] {"component", absolutePomPath}); @@ -696,13 +697,14 @@ void main_with_default_json_format_should_work_with_mocked_api() { } // Test default JSON format for component command (no format flag) + ComponentAnalysisResult mockResult2 = new ComponentAnalysisResult(mockReport, null); try (MockedStatic mockedAppUtils = mockStatic(AppUtils.class); MockedConstruction mockedExhortApi = mockConstruction( ExhortApi.class, (mock, context) -> { - when(mock.componentAnalysis(any(String.class))) - .thenReturn(CompletableFuture.completedFuture(mockReport)); + when(mock.componentAnalysisWithLicense(any(String.class))) + .thenReturn(CompletableFuture.completedFuture(mockResult2)); })) { App.main(new String[] {"component", "pom.xml"}); From 99b0ad8d76063e23039640ad176c44eab799af46 Mon Sep 17 00:00:00 2001 From: Chao Wang Date: Fri, 20 Mar 2026 08:24:05 +0800 Subject: [PATCH 3/3] fix: close XMLStreamReader in JavaMavenProvider and make readLicenseFromManifest abstract --- .../github/guacsec/trustifyda/Provider.java | 12 ++--- .../trustifyda/providers/CargoProvider.java | 5 +- .../providers/GoModulesProvider.java | 6 +++ .../trustifyda/providers/GradleProvider.java | 6 +++ .../providers/JavaMavenProvider.java | 49 +++++++++++-------- .../providers/PythonPipProvider.java | 6 +++ 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/main/java/io/github/guacsec/trustifyda/Provider.java b/src/main/java/io/github/guacsec/trustifyda/Provider.java index 4f41bfae..03f6a17c 100644 --- a/src/main/java/io/github/guacsec/trustifyda/Provider.java +++ b/src/main/java/io/github/guacsec/trustifyda/Provider.java @@ -73,16 +73,14 @@ protected Provider(Ecosystem.Type ecosystem, Path manifest) { public abstract Content provideComponent() throws IOException; /** - * Read the project license from the manifest file. Providers that support manifest-level license - * declarations (e.g., pom.xml {@code }, package.json {@code license}, Cargo.toml {@code - * license}) should override this method. + * Read the project license from the manifest file. Each provider must decide how to extract the + * license from its manifest (e.g., pom.xml {@code }, package.json {@code license}, + * Cargo.toml {@code license}). Providers without a manifest-level license field should fall back + * to {@link LicenseUtils#readLicenseFile(Path)}. * * @return SPDX identifier or license name from the manifest, or null if not available */ - public String readLicenseFromManifest() { - // Default: no manifest license field. Falls back to LICENSE file detection. - return LicenseUtils.readLicenseFile(manifest); - } + public abstract String readLicenseFromManifest(); /** * If a package manager requires having a lock file it must exist in the provided path diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java index 66c3118e..413b6d9c 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java @@ -612,14 +612,15 @@ public CargoProvider(Path manifest) { @Override public String readLicenseFromManifest() { - return readLicenseFromToml(null); + String manifestLicense = readLicenseFromToml(null); + return LicenseUtils.getLicense(manifestLicense, manifest); } private String readLicenseFromToml(TomlParseResult existingResult) { try { TomlParseResult tomlResult = existingResult != null ? existingResult : Toml.parse(manifest); if (tomlResult.hasErrors()) { - return LicenseUtils.getLicense(null, manifest); + return null; } String license = tomlResult.getString("package.license"); return LicenseUtils.getLicense(license, manifest); diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java index 6951ae86..4c28cbca 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/GoModulesProvider.java @@ -22,6 +22,7 @@ import com.github.packageurl.PackageURL; import io.github.guacsec.trustifyda.Api; import io.github.guacsec.trustifyda.Provider; +import io.github.guacsec.trustifyda.license.LicenseUtils; import io.github.guacsec.trustifyda.logging.LoggersFactory; import io.github.guacsec.trustifyda.sbom.Sbom; import io.github.guacsec.trustifyda.sbom.SbomFactory; @@ -70,6 +71,11 @@ public GoModulesProvider(Path manifest) { this.mainModuleVersion = getDefaultMainModuleVersion(); } + @Override + public String readLicenseFromManifest() { + return LicenseUtils.readLicenseFile(manifest); + } + @Override public Content provideStack() throws IOException { // check for custom executable diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java index f9cc79ca..6a90acdf 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/GradleProvider.java @@ -22,6 +22,7 @@ import com.github.packageurl.PackageURL; import io.github.guacsec.trustifyda.Api; import io.github.guacsec.trustifyda.Provider; +import io.github.guacsec.trustifyda.license.LicenseUtils; import io.github.guacsec.trustifyda.logging.LoggersFactory; import io.github.guacsec.trustifyda.sbom.Sbom; import io.github.guacsec.trustifyda.sbom.SbomFactory; @@ -64,6 +65,11 @@ public GradleProvider(Path manifest) { super(Type.GRADLE, manifest); } + @Override + public String readLicenseFromManifest() { + return LicenseUtils.readLicenseFile(manifest); + } + @Override public Content provideStack() throws IOException { Path tempFile = getDependencies(manifest); diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java index b400d243..b83b5da2 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/JavaMavenProvider.java @@ -84,28 +84,37 @@ private String readLicenseFromPom(Path pomPath) { XMLInputFactory factory = XMLInputFactory.newInstance(); try (InputStream is = Files.newInputStream(pomPath)) { XMLStreamReader reader = factory.createXMLStreamReader(is); - boolean insideLicenses = false; - boolean insideLicense = false; - while (reader.hasNext()) { - int event = reader.next(); - if (event == XMLStreamConstants.START_ELEMENT) { - String name = reader.getLocalName(); - if ("licenses".equals(name)) { - insideLicenses = true; - } else if (insideLicenses && "license".equals(name)) { - insideLicense = true; - } else if (insideLicense && "name".equals(name)) { - String license = reader.getElementText(); - if (license != null && !license.isBlank()) { - return license.trim(); + try { + boolean insideLicenses = false; + boolean insideLicense = false; + while (reader.hasNext()) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) { + String name = reader.getLocalName(); + if ("licenses".equals(name)) { + insideLicenses = true; + } else if (insideLicenses && "license".equals(name)) { + insideLicense = true; + } else if (insideLicense && "name".equals(name)) { + String license = reader.getElementText(); + if (license != null && !license.isBlank()) { + return license.trim(); + } + } + } else if (event == XMLStreamConstants.END_ELEMENT) { + String name = reader.getLocalName(); + if ("license".equals(name)) { + insideLicense = false; + } else if ("licenses".equals(name)) { + break; } } - } else if (event == XMLStreamConstants.END_ELEMENT) { - String name = reader.getLocalName(); - if ("license".equals(name)) { - insideLicense = false; - } else if ("licenses".equals(name)) { - break; + } + } finally { + if (!Objects.isNull(reader)) { + try { + reader.close(); + } catch (XMLStreamException e) { } } } diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java index 8ff49e8b..9d6ce7ef 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/PythonPipProvider.java @@ -23,6 +23,7 @@ import com.github.packageurl.PackageURL; import io.github.guacsec.trustifyda.Api; import io.github.guacsec.trustifyda.Provider; +import io.github.guacsec.trustifyda.license.LicenseUtils; import io.github.guacsec.trustifyda.logging.LoggersFactory; import io.github.guacsec.trustifyda.sbom.Sbom; import io.github.guacsec.trustifyda.sbom.SbomFactory; @@ -60,6 +61,11 @@ public PythonPipProvider(Path manifest) { super(Ecosystem.Type.PYTHON, manifest); } + @Override + public String readLicenseFromManifest() { + return LicenseUtils.readLicenseFile(manifest); + } + @Override public Content provideStack() throws IOException { PythonControllerBase pythonController = getPythonController();