Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20f8e76960 |
@@ -1 +0,0 @@
|
|||||||
fail-on-severity: low
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
event.json
|
event.json
|
||||||
.ruby-version
|
|
||||||
|
|
||||||
# Dependency directory
|
# Dependency directory
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
+1
-2
@@ -75,7 +75,6 @@ Here are a few things you can do that will increase the likelihood of your pull
|
|||||||
|
|
||||||
## Cutting a new release
|
## Cutting a new release
|
||||||
|
|
||||||
1. Update the version number in [package.json](https://github.com/actions/dependency-review-action/blob/main/package.json).
|
|
||||||
1. Go to [Draft a new
|
1. Go to [Draft a new
|
||||||
release](https://github.com/actions/dependency-review-action/releases/new)
|
release](https://github.com/actions/dependency-review-action/releases/new)
|
||||||
in the Releases page.
|
in the Releases page.
|
||||||
@@ -101,7 +100,7 @@ major version number (e.g. `v1`) in their workflows while
|
|||||||
automatically getting all the
|
automatically getting all the
|
||||||
minor/patch updates.
|
minor/patch updates.
|
||||||
|
|
||||||
To do this just checkout `main`, force-create a new annotated tag, and push it:
|
To do this just force-create a new annotated tag and push it:
|
||||||
```
|
```
|
||||||
git tag -fa v2 -m "Updating v2 to 2.3.4"
|
git tag -fa v2 -m "Updating v2 to 2.3.4"
|
||||||
git push origin v2 --force
|
git push origin v2 --force
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
### GitHub Enterprise Server
|
### GitHub Enterprise Server
|
||||||
|
|
||||||
This action is available in Enterprise Server starting with version 3.6. Make sure
|
This action is available in GHES starting with version 3.6. Make sure
|
||||||
[GitHub Advanced
|
[GitHub Advanced
|
||||||
Security](https://docs.github.com/en/[email protected]/admin/code-security/managing-github-advanced-security-for-your-enterprise/enabling-github-advanced-security-for-your-enterprise)
|
Security](https://docs.github.com/en/[email protected]/admin/code-security/managing-github-advanced-security-for-your-enterprise/enabling-github-advanced-security-for-your-enterprise)
|
||||||
and [GitHub
|
and [GitHub
|
||||||
@@ -50,6 +50,7 @@ with the label of any of your runners (the default label
|
|||||||
is `self-hosted`):
|
is `self-hosted`):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -64,144 +65,9 @@ jobs:
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configure this action by either using an external configuration file,
|
You can pass additional options to the Dependency Review
|
||||||
or by inlining these options in your workflow file.
|
Action using your workflow file. Here's an example workflow with
|
||||||
|
all the possible configurations:
|
||||||
## Configuration Options
|
|
||||||
|
|
||||||
### config-file
|
|
||||||
|
|
||||||
A string representing the path to an external configuraton file. By
|
|
||||||
default external configuration files are not used.
|
|
||||||
|
|
||||||
**Possible values**: A string representing the absolute path to the
|
|
||||||
configuration file.
|
|
||||||
|
|
||||||
**Example**: `config-file: ./.github/dependency-review-config.yml`.
|
|
||||||
|
|
||||||
### fail-on-severity
|
|
||||||
|
|
||||||
Configure the severity level for alerting. See "[Vulnerability Severity](https://github.com/actions/dependency-review-action#vulnerability-severity)".
|
|
||||||
|
|
||||||
**Possible values**: `critical`, `high`, `moderate`, `low`.
|
|
||||||
|
|
||||||
**Example**: `fail-on-severity: moderate`.
|
|
||||||
|
|
||||||
### fail-on-scopes
|
|
||||||
|
|
||||||
A list of strings representing the build environments you want to
|
|
||||||
support. The default value is `development, runtime`.
|
|
||||||
|
|
||||||
**Possible values**: `development`, `runtime`, `unknown`
|
|
||||||
|
|
||||||
**Inline example**: `fail-on-scopes: development, runtime`
|
|
||||||
|
|
||||||
**YAML example**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# this prevents scanning development dependencies
|
|
||||||
fail-on-scopes:
|
|
||||||
- runtime
|
|
||||||
```
|
|
||||||
|
|
||||||
### allow-licenses
|
|
||||||
|
|
||||||
Only allow the licenses in this list. See "[Licenses](https://github.com/actions/dependency-review-action#licenses)".
|
|
||||||
|
|
||||||
**Possible values**: Any `spdx_id` value(s) from
|
|
||||||
https://docs.github.com/en/rest/licenses.
|
|
||||||
|
|
||||||
**Inline example**: `allow-licenses: BSD-3-Clause, MIT`
|
|
||||||
|
|
||||||
**YAML example**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
allow-licenses:
|
|
||||||
- BSD-3-Clause
|
|
||||||
- MIT
|
|
||||||
```
|
|
||||||
|
|
||||||
### deny-licenses
|
|
||||||
|
|
||||||
Add a custom list of licenses you want to block. See
|
|
||||||
"[Licenses](https://github.com/actions/dependency-review-action#licenses)".
|
|
||||||
|
|
||||||
**Possible values**: Any `spdx_id` value(s) from
|
|
||||||
https://docs.github.com/en/rest/licenses.
|
|
||||||
|
|
||||||
**Inline example**: `deny-licenses: LGPL-2.0, BSD-2-Clause`
|
|
||||||
|
|
||||||
**YAML example**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
deny-licenses:
|
|
||||||
- LGPL-2.0
|
|
||||||
- BSD-2-Clause
|
|
||||||
```
|
|
||||||
|
|
||||||
### allow-ghsas
|
|
||||||
|
|
||||||
Add a custom list of GitHub Advisory IDs that can be skipped during detection.
|
|
||||||
|
|
||||||
**Possible values**: Any valid advisory GHSA ids.
|
|
||||||
|
|
||||||
**Inline example**: `allow-ghsas: GHSA-abcd-1234-5679, GHSA-efgh-1234-5679`
|
|
||||||
|
|
||||||
**YAML example**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
allow-ghsas:
|
|
||||||
- GHSA-abcd-1234-5679
|
|
||||||
- GHSA-efgh-1234-5679
|
|
||||||
```
|
|
||||||
|
|
||||||
### base-ref/head-ref
|
|
||||||
|
|
||||||
Provide custom git references for the git base/head when performing
|
|
||||||
the comparison. If you are using pull requests, or
|
|
||||||
`pull_request_target` events you do not need to worry about setting
|
|
||||||
this. The values need to be specified for all other event types.
|
|
||||||
|
|
||||||
**Possible values**: Any valid git ref(s) in your project.
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
base-ref: 8bb8a58d6a4028b6c2e314d5caaf273f57644896
|
|
||||||
head-ref: 69af5638bf660cf218aad5709a4c100e42a2f37b
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration File
|
|
||||||
|
|
||||||
You can use an external configuration file to specify the settings for
|
|
||||||
this Action.
|
|
||||||
|
|
||||||
Start by specifying that you will be using an external configuration
|
|
||||||
file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Dependency Review
|
|
||||||
uses: actions/dependency-review-action@v2
|
|
||||||
with:
|
|
||||||
config-file: './.github/dependency-review-config.yml'
|
|
||||||
```
|
|
||||||
|
|
||||||
And then create the file in the path you just specified. **All of these fields are
|
|
||||||
optional**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
fail-on-severity: 'critical'
|
|
||||||
allow-licenses:
|
|
||||||
- 'GPL-3.0'
|
|
||||||
- 'BSD-3-Clause'
|
|
||||||
- 'MIT'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Inline Configuration
|
|
||||||
|
|
||||||
You can pass options to the Dependency Review
|
|
||||||
Action using your workflow file. Here's an example of what the full
|
|
||||||
file would look like:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: 'Dependency Review'
|
name: 'Dependency Review'
|
||||||
@@ -217,12 +83,27 @@ jobs:
|
|||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@v2
|
uses: actions/dependency-review-action@v2
|
||||||
with:
|
with:
|
||||||
fail-on-severity: moderate
|
# Possible values: "critical", "high", "moderate", "low"
|
||||||
|
# fail-on-severity: critical
|
||||||
# Use comma-separated names to pass list arguments:
|
#
|
||||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
# Possible values: Any available git ref
|
||||||
|
# base-ref: ${{ github.event.pull_request.base.ref }}
|
||||||
|
# head-ref: ${{ github.event.pull_request.head.ref }}
|
||||||
|
#
|
||||||
|
# You can only include one of these two options: `allow-licenses` and `deny-licenses`. These options are not supported on GHES.
|
||||||
|
#
|
||||||
|
# Possible values: Any `spdx_id` value(s) from https://docs.github.com/en/rest/licenses
|
||||||
|
# allow-licenses: GPL-3.0, BSD-3-Clause, MIT
|
||||||
|
#
|
||||||
|
# Possible values: Any `spdx_id` value(s) from https://docs.github.com/en/rest/licenses
|
||||||
|
# deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When the workflow with this action is caused by a `pull_request` or `pull_request_target` event,
|
||||||
|
the `base-ref` and `head-ref` values have the defaults as shown above. If the workflow is caused by
|
||||||
|
any other event, the `base-ref` and `head-ref` options must be
|
||||||
|
explicitly set in the configuration file.
|
||||||
|
|
||||||
### Vulnerability Severity
|
### Vulnerability Severity
|
||||||
|
|
||||||
By default the action will fail on any pull request that contains a
|
By default the action will fail on any pull request that contains a
|
||||||
@@ -239,23 +120,12 @@ This example will only fail on pull requests with `critical` and `high` vulnerab
|
|||||||
fail-on-severity: high
|
fail-on-severity: high
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dependency Scoping
|
|
||||||
|
|
||||||
By default the action will only fail on `runtime` dependencies that have vulnerabilities or unacceptable licenses, ignoring `development` dependencies. You can override this behavior with the `fail-on-scopes` option, which will allow you to list the specific dependency scopes you care about. The possible values are: `unknown`, `runtime`, and `development`. Note: Filtering by scope will not be supported on Enterprise Server just yet, as the REST API's introduction of `scope` will be released in an upcoming Enterprise Server version. We will treat all dependencies on Enterprise Server as having a `runtime` scope and thus will not be filtered away.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: Dependency Review
|
|
||||||
uses: actions/dependency-review-action@v2
|
|
||||||
with:
|
|
||||||
fail-on-scopes: runtime, development
|
|
||||||
```
|
|
||||||
|
|
||||||
### Licenses
|
### Licenses
|
||||||
|
|
||||||
You can set the action to fail on pull requests based on the licenses of the dependencies
|
You can set the action to fail on pull requests based on the licenses of the dependencies
|
||||||
they introduce. With `allow-licenses` you can define the list of licenses
|
they introduce. With `allow-licenses` you can define the list of licenses
|
||||||
your repository will accept. Alternatively, you can use `deny-licenses` to only
|
your repository will accept. Alternatively, you can use `deny-licenses` to only
|
||||||
forbid a subset of licenses. These options are not supported on Enterprise Server.
|
forbid a subset of licenses. These options are not supported on GHES.
|
||||||
|
|
||||||
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
|
||||||
@@ -278,9 +148,8 @@ to filter. A couple of examples:
|
|||||||
deny-licenses: Apache-1.1, Apache-2.0
|
deny-licenses: Apache-1.1, Apache-2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
### Considerations
|
**Important**
|
||||||
|
|
||||||
- Checking for licenses is not supported on Enterprise Server.
|
|
||||||
- The action will only accept one of the two parameters; an error will
|
- The action will only accept one of the two parameters; an error will
|
||||||
be raised if you provide both.
|
be raised if you provide both.
|
||||||
- By default both parameters are empty (no license checking is
|
- By default both parameters are empty (no license checking is
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {expect, test, beforeEach} from '@jest/globals'
|
import {expect, test, beforeEach} from '@jest/globals'
|
||||||
import {readConfig, readConfigFile} from '../src/config'
|
import {readConfig} from '../src/config'
|
||||||
import {getRefs} from '../src/git-refs'
|
import {getRefs} from '../src/git-refs'
|
||||||
|
|
||||||
// GitHub Action inputs come in the form of environment variables
|
// GitHub Action inputs come in the form of environment variables
|
||||||
@@ -13,11 +13,8 @@ function setInput(input: string, value: string) {
|
|||||||
function clearInputs() {
|
function clearInputs() {
|
||||||
const allowedOptions = [
|
const allowedOptions = [
|
||||||
'FAIL-ON-SEVERITY',
|
'FAIL-ON-SEVERITY',
|
||||||
'FAIL-ON-SCOPES',
|
|
||||||
'ALLOW-LICENSES',
|
'ALLOW-LICENSES',
|
||||||
'DENY-LICENSES',
|
'DENY-LICENSES',
|
||||||
'ALLOW-GHSAS',
|
|
||||||
'CONFIG-FILE',
|
|
||||||
'BASE-REF',
|
'BASE-REF',
|
||||||
'HEAD-REF'
|
'HEAD-REF'
|
||||||
]
|
]
|
||||||
@@ -85,93 +82,3 @@ 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 () => {
|
|
||||||
let options = readConfigFile('./__tests__/fixtures/config-allow-sample.yml')
|
|
||||||
expect(options.fail_on_severity).toEqual('critical')
|
|
||||||
expect(options.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('raises an error when the the config file was not found', async () => {
|
|
||||||
expect(() => readConfigFile('fixtures/i-dont-exist')).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it parses options from both sources', async () => {
|
|
||||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
|
||||||
|
|
||||||
let options = readConfig()
|
|
||||||
expect(options.fail_on_severity).toEqual('critical')
|
|
||||||
|
|
||||||
setInput('base-ref', 'a-custom-base-ref')
|
|
||||||
options = readConfig()
|
|
||||||
expect(options.base_ref).toEqual('a-custom-base-ref')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('in case of conflicts, the external config is the source of truth', async () => {
|
|
||||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml') // this will set fail-on-severity to 'critical'
|
|
||||||
|
|
||||||
let options = readConfig()
|
|
||||||
expect(options.fail_on_severity).toEqual('critical')
|
|
||||||
|
|
||||||
// this should not overwite the previous value
|
|
||||||
setInput('fail-on-severity', 'low')
|
|
||||||
options = readConfig()
|
|
||||||
expect(options.fail_on_severity).toEqual('critical')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it uses the default values when loading external files', async () => {
|
|
||||||
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
|
||||||
let options = readConfig()
|
|
||||||
expect(options.allow_licenses).toEqual(undefined)
|
|
||||||
expect(options.deny_licenses).toEqual(undefined)
|
|
||||||
|
|
||||||
setInput('config-file', './__tests__/fixtures/license-config-sample.yml')
|
|
||||||
options = readConfig()
|
|
||||||
expect(options.fail_on_severity).toEqual('low')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it accepts an external configuration filename', async () => {
|
|
||||||
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
|
||||||
const options = readConfig()
|
|
||||||
expect(options.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')
|
|
||||||
expect(() => readConfig()).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it defaults to runtime scope', async () => {
|
|
||||||
const options = readConfig()
|
|
||||||
expect(options.fail_on_scopes).toEqual(['runtime'])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it parses custom scopes preference', async () => {
|
|
||||||
setInput('fail-on-scopes', 'runtime, development')
|
|
||||||
let options = readConfig()
|
|
||||||
expect(options.fail_on_scopes).toEqual(['runtime', 'development'])
|
|
||||||
|
|
||||||
clearInputs()
|
|
||||||
setInput('fail-on-scopes', 'development')
|
|
||||||
options = readConfig()
|
|
||||||
expect(options.fail_on_scopes).toEqual(['development'])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it raises an error when given invalid scope', async () => {
|
|
||||||
setInput('fail-on-scopes', 'runtime, zombies')
|
|
||||||
expect(() => readConfig()).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it defaults to an empty GHSA allowlist', async () => {
|
|
||||||
const options = readConfig()
|
|
||||||
expect(options.allow_ghsas).toEqual(undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it successfully parses GHSA allowlist', async () => {
|
|
||||||
setInput('allow-ghsas', 'GHSA-abcd-1234-5679, GHSA-efgh-1234-5679')
|
|
||||||
const options = readConfig()
|
|
||||||
expect(options.allow_ghsas).toEqual([
|
|
||||||
'GHSA-abcd-1234-5679',
|
|
||||||
'GHSA-efgh-1234-5679'
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import {expect, test} from '@jest/globals'
|
import {expect, test} from '@jest/globals'
|
||||||
import {Change, Changes} from '../src/schemas'
|
import {Change, Changes} from '../src/schemas'
|
||||||
import {
|
import {filterChangesBySeverity} from '../src/filter'
|
||||||
filterChangesBySeverity,
|
|
||||||
filterChangesByScopes,
|
|
||||||
filterOutAllowedAdvisories
|
|
||||||
} from '../src/filter'
|
|
||||||
|
|
||||||
let npmChange: Change = {
|
let npmChange: Change = {
|
||||||
manifest: 'package.json',
|
manifest: 'package.json',
|
||||||
@@ -15,7 +11,6 @@ let npmChange: Change = {
|
|||||||
package_url: 'pkg:npm/[email protected]',
|
package_url: 'pkg:npm/[email protected]',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
source_repository_url: 'github.com/some-repo',
|
source_repository_url: 'github.com/some-repo',
|
||||||
scope: 'runtime',
|
|
||||||
vulnerabilities: [
|
vulnerabilities: [
|
||||||
{
|
{
|
||||||
severity: 'critical',
|
severity: 'critical',
|
||||||
@@ -35,7 +30,6 @@ let rubyChange: Change = {
|
|||||||
package_url: 'pkg:gem/[email protected]',
|
package_url: 'pkg:gem/[email protected]',
|
||||||
license: 'BSD',
|
license: 'BSD',
|
||||||
source_repository_url: 'github.com/some-repo',
|
source_repository_url: 'github.com/some-repo',
|
||||||
scope: 'development',
|
|
||||||
vulnerabilities: [
|
vulnerabilities: [
|
||||||
{
|
{
|
||||||
severity: 'moderate',
|
severity: 'moderate',
|
||||||
@@ -52,19 +46,6 @@ let rubyChange: Change = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
let noVulnNpmChange: Change = {
|
|
||||||
manifest: 'package.json',
|
|
||||||
change_type: 'added',
|
|
||||||
ecosystem: 'npm',
|
|
||||||
name: 'helpful',
|
|
||||||
version: '1.0.0',
|
|
||||||
package_url: 'pkg:npm/[email protected]',
|
|
||||||
license: 'MIT',
|
|
||||||
source_repository_url: 'github.com/some-repo',
|
|
||||||
scope: 'runtime',
|
|
||||||
vulnerabilities: []
|
|
||||||
}
|
|
||||||
|
|
||||||
test('it properly filters changes by severity', async () => {
|
test('it properly filters changes by severity', async () => {
|
||||||
const changes = [npmChange, rubyChange]
|
const changes = [npmChange, rubyChange]
|
||||||
let result = filterChangesBySeverity('high', changes)
|
let result = filterChangesBySeverity('high', changes)
|
||||||
@@ -76,42 +57,3 @@ test('it properly filters changes by severity', async () => {
|
|||||||
result = filterChangesBySeverity('critical', changes)
|
result = filterChangesBySeverity('critical', changes)
|
||||||
expect(changes).toEqual([npmChange, rubyChange])
|
expect(changes).toEqual([npmChange, rubyChange])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it properly filters changes by scope', async () => {
|
|
||||||
const changes = [npmChange, rubyChange]
|
|
||||||
|
|
||||||
let result = filterChangesByScopes(['runtime'], changes)
|
|
||||||
expect(result).toEqual([npmChange])
|
|
||||||
|
|
||||||
result = filterChangesByScopes(['development'], changes)
|
|
||||||
expect(result).toEqual([rubyChange])
|
|
||||||
|
|
||||||
result = filterChangesByScopes(['runtime', 'development'], changes)
|
|
||||||
expect(result).toEqual([npmChange, rubyChange])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it properly filters changes with allowed vulnerabilities', async () => {
|
|
||||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
|
||||||
|
|
||||||
let result = filterOutAllowedAdvisories(['notrealGHSAID'], changes)
|
|
||||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
|
||||||
|
|
||||||
result = filterOutAllowedAdvisories(['first-random_string'], changes)
|
|
||||||
expect(result).toEqual([rubyChange, noVulnNpmChange])
|
|
||||||
|
|
||||||
result = filterOutAllowedAdvisories(
|
|
||||||
['second-random_string', 'third-random_string'],
|
|
||||||
changes
|
|
||||||
)
|
|
||||||
expect(result).toEqual([npmChange, noVulnNpmChange])
|
|
||||||
|
|
||||||
result = filterOutAllowedAdvisories(
|
|
||||||
['first-random_string', 'second-random_string', 'third-random_string'],
|
|
||||||
changes
|
|
||||||
)
|
|
||||||
expect(result).toEqual([noVulnNpmChange])
|
|
||||||
|
|
||||||
// if we have a change with multiple vulnerabilities but only one is allowed, we still should not filter out that change
|
|
||||||
result = filterOutAllowedAdvisories(['second-random_string'], changes)
|
|
||||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
fail-on-severity: 'so many zombies'
|
|
||||||
deny-licenses:
|
|
||||||
- MIT
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
allow_licenses: ['MIT', 'GPL 2']
|
|
||||||
@@ -11,7 +11,6 @@ let npmChange: Change = {
|
|||||||
package_url: 'pkg:npm/[email protected]',
|
package_url: 'pkg:npm/[email protected]',
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
source_repository_url: 'github.com/some-repo',
|
source_repository_url: 'github.com/some-repo',
|
||||||
scope: 'runtime',
|
|
||||||
vulnerabilities: [
|
vulnerabilities: [
|
||||||
{
|
{
|
||||||
severity: 'critical',
|
severity: 'critical',
|
||||||
@@ -31,7 +30,6 @@ let rubyChange: Change = {
|
|||||||
package_url: 'pkg:gem/[email protected]',
|
package_url: 'pkg:gem/[email protected]',
|
||||||
license: 'BSD',
|
license: 'BSD',
|
||||||
source_repository_url: 'github.com/some-repo',
|
source_repository_url: 'github.com/some-repo',
|
||||||
scope: 'runtime',
|
|
||||||
vulnerabilities: [
|
vulnerabilities: [
|
||||||
{
|
{
|
||||||
severity: 'moderate',
|
severity: 'moderate',
|
||||||
|
|||||||
-10
@@ -10,28 +10,18 @@ inputs:
|
|||||||
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'
|
default: 'low'
|
||||||
fail-on-scopes:
|
|
||||||
description: Dependency scopes to block PRs on. Comma-separated list. Possible values are 'unknown', 'runtime', and 'development' (e.g. "runtime, development")
|
|
||||||
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
|
||||||
head-ref:
|
head-ref:
|
||||||
description: The head 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 head 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
|
||||||
config-file:
|
|
||||||
description: A filepath to the configuration file for the action.
|
|
||||||
required: false
|
|
||||||
allow-licenses:
|
allow-licenses:
|
||||||
description: Comma-separated list of allowed licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
description: Comma-separated list of allowed licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
||||||
required: false
|
required: false
|
||||||
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-ghsas:
|
|
||||||
description: Comma-separated list of allowed Github Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
|
|
||||||
required: false
|
|
||||||
runs:
|
runs:
|
||||||
using: 'node16'
|
using: 'node16'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
|
|||||||
+236
-8780
File diff suppressed because it is too large
Load Diff
+1
-1
File diff suppressed because one or more lines are too long
-17
@@ -697,23 +697,6 @@ 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.
|
||||||
|
|
||||||
|
|
||||||
yaml
|
|
||||||
ISC
|
|
||||||
Copyright Eemeli Aro <[email protected]>
|
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
||||||
with or without fee is hereby granted, provided that the above copyright notice
|
|
||||||
and this permission notice appear in all copies.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
||||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
||||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
||||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
||||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
||||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
||||||
THIS SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
zod
|
zod
|
||||||
MIT
|
MIT
|
||||||
MIT License
|
MIT License
|
||||||
|
|||||||
Generated
+458
-420
File diff suppressed because it is too large
Load Diff
+15
-16
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dependency-review-action",
|
"name": "dependency-review-action",
|
||||||
"version": "2.4.0",
|
"version": "2.0.4",
|
||||||
"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",
|
||||||
@@ -25,31 +25,30 @@
|
|||||||
"author": "GitHub",
|
"author": "GitHub",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.9.1",
|
||||||
"@actions/github": "^5.1.1",
|
"@actions/github": "^5.0.3",
|
||||||
"@octokit/plugin-retry": "^3.0.9",
|
"@octokit/plugin-retry": "^3.0.9",
|
||||||
"@octokit/request-error": "^3.0.1",
|
"@octokit/request-error": "^3.0.1",
|
||||||
"ansi-styles": "^6.1.1",
|
"ansi-styles": "^6.1.0",
|
||||||
"got": "^12.5.1",
|
"got": "^12.3.1",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^2.0.19",
|
||||||
"spdx-satisfies": "^5.0.1",
|
"yaml": "^2.1.1",
|
||||||
"yaml": "^2.1.2",
|
"zod": "^3.18.0"
|
||||||
"zod": "^3.19.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^16.11.63",
|
"@types/node": "^16.11.49",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
||||||
"@typescript-eslint/parser": "^5.38.1",
|
"@typescript-eslint/parser": "^5.33.1",
|
||||||
"@vercel/ncc": "^0.34.0",
|
"@vercel/ncc": "^0.34.0",
|
||||||
"esbuild-register": "^3.3.3",
|
"esbuild-register": "^3.3.3",
|
||||||
"eslint": "^8.24.0",
|
"eslint": "^8.22.0",
|
||||||
"eslint-plugin-github": "^4.3.7",
|
"eslint-plugin-github": "^4.3.7",
|
||||||
"eslint-plugin-jest": "^27.0.4",
|
"eslint-plugin-jest": "^26.8.3",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^2.0.19",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
"ts-jest": "^27.1.4",
|
"ts-jest": "^27.1.4",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-12
@@ -32,25 +32,17 @@ event_file = Tempfile.new
|
|||||||
event_file.write("{ \"pull_request\": #{pr.to_h.to_json}}")
|
event_file.write("{ \"pull_request\": #{pr.to_h.to_json}}")
|
||||||
event_file.close
|
event_file.close
|
||||||
|
|
||||||
action_inputs = {
|
|
||||||
"repo-token" => github_token
|
|
||||||
}
|
|
||||||
|
|
||||||
dev_cmd_env = {
|
dev_cmd_env = {
|
||||||
|
"INPUT_REPO-TOKEN" => github_token,
|
||||||
"GITHUB_REPOSITORY" => repo_nwo,
|
"GITHUB_REPOSITORY" => repo_nwo,
|
||||||
"GITHUB_EVENT_NAME" => "pull_request",
|
"GITHUB_EVENT_NAME" => "pull_request",
|
||||||
"GITHUB_EVENT_PATH" => event_file.path,
|
"GITHUB_EVENT_PATH" => event_file.path
|
||||||
"GITHUB_STEP_SUMMARY" => "/dev/null"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# bash does not like variable names with dashes like the ones Actions
|
dev_cmd = "./node_modules/.bin/nodemon --exec \"node -r esbuild-register\" src/main.ts"
|
||||||
# uses (e.g. INPUT_REPO-TOKEN). Passing them through `env` instead of
|
|
||||||
# manually setting them does the job.
|
|
||||||
action_inputs_env_str = action_inputs.map { |name, value| "\"INPUT_#{name.upcase}=#{value}\"" }.join(" ")
|
|
||||||
dev_cmd = "./node_modules/.bin/nodemon --exec \"env #{action_inputs_env_str} node -r esbuild-register\" src/main.ts"
|
|
||||||
|
|
||||||
Open3.popen2e(dev_cmd_env, dev_cmd) do |stdin, out|
|
Open3.popen2e(dev_cmd_env, dev_cmd) do |stdin, out|
|
||||||
while line = out.gets
|
while line = out.gets
|
||||||
puts line.gsub(github_token, "<REDACTED>")
|
puts line
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+9
-69
@@ -1,92 +1,32 @@
|
|||||||
import * as fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import YAML from 'yaml'
|
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import {
|
import {ConfigurationOptions, SEVERITIES} from './schemas'
|
||||||
ConfigurationOptions,
|
|
||||||
ConfigurationOptionsSchema,
|
|
||||||
SeveritySchema,
|
|
||||||
SCOPES
|
|
||||||
} from './schemas'
|
|
||||||
|
|
||||||
function getOptionalInput(name: string): string | undefined {
|
function getOptionalInput(name: string): string | undefined {
|
||||||
const value = core.getInput(name)
|
const value = core.getInput(name)
|
||||||
return value.length > 0 ? value : undefined
|
return value.length > 0 ? value : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseList(list: string | undefined): string[] | undefined {
|
|
||||||
if (list === undefined) {
|
|
||||||
return list
|
|
||||||
} else {
|
|
||||||
return list.split(',').map(x => x.trim())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readConfig(): ConfigurationOptions {
|
export function readConfig(): ConfigurationOptions {
|
||||||
const externalConfig = getOptionalInput('config-file')
|
const fail_on_severity = z
|
||||||
if (externalConfig !== undefined) {
|
.enum(SEVERITIES)
|
||||||
const config = readConfigFile(externalConfig)
|
.default('low')
|
||||||
// the reasoning behind reading the inline config when an external
|
.parse(getOptionalInput('fail-on-severity'))
|
||||||
// config file is provided is that we still want to allow users to
|
const allow_licenses = getOptionalInput('allow-licenses')
|
||||||
// pass inline options in the presence of an external config file.
|
const deny_licenses = getOptionalInput('deny-licenses')
|
||||||
const inlineConfig = readInlineConfig()
|
|
||||||
// the external config takes precedence
|
|
||||||
return Object.assign({}, inlineConfig, config)
|
|
||||||
} else {
|
|
||||||
return readInlineConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readInlineConfig(): ConfigurationOptions {
|
|
||||||
const fail_on_severity = SeveritySchema.parse(
|
|
||||||
getOptionalInput('fail-on-severity')
|
|
||||||
)
|
|
||||||
const fail_on_scopes = z
|
|
||||||
.array(z.enum(SCOPES))
|
|
||||||
.default(['runtime'])
|
|
||||||
.parse(parseList(getOptionalInput('fail-on-scopes')))
|
|
||||||
|
|
||||||
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
|
|
||||||
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
|
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
|
|
||||||
|
|
||||||
const base_ref = getOptionalInput('base-ref')
|
const base_ref = getOptionalInput('base-ref')
|
||||||
const head_ref = getOptionalInput('head-ref')
|
const head_ref = getOptionalInput('head-ref')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fail_on_severity,
|
fail_on_severity,
|
||||||
fail_on_scopes,
|
allow_licenses: allow_licenses?.split(',').map(x => x.trim()),
|
||||||
allow_licenses,
|
deny_licenses: deny_licenses?.split(',').map(x => x.trim()),
|
||||||
deny_licenses,
|
|
||||||
allow_ghsas,
|
|
||||||
base_ref,
|
base_ref,
|
||||||
head_ref
|
head_ref
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readConfigFile(filePath: string): ConfigurationOptions {
|
|
||||||
let data
|
|
||||||
|
|
||||||
try {
|
|
||||||
data = fs.readFileSync(path.resolve(filePath), 'utf-8')
|
|
||||||
} catch (error: unknown) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
data = YAML.parse(data)
|
|
||||||
|
|
||||||
// get rid of the ugly dashes from the actions conventions
|
|
||||||
for (const key of Object.keys(data)) {
|
|
||||||
if (key.includes('-')) {
|
|
||||||
data[key.replace(/-/g, '_')] = data[key]
|
|
||||||
delete data[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const values = ConfigurationOptionsSchema.parse(data)
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-50
@@ -1,4 +1,4 @@
|
|||||||
import {Changes, Severity, SEVERITIES, Scope} from './schemas'
|
import {Changes, Severity, SEVERITIES} from './schemas'
|
||||||
|
|
||||||
export function filterChangesBySeverity(
|
export function filterChangesBySeverity(
|
||||||
severity: Severity,
|
severity: Severity,
|
||||||
@@ -33,52 +33,3 @@ export function filterChangesBySeverity(
|
|||||||
)
|
)
|
||||||
return filteredChanges
|
return filteredChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterChangesByScopes(
|
|
||||||
scopes: Scope[] | undefined,
|
|
||||||
changes: Changes
|
|
||||||
): Changes {
|
|
||||||
if (scopes === undefined) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredChanges = changes.filter(change => {
|
|
||||||
// if there is no scope on the change (Enterprise Server API for now), we will assume it is a runtime scope
|
|
||||||
const scope = change.scope || 'runtime'
|
|
||||||
return scopes.includes(scope)
|
|
||||||
})
|
|
||||||
|
|
||||||
return filteredChanges
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterOutAllowedAdvisories(
|
|
||||||
ghsas: string[] | undefined,
|
|
||||||
changes: Changes
|
|
||||||
): Changes {
|
|
||||||
if (ghsas === undefined) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredChanges = changes.filter(change => {
|
|
||||||
const noAdvisories =
|
|
||||||
change.vulnerabilities === undefined ||
|
|
||||||
change.vulnerabilities.length === 0
|
|
||||||
|
|
||||||
if (noAdvisories) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let allAllowedAdvisories = true
|
|
||||||
// if there's at least one advisory that is not allowlisted, we will keep the change
|
|
||||||
for (const vulnerability of change.vulnerabilities) {
|
|
||||||
if (!ghsas.includes(vulnerability.advisory_ghsa_id)) {
|
|
||||||
allAllowedAdvisories = false
|
|
||||||
}
|
|
||||||
if (!allAllowedAdvisories) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return filteredChanges
|
|
||||||
}
|
|
||||||
|
|||||||
+50
-113
@@ -3,19 +3,13 @@ import * as dependencyGraph from './dependency-graph'
|
|||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import styles from 'ansi-styles'
|
import styles from 'ansi-styles'
|
||||||
import {RequestError} from '@octokit/request-error'
|
import {RequestError} from '@octokit/request-error'
|
||||||
import {Change, Severity, Changes} from './schemas'
|
import {Change, Severity} from './schemas'
|
||||||
import {readConfig} from '../src/config'
|
import {readConfig} from '../src/config'
|
||||||
import {
|
import {filterChangesBySeverity} from '../src/filter'
|
||||||
filterChangesBySeverity,
|
|
||||||
filterChangesByScopes,
|
|
||||||
filterOutAllowedAdvisories
|
|
||||||
} from '../src/filter'
|
|
||||||
import {getDeniedLicenseChanges} from './licenses'
|
import {getDeniedLicenseChanges} from './licenses'
|
||||||
import * as summary from './summary'
|
import * as summary from './summary'
|
||||||
import {getRefs} from './git-refs'
|
import {getRefs} from './git-refs'
|
||||||
|
|
||||||
import {groupDependenciesByManifest} from './utils'
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const config = readConfig()
|
const config = readConfig()
|
||||||
@@ -28,16 +22,17 @@ async function run(): Promise<void> {
|
|||||||
headRef: refs.head
|
headRef: refs.head
|
||||||
})
|
})
|
||||||
|
|
||||||
const minSeverity = config.fail_on_severity as Severity
|
const minSeverity = config.fail_on_severity
|
||||||
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
|
let failed = false
|
||||||
const filteredChanges = filterOutAllowedAdvisories(
|
|
||||||
config.allow_ghsas,
|
const licenses = {
|
||||||
scopedChanges
|
allow: config.allow_licenses,
|
||||||
)
|
deny: config.deny_licenses
|
||||||
|
}
|
||||||
|
|
||||||
const addedChanges = filterChangesBySeverity(
|
const addedChanges = filterChangesBySeverity(
|
||||||
minSeverity,
|
minSeverity as Severity,
|
||||||
filteredChanges
|
changes
|
||||||
).filter(
|
).filter(
|
||||||
change =>
|
change =>
|
||||||
change.change_type === 'added' &&
|
change.change_type === 'added' &&
|
||||||
@@ -46,21 +41,37 @@ async function run(): Promise<void> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const [licenseErrors, unknownLicenses] = getDeniedLicenseChanges(
|
const [licenseErrors, unknownLicenses] = getDeniedLicenseChanges(
|
||||||
filteredChanges,
|
changes,
|
||||||
{
|
licenses
|
||||||
allow: config.allow_licenses,
|
|
||||||
deny: config.deny_licenses
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses)
|
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses)
|
||||||
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
|
|
||||||
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config)
|
|
||||||
summary.addScannedDependencies(changes)
|
|
||||||
|
|
||||||
printVulnerabilitiesBlock(addedChanges, minSeverity)
|
if (addedChanges.length > 0) {
|
||||||
printLicensesBlock(licenseErrors, unknownLicenses)
|
for (const change of addedChanges) {
|
||||||
printScannedDependencies(changes)
|
printChangeVulnerabilities(change)
|
||||||
|
}
|
||||||
|
failed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity || '')
|
||||||
|
|
||||||
|
if (licenseErrors.length > 0) {
|
||||||
|
printLicensesError(licenseErrors)
|
||||||
|
core.setFailed('Dependency review detected incompatible licenses.')
|
||||||
|
}
|
||||||
|
|
||||||
|
printNullLicenses(unknownLicenses)
|
||||||
|
|
||||||
|
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config)
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
core.setFailed('Dependency review detected vulnerable packages.')
|
||||||
|
} else {
|
||||||
|
core.info(
|
||||||
|
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError && error.status === 404) {
|
if (error instanceof RequestError && error.status === 404) {
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
@@ -82,29 +93,6 @@ async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printVulnerabilitiesBlock(
|
|
||||||
addedChanges: Change[],
|
|
||||||
minSeverity: Severity
|
|
||||||
): void {
|
|
||||||
let failed = false
|
|
||||||
core.group('Vulnerabilities', async () => {
|
|
||||||
if (addedChanges.length > 0) {
|
|
||||||
for (const change of addedChanges) {
|
|
||||||
printChangeVulnerabilities(change)
|
|
||||||
}
|
|
||||||
failed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed) {
|
|
||||||
core.setFailed('Dependency review detected vulnerable packages.')
|
|
||||||
} else {
|
|
||||||
core.info(
|
|
||||||
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function printChangeVulnerabilities(change: Change): void {
|
function printChangeVulnerabilities(change: Change): void {
|
||||||
for (const vuln of change.vulnerabilities) {
|
for (const vuln of change.vulnerabilities) {
|
||||||
core.info(
|
core.info(
|
||||||
@@ -118,17 +106,18 @@ function printChangeVulnerabilities(change: Change): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printLicensesBlock(
|
function renderSeverity(
|
||||||
licenseErrors: Change[],
|
severity: 'critical' | 'high' | 'moderate' | 'low'
|
||||||
unknownLicenses: Change[]
|
): string {
|
||||||
): void {
|
const color = (
|
||||||
core.group('Licenses', async () => {
|
{
|
||||||
if (licenseErrors.length > 0) {
|
critical: 'red',
|
||||||
printLicensesError(licenseErrors)
|
high: 'red',
|
||||||
core.setFailed('Dependency review detected incompatible licenses.')
|
moderate: 'yellow',
|
||||||
}
|
low: 'grey'
|
||||||
printNullLicenses(unknownLicenses)
|
} as const
|
||||||
})
|
)[severity]
|
||||||
|
return `${styles.color[color].open}(${severity} severity)${styles.color[color].close}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function printLicensesError(changes: Change[]): void {
|
function printLicensesError(changes: Change[]): void {
|
||||||
@@ -157,56 +146,4 @@ function printNullLicenses(changes: Change[]): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSeverity(
|
|
||||||
severity: 'critical' | 'high' | 'moderate' | 'low'
|
|
||||||
): string {
|
|
||||||
const color = (
|
|
||||||
{
|
|
||||||
critical: 'red',
|
|
||||||
high: 'red',
|
|
||||||
moderate: 'yellow',
|
|
||||||
low: 'grey'
|
|
||||||
} as const
|
|
||||||
)[severity]
|
|
||||||
return `${styles.color[color].open}(${severity} severity)${styles.color[color].close}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderScannedDependency(change: Change): string {
|
|
||||||
const changeType: string = change.change_type
|
|
||||||
|
|
||||||
if (changeType !== 'added' && changeType !== 'removed') {
|
|
||||||
throw new Error(`Unexpected change type: ${changeType}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = (
|
|
||||||
{
|
|
||||||
added: 'green',
|
|
||||||
removed: 'red'
|
|
||||||
} as const
|
|
||||||
)[changeType]
|
|
||||||
|
|
||||||
const icon = (
|
|
||||||
{
|
|
||||||
added: '+',
|
|
||||||
removed: '-'
|
|
||||||
} as const
|
|
||||||
)[changeType]
|
|
||||||
|
|
||||||
return `${styles.color[color].open}${icon} ${change.manifest}@${change.version}${styles.color[color].close}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function printScannedDependencies(changes: Changes): void {
|
|
||||||
core.group('Dependency Changes', async () => {
|
|
||||||
const dependencies = groupDependenciesByManifest(changes)
|
|
||||||
|
|
||||||
for (const manifestName of dependencies.keys()) {
|
|
||||||
const manifestChanges = dependencies.get(manifestName) || []
|
|
||||||
core.info(`File: ${styles.bold.open}${manifestName}${styles.bold.close}`)
|
|
||||||
for (const change of manifestChanges) {
|
|
||||||
core.info(`${renderScannedDependency(change)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
|||||||
+3
-11
@@ -1,9 +1,6 @@
|
|||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
export const SEVERITIES = ['critical', 'high', 'moderate', 'low'] as const
|
export const SEVERITIES = ['critical', 'high', 'moderate', 'low'] as const
|
||||||
export const SCOPES = ['unknown', 'runtime', 'development'] as const
|
|
||||||
|
|
||||||
export const SeveritySchema = z.enum(SEVERITIES).default('low')
|
|
||||||
|
|
||||||
export const ChangeSchema = z.object({
|
export const ChangeSchema = z.object({
|
||||||
change_type: z.enum(['added', 'removed']),
|
change_type: z.enum(['added', 'removed']),
|
||||||
@@ -14,11 +11,10 @@ export const ChangeSchema = z.object({
|
|||||||
package_url: z.string(),
|
package_url: z.string(),
|
||||||
license: z.string().nullable(),
|
license: z.string().nullable(),
|
||||||
source_repository_url: z.string().nullable(),
|
source_repository_url: z.string().nullable(),
|
||||||
scope: z.enum(SCOPES).optional(),
|
|
||||||
vulnerabilities: z
|
vulnerabilities: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
severity: SeveritySchema,
|
severity: z.enum(['critical', 'high', 'moderate', 'low']),
|
||||||
advisory_ghsa_id: z.string(),
|
advisory_ghsa_id: z.string(),
|
||||||
advisory_summary: z.string(),
|
advisory_summary: z.string(),
|
||||||
advisory_url: z.string()
|
advisory_url: z.string()
|
||||||
@@ -36,12 +32,9 @@ export const PullRequestSchema = z.object({
|
|||||||
|
|
||||||
export const ConfigurationOptionsSchema = z
|
export const ConfigurationOptionsSchema = z
|
||||||
.object({
|
.object({
|
||||||
fail_on_severity: SeveritySchema,
|
fail_on_severity: z.enum(SEVERITIES).default('low'),
|
||||||
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
|
||||||
allow_licenses: z.array(z.string()).default([]),
|
allow_licenses: z.array(z.string()).default([]),
|
||||||
deny_licenses: z.array(z.string()).default([]),
|
deny_licenses: z.array(z.string()).default([]),
|
||||||
allow_ghsas: z.array(z.string()).default([]),
|
|
||||||
config_file: z.string().optional().default('false'),
|
|
||||||
base_ref: z.string(),
|
base_ref: z.string(),
|
||||||
head_ref: z.string()
|
head_ref: z.string()
|
||||||
})
|
})
|
||||||
@@ -56,5 +49,4 @@ export const ChangesSchema = z.array(ChangeSchema)
|
|||||||
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 ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
|
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
|
||||||
export type Severity = z.infer<typeof SeveritySchema>
|
export type Severity = typeof SEVERITIES[number]
|
||||||
export type Scope = typeof SCOPES[number]
|
|
||||||
|
|||||||
+11
-19
@@ -1,7 +1,6 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {ConfigurationOptions, Change, Changes} from './schemas'
|
import {ConfigurationOptions, Change, Changes} from './schemas'
|
||||||
import {SummaryTableRow} from '@actions/core/lib/summary'
|
import {SummaryTableRow} from '@actions/core/lib/summary'
|
||||||
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
|
||||||
|
|
||||||
export function addSummaryToSummary(
|
export function addSummaryToSummary(
|
||||||
addedPackages: Changes,
|
addedPackages: Changes,
|
||||||
@@ -21,7 +20,7 @@ export function addChangeVulnerabilitiesToSummary(
|
|||||||
): void {
|
): void {
|
||||||
const rows: SummaryTableRow[] = []
|
const rows: SummaryTableRow[] = []
|
||||||
|
|
||||||
const manifests = getManifestsSet(addedPackages)
|
const manifests = getManifests(addedPackages)
|
||||||
|
|
||||||
core.summary
|
core.summary
|
||||||
.addHeading('Vulnerabilities')
|
.addHeading('Vulnerabilities')
|
||||||
@@ -100,7 +99,7 @@ export function addLicensesToSummary(
|
|||||||
|
|
||||||
if (licenseErrors.length > 0) {
|
if (licenseErrors.length > 0) {
|
||||||
const rows: SummaryTableRow[] = []
|
const rows: SummaryTableRow[] = []
|
||||||
const manifests = getManifestsSet(licenseErrors)
|
const manifests = getManifests(licenseErrors)
|
||||||
|
|
||||||
core.summary.addHeading('Incompatible Licenses', 3).addSeparator()
|
core.summary.addHeading('Incompatible Licenses', 3).addSeparator()
|
||||||
|
|
||||||
@@ -126,7 +125,7 @@ export function addLicensesToSummary(
|
|||||||
|
|
||||||
if (unknownLicenses.length > 0) {
|
if (unknownLicenses.length > 0) {
|
||||||
const rows: SummaryTableRow[] = []
|
const rows: SummaryTableRow[] = []
|
||||||
const manifests = getManifestsSet(unknownLicenses)
|
const manifests = getManifests(unknownLicenses)
|
||||||
|
|
||||||
core.debug(
|
core.debug(
|
||||||
`found ${manifests.entries.length} manifests for unknown licenses`
|
`found ${manifests.entries.length} manifests for unknown licenses`
|
||||||
@@ -151,21 +150,14 @@ export function addLicensesToSummary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addScannedDependencies(changes: Changes): void {
|
function getManifests(changes: Changes): Set<string> {
|
||||||
const dependencies = groupDependenciesByManifest(changes)
|
return new Set(changes.flatMap(c => c.manifest))
|
||||||
const manifests = dependencies.keys()
|
}
|
||||||
|
|
||||||
const summary = core.summary
|
function renderUrl(url: string | null, text: string): string {
|
||||||
.addHeading('Scanned Dependencies')
|
if (url) {
|
||||||
.addRaw(`We scanned ${dependencies.size} manifest files:`)
|
return `<a href="${url}">${text}</a>`
|
||||||
|
} else {
|
||||||
for (const manifest of manifests) {
|
return text
|
||||||
const deps = dependencies.get(manifest)
|
|
||||||
if (deps) {
|
|
||||||
const dependencyNames = deps.map(
|
|
||||||
dependency => `<li>${dependency.name}@${dependency.version}</li>`
|
|
||||||
)
|
|
||||||
summary.addRaw(`<h3>${manifest}</h3><ul>${dependencyNames.join('')}</ul>`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import {Changes} from './schemas'
|
|
||||||
|
|
||||||
export function groupDependenciesByManifest(
|
|
||||||
changes: Changes
|
|
||||||
): Map<string, Changes> {
|
|
||||||
const dependencies: Map<string, Changes> = new Map()
|
|
||||||
for (const change of changes) {
|
|
||||||
const manifestName = change.manifest
|
|
||||||
|
|
||||||
if (dependencies.get(manifestName) === undefined) {
|
|
||||||
dependencies.set(manifestName, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies.get(manifestName)?.push(change)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getManifestsSet(changes: Changes): Set<string> {
|
|
||||||
return new Set(changes.flatMap(c => c.manifest))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderUrl(url: string | null, text: string): string {
|
|
||||||
if (url) {
|
|
||||||
return `<a href="${url}">${text}</a>`
|
|
||||||
} else {
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user