Compare commits
257 Commits
add-summary
..
v2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0efb1d1d84 | |||
| d4f6425aa4 | |||
| 49a61bd9bd | |||
| 06c01e11e8 | |||
| 4538b29c27 | |||
| 4153ec555a | |||
| 7c8d0843f9 | |||
| fc00198e43 | |||
| 80e573b784 | |||
| b5c3d1e723 | |||
| 7fd272118a | |||
| 3c9a31f5a0 | |||
| d8fba3fdc1 | |||
| e805dd89e8 | |||
| 32276cb73d | |||
| fe226ac019 | |||
| b759175bdb | |||
| 6af054f363 | |||
| 6f32cb0afd | |||
| 2791afab72 | |||
| a8b5c8c24e | |||
| 12a250de95 | |||
| 917e5af203 | |||
| ba6dba6225 | |||
| 63154658bc | |||
| f84c5813e5 | |||
| 228a6404a2 | |||
| c84947f64b | |||
| 71dbf10e60 | |||
| f9deefc2e9 | |||
| 0e5d083be1 | |||
| 2f428eec67 | |||
| dff2fdff0f | |||
| 12a171cf96 | |||
| 3156cf8998 | |||
| fd675ced9c | |||
| f7d03d8b76 | |||
| 7e41a6f1ee | |||
| 4c0961eff6 | |||
| d1e9a12830 | |||
| 2e3713aab8 | |||
| ba9d7c1389 | |||
| 0cd2781117 | |||
| 129f0ad973 | |||
| 0a88a4704b | |||
| 18069caed8 | |||
| 61cee4b12b | |||
| 94670a1af8 | |||
| 577d9714ad | |||
| 9ce6cb532b | |||
| 0b980b1ccd | |||
| bc5f6c2f39 | |||
| 9c96258789 | |||
| f076f221f4 | |||
| 88b817ec8d | |||
| 2dd6c6a3d7 | |||
| 1d9bfbbddf | |||
| f632f5f79d | |||
| ee42a6512f | |||
| 6f58092362 | |||
| b81bfe53ce | |||
| 5679c0f8be | |||
| 2018b3e66f | |||
| 463890c1ed | |||
| c9b9d23e75 | |||
| 4c14cfe593 | |||
| 5b70fe08e7 | |||
| 81216f689b | |||
| afbc15c97f | |||
| 8d974c4ee8 | |||
| cdad98596a | |||
| 0a0eb39992 | |||
| df3ceaf7f0 | |||
| 1997789b86 | |||
| 584e620d09 | |||
| 1fa34689ad | |||
| de2814d20e | |||
| eabc27054f | |||
| b486e073e9 | |||
| 03321307df | |||
| cc2a6ab32f | |||
| 5de8be4c40 | |||
| 1b8bd021a3 | |||
| 65d8cd176f | |||
| 6d500ff869 | |||
| 0259ed8420 | |||
| ec636f3d19 | |||
| 367e85631b | |||
| abf7b5a775 | |||
| ba85772f4b | |||
| 8d812df813 | |||
| 63e12b21ed | |||
| 0385b5b162 | |||
| 8e053e0f5e | |||
| e0ff0cf732 | |||
| ea65cbfc18 | |||
| 5bf43a89cd | |||
| 468485fc8e | |||
| 46c9f79a1f | |||
| cd3f55e8f9 | |||
| f832351766 | |||
| f96ed229f4 | |||
| 629703a27b | |||
| d05bfb69a5 | |||
| 02bcebdd6e | |||
| fbeabf7e29 | |||
| 0515f5cb39 | |||
| 2d1d679f58 | |||
| a3563a05bc | |||
| 8a20ddbf25 | |||
| 2a646668d9 | |||
| 60be833ffd | |||
| edc501a219 | |||
| 000837f2ac | |||
| 89f99d150a | |||
| 0ed41eff02 | |||
| dbe70eb550 | |||
| 78c7c01396 | |||
| 89a5c76329 | |||
| 4a6d691283 | |||
| b58d457243 | |||
| cc033856be | |||
| 8595e805a5 | |||
| fa10a7f0d6 | |||
| 6755d8aa71 | |||
| 375c537008 | |||
| 98f28ebe06 | |||
| 716b322ec9 | |||
| 12ae1bd550 | |||
| bcb52636bd | |||
| 241ff73141 | |||
| 062b749663 | |||
| 4f00b72b84 | |||
| 602f968ea2 | |||
| bd61ea0d9e | |||
| 8ec13c1f01 | |||
| 723ec8c0d3 | |||
| 2843194510 | |||
| 6944531f76 | |||
| 29cdbbed37 | |||
| 88502badc9 | |||
| ff7c97a976 | |||
| 4d3b8e5269 | |||
| 38ee6e8360 | |||
| 54cd9a7cba | |||
| c4693c00ac | |||
| e89f113be2 | |||
| 2b96ea7f03 | |||
| 4300ce8d38 | |||
| de48c615a3 | |||
| eef7e39202 | |||
| 37dc32836b | |||
| 890361387d | |||
| 61f19e6447 | |||
| fd959624bf | |||
| 11dd186eb0 | |||
| 1ab05cf855 | |||
| 7d7d5e7c84 | |||
| 8a8fa8bd07 | |||
| 06daf8e801 | |||
| aeb9ff5438 | |||
| 1ef21ab130 | |||
| 3c95902dd6 | |||
| 4b4ec08f7b | |||
| a91c3ac205 | |||
| bf0cb7fac4 | |||
| 07a7056819 | |||
| b93fcee7ff | |||
| 8bac022bfd | |||
| fc4fb55b25 | |||
| 31c132fdca | |||
| 10bc05df70 | |||
| e641ee9a41 | |||
| 0ba71661e5 | |||
| 8ef181b2cb | |||
| 7e2a489d03 | |||
| eaeaeb3d57 | |||
| 1eaf30e6eb | |||
| 5da3462152 | |||
| 6fa5a8f9c0 | |||
| 0d23c39a5d | |||
| 6549b27685 | |||
| f4b16c52e5 | |||
| 1a7a37c468 | |||
| 38b459efad | |||
| 6410b2cdd2 | |||
| fd3a3b1051 | |||
| 6771e49f11 | |||
| c7c07e1117 | |||
| 59fdb0cce7 | |||
| 950228f7f7 | |||
| 6973819203 | |||
| eee2e3260e | |||
| 7eeddef885 | |||
| 8c58cdad09 | |||
| 380290a89b | |||
| 50c3ed0ba6 | |||
| 0455501026 | |||
| bac3f038ac | |||
| 2d81062605 | |||
| 2ae4b932b7 | |||
| c7d4075ae0 | |||
| 49a0208abf | |||
| 94941958fb | |||
| 2764e60363 | |||
| bcd1b9ab86 | |||
| d96759fedc | |||
| bfd72e7da2 | |||
| d8efcf0c1f | |||
| 3b74514266 | |||
| 6dfe5fd567 | |||
| 71a0ed0a31 | |||
| 7a364ecd6b | |||
| 435083feb7 | |||
| 781a55eaaa | |||
| 335c64c139 | |||
| af9a4fa160 | |||
| 3e04d4bc87 | |||
| be076ebeca | |||
| b74c52c335 | |||
| 2233eb2b88 | |||
| ca11176434 | |||
| c8f5c5518e | |||
| 469156603d | |||
| 6b1d7e7207 | |||
| a57a1dd454 | |||
| 0e8bd1f46f | |||
| dd931c7005 | |||
| d8d78b6ace | |||
| a1eafc653a | |||
| 35b0f5ded9 | |||
| 5a25f0b1b3 | |||
| 88dd76a7ef | |||
| b1427bfe58 | |||
| 0d079c6553 | |||
| ce3b0c8116 | |||
| d01dd09c36 | |||
| 21d1a080df | |||
| c869fcfa38 | |||
| 20229aad71 | |||
| 65d6c26087 | |||
| 8b6795d89d | |||
| 030c97ab49 | |||
| dc44a85a96 | |||
| 9cdfbb83fa | |||
| b1f8412445 | |||
| 0d02efb12c | |||
| 2a09e52261 | |||
| e86dfd8cc0 | |||
| a39d9063b3 | |||
| 9809e06c2d | |||
| 70bbe4186e | |||
| 23d1ffffb6 | |||
| d792f3e8ca | |||
| 5da7945e2b | |||
| a8e7c378a3 | |||
| 0e0d6ec5d6 |
@@ -1,4 +1,5 @@
|
||||
event.json
|
||||
.ruby-version
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
|
||||
+26
-17
@@ -1,4 +1,5 @@
|
||||
# Contributing
|
||||
|
||||
[fork]: https://github.com/actions/dependency-review-action/fork
|
||||
[pr]: https://github.com/actions/dependency-review-action/compare
|
||||
[code-of-conduct]: CODE_OF_CONDUCT.md
|
||||
@@ -9,7 +10,6 @@ Contributions to this project are
|
||||
[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).
|
||||
|
||||
|
||||
Please note that this project is released with a [Contributor Code of
|
||||
Conduct][code-of-conduct]. By participating in this project you agree
|
||||
to abide by its terms.
|
||||
@@ -20,7 +20,6 @@ This Action makes an authenticated query to the Dependency Graph Diff
|
||||
API endpoint (`GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}`)
|
||||
to find out the set of added and removed dependencies for each manifest.
|
||||
|
||||
|
||||
### Bootstrapping the project
|
||||
|
||||
```
|
||||
@@ -35,7 +34,7 @@ npm install
|
||||
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
|
||||
|
||||
@@ -56,16 +55,24 @@ Like this:
|
||||
$ 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
|
||||
|
||||
0. [Fork][fork] and clone the repository
|
||||
0. Configure and install the dependencies: `npm install`
|
||||
0. Make sure the tests pass on your machine: `npm run test`
|
||||
0. Create a new branch: `git checkout -b my-branch-name`
|
||||
0. Make your change, add tests, and make sure the tests still pass
|
||||
0. Make sure to build and package before pushing: `npm run build && npm run package`
|
||||
0. Push to your fork and [submit a pull request][pr]
|
||||
0. Pat your self on the back and wait for your pull request to be reviewed and merged.
|
||||
1. Configure and install the dependencies: `npm install`
|
||||
2. Make sure the tests pass on your machine: `npm run test`
|
||||
3. Create a new branch: `git checkout -b my-branch-name`
|
||||
4. 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`
|
||||
6. 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.
|
||||
|
||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||
|
||||
@@ -75,22 +82,23 @@ Here are a few things you can do that will increase the likelihood of your pull
|
||||
|
||||
## 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
|
||||
release](https://github.com/actions/dependency-review-action/releases/new)
|
||||
in the Releases page.
|
||||
2. Make sure that the `Publish this Action to the GitHub Marketplace`
|
||||
checkbox is enabled
|
||||
release](https://github.com/actions/dependency-review-action/releases/new)
|
||||
in the Releases page.
|
||||
1. Make sure that the `Publish this Action to the GitHub Marketplace`
|
||||
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">
|
||||
|
||||
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").
|
||||
|
||||
<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
|
||||
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".
|
||||
|
||||
You now have a tag and release using the semver version you used
|
||||
@@ -100,7 +108,8 @@ major version number (e.g. `v1`) in their workflows while
|
||||
automatically getting all the
|
||||
minor/patch updates.
|
||||
|
||||
To do this just force-create a new annotated tag and push it:
|
||||
To do this just checkout `main`, force-create a new annotated tag, and push it:
|
||||
|
||||
```
|
||||
git tag -fa v2 -m "Updating v2 to 2.3.4"
|
||||
git push origin v2 --force
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
### GitHub Enterprise Server
|
||||
|
||||
This action is available in GHES starting with version 3.6. Make sure
|
||||
This action is available in Enterprise Server starting with version 3.6. Make sure
|
||||
[GitHub Advanced
|
||||
Security](https://docs.github.com/en/enterprise-server@3.6/admin/code-security/managing-github-advanced-security-for-your-enterprise/enabling-github-advanced-security-for-your-enterprise)
|
||||
and [GitHub
|
||||
@@ -50,7 +50,6 @@ with the label of any of your runners (the default label
|
||||
is `self-hosted`):
|
||||
|
||||
```yaml
|
||||
|
||||
# ...
|
||||
|
||||
jobs:
|
||||
@@ -65,9 +64,144 @@ jobs:
|
||||
|
||||
## Configuration
|
||||
|
||||
You can pass additional options to the Dependency Review
|
||||
Action using your workflow file. Here's an example workflow with
|
||||
all the possible configurations:
|
||||
Configure this action by either using an external configuration file,
|
||||
or by inlining these options in your workflow file.
|
||||
|
||||
## 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
|
||||
name: 'Dependency Review'
|
||||
@@ -83,26 +217,11 @@ jobs:
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
with:
|
||||
# Possible values: "critical", "high", "moderate", "low"
|
||||
# fail-on-severity: critical
|
||||
#
|
||||
# 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
|
||||
```
|
||||
fail-on-severity: moderate
|
||||
|
||||
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.
|
||||
# Use comma-separated names to pass list arguments:
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
```
|
||||
|
||||
### Vulnerability Severity
|
||||
|
||||
@@ -120,12 +239,23 @@ This example will only fail on pull requests with `critical` and `high` vulnerab
|
||||
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
|
||||
|
||||
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
|
||||
your repository will accept. Alternatively, you can use `deny-licenses` to only
|
||||
forbid a subset of licenses. These options are not supported on GHES.
|
||||
forbid a subset of licenses. These options are not supported on Enterprise Server.
|
||||
|
||||
You can use the [Licenses
|
||||
API](https://docs.github.com/en/rest/licenses) to see the full list of
|
||||
@@ -148,8 +278,9 @@ to filter. A couple of examples:
|
||||
deny-licenses: Apache-1.1, Apache-2.0
|
||||
```
|
||||
|
||||
**Important**
|
||||
### Considerations
|
||||
|
||||
- Checking for licenses is not supported on Enterprise Server.
|
||||
- The action will only accept one of the two parameters; an error will
|
||||
be raised if you provide both.
|
||||
- By default both parameters are empty (no license checking is
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {expect, test, beforeEach} from '@jest/globals'
|
||||
import {readConfig} from '../src/config'
|
||||
import {readConfig, readConfigFile} from '../src/config'
|
||||
import {getRefs} from '../src/git-refs'
|
||||
|
||||
// GitHub Action inputs come in the form of environment variables
|
||||
@@ -13,8 +13,11 @@ function setInput(input: string, value: string) {
|
||||
function clearInputs() {
|
||||
const allowedOptions = [
|
||||
'FAIL-ON-SEVERITY',
|
||||
'FAIL-ON-SCOPES',
|
||||
'ALLOW-LICENSES',
|
||||
'DENY-LICENSES',
|
||||
'ALLOW-GHSAS',
|
||||
'CONFIG-FILE',
|
||||
'BASE-REF',
|
||||
'HEAD-REF'
|
||||
]
|
||||
@@ -82,3 +85,93 @@ test('it raises an error when no refs are provided and the event is not a pull r
|
||||
})
|
||||
).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,6 +1,10 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
import {filterChangesBySeverity} from '../src/filter'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
filterChangesByScopes,
|
||||
filterAllowedAdvisories
|
||||
} from '../src/filter'
|
||||
|
||||
let npmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
@@ -11,6 +15,7 @@ let npmChange: Change = {
|
||||
package_url: 'pkg:npm/reeuhq@1.0.2',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
@@ -30,6 +35,7 @@ let rubyChange: Change = {
|
||||
package_url: 'pkg:gem/actionsomething@3.2.0',
|
||||
license: 'BSD',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'development',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
@@ -46,6 +52,19 @@ let rubyChange: Change = {
|
||||
]
|
||||
}
|
||||
|
||||
let noVulnNpmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
name: 'helpful',
|
||||
version: '1.0.0',
|
||||
package_url: 'pkg:npm/helpful@1.0.0',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: []
|
||||
}
|
||||
|
||||
test('it properly filters changes by severity', async () => {
|
||||
const changes = [npmChange, rubyChange]
|
||||
let result = filterChangesBySeverity('high', changes)
|
||||
@@ -57,3 +76,48 @@ test('it properly filters changes by severity', async () => {
|
||||
result = filterChangesBySeverity('critical', changes)
|
||||
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])
|
||||
})
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fail-on-severity: 'so many zombies'
|
||||
deny-licenses:
|
||||
- MIT
|
||||
@@ -0,0 +1 @@
|
||||
allow_licenses: ['MIT', 'GPL 2']
|
||||
@@ -1,4 +1,4 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {expect, jest, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
import {getDeniedLicenseChanges} from '../src/licenses'
|
||||
|
||||
@@ -11,6 +11,7 @@ let npmChange: Change = {
|
||||
package_url: 'pkg:npm/reeuhq@1.0.2',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
@@ -30,6 +31,7 @@ let rubyChange: Change = {
|
||||
package_url: 'pkg:gem/actionsomething@3.2.0',
|
||||
license: 'BSD',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
@@ -46,15 +48,41 @@ 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 () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: ['BSD']
|
||||
})
|
||||
expect(invalidChanges[0]).toBe(npmChange)
|
||||
})
|
||||
|
||||
test('it fails if a license inside the deny list is found', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const [invalidChanges] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
|
||||
const [invalidChanges] = await getDeniedLicenseChanges(changes, {
|
||||
deny: ['BSD']
|
||||
})
|
||||
expect(invalidChanges[0]).toBe(rubyChange)
|
||||
})
|
||||
|
||||
@@ -62,7 +90,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.
|
||||
test('it fails all license checks when allow is provided an empty array', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
let [invalidChanges, _] = getDeniedLicenseChanges(changes, {
|
||||
let [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: [],
|
||||
deny: ['BSD']
|
||||
})
|
||||
@@ -74,7 +102,9 @@ test('it does not fail if a license outside the allow list is found in removed c
|
||||
{...npmChange, change_type: 'removed'},
|
||||
{...rubyChange, change_type: 'removed'}
|
||||
]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: ['BSD']
|
||||
})
|
||||
expect(invalidChanges).toStrictEqual([])
|
||||
})
|
||||
|
||||
@@ -83,7 +113,9 @@ test('it does not fail if a license inside the deny list is found in removed cha
|
||||
{...npmChange, change_type: 'removed'},
|
||||
{...rubyChange, change_type: 'removed'}
|
||||
]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
deny: ['BSD']
|
||||
})
|
||||
expect(invalidChanges).toStrictEqual([])
|
||||
})
|
||||
|
||||
@@ -93,6 +125,47 @@ test('it fails if a license outside the allow list is found in both of added and
|
||||
npmChange,
|
||||
{...rubyChange, change_type: 'removed'}
|
||||
]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: ['BSD']
|
||||
})
|
||||
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
@@ -10,18 +10,28 @@ inputs:
|
||||
description: Don't block PRs below this severity. Possible values are `low`, `moderate`, `high`, `critical`.
|
||||
required: false
|
||||
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:
|
||||
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
|
||||
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.
|
||||
required: false
|
||||
config-file:
|
||||
description: A filepath to the configuration file for the action.
|
||||
required: false
|
||||
allow-licenses:
|
||||
description: Comma-separated list of allowed licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
||||
required: false
|
||||
deny-licenses:
|
||||
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
||||
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:
|
||||
using: 'node16'
|
||||
main: 'dist/index.js'
|
||||
|
||||
+25395
-318
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1066
File diff suppressed because it is too large
Load Diff
Generated
+2390
-580
File diff suppressed because it is too large
Load Diff
+19
-17
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dependency-review-action",
|
||||
"version": "2.0.4",
|
||||
"version": "2.5.1",
|
||||
"private": true,
|
||||
"description": "A GitHub Action for Dependency Review",
|
||||
"main": "lib/main.js",
|
||||
@@ -25,30 +25,32 @@
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.9.1",
|
||||
"@actions/github": "^5.0.3",
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1",
|
||||
"@octokit/plugin-retry": "^3.0.9",
|
||||
"@octokit/request-error": "^3.0.1",
|
||||
"ansi-styles": "^6.1.0",
|
||||
"got": "^12.3.1",
|
||||
"nodemon": "^2.0.19",
|
||||
"yaml": "^2.1.1",
|
||||
"zod": "^3.18.0"
|
||||
"@octokit/request-error": "^3.0.2",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"got": "^12.5.2",
|
||||
"nodemon": "^2.0.20",
|
||||
"octokit": "^2.0.9",
|
||||
"yaml": "^2.1.3",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.49",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
||||
"@typescript-eslint/parser": "^5.33.1",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"esbuild-register": "^3.3.3",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-github": "^4.3.7",
|
||||
"eslint-plugin-jest": "^26.8.3",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-plugin-github": "^4.4.0",
|
||||
"eslint-plugin-jest": "^27.1.3",
|
||||
"jest": "^27.5.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"nodemon": "^2.0.19",
|
||||
"nodemon": "^2.0.20",
|
||||
"prettier": "2.7.1",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
+48
-9
@@ -3,22 +3,52 @@ require 'json'
|
||||
require 'tempfile'
|
||||
require 'open3'
|
||||
require 'bundler/inline'
|
||||
require 'optparse'
|
||||
|
||||
gemfile do
|
||||
source 'https://rubygems.org'
|
||||
gem 'octokit'
|
||||
end
|
||||
|
||||
config_file = nil
|
||||
github_token = ENV["GITHUB_TOKEN"]
|
||||
|
||||
if !github_token || github_token.empty?
|
||||
puts "Please set the GITHUB_TOKEN environment variable"
|
||||
exit -1
|
||||
end
|
||||
|
||||
arg = /(?<repo_nwo>[\w\-]+\/[\w\-]+)\/pull\/(?<pr_number>\d+)/.match(ARGV[0])
|
||||
op = OptionParser.new do |opts|
|
||||
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?
|
||||
puts "Usage: script/scan_pr <pr_url>"
|
||||
puts op
|
||||
exit -1
|
||||
end
|
||||
|
||||
@@ -32,17 +62,26 @@ event_file = Tempfile.new
|
||||
event_file.write("{ \"pull_request\": #{pr.to_h.to_json}}")
|
||||
event_file.close
|
||||
|
||||
dev_cmd_env = {
|
||||
"INPUT_REPO-TOKEN" => github_token,
|
||||
"GITHUB_REPOSITORY" => repo_nwo,
|
||||
"GITHUB_EVENT_NAME" => "pull_request",
|
||||
"GITHUB_EVENT_PATH" => event_file.path
|
||||
action_inputs = {
|
||||
"repo-token": github_token,
|
||||
"config-file": config_file
|
||||
}
|
||||
|
||||
dev_cmd = "./node_modules/.bin/nodemon --exec \"node -r esbuild-register\" src/main.ts"
|
||||
dev_cmd_env = {
|
||||
"GITHUB_REPOSITORY" => repo_nwo,
|
||||
"GITHUB_EVENT_NAME" => "pull_request",
|
||||
"GITHUB_EVENT_PATH" => event_file.path,
|
||||
"GITHUB_STEP_SUMMARY" => "/dev/null"
|
||||
}
|
||||
|
||||
# bash does not like variable names with dashes like the ones Actions
|
||||
# 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|
|
||||
while line = out.gets
|
||||
puts line
|
||||
puts line.gsub(github_token, "<REDACTED>")
|
||||
end
|
||||
end
|
||||
|
||||
+69
-9
@@ -1,32 +1,92 @@
|
||||
import * as fs from 'fs'
|
||||
import path from 'path'
|
||||
import YAML from 'yaml'
|
||||
import * as core from '@actions/core'
|
||||
import * as z from 'zod'
|
||||
import {ConfigurationOptions, SEVERITIES} from './schemas'
|
||||
import {
|
||||
ConfigurationOptions,
|
||||
ConfigurationOptionsSchema,
|
||||
SeveritySchema,
|
||||
SCOPES
|
||||
} from './schemas'
|
||||
|
||||
function getOptionalInput(name: string): string | undefined {
|
||||
const value = core.getInput(name)
|
||||
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 {
|
||||
const fail_on_severity = z
|
||||
.enum(SEVERITIES)
|
||||
.default('low')
|
||||
.parse(getOptionalInput('fail-on-severity'))
|
||||
const allow_licenses = getOptionalInput('allow-licenses')
|
||||
const deny_licenses = getOptionalInput('deny-licenses')
|
||||
const externalConfig = getOptionalInput('config-file')
|
||||
if (externalConfig !== undefined) {
|
||||
const config = readConfigFile(externalConfig)
|
||||
// the reasoning behind reading the inline config when an external
|
||||
// config file is provided is that we still want to allow users to
|
||||
// pass inline options in the presence of an external config file.
|
||||
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) {
|
||||
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 head_ref = getOptionalInput('head-ref')
|
||||
|
||||
return {
|
||||
fail_on_severity,
|
||||
allow_licenses: allow_licenses?.split(',').map(x => x.trim()),
|
||||
deny_licenses: deny_licenses?.split(',').map(x => x.trim()),
|
||||
fail_on_scopes,
|
||||
allow_licenses,
|
||||
deny_licenses,
|
||||
allow_ghsas,
|
||||
base_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
|
||||
}
|
||||
|
||||
+58
-1
@@ -1,4 +1,4 @@
|
||||
import {Changes, Severity, SEVERITIES} from './schemas'
|
||||
import {Changes, Severity, SEVERITIES, Scope} from './schemas'
|
||||
|
||||
export function filterChangesBySeverity(
|
||||
severity: Severity,
|
||||
@@ -33,3 +33,60 @@ export function filterChangesBySeverity(
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
+65
-4
@@ -1,3 +1,5 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Octokit} from 'octokit'
|
||||
import {Change} from './schemas'
|
||||
|
||||
/**
|
||||
@@ -10,21 +12,27 @@ import {Change} from './schemas'
|
||||
* we will ignore the deny list.
|
||||
* @param {Change[]} changes The list of changes to filter.
|
||||
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
|
||||
* @returns {[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
|
||||
* @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
|
||||
*/
|
||||
export function getDeniedLicenseChanges(
|
||||
export async function getDeniedLicenseChanges(
|
||||
changes: Change[],
|
||||
licenses: {
|
||||
allow?: string[]
|
||||
deny?: string[]
|
||||
}
|
||||
): [Change[], Change[]] {
|
||||
): Promise<[Change[], Change[]]> {
|
||||
const {allow, deny} = licenses
|
||||
|
||||
const disallowed: Change[] = []
|
||||
const unknown: Change[] = []
|
||||
|
||||
for (const change of changes) {
|
||||
const consolidatedChanges = changes.some(
|
||||
({source_repository_url, license}) => !license && source_repository_url
|
||||
)
|
||||
? await setGHLicenses(changes)
|
||||
: changes
|
||||
|
||||
for (const change of consolidatedChanges) {
|
||||
if (change.change_type === 'removed') {
|
||||
continue
|
||||
}
|
||||
@@ -47,3 +55,56 @@ export function getDeniedLicenseChanges(
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
+113
-50
@@ -3,13 +3,19 @@ import * as dependencyGraph from './dependency-graph'
|
||||
import * as github from '@actions/github'
|
||||
import styles from 'ansi-styles'
|
||||
import {RequestError} from '@octokit/request-error'
|
||||
import {Change, Severity} from './schemas'
|
||||
import {Change, Severity, Changes} from './schemas'
|
||||
import {readConfig} from '../src/config'
|
||||
import {filterChangesBySeverity} from '../src/filter'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
filterChangesByScopes,
|
||||
filterAllowedAdvisories
|
||||
} from '../src/filter'
|
||||
import {getDeniedLicenseChanges} from './licenses'
|
||||
import * as summary from './summary'
|
||||
import {getRefs} from './git-refs'
|
||||
|
||||
import {groupDependenciesByManifest} from './utils'
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const config = readConfig()
|
||||
@@ -22,17 +28,16 @@ async function run(): Promise<void> {
|
||||
headRef: refs.head
|
||||
})
|
||||
|
||||
const minSeverity = config.fail_on_severity
|
||||
let failed = false
|
||||
|
||||
const licenses = {
|
||||
allow: config.allow_licenses,
|
||||
deny: config.deny_licenses
|
||||
}
|
||||
const minSeverity = config.fail_on_severity as Severity
|
||||
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
|
||||
const filteredChanges = filterAllowedAdvisories(
|
||||
config.allow_ghsas,
|
||||
scopedChanges
|
||||
)
|
||||
|
||||
const addedChanges = filterChangesBySeverity(
|
||||
minSeverity as Severity,
|
||||
changes
|
||||
minSeverity,
|
||||
filteredChanges
|
||||
).filter(
|
||||
change =>
|
||||
change.change_type === 'added' &&
|
||||
@@ -40,38 +45,22 @@ async function run(): Promise<void> {
|
||||
change.vulnerabilities.length > 0
|
||||
)
|
||||
|
||||
const [licenseErrors, unknownLicenses] = getDeniedLicenseChanges(
|
||||
changes,
|
||||
licenses
|
||||
const [licenseErrors, unknownLicenses] = await getDeniedLicenseChanges(
|
||||
filteredChanges,
|
||||
{
|
||||
allow: config.allow_licenses,
|
||||
deny: config.deny_licenses
|
||||
}
|
||||
)
|
||||
|
||||
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses)
|
||||
|
||||
if (addedChanges.length > 0) {
|
||||
for (const change of addedChanges) {
|
||||
printChangeVulnerabilities(change)
|
||||
}
|
||||
failed = true
|
||||
}
|
||||
|
||||
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity || '')
|
||||
|
||||
if (licenseErrors.length > 0) {
|
||||
printLicensesError(licenseErrors)
|
||||
core.setFailed('Dependency review detected incompatible licenses.')
|
||||
}
|
||||
|
||||
printNullLicenses(unknownLicenses)
|
||||
|
||||
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
|
||||
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config)
|
||||
summary.addScannedDependencies(changes)
|
||||
|
||||
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.`
|
||||
)
|
||||
}
|
||||
printVulnerabilitiesBlock(addedChanges, minSeverity)
|
||||
printLicensesBlock(licenseErrors, unknownLicenses)
|
||||
printScannedDependencies(changes)
|
||||
} catch (error) {
|
||||
if (error instanceof RequestError && error.status === 404) {
|
||||
core.setFailed(
|
||||
@@ -93,6 +82,29 @@ 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 {
|
||||
for (const vuln of change.vulnerabilities) {
|
||||
core.info(
|
||||
@@ -106,18 +118,17 @@ function printChangeVulnerabilities(change: 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 printLicensesBlock(
|
||||
licenseErrors: Change[],
|
||||
unknownLicenses: Change[]
|
||||
): void {
|
||||
core.group('Licenses', async () => {
|
||||
if (licenseErrors.length > 0) {
|
||||
printLicensesError(licenseErrors)
|
||||
core.setFailed('Dependency review detected incompatible licenses.')
|
||||
}
|
||||
printNullLicenses(unknownLicenses)
|
||||
})
|
||||
}
|
||||
|
||||
function printLicensesError(changes: Change[]): void {
|
||||
@@ -146,4 +157,56 @@ 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()
|
||||
|
||||
+11
-3
@@ -1,6 +1,9 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
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({
|
||||
change_type: z.enum(['added', 'removed']),
|
||||
@@ -11,10 +14,11 @@ export const ChangeSchema = z.object({
|
||||
package_url: z.string(),
|
||||
license: z.string().nullable(),
|
||||
source_repository_url: z.string().nullable(),
|
||||
scope: z.enum(SCOPES).optional(),
|
||||
vulnerabilities: z
|
||||
.array(
|
||||
z.object({
|
||||
severity: z.enum(['critical', 'high', 'moderate', 'low']),
|
||||
severity: SeveritySchema,
|
||||
advisory_ghsa_id: z.string(),
|
||||
advisory_summary: z.string(),
|
||||
advisory_url: z.string()
|
||||
@@ -32,9 +36,12 @@ export const PullRequestSchema = z.object({
|
||||
|
||||
export const ConfigurationOptionsSchema = z
|
||||
.object({
|
||||
fail_on_severity: z.enum(SEVERITIES).default('low'),
|
||||
fail_on_severity: SeveritySchema,
|
||||
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
||||
allow_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(),
|
||||
head_ref: z.string()
|
||||
})
|
||||
@@ -49,4 +56,5 @@ export const ChangesSchema = z.array(ChangeSchema)
|
||||
export type Change = z.infer<typeof ChangeSchema>
|
||||
export type Changes = z.infer<typeof ChangesSchema>
|
||||
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
|
||||
export type Severity = typeof SEVERITIES[number]
|
||||
export type Severity = z.infer<typeof SeveritySchema>
|
||||
export type Scope = typeof SCOPES[number]
|
||||
|
||||
+19
-11
@@ -1,6 +1,7 @@
|
||||
import * as core from '@actions/core'
|
||||
import {ConfigurationOptions, Change, Changes} from './schemas'
|
||||
import {SummaryTableRow} from '@actions/core/lib/summary'
|
||||
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
||||
|
||||
export function addSummaryToSummary(
|
||||
addedPackages: Changes,
|
||||
@@ -20,7 +21,7 @@ export function addChangeVulnerabilitiesToSummary(
|
||||
): void {
|
||||
const rows: SummaryTableRow[] = []
|
||||
|
||||
const manifests = getManifests(addedPackages)
|
||||
const manifests = getManifestsSet(addedPackages)
|
||||
|
||||
core.summary
|
||||
.addHeading('Vulnerabilities')
|
||||
@@ -99,7 +100,7 @@ export function addLicensesToSummary(
|
||||
|
||||
if (licenseErrors.length > 0) {
|
||||
const rows: SummaryTableRow[] = []
|
||||
const manifests = getManifests(licenseErrors)
|
||||
const manifests = getManifestsSet(licenseErrors)
|
||||
|
||||
core.summary.addHeading('Incompatible Licenses', 3).addSeparator()
|
||||
|
||||
@@ -125,7 +126,7 @@ export function addLicensesToSummary(
|
||||
|
||||
if (unknownLicenses.length > 0) {
|
||||
const rows: SummaryTableRow[] = []
|
||||
const manifests = getManifests(unknownLicenses)
|
||||
const manifests = getManifestsSet(unknownLicenses)
|
||||
|
||||
core.debug(
|
||||
`found ${manifests.entries.length} manifests for unknown licenses`
|
||||
@@ -150,14 +151,21 @@ export function addLicensesToSummary(
|
||||
}
|
||||
}
|
||||
|
||||
function getManifests(changes: Changes): Set<string> {
|
||||
return new Set(changes.flatMap(c => c.manifest))
|
||||
}
|
||||
export function addScannedDependencies(changes: Changes): void {
|
||||
const dependencies = groupDependenciesByManifest(changes)
|
||||
const manifests = dependencies.keys()
|
||||
|
||||
function renderUrl(url: string | null, text: string): string {
|
||||
if (url) {
|
||||
return `<a href="${url}">${text}</a>`
|
||||
} else {
|
||||
return text
|
||||
const summary = core.summary
|
||||
.addHeading('Scanned Dependencies')
|
||||
.addRaw(`We scanned ${dependencies.size} manifest files:`)
|
||||
|
||||
for (const manifest of manifests) {
|
||||
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>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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