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..03f6a17c 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,16 @@ protected Provider(Ecosystem.Type ecosystem, Path manifest) { */ public abstract Content provideComponent() throws IOException; + /** + * 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 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/cli/App.java b/src/main/java/io/github/guacsec/trustifyda/cli/App.java index 99d31daf..b9051a69 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,88 @@ 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 analysis.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) { + 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); + } + 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 +290,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..413b6d9c 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,26 @@ public CargoProvider(Path manifest) { } } + @Override + public String readLicenseFromManifest() { + 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 null; + } + 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 +660,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..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 @@ -288,7 +294,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 +423,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..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); @@ -281,7 +287,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..b83b5da2 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,62 @@ 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); + 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; + } + } + } + } finally { + if (!Objects.isNull(reader)) { + try { + reader.close(); + } catch (XMLStreamException e) { + } + } + } + } 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 +180,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 +229,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..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(); @@ -67,7 +73,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 +107,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..42d21384 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 @@ -613,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}); @@ -693,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"}); 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" }, {