Merge pull request #916 from jebeaudet/spdx-support

Support SPDX expressions with operators in allow/deny license lists
This commit is contained in:
Ashely Tenesaca
2025-04-15 11:33:49 -04:00
committed by GitHub
4 changed files with 138 additions and 6 deletions
+121 -1
View File
@@ -21,7 +21,6 @@ const npmChange: Change = {
} }
] ]
} }
const rubyChange: Change = { const rubyChange: Change = {
change_type: 'added', change_type: 'added',
manifest: 'Gemfile.lock', manifest: 'Gemfile.lock',
@@ -101,6 +100,105 @@ beforeEach(async () => {
jest.resetModules() jest.resetModules()
}) })
test('it should handle SPDX expressions in allow-list that matches a single license project', async () => {
const change: Change = getChangeWithLicense('MIT')
const changes: Changes = [change]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['EPL-1.0 OR MIT']
})
expect(forbidden).toStrictEqual([])
})
test('it should handle SPDX expressions in allow-list with operators and a valid triple licensed project', async () => {
const change: Change = getChangeWithLicense(
'EPL-1.0 AND LGPL-2.1 AND LGPL-2.1-only'
)
const changes: Changes = [change]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['EPL-1.0 AND LGPL-2.1 AND LGPL-2.1-only']
})
expect(forbidden).toStrictEqual([])
})
test('it should handle a valid triple licensed project that does not have a match in the allow-list', async () => {
const change = getChangeWithLicense('EPL-1.0 AND LGPL-2.1 AND LGPL-2.1-only')
const changes: Changes = [change]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['EPL-1.0', 'LGPL-2.1', 'LGPL-2.1-only']
})
expect(forbidden[0]).toBe(change)
expect(forbidden.length).toEqual(1)
})
test('it should handle license with OR SPDX expression and only match on one license in the allow-list', async () => {
const change = getChangeWithLicense('EPL-1.0 OR LGPL-2.1')
const changes: Changes = [change]
for (const allowedLicense of ['EPL-1.0', 'LGPL-2.1']) {
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: [allowedLicense]
})
expect(forbidden).toStrictEqual([])
}
})
test('it should handle SPDX expressions in allow-list with operators when license matches', async () => {
const changes: Changes = [
npmChange // MIT license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['MIT OR Apache-2.0', 'MIT', 'BSD-3-Clause']
})
expect(forbidden).toStrictEqual([])
})
test('it should handle SPDX expressions in allow-list with operators when license does not match', async () => {
const changes: Changes = [
npmChange // MIT license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['MIT AND Apache-2.0', 'BSD-3-Clause']
})
expect(forbidden[0]).toBe(npmChange)
expect(forbidden.length).toEqual(1)
})
test('it should handle SPDX expressions in deny-list with operators when license matches deny list entry', async () => {
const changes: Changes = [
npmChange // MIT license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['MIT OR Apache-2.0', 'BSD-3-Clause']
})
expect(forbidden[0]).toBe(npmChange)
expect(forbidden.length).toEqual(1)
})
test('it should handle SPDX expressions in deny-list with operators when license does not match any deny list entry', async () => {
const changes: Changes = [
npmChange // MIT license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['MIT AND Apache-2.0', 'BSD-3-Clause']
})
expect(forbidden).toStrictEqual([])
})
test('it adds license outside the allow list to forbidden changes', async () => { test('it adds license outside the allow list to forbidden changes', async () => {
const changes: Changes = [ const changes: Changes = [
npmChange, // MIT license npmChange, // MIT license
@@ -264,3 +362,25 @@ describe('GH License API fallback', () => {
expect(unlicensed.length).toEqual(0) expect(unlicensed.length).toEqual(0)
}) })
}) })
function getChangeWithLicense(license: string): Change {
return {
manifest: 'pom.xml',
change_type: 'added',
ecosystem: 'maven',
name: 'dummy-library',
version: '1.0.0',
package_url: 'pkg:org.something:[email protected]',
license,
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'critical',
advisory_ghsa_id: 'first-random_string',
advisory_summary: 'very dangerous',
advisory_url: 'github.com/future-funk'
}
]
}
}
Generated Vendored
+8 -2
View File
@@ -435,7 +435,10 @@ function getInvalidLicenseChanges(changes, licenses) {
try { try {
if (allow !== undefined) { if (allow !== undefined) {
if (spdx.isValid(license)) { if (spdx.isValid(license)) {
const found = spdx.satisfiesAny(license, allow); let found = false;
for (const allowedLicense of allow) {
found || (found = spdx.satisfies(allowedLicense, license));
}
validityCache.set(license, found); validityCache.set(license, found);
} }
else { else {
@@ -444,7 +447,10 @@ function getInvalidLicenseChanges(changes, licenses) {
} }
else if (deny !== undefined) { else if (deny !== undefined) {
if (spdx.isValid(license)) { if (spdx.isValid(license)) {
const found = spdx.satisfiesAny(license, deny); let found = false;
for (const deniedLicense of deny) {
found || (found = spdx.satisfies(deniedLicense, license));
}
validityCache.set(license, !found); validityCache.set(license, !found);
} }
else { else {
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -2
View File
@@ -88,14 +88,20 @@ export async function getInvalidLicenseChanges(
try { try {
if (allow !== undefined) { if (allow !== undefined) {
if (spdx.isValid(license)) { if (spdx.isValid(license)) {
const found = spdx.satisfiesAny(license, allow) let found = false
for (const allowedLicense of allow) {
found ||= spdx.satisfies(allowedLicense, license)
}
validityCache.set(license, found) validityCache.set(license, found)
} else { } else {
invalidLicenseChanges.unresolved.push(change) invalidLicenseChanges.unresolved.push(change)
} }
} else if (deny !== undefined) { } else if (deny !== undefined) {
if (spdx.isValid(license)) { if (spdx.isValid(license)) {
const found = spdx.satisfiesAny(license, deny) let found = false
for (const deniedLicense of deny) {
found ||= spdx.satisfies(deniedLicense, license)
}
validityCache.set(license, !found) validityCache.set(license, !found)
} else { } else {
invalidLicenseChanges.unresolved.push(change) invalidLicenseChanges.unresolved.push(change)