Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ let imageAnalysisWithArch = await client.imageAnalysis(['httpd:2.4.49^^amd64'])
```
</li>
</ul>

<h3>License Detection</h3>
<p>
The client automatically detects your project's license with intelligent fallback:
</p>
<ul>
<li><strong>Manifest-first:</strong> For ecosystems with license support (Maven, JavaScript), reads from manifest file (<code>pom.xml</code>, <code>package.json</code>)</li>
<li><strong>LICENSE file fallback:</strong> If no license in manifest, or for ecosystems without license support (Gradle, Go, Python), automatically reads from <code>LICENSE</code>, <code>LICENSE.md</code>, or <code>LICENSE.txt</code></li>
<li><strong>SBOM integration:</strong> Detected licenses are included in generated SBOMs for all ecosystems</li>
<li><strong>SPDX support:</strong> Automatically detects common licenses (Apache-2.0, MIT, GPL, BSD) from LICENSE file content</li>
</ul>
<p>
See <a href="./docs/license-resolution-and-compliance.md">License Resolution and Compliance</a> for detailed documentation.
</p>

<ul>
<li>
Use as ESM Module from Common-JS module
Expand Down Expand Up @@ -183,6 +198,21 @@ $ trustify-da-javascript-client license /path/to/package.json
<li><a href="https://gradle.org/">Gradle (Groovy and Kotlin DSL)</a> - <a href="https://gradle.org/install/">Gradle Installation</a></li>
</ul>

<h3>License Detection</h3>
<p>
The client automatically detects your project's license with intelligent fallback:
</p>
<ul>
<li><strong>Manifest-first:</strong> For ecosystems with license support (Maven, JavaScript), reads from manifest file (<code>pom.xml</code>, <code>package.json</code>)</li>
<li><strong>LICENSE file fallback:</strong> If no license in manifest, or for ecosystems without license support (Gradle, Go, Python), automatically reads from <code>LICENSE</code>, <code>LICENSE.md</code>, or <code>LICENSE.txt</code></li>
<li><strong>SBOM integration:</strong> Detected licenses are included in generated SBOMs for all ecosystems</li>
<li><strong>SPDX support:</strong> Automatically detects common licenses (Apache-2.0, MIT, GPL, BSD) from LICENSE file content</li>
</ul>
<p>
See <a href="./docs/license-resolution-and-compliance.md">License Resolution and Compliance</a> for detailed documentation.
</p>


<h3>Excluding Packages</h3>
<p>
Excluding a package from any analysis can be achieved by marking the package for exclusion.
Expand Down
23 changes: 18 additions & 5 deletions docs/license-resolution-and-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,23 @@ License analysis is **enabled by default** and provides:

### Project License Detection

The client looks for your project’s license in two places:
The client looks for your project’s license with **automatic fallback**:

1. **Manifest file** — Reads the license field from:
1. **Primary: Manifest file** — Reads the license field from:
- `package.json`: `license` field
- `pom.xml`: `<licenses><license><name>` element
- Other ecosystems: varies by ecosystem (some don’t have standard license fields)
- `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. **LICENSE file** — Searches for `LICENSE`, `LICENSE.md`, or `LICENSE.txt` in the same directory as your manifest
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

The backend’s license identification API is used for accurate LICENSE file detection.
**How the fallback works:**
- **Ecosystems with manifest license support** (Maven, JavaScript): 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 is used for accurate LICENSE file detection when available.

### Compatibility Checking

Expand Down Expand Up @@ -156,3 +163,9 @@ If you have a permissive-licensed project (MIT, Apache) but depend on GPL-licens
## SBOM Integration

Project license information is automatically included in generated SBOMs (CycloneDX format) in the root component’s `licenses` field.

**LICENSE file fallback in SBOMs:**
- **All ecosystems** now include license information in the SBOM when available
- **Gradle, Go, Python projects**: Even though these ecosystems don’t have manifest license fields, the SBOM will include the license from your LICENSE file
- **Maven, JavaScript projects**: The SBOM uses the manifest license, or falls back to LICENSE file if not specified in manifest
- If neither manifest nor LICENSE file contains a license, the SBOM root component will have no `licenses` field
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"tests": "mocha --config .mocharc.json --grep \".*analysis module.*\" --invert",
"tests:rep": "mocha --reporter-option maxDiffSize=0 --reporter json > unit-tests-result.json",
"precompile": "rm -rf dist",
"compile": "tsc -p tsconfig.json"
"compile": "tsc -p tsconfig.json",
"compile:dev": "tsc -p tsconfig.dev.json"
},
"dependencies": {
"@babel/core": "^7.23.2",
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as url from 'url';

export { parseImageRef } from "./oci_image/utils.js";
export { ImageRef } from "./oci_image/images.js";
export { getProjectLicense, findLicenseFilePath, identifyLicenseViaBackend, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";
export { getProjectLicense, findLicenseFilePath, identifyLicense, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";

Comment thread
ruromero marked this conversation as resolved.
export default { componentAnalysis, stackAnalysis, imageAnalysis, validateToken }

Expand Down
53 changes: 0 additions & 53 deletions src/license/compatibility.js

This file was deleted.

8 changes: 4 additions & 4 deletions src/license/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

import { getProjectLicense, findLicenseFilePath, identifyLicense } from './project_license.js';
import { licensesFromReport, getLicenseDetails } from './licenses_api.js';
import { getCompatibility } from './compatibility.js';
import { getCompatibility } from './license_utils.js';

export { getProjectLicense, findLicenseFilePath, identifyLicense as identifyLicenseViaBackend } from './project_license.js';
export { getProjectLicense, findLicenseFilePath, identifyLicense } from './project_license.js';
export { licensesFromReport, normalizeLicensesResponse, getLicenseDetails } from './licenses_api.js';
export { getCompatibility } from './compatibility.js';
export { getCompatibility } from './license_utils.js';

/**
* Run full license check: resolve project license (with backend identification and details),
Expand All @@ -23,7 +23,7 @@ export { getCompatibility } from './compatibility.js';
*/
export async function runLicenseCheck(sbomContent, manifestPath, url, opts = {}, analysisResult = null) {
// Resolve project license from manifest and LICENSE file
const projectLicense = getProjectLicense(manifestPath, opts);
const projectLicense = getProjectLicense(manifestPath);

// Try backend identification for LICENSE file (more accurate than local pattern matching)
const licenseFilePath = findLicenseFilePath(manifestPath);
Expand Down
127 changes: 127 additions & 0 deletions src/license/license_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* License utilities: file reading, SPDX detection, normalization, compatibility.
* This module has NO dependencies on providers or backend to avoid circular dependencies.
*/

import fs from 'node:fs';
import path from 'node:path';

const LICENSE_FILES = ['LICENSE', 'LICENSE.md', 'LICENSE.txt'];

/**
* Find LICENSE file path in the same directory as the manifest.
* @param {string} manifestPath
* @returns {string|null} - path to LICENSE file or null if not found
*/
export function findLicenseFilePath(manifestPath) {
const manifestDir = path.dirname(path.resolve(manifestPath));

for (const name of LICENSE_FILES) {
const filePath = path.join(manifestDir, name);
try {
if (fs.statSync(filePath).isFile()) {
return filePath;
}
} catch {
// skip
}
}
return null;
}

/**
* Very simple SPDX detection from common license text (first ~500 chars).
* @param {string} text
* @returns {string|null}
*/
export function detectSpdxFromText(text) {
const head = text.slice(0, 500);
if (/Apache License,?\s*Version 2\.0/i.test(head)) { return 'Apache-2.0'; }
if (/MIT License/i.test(head) && /Permission is hereby granted/i.test(head)) { return 'MIT'; }
if (/GNU AFFERO GENERAL PUBLIC LICENSE\s+Version 3/i.test(head)) { return 'AGPL-3.0-only'; }
if (/GNU LESSER GENERAL PUBLIC LICENSE\s+Version 3/i.test(head)) { return 'LGPL-3.0-only'; }
if (/GNU LESSER GENERAL PUBLIC LICENSE\s+Version 2\.1/i.test(head)) { return 'LGPL-2.1-only'; }
if (/GNU GENERAL PUBLIC LICENSE\s+Version 2/i.test(head)) { return 'GPL-2.0-only'; }
if (/GNU GENERAL PUBLIC LICENSE\s+Version 3/i.test(head)) { return 'GPL-3.0-only'; }
if (/BSD 2-Clause/i.test(head)) { return 'BSD-2-Clause'; }
if (/BSD 3-Clause/i.test(head)) { return 'BSD-3-Clause'; }
return null;
}

/**
* Read LICENSE file and detect SPDX identifier.
* @param {string} manifestPath - path to manifest
* @returns {string|null} - SPDX identifier from LICENSE file or null
*/
export function readLicenseFile(manifestPath) {
const licenseFilePath = findLicenseFilePath(manifestPath);
if (!licenseFilePath) { return null; }

try {
const content = fs.readFileSync(licenseFilePath, 'utf-8');
return detectSpdxFromText(content) || content.split('\n')[0]?.trim() || null;
} catch {
return null;
}
Comment thread
ruromero marked this conversation as resolved.
}

/**
* Get project license from manifest or LICENSE file.
* Returns manifestLicense if provided, otherwise tries LICENSE file.
* @param {string|null} manifestLicense - license from manifest (or null)
* @param {string} manifestPath - path to manifest
* @returns {string|null} - SPDX identifier or null
*/
export function getLicense(manifestLicense, manifestPath) {
return manifestLicense || readLicenseFile(manifestPath) || null;
}

/**
* Normalize SPDX identifier for comparison (lowercase, strip common suffixes).
* @param {string} spdxOrName
* @returns {string}
*/
export function normalizeSpdx(spdxOrName) {
const s = String(spdxOrName).trim().toLowerCase();
if (s.endsWith(' license')) { return s.slice(0, -8); }
return s;
}

/**
* Check if a dependency's license is compatible with the project license based on backend categories.
*
* @param {string} [projectCategory] - backend category for project license: PERMISSIVE | WEAK_COPYLEFT | STRONG_COPYLEFT | UNKNOWN
* @param {string} [dependencyCategory] - backend category for dependency license: PERMISSIVE | WEAK_COPYLEFT | STRONG_COPYLEFT | UNKNOWN
* @returns {'compatible'|'incompatible'|'unknown'}
*/
export function getCompatibility(projectCategory, dependencyCategory) {
if (!projectCategory || !dependencyCategory) {
return 'unknown';
}

const proj = projectCategory.toUpperCase();
const dep = dependencyCategory.toUpperCase();

if (proj === 'UNKNOWN' || dep === 'UNKNOWN') {
return 'unknown';
}

const restrictiveness = {
'PERMISSIVE': 1,
'WEAK_COPYLEFT': 2,
'STRONG_COPYLEFT': 3
};

const projLevel = restrictiveness[proj];
const depLevel = restrictiveness[dep];

if (projLevel === undefined || depLevel === undefined) {
return 'unknown';
}

if (depLevel > projLevel) {
return 'incompatible';
}

return 'compatible';
}
Loading
Loading