|
| 1 | +# License resolution and compliance |
| 2 | + |
| 3 | +This document describes how the client resolves the **project license** (from manifest vs repository file), how it uses the **backend licenses-by-purl endpoint** to get dependency licenses, and how **component analysis** can report when the project license is missing, inconsistent, or incompatible with dependency licenses. |
| 4 | + |
| 5 | +## Goals |
| 6 | + |
| 7 | +1. **Resolve project license** from: |
| 8 | + - The manifest (e.g. `package.json` `license`, `pom.xml` `<licenses>`) when present. |
| 9 | + - A `LICENSE`, `LICENSE.md`, or `LICENSE.txt` file in the same directory as the manifest. |
| 10 | +2. **Report to the user** when the license declared in the manifest differs from the license text file. |
| 11 | +3. **Component analysis**: when the user runs component analysis (e.g. on opening a manifest), optionally check dependency licenses (via backend) and report if any are **incompatible** with the project license. |
| 12 | + |
| 13 | +## Resolving the project license |
| 14 | + |
| 15 | +### From the manifest |
| 16 | + |
| 17 | +| Ecosystem | Manifest | Where license is read | |
| 18 | +|----------------|-----------------|------------------------| |
| 19 | +| JavaScript | package.json | `license` (string) or `licenses` (array); can be SPDX id or "SEE LICENSE IN FILE" | |
| 20 | +| Java (Maven) | pom.xml | Effective POM: `<licenses><license><name>` / `<url>`; map common names to SPDX where possible | |
| 21 | +| Java (Gradle) | build.gradle(*) | No standard; some projects set extra/license in properties; optional best-effort | |
| 22 | +| Go | go.mod | No standard license field | |
| 23 | +| Python | requirements.txt| No license in manifest | |
| 24 | + |
| 25 | +(*) Gradle: license is often in a separate file or NOTICE; we do not parse build logic. |
| 26 | + |
| 27 | +### From the repository (LICENSE / LICENSE.md) |
| 28 | + |
| 29 | +- Look in the **manifest directory** for `LICENSE`, `LICENSE.md`, or `LICENSE.txt`. |
| 30 | +- Only searches the same directory as the manifest (not parent directories or git root). |
| 31 | +- Read file content; try to **detect** or **normalize** to an SPDX identifier (e.g. "Apache-2.0") for comparison using local pattern matching or the backend `/licenses/identify` endpoint for more accuracy. |
| 32 | + |
| 33 | +### Reporting manifest vs file mismatch |
| 34 | + |
| 35 | +- If both manifest and file are present and indicate different licenses → set `licenseSummary.manifestVsFileMismatch: true` and include both values so the user can fix the inconsistency. |
| 36 | +- If only one is present, we still expose both `fromManifest` and `fromFile` so the user sees what was found. |
| 37 | + |
| 38 | +## Backend: License Analysis API (v5) and license data in the analysis report |
| 39 | + |
| 40 | +The Trustify Dependency Analytics backend provides: |
| 41 | + |
| 42 | +1. **License data in the dependency analysis report** — When using the analysis API (e.g. stack or component analysis), the JSON report includes a **`licenses`** field with license information for all dependencies. This is a **LicensesResponse**: an array of provider results, each with `status`, `summary`, and `packages` (object keyed by purl). Each package has `concluded` (with `identifiers`, `expression`, `name`, `category`) and `evidence`. Categories are: **PERMISSIVE**, **WEAK_COPYLEFT**, **STRONG_COPYLEFT**, **UNKNOWN** (see [OpenAPI v5](https://github.com/guacsec/trustify-da-api-spec/blob/main/api/v5/openapi.yaml)). |
| 43 | + |
| 44 | +2. **License identification endpoint** — `POST /licenses/identify` accepts a LICENSE file (binary/text) and returns the identified SPDX license. This is used to accurately identify the project's LICENSE file when more precision is needed than local pattern matching. |
| 45 | + |
| 46 | +3. **License details endpoint** — `GET /api/v5/licenses/{spdx}` returns detailed information about a specific license by SPDX identifier, including `category`, `name`, `identifiers`, `expression`, `source`, etc. Used by the CLI `license` command to provide rich license information. |
| 47 | + |
| 48 | +The client: |
| 49 | + |
| 50 | +- When running the license check **after component analysis**, it uses the `result.licenses` data from the analysis response (no extra request needed for dependency licenses). |
| 51 | +- For the **project LICENSE file**, it first attempts local pattern matching (fast, synchronous). During the license check (async), it can optionally call `POST /licenses/identify` for more accurate backend-based identification. |
| 52 | +- Normalizes the license response into a map of purl → `{ licenses: string[], category? }` and uses the backend **category** for compatibility checking (e.g. project permissive + dependency STRONG_COPYLEFT → incompatible). |
| 53 | + |
| 54 | +## Component analysis: license incompatibility report |
| 55 | + |
| 56 | +When the user runs **component analysis** (e.g. upon opening a manifest): |
| 57 | + |
| 58 | +1. The client builds the SBOM (direct dependencies only) and sends it to the backend for the usual component analysis. |
| 59 | +2. By default or unless `TRUSTIFY_DA_LICENSE_CHECK=false` or an option like `licenseCheck: false` is set: |
| 60 | + - **Resolve project license** from manifest (synchronous). |
| 61 | + - If a LICENSE file exists, optionally call `POST /licenses/identify` to accurately identify it via the backend. |
| 62 | + - Set `licenseSummary.manifestVsFileMismatch` if manifest and LICENSE file licenses differ. |
| 63 | + - **Extract purls** from the SBOM that was sent (from `provided.content`). |
| 64 | + - **Get dependency licenses** from the analysis result’s `licenses` array (already included in the response). |
| 65 | + - For each dependency, determine if its license(s) are **compatible** with the project license, using the backend category (PERMISSIVE / WEAK_COPYLEFT / STRONG_COPYLEFT). |
| 66 | + - Attach a **license summary** to the component analysis result, e.g.: |
| 67 | + - `licenseSummary.projectLicenseFromManifest` |
| 68 | + - `licenseSummary.projectLicenseFromFile` |
| 69 | + - `licenseSummary.manifestVsFileMismatch` |
| 70 | + - `licenseSummary.incompatibleDependencies`: `[{ purl, licenses, category, reason }]` |
| 71 | + - `licenseSummary.dependencyLicenses`: `[{ purl, licenses, category }]` |
| 72 | + - So the user can see which dependencies have licenses incompatible with the project license. |
| 73 | + |
| 74 | +The client uses the backend **category** from the analysis report’s `licenses` field, plus a small in-client compatibility matrix (e.g. permissive project + STRONG_COPYLEFT dependency → incompatible). |
| 75 | + |
| 76 | +## Disabling the license check in component analysis |
| 77 | + |
| 78 | +- **Environment variable**: `TRUSTIFY_DA_LICENSE_CHECK=false` |
| 79 | +- **Option**: `opts.licenseCheck = false` when calling `client.componentAnalysis(manifest, opts)` |
| 80 | + |
| 81 | +By default (unless disabled), after the component analysis response is received, the client: |
| 82 | +1. Resolves the project license from manifest and LICENSE file (with optional backend identification for LICENSE file) |
| 83 | +2. Extracts dependency licenses from `result.licenses` in the analysis response |
| 84 | +3. Computes compatibility and attaches a `licenseSummary` to the report |
| 85 | + |
| 86 | +## API surface (client) |
| 87 | + |
| 88 | +- **Project license resolution** (local, synchronous): |
| 89 | + - `getProjectLicense(manifestPath, opts)` → `{ fromManifest, fromFile, mismatch }` |
| 90 | + - `getProjectLicenseFromManifestOnly(manifestPath, opts)` → `{ fromManifest, fromFile: null, mismatch: false }` |
| 91 | + - `findLicenseFilePath(manifestPath)` → `string|null` (path to LICENSE file) |
| 92 | + |
| 93 | +- **Backend license identification and details**: |
| 94 | + - `identifyLicenseViaBackend(licenseFilePath, backendUrl, opts)` → `Promise<string|null>` (SPDX id from `POST /licenses/identify`) |
| 95 | + - `getLicenseDetails(spdxId, backendUrl, opts)` → `Promise<Object|null>` (detailed license info from `GET /api/v5/licenses/{spdx}`, includes category, name, identifiers, etc.) |
| 96 | + |
| 97 | +- **Dependency licenses from analysis report**: |
| 98 | + - `licenseMapFromAnalysisReport(analysisReport, purls?)` → `Map<purl, { licenses, category? }>` |
| 99 | + - Extracts license data from the analysis response's `licenses` field (no extra request) |
| 100 | + |
| 101 | +- **Full license check** (for component analysis): |
| 102 | + - `runLicenseCheck(sbomContent, manifestPath, backendUrl, opts, analysisResult)` → `Promise<LicenseSummary>` |
| 103 | + - Uses `analysisResult.licenses` for dependency licenses. Optionally calls `POST /licenses/identify` for accurate project LICENSE file identification. Used inside `componentAnalysis()` when license check is enabled; result is attached as `report.licenseSummary`. |
| 104 | + |
| 105 | +- **Compatibility checking**: |
| 106 | + - `getCompatibility(projectLicense, dependencyLicenses[], dependencyCategory?)` → `'compatible' | 'incompatible' | 'unknown'` |
| 107 | + |
| 108 | +- **Utility** (typically not needed): |
| 109 | + - `getLicensesByPurl(purls, backendUrl, opts)` → `Promise<Map<purl, { licenses, category? }>>` (standalone call to `POST /api/v5/licenses`) |
| 110 | + |
| 111 | +Options (e.g. proxy, token) are passed through to the backend fetch in the same way as for analysis requests. |
0 commit comments