Merge branch 'main' into external-repo-config

This commit is contained in:
Federico Builes
2022-11-01 08:11:26 +01:00
committed by GitHub
20 changed files with 5936 additions and 9848 deletions

View File

@@ -0,0 +1,9 @@
{
"name": "Dependency Review Action",
"image": "mcr.microsoft.com/devcontainers/typescript-node:18",
"postCreateCommand": "npm install",
"remoteUser": "node",
"features": {
"ghcr.io/devcontainers/features/ruby:1": {}
}
}

View File

@@ -23,10 +23,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set Node.js 16.x
- name: Set Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x
- name: Install dependencies
run: npm ci

View File

@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts
@@ -30,7 +30,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: npm
- name: Install dependencies
run: npm ci --ignore-scripts

View File

@@ -38,6 +38,7 @@ _Note_: We don't have any useful tests yet, contributions are welcome!
## Local Development
It is recommended to have atleast [Node 18](https://nodejs.org/en/) installed.
We have a script to scan a given PR for vulnerabilities, this will
help you test your local changes. Make sure to [grab a Personal Access Token (PAT)](https://github.com/settings/tokens) before proceeding (you'll need `repo` permissions for private repos):
@@ -56,11 +57,11 @@ $ GITHUB_TOKEN=my-secret-token ./scripts/scan_pr https://github.com/actions/depe
```
[Configuration options](README.md#configuration-options) can be set by
passing an external YAML [configuration file](README.md#configuration-file) to the
passing an external YAML [configuration file](README.md#configuration-file) to the
`scan_pr` script with the `-c`/`--config-file` option:
```sh
$ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_url>
$ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_url>
```
## Submitting a pull request

View File

@@ -115,19 +115,20 @@ fail-on-scopes:
### allow-licenses
Only allow the licenses in this list. See "[Licenses](https://github.com/actions/dependency-review-action#licenses)".
Only allow the licenses that comply with the expressions in this list. See "[Licenses](https://github.com/actions/dependency-review-action#licenses)".
**Possible values**: Any `spdx_id` value(s) from
https://docs.github.com/en/rest/licenses.
**Possible values**: A list of of [SPDX-compliant license identifiers](https://spdx.org/licenses/).
**Inline example**: `allow-licenses: BSD-3-Clause, MIT`
**Inline example**: `allow-licenses: BSD-3-Clause, LGPL-2.1 OR MIT OR BSD-3-Clause`
**YAML example**:
```yaml
allow-licenses:
- BSD-3-Clause
- LGPL-2.1
- MIT
- BSD-3-Clause
```
### deny-licenses
@@ -135,17 +136,16 @@ allow-licenses:
Add a custom list of licenses you want to block. See
"[Licenses](https://github.com/actions/dependency-review-action#licenses)".
**Possible values**: Any `spdx_id` value(s) from
https://docs.github.com/en/rest/licenses.
**Possible values**: Any valid set of [SPDX licenses](https://spdx.org/licenses/).
**Inline example**: `deny-licenses: LGPL-2.0, BSD-2-Clause`
**Inline example**: `deny-licenses: LGPL-2.0, GPL-2.0+ WITH Bison-exception-2.2`
**YAML example**:
```yaml
deny-licenses:
- LGPL-2.0
- BSD-2-Clause
- GPL-2.0+ WITH Bison-exception-2.2
```
### allow-ghsas
@@ -164,6 +164,20 @@ allow-ghsas:
- GHSA-efgh-1234-5679
```
### license-check/vulnerability-check
Disable the license checks or vulnerability checks performed by this Action.
You can't disable both checks.
**Possible values**: `true` or `false`
**Example**:
```yaml
license-check: true
vulnerability-check: false
```
### base-ref/head-ref
Provide custom git references for the git base/head when performing
@@ -280,8 +294,8 @@ forbid a subset of licenses. These options are not supported on Enterprise Serve
You can use the [Licenses
API](https://docs.github.com/en/rest/licenses) to see the full list of
supported licenses. Use the `spdx_id` field for every license you want
to filter. A couple of examples:
supported licenses. Use [SPDX licenses](https://spdx.org/licenses/)
to filter the licenses. A couple of examples:
```yaml
# only allow MIT-licensed dependents
@@ -296,7 +310,7 @@ to filter. A couple of examples:
- name: Dependency Review
uses: actions/dependency-review-action@v2
with:
deny-licenses: Apache-1.1, Apache-2.0
deny-licenses: Apache-1.1+
```
### Considerations

View File

@@ -1,6 +1,7 @@
import {expect, test, beforeEach} from '@jest/globals'
import {readConfig, readConfigFile} from '../src/config'
import {getRefs} from '../src/git-refs'
import * as Utils from '../src/utils'
// GitHub Action inputs come in the form of environment variables
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
@@ -17,6 +18,8 @@ function clearInputs() {
'ALLOW-LICENSES',
'DENY-LICENSES',
'ALLOW-GHSAS',
'LICENSE-CHECK',
'VULNERABILITY-CHECK',
'CONFIG-FILE',
'BASE-REF',
'HEAD-REF'
@@ -27,6 +30,10 @@ function clearInputs() {
})
}
beforeAll(() => {
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(true)
})
beforeEach(() => {
clearInputs()
})
@@ -175,3 +182,63 @@ test('it successfully parses GHSA allowlist', async () => {
'GHSA-efgh-1234-5679'
])
})
test('it defaults to checking licenses', async () => {
const options = readConfig()
expect(options.license_check).toBe(true)
})
test('it parses the license-check input', async () => {
setInput('license-check', 'false')
let options = readConfig()
expect(options.license_check).toEqual(false)
clearInputs()
setInput('license-check', 'true')
options = readConfig()
expect(options.license_check).toEqual(true)
})
test('it defaults to checking vulnerabilities', async () => {
const options = readConfig()
expect(options.vulnerability_check).toBe(true)
})
test('it parses the vulnerability-check input', async () => {
setInput('vulnerability-check', 'false')
let options = readConfig()
expect(options.vulnerability_check).toEqual(false)
clearInputs()
setInput('vulnerability-check', 'true')
options = readConfig()
expect(options.vulnerability_check).toEqual(true)
})
test('it is not possible to disable both checks', async () => {
setInput('license-check', 'false')
setInput('vulnerability-check', 'false')
expect(() => {
readConfig()
}).toThrow("Can't disable both license-check and vulnerability-check")
})
describe('licenses that are not valid SPDX licenses', () => {
beforeAll(() => {
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(false)
})
test('it raises an error for invalid licenses in allow-licenses', async () => {
setInput('allow-licenses', ' BSD, GPL 2')
expect(() => {
readConfig()
}).toThrow('Invalid license(s) in allow-licenses: BSD, GPL 2')
})
test('it raises an error for invalid licenses in deny-licenses', async () => {
setInput('deny-licenses', ' BSD, GPL 2')
expect(() => {
readConfig()
}).toThrow('Invalid license(s) in deny-licenses: BSD, GPL 2')
})
})

View File

@@ -1,6 +1,7 @@
import {expect, jest, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
import {getDeniedLicenseChanges} from '../src/licenses'
let getInvalidLicenseChanges: Function
let npmChange: Change = {
manifest: 'package.json',
@@ -70,65 +71,94 @@ jest.mock('octokit', () => {
}
})
test('it fails if a license outside the allow list is found', async () => {
const changes: Changes = [npmChange, rubyChange]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
allow: ['BSD']
beforeEach(async () => {
jest.resetModules()
jest.doMock('spdx-satisfies', () => {
// mock spdx-satisfies return value
// true for BSD, false for all others
return jest.fn((license: string, _: string): boolean => license === 'BSD')
})
expect(invalidChanges[0]).toBe(npmChange)
;({getInvalidLicenseChanges} = require('../src/licenses'))
})
test('it fails if a license inside the deny list is found', async () => {
test('it adds license outside the allow list to forbidden changes', async () => {
const changes: Changes = [npmChange, rubyChange]
const [invalidChanges] = await getDeniedLicenseChanges(changes, {
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD']
})
expect(forbidden[0]).toBe(npmChange)
expect(forbidden.length).toEqual(1)
})
test('it adds license inside the deny list to forbidden changes', async () => {
const changes: Changes = [npmChange, rubyChange]
const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['BSD']
})
expect(invalidChanges[0]).toBe(rubyChange)
expect(forbidden[0]).toBe(rubyChange)
expect(forbidden.length).toEqual(1)
})
// This is more of a "here's a behavior that might be surprising" than an actual
// thing we want in the system. Please remove this test after refactoring.
test('it fails all license checks when allow is provided an empty array', async () => {
test('it adds all licenses to forbidden changes when allow is provided an empty array', async () => {
const changes: Changes = [npmChange, rubyChange]
let [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
let {forbidden} = await getInvalidLicenseChanges(changes, {
allow: [],
deny: ['BSD']
})
expect(invalidChanges.length).toBe(2)
expect(forbidden.length).toBe(2)
})
test('it does not fail if a license outside the allow list is found in removed changes', async () => {
test('it does not add license outside the allow list to forbidden changes if it is in removed changes', async () => {
const changes: Changes = [
{...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'}
]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD']
})
expect(invalidChanges).toStrictEqual([])
expect(forbidden).toStrictEqual([])
})
test('it does not fail if a license inside the deny list is found in removed changes', async () => {
test('it does not add license inside the deny list to forbidden changes if it is in removed changes', async () => {
const changes: Changes = [
{...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'}
]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['BSD']
})
expect(invalidChanges).toStrictEqual([])
expect(forbidden).toStrictEqual([])
})
test('it fails if a license outside the allow list is found in both of added and removed changes', async () => {
test('it adds license outside the allow list to forbidden changes if it is in both added and removed changes', async () => {
const changes: Changes = [
{...npmChange, change_type: 'removed'},
npmChange,
{...rubyChange, change_type: 'removed'}
]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD']
})
expect(invalidChanges).toStrictEqual([npmChange])
expect(forbidden).toStrictEqual([npmChange])
})
test('it adds all licenses to unresolved if it is unable to determine the validity', async () => {
jest.resetModules() // reset module set in before
jest.doMock('spdx-satisfies', () => {
return jest.fn((_first: string, _second: string) => {
throw new Error('Some Error')
})
})
;({getInvalidLicenseChanges} = require('../src/licenses'))
const changes: Changes = [npmChange, rubyChange]
const invalidLicenses = await getInvalidLicenseChanges(changes, {
allow: ['BSD']
})
expect(invalidLicenses.forbidden.length).toEqual(0)
expect(invalidLicenses.unlicensed.length).toEqual(0)
expect(invalidLicenses.unresolved.length).toEqual(2)
})
describe('GH License API fallback', () => {
@@ -138,7 +168,7 @@ describe('GH License API fallback', () => {
license: null,
source_repository_url: 'http://github.com/some-owner/some-repo'
}
const [_, unknownChanges] = await getDeniedLicenseChanges(
const {unlicensed} = await getInvalidLicenseChanges(
[nullLicenseChange, rubyChange],
{}
)
@@ -147,25 +177,25 @@ describe('GH License API fallback', () => {
owner: 'some-owner',
repo: 'some-repo'
})
expect(unknownChanges.length).toEqual(0)
expect(unlicensed.length).toEqual(0)
})
test('it does not call licenses API endpoint for change with null license and invalid source_repository_url ', async () => {
const [_, unknownChanges] = await getDeniedLicenseChanges(
const {unlicensed} = await getInvalidLicenseChanges(
[{...npmChange, license: null}],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unknownChanges.length).toEqual(1)
expect(unlicensed.length).toEqual(1)
})
test('it does not call licenses API endpoint if licenses for all changes are present', async () => {
const [_, unknownChanges] = await getDeniedLicenseChanges(
const {unlicensed} = await getInvalidLicenseChanges(
[npmChange, rubyChange],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unknownChanges.length).toEqual(0)
expect(unlicensed.length).toEqual(0)
})
})

8844
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

108
dist/licenses.txt generated vendored
View File

@@ -517,6 +517,31 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
array-find-index
MIT
The MIT License (MIT)
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
before-after-hook
Apache-2.0
Apache License
@@ -1590,6 +1615,89 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
spdx-compare
MIT
The MIT License
Copyright (c) 2015 Kyle E. Mitchell
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
spdx-exceptions
CC-BY-3.0
spdx-expression-parse
MIT
The MIT License
Copyright (c) 2015 Kyle E. Mitchell & other authors listed in AUTHORS
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
spdx-license-ids
CC0-1.0
spdx-ranges
(MIT AND CC-BY-3.0)
The MIT License
Copyright (c) 2015 Kyle E. Mitchell
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
spdx-satisfies
MIT
The MIT License
Copyright (c) spdx-satisfies.js contributors
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
tr46
MIT

View File

@@ -1,9 +1,9 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
moduleFileExtensions: ['js', 'json', 'ts'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
}

6268
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "dependency-review-action",
"version": "2.5.0",
"version": "2.5.1",
"private": true,
"description": "A GitHub Action for Dependency Review",
"main": "lib/main.js",
@@ -27,23 +27,27 @@
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/github": "^5.1.1",
"@octokit/plugin-retry": "^3.0.9",
"@octokit/plugin-retry": "^4.0.3",
"@octokit/request-error": "^3.0.2",
"ansi-styles": "^6.2.1",
"got": "^12.5.2",
"nodemon": "^2.0.20",
"octokit": "^2.0.9",
"octokit": "^2.0.10",
"spdx-expression-parse": "^3.0.1",
"spdx-satisfies": "^5.0.1",
"yaml": "^2.1.3",
"zod": "^3.19.1"
},
"devDependencies": {
"@types/jest": "^27.5.2",
"@types/node": "^16.11.68",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"@types/node": "^16.18.3",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.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.25.0",
"eslint": "^8.26.0",
"eslint-plugin-github": "^4.4.0",
"eslint-plugin-jest": "^27.1.3",
"jest": "^27.5.1",

View File

@@ -23,10 +23,10 @@ op = OptionParser.new do |opts|
Run Dependency Review on a repository.
\e[1mUsage:\e[22m
script/scan_pr [options] <pr_url>
scripts/scan_pr [options] <pr_url>
\e[1mExample:\e[22m
script/scan_pr https://github.com/actions/dependency-review-action/pull/294
scripts/scan_pr https://github.com/actions/dependency-review-action/pull/294
EOF

View File

@@ -9,6 +9,14 @@ import {
SeveritySchema,
SCOPES
} from './schemas'
import {isSPDXValid} from './utils'
type licenseKey = 'allow-licenses' | 'deny-licenses'
function getOptionalBoolean(name: string): boolean | undefined {
const value = core.getInput(name)
return value.length > 0 ? core.getBooleanInput(name) : undefined
}
function getOptionalInput(name: string): string | undefined {
const value = core.getInput(name)
@@ -23,6 +31,22 @@ function parseList(list: string | undefined): string[] | undefined {
}
}
function validateLicenses(
key: licenseKey,
licenses: string[] | undefined
): void {
if (licenses === undefined) {
return
}
const invalid_licenses = licenses.filter(license => !isSPDXValid(license))
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,9 +77,23 @@ 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'))
const license_check = z
.boolean()
.default(true)
.parse(getOptionalBoolean('license-check'))
const vulnerability_check = z
.boolean()
.default(true)
.parse(getOptionalBoolean('vulnerability-check'))
if (license_check === false && vulnerability_check === false) {
throw new Error("Can't disable both license-check and vulnerability-check")
}
const base_ref = getOptionalInput('base-ref')
const head_ref = getOptionalInput('head-ref')
@@ -65,6 +103,8 @@ export function readInlineConfig(): ConfigurationOptions {
allow_licenses,
deny_licenses,
allow_ghsas,
license_check,
vulnerability_check,
base_ref,
head_ref
}
@@ -80,8 +120,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]

View File

@@ -1,6 +1,8 @@
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
@@ -12,48 +14,62 @@ import {Change} from './schemas'
* we will ignore the deny list.
* @param {Change[]} changes The list of changes to filter.
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
* @returns {Promise<[Array.<Change>, Array.<Change>]>} A promise to a 2 element tuple. The first element is the list of denied changes and the second one is the list of changes with unknown licenses
* @returns {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
*/
export async function getDeniedLicenseChanges(
export async function getInvalidLicenseChanges(
changes: Change[],
licenses: {
allow?: string[]
deny?: string[]
}
): Promise<[Change[], Change[]]> {
): Promise<Record<string, Changes>> {
const {allow, deny} = licenses
const disallowed: Change[] = []
const unknown: Change[] = []
const groupedChanges = await groupChanges(changes)
const licensedChanges: Changes = groupedChanges.licensed
const consolidatedChanges = changes.some(
({source_repository_url, license}) => !license && source_repository_url
)
? await setGHLicenses(changes)
: changes
const invalidLicenseChanges: Record<string, Changes> = {
unlicensed: groupedChanges.unlicensed,
unresolved: [],
forbidden: []
}
for (const change of consolidatedChanges) {
if (change.change_type === 'removed') {
continue
}
const validityCache = new Map<string, boolean>()
for (const change of licensedChanges) {
const license = change.license
// should never happen since licensedChanges always have licenses but license is nullable in changes schema
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 (license === 'NOASSERTION') {
invalidLicenseChanges.unlicensed.push(change)
} else if (validityCache.get(license) === undefined) {
try {
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)
}
} catch (err) {
invalidLicenseChanges.unresolved.push(change)
}
}
if (validityCache.get(license) === false) {
invalidLicenseChanges.forbidden.push(change)
}
}
return [disallowed, unknown]
return invalidLicenseChanges
}
const fetchGHLicense = async (
@@ -108,3 +124,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
}

View File

@@ -10,7 +10,7 @@ import {
filterChangesByScopes,
filterAllowedAdvisories
} from '../src/filter'
import {getDeniedLicenseChanges} from './licenses'
import {getInvalidLicenseChanges} from './licenses'
import * as summary from './summary'
import {getRefs} from './git-refs'
@@ -45,7 +45,7 @@ async function run(): Promise<void> {
change.vulnerabilities.length > 0
)
const [licenseErrors, unknownLicenses] = await getDeniedLicenseChanges(
const invalidLicenseChanges = await getInvalidLicenseChanges(
filteredChanges,
{
allow: config.allow_licenses,
@@ -53,13 +53,21 @@ async function run(): Promise<void> {
}
)
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses)
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config)
summary.addScannedDependencies(changes)
summary.addSummaryToSummary(
config.vulnerability_check ? addedChanges : null,
config.license_check ? invalidLicenseChanges : null
)
printVulnerabilitiesBlock(addedChanges, minSeverity)
printLicensesBlock(licenseErrors, unknownLicenses)
if (config.vulnerability_check) {
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
printVulnerabilitiesBlock(addedChanges, minSeverity)
}
if (config.license_check) {
summary.addLicensesToSummary(invalidLicenseChanges, config)
printLicensesBlock(invalidLicenseChanges)
}
summary.addScannedDependencies(changes)
printScannedDependencies(changes)
} catch (error) {
if (error instanceof RequestError && error.status === 404) {
@@ -83,7 +91,7 @@ async function run(): Promise<void> {
}
function printVulnerabilitiesBlock(
addedChanges: Change[],
addedChanges: Changes,
minSeverity: Severity
): void {
let failed = false
@@ -119,24 +127,28 @@ function printChangeVulnerabilities(change: Change): void {
}
function printLicensesBlock(
licenseErrors: Change[],
unknownLicenses: Change[]
invalidLicenseChanges: Record<string, Changes>
): void {
core.group('Licenses', async () => {
if (licenseErrors.length > 0) {
printLicensesError(licenseErrors)
if (invalidLicenseChanges.forbidden.length > 0) {
core.info('\nThe following dependencies have incompatible licenses:')
printLicensesError(invalidLicenseChanges.forbidden)
core.setFailed('Dependency review detected incompatible licenses.')
}
printNullLicenses(unknownLicenses)
if (invalidLicenseChanges.unresolved.length > 0) {
core.warning(
'\nThe validity of the licenses of the dependencies below could not be determined. Ensure that they are valid SPDX licenses:'
)
printLicensesError(invalidLicenseChanges.unresolved)
core.setFailed(
'Dependency review could not detect the validity of all licenses.'
)
}
printNullLicenses(invalidLicenseChanges.unlicensed)
})
}
function printLicensesError(changes: Change[]): void {
if (changes.length === 0) {
return
}
core.info('\nThe following dependencies have incompatible licenses:\n')
function printLicensesError(changes: Changes): void {
for (const change of changes) {
core.info(
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close} License: ${styles.color.red.open}${change.license}${styles.color.red.close}`
@@ -144,12 +156,12 @@ function printLicensesError(changes: Change[]): void {
}
}
function printNullLicenses(changes: Change[]): void {
function printNullLicenses(changes: Changes): void {
if (changes.length === 0) {
return
}
core.info('\nWe could not detect a license for the following dependencies:\n')
core.info('\nWe could not detect a license for the following dependencies:')
for (const change of changes) {
core.info(
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close}`

View File

@@ -41,6 +41,8 @@ export const ConfigurationOptionsSchema = z
allow_licenses: z.array(z.string()).default([]),
deny_licenses: z.array(z.string()).default([]),
allow_ghsas: z.array(z.string()).default([]),
license_check: z.boolean().default(true),
vulnerability_check: z.boolean().default(true),
config_file: z.string().optional().default('false'),
base_ref: z.string(),
head_ref: z.string()

View File

@@ -1,18 +1,27 @@
import * as core from '@actions/core'
import {ConfigurationOptions, Change, Changes} from './schemas'
import {ConfigurationOptions, Changes} from './schemas'
import {SummaryTableRow} from '@actions/core/lib/summary'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
export function addSummaryToSummary(
addedPackages: Changes,
licenseErrors: Change[],
unknownLicenses: Change[]
addedPackages: Changes | null,
invalidLicenseChanges: Record<string, Changes> | null
): void {
core.summary
.addHeading('Dependency Review')
.addRaw(
`We found ${addedPackages.length} vulnerable package(s), ${licenseErrors.length} package(s) with incompatible licenses, and ${unknownLicenses.length} package(s) with unknown licenses.`
)
.addRaw('We found:')
.addList([
...(addedPackages
? [`${addedPackages.length} vulnerable package(s)`]
: []),
...(invalidLicenseChanges
? [
`${invalidLicenseChanges.unresolved.length} package(s) with invalid SPDX license definitions`,
`${invalidLicenseChanges.forbidden.length} package(s) with incompatible licenses`,
`${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.`
]
: [])
])
}
export function addChangeVulnerabilitiesToSummary(
@@ -76,8 +85,7 @@ export function addChangeVulnerabilitiesToSummary(
}
export function addLicensesToSummary(
licenseErrors: Change[],
unknownLicenses: Change[],
invalidLicenseChanges: Record<string, Changes>,
config: ConfigurationOptions
): void {
core.summary.addHeading('Licenses')
@@ -93,62 +101,59 @@ export function addLicensesToSummary(
)
}
if (licenseErrors.length === 0 && unknownLicenses.length === 0) {
if (Object.values(invalidLicenseChanges).every(item => item.length === 0)) {
core.summary.addQuote('No license violations detected.')
return
}
if (licenseErrors.length > 0) {
const rows: SummaryTableRow[] = []
const manifests = getManifestsSet(licenseErrors)
core.debug(
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
)
core.summary.addHeading('Incompatible Licenses', 3).addSeparator()
core.debug(
`${invalidLicenseChanges.unresolved.length} licenses could not be validated`
)
printLicenseViolation(
'Incompatible Licenses',
invalidLicenseChanges.forbidden
)
printLicenseViolation('Unknown Licenses', invalidLicenseChanges.unlicensed)
printLicenseViolation(
'Invalid SPDX License Definitions',
invalidLicenseChanges.unresolved
)
}
function printLicenseViolation(heading: string, changes: Changes): void {
core.summary.addHeading(heading, 5).addSeparator()
if (changes.length > 0) {
const rows: SummaryTableRow[] = []
const manifests = getManifestsSet(changes)
for (const manifest of manifests) {
core.summary.addHeading(`<em>${manifest}</em>`, 4)
for (const change of licenseErrors.filter(
pkg => pkg.manifest === manifest
)) {
for (const change of changes.filter(pkg => pkg.manifest === manifest)) {
rows.push([
renderUrl(change.source_repository_url, change.name),
change.version,
change.license || ''
formatLicense(change.license)
])
}
core.summary.addTable([['Package', 'Version', 'License'], ...rows])
}
} else {
core.summary.addQuote('No license violations detected.')
core.summary.addQuote(`No ${heading.toLowerCase()} detected.`)
}
}
core.debug(`found ${unknownLicenses.length} unknown licenses`)
if (unknownLicenses.length > 0) {
const rows: SummaryTableRow[] = []
const manifests = getManifestsSet(unknownLicenses)
core.debug(
`found ${manifests.entries.length} manifests for unknown licenses`
)
core.summary.addHeading('Unknown Licenses', 3).addSeparator()
for (const manifest of manifests) {
core.summary.addHeading(`<em>${manifest}</em>`, 4)
for (const change of unknownLicenses.filter(
pkg => pkg.manifest === manifest
)) {
rows.push([
renderUrl(change.source_repository_url, change.name),
change.version
])
}
core.summary.addTable([['Package', 'Version'], ...rows])
}
function formatLicense(license: string | null): string {
if (license === null || license === 'NOASSERTION') {
return 'Null'
}
return license
}
export function addScannedDependencies(changes: Changes): void {
@@ -157,7 +162,7 @@ export function addScannedDependencies(changes: Changes): void {
const summary = core.summary
.addHeading('Scanned Dependencies')
.addRaw(`We scanned ${dependencies.size} manifest files:`)
.addHeading(`We scanned ${dependencies.size} manifest files:`, 5)
for (const manifest of manifests) {
const deps = dependencies.get(manifest)
@@ -165,7 +170,7 @@ export function addScannedDependencies(changes: Changes): void {
const dependencyNames = deps.map(
dependency => `<li>${dependency.name}@${dependency.version}</li>`
)
summary.addRaw(`<h3>${manifest}</h3><ul>${dependencyNames.join('')}</ul>`)
summary.addDetails(manifest, `<ul>${dependencyNames.join('')}</ul>`)
}
}
}

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
}
}