Compare commits
121
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
123b58703a | ||
|
|
cd559bc984 | ||
|
|
70f8094bec | ||
|
|
0b306aef97 | ||
|
|
554aaf5c3d | ||
|
|
c6e94c1336 | ||
|
|
88d6af3d4a | ||
|
|
f1c8401a59 | ||
|
|
ef8ebf0eef | ||
|
|
1f7c838fcb | ||
|
|
1ee07d8652 | ||
|
|
861f696c44 | ||
|
|
ce9db3928f | ||
|
|
854aa8a142 | ||
|
|
9fbf14f620 | ||
|
|
64222d2efe | ||
|
|
f2a3e1af33 | ||
|
|
e3de7a00a8 | ||
|
|
627344199b | ||
|
|
2406ed1539 | ||
|
|
20f1bbadfc | ||
|
|
32e5b32ec4 | ||
|
|
4ee0db82cc | ||
|
|
f303e9cd65 | ||
|
|
fa8ddf1781 | ||
|
|
70422dcfbd | ||
|
|
fe724aebb5 | ||
|
|
6ab307aa49 | ||
|
|
7b02d77054 | ||
|
|
98717099a1 | ||
|
|
b582a7ae96 | ||
|
|
894a896fb1 | ||
|
|
eb565747bb | ||
|
|
7b5fa84cfc | ||
|
|
8ef2903f61 | ||
|
|
16c0c13a8b | ||
|
|
b36110c8a0 | ||
|
|
0574926a14 | ||
|
|
57c07f037a | ||
|
|
8fba746b74 | ||
|
|
632eabaaf6 | ||
|
|
d1f8348e2e | ||
|
|
66da8857a8 | ||
|
|
9fe22cbd4d | ||
|
|
192b846247 | ||
|
|
faed3d989f | ||
|
|
7d25be7d68 | ||
|
|
57e6a1aeb8 | ||
|
|
8450611ed5 | ||
|
|
adc7610fb4 | ||
|
|
b81c2dfce6 | ||
|
|
70370c1727 | ||
|
|
1e46123a48 | ||
|
|
de626ab5bc | ||
|
|
5907e06ae4 | ||
|
|
9bc0593cb7 | ||
|
|
7070612acc | ||
|
|
f6e0fab375 | ||
|
|
51fa253565 | ||
|
|
2dffe8e22c | ||
|
|
e263d60b8b | ||
|
|
bf512683a2 | ||
|
|
6c9f94c4e5 | ||
|
|
8321ca9367 | ||
|
|
00ef46c947 | ||
|
|
b206cbf92e | ||
|
|
d482d746c3 | ||
|
|
041e4f1437 | ||
|
|
c883e5a202 | ||
|
|
8938bd9ef0 | ||
|
|
35a369d1cd | ||
|
|
0a9f43e15a | ||
|
|
e0ec35dfb0 | ||
|
|
73625ad716 | ||
|
|
654eb5ca1c | ||
|
|
9885d0c74c | ||
|
|
cebb5b1214 | ||
|
|
50b918791f | ||
|
|
3f6a17c81c | ||
|
|
2c065db296 | ||
|
|
ff46a4b16e | ||
|
|
153f274eb4 | ||
|
|
0041d7fa41 | ||
|
|
1896d6f936 | ||
|
|
39dca1ce09 | ||
|
|
d3fdbc93c5 | ||
|
|
9ad7edb033 | ||
|
|
97c9465751 | ||
|
|
8b0d4b3327 | ||
|
|
8c24360582 | ||
|
|
80be5a7079 | ||
|
|
35bd59fb9e | ||
|
|
b7ce9d546d | ||
|
|
5875c70f8f | ||
|
|
43274f6899 | ||
|
|
81d482fe7f | ||
|
|
420f61c64a | ||
|
|
866b422c9e | ||
|
|
76b8e83d1a | ||
|
|
91eae64e0c | ||
|
|
f5f2eae995 | ||
|
|
355bcf860e | ||
|
|
5726b20f6c | ||
|
|
4d05b525ee | ||
|
|
81ee3a8dc8 | ||
|
|
3b871daeea | ||
|
|
3f5b40d019 | ||
|
|
89b3ba9416 | ||
|
|
a44d7c538d | ||
|
|
7e1f7be1f6 | ||
|
|
0c01e947d6 | ||
|
|
782549c724 | ||
|
|
419396de41 | ||
|
|
e17845d155 | ||
|
|
f0bda66bbf | ||
|
|
1d7d9a2c93 | ||
|
|
d5c2f70a7f | ||
|
|
f92376010c | ||
|
|
c2303c3070 | ||
|
|
884b7abd2d | ||
|
|
600458c5dd |
@@ -66,19 +66,20 @@ jobs:
|
|||||||
|
|
||||||
Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional.
|
Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional.
|
||||||
|
|
||||||
| Option | Usage | Possible values | Default value |
|
| Option | Usage | Possible values | Default value |
|
||||||
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- |
|
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- |
|
||||||
| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` |
|
| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` |
|
||||||
| `allow-licenses`* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
| `allow-licenses`\* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||||
| `deny-licenses`* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
| `deny-licenses`\* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||||
| `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. | `runtime`, `development`, `unknown` | `runtime` |
|
| `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. | `runtime`, `development`, `unknown` | `runtime` |
|
||||||
| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none |
|
| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none |
|
||||||
| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` |
|
| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` |
|
||||||
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
|
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
|
||||||
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
|
| `allow-dependencies-licenses`\* | Contains a list of packages that will be excluded from license checks. | Any package(s) in [purl](https://github.com/package-url/purl-spec) format | none |
|
||||||
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `true`, `false` | `false` |
|
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
|
||||||
|
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `true`, `false` | `false` |
|
||||||
|
|
||||||
*not supported for use with GitHub Enterprise Server
|
\*not supported for use with GitHub Enterprise Server
|
||||||
|
|
||||||
†will be supported with GitHub Enterprise Server 3.8
|
†will be supported with GitHub Enterprise Server 3.8
|
||||||
|
|
||||||
@@ -128,16 +129,19 @@ Start by specifying that you will be using an external configuration file:
|
|||||||
config-file: './.github/dependency-review-config.yml'
|
config-file: './.github/dependency-review-config.yml'
|
||||||
```
|
```
|
||||||
|
|
||||||
And then create the file in the path you just specified:
|
And then create the file in the path you just specified. Please note
|
||||||
|
that the **option names in external files use underscores instead of dashes**:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
fail-on-severity: 'critical'
|
fail_on_severity: 'critical'
|
||||||
allow-licenses:
|
allow_licenses:
|
||||||
- 'GPL-3.0'
|
- 'GPL-3.0'
|
||||||
- 'BSD-3-Clause'
|
- 'BSD-3-Clause'
|
||||||
- 'MIT'
|
- 'MIT'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For more examples of how to use this action and its configuration options, see the [examples](docs/examples.md) page.
|
||||||
|
|
||||||
### Considerations
|
### Considerations
|
||||||
|
|
||||||
- Checking for licenses is not supported on Enterprise Server.
|
- Checking for licenses is not supported on Enterprise Server.
|
||||||
|
|||||||
@@ -2,35 +2,7 @@ import {expect, test, beforeEach} from '@jest/globals'
|
|||||||
import {readConfig} from '../src/config'
|
import {readConfig} from '../src/config'
|
||||||
import {getRefs} from '../src/git-refs'
|
import {getRefs} from '../src/git-refs'
|
||||||
import * as Utils from '../src/utils'
|
import * as Utils from '../src/utils'
|
||||||
|
import {setInput, clearInputs} from './test-helpers'
|
||||||
// GitHub Action inputs come in the form of environment variables
|
|
||||||
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
|
|
||||||
function setInput(input: string, value: string): void {
|
|
||||||
process.env[`INPUT_${input.toUpperCase()}`] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want a clean ENV before each test. We use `delete`
|
|
||||||
// since we want `undefined` values and not empty strings.
|
|
||||||
function clearInputs(): void {
|
|
||||||
const allowedOptions = [
|
|
||||||
'FAIL-ON-SEVERITY',
|
|
||||||
'FAIL-ON-SCOPES',
|
|
||||||
'ALLOW-LICENSES',
|
|
||||||
'DENY-LICENSES',
|
|
||||||
'ALLOW-GHSAS',
|
|
||||||
'LICENSE-CHECK',
|
|
||||||
'VULNERABILITY-CHECK',
|
|
||||||
'CONFIG-FILE',
|
|
||||||
'BASE-REF',
|
|
||||||
'HEAD-REF',
|
|
||||||
'COMMENT-SUMMARY-IN-PR'
|
|
||||||
]
|
|
||||||
|
|
||||||
// eslint-disable-next-line github/array-foreach
|
|
||||||
allowedOptions.forEach(option => {
|
|
||||||
delete process.env[`INPUT_${option.toUpperCase()}`]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(true)
|
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(true)
|
||||||
@@ -105,60 +77,6 @@ test('it raises an error when no refs are provided and the event is not a pull r
|
|||||||
).toThrow()
|
).toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it reads an external config file', async () => {
|
|
||||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
|
||||||
|
|
||||||
const config = await readConfig()
|
|
||||||
expect(config.fail_on_severity).toEqual('critical')
|
|
||||||
expect(config.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('raises an error when the config file was not found', async () => {
|
|
||||||
setInput('config-file', 'fixtures/i-dont-exist')
|
|
||||||
await expect(readConfig()).rejects.toThrow(/Unable to fetch/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it parses options from both sources', async () => {
|
|
||||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
|
||||||
|
|
||||||
let config = await readConfig()
|
|
||||||
expect(config.fail_on_severity).toEqual('critical')
|
|
||||||
|
|
||||||
setInput('base-ref', 'a-custom-base-ref')
|
|
||||||
config = await readConfig()
|
|
||||||
expect(config.base_ref).toEqual('a-custom-base-ref')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('in case of conflicts, the inline config is the source of truth', async () => {
|
|
||||||
setInput('fail-on-severity', 'low')
|
|
||||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml') // this will set fail-on-severity to 'critical'
|
|
||||||
|
|
||||||
const config = await readConfig()
|
|
||||||
expect(config.fail_on_severity).toEqual('low')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it uses the default values when loading external files', async () => {
|
|
||||||
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
|
||||||
let config = await readConfig()
|
|
||||||
expect(config.allow_licenses).toEqual(undefined)
|
|
||||||
expect(config.deny_licenses).toEqual(undefined)
|
|
||||||
|
|
||||||
setInput('config-file', './__tests__/fixtures/license-config-sample.yml')
|
|
||||||
config = await readConfig()
|
|
||||||
expect(config.fail_on_severity).toEqual('low')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it accepts an external configuration filename', async () => {
|
|
||||||
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
|
||||||
const config = await readConfig()
|
|
||||||
expect(config.fail_on_severity).toEqual('critical')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it raises an error when given an unknown severity in an external config file', async () => {
|
|
||||||
setInput('config-file', './__tests__/fixtures/invalid-severity-config.yml')
|
|
||||||
await expect(readConfig()).rejects.toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it defaults to runtime scope', async () => {
|
test('it defaults to runtime scope', async () => {
|
||||||
const config = await readConfig()
|
const config = await readConfig()
|
||||||
expect(config.fail_on_scopes).toEqual(['runtime'])
|
expect(config.fail_on_scopes).toEqual(['runtime'])
|
||||||
@@ -234,16 +152,6 @@ test('it is not possible to disable both checks', async () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it supports comma-separated lists', async () => {
|
|
||||||
setInput(
|
|
||||||
'config-file',
|
|
||||||
'./__tests__/fixtures/inline-license-config-sample.yml'
|
|
||||||
)
|
|
||||||
const config = await readConfig()
|
|
||||||
|
|
||||||
expect(config.allow_licenses).toEqual(['MIT', 'GPL-2.0-only'])
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('licenses that are not valid SPDX licenses', () => {
|
describe('licenses that are not valid SPDX licenses', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(false)
|
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(false)
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import {expect, test, beforeEach} from '@jest/globals'
|
||||||
|
import {readConfig} from '../src/config'
|
||||||
|
import * as Utils from '../src/utils'
|
||||||
|
import {setInput, clearInputs} from './test-helpers'
|
||||||
|
|
||||||
|
const externalConfig = `fail_on_severity: 'high'
|
||||||
|
allow_licenses: ['GPL-2.0-only']
|
||||||
|
`
|
||||||
|
const mockOctokit = {
|
||||||
|
rest: {
|
||||||
|
repos: {
|
||||||
|
getContent: jest.fn().mockReturnValue({data: externalConfig})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.mock('octokit', () => {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||||
|
Octokit: class {
|
||||||
|
constructor() {
|
||||||
|
return mockOctokit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clearInputs()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it reads an external config file', async () => {
|
||||||
|
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
||||||
|
|
||||||
|
const config = await readConfig()
|
||||||
|
expect(config.fail_on_severity).toEqual('critical')
|
||||||
|
expect(config.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('raises an error when the config file was not found', async () => {
|
||||||
|
setInput('config-file', 'fixtures/i-dont-exist')
|
||||||
|
await expect(readConfig()).rejects.toThrow(/Unable to fetch/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it parses options from both sources', async () => {
|
||||||
|
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
||||||
|
|
||||||
|
let config = await readConfig()
|
||||||
|
expect(config.fail_on_severity).toEqual('critical')
|
||||||
|
|
||||||
|
setInput('base-ref', 'a-custom-base-ref')
|
||||||
|
config = await readConfig()
|
||||||
|
expect(config.base_ref).toEqual('a-custom-base-ref')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('in case of conflicts, the inline config is the source of truth', async () => {
|
||||||
|
setInput('fail-on-severity', 'low')
|
||||||
|
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml') // this will set fail-on-severity to 'critical'
|
||||||
|
|
||||||
|
const config = await readConfig()
|
||||||
|
expect(config.fail_on_severity).toEqual('low')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it uses the default values when loading external files', async () => {
|
||||||
|
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
||||||
|
let config = await readConfig()
|
||||||
|
expect(config.allow_licenses).toEqual(undefined)
|
||||||
|
expect(config.deny_licenses).toEqual(undefined)
|
||||||
|
|
||||||
|
setInput('config-file', './__tests__/fixtures/license-config-sample.yml')
|
||||||
|
config = await readConfig()
|
||||||
|
expect(config.fail_on_severity).toEqual('low')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it accepts an external configuration filename', async () => {
|
||||||
|
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
||||||
|
const config = await readConfig()
|
||||||
|
expect(config.fail_on_severity).toEqual('critical')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it raises an error when given an unknown severity in an external config file', async () => {
|
||||||
|
setInput('config-file', './__tests__/fixtures/invalid-severity-config.yml')
|
||||||
|
await expect(readConfig()).rejects.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it supports comma-separated lists', async () => {
|
||||||
|
setInput(
|
||||||
|
'config-file',
|
||||||
|
'./__tests__/fixtures/inline-license-config-sample.yml'
|
||||||
|
)
|
||||||
|
const config = await readConfig()
|
||||||
|
|
||||||
|
expect(config.allow_licenses).toEqual(['MIT', 'GPL-2.0-only'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it reads a config file hosted in another repo', async () => {
|
||||||
|
setInput(
|
||||||
|
'config-file',
|
||||||
|
'future-funk/anyone-cualkiera/external-config.yml@main'
|
||||||
|
)
|
||||||
|
setInput('external-repo-token', 'gh_viptoken')
|
||||||
|
|
||||||
|
const config = await readConfig()
|
||||||
|
|
||||||
|
expect(config.fail_on_severity).toEqual('high')
|
||||||
|
expect(config.allow_licenses).toEqual(['GPL-2.0-only'])
|
||||||
|
})
|
||||||
@@ -1 +1 @@
|
|||||||
allow-licenses: MIT, GPL-2.0-only
|
allow-licenses: "MIT, GPL-2.0-only"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
fail-on-severity: 'so many zombies'
|
fail_on_severity: 'so many zombies'
|
||||||
deny-licenses:
|
deny_licenses:
|
||||||
- MIT
|
- MIT
|
||||||
|
|||||||
@@ -49,6 +49,32 @@ const rubyChange: Change = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pipChange: Change = {
|
||||||
|
change_type: 'added',
|
||||||
|
manifest: 'requirements.txt',
|
||||||
|
ecosystem: 'pip',
|
||||||
|
name: 'package-1',
|
||||||
|
version: '1.1.1',
|
||||||
|
package_url: 'pkg:pip/[email protected]',
|
||||||
|
license: 'MIT',
|
||||||
|
source_repository_url: 'github.com/some-repo',
|
||||||
|
scope: 'runtime',
|
||||||
|
vulnerabilities: [
|
||||||
|
{
|
||||||
|
severity: 'moderate',
|
||||||
|
advisory_ghsa_id: 'second-random_string',
|
||||||
|
advisory_summary: 'not so dangerous',
|
||||||
|
advisory_url: 'github.com/future-funk'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
severity: 'low',
|
||||||
|
advisory_ghsa_id: 'third-random_string',
|
||||||
|
advisory_summary: 'dont page me',
|
||||||
|
advisory_url: 'github.com/future-funk'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
jest.mock('@actions/core')
|
jest.mock('@actions/core')
|
||||||
|
|
||||||
const mockOctokit = {
|
const mockOctokit = {
|
||||||
@@ -153,6 +179,51 @@ test('it adds all licenses to unresolved if it is unable to determine the validi
|
|||||||
expect(invalidLicenses.unresolved.length).toEqual(2)
|
expect(invalidLicenses.unresolved.length).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('it does not filter out changes that are on the exclusions list', async () => {
|
||||||
|
const changes: Changes = [pipChange, npmChange, rubyChange]
|
||||||
|
const licensesConfig = {
|
||||||
|
allow: ['BSD'],
|
||||||
|
licenseExclusions: ['pkg:pip/[email protected]', 'pkg:npm/[email protected]']
|
||||||
|
}
|
||||||
|
const invalidLicenses = await getInvalidLicenseChanges(
|
||||||
|
changes,
|
||||||
|
licensesConfig
|
||||||
|
)
|
||||||
|
expect(invalidLicenses.forbidden.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it does not fail when the packages dont have a valid PURL', async () => {
|
||||||
|
const emptyPurlChange = pipChange
|
||||||
|
emptyPurlChange.package_url = ''
|
||||||
|
|
||||||
|
const changes: Changes = [emptyPurlChange, npmChange, rubyChange]
|
||||||
|
const licensesConfig = {
|
||||||
|
allow: ['BSD'],
|
||||||
|
licenseExclusions: ['pkg:pip/[email protected]', 'pkg:npm/[email protected]']
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidLicenses = await getInvalidLicenseChanges(
|
||||||
|
changes,
|
||||||
|
licensesConfig
|
||||||
|
)
|
||||||
|
expect(invalidLicenses.forbidden.length).toEqual(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it does filters out changes if they are not on the exclusions list', async () => {
|
||||||
|
const changes: Changes = [pipChange, npmChange, rubyChange]
|
||||||
|
const licensesConfig = {
|
||||||
|
allow: ['BSD'],
|
||||||
|
licenseExclusions: ['pkg:pip/[email protected]', 'pkg:npm/[email protected]']
|
||||||
|
}
|
||||||
|
const invalidLicenses = await getInvalidLicenseChanges(
|
||||||
|
changes,
|
||||||
|
licensesConfig
|
||||||
|
)
|
||||||
|
expect(invalidLicenses.forbidden.length).toEqual(2)
|
||||||
|
expect(invalidLicenses.forbidden[0]).toBe(pipChange)
|
||||||
|
expect(invalidLicenses.forbidden[1]).toBe(npmChange)
|
||||||
|
})
|
||||||
|
|
||||||
describe('GH License API fallback', () => {
|
describe('GH License API fallback', () => {
|
||||||
test('it calls licenses endpoint if atleast one of the changes has null license and valid source_repository_url', async () => {
|
test('it calls licenses endpoint if atleast one of the changes has null license and valid source_repository_url', async () => {
|
||||||
const nullLicenseChange = {
|
const nullLicenseChange = {
|
||||||
|
|||||||
@@ -27,6 +27,45 @@ const defaultConfig: ConfigurationOptions = {
|
|||||||
comment_summary_in_pr: true
|
comment_summary_in_pr: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changesWithEmptyManifests: Changes = [
|
||||||
|
{
|
||||||
|
change_type: 'added',
|
||||||
|
manifest: '',
|
||||||
|
ecosystem: 'unknown',
|
||||||
|
name: 'castore',
|
||||||
|
version: '0.1.17',
|
||||||
|
package_url: 'pkg:hex/[email protected]',
|
||||||
|
license: null,
|
||||||
|
source_repository_url: null,
|
||||||
|
scope: 'runtime',
|
||||||
|
vulnerabilities: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
change_type: 'added',
|
||||||
|
manifest: '',
|
||||||
|
ecosystem: 'unknown',
|
||||||
|
name: 'connection',
|
||||||
|
version: '1.1.0',
|
||||||
|
package_url: 'pkg:hex/[email protected]',
|
||||||
|
license: null,
|
||||||
|
source_repository_url: null,
|
||||||
|
scope: 'runtime',
|
||||||
|
vulnerabilities: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
change_type: 'added',
|
||||||
|
manifest: 'python/dist-info/METADATA',
|
||||||
|
ecosystem: 'pip',
|
||||||
|
name: 'pygments',
|
||||||
|
version: '2.6.1',
|
||||||
|
package_url: 'pkg:pypi/[email protected]',
|
||||||
|
license: 'BSD-2-Clause',
|
||||||
|
source_repository_url: 'https://github.com/pygments/pygments',
|
||||||
|
scope: 'runtime',
|
||||||
|
vulnerabilities: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
test('prints headline as h1', () => {
|
test('prints headline as h1', () => {
|
||||||
summary.addSummaryToSummary(
|
summary.addSummaryToSummary(
|
||||||
emptyChanges,
|
emptyChanges,
|
||||||
@@ -65,6 +104,22 @@ test('only includes "No license issues found"-message if "vulnerability_check" i
|
|||||||
expect(text).toContain('✅ No license issues found.')
|
expect(text).toContain('✅ No license issues found.')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('groups dependencies with empty manifest paths together', () => {
|
||||||
|
summary.addSummaryToSummary(
|
||||||
|
changesWithEmptyManifests,
|
||||||
|
emptyInvalidLicenseChanges,
|
||||||
|
defaultConfig
|
||||||
|
)
|
||||||
|
summary.addScannedDependencies(changesWithEmptyManifests)
|
||||||
|
const text = core.summary.stringify()
|
||||||
|
|
||||||
|
expect(text).toContain('<summary>Unnamed Manifest</summary>')
|
||||||
|
expect(text).toContain('castore')
|
||||||
|
expect(text).toContain('connection')
|
||||||
|
expect(text).toContain('<summary>python/dist-info/METADATA</summary>')
|
||||||
|
expect(text).toContain('pygments')
|
||||||
|
})
|
||||||
|
|
||||||
test('does not include status section if nothing was found', () => {
|
test('does not include status section if nothing was found', () => {
|
||||||
summary.addSummaryToSummary(
|
summary.addSummaryToSummary(
|
||||||
emptyChanges,
|
emptyChanges,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// GitHub Action inputs come in the form of environment variables
|
||||||
|
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
|
||||||
|
export function setInput(input: string, value: string): void {
|
||||||
|
process.env[`INPUT_${input.toUpperCase()}`] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want a clean ENV before each test. We use `delete`
|
||||||
|
// since we want `undefined` values and not empty strings.
|
||||||
|
export function clearInputs(): void {
|
||||||
|
const allowedOptions = [
|
||||||
|
'FAIL-ON-SEVERITY',
|
||||||
|
'FAIL-ON-SCOPES',
|
||||||
|
'ALLOW-LICENSES',
|
||||||
|
'DENY-LICENSES',
|
||||||
|
'ALLOW-GHSAS',
|
||||||
|
'LICENSE-CHECK',
|
||||||
|
'VULNERABILITY-CHECK',
|
||||||
|
'CONFIG-FILE',
|
||||||
|
'BASE-REF',
|
||||||
|
'HEAD-REF',
|
||||||
|
'COMMENT-SUMMARY-IN-PR'
|
||||||
|
]
|
||||||
|
|
||||||
|
// eslint-disable-next-line github/array-foreach
|
||||||
|
allowedOptions.forEach(option => {
|
||||||
|
delete process.env[`INPUT_${option.toUpperCase()}`]
|
||||||
|
})
|
||||||
|
}
|
||||||
+5
-2
@@ -1,3 +1,5 @@
|
|||||||
|
# Avoid using default values for options here since they will
|
||||||
|
# end up overriding external configurations.
|
||||||
name: 'Dependency Review'
|
name: 'Dependency Review'
|
||||||
description: 'Prevent the introduction of dependencies with known vulnerabilities'
|
description: 'Prevent the introduction of dependencies with known vulnerabilities'
|
||||||
author: 'GitHub'
|
author: 'GitHub'
|
||||||
@@ -9,11 +11,9 @@ inputs:
|
|||||||
fail-on-severity:
|
fail-on-severity:
|
||||||
description: Don't block PRs below this severity. Possible values are `low`, `moderate`, `high`, `critical`.
|
description: Don't block PRs below this severity. Possible values are `low`, `moderate`, `high`, `critical`.
|
||||||
required: false
|
required: false
|
||||||
default: 'low'
|
|
||||||
fail-on-scopes:
|
fail-on-scopes:
|
||||||
description: Dependency scopes to block PRs on. Comma-separated list. Possible values are 'unknown', 'runtime', and 'development' (e.g. "runtime, development")
|
description: Dependency scopes to block PRs on. Comma-separated list. Possible values are 'unknown', 'runtime', and 'development' (e.g. "runtime, development")
|
||||||
required: false
|
required: false
|
||||||
default: 'runtime'
|
|
||||||
base-ref:
|
base-ref:
|
||||||
description: The base git ref to be used for this check. Has a default value when the workflow event is `pull_request` or `pull_request_target`. Must be provided otherwise.
|
description: The base git ref to be used for this check. Has a default value when the workflow event is `pull_request` or `pull_request_target`. Must be provided otherwise.
|
||||||
required: false
|
required: false
|
||||||
@@ -29,6 +29,9 @@ inputs:
|
|||||||
deny-licenses:
|
deny-licenses:
|
||||||
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
||||||
required: false
|
required: false
|
||||||
|
allow-dependencies-licenses:
|
||||||
|
description: Comma-separated list of dependencies in purl format (e.g. "pkg:npm/express, pkg:pip/pycrypto"). These dependencies will be permitted to use any license, no matter what license policy is enforced otherwise.
|
||||||
|
required: false
|
||||||
allow-ghsas:
|
allow-ghsas:
|
||||||
description: Comma-separated list of allowed GitHub Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
|
description: Comma-separated list of allowed GitHub Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
+4712
-1849
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+22
@@ -1340,6 +1340,28 @@ 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.
|
||||||
|
|
||||||
|
|
||||||
|
packageurl-js
|
||||||
|
MIT
|
||||||
|
Copyright (c) the purl 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.
|
||||||
|
|
||||||
|
|
||||||
safe-buffer
|
safe-buffer
|
||||||
MIT
|
MIT
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
# Examples on how to use the Dependancy Review Action
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
A very basic example of how to use the action. This will run the action with the default configuration.
|
||||||
|
|
||||||
|
The full list of configuration options can be found [here](../README.md#configuration-options).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using an inline configuration
|
||||||
|
|
||||||
|
The following example will fail the action if any vulnerabilities are found with a severity of medium or higher; and if any packages are found with an incompatible license - in this case, the LGPL-2.0 and BSD-2-Clause licenses.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
|
with:
|
||||||
|
fail-on-severity: critical
|
||||||
|
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using a configuration file
|
||||||
|
|
||||||
|
The following example will use a configuration file to configure the action. This is useful if you want to keep your configuration in a single place and makes it easier to manage as the configuration grows.
|
||||||
|
|
||||||
|
The configuration file can be located in the same repository or in a separate repository. Having it in a separate repository might be useful if you plan to use the same configuration across multiple repositories and control it centrally.
|
||||||
|
|
||||||
|
In this example, the configuration file is located in the same repository under `.github/dependency-review-config.yml`. The following configuration will fail the action if any vulnerabilities are found with a severity of critical; and if any packages are found with an incompatible license - in this case, the LGPL-2.0 and BSD-2-Clause licenses.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
fail_on_severity: 'critical'
|
||||||
|
allow_licenses:
|
||||||
|
- 'LGPL-2.0'
|
||||||
|
- 'BSD-2-Clause'
|
||||||
|
```
|
||||||
|
|
||||||
|
The Dependancy Review Action workflow file will then look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
|
with:
|
||||||
|
config-file: './.github/dependency-review-config.yml'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using a configuration file from a external repository
|
||||||
|
|
||||||
|
The following example will use a configuration file from an external public GitHub repository to configure the action.
|
||||||
|
|
||||||
|
Let's say that the configuration file is located in `github/octorepo/dependency-review-config.yml@main`
|
||||||
|
|
||||||
|
The Dependancy Review Action workflow file will then look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
|
with:
|
||||||
|
config-file: 'github/octorepo/dependency-review-config.yml@main'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using a configuration file from a external repository with a personal access token
|
||||||
|
|
||||||
|
The following example will use a configuration file from an external private GtiHub repository to configure the action.
|
||||||
|
|
||||||
|
Let's say that the configuration file is located in `github/octorepo-private/dependency-review-config.yml@main`
|
||||||
|
|
||||||
|
The Dependancy Review Action workflow file will then look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
|
with:
|
||||||
|
config-file: 'github/octorepo-private/dependency-review-config.yml@main'
|
||||||
|
config-file-token: ${{ secrets.GITHUB_TOKEN }} # or a personal access token
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting the results of the action in the PR as a comment
|
||||||
|
|
||||||
|
Using the `comment-summary-in-pr` you can get the results of the action in the PR as a comment. In order for this to work, the action needs to be able to create a comment in the PR. This requires additional `pull-requests: write` permission.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
|
with:
|
||||||
|
fail-on-severity: critical
|
||||||
|
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||||
|
comment-summary-in-pr: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exclude dependencies from the license check
|
||||||
|
|
||||||
|
Using the `allow-dependencies-licenses` you can exclude dependencies from the license check. The values should be provided in [purl](https://github.com/package-url/purl-spec) format.
|
||||||
|
|
||||||
|
In this example, we are excluding `lodash` from `npm` and `requests` from `pip` dependencies from the license check
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
|
with:
|
||||||
|
fail-on-severity: critical
|
||||||
|
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||||
|
comment-summary-in-pr: true
|
||||||
|
allow-dependencies-licenses: 'pkg:npm/loadash, pkg:pip/requests'
|
||||||
|
```
|
||||||
|
|
||||||
|
If we were to use configuration file, the configuration would look like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
fail-on-severity: 'critical'
|
||||||
|
allow-licenses:
|
||||||
|
- 'LGPL-2.0'
|
||||||
|
- 'BSD-2-Clause'
|
||||||
|
allow-dependencies-licenses:
|
||||||
|
- 'pkg:npm/loadash'
|
||||||
|
- 'pkg:pip/requests'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Only check for vulnerabilities
|
||||||
|
|
||||||
|
To only do the vulnerability check you can use the `license-check` to disable the license compatibility check (which is done by default).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
|
with:
|
||||||
|
fail-on-severity: critical
|
||||||
|
comment-summary-in-pr: true
|
||||||
|
license-check: false
|
||||||
|
```
|
||||||
Generated
+390
-6897
File diff suppressed because it is too large
Load Diff
+12
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dependency-review-action",
|
"name": "dependency-review-action",
|
||||||
"version": "3.0.4",
|
"version": "3.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A GitHub Action for Dependency Review",
|
"description": "A GitHub Action for Dependency Review",
|
||||||
"main": "lib/main.js",
|
"main": "lib/main.js",
|
||||||
@@ -31,31 +31,32 @@
|
|||||||
"@octokit/request-error": "^2.1.0",
|
"@octokit/request-error": "^2.1.0",
|
||||||
"ansi-styles": "^6.2.1",
|
"ansi-styles": "^6.2.1",
|
||||||
"got": "^12.6.0",
|
"got": "^12.6.0",
|
||||||
"nodemon": "^2.0.21",
|
"nodemon": "^2.0.22",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.16",
|
||||||
|
"packageurl-js": "^1.0.2",
|
||||||
"spdx-expression-parse": "^3.0.1",
|
"spdx-expression-parse": "^3.0.1",
|
||||||
"spdx-satisfies": "^5.0.1",
|
"spdx-satisfies": "^5.0.1",
|
||||||
"yaml": "^2.2.1",
|
"yaml": "^2.3.1",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.16",
|
"@types/node": "^16.18.34",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.48.0",
|
"@typescript-eslint/parser": "^5.48.0",
|
||||||
"@types/spdx-expression-parse": "^3.0.2",
|
"@types/spdx-expression-parse": "^3.0.2",
|
||||||
"@types/spdx-satisfies": "^0.1.0",
|
"@types/spdx-satisfies": "^0.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||||
"@typescript-eslint/parser": "^5.55.0",
|
"@typescript-eslint/parser": "^5.59.8",
|
||||||
"@vercel/ncc": "^0.36.1",
|
"@vercel/ncc": "^0.36.1",
|
||||||
"esbuild-register": "^3.4.2",
|
"esbuild-register": "^3.4.2",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.41.0",
|
||||||
"eslint-plugin-github": "^4.6.1",
|
"eslint-plugin-github": "^4.7.0",
|
||||||
"eslint-plugin-jest": "^27.2.1",
|
"eslint-plugin-jest": "^27.2.1",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"nodemon": "^2.0.21",
|
"nodemon": "^2.0.22",
|
||||||
"prettier": "2.8.4",
|
"prettier": "2.8.8",
|
||||||
"ts-jest": "^27.1.4",
|
"ts-jest": "^27.1.4",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const defaultConfig: ConfigurationOptions = {
|
|||||||
allow_ghsas: [],
|
allow_ghsas: [],
|
||||||
allow_licenses: ['MIT'],
|
allow_licenses: ['MIT'],
|
||||||
deny_licenses: [],
|
deny_licenses: [],
|
||||||
|
allow_dependencies_licenses: [
|
||||||
|
'pkg:npm/[email protected]',
|
||||||
|
'pkg:pip/requests',
|
||||||
|
'pkg:pip/certifi',
|
||||||
|
'pkg:pip/[email protected]'
|
||||||
|
],
|
||||||
comment_summary_in_pr: true
|
comment_summary_in_pr: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-1
@@ -5,6 +5,7 @@ import * as core from '@actions/core'
|
|||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas'
|
import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas'
|
||||||
import {isSPDXValid, octokitClient} from './utils'
|
import {isSPDXValid, octokitClient} from './utils'
|
||||||
|
import {PackageURL} from 'packageurl-js'
|
||||||
|
|
||||||
type ConfigurationOptionsPartial = Partial<ConfigurationOptions>
|
type ConfigurationOptionsPartial = Partial<ConfigurationOptions>
|
||||||
|
|
||||||
@@ -29,6 +30,9 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
|||||||
const fail_on_scopes = parseList(getOptionalInput('fail-on-scopes'))
|
const fail_on_scopes = parseList(getOptionalInput('fail-on-scopes'))
|
||||||
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
|
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
|
||||||
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
|
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
|
||||||
|
const allow_dependencies_licenses = parseList(
|
||||||
|
getOptionalInput('allow-dependencies-licenses')
|
||||||
|
)
|
||||||
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
|
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
|
||||||
const license_check = getOptionalBoolean('license-check')
|
const license_check = getOptionalBoolean('license-check')
|
||||||
const vulnerability_check = getOptionalBoolean('vulnerability-check')
|
const vulnerability_check = getOptionalBoolean('vulnerability-check')
|
||||||
@@ -36,6 +40,7 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
|||||||
const head_ref = getOptionalInput('head-ref')
|
const head_ref = getOptionalInput('head-ref')
|
||||||
const comment_summary_in_pr = getOptionalBoolean('comment-summary-in-pr')
|
const comment_summary_in_pr = getOptionalBoolean('comment-summary-in-pr')
|
||||||
|
|
||||||
|
validatePURL(allow_dependencies_licenses)
|
||||||
validateLicenses('allow-licenses', allow_licenses)
|
validateLicenses('allow-licenses', allow_licenses)
|
||||||
validateLicenses('deny-licenses', deny_licenses)
|
validateLicenses('deny-licenses', deny_licenses)
|
||||||
|
|
||||||
@@ -44,6 +49,7 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
|||||||
fail_on_scopes,
|
fail_on_scopes,
|
||||||
allow_licenses,
|
allow_licenses,
|
||||||
deny_licenses,
|
deny_licenses,
|
||||||
|
allow_dependencies_licenses,
|
||||||
allow_ghsas,
|
allow_ghsas,
|
||||||
license_check,
|
license_check,
|
||||||
vulnerability_check,
|
vulnerability_check,
|
||||||
@@ -130,7 +136,8 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
|
|||||||
'allow-licenses',
|
'allow-licenses',
|
||||||
'deny-licenses',
|
'deny-licenses',
|
||||||
'fail-on-scopes',
|
'fail-on-scopes',
|
||||||
'allow-ghsas'
|
'allow-ghsas',
|
||||||
|
'allow-dependencies-licenses'
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const key of Object.keys(data)) {
|
for (const key of Object.keys(data)) {
|
||||||
@@ -149,6 +156,11 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
|
|||||||
validateLicenses(key, data[key])
|
validateLicenses(key, data[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate purls from the allow-dependencies-licenses
|
||||||
|
if (key === 'allow-dependencies-licenses') {
|
||||||
|
validatePURL(data[key])
|
||||||
|
}
|
||||||
|
|
||||||
// get rid of the ugly dashes from the actions conventions
|
// 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]
|
||||||
@@ -187,3 +199,19 @@ async function getRemoteConfig(configOpts: {
|
|||||||
throw new Error('Error fetching remote config file')
|
throw new Error('Error fetching remote config file')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function validatePURL(allow_dependencies_licenses: string[] | undefined): void {
|
||||||
|
//validate that the provided elements of the string are in valid purl format
|
||||||
|
if (allow_dependencies_licenses === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const invalid_purls = allow_dependencies_licenses.filter(
|
||||||
|
purl => !PackageURL.fromString(purl)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (invalid_purls.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid purl(s) in allow-dependencies-licenses: ${invalid_purls}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
+26
-4
@@ -1,9 +1,14 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as githubUtils from '@actions/github/lib/utils'
|
import * as githubUtils from '@actions/github/lib/utils'
|
||||||
import * as retry from '@octokit/plugin-retry'
|
import * as retry from '@octokit/plugin-retry'
|
||||||
import {Changes, ChangesSchema} from './schemas'
|
import {
|
||||||
|
ChangesSchema,
|
||||||
|
ComparisonResponse,
|
||||||
|
ComparisonResponseSchema
|
||||||
|
} from './schemas'
|
||||||
|
|
||||||
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry)
|
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry)
|
||||||
|
const SnapshotWarningsHeader = 'x-github-dependency-graph-snapshot-warnings'
|
||||||
const octo = new retryingOctokit(
|
const octo = new retryingOctokit(
|
||||||
githubUtils.getOctokitOptions(core.getInput('repo-token', {required: true}))
|
githubUtils.getOctokitOptions(core.getInput('repo-token', {required: true}))
|
||||||
)
|
)
|
||||||
@@ -18,14 +23,31 @@ export async function compare({
|
|||||||
repo: string
|
repo: string
|
||||||
baseRef: string
|
baseRef: string
|
||||||
headRef: string
|
headRef: string
|
||||||
}): Promise<Changes> {
|
}): Promise<ComparisonResponse> {
|
||||||
|
let snapshot_warnings = ''
|
||||||
const changes = await octo.paginate(
|
const changes = await octo.paginate(
|
||||||
'GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}',
|
|
||||||
{
|
{
|
||||||
|
method: 'GET',
|
||||||
|
url: '/repos/{owner}/{repo}/dependency-graph/compare/{basehead}',
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
basehead: `${baseRef}...${headRef}`
|
basehead: `${baseRef}...${headRef}`
|
||||||
|
},
|
||||||
|
response => {
|
||||||
|
if (
|
||||||
|
response.headers[SnapshotWarningsHeader] &&
|
||||||
|
typeof response.headers[SnapshotWarningsHeader] === 'string'
|
||||||
|
) {
|
||||||
|
snapshot_warnings = Buffer.from(
|
||||||
|
response.headers[SnapshotWarningsHeader],
|
||||||
|
'base64'
|
||||||
|
).toString('utf-8')
|
||||||
|
}
|
||||||
|
return ChangesSchema.parse(response.data)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return ChangesSchema.parse(changes)
|
return ComparisonResponseSchema.parse({
|
||||||
|
changes,
|
||||||
|
snapshot_warnings
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-1
@@ -1,17 +1,19 @@
|
|||||||
import spdxSatisfies from 'spdx-satisfies'
|
import spdxSatisfies from 'spdx-satisfies'
|
||||||
import {Change, Changes} from './schemas'
|
import {Change, Changes} from './schemas'
|
||||||
import {isSPDXValid, octokitClient} from './utils'
|
import {isSPDXValid, octokitClient} from './utils'
|
||||||
|
import {PackageURL} from 'packageurl-js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loops through a list of changes, filtering and returning the
|
* Loops through a list of changes, filtering and returning the
|
||||||
* ones that don't conform to the licenses allow/deny lists.
|
* ones that don't conform to the licenses allow/deny lists.
|
||||||
|
* It will also filter out the changes which are defined in the licenseExclusions list.
|
||||||
*
|
*
|
||||||
* Keep in mind that we don't let users specify both an allow and a deny
|
* Keep in mind that we don't let users specify both an allow and a deny
|
||||||
* list in their config files, so this code works under the assumption that
|
* list in their config files, so this code works under the assumption that
|
||||||
* one of the two list parameters will be empty. If both lists are provided,
|
* one of the two list parameters will be empty. If both lists are provided,
|
||||||
* 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[], licenseExclusions?: string[]}} licenses An object with `allow`/`deny`/`licenseExclusions` keys, each containing a list of 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
|
* @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 type InvalidLicenseChangeTypes =
|
export type InvalidLicenseChangeTypes =
|
||||||
@@ -24,11 +26,44 @@ export async function getInvalidLicenseChanges(
|
|||||||
licenses: {
|
licenses: {
|
||||||
allow?: string[]
|
allow?: string[]
|
||||||
deny?: string[]
|
deny?: string[]
|
||||||
|
licenseExclusions?: string[]
|
||||||
}
|
}
|
||||||
): Promise<InvalidLicenseChanges> {
|
): Promise<InvalidLicenseChanges> {
|
||||||
const {allow, deny} = licenses
|
const {allow, deny} = licenses
|
||||||
|
const licenseExclusions = licenses.licenseExclusions?.map(
|
||||||
|
(pkgUrl: string) => {
|
||||||
|
return PackageURL.fromString(pkgUrl)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const groupedChanges = await groupChanges(changes)
|
const groupedChanges = await groupChanges(changes)
|
||||||
|
|
||||||
|
// Takes the changes from the groupedChanges object and filters out the ones that are part of the exclusions list
|
||||||
|
// It does by creating a new PackageURL object from the change and comparing it to the exclusions list
|
||||||
|
groupedChanges.licensed = groupedChanges.licensed.filter(change => {
|
||||||
|
if (change.package_url.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeAsPackageURL = PackageURL.fromString(change.package_url)
|
||||||
|
|
||||||
|
// We want to find if the licenseExclussion list contains the PackageURL of the Change
|
||||||
|
// If it does, we want to filter it out and therefore return false
|
||||||
|
// If it doesn't, we want to keep it and therefore return true
|
||||||
|
if (
|
||||||
|
licenseExclusions !== null &&
|
||||||
|
licenseExclusions !== undefined &&
|
||||||
|
licenseExclusions.findIndex(
|
||||||
|
exclusion =>
|
||||||
|
exclusion.type === changeAsPackageURL.type &&
|
||||||
|
exclusion.name === changeAsPackageURL.name
|
||||||
|
) !== -1
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
const licensedChanges: Changes = groupedChanges.licensed
|
const licensedChanges: Changes = groupedChanges.licensed
|
||||||
|
|
||||||
const invalidLicenseChanges: InvalidLicenseChanges = {
|
const invalidLicenseChanges: InvalidLicenseChanges = {
|
||||||
|
|||||||
+10
-2
@@ -20,14 +20,17 @@ import {commentPr} from './comment-pr'
|
|||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const config = await readConfig()
|
const config = await readConfig()
|
||||||
|
|
||||||
const refs = getRefs(config, github.context)
|
const refs = getRefs(config, github.context)
|
||||||
|
|
||||||
const changes = await dependencyGraph.compare({
|
const comparison = await dependencyGraph.compare({
|
||||||
owner: github.context.repo.owner,
|
owner: github.context.repo.owner,
|
||||||
repo: github.context.repo.repo,
|
repo: github.context.repo.repo,
|
||||||
baseRef: refs.base,
|
baseRef: refs.base,
|
||||||
headRef: refs.head
|
headRef: refs.head
|
||||||
})
|
})
|
||||||
|
const changes = comparison.changes
|
||||||
|
const snapshot_warnings = comparison.snapshot_warnings
|
||||||
|
|
||||||
if (!changes) {
|
if (!changes) {
|
||||||
core.info('No Dependency Changes found. Skipping Dependency Review.')
|
core.info('No Dependency Changes found. Skipping Dependency Review.')
|
||||||
@@ -55,7 +58,8 @@ async function run(): Promise<void> {
|
|||||||
filteredChanges,
|
filteredChanges,
|
||||||
{
|
{
|
||||||
allow: config.allow_licenses,
|
allow: config.allow_licenses,
|
||||||
deny: config.deny_licenses
|
deny: config.deny_licenses,
|
||||||
|
licenseExclusions: config.allow_dependencies_licenses
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,6 +69,10 @@ async function run(): Promise<void> {
|
|||||||
config
|
config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (snapshot_warnings) {
|
||||||
|
summary.addSnapshotWarnings(snapshot_warnings)
|
||||||
|
}
|
||||||
|
|
||||||
if (config.vulnerability_check) {
|
if (config.vulnerability_check) {
|
||||||
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
|
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
|
||||||
printVulnerabilitiesBlock(vulnerableChanges, minSeverity)
|
printVulnerabilitiesBlock(vulnerableChanges, minSeverity)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const ConfigurationOptionsSchema = z
|
|||||||
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
||||||
allow_licenses: z.array(z.string()).optional(),
|
allow_licenses: z.array(z.string()).optional(),
|
||||||
deny_licenses: z.array(z.string()).optional(),
|
deny_licenses: z.array(z.string()).optional(),
|
||||||
|
allow_dependencies_licenses: z.array(z.string()).optional(),
|
||||||
allow_ghsas: z.array(z.string()).default([]),
|
allow_ghsas: z.array(z.string()).default([]),
|
||||||
license_check: z.boolean().default(true),
|
license_check: z.boolean().default(true),
|
||||||
vulnerability_check: z.boolean().default(true),
|
vulnerability_check: z.boolean().default(true),
|
||||||
@@ -73,9 +74,14 @@ export const ConfigurationOptionsSchema = z
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const ChangesSchema = z.array(ChangeSchema)
|
export const ChangesSchema = z.array(ChangeSchema)
|
||||||
|
export const ComparisonResponseSchema = z.object({
|
||||||
|
changes: z.array(ChangeSchema),
|
||||||
|
snapshot_warnings: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
export type Change = z.infer<typeof ChangeSchema>
|
export type Change = z.infer<typeof ChangeSchema>
|
||||||
export type Changes = z.infer<typeof ChangesSchema>
|
export type Changes = z.infer<typeof ChangesSchema>
|
||||||
|
export type ComparisonResponse = z.infer<typeof ComparisonResponseSchema>
|
||||||
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
|
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
|
||||||
export type Severity = z.infer<typeof SeveritySchema>
|
export type Severity = z.infer<typeof SeveritySchema>
|
||||||
export type Scope = (typeof SCOPES)[number]
|
export type Scope = (typeof SCOPES)[number]
|
||||||
|
|||||||
@@ -143,6 +143,13 @@ export function addLicensesToSummary(
|
|||||||
`<strong>Denied Licenses</strong>: ${config.deny_licenses.join(', ')}`
|
`<strong>Denied Licenses</strong>: ${config.deny_licenses.join(', ')}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (config.allow_dependencies_licenses) {
|
||||||
|
core.summary.addQuote(
|
||||||
|
`<strong>Excluded from license check</strong>: ${config.allow_dependencies_licenses.join(
|
||||||
|
', '
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
core.debug(
|
core.debug(
|
||||||
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
|
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
|
||||||
@@ -215,6 +222,23 @@ export function addScannedDependencies(changes: Changes): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addSnapshotWarnings(warnings: string): void {
|
||||||
|
// For now, we want to ignore warnings that just complain
|
||||||
|
// about missing snapshots on the head SHA. This is a product
|
||||||
|
// decision to avoid presenting warnings to users who simply
|
||||||
|
// don't use snapshots.
|
||||||
|
const ignore_regex = new RegExp(/No.*snapshot.*found.*head.*/, 'i')
|
||||||
|
if (ignore_regex.test(warnings)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.summary.addHeading('Snapshot Warnings', 2)
|
||||||
|
core.summary.addQuote(`${icons.warning}: ${warnings}`)
|
||||||
|
core.summary.addRaw(
|
||||||
|
'Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function countLicenseIssues(
|
function countLicenseIssues(
|
||||||
invalidLicenseChanges: InvalidLicenseChanges
|
invalidLicenseChanges: InvalidLicenseChanges
|
||||||
): number {
|
): number {
|
||||||
|
|||||||
+3
-1
@@ -8,7 +8,9 @@ export function groupDependenciesByManifest(
|
|||||||
): Map<string, Changes> {
|
): Map<string, Changes> {
|
||||||
const dependencies: Map<string, Changes> = new Map()
|
const dependencies: Map<string, Changes> = new Map()
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
const manifestName = change.manifest
|
// If the manifest is null or empty, give it a name now to avoid
|
||||||
|
// breaking the HTML rendering later
|
||||||
|
const manifestName = change.manifest || 'Unnamed Manifest'
|
||||||
|
|
||||||
if (dependencies.get(manifestName) === undefined) {
|
if (dependencies.get(manifestName) === undefined) {
|
||||||
dependencies.set(manifestName, [])
|
dependencies.set(manifestName, [])
|
||||||
|
|||||||
Reference in New Issue
Block a user