Compare commits
99
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e42c3395a | ||
|
|
a3074cd699 | ||
|
|
51a29d6960 | ||
|
|
235a221cf4 | ||
|
|
9b3a7f61dd | ||
|
|
a4761312ac | ||
|
|
28c7c8c314 | ||
|
|
9da0fd4871 | ||
|
|
fe45fd6645 | ||
|
|
c41b9f9cfb | ||
|
|
10c5aa9564 | ||
|
|
8e5000107a | ||
|
|
89a074ec7e | ||
|
|
8d7a4c48ad | ||
|
|
2f59625b62 | ||
|
|
9e552623cc | ||
|
|
4108a15bd3 | ||
|
|
5ea8fbfb83 | ||
|
|
c72eb06e71 | ||
|
|
aa409fa6cd | ||
|
|
5aaa78ce3c | ||
|
|
8d9ea3eb63 | ||
|
|
59a4f4c4ba | ||
|
|
bf8cfe8b38 | ||
|
|
ae538ebe32 | ||
|
|
b4126ce983 | ||
|
|
418ae59d51 | ||
|
|
c38007a979 | ||
|
|
ebe5527e72 | ||
|
|
1589654682 | ||
|
|
f0ff0b670a | ||
|
|
336da03de2 | ||
|
|
78565a954f | ||
|
|
3c73a622ba | ||
|
|
7a42af0f2f | ||
|
|
abfd4a1fc7 | ||
|
|
40e460b464 | ||
|
|
9f1bc9b354 | ||
|
|
774bd6c1d5 | ||
|
|
f7686f8c21 | ||
|
|
688e92b5e5 | ||
|
|
7ce779229f | ||
|
|
543eecb644 | ||
|
|
a7ec2eb771 | ||
|
|
13455c7175 | ||
|
|
6d941b396a | ||
|
|
49ed3f2876 | ||
|
|
b55cddb69d | ||
|
|
dcdeb7de77 | ||
|
|
b4a2fbfa16 | ||
|
|
97e5a607ba | ||
|
|
3b410dc4ad | ||
|
|
683cbc4872 | ||
|
|
2f696d8c7a | ||
|
|
1a9033d563 | ||
|
|
ad6e320da1 | ||
|
|
3d86825394 | ||
|
|
10dc05ba09 | ||
|
|
04f48dec81 | ||
|
|
bb5c0c7ca0 | ||
|
|
2d7d700469 | ||
|
|
f095b5a541 | ||
|
|
f54a1f3b74 | ||
|
|
84921e5e4a | ||
|
|
c5af7ff272 | ||
|
|
31279d265a | ||
|
|
2532504548 | ||
|
|
cc6d251652 | ||
|
|
516e8497ac | ||
|
|
43c5083e6c | ||
|
|
fa62a0febc | ||
|
|
e897e8ebdd | ||
|
|
216fafaed5 | ||
|
|
0144419c8e | ||
|
|
7b16bd0b54 | ||
|
|
4525a8c091 | ||
|
|
72273c9a36 | ||
|
|
562a2f3c0a | ||
|
|
c82c183029 | ||
|
|
26be1f407e | ||
|
|
022ea02fbb | ||
|
|
d6e28cdfae | ||
|
|
da3d8af3e3 | ||
|
|
52fa73c086 | ||
|
|
3baea959cf | ||
|
|
782c57b17e | ||
|
|
ac5ed8754d | ||
|
|
024a5a6342 | ||
|
|
b2fc686406 | ||
|
|
4ec1d46392 | ||
|
|
cfef8bfe29 | ||
|
|
bd43b8d1e2 | ||
|
|
fced408b87 | ||
|
|
65f9f50468 | ||
|
|
a393c83ce5 | ||
|
|
56163c5659 | ||
|
|
5dc2e6e4bb | ||
|
|
9760f87258 | ||
|
|
74c047086c |
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Dependency Review Action",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/typescript-node:18",
|
||||||
|
"postCreateCommand": "npm install",
|
||||||
|
"remoteUser": "node",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/ruby:1": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,10 +23,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set Node.js 16.x
|
- name: Set Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18
|
||||||
cache: npm
|
cache: npm
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --ignore-scripts
|
run: npm ci --ignore-scripts
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 18
|
||||||
cache: npm
|
cache: npm
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --ignore-scripts
|
run: npm ci --ignore-scripts
|
||||||
|
|||||||
+3
-2
@@ -38,6 +38,7 @@ _Note_: We don't have any useful tests yet, contributions are welcome!
|
|||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
|
|
||||||
|
It is recommended to have atleast [Node 18](https://nodejs.org/en/) installed.
|
||||||
We have a script to scan a given PR for vulnerabilities, this will
|
We have a script to scan a given PR for vulnerabilities, this will
|
||||||
help you test your local changes. Make sure to [grab a Personal Access Token (PAT)](https://github.com/settings/tokens) before proceeding (you'll need `repo` permissions for private repos):
|
help you test your local changes. Make sure to [grab a Personal Access Token (PAT)](https://github.com/settings/tokens) before proceeding (you'll need `repo` permissions for private repos):
|
||||||
|
|
||||||
@@ -56,11 +57,11 @@ $ GITHUB_TOKEN=my-secret-token ./scripts/scan_pr https://github.com/actions/depe
|
|||||||
```
|
```
|
||||||
|
|
||||||
[Configuration options](README.md#configuration-options) can be set by
|
[Configuration options](README.md#configuration-options) can be set by
|
||||||
passing an external YAML [configuration file](README.md#configuration-file) to the
|
passing an external YAML [configuration file](README.md#configuration-file) to the
|
||||||
`scan_pr` script with the `-c`/`--config-file` option:
|
`scan_pr` script with the `-c`/`--config-file` option:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_url>
|
$ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_url>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Submitting a pull request
|
## Submitting a pull request
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ raise an error if any vulnerabilities or invalid licenses are being introduced.
|
|||||||
|
|
||||||
The action is available for all public repositories, as well as private repositories that have GitHub Advanced Security licensed.
|
The action is available for all public repositories, as well as private repositories that have GitHub Advanced Security licensed.
|
||||||
|
|
||||||
You can see the results on the job logs
|
You can see the results on the job logs:
|
||||||
|
|
||||||
<img width="854" alt="Screen Shot 2022-03-31 at 1 10 51 PM" src="https://user-images.githubusercontent.com/2161/161042286-b22d7dd3-13cb-458d-8744-ce70ed9bf562.png">
|
<img width="854" alt="Screen Shot 2022-03-31 at 1 10 51 PM" src="https://user-images.githubusercontent.com/2161/161042286-b22d7dd3-13cb-458d-8744-ce70ed9bf562.png">
|
||||||
|
|
||||||
or on the job summary
|
or on the job summary:
|
||||||
|
|
||||||
<img src="https://user-images.githubusercontent.com/7847935/182871416-50332bbb-b279-4621-a136-ca72a4314301.png">
|
<img src="https://user-images.githubusercontent.com/7847935/182871416-50332bbb-b279-4621-a136-ca72a4314301.png">
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@v2
|
uses: actions/dependency-review-action@v3
|
||||||
```
|
```
|
||||||
|
|
||||||
### GitHub Enterprise Server
|
### GitHub Enterprise Server
|
||||||
@@ -59,149 +59,34 @@ jobs:
|
|||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@v2
|
uses: actions/dependency-review-action@v3
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration options
|
||||||
|
|
||||||
Configure this action by either using an external configuration file,
|
Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional.
|
||||||
or by inlining these options in your workflow file.
|
|
||||||
|
|
||||||
## Configuration Options
|
| Option | Usage | Possible values | Default value |
|
||||||
|
|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|---------------|
|
||||||
|
| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` |
|
||||||
|
| `allow-licenses`* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||||
|
| `deny-licenses`* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||||
|
| `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. |`runtime`, `development`, `unknown` | `runtime` |
|
||||||
|
| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none |
|
||||||
|
| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` |
|
||||||
|
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
|
||||||
|
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
|
||||||
|
|
||||||
### config-file
|
*not supported for use with GitHub Enterprise Server
|
||||||
|
|
||||||
A string representing the path to an external configuraton file. By
|
†will be supported with GitHub Enterprise Server 3.8
|
||||||
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
|
### Inline Configuration
|
||||||
|
|
||||||
You can pass options to the Dependency Review
|
You can pass options to the Dependency Review GitHub Action using your workflow file.
|
||||||
Action using your workflow file. Here's an example of what the full
|
|
||||||
file would look like:
|
#### Example
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: 'Dependency Review'
|
name: 'Dependency Review'
|
||||||
@@ -215,7 +100,7 @@ jobs:
|
|||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@v2
|
uses: actions/dependency-review-action@v3
|
||||||
with:
|
with:
|
||||||
fail-on-severity: moderate
|
fail-on-severity: moderate
|
||||||
|
|
||||||
@@ -223,71 +108,41 @@ jobs:
|
|||||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||||
```
|
```
|
||||||
|
|
||||||
### Vulnerability Severity
|
### Configuration File
|
||||||
|
|
||||||
By default the action will fail on any pull request that contains a
|
You can use an external configuration file to specify the settings for this action. It can be a local file or a file in an external repository. Refer to the following options for the specification.
|
||||||
vulnerable dependency, regardless of the severity level. You can override this behavior by
|
|
||||||
using the `fail-on-severity` option, which will cause a failure on any pull requests that introduce vulnerabilities of the specified severity level or higher. The possible values are: `critical`, `high`, `moderate`, or `low`. The
|
|
||||||
action defaults to `low`.
|
|
||||||
|
|
||||||
This example will only fail on pull requests with `critical` and `high` vulnerabilities:
|
| Option | Usage | Possible values |
|
||||||
|
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `config-file` | A path to a file in the current repository or an external repository. Use this syntax for external files: `OWNER/REPOSITORY/FILENAME@BRANCH` | **Local file**: `./.github/dependency-review-config.yml` <br> **External repo**: `github/octorepo/dependency-review-config.yml@main` |
|
||||||
|
| `external-repo-token` | Specifies a token for fetching the configuration file if the file resides in a private external repository. Create a token in [developer settings](https://github.com/settings/tokens). | Any token with `read` permissions to the repository hosting the config file. |
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
Start by specifying that you will be using an external configuration file:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@v2
|
uses: actions/dependency-review-action@v2
|
||||||
with:
|
with:
|
||||||
fail-on-severity: high
|
config-file: './.github/dependency-review-config.yml'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dependency Scoping
|
And then create the file in the path you just specified:
|
||||||
|
|
||||||
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
|
```yaml
|
||||||
- name: Dependency Review
|
fail-on-severity: 'critical'
|
||||||
uses: actions/dependency-review-action@v2
|
allow-licenses:
|
||||||
with:
|
- 'GPL-3.0'
|
||||||
fail-on-scopes: runtime, development
|
- 'BSD-3-Clause'
|
||||||
```
|
- 'MIT'
|
||||||
|
|
||||||
### 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 Enterprise Server.
|
|
||||||
|
|
||||||
You can use the [Licenses
|
|
||||||
API](https://docs.github.com/en/rest/licenses) to see the full list of
|
|
||||||
supported licenses. Use the `spdx_id` field for every license you want
|
|
||||||
to filter. A couple of examples:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# only allow MIT-licensed dependents
|
|
||||||
- name: Dependency Review
|
|
||||||
uses: actions/dependency-review-action@v2
|
|
||||||
with:
|
|
||||||
allow-licenses: MIT
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Block Apache 1.1 and 2.0 licensed dependents
|
|
||||||
- name: Dependency Review
|
|
||||||
uses: actions/dependency-review-action@v2
|
|
||||||
with:
|
|
||||||
deny-licenses: Apache-1.1, Apache-2.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Considerations
|
### Considerations
|
||||||
|
|
||||||
- Checking for licenses is not supported on Enterprise Server.
|
- 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 `license` parameters; an error will be raised if you provide both.
|
||||||
be raised if you provide both.
|
- We don't have license information for all of your dependents. If we can't detect the license for a dependency **we will inform you, but the action won't fail**.
|
||||||
- By default both parameters are empty (no license checking is
|
|
||||||
performed).
|
|
||||||
- We don't have license information for all of your dependents. If we
|
|
||||||
can't detect the license for a dependency **we will inform you, but the
|
|
||||||
action won't fail**.
|
|
||||||
|
|
||||||
## Blocking pull requests
|
## Blocking pull requests
|
||||||
|
|
||||||
@@ -295,14 +150,11 @@ The Dependency Review GitHub Action check will only block a pull request from be
|
|||||||
|
|
||||||
## Getting help
|
## Getting help
|
||||||
|
|
||||||
If you have bug reports, questions or suggestions please [create a new
|
If you have bug reports, questions or suggestions please [create a new issue](https://github.com/actions/dependency-review-action/issues/new/choose).
|
||||||
issue](https://github.com/actions/dependency-review-action/issues/new/choose).
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We are grateful for any contributions made to this project.
|
We are grateful for any contributions made to this project. Please read [CONTRIBUTING.MD](https://github.com/actions/dependency-review-action/blob/main/CONTRIBUTING.md) to get started.
|
||||||
|
|
||||||
Please read [CONTRIBUTING.MD](https://github.com/actions/dependency-review-action/blob/main/CONTRIBUTING.md) to get started.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+125
-49
@@ -1,6 +1,7 @@
|
|||||||
import {expect, test, beforeEach} from '@jest/globals'
|
import {expect, test, beforeEach} from '@jest/globals'
|
||||||
import {readConfig, readConfigFile} from '../src/config'
|
import {readConfig} from '../src/config'
|
||||||
import {getRefs} from '../src/git-refs'
|
import {getRefs} from '../src/git-refs'
|
||||||
|
import * as Utils from '../src/utils'
|
||||||
|
|
||||||
// GitHub Action inputs come in the form of environment variables
|
// GitHub Action inputs come in the form of environment variables
|
||||||
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
|
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
|
||||||
@@ -17,6 +18,8 @@ function clearInputs() {
|
|||||||
'ALLOW-LICENSES',
|
'ALLOW-LICENSES',
|
||||||
'DENY-LICENSES',
|
'DENY-LICENSES',
|
||||||
'ALLOW-GHSAS',
|
'ALLOW-GHSAS',
|
||||||
|
'LICENSE-CHECK',
|
||||||
|
'VULNERABILITY-CHECK',
|
||||||
'CONFIG-FILE',
|
'CONFIG-FILE',
|
||||||
'BASE-REF',
|
'BASE-REF',
|
||||||
'HEAD-REF'
|
'HEAD-REF'
|
||||||
@@ -27,48 +30,62 @@ function clearInputs() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(true)
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
clearInputs()
|
clearInputs()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it defaults to low severity', async () => {
|
test('it defaults to low severity', async () => {
|
||||||
const options = readConfig()
|
const config = await readConfig()
|
||||||
expect(options.fail_on_severity).toEqual('low')
|
expect(config.fail_on_severity).toEqual('low')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it reads custom configs', async () => {
|
test('it reads custom configs', async () => {
|
||||||
setInput('fail-on-severity', 'critical')
|
setInput('fail-on-severity', 'critical')
|
||||||
setInput('allow-licenses', ' BSD, GPL 2')
|
setInput('allow-licenses', ' BSD, GPL 2')
|
||||||
|
|
||||||
const options = readConfig()
|
const config = await readConfig()
|
||||||
expect(options.fail_on_severity).toEqual('critical')
|
expect(config.fail_on_severity).toEqual('critical')
|
||||||
expect(options.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
expect(config.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it defaults to empty allow/deny lists ', async () => {
|
test('it defaults to empty allow/deny lists ', async () => {
|
||||||
const options = readConfig()
|
const config = await readConfig()
|
||||||
|
|
||||||
expect(options.allow_licenses).toEqual(undefined)
|
expect(config.allow_licenses).toEqual(undefined)
|
||||||
expect(options.deny_licenses).toEqual(undefined)
|
expect(config.deny_licenses).toEqual(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it raises an error if both an allow and denylist are specified', async () => {
|
test('it raises an error if both an allow and denylist are specified', async () => {
|
||||||
setInput('allow-licenses', 'MIT')
|
setInput('allow-licenses', 'MIT')
|
||||||
setInput('deny-licenses', 'BSD')
|
setInput('deny-licenses', 'BSD')
|
||||||
|
|
||||||
expect(() => readConfig()).toThrow()
|
await expect(readConfig()).rejects.toThrow(
|
||||||
|
'You cannot specify both allow-licenses and deny-licenses'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
test('it raises an error if an empty allow list is specified', async () => {
|
||||||
|
setInput('config-file', './__tests__/fixtures/config-empty-allow-sample.yml')
|
||||||
|
|
||||||
|
await expect(readConfig()).rejects.toThrow(
|
||||||
|
'You should provide at least one license in allow-licenses'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it raises an error when given an unknown severity', async () => {
|
test('it raises an error when given an unknown severity', async () => {
|
||||||
setInput('fail-on-severity', 'zombies')
|
setInput('fail-on-severity', 'zombies')
|
||||||
expect(() => readConfig()).toThrow()
|
|
||||||
|
await expect(readConfig()).rejects.toThrow(/received 'zombies'/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it uses the given refs when the event is not a pull request', async () => {
|
test('it uses the given refs when the event is not a pull request', async () => {
|
||||||
setInput('base-ref', 'a-custom-base-ref')
|
setInput('base-ref', 'a-custom-base-ref')
|
||||||
setInput('head-ref', 'a-custom-head-ref')
|
setInput('head-ref', 'a-custom-head-ref')
|
||||||
|
|
||||||
const refs = getRefs(readConfig(), {
|
const refs = getRefs(await readConfig(), {
|
||||||
payload: {},
|
payload: {},
|
||||||
eventName: 'workflow_dispatch'
|
eventName: 'workflow_dispatch'
|
||||||
})
|
})
|
||||||
@@ -77,9 +94,9 @@ test('it uses the given refs when the event is not a pull request', async () =>
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('it raises an error when no refs are provided and the event is not a pull request', async () => {
|
test('it raises an error when no refs are provided and the event is not a pull request', async () => {
|
||||||
const options = readConfig()
|
const config = await readConfig()
|
||||||
expect(() =>
|
expect(() =>
|
||||||
getRefs(options, {
|
getRefs(config, {
|
||||||
payload: {},
|
payload: {},
|
||||||
eventName: 'workflow_dispatch'
|
eventName: 'workflow_dispatch'
|
||||||
})
|
})
|
||||||
@@ -87,91 +104,150 @@ test('it raises an error when no refs are provided and the event is not a pull r
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('it reads an external config file', async () => {
|
test('it reads an external config file', async () => {
|
||||||
let options = readConfigFile('./__tests__/fixtures/config-allow-sample.yml')
|
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
||||||
expect(options.fail_on_severity).toEqual('critical')
|
|
||||||
expect(options.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
const config = await readConfig()
|
||||||
|
expect(config.fail_on_severity).toEqual('critical')
|
||||||
|
expect(config.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('raises an error when the the config file was not found', async () => {
|
test('raises an error when the the config file was not found', async () => {
|
||||||
expect(() => readConfigFile('fixtures/i-dont-exist')).toThrow()
|
setInput('config-file', 'fixtures/i-dont-exist')
|
||||||
|
await expect(readConfig()).rejects.toThrow(/Unable to fetch config file/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it parses options from both sources', async () => {
|
test('it parses options from both sources', async () => {
|
||||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
||||||
|
|
||||||
let options = readConfig()
|
let config = await readConfig()
|
||||||
expect(options.fail_on_severity).toEqual('critical')
|
expect(config.fail_on_severity).toEqual('critical')
|
||||||
|
|
||||||
setInput('base-ref', 'a-custom-base-ref')
|
setInput('base-ref', 'a-custom-base-ref')
|
||||||
options = readConfig()
|
config = await readConfig()
|
||||||
expect(options.base_ref).toEqual('a-custom-base-ref')
|
expect(config.base_ref).toEqual('a-custom-base-ref')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('in case of conflicts, the external config is the source of truth', async () => {
|
test('in case of conflicts, the inline config is the source of truth', async () => {
|
||||||
|
setInput('fail-on-severity', 'low')
|
||||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml') // this will set fail-on-severity to 'critical'
|
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml') // this will set fail-on-severity to 'critical'
|
||||||
|
|
||||||
let options = readConfig()
|
const config = await readConfig()
|
||||||
expect(options.fail_on_severity).toEqual('critical')
|
expect(config.fail_on_severity).toEqual('low')
|
||||||
|
|
||||||
// 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 () => {
|
test('it uses the default values when loading external files', async () => {
|
||||||
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
||||||
let options = readConfig()
|
let config = await readConfig()
|
||||||
expect(options.allow_licenses).toEqual(undefined)
|
expect(config.allow_licenses).toEqual(undefined)
|
||||||
expect(options.deny_licenses).toEqual(undefined)
|
expect(config.deny_licenses).toEqual(undefined)
|
||||||
|
|
||||||
setInput('config-file', './__tests__/fixtures/license-config-sample.yml')
|
setInput('config-file', './__tests__/fixtures/license-config-sample.yml')
|
||||||
options = readConfig()
|
config = await readConfig()
|
||||||
expect(options.fail_on_severity).toEqual('low')
|
expect(config.fail_on_severity).toEqual('low')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it accepts an external configuration filename', async () => {
|
test('it accepts an external configuration filename', async () => {
|
||||||
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
||||||
const options = readConfig()
|
const config = await readConfig()
|
||||||
expect(options.fail_on_severity).toEqual('critical')
|
expect(config.fail_on_severity).toEqual('critical')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it raises an error when given an unknown severity in an external config file', async () => {
|
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')
|
setInput('config-file', './__tests__/fixtures/invalid-severity-config.yml')
|
||||||
expect(() => readConfig()).toThrow()
|
await expect(readConfig()).rejects.toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it defaults to runtime scope', async () => {
|
test('it defaults to runtime scope', async () => {
|
||||||
const options = readConfig()
|
const config = await readConfig()
|
||||||
expect(options.fail_on_scopes).toEqual(['runtime'])
|
expect(config.fail_on_scopes).toEqual(['runtime'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it parses custom scopes preference', async () => {
|
test('it parses custom scopes preference', async () => {
|
||||||
setInput('fail-on-scopes', 'runtime, development')
|
setInput('fail-on-scopes', 'runtime, development')
|
||||||
let options = readConfig()
|
let config = await readConfig()
|
||||||
expect(options.fail_on_scopes).toEqual(['runtime', 'development'])
|
expect(config.fail_on_scopes).toEqual(['runtime', 'development'])
|
||||||
|
|
||||||
clearInputs()
|
clearInputs()
|
||||||
setInput('fail-on-scopes', 'development')
|
setInput('fail-on-scopes', 'development')
|
||||||
options = readConfig()
|
config = await readConfig()
|
||||||
expect(options.fail_on_scopes).toEqual(['development'])
|
expect(config.fail_on_scopes).toEqual(['development'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it raises an error when given invalid scope', async () => {
|
test('it raises an error when given invalid scope', async () => {
|
||||||
setInput('fail-on-scopes', 'runtime, zombies')
|
setInput('fail-on-scopes', 'runtime, zombies')
|
||||||
expect(() => readConfig()).toThrow()
|
await expect(readConfig()).rejects.toThrow(/received 'zombies'/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it defaults to an empty GHSA allowlist', async () => {
|
test('it defaults to an empty GHSA allowlist', async () => {
|
||||||
const options = readConfig()
|
const config = await readConfig()
|
||||||
expect(options.allow_ghsas).toEqual(undefined)
|
expect(config.allow_ghsas).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it successfully parses GHSA allowlist', async () => {
|
test('it successfully parses GHSA allowlist', async () => {
|
||||||
setInput('allow-ghsas', 'GHSA-abcd-1234-5679, GHSA-efgh-1234-5679')
|
setInput('allow-ghsas', 'GHSA-abcd-1234-5679, GHSA-efgh-1234-5679')
|
||||||
const options = readConfig()
|
const config = await readConfig()
|
||||||
expect(options.allow_ghsas).toEqual([
|
expect(config.allow_ghsas).toEqual([
|
||||||
'GHSA-abcd-1234-5679',
|
'GHSA-abcd-1234-5679',
|
||||||
'GHSA-efgh-1234-5679'
|
'GHSA-efgh-1234-5679'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('it defaults to checking licenses', async () => {
|
||||||
|
const config = await readConfig()
|
||||||
|
expect(config.license_check).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it parses the license-check input', async () => {
|
||||||
|
setInput('license-check', 'false')
|
||||||
|
let config = await readConfig()
|
||||||
|
expect(config.license_check).toEqual(false)
|
||||||
|
|
||||||
|
clearInputs()
|
||||||
|
setInput('license-check', 'true')
|
||||||
|
config = await readConfig()
|
||||||
|
expect(config.license_check).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it defaults to checking vulnerabilities', async () => {
|
||||||
|
const config = await readConfig()
|
||||||
|
expect(config.vulnerability_check).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it parses the vulnerability-check input', async () => {
|
||||||
|
setInput('vulnerability-check', 'false')
|
||||||
|
let config = await readConfig()
|
||||||
|
expect(config.vulnerability_check).toEqual(false)
|
||||||
|
|
||||||
|
clearInputs()
|
||||||
|
setInput('vulnerability-check', 'true')
|
||||||
|
config = await readConfig()
|
||||||
|
expect(config.vulnerability_check).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it is not possible to disable both checks', async () => {
|
||||||
|
setInput('license-check', 'false')
|
||||||
|
setInput('vulnerability-check', 'false')
|
||||||
|
await expect(readConfig()).rejects.toThrow(
|
||||||
|
/Can't disable both license-check and vulnerability-check/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('licenses that are not valid SPDX licenses', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it raises an error for invalid licenses in allow-licenses', async () => {
|
||||||
|
setInput('allow-licenses', ' BSD, GPL 2')
|
||||||
|
await expect(readConfig()).rejects.toThrow(
|
||||||
|
'Invalid license(s) in allow-licenses: BSD, GPL 2'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it raises an error for invalid licenses in deny-licenses', async () => {
|
||||||
|
setInput('deny-licenses', ' BSD, GPL 2')
|
||||||
|
await expect(readConfig()).rejects.toThrow(
|
||||||
|
'Invalid license(s) in deny-licenses: BSD, GPL 2'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fail_on_severity: critical
|
||||||
|
allow_licenses: []
|
||||||
+52
-33
@@ -1,6 +1,7 @@
|
|||||||
import {expect, jest, test} from '@jest/globals'
|
import {expect, jest, test} from '@jest/globals'
|
||||||
import {Change, Changes} from '../src/schemas'
|
import {Change, Changes} from '../src/schemas'
|
||||||
import {getDeniedLicenseChanges} from '../src/licenses'
|
|
||||||
|
let getInvalidLicenseChanges: Function
|
||||||
|
|
||||||
let npmChange: Change = {
|
let npmChange: Change = {
|
||||||
manifest: 'package.json',
|
manifest: 'package.json',
|
||||||
@@ -70,65 +71,83 @@ jest.mock('octokit', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it fails if a license outside the allow list is found', async () => {
|
beforeEach(async () => {
|
||||||
|
jest.resetModules()
|
||||||
|
jest.doMock('spdx-satisfies', () => {
|
||||||
|
// mock spdx-satisfies return value
|
||||||
|
// true for BSD, false for all others
|
||||||
|
return jest.fn((license: string, _: string): boolean => license === 'BSD')
|
||||||
|
})
|
||||||
|
;({getInvalidLicenseChanges} = require('../src/licenses'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it adds license outside the allow list to forbidden changes', async () => {
|
||||||
const changes: Changes = [npmChange, rubyChange]
|
const changes: Changes = [npmChange, rubyChange]
|
||||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
const {forbidden} = await getInvalidLicenseChanges(changes, {
|
||||||
allow: ['BSD']
|
allow: ['BSD']
|
||||||
})
|
})
|
||||||
expect(invalidChanges[0]).toBe(npmChange)
|
expect(forbidden[0]).toBe(npmChange)
|
||||||
|
expect(forbidden.length).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it fails if a license inside the deny list is found', async () => {
|
test('it adds license inside the deny list to forbidden changes', async () => {
|
||||||
const changes: Changes = [npmChange, rubyChange]
|
const changes: Changes = [npmChange, rubyChange]
|
||||||
const [invalidChanges] = await getDeniedLicenseChanges(changes, {
|
const {forbidden} = await getInvalidLicenseChanges(changes, {
|
||||||
deny: ['BSD']
|
deny: ['BSD']
|
||||||
})
|
})
|
||||||
expect(invalidChanges[0]).toBe(rubyChange)
|
expect(forbidden[0]).toBe(rubyChange)
|
||||||
|
expect(forbidden.length).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
// This is more of a "here's a behavior that might be surprising" than an actual
|
test('it does not add license outside the allow list to forbidden changes if it is in removed changes', 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, _] = await getDeniedLicenseChanges(changes, {
|
|
||||||
allow: [],
|
|
||||||
deny: ['BSD']
|
|
||||||
})
|
|
||||||
expect(invalidChanges.length).toBe(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('it does not fail if a license outside the allow list is found in removed changes', async () => {
|
|
||||||
const changes: Changes = [
|
const changes: Changes = [
|
||||||
{...npmChange, change_type: 'removed'},
|
{...npmChange, change_type: 'removed'},
|
||||||
{...rubyChange, change_type: 'removed'}
|
{...rubyChange, change_type: 'removed'}
|
||||||
]
|
]
|
||||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
const {forbidden} = await getInvalidLicenseChanges(changes, {
|
||||||
allow: ['BSD']
|
allow: ['BSD']
|
||||||
})
|
})
|
||||||
expect(invalidChanges).toStrictEqual([])
|
expect(forbidden).toStrictEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it does not fail if a license inside the deny list is found in removed changes', async () => {
|
test('it does not add license inside the deny list to forbidden changes if it is in removed changes', async () => {
|
||||||
const changes: Changes = [
|
const changes: Changes = [
|
||||||
{...npmChange, change_type: 'removed'},
|
{...npmChange, change_type: 'removed'},
|
||||||
{...rubyChange, change_type: 'removed'}
|
{...rubyChange, change_type: 'removed'}
|
||||||
]
|
]
|
||||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
const {forbidden} = await getInvalidLicenseChanges(changes, {
|
||||||
deny: ['BSD']
|
deny: ['BSD']
|
||||||
})
|
})
|
||||||
expect(invalidChanges).toStrictEqual([])
|
expect(forbidden).toStrictEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it fails if a license outside the allow list is found in both of added and removed changes', async () => {
|
test('it adds license outside the allow list to forbidden changes if it is in both added and removed changes', async () => {
|
||||||
const changes: Changes = [
|
const changes: Changes = [
|
||||||
{...npmChange, change_type: 'removed'},
|
{...npmChange, change_type: 'removed'},
|
||||||
npmChange,
|
npmChange,
|
||||||
{...rubyChange, change_type: 'removed'}
|
{...rubyChange, change_type: 'removed'}
|
||||||
]
|
]
|
||||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
const {forbidden} = await getInvalidLicenseChanges(changes, {
|
||||||
allow: ['BSD']
|
allow: ['BSD']
|
||||||
})
|
})
|
||||||
expect(invalidChanges).toStrictEqual([npmChange])
|
expect(forbidden).toStrictEqual([npmChange])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it adds all licenses to unresolved if it is unable to determine the validity', async () => {
|
||||||
|
jest.resetModules() // reset module set in before
|
||||||
|
jest.doMock('spdx-satisfies', () => {
|
||||||
|
return jest.fn((_first: string, _second: string) => {
|
||||||
|
throw new Error('Some Error')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
;({getInvalidLicenseChanges} = require('../src/licenses'))
|
||||||
|
const changes: Changes = [npmChange, rubyChange]
|
||||||
|
const invalidLicenses = await getInvalidLicenseChanges(changes, {
|
||||||
|
allow: ['BSD']
|
||||||
|
})
|
||||||
|
expect(invalidLicenses.forbidden.length).toEqual(0)
|
||||||
|
expect(invalidLicenses.unlicensed.length).toEqual(0)
|
||||||
|
expect(invalidLicenses.unresolved.length).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('GH License API fallback', () => {
|
describe('GH License API fallback', () => {
|
||||||
@@ -138,7 +157,7 @@ describe('GH License API fallback', () => {
|
|||||||
license: null,
|
license: null,
|
||||||
source_repository_url: 'http://github.com/some-owner/some-repo'
|
source_repository_url: 'http://github.com/some-owner/some-repo'
|
||||||
}
|
}
|
||||||
const [_, unknownChanges] = await getDeniedLicenseChanges(
|
const {unlicensed} = await getInvalidLicenseChanges(
|
||||||
[nullLicenseChange, rubyChange],
|
[nullLicenseChange, rubyChange],
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
@@ -147,25 +166,25 @@ describe('GH License API fallback', () => {
|
|||||||
owner: 'some-owner',
|
owner: 'some-owner',
|
||||||
repo: 'some-repo'
|
repo: 'some-repo'
|
||||||
})
|
})
|
||||||
expect(unknownChanges.length).toEqual(0)
|
expect(unlicensed.length).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it does not call licenses API endpoint for change with null license and invalid source_repository_url ', async () => {
|
test('it does not call licenses API endpoint for change with null license and invalid source_repository_url ', async () => {
|
||||||
const [_, unknownChanges] = await getDeniedLicenseChanges(
|
const {unlicensed} = await getInvalidLicenseChanges(
|
||||||
[{...npmChange, license: null}],
|
[{...npmChange, license: null}],
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
|
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
|
||||||
expect(unknownChanges.length).toEqual(1)
|
expect(unlicensed.length).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('it does not call licenses API endpoint if licenses for all changes are present', async () => {
|
test('it does not call licenses API endpoint if licenses for all changes are present', async () => {
|
||||||
const [_, unknownChanges] = await getDeniedLicenseChanges(
|
const {unlicensed} = await getInvalidLicenseChanges(
|
||||||
[npmChange, rubyChange],
|
[npmChange, rubyChange],
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
|
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
|
||||||
expect(unknownChanges.length).toEqual(0)
|
expect(unlicensed.length).toEqual(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+10
-1
@@ -21,7 +21,7 @@ inputs:
|
|||||||
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:
|
config-file:
|
||||||
description: A filepath to the configuration file for the action.
|
description: A path to the configuration file for the action.
|
||||||
required: false
|
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")
|
||||||
@@ -32,6 +32,15 @@ inputs:
|
|||||||
allow-ghsas:
|
allow-ghsas:
|
||||||
description: Comma-separated list of allowed Github Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
|
description: Comma-separated list of allowed Github Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
|
||||||
required: false
|
required: false
|
||||||
|
external-repo-token:
|
||||||
|
description: A token for fetching external configuration file if it lives in another repository. It is required if the repository is private
|
||||||
|
required: false
|
||||||
|
license-check:
|
||||||
|
description: A boolean to determine if license checks should be performed
|
||||||
|
required: false
|
||||||
|
vulnerability-check:
|
||||||
|
description: A boolean to determine if vulnerability checks should be performed
|
||||||
|
required: false
|
||||||
runs:
|
runs:
|
||||||
using: 'node16'
|
using: 'node16'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
|
|||||||
+2629
-6525
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+108
@@ -517,6 +517,31 @@ The above copyright notice and this permission notice shall be included in all c
|
|||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
array-find-index
|
||||||
|
MIT
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) Sindre Sorhus <[email protected]> (sindresorhus.com)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
before-after-hook
|
before-after-hook
|
||||||
Apache-2.0
|
Apache-2.0
|
||||||
Apache License
|
Apache License
|
||||||
@@ -1590,6 +1615,89 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
|||||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
spdx-compare
|
||||||
|
MIT
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2015 Kyle E. Mitchell
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
spdx-exceptions
|
||||||
|
CC-BY-3.0
|
||||||
|
|
||||||
|
spdx-expression-parse
|
||||||
|
MIT
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2015 Kyle E. Mitchell & other authors listed in AUTHORS
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
spdx-license-ids
|
||||||
|
CC0-1.0
|
||||||
|
|
||||||
|
spdx-ranges
|
||||||
|
(MIT AND CC-BY-3.0)
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2015 Kyle E. Mitchell
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
spdx-satisfies
|
||||||
|
MIT
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) spdx-satisfies.js contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
tr46
|
tr46
|
||||||
MIT
|
MIT
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
moduleFileExtensions: ['js', 'ts'],
|
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||||
testMatch: ['**/*.test.ts'],
|
testMatch: ['**/*.test.ts'],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.ts$': 'ts-jest'
|
'^.+\\.ts$': 'ts-jest'
|
||||||
},
|
},
|
||||||
verbose: true
|
verbose: true
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2997
-3277
File diff suppressed because it is too large
Load Diff
+13
-9
@@ -27,25 +27,29 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/github": "^5.1.1",
|
"@actions/github": "^5.1.1",
|
||||||
"@octokit/plugin-retry": "^3.0.9",
|
"@octokit/plugin-retry": "^4.0.3",
|
||||||
"@octokit/request-error": "^3.0.2",
|
"@octokit/request-error": "^3.0.2",
|
||||||
"ansi-styles": "^6.2.1",
|
"ansi-styles": "^6.2.1",
|
||||||
"got": "^12.5.2",
|
"got": "^12.5.2",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^2.0.20",
|
||||||
"octokit": "^2.0.9",
|
"octokit": "^2.0.10",
|
||||||
|
"spdx-expression-parse": "^3.0.1",
|
||||||
|
"spdx-satisfies": "^5.0.1",
|
||||||
"yaml": "^2.1.3",
|
"yaml": "^2.1.3",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.0",
|
"@types/node": "^16.18.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||||
"@typescript-eslint/parser": "^5.40.1",
|
"@typescript-eslint/parser": "^5.42.1",
|
||||||
|
"@types/spdx-expression-parse": "^3.0.2",
|
||||||
|
"@types/spdx-satisfies": "^0.1.0",
|
||||||
"@vercel/ncc": "^0.34.0",
|
"@vercel/ncc": "^0.34.0",
|
||||||
"esbuild-register": "^3.3.3",
|
"esbuild-register": "^3.4.1",
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.27.0",
|
||||||
"eslint-plugin-github": "^4.4.0",
|
"eslint-plugin-github": "^4.4.1",
|
||||||
"eslint-plugin-jest": "^27.1.3",
|
"eslint-plugin-jest": "^27.1.5",
|
||||||
"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.20",
|
||||||
|
|||||||
+134
-61
@@ -3,12 +3,62 @@ import path from 'path'
|
|||||||
import YAML from 'yaml'
|
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, ConfigurationOptionsSchema} from './schemas'
|
||||||
ConfigurationOptions,
|
import {isSPDXValid, octokitClient} from './utils'
|
||||||
ConfigurationOptionsSchema,
|
|
||||||
SeveritySchema,
|
type ConfigurationOptionsPartial = Partial<ConfigurationOptions>
|
||||||
SCOPES
|
|
||||||
} from './schemas'
|
export async function readConfig(): Promise<ConfigurationOptions> {
|
||||||
|
const inlineConfig = readInlineConfig()
|
||||||
|
|
||||||
|
const configFile = getOptionalInput('config-file')
|
||||||
|
if (configFile !== undefined) {
|
||||||
|
const externalConfig = await readConfigFile(configFile)
|
||||||
|
|
||||||
|
return ConfigurationOptionsSchema.parse({
|
||||||
|
...externalConfig,
|
||||||
|
...inlineConfig
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConfigurationOptionsSchema.parse(inlineConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInlineConfig(): ConfigurationOptionsPartial {
|
||||||
|
const fail_on_severity = getOptionalInput('fail-on-severity')
|
||||||
|
const fail_on_scopes = parseList(getOptionalInput('fail-on-scopes'))
|
||||||
|
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
|
||||||
|
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
|
||||||
|
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
|
||||||
|
const license_check = getOptionalBoolean('license-check')
|
||||||
|
const vulnerability_check = getOptionalBoolean('vulnerability-check')
|
||||||
|
const base_ref = getOptionalInput('base-ref')
|
||||||
|
const head_ref = getOptionalInput('head-ref')
|
||||||
|
|
||||||
|
validateLicenses('allow-licenses', allow_licenses)
|
||||||
|
validateLicenses('deny-licenses', deny_licenses)
|
||||||
|
|
||||||
|
const keys = {
|
||||||
|
fail_on_severity,
|
||||||
|
fail_on_scopes,
|
||||||
|
allow_licenses,
|
||||||
|
deny_licenses,
|
||||||
|
allow_ghsas,
|
||||||
|
license_check,
|
||||||
|
vulnerability_check,
|
||||||
|
base_ref,
|
||||||
|
head_ref
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(keys).filter(([_, value]) => value !== undefined)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptionalBoolean(name: string): boolean | undefined {
|
||||||
|
const value = core.getInput(name)
|
||||||
|
return value.length > 0 ? core.getBooleanInput(name) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
function getOptionalInput(name: string): string | undefined {
|
function getOptionalInput(name: string): string | undefined {
|
||||||
const value = core.getInput(name)
|
const value = core.getInput(name)
|
||||||
@@ -23,70 +73,93 @@ function parseList(list: string | undefined): string[] | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readConfig(): ConfigurationOptions {
|
function validateLicenses(
|
||||||
const externalConfig = getOptionalInput('config-file')
|
key: 'allow-licenses' | 'deny-licenses',
|
||||||
if (externalConfig !== undefined) {
|
licenses: string[] | undefined
|
||||||
const config = readConfigFile(externalConfig)
|
): void {
|
||||||
// the reasoning behind reading the inline config when an external
|
if (licenses === undefined) {
|
||||||
// config file is provided is that we still want to allow users to
|
return
|
||||||
// pass inline options in the presence of an external config file.
|
}
|
||||||
const inlineConfig = readInlineConfig()
|
const invalid_licenses = licenses.filter(license => !isSPDXValid(license))
|
||||||
// the external config takes precedence
|
|
||||||
return Object.assign({}, inlineConfig, config)
|
if (invalid_licenses.length > 0) {
|
||||||
} else {
|
throw new Error(
|
||||||
return readInlineConfig()
|
`Invalid license(s) in ${key}: ${invalid_licenses.join(', ')}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readInlineConfig(): ConfigurationOptions {
|
async function readConfigFile(
|
||||||
const fail_on_severity = SeveritySchema.parse(
|
filePath: string
|
||||||
getOptionalInput('fail-on-severity')
|
): Promise<ConfigurationOptionsPartial> {
|
||||||
|
// match a remote config (e.g. 'owner/repo/filepath@someref')
|
||||||
|
const format = new RegExp(
|
||||||
|
'(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)'
|
||||||
)
|
)
|
||||||
const fail_on_scopes = z
|
|
||||||
.array(z.enum(SCOPES))
|
|
||||||
.default(['runtime'])
|
|
||||||
.parse(parseList(getOptionalInput('fail-on-scopes')))
|
|
||||||
|
|
||||||
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
|
let data: string
|
||||||
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
|
const pieces = format.exec(filePath)
|
||||||
|
|
||||||
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,
|
|
||||||
fail_on_scopes,
|
|
||||||
allow_licenses,
|
|
||||||
deny_licenses,
|
|
||||||
allow_ghsas,
|
|
||||||
base_ref,
|
|
||||||
head_ref
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readConfigFile(filePath: string): ConfigurationOptions {
|
|
||||||
let data
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data = fs.readFileSync(path.resolve(filePath), 'utf-8')
|
if (pieces?.groups && pieces.length === 5) {
|
||||||
} catch (error: unknown) {
|
data = await getRemoteConfig({
|
||||||
|
owner: pieces.groups.owner,
|
||||||
|
repo: pieces.groups.repo,
|
||||||
|
path: pieces.groups.path,
|
||||||
|
ref: pieces.groups.ref
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
data = fs.readFileSync(path.resolve(filePath), 'utf-8')
|
||||||
|
}
|
||||||
|
return parseConfigFile(data)
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(error as string)
|
||||||
|
throw new Error('Unable to fetch config file')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfigFile(configData: string): ConfigurationOptionsPartial {
|
||||||
|
try {
|
||||||
|
const data = YAML.parse(configData)
|
||||||
|
for (const key of Object.keys(data)) {
|
||||||
|
if (key === 'allow-licenses' || key === 'deny-licenses') {
|
||||||
|
validateLicenses(key, data[key])
|
||||||
|
}
|
||||||
|
// get rid of the ugly dashes from the actions conventions
|
||||||
|
if (key.includes('-')) {
|
||||||
|
data[key.replace(/-/g, '_')] = data[key]
|
||||||
|
delete data[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
} catch (error) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
data = YAML.parse(data)
|
}
|
||||||
|
|
||||||
// get rid of the ugly dashes from the actions conventions
|
async function getRemoteConfig(configOpts: {
|
||||||
for (const key of Object.keys(data)) {
|
[key: string]: string
|
||||||
if (key.includes('-')) {
|
}): Promise<string> {
|
||||||
data[key.replace(/-/g, '_')] = data[key]
|
try {
|
||||||
delete data[key]
|
const {data} = await octokitClient(
|
||||||
}
|
'external-repo-token',
|
||||||
|
false
|
||||||
|
).rest.repos.getContent({
|
||||||
|
mediaType: {
|
||||||
|
format: 'raw'
|
||||||
|
},
|
||||||
|
owner: configOpts.owner,
|
||||||
|
repo: configOpts.repo,
|
||||||
|
path: configOpts.path,
|
||||||
|
ref: configOpts.ref
|
||||||
|
})
|
||||||
|
|
||||||
|
// When using mediaType.format = 'raw', the response.data is a string
|
||||||
|
// but this is not reflected in the return type of getContent, so we're
|
||||||
|
// casting the return value to a string.
|
||||||
|
return z.string().parse(data as unknown)
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(error as string)
|
||||||
|
throw new Error('Error fetching remote config file')
|
||||||
}
|
}
|
||||||
const values = ConfigurationOptionsSchema.parse(data)
|
|
||||||
return values
|
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-31
@@ -1,6 +1,6 @@
|
|||||||
import * as core from '@actions/core'
|
import spdxSatisfies from 'spdx-satisfies'
|
||||||
import {Octokit} from 'octokit'
|
import {Change, Changes} from './schemas'
|
||||||
import {Change} from './schemas'
|
import {isSPDXValid, octokitClient} from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loops through a list of changes, filtering and returning the
|
* Loops through a list of changes, filtering and returning the
|
||||||
@@ -12,60 +12,73 @@ import {Change} from './schemas'
|
|||||||
* we will ignore the deny list.
|
* we will ignore the deny list.
|
||||||
* @param {Change[]} changes The list of changes to filter.
|
* @param {Change[]} changes The list of changes to filter.
|
||||||
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
|
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
|
||||||
* @returns {Promise<[Array.<Change>, Array.<Change>]>} A promise to a 2 element tuple. The first element is the list of denied changes and the second one is the list of changes with unknown licenses
|
* @returns {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
|
||||||
*/
|
*/
|
||||||
export async function getDeniedLicenseChanges(
|
export async function getInvalidLicenseChanges(
|
||||||
changes: Change[],
|
changes: Change[],
|
||||||
licenses: {
|
licenses: {
|
||||||
allow?: string[]
|
allow?: string[]
|
||||||
deny?: string[]
|
deny?: string[]
|
||||||
}
|
}
|
||||||
): Promise<[Change[], Change[]]> {
|
): Promise<Record<string, Changes>> {
|
||||||
const {allow, deny} = licenses
|
const {allow, deny} = licenses
|
||||||
|
|
||||||
const disallowed: Change[] = []
|
const groupedChanges = await groupChanges(changes)
|
||||||
const unknown: Change[] = []
|
const licensedChanges: Changes = groupedChanges.licensed
|
||||||
|
|
||||||
const consolidatedChanges = changes.some(
|
const invalidLicenseChanges: Record<string, Changes> = {
|
||||||
({source_repository_url, license}) => !license && source_repository_url
|
unlicensed: groupedChanges.unlicensed,
|
||||||
)
|
unresolved: [],
|
||||||
? await setGHLicenses(changes)
|
forbidden: []
|
||||||
: changes
|
}
|
||||||
|
|
||||||
for (const change of consolidatedChanges) {
|
const validityCache = new Map<string, boolean>()
|
||||||
if (change.change_type === 'removed') {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (const change of licensedChanges) {
|
||||||
const license = change.license
|
const license = change.license
|
||||||
|
|
||||||
|
// should never happen since licensedChanges always have licenses but license is nullable in changes schema
|
||||||
if (license === null) {
|
if (license === null) {
|
||||||
unknown.push(change)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (allow !== undefined) {
|
|
||||||
if (!allow.includes(license)) {
|
if (license === 'NOASSERTION') {
|
||||||
disallowed.push(change)
|
invalidLicenseChanges.unlicensed.push(change)
|
||||||
}
|
} else if (validityCache.get(license) === undefined) {
|
||||||
} else if (deny !== undefined) {
|
try {
|
||||||
if (deny.includes(license)) {
|
if (allow !== undefined) {
|
||||||
disallowed.push(change)
|
const found = allow.find(spdxExpression =>
|
||||||
|
spdxSatisfies(license, spdxExpression)
|
||||||
|
)
|
||||||
|
validityCache.set(license, found !== undefined)
|
||||||
|
} else if (deny !== undefined) {
|
||||||
|
const found = deny.find(spdxExpression =>
|
||||||
|
spdxSatisfies(license, spdxExpression)
|
||||||
|
)
|
||||||
|
validityCache.set(license, found === undefined)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
invalidLicenseChanges.unresolved.push(change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (validityCache.get(license) === false) {
|
||||||
|
invalidLicenseChanges.forbidden.push(change)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [disallowed, unknown]
|
return invalidLicenseChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchGHLicense = async (
|
const fetchGHLicense = async (
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string
|
repo: string
|
||||||
): Promise<string | null> => {
|
): Promise<string | null> => {
|
||||||
const octokit = new Octokit({
|
|
||||||
auth: core.getInput('repo-token', {required: true})
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await octokit.rest.licenses.getForRepo({owner, repo})
|
const response = await octokitClient().rest.licenses.getForRepo({
|
||||||
|
owner,
|
||||||
|
repo
|
||||||
|
})
|
||||||
return response.data.license?.spdx_id ?? null
|
return response.data.license?.spdx_id ?? null
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null
|
return null
|
||||||
@@ -108,3 +121,54 @@ const setGHLicenses = async (changes: Change[]): Promise<Change[]> => {
|
|||||||
|
|
||||||
return Promise.all(updatedChanges)
|
return Promise.all(updatedChanges)
|
||||||
}
|
}
|
||||||
|
// Currently Dependency Graph licenses are truncated to 255 characters
|
||||||
|
// This possibly makes them invalid spdx ids
|
||||||
|
const truncatedDGLicense = (license: string): boolean =>
|
||||||
|
license.length === 255 && !isSPDXValid(license)
|
||||||
|
|
||||||
|
async function groupChanges(
|
||||||
|
changes: Changes
|
||||||
|
): Promise<Record<string, Changes>> {
|
||||||
|
const result: Record<string, Changes> = {
|
||||||
|
licensed: [],
|
||||||
|
unlicensed: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const ghChanges = []
|
||||||
|
|
||||||
|
for (const change of changes) {
|
||||||
|
if (change.change_type === 'removed') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.license === null) {
|
||||||
|
if (change.source_repository_url !== null) {
|
||||||
|
ghChanges.push(change)
|
||||||
|
} else {
|
||||||
|
result.unlicensed.push(change)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
truncatedDGLicense(change.license) &&
|
||||||
|
change.source_repository_url !== null
|
||||||
|
) {
|
||||||
|
ghChanges.push(change)
|
||||||
|
} else {
|
||||||
|
result.licensed.push(change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ghChanges.length > 0) {
|
||||||
|
const ghLicenses = await setGHLicenses(ghChanges)
|
||||||
|
for (const change of ghLicenses) {
|
||||||
|
if (change.license === null) {
|
||||||
|
result.unlicensed.push(change)
|
||||||
|
} else {
|
||||||
|
result.licensed.push(change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
+36
-24
@@ -10,7 +10,7 @@ import {
|
|||||||
filterChangesByScopes,
|
filterChangesByScopes,
|
||||||
filterAllowedAdvisories
|
filterAllowedAdvisories
|
||||||
} from '../src/filter'
|
} from '../src/filter'
|
||||||
import {getDeniedLicenseChanges} from './licenses'
|
import {getInvalidLicenseChanges} from './licenses'
|
||||||
import * as summary from './summary'
|
import * as summary from './summary'
|
||||||
import {getRefs} from './git-refs'
|
import {getRefs} from './git-refs'
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ import {groupDependenciesByManifest} from './utils'
|
|||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const config = readConfig()
|
const config = await readConfig()
|
||||||
const refs = getRefs(config, github.context)
|
const refs = getRefs(config, github.context)
|
||||||
|
|
||||||
const changes = await dependencyGraph.compare({
|
const changes = await dependencyGraph.compare({
|
||||||
@@ -28,7 +28,7 @@ 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)
|
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
|
||||||
const filteredChanges = filterAllowedAdvisories(
|
const filteredChanges = filterAllowedAdvisories(
|
||||||
config.allow_ghsas,
|
config.allow_ghsas,
|
||||||
@@ -45,7 +45,7 @@ async function run(): Promise<void> {
|
|||||||
change.vulnerabilities.length > 0
|
change.vulnerabilities.length > 0
|
||||||
)
|
)
|
||||||
|
|
||||||
const [licenseErrors, unknownLicenses] = await getDeniedLicenseChanges(
|
const invalidLicenseChanges = await getInvalidLicenseChanges(
|
||||||
filteredChanges,
|
filteredChanges,
|
||||||
{
|
{
|
||||||
allow: config.allow_licenses,
|
allow: config.allow_licenses,
|
||||||
@@ -53,13 +53,21 @@ async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses)
|
summary.addSummaryToSummary(
|
||||||
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
|
config.vulnerability_check ? addedChanges : null,
|
||||||
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config)
|
config.license_check ? invalidLicenseChanges : null
|
||||||
summary.addScannedDependencies(changes)
|
)
|
||||||
|
|
||||||
printVulnerabilitiesBlock(addedChanges, minSeverity)
|
if (config.vulnerability_check) {
|
||||||
printLicensesBlock(licenseErrors, unknownLicenses)
|
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
|
||||||
|
printVulnerabilitiesBlock(addedChanges, minSeverity)
|
||||||
|
}
|
||||||
|
if (config.license_check) {
|
||||||
|
summary.addLicensesToSummary(invalidLicenseChanges, config)
|
||||||
|
printLicensesBlock(invalidLicenseChanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.addScannedDependencies(changes)
|
||||||
printScannedDependencies(changes)
|
printScannedDependencies(changes)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError && error.status === 404) {
|
if (error instanceof RequestError && error.status === 404) {
|
||||||
@@ -83,7 +91,7 @@ async function run(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printVulnerabilitiesBlock(
|
function printVulnerabilitiesBlock(
|
||||||
addedChanges: Change[],
|
addedChanges: Changes,
|
||||||
minSeverity: Severity
|
minSeverity: Severity
|
||||||
): void {
|
): void {
|
||||||
let failed = false
|
let failed = false
|
||||||
@@ -119,24 +127,28 @@ function printChangeVulnerabilities(change: Change): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printLicensesBlock(
|
function printLicensesBlock(
|
||||||
licenseErrors: Change[],
|
invalidLicenseChanges: Record<string, Changes>
|
||||||
unknownLicenses: Change[]
|
|
||||||
): void {
|
): void {
|
||||||
core.group('Licenses', async () => {
|
core.group('Licenses', async () => {
|
||||||
if (licenseErrors.length > 0) {
|
if (invalidLicenseChanges.forbidden.length > 0) {
|
||||||
printLicensesError(licenseErrors)
|
core.info('\nThe following dependencies have incompatible licenses:')
|
||||||
|
printLicensesError(invalidLicenseChanges.forbidden)
|
||||||
core.setFailed('Dependency review detected incompatible licenses.')
|
core.setFailed('Dependency review detected incompatible licenses.')
|
||||||
}
|
}
|
||||||
printNullLicenses(unknownLicenses)
|
if (invalidLicenseChanges.unresolved.length > 0) {
|
||||||
|
core.warning(
|
||||||
|
'\nThe validity of the licenses of the dependencies below could not be determined. Ensure that they are valid SPDX licenses:'
|
||||||
|
)
|
||||||
|
printLicensesError(invalidLicenseChanges.unresolved)
|
||||||
|
core.setFailed(
|
||||||
|
'Dependency review could not detect the validity of all licenses.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
printNullLicenses(invalidLicenseChanges.unlicensed)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function printLicensesError(changes: Change[]): void {
|
function printLicensesError(changes: Changes): void {
|
||||||
if (changes.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info('\nThe following dependencies have incompatible licenses:\n')
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
core.info(
|
core.info(
|
||||||
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close} – License: ${styles.color.red.open}${change.license}${styles.color.red.close}`
|
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close} – License: ${styles.color.red.open}${change.license}${styles.color.red.close}`
|
||||||
@@ -144,12 +156,12 @@ function printLicensesError(changes: Change[]): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printNullLicenses(changes: Change[]): void {
|
function printNullLicenses(changes: Changes): void {
|
||||||
if (changes.length === 0) {
|
if (changes.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
core.info('\nWe could not detect a license for the following dependencies:\n')
|
core.info('\nWe could not detect a license for the following dependencies:')
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
core.info(
|
core.info(
|
||||||
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close}`
|
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close}`
|
||||||
|
|||||||
+30
-10
@@ -38,18 +38,38 @@ export const ConfigurationOptionsSchema = z
|
|||||||
.object({
|
.object({
|
||||||
fail_on_severity: SeveritySchema,
|
fail_on_severity: SeveritySchema,
|
||||||
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
||||||
allow_licenses: z.array(z.string()).default([]),
|
allow_licenses: z.array(z.string()).optional(),
|
||||||
deny_licenses: z.array(z.string()).default([]),
|
deny_licenses: z.array(z.string()).optional(),
|
||||||
allow_ghsas: z.array(z.string()).default([]),
|
allow_ghsas: z.array(z.string()).default([]),
|
||||||
config_file: z.string().optional().default('false'),
|
license_check: z.boolean().default(true),
|
||||||
base_ref: z.string(),
|
vulnerability_check: z.boolean().default(true),
|
||||||
head_ref: z.string()
|
config_file: z.string().optional(),
|
||||||
|
base_ref: z.string().optional(),
|
||||||
|
head_ref: z.string().optional()
|
||||||
|
})
|
||||||
|
.superRefine((config, context) => {
|
||||||
|
if (config.allow_licenses && config.deny_licenses) {
|
||||||
|
context.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: 'You cannot specify both allow-licenses and deny-licenses'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (config.allow_licenses && config.allow_licenses.length < 1) {
|
||||||
|
context.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: 'You should provide at least one license in allow-licenses'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
config.license_check === false &&
|
||||||
|
config.vulnerability_check === false
|
||||||
|
) {
|
||||||
|
context.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Can't disable both license-check and vulnerability-check"
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.partial()
|
|
||||||
.refine(
|
|
||||||
obj => !(obj.allow_licenses && obj.deny_licenses),
|
|
||||||
'Your workflow file has both an allow_licenses list and deny_licenses list, but you can only set one or the other.'
|
|
||||||
)
|
|
||||||
|
|
||||||
export const ChangesSchema = z.array(ChangeSchema)
|
export const ChangesSchema = z.array(ChangeSchema)
|
||||||
|
|
||||||
|
|||||||
+52
-47
@@ -1,18 +1,27 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {ConfigurationOptions, Change, Changes} from './schemas'
|
import {ConfigurationOptions, Changes} from './schemas'
|
||||||
import {SummaryTableRow} from '@actions/core/lib/summary'
|
import {SummaryTableRow} from '@actions/core/lib/summary'
|
||||||
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
||||||
|
|
||||||
export function addSummaryToSummary(
|
export function addSummaryToSummary(
|
||||||
addedPackages: Changes,
|
addedPackages: Changes | null,
|
||||||
licenseErrors: Change[],
|
invalidLicenseChanges: Record<string, Changes> | null
|
||||||
unknownLicenses: Change[]
|
|
||||||
): void {
|
): void {
|
||||||
core.summary
|
core.summary
|
||||||
.addHeading('Dependency Review')
|
.addHeading('Dependency Review')
|
||||||
.addRaw(
|
.addRaw('We found:')
|
||||||
`We found ${addedPackages.length} vulnerable package(s), ${licenseErrors.length} package(s) with incompatible licenses, and ${unknownLicenses.length} package(s) with unknown licenses.`
|
.addList([
|
||||||
)
|
...(addedPackages
|
||||||
|
? [`${addedPackages.length} vulnerable package(s)`]
|
||||||
|
: []),
|
||||||
|
...(invalidLicenseChanges
|
||||||
|
? [
|
||||||
|
`${invalidLicenseChanges.unresolved.length} package(s) with invalid SPDX license definitions`,
|
||||||
|
`${invalidLicenseChanges.forbidden.length} package(s) with incompatible licenses`,
|
||||||
|
`${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.`
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addChangeVulnerabilitiesToSummary(
|
export function addChangeVulnerabilitiesToSummary(
|
||||||
@@ -76,8 +85,7 @@ export function addChangeVulnerabilitiesToSummary(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function addLicensesToSummary(
|
export function addLicensesToSummary(
|
||||||
licenseErrors: Change[],
|
invalidLicenseChanges: Record<string, Changes>,
|
||||||
unknownLicenses: Change[],
|
|
||||||
config: ConfigurationOptions
|
config: ConfigurationOptions
|
||||||
): void {
|
): void {
|
||||||
core.summary.addHeading('Licenses')
|
core.summary.addHeading('Licenses')
|
||||||
@@ -93,62 +101,59 @@ export function addLicensesToSummary(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (licenseErrors.length === 0 && unknownLicenses.length === 0) {
|
if (Object.values(invalidLicenseChanges).every(item => item.length === 0)) {
|
||||||
core.summary.addQuote('No license violations detected.')
|
core.summary.addQuote('No license violations detected.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (licenseErrors.length > 0) {
|
core.debug(
|
||||||
const rows: SummaryTableRow[] = []
|
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
|
||||||
const manifests = getManifestsSet(licenseErrors)
|
)
|
||||||
|
|
||||||
core.summary.addHeading('Incompatible Licenses', 3).addSeparator()
|
core.debug(
|
||||||
|
`${invalidLicenseChanges.unresolved.length} licenses could not be validated`
|
||||||
|
)
|
||||||
|
|
||||||
|
printLicenseViolation(
|
||||||
|
'Incompatible Licenses',
|
||||||
|
invalidLicenseChanges.forbidden
|
||||||
|
)
|
||||||
|
printLicenseViolation('Unknown Licenses', invalidLicenseChanges.unlicensed)
|
||||||
|
printLicenseViolation(
|
||||||
|
'Invalid SPDX License Definitions',
|
||||||
|
invalidLicenseChanges.unresolved
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function printLicenseViolation(heading: string, changes: Changes): void {
|
||||||
|
core.summary.addHeading(heading, 5).addSeparator()
|
||||||
|
|
||||||
|
if (changes.length > 0) {
|
||||||
|
const rows: SummaryTableRow[] = []
|
||||||
|
const manifests = getManifestsSet(changes)
|
||||||
|
|
||||||
for (const manifest of manifests) {
|
for (const manifest of manifests) {
|
||||||
core.summary.addHeading(`<em>${manifest}</em>`, 4)
|
core.summary.addHeading(`<em>${manifest}</em>`, 4)
|
||||||
|
|
||||||
for (const change of licenseErrors.filter(
|
for (const change of changes.filter(pkg => pkg.manifest === manifest)) {
|
||||||
pkg => pkg.manifest === manifest
|
|
||||||
)) {
|
|
||||||
rows.push([
|
rows.push([
|
||||||
renderUrl(change.source_repository_url, change.name),
|
renderUrl(change.source_repository_url, change.name),
|
||||||
change.version,
|
change.version,
|
||||||
change.license || ''
|
formatLicense(change.license)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
core.summary.addTable([['Package', 'Version', 'License'], ...rows])
|
core.summary.addTable([['Package', 'Version', 'License'], ...rows])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
core.summary.addQuote('No license violations detected.')
|
core.summary.addQuote(`No ${heading.toLowerCase()} detected.`)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
core.debug(`found ${unknownLicenses.length} unknown licenses`)
|
function formatLicense(license: string | null): string {
|
||||||
|
if (license === null || license === 'NOASSERTION') {
|
||||||
if (unknownLicenses.length > 0) {
|
return 'Null'
|
||||||
const rows: SummaryTableRow[] = []
|
|
||||||
const manifests = getManifestsSet(unknownLicenses)
|
|
||||||
|
|
||||||
core.debug(
|
|
||||||
`found ${manifests.entries.length} manifests for unknown licenses`
|
|
||||||
)
|
|
||||||
|
|
||||||
core.summary.addHeading('Unknown Licenses', 3).addSeparator()
|
|
||||||
|
|
||||||
for (const manifest of manifests) {
|
|
||||||
core.summary.addHeading(`<em>${manifest}</em>`, 4)
|
|
||||||
|
|
||||||
for (const change of unknownLicenses.filter(
|
|
||||||
pkg => pkg.manifest === manifest
|
|
||||||
)) {
|
|
||||||
rows.push([
|
|
||||||
renderUrl(change.source_repository_url, change.name),
|
|
||||||
change.version
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
core.summary.addTable([['Package', 'Version'], ...rows])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return license
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addScannedDependencies(changes: Changes): void {
|
export function addScannedDependencies(changes: Changes): void {
|
||||||
@@ -157,7 +162,7 @@ export function addScannedDependencies(changes: Changes): void {
|
|||||||
|
|
||||||
const summary = core.summary
|
const summary = core.summary
|
||||||
.addHeading('Scanned Dependencies')
|
.addHeading('Scanned Dependencies')
|
||||||
.addRaw(`We scanned ${dependencies.size} manifest files:`)
|
.addHeading(`We scanned ${dependencies.size} manifest files:`, 5)
|
||||||
|
|
||||||
for (const manifest of manifests) {
|
for (const manifest of manifests) {
|
||||||
const deps = dependencies.get(manifest)
|
const deps = dependencies.get(manifest)
|
||||||
@@ -165,7 +170,7 @@ export function addScannedDependencies(changes: Changes): void {
|
|||||||
const dependencyNames = deps.map(
|
const dependencyNames = deps.map(
|
||||||
dependency => `<li>${dependency.name}@${dependency.version}</li>`
|
dependency => `<li>${dependency.name}@${dependency.version}</li>`
|
||||||
)
|
)
|
||||||
summary.addRaw(`<h3>${manifest}</h3><ul>${dependencyNames.join('')}</ul>`)
|
summary.addDetails(manifest, `<ul>${dependencyNames.join('')}</ul>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import {Octokit} from 'octokit'
|
||||||
|
import spdxParse from 'spdx-expression-parse'
|
||||||
import {Changes} from './schemas'
|
import {Changes} from './schemas'
|
||||||
|
|
||||||
export function groupDependenciesByManifest(
|
export function groupDependenciesByManifest(
|
||||||
@@ -28,3 +31,25 @@ export function renderUrl(url: string | null, text: string): string {
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSPDXValid(license: string): boolean {
|
||||||
|
try {
|
||||||
|
spdxParse(license)
|
||||||
|
return true
|
||||||
|
} catch (_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function octokitClient(token = 'repo-token', required = true): Octokit {
|
||||||
|
const opts: Record<string, unknown> = {}
|
||||||
|
|
||||||
|
// auth is only added if token is present. For remote config files in public
|
||||||
|
// repos the token is optional, so it could be undefined.
|
||||||
|
const auth = core.getInput(token, {required})
|
||||||
|
if (auth !== undefined) {
|
||||||
|
opts['auth'] = auth
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Octokit(opts)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user