diff --git a/README.md b/README.md
index b74c62a3..a01da90b 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,21 @@ let imageAnalysisWithArch = await client.imageAnalysis(['httpd:2.4.49^^amd64'])
```
+
+
License Detection
+
+The client automatically detects your project's license with intelligent fallback:
+
+
+- Manifest-first: For ecosystems with license support (Maven, JavaScript), reads from manifest file (
pom.xml, package.json)
+- LICENSE file fallback: If no license in manifest, or for ecosystems without license support (Gradle, Go, Python), automatically reads from
LICENSE, LICENSE.md, or LICENSE.txt
+- SBOM integration: Detected licenses are included in generated SBOMs for all ecosystems
+- SPDX support: Automatically detects common licenses (Apache-2.0, MIT, GPL, BSD) from LICENSE file content
+
+
+See License Resolution and Compliance for detailed documentation.
+
+
+License Detection
+
+The client automatically detects your project's license with intelligent fallback:
+
+
+- Manifest-first: For ecosystems with license support (Maven, JavaScript), reads from manifest file (
pom.xml, package.json)
+- LICENSE file fallback: If no license in manifest, or for ecosystems without license support (Gradle, Go, Python), automatically reads from
LICENSE, LICENSE.md, or LICENSE.txt
+- SBOM integration: Detected licenses are included in generated SBOMs for all ecosystems
+- SPDX support: Automatically detects common licenses (Apache-2.0, MIT, GPL, BSD) from LICENSE file content
+
+
+See License Resolution and Compliance for detailed documentation.
+
+
+
Excluding Packages
Excluding a package from any analysis can be achieved by marking the package for exclusion.
diff --git a/docs/license-resolution-and-compliance.md b/docs/license-resolution-and-compliance.md
index bfd88acc..479f018c 100644
--- a/docs/license-resolution-and-compliance.md
+++ b/docs/license-resolution-and-compliance.md
@@ -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`: `` 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
@@ -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
diff --git a/package.json b/package.json
index 13e80b15..fcf2ed35 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/index.js b/src/index.js
index 567a475b..436dcc88 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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";
export default { componentAnalysis, stackAnalysis, imageAnalysis, validateToken }
diff --git a/src/license/compatibility.js b/src/license/compatibility.js
deleted file mode 100644
index d8466b6f..00000000
--- a/src/license/compatibility.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * License compatibility: whether a dependency license is compatible with the project license.
- * Relies on backend-provided license categories.
- *
- * Compatibility is based on restrictiveness hierarchy:
- * PERMISSIVE < WEAK_COPYLEFT < STRONG_COPYLEFT
- *
- * A dependency is compatible if it's equal or less restrictive than the project license.
- * A dependency is incompatible if it's more restrictive than the project license.
- */
-
-/**
- * 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();
-
- // Unknown categories
- if (proj === 'UNKNOWN' || dep === 'UNKNOWN') {
- return 'unknown';
- }
-
- // Define restrictiveness levels (higher number = more restrictive)
- 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';
- }
-
- // Dependency is more restrictive than project → incompatible
- if (depLevel > projLevel) {
- return 'incompatible';
- }
-
- // Dependency is equal or less restrictive → compatible
- return 'compatible';
-}
diff --git a/src/license/index.js b/src/license/index.js
index de008967..112ae842 100644
--- a/src/license/index.js
+++ b/src/license/index.js
@@ -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),
@@ -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);
diff --git a/src/license/license_utils.js b/src/license/license_utils.js
new file mode 100644
index 00000000..4e845a00
--- /dev/null
+++ b/src/license/license_utils.js
@@ -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;
+ }
+}
+
+/**
+ * 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';
+}
diff --git a/src/license/project_license.js b/src/license/project_license.js
index ce4b7095..aec3f64f 100644
--- a/src/license/project_license.js
+++ b/src/license/project_license.js
@@ -10,7 +10,10 @@ import { selectTrustifyDABackend } from '../index.js';
import { matchForLicense, availableProviders } from '../provider.js';
import { addProxyAgent, getTokenHeaders } from '../tools.js';
-const LICENSE_FILES = ['LICENSE', 'LICENSE.md', 'LICENSE.txt'];
+import {
+ normalizeSpdx,
+ readLicenseFile
+} from './license_utils.js';
/**
* Resolve project license from manifest and from LICENSE / LICENSE.md in manifest dir or git root.
@@ -23,7 +26,7 @@ export function getProjectLicense(manifestPath) {
const resolved = path.resolve(manifestPath);
const provider = matchForLicense(resolved, availableProviders);
const fromManifest = provider.readLicenseFromManifest(resolved);
- const fromFile = readLicenseFromFile(resolved);
+ const fromFile = readLicenseFile(resolved);
const mismatch = Boolean(
fromManifest && fromFile && normalizeSpdx(fromManifest) !== normalizeSpdx(fromFile)
);
@@ -34,26 +37,7 @@ export function getProjectLicense(manifestPath) {
};
}
-/**
- * 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;
-}
+export { findLicenseFilePath, readLicenseFile } from './license_utils.js';
/**
* Call backend /licenses/identify endpoint to identify license from file.
@@ -65,7 +49,7 @@ export async function identifyLicense(licenseFilePath, opts = {}) {
try {
const fileContent = fs.readFileSync(licenseFilePath);
const backendUrl = selectTrustifyDABackend(opts);
- const url = new URL(`${backendUrl}/licenses/identify`);
+ const url = new URL(`${backendUrl}/api/v5/licenses/identify`);
const tokenHeaders = getTokenHeaders(opts);
const fetchOptions = addProxyAgent({
method: 'POST',
@@ -90,46 +74,13 @@ export async function identifyLicense(licenseFilePath, opts = {}) {
}
/**
- * Find and read LICENSE or LICENSE.md; use local pattern matching for identification.
- * @param {string} manifestPath
- * @returns {string|null}
- */
-function readLicenseFromFile(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;
- }
-}
-
-/**
- * Very simple SPDX detection from common license text (first ~500 chars).
- * @param {string} text
- * @returns {string|null}
- */
-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 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;
-}
-
-/**
- * Normalize for comparison (lowercase, strip common suffixes).
- * @param {string} spdxOrName
- * @returns {string}
+ * Get license for SBOM root component with fallback to LICENSE file.
+ * Priority: manifest license > LICENSE file > null
+ * This is used by providers to populate the license field in the SBOM.
+ * @param {string} manifestPath - path to manifest
+ * @returns {string|null} - SPDX identifier or null
*/
-function normalizeSpdx(spdxOrName) {
- const s = String(spdxOrName).trim().toLowerCase();
- // e.g. "MIT" vs "MIT License"
- if (s.endsWith(' license')) {return s.slice(0, -8);}
- return s;
+export function getLicenseForSbom(manifestPath) {
+ const { fromManifest, fromFile } = getProjectLicense(manifestPath);
+ return fromManifest || fromFile || null;
}
diff --git a/src/providers/base_javascript.js b/src/providers/base_javascript.js
index 236ca4c7..2d863539 100644
--- a/src/providers/base_javascript.js
+++ b/src/providers/base_javascript.js
@@ -2,6 +2,7 @@ import fs from 'node:fs'
import os from "node:os";
import path from 'node:path'
+import { getLicense } from '../license/license_utils.js'
import Sbom from '../sbom.js'
import { getCustom, getCustomPath, invokeCommand, toPurl, toPurlFromString } from "../tools.js";
@@ -156,20 +157,20 @@ export default class Base_javascript {
* @returns {string|null}
*/
readLicenseFromManifest(manifestPath) {
+ let manifestLicense;
try {
const content = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
if (typeof content.license === 'string') {
- return content.license.trim() || null;
- }
- if (Array.isArray(content.licenses) && content.licenses.length > 0) {
+ manifestLicense = content.license.trim() || null;
+ } else if (Array.isArray(content.licenses) && content.licenses.length > 0) {
const first = content.licenses[0];
const name = first.type || first.name;
- return typeof name === 'string' ? name.trim() : null;
+ manifestLicense = (typeof name === 'string' ? name.trim() : null);
}
- return null;
} catch {
- return null;
+ manifestLicense = null;
}
+ return getLicense(manifestLicense, manifestPath);
}
/**
diff --git a/src/providers/golang_gomodules.js b/src/providers/golang_gomodules.js
index 8b5efff5..f741e8d3 100644
--- a/src/providers/golang_gomodules.js
+++ b/src/providers/golang_gomodules.js
@@ -4,6 +4,7 @@ import { EOL } from "os";
import { PackageURL } from 'packageurl-js'
+import { readLicenseFile } from '../license/license_utils.js'
import Sbom from '../sbom.js'
import { getCustom, getCustomPath, invokeCommand } from "../tools.js";
@@ -40,7 +41,7 @@ function isSupported(manifestName) {
* @returns {string|null}
*/
// eslint-disable-next-line no-unused-vars
-function readLicenseFromManifest(manifestPath) { return null }
+function readLicenseFromManifest(manifestPath) { return readLicenseFile(manifestPath); }
/**
* @param {string} manifestDir - the directory where the manifest lies
diff --git a/src/providers/java_gradle.js b/src/providers/java_gradle.js
index 6481a154..f7095dac 100644
--- a/src/providers/java_gradle.js
+++ b/src/providers/java_gradle.js
@@ -4,6 +4,7 @@ import { EOL } from 'os'
import TOML from 'fast-toml'
+import { readLicenseFile } from '../license/license_utils.js'
import Sbom from '../sbom.js'
import Base_java, { ecosystem_gradle } from "./base_java.js";
@@ -75,7 +76,7 @@ export default class Java_gradle extends Base_java {
* @returns {null}
*/
// eslint-disable-next-line no-unused-vars
- readLicenseFromManifest(manifestPath) { return null; }
+ readLicenseFromManifest(manifestPath) { return readLicenseFile(manifestPath); }
/**
* Provide content and content type for stack analysis.
diff --git a/src/providers/java_maven.js b/src/providers/java_maven.js
index f87127e1..0e827172 100644
--- a/src/providers/java_maven.js
+++ b/src/providers/java_maven.js
@@ -5,6 +5,7 @@ import { EOL } from 'os'
import { XMLParser } from 'fast-xml-parser'
+import { getLicense } from '../license/license_utils.js'
import Sbom from '../sbom.js'
import { getCustom } from '../tools.js'
@@ -66,27 +67,27 @@ export default class Java_maven extends Base_java {
}
/**
- * Read license from pom.xml manifest
+ * Read license from pom.xml manifest, with fallback to LICENSE file
* @param {string} manifestPath - path to pom.xml
* @returns {string|null}
*/
readLicenseFromManifest(manifestPath) {
+ let fromPom = null;
try {
const xml = fs.readFileSync(manifestPath, 'utf-8');
const parser = new XMLParser({ ignoreAttributes: false });
const obj = parser.parse(xml);
const project = obj?.project;
- if (!project?.licenses?.license) {
- return null;
+ if (project?.licenses?.license) {
+ const license = Array.isArray(project.licenses.license)
+ ? project.licenses.license[0]
+ : project.licenses.license;
+ fromPom = (license?.name && license.name.trim()) || null;
}
- const license = Array.isArray(project.licenses.license)
- ? project.licenses.license[0]
- : project.licenses.license;
- const name = (license?.name && license.name.trim()) || null;
- return name || null;
} catch {
- return null;
+ // leave fromPom as null
}
+ return getLicense(fromPom, manifestPath);
}
/**
diff --git a/src/providers/python_pip.js b/src/providers/python_pip.js
index 3fea30e1..139b4544 100644
--- a/src/providers/python_pip.js
+++ b/src/providers/python_pip.js
@@ -2,6 +2,7 @@ import fs from 'node:fs'
import { PackageURL } from 'packageurl-js'
+import { readLicenseFile } from '../license/license_utils.js'
import Sbom from '../sbom.js'
import {
environmentVariableIsPopulated,
@@ -37,7 +38,7 @@ function isSupported(manifestName) {
* @returns {string|null}
*/
// eslint-disable-next-line no-unused-vars
-function readLicenseFromManifest(manifestPath) { return null }
+function readLicenseFromManifest(manifestPath) { return readLicenseFile(manifestPath); }
/**
* @param {string} manifestDir - the directory where the manifest lies
diff --git a/test/providers/license_fallback.test.js b/test/providers/license_fallback.test.js
new file mode 100644
index 00000000..9d4060a0
--- /dev/null
+++ b/test/providers/license_fallback.test.js
@@ -0,0 +1,70 @@
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+import { expect } from 'chai';
+
+import golangGomodulesProvider from '../../src/providers/golang_gomodules.js';
+import Java_gradle_groovy from '../../src/providers/java_gradle_groovy.js';
+import pythonPipProvider from '../../src/providers/python_pip.js';
+
+// Test LICENSE file fallback feature
+suite('LICENSE file fallback for providers without manifest license support', () => {
+ let tempDir;
+
+ setup(() => {
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'license-test-'));
+ });
+
+ teardown(() => {
+ if (tempDir && fs.existsSync(tempDir)) {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ }
+ });
+
+ test('Gradle provider should read LICENSE file when present', () => {
+ const buildGradle = path.join(tempDir, 'build.gradle');
+ const licenseFile = path.join(tempDir, 'LICENSE');
+
+ fs.writeFileSync(buildGradle, 'plugins { id "java" }');
+ fs.writeFileSync(licenseFile, 'Apache License, Version 2.0');
+
+ const provider = new Java_gradle_groovy();
+ const license = provider.readLicenseFromManifest(buildGradle);
+
+ expect(license).to.equal('Apache-2.0');
+ });
+
+ test('Golang provider should read LICENSE file when present', () => {
+ const goMod = path.join(tempDir, 'go.mod');
+ const licenseFile = path.join(tempDir, 'LICENSE');
+
+ fs.writeFileSync(goMod, 'module example.com/test');
+ fs.writeFileSync(licenseFile, 'MIT License\n\nPermission is hereby granted');
+
+ const license = golangGomodulesProvider.readLicenseFromManifest(goMod);
+
+ expect(license).to.equal('MIT');
+ });
+
+ test('Python provider should read LICENSE file when present', () => {
+ const requirements = path.join(tempDir, 'requirements.txt');
+ const licenseFile = path.join(tempDir, 'LICENSE');
+
+ fs.writeFileSync(requirements, 'requests==2.28.0');
+ fs.writeFileSync(licenseFile, 'BSD 3-Clause License');
+
+ const license = pythonPipProvider.readLicenseFromManifest(requirements);
+
+ expect(license).to.equal('BSD-3-Clause');
+ });
+
+ test('Providers should return null when no LICENSE file exists', () => {
+ const goMod = path.join(tempDir, 'go.mod');
+ fs.writeFileSync(goMod, 'module example.com/test');
+
+ const license = golangGomodulesProvider.readLicenseFromManifest(goMod);
+
+ expect(license).to.be.null;
+ });
+});
diff --git a/tsconfig.dev.json b/tsconfig.dev.json
new file mode 100644
index 00000000..738679d7
--- /dev/null
+++ b/tsconfig.dev.json
@@ -0,0 +1,6 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "sourceMap": true
+ }
+}