Use SPDX license expressions

This commit is contained in:
cnagadya
2022-10-26 09:56:34 +00:00
parent 024a5a6342
commit ac5ed8754d
5 changed files with 2957 additions and 3187 deletions
+2831 -3164
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -33,6 +33,8 @@
"got": "^12.5.2",
"nodemon": "^2.0.20",
"octokit": "^2.0.10",
"spdx-expression-parse": "^3.0.1",
"spdx-satisfies": "^5.0.1",
"yaml": "^2.1.3",
"zod": "^3.19.1"
},
@@ -41,6 +43,8 @@
"@types/node": "^16.18.0",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.41.0",
"@types/spdx-expression-parse": "^3.0.2",
"@types/spdx-satisfies": "^0.1.0",
"@vercel/ncc": "^0.34.0",
"esbuild-register": "^3.3.3",
"eslint": "^8.26.0",
+29 -1
View File
@@ -9,6 +9,9 @@ import {
SeveritySchema,
SCOPES
} from './schemas'
import {isSPDXValid} from './utils'
type licenseKey = 'allow-licenses' | 'deny-licenses'
function getOptionalInput(name: string): string | undefined {
const value = core.getInput(name)
@@ -23,6 +26,26 @@ function parseList(list: string | undefined): string[] | undefined {
}
}
function getInvalidLicenses(licenses: string[]): string[] {
return licenses.filter(license => !isSPDXValid(license))
}
function validateLicenses(
key: licenseKey,
licenses: string[] | undefined
): void {
if (licenses === undefined) {
return
}
const invalid_licenses = getInvalidLicenses(licenses)
if (invalid_licenses.length > 0) {
throw new Error(
`Invalid license(s) in ${key}: ${invalid_licenses.join(', ')}`
)
}
}
export function readConfig(): ConfigurationOptions {
const externalConfig = getOptionalInput('config-file')
if (externalConfig !== undefined) {
@@ -53,6 +76,8 @@ export function readInlineConfig(): ConfigurationOptions {
if (allow_licenses !== undefined && deny_licenses !== undefined) {
throw new Error("Can't specify both allow_licenses and deny_licenses")
}
validateLicenses('allow-licenses', allow_licenses)
validateLicenses('deny-licenses', deny_licenses)
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
@@ -80,8 +105,11 @@ export function readConfigFile(filePath: string): ConfigurationOptions {
}
data = YAML.parse(data)
// get rid of the ugly dashes from the actions conventions
for (const key of Object.keys(data)) {
if (key === 'allow-licenses' || key === 'deny-licenses') {
validateLicenses(key, data[key])
}
// get rid of the ugly dashes from the actions conventions
if (key.includes('-')) {
data[key.replace(/-/g, '_')] = data[key]
delete data[key]
+83 -22
View File
@@ -1,6 +1,9 @@
import * as core from '@actions/core'
import spdxSatisfies from 'spdx-satisfies'
import {Octokit} from 'octokit'
import {Change} from './schemas'
import {Change, Changes} from './schemas'
import {isSPDXValid} from './utils'
/**
* Loops through a list of changes, filtering and returning the
@@ -23,37 +26,44 @@ export async function getDeniedLicenseChanges(
): Promise<[Change[], Change[]]> {
const {allow, deny} = licenses
const disallowed: Change[] = []
const unknown: Change[] = []
const groupedChanges = await groupChanges(changes)
const unlicensedChanges: Changes = groupedChanges.unlicensed
const licensedChanges: Changes = groupedChanges.licensed
const consolidatedChanges = changes.some(
({source_repository_url, license}) => !license && source_repository_url
)
? await setGHLicenses(changes)
: changes
for (const change of consolidatedChanges) {
if (change.change_type === 'removed') {
continue
}
const forbiddenLicenseChanges: Changes = []
const validityCache = new Map<string, boolean>()
for (const change of licensedChanges) {
// should never happen since licensedChanges have licenses. Look into Intersection Types
const license = change.license
if (license === null) {
unknown.push(change)
continue
}
if (allow !== undefined) {
if (!allow.includes(license)) {
disallowed.push(change)
}
} else if (deny !== undefined) {
if (deny.includes(license)) {
disallowed.push(change)
if (validityCache.get(license) === undefined) {
if (allow !== undefined) {
const found = allow.find(spdxExpression =>
spdxSatisfies(license, spdxExpression)
)
validityCache.set(license, found !== undefined)
} else if (deny !== undefined) {
const found = deny.find(spdxExpression =>
spdxSatisfies(license, spdxExpression)
)
validityCache.set(license, found === undefined)
}
}
// TODO: Verify spdxSatisfies is working as expected as currently:
// spdxSatisfies("MIT", "MIT AND (GPL-2.0 OR ISC)") => true
// spdxSatisfies("MIT AND (GPL-2.0 OR ISC)", "MIT") => false
if (validityCache.get(license) === false) {
forbiddenLicenseChanges.push(change)
}
}
return [disallowed, unknown]
return [forbiddenLicenseChanges, unlicensedChanges]
}
const fetchGHLicense = async (
@@ -108,3 +118,54 @@ const setGHLicenses = async (changes: Change[]): Promise<Change[]> => {
return Promise.all(updatedChanges)
}
// Currently Dependency Graph licenses are truncated to 255 characters
// This possibly makes them invalid spdx ids
const truncatedDGLicense = (license: string): boolean =>
license.length === 255 && !isSPDXValid(license)
async function groupChanges(
changes: Changes
): Promise<Record<string, Changes>> {
const result: Record<string, Changes> = {
licensed: [],
unlicensed: []
}
const ghChanges = []
for (const change of changes) {
if (change.change_type === 'removed') {
continue
}
if (change.license === null) {
if (change.source_repository_url !== null) {
ghChanges.push(change)
} else {
result.unlicensed.push(change)
}
} else {
if (
truncatedDGLicense(change.license) &&
change.source_repository_url !== null
) {
ghChanges.push(change)
} else {
result.licensed.push(change)
}
}
}
if (ghChanges.length > 0) {
const ghLicenses = await setGHLicenses(ghChanges)
for (const change of ghLicenses) {
if (change.license === null) {
result.unlicensed.push(change)
} else {
result.licensed.push(change)
}
}
}
return result
}
+10
View File
@@ -1,3 +1,4 @@
import spdxParse from 'spdx-expression-parse'
import {Changes} from './schemas'
export function groupDependenciesByManifest(
@@ -28,3 +29,12 @@ export function renderUrl(url: string | null, text: string): string {
return text
}
}
export function isSPDXValid(license: string): boolean {
try {
spdxParse(license)
return true
} catch (_) {
return false
}
}