Compare commits

...
26 Commits
Author SHA1 Message Date
cnagadya e897e8ebdd Add dist folder 2022-10-28 09:25:16 +00:00
cnagadyaandFederico Builes 216fafaed5 PR feedback
Co-authored-by: Federico Builes <[email protected]>
2022-10-28 11:23:05 +02:00
cnagadya 0144419c8e Format violations area 2022-10-27 16:43:45 +00:00
cnagadya 7b16bd0b54 Add unvalidated changes to summary 2022-10-27 16:24:30 +00:00
cnagadya 4525a8c091 Format summary findings 2022-10-27 15:41:19 +00:00
cnagadya 72273c9a36 Update dist folder 2022-10-27 15:22:00 +00:00
cnagadya 562a2f3c0a Improve summary formatting 2022-10-27 15:19:32 +00:00
cnagadya c82c183029 Resolve package-lock conflicts 2022-10-27 14:37:08 +00:00
cnagadya 26be1f407e Merge pull request #309 from actions/codespace-actions-dependency-review-action-p79j7j9pxqrh669p
Add unresolved licenses section
2022-10-27 15:43:28 +02:00
cnagadya 022ea02fbb Add unresolved licenses section 2022-10-27 13:09:37 +00:00
Federico Builes d6e28cdfae Merge pull request #307 from actions/dependabot/npm_and_yarn/types/node-16.18.2
Bump @types/node from 16.18.0 to 16.18.2
2022-10-27 07:34:11 +02:00
dependabot[bot] da3d8af3e3 Bump @types/node from 16.18.0 to 16.18.2
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 16.18.0 to 16.18.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-10-27 01:26:35 +00:00
cnagadya 52fa73c086 Update readme licenses sections 2022-10-26 10:54:12 +00:00
cnagadya 3baea959cf Fix license test failures 2022-10-26 09:58:00 +00:00
cnagadya 782c57b17e Fix config test failures 2022-10-26 09:57:02 +00:00
cnagadya ac5ed8754d Use SPDX license expressions 2022-10-26 09:56:34 +00:00
Federico Builes 024a5a6342 Merge pull request #305 from actions/dependabot/npm_and_yarn/octokit-2.0.10
Bump octokit from 2.0.9 to 2.0.10
2022-10-26 08:49:12 +02:00
Federico Builes b2fc686406 Resolving merge conflicts 2022-10-26 08:47:43 +02:00
dependabot[bot] 4ec1d46392 Bump octokit from 2.0.9 to 2.0.10
Bumps [octokit](https://github.com/octokit/octokit.js) from 2.0.9 to 2.0.10.
- [Release notes](https://github.com/octokit/octokit.js/releases)
- [Commits](https://github.com/octokit/octokit.js/compare/v2.0.9...v2.0.10)

---
updated-dependencies:
- dependency-name: octokit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-10-26 06:46:16 +00:00
Federico Builes cfef8bfe29 Merge pull request #304 from actions/dependabot/npm_and_yarn/octokit/plugin-retry-4.0.3
Bump @octokit/plugin-retry from 3.0.9 to 4.0.3
2022-10-26 08:45:28 +02:00
Federico Builes bd43b8d1e2 updating dist 2022-10-26 08:45:18 +02:00
dependabot[bot] fced408b87 Bump @octokit/plugin-retry from 3.0.9 to 4.0.3
Bumps [@octokit/plugin-retry](https://github.com/octokit/plugin-retry.js) from 3.0.9 to 4.0.3.
- [Release notes](https://github.com/octokit/plugin-retry.js/releases)
- [Commits](https://github.com/octokit/plugin-retry.js/compare/v3.0.9...v4.0.3)

---
updated-dependencies:
- dependency-name: "@octokit/plugin-retry"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-10-26 01:51:57 +00:00
Federico Builes 65f9f50468 Merge pull request #303 from actions/dependabot/npm_and_yarn/typescript-eslint/parser-5.41.0
Bump @typescript-eslint/parser from 5.40.1 to 5.41.0
2022-10-25 07:57:41 +02:00
dependabot[bot] a393c83ce5 Bump @typescript-eslint/parser from 5.40.1 to 5.41.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.40.1 to 5.41.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.41.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-10-25 05:56:55 +00:00
Federico Builes 56163c5659 Merge pull request #302 from actions/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-5.41.0
Bump @typescript-eslint/eslint-plugin from 5.40.1 to 5.41.0
2022-10-25 07:56:10 +02:00
dependabot[bot] 5dc2e6e4bb Bump @typescript-eslint/eslint-plugin from 5.40.1 to 5.41.0
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.40.1 to 5.41.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v5.41.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-10-25 01:44:33 +00:00
14 changed files with 5736 additions and 9811 deletions
+11 -11
View File
@@ -106,19 +106,20 @@ fail-on-scopes:
### allow-licenses ### 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 **Possible values**: A list of of [SPDX-compliant license identifiers](https://spdx.org/licenses/).
https://docs.github.com/en/rest/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 example**:
```yaml ```yaml
allow-licenses: allow-licenses:
- BSD-3-Clause - BSD-3-Clause
- LGPL-2.1
- MIT - MIT
- BSD-3-Clause
``` ```
### deny-licenses ### deny-licenses
@@ -126,17 +127,16 @@ allow-licenses:
Add a custom list of licenses you want to block. See Add a custom list of licenses you want to block. See
"[Licenses](https://github.com/actions/dependency-review-action#licenses)". "[Licenses](https://github.com/actions/dependency-review-action#licenses)".
**Possible values**: Any `spdx_id` value(s) from **Possible values**: Any valid set of [SPDX licenses](https://spdx.org/licenses/).
https://docs.github.com/en/rest/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 example**:
```yaml ```yaml
deny-licenses: deny-licenses:
- LGPL-2.0 - LGPL-2.0
- BSD-2-Clause - GPL-2.0+ WITH Bison-exception-2.2
``` ```
### allow-ghsas ### allow-ghsas
@@ -259,8 +259,8 @@ forbid a subset of licenses. These options are not supported on Enterprise Serve
You can use the [Licenses You can use the [Licenses
API](https://docs.github.com/en/rest/licenses) to see the full list of 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 supported licenses. Use [SPDX licenses](https://spdx.org/licenses/)
to filter. A couple of examples: to filter the licenses. A couple of examples:
```yaml ```yaml
# only allow MIT-licensed dependents # only allow MIT-licensed dependents
@@ -275,7 +275,7 @@ to filter. A couple of examples:
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@v2 uses: actions/dependency-review-action@v2
with: with:
deny-licenses: Apache-1.1, Apache-2.0 deny-licenses: Apache-1.1+
``` ```
### Considerations ### Considerations
+25
View File
@@ -1,6 +1,7 @@
import {expect, test, beforeEach} from '@jest/globals' import {expect, test, beforeEach} from '@jest/globals'
import {readConfig, readConfigFile} from '../src/config' import {readConfig, readConfigFile} from '../src/config'
import {getRefs} from '../src/git-refs' import {getRefs} from '../src/git-refs'
import * as Utils from '../src/utils'
// GitHub Action inputs come in the form of environment variables // GitHub Action inputs come in the form of environment variables
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY) // with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
@@ -27,6 +28,10 @@ function clearInputs() {
}) })
} }
beforeAll(() => {
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(true)
})
beforeEach(() => { beforeEach(() => {
clearInputs() clearInputs()
}) })
@@ -175,3 +180,23 @@ test('it successfully parses GHSA allowlist', async () => {
'GHSA-efgh-1234-5679' 'GHSA-efgh-1234-5679'
]) ])
}) })
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')
})
})
+57 -27
View File
@@ -1,6 +1,7 @@
import {expect, jest, test} from '@jest/globals' import {expect, jest, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas' import {Change, Changes} from '../src/schemas'
import {getDeniedLicenseChanges} from '../src/licenses'
let getInvalidLicenseChanges: Function
let npmChange: Change = { let npmChange: Change = {
manifest: 'package.json', manifest: 'package.json',
@@ -70,65 +71,94 @@ jest.mock('octokit', () => {
} }
}) })
test('it fails if a license outside the allow list is found', async () => { beforeEach(async () => {
const changes: Changes = [npmChange, rubyChange] jest.resetModules()
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { jest.doMock('spdx-satisfies', () => {
allow: ['BSD'] // 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 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'] 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 // 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. // 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] const changes: Changes = [npmChange, rubyChange]
let [invalidChanges, _] = await getDeniedLicenseChanges(changes, { let {forbidden} = await getInvalidLicenseChanges(changes, {
allow: [], allow: [],
deny: ['BSD'] 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 = [ const changes: Changes = [
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD'] 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 = [ const changes: Changes = [
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['BSD'] 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 = [ const changes: Changes = [
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
npmChange, npmChange,
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD'] 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', () => { describe('GH License API fallback', () => {
@@ -138,7 +168,7 @@ describe('GH License API fallback', () => {
license: null, license: null,
source_repository_url: 'http://github.com/some-owner/some-repo' source_repository_url: 'http://github.com/some-owner/some-repo'
} }
const [_, unknownChanges] = await getDeniedLicenseChanges( const {unlicensed} = await getInvalidLicenseChanges(
[nullLicenseChange, rubyChange], [nullLicenseChange, rubyChange],
{} {}
) )
@@ -147,25 +177,25 @@ describe('GH License API fallback', () => {
owner: 'some-owner', owner: 'some-owner',
repo: 'some-repo' 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 () => { 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}], [{...npmChange, license: null}],
{} {}
) )
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled() 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 () => { 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], [npmChange, rubyChange],
{} {}
) )
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled() expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unknownChanges.length).toEqual(0) expect(unlicensed.length).toEqual(0)
}) })
}) })
Generated Vendored
+2381 -6428
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+108
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. 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 <[email protected]> (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 before-after-hook
Apache-2.0 Apache-2.0
Apache License 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. 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 tr46
MIT MIT
+1 -1
View File
@@ -1,6 +1,6 @@
module.exports = { module.exports = {
clearMocks: true, clearMocks: true,
moduleFileExtensions: ['js', 'ts'], moduleFileExtensions: ['js', 'json', 'ts'],
testMatch: ['**/*.test.ts'], testMatch: ['**/*.test.ts'],
transform: { transform: {
'^.+\\.ts$': 'ts-jest' '^.+\\.ts$': 'ts-jest'
+2948 -3247
View File
File diff suppressed because it is too large Load Diff
+9 -5
View File
@@ -27,20 +27,24 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
"@actions/github": "^5.1.1", "@actions/github": "^5.1.1",
"@octokit/plugin-retry": "^3.0.9", "@octokit/plugin-retry": "^4.0.3",
"@octokit/request-error": "^3.0.2", "@octokit/request-error": "^3.0.2",
"ansi-styles": "^6.2.1", "ansi-styles": "^6.2.1",
"got": "^12.5.2", "got": "^12.5.2",
"nodemon": "^2.0.20", "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", "yaml": "^2.1.3",
"zod": "^3.19.1" "zod": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^16.18.0", "@types/node": "^16.18.2",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/parser": "^5.41.0",
"@types/spdx-expression-parse": "^3.0.2",
"@types/spdx-satisfies": "^0.1.0",
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.34.0",
"esbuild-register": "^3.3.3", "esbuild-register": "^3.3.3",
"eslint": "^8.26.0", "eslint": "^8.26.0",
+25 -1
View File
@@ -9,6 +9,9 @@ import {
SeveritySchema, SeveritySchema,
SCOPES SCOPES
} from './schemas' } from './schemas'
import {isSPDXValid} from './utils'
type licenseKey = 'allow-licenses' | 'deny-licenses'
function getOptionalInput(name: string): string | undefined { function getOptionalInput(name: string): string | undefined {
const value = core.getInput(name) const value = core.getInput(name)
@@ -23,6 +26,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 { export function readConfig(): ConfigurationOptions {
const externalConfig = getOptionalInput('config-file') const externalConfig = getOptionalInput('config-file')
if (externalConfig !== undefined) { if (externalConfig !== undefined) {
@@ -53,6 +72,8 @@ export function readInlineConfig(): ConfigurationOptions {
if (allow_licenses !== undefined && deny_licenses !== undefined) { if (allow_licenses !== undefined && deny_licenses !== undefined) {
throw new Error("Can't specify both allow_licenses and deny_licenses") 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 allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
@@ -80,8 +101,11 @@ export function readConfigFile(filePath: string): ConfigurationOptions {
} }
data = YAML.parse(data) data = YAML.parse(data)
// get rid of the ugly dashes from the actions conventions
for (const key of Object.keys(data)) { 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('-')) { if (key.includes('-')) {
data[key.replace(/-/g, '_')] = data[key] data[key.replace(/-/g, '_')] = data[key]
delete data[key] delete data[key]
+91 -24
View File
@@ -1,6 +1,8 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import spdxSatisfies from 'spdx-satisfies'
import {Octokit} from 'octokit' 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 * Loops through a list of changes, filtering and returning the
@@ -12,48 +14,62 @@ import {Change} from './schemas'
* we will ignore the deny list. * we will ignore the deny list.
* @param {Change[]} changes The list of changes to filter. * @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. * @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[], changes: Change[],
licenses: { licenses: {
allow?: string[] allow?: string[]
deny?: string[] deny?: string[]
} }
): Promise<[Change[], Change[]]> { ): Promise<Record<string, Changes>> {
const {allow, deny} = licenses const {allow, deny} = licenses
const disallowed: Change[] = [] const groupedChanges = await groupChanges(changes)
const unknown: Change[] = [] const licensedChanges: Changes = groupedChanges.licensed
const consolidatedChanges = changes.some( const invalidLicenseChanges: Record<string, Changes> = {
({source_repository_url, license}) => !license && source_repository_url unlicensed: groupedChanges.unlicensed,
) unresolved: [],
? await setGHLicenses(changes) forbidden: []
: changes }
for (const change of consolidatedChanges) { const validityCache = new Map<string, boolean>()
if (change.change_type === 'removed') {
continue
}
for (const change of licensedChanges) {
const license = change.license const license = change.license
// should never happen since licensedChanges always have licenses but license is nullable in changes schema
if (license === null) { if (license === null) {
unknown.push(change)
continue continue
} }
if (allow !== undefined) {
if (!allow.includes(license)) { if (license === 'NOASSERTION') {
disallowed.push(change) invalidLicenseChanges.unlicensed.push(change)
} } else if (validityCache.get(license) === undefined) {
} else if (deny !== undefined) { try {
if (deny.includes(license)) { if (allow !== undefined) {
disallowed.push(change) 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 ( const fetchGHLicense = async (
@@ -108,3 +124,54 @@ const setGHLicenses = async (changes: Change[]): Promise<Change[]> => {
return Promise.all(updatedChanges) 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
}
+23 -19
View File
@@ -10,7 +10,7 @@ import {
filterChangesByScopes, filterChangesByScopes,
filterAllowedAdvisories filterAllowedAdvisories
} from '../src/filter' } from '../src/filter'
import {getDeniedLicenseChanges} from './licenses' import {getInvalidLicenseChanges} from './licenses'
import * as summary from './summary' import * as summary from './summary'
import {getRefs} from './git-refs' import {getRefs} from './git-refs'
@@ -45,7 +45,7 @@ async function run(): Promise<void> {
change.vulnerabilities.length > 0 change.vulnerabilities.length > 0
) )
const [licenseErrors, unknownLicenses] = await getDeniedLicenseChanges( const invalidLicenseChanges = await getInvalidLicenseChanges(
filteredChanges, filteredChanges,
{ {
allow: config.allow_licenses, allow: config.allow_licenses,
@@ -53,13 +53,13 @@ async function run(): Promise<void> {
} }
) )
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses) summary.addSummaryToSummary(addedChanges, invalidLicenseChanges)
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity) summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config) summary.addLicensesToSummary(invalidLicenseChanges, config)
summary.addScannedDependencies(changes) summary.addScannedDependencies(changes)
printVulnerabilitiesBlock(addedChanges, minSeverity) printVulnerabilitiesBlock(addedChanges, minSeverity)
printLicensesBlock(licenseErrors, unknownLicenses) printLicensesBlock(invalidLicenseChanges)
printScannedDependencies(changes) printScannedDependencies(changes)
} catch (error) { } catch (error) {
if (error instanceof RequestError && error.status === 404) { if (error instanceof RequestError && error.status === 404) {
@@ -83,7 +83,7 @@ async function run(): Promise<void> {
} }
function printVulnerabilitiesBlock( function printVulnerabilitiesBlock(
addedChanges: Change[], addedChanges: Changes,
minSeverity: Severity minSeverity: Severity
): void { ): void {
let failed = false let failed = false
@@ -119,24 +119,28 @@ function printChangeVulnerabilities(change: Change): void {
} }
function printLicensesBlock( function printLicensesBlock(
licenseErrors: Change[], invalidLicenseChanges: Record<string, Changes>
unknownLicenses: Change[]
): void { ): void {
core.group('Licenses', async () => { core.group('Licenses', async () => {
if (licenseErrors.length > 0) { if (invalidLicenseChanges.forbidden.length > 0) {
printLicensesError(licenseErrors) core.info('\nThe following dependencies have incompatible licenses:')
printLicensesError(invalidLicenseChanges.forbidden)
core.setFailed('Dependency review detected incompatible licenses.') 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 { function printLicensesError(changes: Changes): void {
if (changes.length === 0) {
return
}
core.info('\nThe following dependencies have incompatible licenses:\n')
for (const change of changes) { for (const change of changes) {
core.info( 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}` `${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 +148,12 @@ function printLicensesError(changes: Change[]): void {
} }
} }
function printNullLicenses(changes: Change[]): void { function printNullLicenses(changes: Changes): void {
if (changes.length === 0) { if (changes.length === 0) {
return 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) { for (const change of changes) {
core.info( core.info(
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close}` `${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close}`
+45 -46
View File
@@ -1,18 +1,21 @@
import * as core from '@actions/core' 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 {SummaryTableRow} from '@actions/core/lib/summary'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils' import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
export function addSummaryToSummary( export function addSummaryToSummary(
addedPackages: Changes, addedPackages: Changes,
licenseErrors: Change[], invalidLicenseChanges: Record<string, Changes>
unknownLicenses: Change[]
): void { ): void {
core.summary core.summary
.addHeading('Dependency Review') .addHeading('Dependency Review')
.addRaw( .addRaw('We found:')
`We found ${addedPackages.length} vulnerable package(s), ${licenseErrors.length} package(s) with incompatible licenses, and ${unknownLicenses.length} package(s) with unknown licenses.` .addList([
) `${addedPackages.length} vulnerable package(s)`,
`${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( export function addChangeVulnerabilitiesToSummary(
@@ -76,8 +79,7 @@ export function addChangeVulnerabilitiesToSummary(
} }
export function addLicensesToSummary( export function addLicensesToSummary(
licenseErrors: Change[], invalidLicenseChanges: Record<string, Changes>,
unknownLicenses: Change[],
config: ConfigurationOptions config: ConfigurationOptions
): void { ): void {
core.summary.addHeading('Licenses') core.summary.addHeading('Licenses')
@@ -93,62 +95,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.') core.summary.addQuote('No license violations detected.')
return return
} }
if (licenseErrors.length > 0) { core.debug(
const rows: SummaryTableRow[] = [] `found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
const manifests = getManifestsSet(licenseErrors) )
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) { for (const manifest of manifests) {
core.summary.addHeading(`<em>${manifest}</em>`, 4) core.summary.addHeading(`<em>${manifest}</em>`, 4)
for (const change of licenseErrors.filter( for (const change of changes.filter(pkg => pkg.manifest === manifest)) {
pkg => pkg.manifest === manifest
)) {
rows.push([ rows.push([
renderUrl(change.source_repository_url, change.name), renderUrl(change.source_repository_url, change.name),
change.version, change.version,
change.license || '' formatLicense(change.license)
]) ])
} }
core.summary.addTable([['Package', 'Version', 'License'], ...rows]) core.summary.addTable([['Package', 'Version', 'License'], ...rows])
} }
} else { } else {
core.summary.addQuote('No license violations detected.') core.summary.addQuote(`No ${heading.toLowerCase()} detected.`)
} }
}
core.debug(`found ${unknownLicenses.length} unknown licenses`) function formatLicense(license: string | null): string {
if (license === null || license === 'NOASSERTION') {
if (unknownLicenses.length > 0) { return 'Null'
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])
}
} }
return license
} }
export function addScannedDependencies(changes: Changes): void { export function addScannedDependencies(changes: Changes): void {
@@ -157,7 +156,7 @@ export function addScannedDependencies(changes: Changes): void {
const summary = core.summary const summary = core.summary
.addHeading('Scanned Dependencies') .addHeading('Scanned Dependencies')
.addRaw(`We scanned ${dependencies.size} manifest files:`) .addHeading(`We scanned ${dependencies.size} manifest files:`, 5)
for (const manifest of manifests) { for (const manifest of manifests) {
const deps = dependencies.get(manifest) const deps = dependencies.get(manifest)
@@ -165,7 +164,7 @@ export function addScannedDependencies(changes: Changes): void {
const dependencyNames = deps.map( const dependencyNames = deps.map(
dependency => `<li>${dependency.name}@${dependency.version}</li>` dependency => `<li>${dependency.name}@${dependency.version}</li>`
) )
summary.addRaw(`<h3>${manifest}</h3><ul>${dependencyNames.join('')}</ul>`) summary.addDetails(manifest, `<ul>${dependencyNames.join('')}</ul>`)
} }
} }
} }
+10
View File
@@ -1,3 +1,4 @@
import spdxParse from 'spdx-expression-parse'
import {Changes} from './schemas' import {Changes} from './schemas'
export function groupDependenciesByManifest( export function groupDependenciesByManifest(
@@ -28,3 +29,12 @@ export function renderUrl(url: string | null, text: string): string {
return text return text
} }
} }
export function isSPDXValid(license: string): boolean {
try {
spdxParse(license)
return true
} catch (_) {
return false
}
}