Compare commits

..
1 Commits
Author SHA1 Message Date
Federico Builes 20f8e76960 Merge branch 'main' into add-summary
# Conflicts:
#	README.md
#	dist/index.js
#	dist/index.js.map
#	src/main.ts
2022-08-18 15:48:03 +02:00
22 changed files with 1052 additions and 29718 deletions
-1
View File
@@ -1,5 +1,4 @@
event.json event.json
.ruby-version
# Dependency directory # Dependency directory
node_modules node_modules
+17 -26
View File
@@ -1,5 +1,4 @@
# Contributing # Contributing
[fork]: https://github.com/actions/dependency-review-action/fork [fork]: https://github.com/actions/dependency-review-action/fork
[pr]: https://github.com/actions/dependency-review-action/compare [pr]: https://github.com/actions/dependency-review-action/compare
[code-of-conduct]: CODE_OF_CONDUCT.md [code-of-conduct]: CODE_OF_CONDUCT.md
@@ -10,6 +9,7 @@ Contributions to this project are
[released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license)
to the public under the [project's open source license](LICENSE). to the public under the [project's open source license](LICENSE).
Please note that this project is released with a [Contributor Code of Please note that this project is released with a [Contributor Code of
Conduct][code-of-conduct]. By participating in this project you agree Conduct][code-of-conduct]. By participating in this project you agree
to abide by its terms. to abide by its terms.
@@ -20,6 +20,7 @@ This Action makes an authenticated query to the Dependency Graph Diff
API endpoint (`GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}`) API endpoint (`GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}`)
to find out the set of added and removed dependencies for each manifest. to find out the set of added and removed dependencies for each manifest.
### Bootstrapping the project ### Bootstrapping the project
``` ```
@@ -34,7 +35,7 @@ npm install
npm run test npm run test
``` ```
_Note_: We don't have any useful tests yet, contributions are welcome! *Note*: We don't have any useful tests yet, contributions are welcome!
## Local Development ## Local Development
@@ -55,24 +56,16 @@ Like this:
$ GITHUB_TOKEN=my-secret-token ./scripts/scan_pr https://github.com/actions/dependency-review-action/pull/3 $ GITHUB_TOKEN=my-secret-token ./scripts/scan_pr https://github.com/actions/dependency-review-action/pull/3
``` ```
[Configuration options](README.md#configuration-options) can be set by
passing an external YAML [configuration file](README.md#configuration-file) to the
`scan_pr` script with the `-c`/`--config-file` option:
```sh
$ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_url>
```
## Submitting a pull request ## Submitting a pull request
0. [Fork][fork] and clone the repository 0. [Fork][fork] and clone the repository
1. Configure and install the dependencies: `npm install` 0. Configure and install the dependencies: `npm install`
2. Make sure the tests pass on your machine: `npm run test` 0. Make sure the tests pass on your machine: `npm run test`
3. Create a new branch: `git checkout -b my-branch-name` 0. Create a new branch: `git checkout -b my-branch-name`
4. Make your change, add tests, and make sure the tests still pass 0. Make your change, add tests, and make sure the tests still pass
5. Make sure to build and package before pushing: `npm run build && npm run package` 0. Make sure to build and package before pushing: `npm run build && npm run package`
6. Push to your fork and [submit a pull request][pr] 0. Push to your fork and [submit a pull request][pr]
7. Pat your self on the back and wait for your pull request to be reviewed and merged. 0. Pat your self on the back and wait for your pull request to be reviewed and merged.
Here are a few things you can do that will increase the likelihood of your pull request being accepted: Here are a few things you can do that will increase the likelihood of your pull request being accepted:
@@ -82,23 +75,22 @@ 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.
1. Make sure that the `Publish this Action to the GitHub Marketplace` 2. Make sure that the `Publish this Action to the GitHub Marketplace`
checkbox is enabled checkbox is enabled
<img width="481" alt="Screenshot 2022-06-15 at 12 08 19" src="https://user-images.githubusercontent.com/2161/173822484-4b60d8b4-c674-4bff-b5ff-b0c4a3650ab7.png"> <img width="481" alt="Screenshot 2022-06-15 at 12 08 19" src="https://user-images.githubusercontent.com/2161/173822484-4b60d8b4-c674-4bff-b5ff-b0c4a3650ab7.png">
3. Click "Choose a tag" and then "Create new tag", where the tag name 3. Click "Choose a tag" and then "Create new tag", where the tag name
will be your version prefixed by a `v` (e.g. `v1.2.3`). will be your version prefixed by a `v` (e.g. `v1.2.3`).
4. Use a version number for the release title (e.g. "1.2.3"). 4. Use a version number for the release title (e.g. "1.2.3").
<img width="700" alt="Screenshot 2022-06-15 at 12 08 36" src="https://user-images.githubusercontent.com/2161/173822548-33ab3432-d679-4dc1-adf8-b50fdaf47de3.png"> <img width="700" alt="Screenshot 2022-06-15 at 12 08 36" src="https://user-images.githubusercontent.com/2161/173822548-33ab3432-d679-4dc1-adf8-b50fdaf47de3.png">
5. Add your release notes. If this is a major version make sure to 5. Add your release notes. If this is a major version make sure to
include a small description of the biggest changes in the new version. include a small description of the biggest changes in the new version.
6. Click "Publish Release". 6. Click "Publish Release".
You now have a tag and release using the semver version you used You now have a tag and release using the semver version you used
@@ -108,8 +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
+26 -157
View File
@@ -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 -94
View File
@@ -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 -65
View File
@@ -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,
filterAllowedAdvisories
} 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,48 +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 handles undefined advisory IDs', async () => {
const changes = [npmChange, rubyChange, noVulnNpmChange]
let result = filterAllowedAdvisories(undefined, changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
})
test('it properly filters changes with allowed vulnerabilities', async () => {
const changes = [npmChange, rubyChange, noVulnNpmChange]
let result = filterAllowedAdvisories(['notrealGHSAID'], changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
result = filterAllowedAdvisories(['first-random_string'], changes)
expect(result).toEqual([rubyChange, noVulnNpmChange])
result = filterAllowedAdvisories(
['second-random_string', 'third-random_string'],
changes
)
expect(result).toEqual([npmChange, noVulnNpmChange])
result = filterAllowedAdvisories(
['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 = filterAllowedAdvisories(['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']
+7 -80
View File
@@ -1,4 +1,4 @@
import {expect, jest, test} from '@jest/globals' import {expect, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas' import {Change, Changes} from '../src/schemas'
import {getDeniedLicenseChanges} from '../src/licenses' import {getDeniedLicenseChanges} from '../src/licenses'
@@ -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',
@@ -48,41 +46,15 @@ let rubyChange: Change = {
] ]
} }
jest.mock('@actions/core')
const mockOctokit = {
rest: {
licenses: {
getForRepo: jest
.fn()
.mockReturnValue({data: {license: {spdx_id: 'AGPL'}}})
}
}
}
jest.mock('octokit', () => {
return {
Octokit: class {
constructor() {
return mockOctokit
}
}
}
})
test('it fails if a license outside the allow list is found', async () => { test('it fails if a license outside the allow list is found', async () => {
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
allow: ['BSD']
})
expect(invalidChanges[0]).toBe(npmChange) expect(invalidChanges[0]).toBe(npmChange)
}) })
test('it fails if a license inside the deny list is found', async () => { test('it fails if a license inside the deny list is found', async () => {
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
const [invalidChanges] = await getDeniedLicenseChanges(changes, { const [invalidChanges] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
deny: ['BSD']
})
expect(invalidChanges[0]).toBe(rubyChange) expect(invalidChanges[0]).toBe(rubyChange)
}) })
@@ -90,7 +62,7 @@ test('it fails if a license inside the deny list is found', async () => {
// thing we want in the system. Please remove this test after refactoring. // thing we want in the system. Please remove this test after refactoring.
test('it fails all license checks when allow is provided an empty array', async () => { test('it fails all license checks when allow is provided an empty array', async () => {
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
let [invalidChanges, _] = await getDeniedLicenseChanges(changes, { let [invalidChanges, _] = getDeniedLicenseChanges(changes, {
allow: [], allow: [],
deny: ['BSD'] deny: ['BSD']
}) })
@@ -102,9 +74,7 @@ test('it does not fail if a license outside the allow list is found in removed c
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
allow: ['BSD']
})
expect(invalidChanges).toStrictEqual([]) expect(invalidChanges).toStrictEqual([])
}) })
@@ -113,9 +83,7 @@ test('it does not fail if a license inside the deny list is found in removed cha
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const [invalidChanges, _] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
deny: ['BSD']
})
expect(invalidChanges).toStrictEqual([]) expect(invalidChanges).toStrictEqual([])
}) })
@@ -125,47 +93,6 @@ test('it fails if a license outside the allow list is found in both of added and
npmChange, npmChange,
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
allow: ['BSD']
})
expect(invalidChanges).toStrictEqual([npmChange]) expect(invalidChanges).toStrictEqual([npmChange])
}) })
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 () => {
const nullLicenseChange = {
...npmChange,
license: null,
source_repository_url: 'http://github.com/some-owner/some-repo'
}
const [_, unknownChanges] = await getDeniedLicenseChanges(
[nullLicenseChange, rubyChange],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).toHaveBeenNthCalledWith(1, {
owner: 'some-owner',
repo: 'some-repo'
})
expect(unknownChanges.length).toEqual(0)
})
test('it does not call licenses API endpoint for change with null license and invalid source_repository_url ', async () => {
const [_, unknownChanges] = await getDeniedLicenseChanges(
[{...npmChange, license: null}],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unknownChanges.length).toEqual(1)
})
test('it does not call licenses API endpoint if licenses for all changes are present', async () => {
const [_, unknownChanges] = await getDeniedLicenseChanges(
[npmChange, rubyChange],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unknownChanges.length).toEqual(0)
})
})
-10
View File
@@ -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'
Generated Vendored
+318 -25395
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
-1066
View File
File diff suppressed because it is too large Load Diff
+579 -2389
View File
File diff suppressed because it is too large Load Diff
+17 -19
View File
@@ -1,6 +1,6 @@
{ {
"name": "dependency-review-action", "name": "dependency-review-action",
"version": "2.5.1", "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,32 +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.2", "@octokit/request-error": "^3.0.1",
"ansi-styles": "^6.2.1", "ansi-styles": "^6.1.0",
"got": "^12.5.2", "got": "^12.3.1",
"nodemon": "^2.0.20", "nodemon": "^2.0.19",
"octokit": "^2.0.9", "yaml": "^2.1.1",
"yaml": "^2.1.3", "zod": "^3.18.0"
"zod": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.5.2", "@types/node": "^16.11.49",
"@types/node": "^16.18.0", "@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/parser": "^5.33.1",
"@typescript-eslint/parser": "^5.40.1",
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.34.0",
"esbuild-register": "^3.3.3", "esbuild-register": "^3.3.3",
"eslint": "^8.26.0", "eslint": "^8.22.0",
"eslint-plugin-github": "^4.4.0", "eslint-plugin-github": "^4.3.7",
"eslint-plugin-jest": "^27.1.3", "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"
} }
} }
+6 -45
View File
@@ -3,52 +3,22 @@ require 'json'
require 'tempfile' require 'tempfile'
require 'open3' require 'open3'
require 'bundler/inline' require 'bundler/inline'
require 'optparse'
gemfile do gemfile do
source 'https://rubygems.org' source 'https://rubygems.org'
gem 'octokit' gem 'octokit'
end end
config_file = nil
github_token = ENV["GITHUB_TOKEN"] github_token = ENV["GITHUB_TOKEN"]
if !github_token || github_token.empty? if !github_token || github_token.empty?
puts "Please set the GITHUB_TOKEN environment variable" puts "Please set the GITHUB_TOKEN environment variable"
exit -1 exit -1
end end
op = OptionParser.new do |opts| arg = /(?<repo_nwo>[\w\-]+\/[\w\-]+)\/pull\/(?<pr_number>\d+)/.match(ARGV[0])
usage = <<EOF
Run Dependency Review on a repository.
\e[1mUsage:\e[22m
scripts/scan_pr [options] <pr_url>
\e[1mExample:\e[22m
scripts/scan_pr https://github.com/actions/dependency-review-action/pull/294
EOF
opts.banner = usage
opts.on('-c', '--config-file <FILE>', 'Use an external configuration file') do |cf|
config_file = cf
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end
op.parse!
# make sure we have a NWO somewhere in the parameters
arg = /(?<repo_nwo>[\w\-]+\/[\w\-]+)\/pull\/(?<pr_number>\d+)/.match(ARGV.join(" "))
if arg.nil? if arg.nil?
puts op puts "Usage: script/scan_pr <pr_url>"
exit -1 exit -1
end end
@@ -62,26 +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,
"config-file": config_file
}
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
View File
@@ -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 -58
View File
@@ -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,60 +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
}
/**
* Filter out changes that are allowed by the allow_ghsas config
* option. We want to remove these changes before we do any
* processing.
* @param ghsas - list of GHSA IDs to allow
* @param changes - list of changes to filter
* @returns a list of changes with the allowed GHSAs removed
*/
export function filterAllowedAdvisories(
ghsas: string[] | undefined,
changes: Changes
): Changes {
if (ghsas === undefined) {
return changes
}
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
}
+4 -65
View File
@@ -1,5 +1,3 @@
import * as core from '@actions/core'
import {Octokit} from 'octokit'
import {Change} from './schemas' import {Change} from './schemas'
/** /**
@@ -12,27 +10,21 @@ import {Change} from './schemas'
* we will ignore the deny list. * we will ignore the deny list.
* @param {Change[]} changes The list of changes to filter. * @param {Change[]} changes The list of changes to filter.
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses. * @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
* @returns {Promise<[Array.<Change>, Array.<Change>]>} A promise to a 2 element tuple. The first element is the list of denied changes and the second one is the list of changes with unknown licenses * @returns {[Array<Change>, Array<Change]} A tuple where the first element is the list of denied changes and the second one is the list of changes with unknown licenses
*/ */
export async function getDeniedLicenseChanges( export function getDeniedLicenseChanges(
changes: Change[], changes: Change[],
licenses: { licenses: {
allow?: string[] allow?: string[]
deny?: string[] deny?: string[]
} }
): Promise<[Change[], Change[]]> { ): [Change[], Change[]] {
const {allow, deny} = licenses const {allow, deny} = licenses
const disallowed: Change[] = [] const disallowed: Change[] = []
const unknown: Change[] = [] const unknown: Change[] = []
const consolidatedChanges = changes.some( for (const change of changes) {
({source_repository_url, license}) => !license && source_repository_url
)
? await setGHLicenses(changes)
: changes
for (const change of consolidatedChanges) {
if (change.change_type === 'removed') { if (change.change_type === 'removed') {
continue continue
} }
@@ -55,56 +47,3 @@ export async function getDeniedLicenseChanges(
return [disallowed, unknown] return [disallowed, unknown]
} }
const fetchGHLicense = async (
owner: string,
repo: string
): Promise<string | null> => {
const octokit = new Octokit({
auth: core.getInput('repo-token', {required: true})
})
try {
const response = await octokit.rest.licenses.getForRepo({owner, repo})
return response.data.license?.spdx_id ?? null
} catch (_) {
return null
}
}
const parseGitHubURL = (url: string): {owner: string; repo: string} | null => {
try {
const parsed = new URL(url)
if (parsed.host !== 'github.com') {
return null
}
const components = parsed.pathname.split('/')
if (components.length < 3) {
return null
}
return {owner: components[1], repo: components[2]}
} catch (_) {
return null
}
}
const setGHLicenses = async (changes: Change[]): Promise<Change[]> => {
const updatedChanges = changes.map(async change => {
if (change.license !== null || change.source_repository_url === null) {
return change
}
const githubUrl = parseGitHubURL(change.source_repository_url)
if (githubUrl === null) {
return change
}
return {
...change,
license: await fetchGHLicense(githubUrl.owner, githubUrl.repo)
}
})
return Promise.all(updatedChanges)
}
+51 -114
View File
@@ -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,
filterAllowedAdvisories
} 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 = filterAllowedAdvisories(
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' &&
@@ -45,22 +40,38 @@ async function run(): Promise<void> {
change.vulnerabilities.length > 0 change.vulnerabilities.length > 0
) )
const [licenseErrors, unknownLicenses] = await 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.name}@${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
View File
@@ -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
View File
@@ -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>`)
}
} }
} }
-30
View File
@@ -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
}
}