Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77a34f96cc |
@@ -2,5 +2,8 @@
|
||||
"name": "Dependency Review Action",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:18",
|
||||
"postCreateCommand": "npm install",
|
||||
"remoteUser": "node"
|
||||
"remoteUser": "node",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/ruby:1": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: daily
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: daily
|
||||
ignore:
|
||||
- dependency-name: '@types/node'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
@@ -21,13 +21,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
- name: Set Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -47,7 +46,7 @@ jobs:
|
||||
id: diff
|
||||
|
||||
# If index.js was different than expected, upload the expected version as an artifact
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||
with:
|
||||
name: dist
|
||||
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
@@ -27,10 +27,10 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 18
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '21 0 * * 4'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 360
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript-typescript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
config: |
|
||||
paths-ignore:
|
||||
- dist/index.js
|
||||
- dist/sourcemap-register.js
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@main
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Close stale PRs and Issues
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "00 0 * * *" # runs at 00:00 daily
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9.0.0
|
||||
name: Clean up stale PRs and Issues
|
||||
with:
|
||||
stale-pr-message: "👋 This pull request has been marked as stale because it has been open with no activity. You can: comment on the issue or remove the stale label to hold stale off for a while, add the `Keep` label to hold stale off permanently, or do nothing. If you do nothing, this pull request will be closed eventually by the stale bot. Please see CONTRIBUTING.md for more policy details."
|
||||
stale-pr-label: "Stale"
|
||||
stale-issue-label: "Stale"
|
||||
exempt-pr-labels: "Keep" # a "Keep" label will keep the PR from being closed as stale
|
||||
exempt-issue-labels: "Keep" # a "Keep" label will keep the issue from being closed as stale
|
||||
days-before-pr-stale: 180 # when the PR is considered stale
|
||||
days-before-pr-close: 15 # when the PR is closed by the bot
|
||||
days-before-issue-stale: 180 # when the issue is considered stale
|
||||
days-before-issue-close: 15 # when the issue is closed by the bot
|
||||
exempt-assignees: 'advanced-security-dependency-graph'
|
||||
ascending: true
|
||||
@@ -100,5 +100,3 @@ Thumbs.db
|
||||
# Ignore built ts files
|
||||
__tests__/runner/*
|
||||
lib/**/*
|
||||
|
||||
tmp
|
||||
|
||||
+4
-10
@@ -77,11 +77,9 @@ $ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_
|
||||
|
||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||
|
||||
- Add unit tests for new features.
|
||||
- Write tests.
|
||||
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
|
||||
- Write a [good commit message](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
- Add examples of the usage to [examples.md](examples.md)
|
||||
- Link to a sample PR in a custom repository running your version of the Action.
|
||||
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
|
||||
## Cutting a new release
|
||||
|
||||
@@ -114,14 +112,10 @@ minor/patch updates.
|
||||
To do this just checkout `main`, force-create a new annotated tag, and push it:
|
||||
|
||||
```
|
||||
git tag -fa v4 -m "Updating v4 to 4.0.1"
|
||||
git push origin v4 --force
|
||||
git tag -fa v2 -m "Updating v2 to 2.3.4"
|
||||
git push origin v2 --force
|
||||
```
|
||||
|
||||
## Stalebot
|
||||
|
||||
We have begun using a [Stalebot action](https://github.com/actions/stale) to help keep the Issues and Pull requests backlogs tidy. You can see the configuration [here](.github/workflows/stalebot.yml). If you'd like to keep an issue open after getting a stalebot warning, simply comment on it and it'll reset the clock.
|
||||
|
||||
## Resources
|
||||
|
||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# dependency-review-action
|
||||
|
||||
This action scans your pull requests for dependency changes, and will
|
||||
raise an error if any vulnerabilities or invalid licenses are being introduced. The action is supported by an [API endpoint](https://docs.github.com/rest/dependency-graph/dependency-review) that diffs the dependencies between any two revisions on your default branch.
|
||||
raise an error if any vulnerabilities or invalid licenses are being introduced. The action is supported by an [API endpoint](https://docs.github.com/en/rest/reference/dependency-graph#dependency-review) that diffs the dependencies between any two revisions.
|
||||
|
||||
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="850" alt="GitHub workflow run log showing Dependency Review job output" 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 width="850" alt="GitHub job summary showing Dependency Review output" src="https://github.com/actions/dependency-review-action/assets/2161/42fbed1d-64a7-42bf-9b05-c416bc67493f">
|
||||
<img src="https://user-images.githubusercontent.com/7847935/182871416-50332bbb-b279-4621-a136-ca72a4314301.png">
|
||||
|
||||
## Installation
|
||||
|
||||
**Please keep in mind that you need a [GitHub Advanced Security](https://docs.github.com/enterprise-cloud@latest/get-started/learning-about-github/about-github-advanced-security) license if you're running this action on private repositories.**
|
||||
**Please keep in mind that you need a [GitHub Advanced Security](https://docs.github.com/en/enterprise-cloud@latest/get-started/learning-about-github/about-github-advanced-security) license if you're running this action on private repositories.**
|
||||
|
||||
1. Add a new YAML workflow to your `.github/workflows` folder:
|
||||
|
||||
@@ -31,19 +31,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v2
|
||||
```
|
||||
|
||||
### GitHub Enterprise Server
|
||||
|
||||
Make sure
|
||||
This action is available in Enterprise Server starting with version 3.6. Make sure
|
||||
[GitHub Advanced
|
||||
Security](https://docs.github.com/enterprise-server@3.8/admin/code-security/managing-github-advanced-security-for-your-enterprise/enabling-github-advanced-security-for-your-enterprise)
|
||||
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
|
||||
Connect](https://docs.github.com/enterprise-server@3.8/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect)
|
||||
are enabled, and that you have installed the [dependency-review-action](https://github.com/actions/dependency-review-action) on the server.
|
||||
Connect](https://docs.github.com/en/enterprise-server@3.6/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect)
|
||||
are enabled.
|
||||
|
||||
You can use the same workflow as above, replacing the `runs-on` value
|
||||
with the label of any of your runners (the default label
|
||||
@@ -57,44 +57,165 @@ jobs:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v2
|
||||
```
|
||||
|
||||
## Configuration options
|
||||
## Configuration
|
||||
|
||||
Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional.
|
||||
Configure this action by either using an external configuration file,
|
||||
or by inlining these options in your workflow file.
|
||||
|
||||
| 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` |
|
||||
| `allow-dependencies-licenses`\* | Contains a list of packages that will be excluded from license checks. | Any package(s) in [purl](https://github.com/package-url/purl-spec) format | none |
|
||||
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
|
||||
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job the `pull-requests: write` permission. | `always`, `on-failure`, `never` | `never` |
|
||||
| `deny-packages` | Any number of packages to block in a PR. | Package(s) in [purl](https://github.com/package-url/purl-spec) format | empty |
|
||||
| `deny-groups` | Any number of groups (namespaces) to block in a PR. | Namespace(s) in [purl](https://github.com/package-url/purl-spec) format (no package name, no version number) | empty |
|
||||
| `retry-on-snapshot-warnings`\* | Enable or disable retrying the action every 10 seconds while waiting for dependency submission actions to complete. | `true`, `false` | `false` |
|
||||
| `retry-on-snapshot-warnings-timeout`\* | Maximum amount of time (in seconds) to retry the action while waiting for dependency submission actions to complete. | Any positive integer | 120 |
|
||||
| `warn-only`+ | When set to `true`, the action will log all vulnerabilities as warnings regardless of the severity, and the action will complete with a `success` status. This overrides the `fail-on-severity` option. | `true`, `false` | `false` |
|
||||
| `show-openssf-scorecard-levels` | When set to `true`, the action will output information about all the known OpenSSF Scorecard scores for the dependencies changed in this pull request. | `true`, `false` | `true` |
|
||||
| `warn-on-openssf-scorecard-level` | When `show-openssf-scorecard-levels` is set to `true`, this option lets you configure the threshold for when a score is considered too low and gets a :warning: warning in the CI. | Any positive integer | 3 |
|
||||
## Configuration Options
|
||||
|
||||
\*not supported for use with GitHub Enterprise Server
|
||||
### config-file
|
||||
|
||||
+when `warn-only` is set to `true`, all vulnerabilities, independently of the severity, will be reported as warnings and the action will not fail.
|
||||
A string representing the path to an external configuration 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 that comply with the expressions in this list. See "[Licenses](https://github.com/actions/dependency-review-action#licenses)".
|
||||
|
||||
**Possible values**: A list of of [SPDX-compliant license identifiers](https://spdx.org/licenses/).
|
||||
|
||||
**Inline example**: `allow-licenses: BSD-3-Clause, LGPL-2.1 OR MIT OR BSD-3-Clause`
|
||||
|
||||
**YAML example**:
|
||||
|
||||
```yaml
|
||||
allow-licenses:
|
||||
- BSD-3-Clause
|
||||
- LGPL-2.1
|
||||
- MIT
|
||||
- BSD-3-Clause
|
||||
```
|
||||
|
||||
### 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 valid set of [SPDX licenses](https://spdx.org/licenses/).
|
||||
|
||||
**Inline example**: `deny-licenses: LGPL-2.0, GPL-2.0+ WITH Bison-exception-2.2`
|
||||
|
||||
**YAML example**:
|
||||
|
||||
```yaml
|
||||
deny-licenses:
|
||||
- LGPL-2.0
|
||||
- GPL-2.0+ WITH Bison-exception-2.2
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### license-check/vulnerability-check
|
||||
|
||||
Disable the license checks or vulnerability checks performed by this Action.
|
||||
You can't disable both checks.
|
||||
|
||||
**Possible values**: `true` or `false`
|
||||
|
||||
**Example**:
|
||||
|
||||
```yaml
|
||||
license-check: true
|
||||
vulnerability-check: false
|
||||
```
|
||||
|
||||
### 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 GitHub Action using your workflow file.
|
||||
|
||||
#### Example
|
||||
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'
|
||||
@@ -106,9 +227,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v2
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
|
||||
@@ -116,79 +237,86 @@ jobs:
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
```
|
||||
|
||||
### Configuration File
|
||||
### Vulnerability Severity
|
||||
|
||||
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.
|
||||
By default the action will fail on any pull request that contains a
|
||||
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`.
|
||||
|
||||
| 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. It is required if the file resides in a private external repository and for all GitHub Enterprise Server repositories. 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:
|
||||
This example will only fail on pull requests with `critical` and `high` vulnerabilities:
|
||||
|
||||
```yaml
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v2
|
||||
with:
|
||||
config-file: './.github/dependency-review-config.yml'
|
||||
fail-on-severity: high
|
||||
```
|
||||
|
||||
And then create the file in the path you just specified. Please note
|
||||
that the **option names in external files use underscores instead of dashes**:
|
||||
### 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
|
||||
fail_on_severity: 'critical'
|
||||
allow_licenses:
|
||||
- 'GPL-3.0'
|
||||
- 'BSD-3-Clause'
|
||||
- 'MIT'
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
with:
|
||||
fail-on-scopes: runtime, development
|
||||
```
|
||||
|
||||
For more examples of how to use this action and its configuration options, see the [examples](docs/examples.md) page.
|
||||
### 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 [SPDX licenses](https://spdx.org/licenses/)
|
||||
to filter the licenses. 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+
|
||||
```
|
||||
|
||||
### Considerations
|
||||
|
||||
- Checking for licenses is not supported on Enterprise Server as the API does not return license information.
|
||||
- The `allow-licenses` and `deny-licenses` options are mutually exclusive; an error will 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**.
|
||||
- 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
|
||||
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
|
||||
|
||||
The Dependency Review GitHub Action check will only block a pull request from being merged if the repository owner has required the check to pass before merging. For more information, see the [documentation on protected branches](https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches#require-status-checks-before-merging).
|
||||
|
||||
## Outputs
|
||||
|
||||
- `comment-content` is generated with the same content as would be present in a Dependency Review Action comment.
|
||||
- `dependency-changes` holds all dependency changes in a JSON format. The following outputs are subsets of `dependency-changes` filtered based on the configuration:
|
||||
- `vulnerable-changes` holds information about dependency changes with vulnerable dependencies in a JSON format.
|
||||
- `invalid-license-changes` holds information about invalid or non-compliant license dependency changes in a JSON format.
|
||||
- `denied-changes` holds information about denied dependency changes in a JSON format.
|
||||
|
||||
> [!NOTE]
|
||||
> Action outputs are unicode strings [with a 1MB size limit](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputs-for-docker-container-and-javascript-actions).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> If you use these outputs in a run-step, you must store the ouput data in an envrioment variable instead of using the output directly. Using an output directly might break shell scripts. For example:
|
||||
>
|
||||
> ```yaml
|
||||
> env:
|
||||
> VULNERABLE_CHANGES: ${{ steps.review.outputs.vulnerable-changes }}
|
||||
> run: |
|
||||
> echo "$VULNERABLE_CHANGES" | jq
|
||||
> ```
|
||||
>
|
||||
> instead of direct `echo '${{ steps.review.outputs.vulnerable-changes }}'`. See [examples](docs/examples.md) for more.
|
||||
The Dependency Review GitHub Action check will only block a pull request from being merged if the repository owner has required the check to pass before merging. For more information, see the [documentation on protected branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches#require-status-checks-before-merging).
|
||||
|
||||
## Getting help
|
||||
|
||||
If you have bug reports, questions or suggestions please [create a new issue](https://github.com/actions/dependency-review-action/issues/new/choose).
|
||||
If you have bug reports, questions or suggestions please [create a new
|
||||
issue](https://github.com/actions/dependency-review-action/issues/new/choose).
|
||||
|
||||
## Contributing
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+128
-88
@@ -1,8 +1,34 @@
|
||||
import {expect, test, beforeEach} from '@jest/globals'
|
||||
import {readConfig} from '../src/config'
|
||||
import {readConfig, readConfigFile} from '../src/config'
|
||||
import {getRefs} from '../src/git-refs'
|
||||
import * as Utils from '../src/utils'
|
||||
import {setInput, clearInputs} from './test-helpers'
|
||||
|
||||
// GitHub Action inputs come in the form of environment variables
|
||||
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
|
||||
function setInput(input: string, value: string) {
|
||||
process.env[`INPUT_${input.toUpperCase()}`] = value
|
||||
}
|
||||
|
||||
// We want a clean ENV before each test. We use `delete`
|
||||
// since we want `undefined` values and not empty strings.
|
||||
function clearInputs() {
|
||||
const allowedOptions = [
|
||||
'FAIL-ON-SEVERITY',
|
||||
'FAIL-ON-SCOPES',
|
||||
'ALLOW-LICENSES',
|
||||
'DENY-LICENSES',
|
||||
'ALLOW-GHSAS',
|
||||
'LICENSE-CHECK',
|
||||
'VULNERABILITY-CHECK',
|
||||
'CONFIG-FILE',
|
||||
'BASE-REF',
|
||||
'HEAD-REF'
|
||||
]
|
||||
|
||||
allowedOptions.forEach(option => {
|
||||
delete process.env[`INPUT_${option.toUpperCase()}`]
|
||||
})
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(true)
|
||||
@@ -13,58 +39,43 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
test('it defaults to low severity', async () => {
|
||||
const config = await readConfig()
|
||||
expect(config.fail_on_severity).toEqual('low')
|
||||
const options = readConfig()
|
||||
expect(options.fail_on_severity).toEqual('low')
|
||||
})
|
||||
|
||||
test('it reads custom configs', async () => {
|
||||
setInput('fail-on-severity', 'critical')
|
||||
setInput('allow-licenses', ' BSD, GPL 2')
|
||||
|
||||
const config = await readConfig()
|
||||
expect(config.fail_on_severity).toEqual('critical')
|
||||
expect(config.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
||||
})
|
||||
|
||||
test('it defaults to false for warn-only', async () => {
|
||||
const config = await readConfig()
|
||||
expect(config.warn_only).toEqual(false)
|
||||
const options = readConfig()
|
||||
expect(options.fail_on_severity).toEqual('critical')
|
||||
expect(options.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
||||
})
|
||||
|
||||
test('it defaults to empty allow/deny lists ', async () => {
|
||||
const config = await readConfig()
|
||||
const options = readConfig()
|
||||
|
||||
expect(config.allow_licenses).toEqual(undefined)
|
||||
expect(config.deny_licenses).toEqual(undefined)
|
||||
expect(options.allow_licenses).toEqual(undefined)
|
||||
expect(options.deny_licenses).toEqual(undefined)
|
||||
})
|
||||
|
||||
test('it raises an error if both an allow and denylist are specified', async () => {
|
||||
setInput('allow-licenses', 'MIT')
|
||||
setInput('deny-licenses', 'BSD')
|
||||
|
||||
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'
|
||||
)
|
||||
expect(() => readConfig()).toThrow()
|
||||
})
|
||||
|
||||
test('it raises an error when given an unknown severity', async () => {
|
||||
setInput('fail-on-severity', 'zombies')
|
||||
|
||||
await expect(readConfig()).rejects.toThrow(/received 'zombies'/)
|
||||
expect(() => readConfig()).toThrow()
|
||||
})
|
||||
|
||||
test('it uses the given refs when the event is not a pull request', async () => {
|
||||
setInput('base-ref', 'a-custom-base-ref')
|
||||
setInput('head-ref', 'a-custom-head-ref')
|
||||
|
||||
const refs = getRefs(await readConfig(), {
|
||||
const refs = getRefs(readConfig(), {
|
||||
payload: {},
|
||||
eventName: 'workflow_dispatch'
|
||||
})
|
||||
@@ -73,88 +84,143 @@ 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 () => {
|
||||
const config = await readConfig()
|
||||
const options = readConfig()
|
||||
expect(() =>
|
||||
getRefs(config, {
|
||||
getRefs(options, {
|
||||
payload: {},
|
||||
eventName: 'workflow_dispatch'
|
||||
})
|
||||
).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 config = await readConfig()
|
||||
expect(config.fail_on_scopes).toEqual(['runtime'])
|
||||
const options = readConfig()
|
||||
expect(options.fail_on_scopes).toEqual(['runtime'])
|
||||
})
|
||||
|
||||
test('it parses custom scopes preference', async () => {
|
||||
setInput('fail-on-scopes', 'runtime, development')
|
||||
let config = await readConfig()
|
||||
expect(config.fail_on_scopes).toEqual(['runtime', 'development'])
|
||||
let options = readConfig()
|
||||
expect(options.fail_on_scopes).toEqual(['runtime', 'development'])
|
||||
|
||||
clearInputs()
|
||||
setInput('fail-on-scopes', 'development')
|
||||
config = await readConfig()
|
||||
expect(config.fail_on_scopes).toEqual(['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')
|
||||
await expect(readConfig()).rejects.toThrow(/received 'zombies'/)
|
||||
expect(() => readConfig()).toThrow()
|
||||
})
|
||||
|
||||
test('it defaults to an empty GHSA allowlist', async () => {
|
||||
const config = await readConfig()
|
||||
expect(config.allow_ghsas).toEqual([])
|
||||
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 config = await readConfig()
|
||||
expect(config.allow_ghsas).toEqual([
|
||||
const options = readConfig()
|
||||
expect(options.allow_ghsas).toEqual([
|
||||
'GHSA-abcd-1234-5679',
|
||||
'GHSA-efgh-1234-5679'
|
||||
])
|
||||
})
|
||||
|
||||
test('it defaults to checking licenses', async () => {
|
||||
const config = await readConfig()
|
||||
expect(config.license_check).toBe(true)
|
||||
const options = readConfig()
|
||||
expect(options.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)
|
||||
let options = readConfig()
|
||||
expect(options.license_check).toEqual(false)
|
||||
|
||||
clearInputs()
|
||||
setInput('license-check', 'true')
|
||||
config = await readConfig()
|
||||
expect(config.license_check).toEqual(true)
|
||||
options = readConfig()
|
||||
expect(options.license_check).toEqual(true)
|
||||
})
|
||||
|
||||
test('it defaults to checking vulnerabilities', async () => {
|
||||
const config = await readConfig()
|
||||
expect(config.vulnerability_check).toBe(true)
|
||||
const options = readConfig()
|
||||
expect(options.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)
|
||||
let options = readConfig()
|
||||
expect(options.vulnerability_check).toEqual(false)
|
||||
|
||||
clearInputs()
|
||||
setInput('vulnerability-check', 'true')
|
||||
config = await readConfig()
|
||||
expect(config.vulnerability_check).toEqual(true)
|
||||
options = readConfig()
|
||||
expect(options.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/
|
||||
)
|
||||
expect(() => {
|
||||
readConfig()
|
||||
}).toThrow("Can't disable both license-check and vulnerability-check")
|
||||
})
|
||||
|
||||
describe('licenses that are not valid SPDX licenses', () => {
|
||||
@@ -164,41 +230,15 @@ describe('licenses that are not valid SPDX licenses', () => {
|
||||
|
||||
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'
|
||||
)
|
||||
expect(() => {
|
||||
readConfig()
|
||||
}).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'
|
||||
)
|
||||
expect(() => {
|
||||
readConfig()
|
||||
}).toThrow('Invalid license(s) in deny-licenses: BSD, GPL 2')
|
||||
})
|
||||
})
|
||||
|
||||
test('it parses the comment-summary-in-pr input', async () => {
|
||||
setInput('comment-summary-in-pr', 'true')
|
||||
let config = await readConfig()
|
||||
expect(config.comment_summary_in_pr).toBe('always')
|
||||
|
||||
clearInputs()
|
||||
setInput('comment-summary-in-pr', 'false')
|
||||
config = await readConfig()
|
||||
expect(config.comment_summary_in_pr).toBe('never')
|
||||
|
||||
clearInputs()
|
||||
setInput('comment-summary-in-pr', 'always')
|
||||
config = await readConfig()
|
||||
expect(config.comment_summary_in_pr).toBe('always')
|
||||
|
||||
clearInputs()
|
||||
setInput('comment-summary-in-pr', 'never')
|
||||
config = await readConfig()
|
||||
expect(config.comment_summary_in_pr).toBe('never')
|
||||
|
||||
clearInputs()
|
||||
setInput('comment-summary-in-pr', 'on-failure')
|
||||
config = await readConfig()
|
||||
expect(config.comment_summary_in_pr).toBe('on-failure')
|
||||
})
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import {expect, jest, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
|
||||
let getDeniedChanges: Function
|
||||
|
||||
const npmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
name: 'Reeuhq',
|
||||
version: '1.0.2',
|
||||
package_url: 'pkg:npm/reeuhq@1.0.2',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'first-random_string',
|
||||
advisory_summary: 'very dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const rubyChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'Gemfile.lock',
|
||||
ecosystem: 'rubygems',
|
||||
name: 'actionsomething',
|
||||
version: '3.2.0',
|
||||
package_url: 'pkg:gem/actionsomething@3.2.0',
|
||||
license: 'BSD',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
},
|
||||
{
|
||||
severity: 'low',
|
||||
advisory_ghsa_id: 'third-random_string',
|
||||
advisory_summary: 'dont page me',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const pipChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'requirements.txt',
|
||||
ecosystem: 'pip',
|
||||
name: 'package-1',
|
||||
version: '1.1.1',
|
||||
package_url: 'pkg:pypi/package-1@1.1.1',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
},
|
||||
{
|
||||
severity: 'low',
|
||||
advisory_ghsa_id: 'third-random_string',
|
||||
advisory_summary: 'dont page me',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const mvnChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'pom.xml',
|
||||
ecosystem: 'maven',
|
||||
name: 'org.apache.logging.log4j:log4j-core',
|
||||
version: '2.15.0',
|
||||
package_url: 'pkg:maven/org.apache.logging.log4j/log4j-core@2.14.7',
|
||||
license: 'Apache-2.0',
|
||||
source_repository_url:
|
||||
'https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core',
|
||||
scope: 'unknown',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
jest.mock('@actions/core')
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
licenses: {
|
||||
getForRepo: jest
|
||||
.fn()
|
||||
.mockReturnValue({data: {license: {spdx_id: 'AGPL'}}})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('octokit', () => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
Octokit: class {
|
||||
constructor() {
|
||||
return mockOctokit
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
;({getDeniedChanges} = require('../src/deny'))
|
||||
})
|
||||
|
||||
test('it adds packages in the deny packages list', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const deniedChanges = await getDeniedChanges(
|
||||
changes,
|
||||
['pkg:gem/actionsomething'],
|
||||
[]
|
||||
)
|
||||
|
||||
expect(deniedChanges[0]).toBe(rubyChange)
|
||||
expect(deniedChanges.length).toEqual(1)
|
||||
})
|
||||
|
||||
test('it adds packages in the deny group list', async () => {
|
||||
const changes: Changes = [mvnChange, rubyChange]
|
||||
const deniedChanges = await getDeniedChanges(
|
||||
changes,
|
||||
[],
|
||||
['pkg:maven/org.apache.logging.log4j']
|
||||
)
|
||||
|
||||
expect(deniedChanges[0]).toBe(mvnChange)
|
||||
expect(deniedChanges.length).toEqual(1)
|
||||
})
|
||||
|
||||
test('it adds packages outside of the deny lists', async () => {
|
||||
const changes: Changes = [npmChange, pipChange]
|
||||
const deniedChanges = await getDeniedChanges(
|
||||
changes,
|
||||
['pkg:gem/actionsomething'],
|
||||
['pkg:maven:org.apache.logging.log4j']
|
||||
)
|
||||
|
||||
expect(deniedChanges.length).toEqual(0)
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import {RequestError} from '@octokit/request-error'
|
||||
import * as dependencyGraph from '../src/dependency-graph'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
// mock call to core.getInput('repo-token'.. to avoid environment setup - Input required and not supplied: repo-token
|
||||
jest.mock('@actions/core', () => ({
|
||||
getInput: (input: string) => {
|
||||
if (input === 'repo-token') {
|
||||
return 'gh_testtoken'
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
test('it properly catches RequestError type', async () => {
|
||||
const token = core.getInput('repo-token', {required: true})
|
||||
expect(token).toBe('gh_testtoken')
|
||||
|
||||
//Integration test to make an API request using current dependencies and ensure response can parse into RequestError
|
||||
try {
|
||||
await dependencyGraph.compare({
|
||||
owner: 'actions',
|
||||
repo: 'dependency-review-action',
|
||||
baseRef: 'refs/heads/master',
|
||||
headRef: 'refs/heads/master'
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(RequestError)
|
||||
}
|
||||
})
|
||||
@@ -1,111 +0,0 @@
|
||||
import {expect, test, beforeEach} from '@jest/globals'
|
||||
import {readConfig} from '../src/config'
|
||||
import * as Utils from '../src/utils'
|
||||
import {setInput, clearInputs} from './test-helpers'
|
||||
|
||||
const externalConfig = `fail_on_severity: 'high'
|
||||
allow_licenses: ['GPL-2.0-only']
|
||||
`
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
repos: {
|
||||
getContent: jest.fn().mockReturnValue({data: externalConfig})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('octokit', () => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
Octokit: class {
|
||||
constructor() {
|
||||
return mockOctokit
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(true)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
clearInputs()
|
||||
})
|
||||
|
||||
test('it reads an external config file', async () => {
|
||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
||||
|
||||
const config = await readConfig()
|
||||
expect(config.fail_on_severity).toEqual('critical')
|
||||
expect(config.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
||||
})
|
||||
|
||||
test('raises an error when the config file was not found', async () => {
|
||||
setInput('config-file', 'fixtures/i-dont-exist')
|
||||
await expect(readConfig()).rejects.toThrow(/Unable to fetch/)
|
||||
})
|
||||
|
||||
test('it parses options from both sources', async () => {
|
||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml')
|
||||
|
||||
let config = await readConfig()
|
||||
expect(config.fail_on_severity).toEqual('critical')
|
||||
|
||||
setInput('base-ref', 'a-custom-base-ref')
|
||||
config = await readConfig()
|
||||
expect(config.base_ref).toEqual('a-custom-base-ref')
|
||||
})
|
||||
|
||||
test('in case of conflicts, the inline config is the source of truth', async () => {
|
||||
setInput('fail-on-severity', 'low')
|
||||
setInput('config-file', './__tests__/fixtures/config-allow-sample.yml') // this will set fail-on-severity to 'critical'
|
||||
|
||||
const config = await readConfig()
|
||||
expect(config.fail_on_severity).toEqual('low')
|
||||
})
|
||||
|
||||
test('it uses the default values when loading external files', async () => {
|
||||
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
||||
let config = await readConfig()
|
||||
expect(config.allow_licenses).toEqual(undefined)
|
||||
expect(config.deny_licenses).toEqual(undefined)
|
||||
|
||||
setInput('config-file', './__tests__/fixtures/license-config-sample.yml')
|
||||
config = await readConfig()
|
||||
expect(config.fail_on_severity).toEqual('low')
|
||||
})
|
||||
|
||||
test('it accepts an external configuration filename', async () => {
|
||||
setInput('config-file', './__tests__/fixtures/no-licenses-config.yml')
|
||||
const config = await readConfig()
|
||||
expect(config.fail_on_severity).toEqual('critical')
|
||||
})
|
||||
|
||||
test('it raises an error when given an unknown severity in an external config file', async () => {
|
||||
setInput('config-file', './__tests__/fixtures/invalid-severity-config.yml')
|
||||
await expect(readConfig()).rejects.toThrow()
|
||||
})
|
||||
|
||||
test('it supports comma-separated lists', async () => {
|
||||
setInput(
|
||||
'config-file',
|
||||
'./__tests__/fixtures/inline-license-config-sample.yml'
|
||||
)
|
||||
const config = await readConfig()
|
||||
|
||||
expect(config.allow_licenses).toEqual(['MIT', 'GPL-2.0-only'])
|
||||
})
|
||||
|
||||
test('it reads a config file hosted in another repo', async () => {
|
||||
setInput(
|
||||
'config-file',
|
||||
'future-funk/anyone-cualkiera/external-config.yml@main'
|
||||
)
|
||||
setInput('external-repo-token', 'gh_viptoken')
|
||||
|
||||
const config = await readConfig()
|
||||
|
||||
expect(config.fail_on_severity).toEqual('high')
|
||||
expect(config.allow_licenses).toEqual(['GPL-2.0-only'])
|
||||
})
|
||||
+23
-117
@@ -1,12 +1,12 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {Change} from '../src/schemas'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
filterChangesByScopes,
|
||||
filterAllowedAdvisories
|
||||
} from '../src/filter'
|
||||
|
||||
const npmChange: Change = {
|
||||
let npmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
@@ -19,14 +19,14 @@ const npmChange: Change = {
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'vulnerable-ghsa-id',
|
||||
advisory_ghsa_id: 'first-random_string',
|
||||
advisory_summary: 'very dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const rubyChange: Change = {
|
||||
let rubyChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'Gemfile.lock',
|
||||
ecosystem: 'rubygems',
|
||||
@@ -39,20 +39,20 @@ const rubyChange: Change = {
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'moderate-ghsa-id',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
},
|
||||
{
|
||||
severity: 'low',
|
||||
advisory_ghsa_id: 'low-ghsa-id',
|
||||
advisory_ghsa_id: 'third-random_string',
|
||||
advisory_summary: 'dont page me',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const noVulnNpmChange: Change = {
|
||||
let noVulnNpmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
@@ -65,64 +65,6 @@ const noVulnNpmChange: Change = {
|
||||
vulnerabilities: []
|
||||
}
|
||||
|
||||
const lodashChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'package.json',
|
||||
ecosystem: 'npm',
|
||||
name: 'lodash',
|
||||
version: '4.17.0',
|
||||
package_url: 'pkg:npm/lodash@4.17.0',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'https://github.com/lodash/lodash',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'GHSA-jf85-cpcp-j695',
|
||||
advisory_summary: 'Prototype Pollution in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-jf85-cpcp-j695'
|
||||
},
|
||||
{
|
||||
severity: 'high',
|
||||
advisory_ghsa_id: 'GHSA-4xc9-xhrj-v574',
|
||||
advisory_summary: 'Prototype Pollution in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-4xc9-xhrj-v574'
|
||||
},
|
||||
{
|
||||
severity: 'high',
|
||||
advisory_ghsa_id: 'GHSA-35jh-r3h4-6jhm',
|
||||
advisory_summary: 'Command Injection in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-35jh-r3h4-6jhm'
|
||||
},
|
||||
{
|
||||
severity: 'high',
|
||||
advisory_ghsa_id: 'GHSA-p6mc-m468-83gw',
|
||||
advisory_summary: 'Prototype Pollution in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-p6mc-m468-83gw'
|
||||
},
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'GHSA-x5rq-j2xg-h7qm',
|
||||
advisory_summary:
|
||||
'Regular Expression Denial of Service (ReDoS) in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-x5rq-j2xg-h7qm'
|
||||
},
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'GHSA-29mw-wpgm-hmr9',
|
||||
advisory_summary:
|
||||
'Regular Expression Denial of Service (ReDoS) in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-29mw-wpgm-hmr9'
|
||||
},
|
||||
{
|
||||
severity: 'low',
|
||||
advisory_ghsa_id: 'GHSA-fvqr-27wr-82fm',
|
||||
advisory_summary: 'Prototype Pollution in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-fvqr-27wr-82fm'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
test('it properly filters changes by severity', async () => {
|
||||
const changes = [npmChange, rubyChange]
|
||||
let result = filterChangesBySeverity('high', changes)
|
||||
@@ -150,68 +92,32 @@ test('it properly filters changes by scope', async () => {
|
||||
|
||||
test('it properly handles undefined advisory IDs', async () => {
|
||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
||||
const result = filterAllowedAdvisories(undefined, changes)
|
||||
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]
|
||||
|
||||
const fakeGHSAChanges = filterAllowedAdvisories(['notrealGHSAID'], changes)
|
||||
expect(fakeGHSAChanges).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
})
|
||||
let result = filterAllowedAdvisories(['notrealGHSAID'], changes)
|
||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
|
||||
test('it properly filters only allowed vulnerabilities', async () => {
|
||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
||||
const oldVulns = [
|
||||
...npmChange.vulnerabilities,
|
||||
...rubyChange.vulnerabilities,
|
||||
...noVulnNpmChange.vulnerabilities
|
||||
]
|
||||
result = filterAllowedAdvisories(['first-random_string'], changes)
|
||||
expect(result).toEqual([rubyChange, noVulnNpmChange])
|
||||
|
||||
const vulnerable = filterAllowedAdvisories(['vulnerable-ghsa-id'], changes)
|
||||
|
||||
const newVulns = vulnerable.map(change => change.vulnerabilities).flat()
|
||||
|
||||
expect(newVulns.length).toEqual(oldVulns.length - 1)
|
||||
expect(newVulns).not.toContainEqual(
|
||||
expect.objectContaining({advisory_ghsa_id: 'vulnerable-ghsa-id'})
|
||||
)
|
||||
})
|
||||
|
||||
test('does not drop dependencies when filtering by GHSA', async () => {
|
||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
||||
const result = filterAllowedAdvisories(
|
||||
['moderate-ghsa-id', 'low-ghsa-id', 'GHSA-jf85-cpcp-j695'],
|
||||
result = filterAllowedAdvisories(
|
||||
['second-random_string', 'third-random_string'],
|
||||
changes
|
||||
)
|
||||
expect(result).toEqual([npmChange, noVulnNpmChange])
|
||||
|
||||
expect(result.map(change => change.name)).toEqual(
|
||||
changes.map(change => change.name)
|
||||
result = filterAllowedAdvisories(
|
||||
['first-random_string', 'second-random_string', 'third-random_string'],
|
||||
changes
|
||||
)
|
||||
})
|
||||
|
||||
test('it properly filters multiple GHSAs', async () => {
|
||||
const allowedGHSAs = ['vulnerable-ghsa-id', 'moderate-ghsa-id', 'low-ghsa-id']
|
||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
||||
const oldVulns = changes.map(change => change.vulnerabilities).flat()
|
||||
|
||||
const result = filterAllowedAdvisories(allowedGHSAs, changes)
|
||||
|
||||
const newVulns = result.map(change => change.vulnerabilities).flat()
|
||||
|
||||
expect(newVulns.length).toEqual(oldVulns.length - 3)
|
||||
})
|
||||
|
||||
test('it filters out GHSA dependencies', async () => {
|
||||
const lodash = filterAllowedAdvisories(
|
||||
['GHSA-jf85-cpcp-j695'],
|
||||
[lodashChange]
|
||||
)[0]
|
||||
// the filter should have removed a single GHSA from the list
|
||||
const expected = lodashChange.vulnerabilities.filter(
|
||||
vuln => vuln.advisory_ghsa_id !== 'GHSA-jf85-cpcp-j695'
|
||||
)
|
||||
expect(expected.length).toEqual(lodashChange.vulnerabilities.length - 1)
|
||||
expect(lodash.vulnerabilities).toEqual(expected)
|
||||
expect(result).toEqual([noVulnNpmChange])
|
||||
|
||||
// if we have a change with multiple vulnerabilities but only one is allowed, we still should not filter out that change
|
||||
result = filterAllowedAdvisories(['second-random_string'], changes)
|
||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
})
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
fail_on_severity: critical
|
||||
allow_licenses: []
|
||||
@@ -1,36 +0,0 @@
|
||||
import {Change} from '../../src/schemas'
|
||||
import {createTestVulnerability} from './create-test-vulnerability'
|
||||
|
||||
const defaultChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'package.json',
|
||||
ecosystem: 'npm',
|
||||
name: 'lodash',
|
||||
version: '4.17.20',
|
||||
package_url: 'pkg:npm/lodash@4.17.20',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'https://github.com/lodash/lodash',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
createTestVulnerability({
|
||||
severity: 'high',
|
||||
advisory_ghsa_id: 'GHSA-35jh-r3h4-6jhm',
|
||||
advisory_summary: 'Command Injection in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-35jh-r3h4-6jhm'
|
||||
}),
|
||||
createTestVulnerability({
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'GHSA-29mw-wpgm-hmr9',
|
||||
advisory_summary:
|
||||
'Regular Expression Denial of Service (ReDoS) in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-29mw-wpgm-hmr9'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
const createTestChange = (overwrites: Partial<Change> = {}): Change => ({
|
||||
...defaultChange,
|
||||
...overwrites
|
||||
})
|
||||
|
||||
export {createTestChange}
|
||||
@@ -1,19 +0,0 @@
|
||||
import {Change} from '../../src/schemas'
|
||||
|
||||
type Vulnerability = Change['vulnerabilities'][0]
|
||||
|
||||
const defaultTestVulnerability: Vulnerability = {
|
||||
severity: 'high',
|
||||
advisory_ghsa_id: 'GHSA-35jh-r3h4-6jhm',
|
||||
advisory_summary: 'Command Injection in lodash',
|
||||
advisory_url: 'https://github.com/advisories/GHSA-35jh-r3h4-6jhm'
|
||||
}
|
||||
|
||||
const createTestVulnerability = (
|
||||
overwrites: Partial<Vulnerability> = {}
|
||||
): Vulnerability => ({
|
||||
...defaultTestVulnerability,
|
||||
...overwrites
|
||||
})
|
||||
|
||||
export {createTestVulnerability}
|
||||
@@ -1 +0,0 @@
|
||||
allow-licenses: "MIT, GPL-2.0-only"
|
||||
@@ -1,3 +1,3 @@
|
||||
fail_on_severity: 'so many zombies'
|
||||
deny_licenses:
|
||||
fail-on-severity: 'so many zombies'
|
||||
deny-licenses:
|
||||
- MIT
|
||||
|
||||
+13
-79
@@ -3,7 +3,7 @@ import {Change, Changes} from '../src/schemas'
|
||||
|
||||
let getInvalidLicenseChanges: Function
|
||||
|
||||
const npmChange: Change = {
|
||||
let npmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
@@ -23,7 +23,7 @@ const npmChange: Change = {
|
||||
]
|
||||
}
|
||||
|
||||
const rubyChange: Change = {
|
||||
let rubyChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'Gemfile.lock',
|
||||
ecosystem: 'rubygems',
|
||||
@@ -49,32 +49,6 @@ const rubyChange: Change = {
|
||||
]
|
||||
}
|
||||
|
||||
const pipChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'requirements.txt',
|
||||
ecosystem: 'pip',
|
||||
name: 'package-1',
|
||||
version: '1.1.1',
|
||||
package_url: 'pkg:pypi/package-1@1.1.1',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
},
|
||||
{
|
||||
severity: 'low',
|
||||
advisory_ghsa_id: 'third-random_string',
|
||||
advisory_summary: 'dont page me',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
jest.mock('@actions/core')
|
||||
|
||||
const mockOctokit = {
|
||||
@@ -89,7 +63,6 @@ const mockOctokit = {
|
||||
|
||||
jest.mock('octokit', () => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
Octokit: class {
|
||||
constructor() {
|
||||
return mockOctokit
|
||||
@@ -105,7 +78,6 @@ beforeEach(async () => {
|
||||
// true for BSD, false for all others
|
||||
return jest.fn((license: string, _: string): boolean => license === 'BSD')
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
;({getInvalidLicenseChanges} = require('../src/licenses'))
|
||||
})
|
||||
|
||||
@@ -127,6 +99,17 @@ test('it adds license inside the deny list to forbidden changes', async () => {
|
||||
expect(forbidden.length).toEqual(1)
|
||||
})
|
||||
|
||||
// This is more of a "here's a behavior that might be surprising" than an actual
|
||||
// thing we want in the system. Please remove this test after refactoring.
|
||||
test('it adds all licenses to forbidden changes when allow is provided an empty array', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
let {forbidden} = await getInvalidLicenseChanges(changes, {
|
||||
allow: [],
|
||||
deny: ['BSD']
|
||||
})
|
||||
expect(forbidden.length).toBe(2)
|
||||
})
|
||||
|
||||
test('it does not add license outside the allow list to forbidden changes if it is in removed changes', async () => {
|
||||
const changes: Changes = [
|
||||
{...npmChange, change_type: 'removed'},
|
||||
@@ -168,7 +151,6 @@ test('it adds all licenses to unresolved if it is unable to determine the validi
|
||||
throw new Error('Some Error')
|
||||
})
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
;({getInvalidLicenseChanges} = require('../src/licenses'))
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const invalidLicenses = await getInvalidLicenseChanges(changes, {
|
||||
@@ -179,54 +161,6 @@ test('it adds all licenses to unresolved if it is unable to determine the validi
|
||||
expect(invalidLicenses.unresolved.length).toEqual(2)
|
||||
})
|
||||
|
||||
test('it does not filter out changes that are on the exclusions list', async () => {
|
||||
const changes: Changes = [pipChange, npmChange, rubyChange]
|
||||
const licensesConfig = {
|
||||
allow: ['BSD'],
|
||||
licenseExclusions: ['pkg:pypi/package-1@1.1.1', 'pkg:npm/reeuhq@1.0.2']
|
||||
}
|
||||
const invalidLicenses = await getInvalidLicenseChanges(
|
||||
changes,
|
||||
licensesConfig
|
||||
)
|
||||
expect(invalidLicenses.forbidden.length).toEqual(0)
|
||||
})
|
||||
|
||||
test('it does not fail when the packages dont have a valid PURL', async () => {
|
||||
const emptyPurlChange = pipChange
|
||||
emptyPurlChange.package_url = ''
|
||||
|
||||
const changes: Changes = [emptyPurlChange, npmChange, rubyChange]
|
||||
const licensesConfig = {
|
||||
allow: ['BSD'],
|
||||
licenseExclusions: ['pkg:pypi/package-1@1.1.1', 'pkg:npm/reeuhq@1.0.2']
|
||||
}
|
||||
|
||||
const invalidLicenses = await getInvalidLicenseChanges(
|
||||
changes,
|
||||
licensesConfig
|
||||
)
|
||||
expect(invalidLicenses.forbidden.length).toEqual(1)
|
||||
})
|
||||
|
||||
test('it does filters out changes if they are not on the exclusions list', async () => {
|
||||
const changes: Changes = [pipChange, npmChange, rubyChange]
|
||||
const licensesConfig = {
|
||||
allow: ['BSD'],
|
||||
licenseExclusions: [
|
||||
'pkg:pypi/notmypackage-1@1.1.1',
|
||||
'pkg:npm/alsonot@1.0.2'
|
||||
]
|
||||
}
|
||||
const invalidLicenses = await getInvalidLicenseChanges(
|
||||
changes,
|
||||
licensesConfig
|
||||
)
|
||||
expect(invalidLicenses.forbidden.length).toEqual(2)
|
||||
expect(invalidLicenses.forbidden[0]).toBe(pipChange)
|
||||
expect(invalidLicenses.forbidden[1]).toBe(npmChange)
|
||||
})
|
||||
|
||||
describe('GH License API fallback', () => {
|
||||
test('it calls licenses endpoint if atleast one of the changes has null license and valid source_repository_url', async () => {
|
||||
const nullLicenseChange = {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
import {getScorecardLevels, getProjectUrl} from '../src/scorecard'
|
||||
|
||||
const npmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
name: 'type-is',
|
||||
version: '1.6.18',
|
||||
package_url: 'pkg:npm/type-is@1.6.18',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/jshttp/type-is',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'first-random_string',
|
||||
advisory_summary: 'very dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
test('Get scorecard from API', async () => {
|
||||
const changes: Changes = [npmChange]
|
||||
const scorecard = await getScorecardLevels(changes)
|
||||
expect(scorecard).not.toBeNull()
|
||||
expect(scorecard.dependencies).toHaveLength(1)
|
||||
expect(scorecard.dependencies[0].scorecard?.score).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Get project URL from deps.dev API', async () => {
|
||||
const result = await getProjectUrl(
|
||||
npmChange.ecosystem,
|
||||
npmChange.name,
|
||||
npmChange.version
|
||||
)
|
||||
expect(result).not.toBeNull()
|
||||
})
|
||||
@@ -1,424 +0,0 @@
|
||||
import {expect, jest, test} from '@jest/globals'
|
||||
import {Changes, ConfigurationOptions, Scorecard} from '../src/schemas'
|
||||
import * as summary from '../src/summary'
|
||||
import * as core from '@actions/core'
|
||||
import {createTestChange} from './fixtures/create-test-change'
|
||||
import {createTestVulnerability} from './fixtures/create-test-vulnerability'
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
core.summary.emptyBuffer()
|
||||
})
|
||||
|
||||
const emptyChanges: Changes = []
|
||||
const emptyInvalidLicenseChanges = {
|
||||
forbidden: [],
|
||||
unresolved: [],
|
||||
unlicensed: []
|
||||
}
|
||||
const emptyScorecard: Scorecard = {
|
||||
dependencies: []
|
||||
}
|
||||
const defaultConfig: ConfigurationOptions = {
|
||||
vulnerability_check: true,
|
||||
license_check: true,
|
||||
fail_on_severity: 'high',
|
||||
fail_on_scopes: ['runtime'],
|
||||
allow_ghsas: [],
|
||||
allow_licenses: [],
|
||||
deny_licenses: [],
|
||||
deny_packages: [],
|
||||
deny_groups: [],
|
||||
comment_summary_in_pr: true,
|
||||
retry_on_snapshot_warnings: false,
|
||||
retry_on_snapshot_warnings_timeout: 120,
|
||||
warn_only: false,
|
||||
warn_on_openssf_scorecard_level: 3,
|
||||
show_openssf_scorecard: false
|
||||
}
|
||||
|
||||
const changesWithEmptyManifests: Changes = [
|
||||
{
|
||||
change_type: 'added',
|
||||
manifest: '',
|
||||
ecosystem: 'unknown',
|
||||
name: 'castore',
|
||||
version: '0.1.17',
|
||||
package_url: 'pkg:hex/castore@0.1.17',
|
||||
license: null,
|
||||
source_repository_url: null,
|
||||
scope: 'runtime',
|
||||
vulnerabilities: []
|
||||
},
|
||||
{
|
||||
change_type: 'added',
|
||||
manifest: '',
|
||||
ecosystem: 'unknown',
|
||||
name: 'connection',
|
||||
version: '1.1.0',
|
||||
package_url: 'pkg:hex/connection@1.1.0',
|
||||
license: null,
|
||||
source_repository_url: null,
|
||||
scope: 'runtime',
|
||||
vulnerabilities: []
|
||||
},
|
||||
{
|
||||
change_type: 'added',
|
||||
manifest: 'python/dist-info/METADATA',
|
||||
ecosystem: 'pip',
|
||||
name: 'pygments',
|
||||
version: '2.6.1',
|
||||
package_url: 'pkg:pypi/pygments@2.6.1',
|
||||
license: 'BSD-2-Clause',
|
||||
source_repository_url: 'https://github.com/pygments/pygments',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: []
|
||||
}
|
||||
]
|
||||
|
||||
const scorecard: Scorecard = {
|
||||
dependencies: [
|
||||
{
|
||||
change: {
|
||||
change_type: 'added',
|
||||
manifest: '',
|
||||
ecosystem: 'unknown',
|
||||
name: 'castore',
|
||||
version: '0.1.17',
|
||||
package_url: 'pkg:hex/castore@0.1.17',
|
||||
license: null,
|
||||
source_repository_url: null,
|
||||
scope: 'runtime',
|
||||
vulnerabilities: []
|
||||
},
|
||||
scorecard: null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
test('prints headline as h1', () => {
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
scorecard,
|
||||
defaultConfig
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
|
||||
expect(text).toContain('<h1>Dependency Review</h1>')
|
||||
})
|
||||
|
||||
test('only includes "No vulnerabilities or license issues found"-message if both are configured and nothing was found', () => {
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
|
||||
expect(text).toContain('✅ No vulnerabilities or license issues found.')
|
||||
})
|
||||
|
||||
test('only includes "No vulnerabilities found"-message if "license_check" is set to false and nothing was found', () => {
|
||||
const config = {...defaultConfig, license_check: false}
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
config
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
|
||||
expect(text).toContain('✅ No vulnerabilities found.')
|
||||
})
|
||||
|
||||
test('only includes "No license issues found"-message if "vulnerability_check" is set to false and nothing was found', () => {
|
||||
const config = {...defaultConfig, vulnerability_check: false}
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
config
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
|
||||
expect(text).toContain('✅ No license issues found.')
|
||||
})
|
||||
|
||||
test('groups dependencies with empty manifest paths together', () => {
|
||||
summary.addSummaryToSummary(
|
||||
changesWithEmptyManifests,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
summary.addScannedDependencies(changesWithEmptyManifests)
|
||||
const text = core.summary.stringify()
|
||||
|
||||
expect(text).toContain('<summary>Unnamed Manifest</summary>')
|
||||
expect(text).toContain('castore')
|
||||
expect(text).toContain('connection')
|
||||
expect(text).toContain('<summary>python/dist-info/METADATA</summary>')
|
||||
expect(text).toContain('pygments')
|
||||
})
|
||||
|
||||
test('does not include status section if nothing was found', () => {
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
|
||||
expect(text).not.toContain('The following issues were found:')
|
||||
})
|
||||
|
||||
test('includes count and status icons for all findings', () => {
|
||||
const vulnerabilities = [
|
||||
createTestChange({name: 'lodash'}),
|
||||
createTestChange({name: 'underscore', package_url: 'test-url'})
|
||||
]
|
||||
const licenseIssues = {
|
||||
forbidden: [createTestChange()],
|
||||
unresolved: [createTestChange(), createTestChange()],
|
||||
unlicensed: [createTestChange(), createTestChange(), createTestChange()]
|
||||
}
|
||||
|
||||
summary.addSummaryToSummary(
|
||||
vulnerabilities,
|
||||
licenseIssues,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('❌ 2 vulnerable package(s)')
|
||||
expect(text).toContain(
|
||||
'❌ 2 package(s) with invalid SPDX license definitions'
|
||||
)
|
||||
expect(text).toContain('❌ 1 package(s) with incompatible licenses')
|
||||
expect(text).toContain('⚠️ 3 package(s) with unknown licenses')
|
||||
})
|
||||
|
||||
test('uses checkmarks for license issues if only vulnerabilities were found', () => {
|
||||
const vulnerabilities = [createTestChange()]
|
||||
|
||||
summary.addSummaryToSummary(
|
||||
vulnerabilities,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('❌ 1 vulnerable package(s)')
|
||||
expect(text).toContain(
|
||||
'✅ 0 package(s) with invalid SPDX license definitions'
|
||||
)
|
||||
expect(text).toContain('✅ 0 package(s) with incompatible licenses')
|
||||
expect(text).toContain('✅ 0 package(s) with unknown licenses')
|
||||
})
|
||||
|
||||
test('uses checkmarks for vulnerabilities if only license issues were found', () => {
|
||||
const licenseIssues = {
|
||||
forbidden: [createTestChange()],
|
||||
unresolved: [],
|
||||
unlicensed: []
|
||||
}
|
||||
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
licenseIssues,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('✅ 0 vulnerable package(s)')
|
||||
expect(text).toContain(
|
||||
'✅ 0 package(s) with invalid SPDX license definitions'
|
||||
)
|
||||
expect(text).toContain('❌ 1 package(s) with incompatible licenses')
|
||||
expect(text).toContain('✅ 0 package(s) with unknown licenses')
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - only includes section if any vulnerabilites found', () => {
|
||||
summary.addChangeVulnerabilitiesToSummary(emptyChanges, 'low')
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toEqual('')
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - includes all vulnerabilities', () => {
|
||||
const changes = [
|
||||
createTestChange({name: 'lodash'}),
|
||||
createTestChange({name: 'underscore', package_url: 'test-url'})
|
||||
]
|
||||
|
||||
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('<h2>Vulnerabilities</h2>')
|
||||
expect(text).toContain('lodash')
|
||||
expect(text).toContain('underscore')
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - includes advisory url if available', () => {
|
||||
const changes = [
|
||||
createTestChange({
|
||||
name: 'underscore',
|
||||
vulnerabilities: [
|
||||
createTestVulnerability({
|
||||
advisory_summary: 'test-summary',
|
||||
advisory_url: 'test-url'
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
|
||||
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('lodash')
|
||||
expect(text).toContain('<a href="test-url">test-summary</a>')
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - groups vulnerabilities of a single package', () => {
|
||||
const changes = [
|
||||
createTestChange({
|
||||
name: 'package-with-multiple-vulnerabilities',
|
||||
vulnerabilities: [
|
||||
createTestVulnerability({advisory_summary: 'test-summary-1'}),
|
||||
createTestVulnerability({advisory_summary: 'test-summary-2'})
|
||||
]
|
||||
})
|
||||
]
|
||||
|
||||
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text.match('package-with-multiple-vulnerabilities')).toHaveLength(1)
|
||||
expect(text).toContain('test-summary-1')
|
||||
expect(text).toContain('test-summary-2')
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - prints severity statement if above low', () => {
|
||||
const changes = [createTestChange()]
|
||||
|
||||
summary.addChangeVulnerabilitiesToSummary(changes, 'medium')
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain(
|
||||
'Only included vulnerabilities with severity <strong>medium</strong> or higher.'
|
||||
)
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - does not print severity statment if it is set to "low"', () => {
|
||||
const changes = [createTestChange()]
|
||||
|
||||
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).not.toContain('Only included vulnerabilities')
|
||||
})
|
||||
|
||||
test('addLicensesToSummary() - does not include entire section if no license issues found', () => {
|
||||
summary.addLicensesToSummary(emptyInvalidLicenseChanges, defaultConfig)
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toEqual('')
|
||||
})
|
||||
|
||||
test('addLicensesToSummary() - includes all license issues in table', () => {
|
||||
const licenseIssues = {
|
||||
forbidden: [createTestChange()],
|
||||
unresolved: [createTestChange(), createTestChange()],
|
||||
unlicensed: [createTestChange(), createTestChange(), createTestChange()]
|
||||
}
|
||||
|
||||
summary.addLicensesToSummary(licenseIssues, defaultConfig)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('<h2>License Issues</h2>')
|
||||
expect(text).toContain('<td>Incompatible License</td>')
|
||||
expect(text).toContain('<td>Invalid SPDX License</td>')
|
||||
expect(text).toContain('<td>Unknown License</td>')
|
||||
})
|
||||
|
||||
test('addLicenseToSummary() - adds one table per manifest', () => {
|
||||
const licenseIssues = {
|
||||
forbidden: [
|
||||
createTestChange({manifest: 'package.json'}),
|
||||
createTestChange({manifest: '.github/workflows/test.yml'})
|
||||
],
|
||||
unresolved: [],
|
||||
unlicensed: []
|
||||
}
|
||||
|
||||
summary.addLicensesToSummary(licenseIssues, defaultConfig)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
|
||||
expect(text).toContain('<h4><em>package.json</em></h4>')
|
||||
expect(text).toContain('<h4><em>.github/workflows/test.yml</em></h4>')
|
||||
})
|
||||
|
||||
test('addLicensesToSummary() - does not include specific license type sub-section if nothing is found', () => {
|
||||
const licenseIssues = {
|
||||
forbidden: [],
|
||||
unlicensed: [],
|
||||
unresolved: [createTestChange()]
|
||||
}
|
||||
|
||||
summary.addLicensesToSummary(licenseIssues, defaultConfig)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).not.toContain('<td>Incompatible License</td>')
|
||||
expect(text).not.toContain('<td>Unknown License</td>')
|
||||
expect(text).toContain('<td>Invalid SPDX License</td>')
|
||||
})
|
||||
|
||||
test('addLicensesToSummary() - includes list of configured allowed licenses', () => {
|
||||
const licenseIssues = {
|
||||
forbidden: [createTestChange()],
|
||||
unresolved: [],
|
||||
unlicensed: []
|
||||
}
|
||||
|
||||
const config: ConfigurationOptions = {
|
||||
...defaultConfig,
|
||||
allow_licenses: ['MIT', 'Apache-2.0']
|
||||
}
|
||||
|
||||
summary.addLicensesToSummary(licenseIssues, config)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('<strong>Allowed Licenses</strong>: MIT, Apache-2.0')
|
||||
})
|
||||
|
||||
test('addLicensesToSummary() - includes configured denied license', () => {
|
||||
const licenseIssues = {
|
||||
forbidden: [createTestChange()],
|
||||
unresolved: [],
|
||||
unlicensed: []
|
||||
}
|
||||
|
||||
const config: ConfigurationOptions = {
|
||||
...defaultConfig,
|
||||
deny_licenses: ['MIT']
|
||||
}
|
||||
|
||||
summary.addLicensesToSummary(licenseIssues, config)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('<strong>Denied Licenses</strong>: MIT')
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
// GitHub Action inputs come in the form of environment variables
|
||||
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
|
||||
export function setInput(input: string, value: string): void {
|
||||
process.env[`INPUT_${input.toUpperCase()}`] = value
|
||||
}
|
||||
|
||||
// We want a clean ENV before each test. We use `delete`
|
||||
// since we want `undefined` values and not empty strings.
|
||||
export function clearInputs(): void {
|
||||
const allowedOptions = [
|
||||
'FAIL-ON-SEVERITY',
|
||||
'FAIL-ON-SCOPES',
|
||||
'ALLOW-LICENSES',
|
||||
'DENY-LICENSES',
|
||||
'ALLOW-GHSAS',
|
||||
'LICENSE-CHECK',
|
||||
'VULNERABILITY-CHECK',
|
||||
'CONFIG-FILE',
|
||||
'BASE-REF',
|
||||
'HEAD-REF',
|
||||
'COMMENT-SUMMARY-IN-PR',
|
||||
'WARN-ONLY'
|
||||
]
|
||||
|
||||
// eslint-disable-next-line github/array-foreach
|
||||
allowedOptions.forEach(option => {
|
||||
delete process.env[`INPUT_${option.toUpperCase()}`]
|
||||
})
|
||||
}
|
||||
+5
-58
@@ -1,5 +1,3 @@
|
||||
# Avoid using default values for options here since they will
|
||||
# end up overriding external configurations.
|
||||
name: 'Dependency Review'
|
||||
description: 'Prevent the introduction of dependencies with known vulnerabilities'
|
||||
author: 'GitHub'
|
||||
@@ -11,9 +9,11 @@ inputs:
|
||||
fail-on-severity:
|
||||
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
|
||||
@@ -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.
|
||||
required: false
|
||||
config-file:
|
||||
description: A path to the configuration file for the action.
|
||||
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")
|
||||
@@ -29,62 +29,9 @@ inputs:
|
||||
deny-licenses:
|
||||
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
||||
required: false
|
||||
allow-dependencies-licenses:
|
||||
description: Comma-separated list of dependencies in purl format (e.g. "pkg:npm/express, pkg:pypi/pycrypto"). These dependencies will be permitted to use any license, no matter what license policy is enforced otherwise.
|
||||
required: false
|
||||
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
|
||||
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
|
||||
comment-summary-in-pr:
|
||||
description: Determines if the summary is posted as a comment in the PR itself. Setting this to `always` or `on-failure` requires you to give the workflow the write permissions for pull-requests
|
||||
required: false
|
||||
deny-packages:
|
||||
description: A comma-separated list of package URLs to deny (e.g. "pkg:npm/express, pkg:pypi/pycrypto")
|
||||
required: false
|
||||
deny-groups:
|
||||
description: A comma-separated list of package URLs for group(s)/namespace(s) to deny (e.g. "pkg:npm/express, pkg:pypi/pycrypto")
|
||||
required: false
|
||||
retry-on-snapshot-warnings:
|
||||
description: Whether to retry on snapshot warnings
|
||||
required: false
|
||||
default: false
|
||||
retry-on-snapshot-warnings-timeout:
|
||||
description: Number of seconds to wait before stopping snapshot retries.
|
||||
required: false
|
||||
default: 120
|
||||
warn-only:
|
||||
description: When set to `true` this action will always complete with success, overriding the `fail-on-severity` parameter.
|
||||
required: false
|
||||
default: false
|
||||
show-openssf-scorecard:
|
||||
description: Show a summary of the OpenSSF Scorecard scores.
|
||||
required: false
|
||||
default: true
|
||||
warn-on-openssf-scorecard-level:
|
||||
description: Numeric threshold for the OpenSSF Scorecard score. If the score is below this threshold, the action will warn you.
|
||||
required: false
|
||||
default: 3
|
||||
outputs:
|
||||
comment-content:
|
||||
description: Prepared dependency report comment
|
||||
dependency-changes:
|
||||
description: All dependency changes (JSON)
|
||||
vulnerable-changes:
|
||||
description: Vulnerable dependency changes (JSON)
|
||||
invalid-license-changes:
|
||||
description: Invalid license dependency changes (JSON)
|
||||
denied-changes:
|
||||
description: Denied dependency changes (JSON)
|
||||
|
||||
runs:
|
||||
using: 'node20'
|
||||
using: 'node16'
|
||||
main: 'dist/index.js'
|
||||
|
||||
+12607
-36776
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+130
-81
@@ -47,28 +47,6 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
@fastify/busboy
|
||||
MIT
|
||||
Copyright Brian White. All rights reserved.
|
||||
|
||||
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.
|
||||
|
||||
@octokit/app
|
||||
MIT
|
||||
The MIT License
|
||||
@@ -344,17 +322,6 @@ 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.
|
||||
|
||||
|
||||
@octokit/plugin-paginate-graphql
|
||||
MIT
|
||||
MIT License Copyright (c) 2019 Octokit 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 (including the next paragraph) 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.
|
||||
|
||||
|
||||
@octokit/plugin-paginate-rest
|
||||
MIT
|
||||
MIT License Copyright (c) 2019 Octokit contributors
|
||||
@@ -514,6 +481,16 @@ 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.
|
||||
|
||||
|
||||
@vercel/ncc
|
||||
MIT
|
||||
Copyright 2018 ZEIT, Inc.
|
||||
|
||||
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.
|
||||
|
||||
aggregate-error
|
||||
MIT
|
||||
MIT License
|
||||
@@ -1069,6 +1046,30 @@ Apache License
|
||||
limitations under the License.
|
||||
|
||||
|
||||
fromentries
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Feross Aboukhadijeh
|
||||
|
||||
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.
|
||||
|
||||
|
||||
indent-string
|
||||
MIT
|
||||
MIT License
|
||||
@@ -1082,6 +1083,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.
|
||||
|
||||
|
||||
is-plain-object
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2017, Jon Schlinkert.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
jsonwebtoken
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
@@ -1475,11 +1501,37 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
node-fetch
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 David Frank
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
octokit
|
||||
MIT
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2023 Octokit contributors
|
||||
Copyright (c) 2018 Octokit 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
|
||||
@@ -1519,28 +1571,6 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
packageurl-js
|
||||
MIT
|
||||
Copyright (c) the purl authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
safe-buffer
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
@@ -1668,6 +1698,9 @@ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
tr46
|
||||
MIT
|
||||
|
||||
tunnel
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
@@ -1693,31 +1726,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
undici
|
||||
MIT
|
||||
MIT License
|
||||
|
||||
Copyright (c) Matteo Collina and Undici 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.
|
||||
|
||||
|
||||
universal-github-app-jwt
|
||||
MIT
|
||||
The MIT License
|
||||
@@ -1767,6 +1775,47 @@ 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.
|
||||
|
||||
|
||||
webidl-conversions
|
||||
BSD-2-Clause
|
||||
# The BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2014, Domenic Denicola
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
whatwg-url
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015–2016 Sebastian Mayr
|
||||
|
||||
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.
|
||||
|
||||
|
||||
wrappy
|
||||
ISC
|
||||
The ISC License
|
||||
|
||||
@@ -1,339 +0,0 @@
|
||||
# Examples on how to use the Dependency Review Action
|
||||
|
||||
## Basic Usage
|
||||
|
||||
A very basic example of how to use the action. This will run the action with the default configuration.
|
||||
|
||||
The full list of configuration options can be found [here](../README.md#configuration-options).
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
```
|
||||
|
||||
## Using an inline configuration
|
||||
|
||||
The following example will fail the action if any vulnerabilities are found with a severity of medium or higher; and if any packages are found with an incompatible license - in this case, the LGPL-2.0 and BSD-2-Clause licenses.
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
```
|
||||
|
||||
## Using a configuration file
|
||||
|
||||
The following example will use a configuration file to configure the action. This is useful if you want to keep your configuration in a single place and makes it easier to manage as the configuration grows.
|
||||
|
||||
The configuration file can be located in the same repository or in a separate repository. Having it in a separate repository might be useful if you plan to use the same configuration across multiple repositories and control it centrally.
|
||||
|
||||
In this example, the configuration file is located in the same repository under `.github/dependency-review-config.yml`. The following configuration will fail the action if any vulnerabilities are found with a severity of critical; and if any packages are found with an incompatible license - in this case, the LGPL-2.0 and BSD-2-Clause licenses.
|
||||
|
||||
```yaml
|
||||
fail_on_severity: 'critical'
|
||||
allow_licenses:
|
||||
- 'LGPL-2.0'
|
||||
- 'BSD-2-Clause'
|
||||
```
|
||||
|
||||
The Dependency Review Action workflow file will then look like this:
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
config-file: './.github/dependency-review-config.yml'
|
||||
```
|
||||
|
||||
## Using a configuration file from an external repository
|
||||
|
||||
The following example will use a configuration file from an external public GitHub repository to configure the action.
|
||||
|
||||
Let's say that the configuration file is located in `github/octorepo/dependency-review-config.yml@main`
|
||||
|
||||
The Dependancy Review Action workflow file will then look like this:
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
config-file: 'github/octorepo/dependency-review-config.yml@main'
|
||||
```
|
||||
|
||||
## Using a configuration file from an external repository with a personal access token
|
||||
|
||||
The following example will use a configuration file from an external private GtiHub repository to configure the action.
|
||||
|
||||
Let's say that the configuration file is located in `github/octorepo-private/dependency-review-config.yml@main`
|
||||
|
||||
The Dependancy Review Action workflow file will then look like this:
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
config-file: 'github/octorepo-private/dependency-review-config.yml@main'
|
||||
external-repo-token: ${{ secrets.GITHUB_TOKEN }} # or a personal access token
|
||||
```
|
||||
|
||||
## Getting the results of the action in the PR as a comment
|
||||
|
||||
Using the `comment-summary-in-pr` you can get the results of the action in the PR as a comment. In order for this to work, the action needs to be able to create a comment in the PR. This requires additional `pull-requests: write` permission.
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
comment-summary-in-pr: always
|
||||
```
|
||||
|
||||
## Getting the results of the action in a later step
|
||||
|
||||
- `comment-content` contains the output of the results comment for the entire run.
|
||||
`dependency-changes`, `vulnerable-changes`, `invalid-license-changes` and `denied-changes` are all JSON objects that allow you to access individual sets of changes.
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
id: review
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
- name: 'Report'
|
||||
# make sure this step runs even if the previous failed
|
||||
if: ${{ failure() && steps.review.conclusion == 'failure' }}
|
||||
shell: bash
|
||||
env: # store comment HTML data in an environment variable
|
||||
COMMENT: ${{ steps.review.outputs.comment-content }}
|
||||
run: | # do something with the comment:
|
||||
echo "$COMMENT"
|
||||
- name: 'List vulnerable dependencies'
|
||||
# make sure this step runs even if the previous failed
|
||||
if: ${{ failure() && steps.review.conclusion == 'failure' }}
|
||||
shell: bash
|
||||
env: # store JSON data in an environment variable
|
||||
VULNERABLE_CHANGES: ${{ steps.review.outputs.vulnerable-changes }}
|
||||
run: | # do something with the JSON:
|
||||
echo "$VULNERABLE_CHANGES" | jq '.[].package_url'
|
||||
```
|
||||
|
||||
## Exclude dependencies from the license check
|
||||
|
||||
Using the `allow-dependencies-licenses` you can exclude dependencies from the license check. The values should be provided in [purl](https://github.com/package-url/purl-spec) format.
|
||||
|
||||
In this example, we are excluding `lodash` from `npm` and `requests` from `pip` dependencies from the license check
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
comment-summary-in-pr: always
|
||||
allow-dependencies-licenses: 'pkg:npm/loadash, pkg:pypi/requests'
|
||||
```
|
||||
|
||||
If we were to use configuration file, the configuration would look like this:
|
||||
|
||||
```yaml
|
||||
fail-on-severity: 'critical'
|
||||
allow-licenses:
|
||||
- 'LGPL-2.0'
|
||||
- 'BSD-2-Clause'
|
||||
allow-dependencies-licenses:
|
||||
- 'pkg:npm/loadash'
|
||||
- 'pkg:pypi/requests'
|
||||
```
|
||||
|
||||
## Only check for vulnerabilities
|
||||
|
||||
To only do the vulnerability check you can use the `license-check` to disable the license compatibility check (which is done by default).
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
comment-summary-in-pr: always
|
||||
license-check: false
|
||||
```
|
||||
|
||||
## Exclude dependencies from their name or groups
|
||||
|
||||
Using the `deny-packages` option you can exclude dependencies by their PURL. You can add multiple values separated by a commas.
|
||||
Using the `deny-groups` option you can exclude dependencies by their group name/namespace. You can add multiple values separated by a comma.
|
||||
|
||||
In this example, we are excluding `pkg:maven/org.apache.logging.log4j:log4j-api` and `pkg:maven/org.apache.logging.log4j/log4j-core` from `maven` and all packages in the group `pkg:maven/com.bazaarvoice.maven`
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
deny-packages: 'pkg:maven/org.apache.logging.log4j/log4j-api,pkg:maven/org.apache.logging.log4j/log4j-core'
|
||||
deny-groups: 'pkg:maven/com.bazaarvoice.jolt'
|
||||
```
|
||||
|
||||
## Waiting for dependency submission jobs to complete
|
||||
|
||||
When possible, this action will [include dependencies submitted through the dependency submission API][DSAPI]. In this case,
|
||||
it's important for the action not to complete until all of the relevant dependencies have been submitted for both the base
|
||||
and head commits.
|
||||
|
||||
When this action runs before one or more of the dependency submission actions, there will be an unequal number of dependency
|
||||
snapshots between the base and head commits. For example, there may be one snapshot available for the tip of `main` and none
|
||||
for the PR branch. In that case, the API response will contain a "snapshot warning" explaining the discrepancy.
|
||||
|
||||
In this example, when the action encounters one of these warnings it will retry every 10 seconds after that for 60 seconds
|
||||
or until there is no warning in the response.
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
retry-on-snapshot-warnings: true
|
||||
retry-on-snapshot-warnings-timeout: 60
|
||||
```
|
||||
|
||||
[DSAPI]: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#best-practices-for-using-the-dependency-review-api-and-the-dependency-submission-api-together
|
||||
Generated
+9246
-2985
File diff suppressed because it is too large
Load Diff
+27
-28
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "dependency-review-action",
|
||||
"version": "4.2.3",
|
||||
"version": "2.5.1",
|
||||
"private": true,
|
||||
"description": "A GitHub Action for Dependency Review",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"build": "tsc",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
@@ -25,37 +25,36 @@
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@octokit/plugin-retry": "^6.0.1",
|
||||
"@octokit/request-error": "^5.0.1",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1",
|
||||
"@octokit/plugin-retry": "^4.0.3",
|
||||
"@octokit/request-error": "^3.0.2",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"got": "^14.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"octokit": "^3.1.2",
|
||||
"packageurl-js": "^1.2.0",
|
||||
"got": "^12.5.2",
|
||||
"nodemon": "^2.0.20",
|
||||
"octokit": "^2.0.10",
|
||||
"spdx-expression-parse": "^3.0.1",
|
||||
"spdx-satisfies": "^5.0.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
"yaml": "^2.3.4",
|
||||
"zod": "^3.22.3"
|
||||
"yaml": "^2.1.3",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/spdx-expression-parse": "^3.0.4",
|
||||
"@types/spdx-satisfies": "^0.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vercel/ncc": "^0.38.0",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-github": "^4.10.1",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||
"@typescript-eslint/parser": "^5.41.0",
|
||||
"@types/spdx-expression-parse": "^3.0.2",
|
||||
"@types/spdx-satisfies": "^0.1.0",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"esbuild-register": "^3.3.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": "^3.1.0",
|
||||
"prettier": "3.2.5",
|
||||
"typescript": "^5.3.3"
|
||||
"nodemon": "^2.0.20",
|
||||
"prettier": "2.7.1",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* This scripts creates example markdown files for the summary in the ./tmp folder.
|
||||
* You can use it to preview changes to the summary.
|
||||
*
|
||||
* You can execute it like this:
|
||||
* npx ts-node scripts/create_summary.ts
|
||||
*/
|
||||
|
||||
import {Change, Changes, ConfigurationOptions, Scorecard} from '../src/schemas'
|
||||
import {createTestChange} from '../__tests__/fixtures/create-test-change'
|
||||
import {InvalidLicenseChanges} from '../src/licenses'
|
||||
import * as fs from 'fs'
|
||||
import * as core from '@actions/core'
|
||||
import * as summary from '../src/summary'
|
||||
import * as path from 'path'
|
||||
|
||||
const defaultConfig: ConfigurationOptions = {
|
||||
vulnerability_check: true,
|
||||
license_check: true,
|
||||
fail_on_severity: 'high',
|
||||
fail_on_scopes: ['runtime'],
|
||||
allow_ghsas: [],
|
||||
allow_licenses: ['MIT'],
|
||||
deny_licenses: [],
|
||||
deny_packages: [],
|
||||
deny_groups: [],
|
||||
allow_dependencies_licenses: [
|
||||
'pkg:npm/express@4.17.1',
|
||||
'pkg:pypi/requests',
|
||||
'pkg:pypi/certifi',
|
||||
'pkg:pypi/pycrypto@2.6.1'
|
||||
],
|
||||
comment_summary_in_pr: true,
|
||||
retry_on_snapshot_warnings: false,
|
||||
retry_on_snapshot_warnings_timeout: 120,
|
||||
warn_only: false,
|
||||
warn_on_openssf_scorecard_level: 3,
|
||||
show_openssf_scorecard: true
|
||||
}
|
||||
|
||||
const scorecard: Scorecard = {
|
||||
dependencies: [
|
||||
{
|
||||
change: {
|
||||
change_type: 'added',
|
||||
manifest: '',
|
||||
ecosystem: 'unknown',
|
||||
name: 'castore',
|
||||
version: '0.1.17',
|
||||
package_url: 'pkg:hex/castore@0.1.17',
|
||||
license: null,
|
||||
source_repository_url: null,
|
||||
scope: 'runtime',
|
||||
vulnerabilities: []
|
||||
},
|
||||
scorecard: null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const tmpDir = path.resolve(__dirname, '../tmp')
|
||||
|
||||
const createExampleSummaries = async (): Promise<void> => {
|
||||
await fs.promises.mkdir(tmpDir, {recursive: true})
|
||||
|
||||
await createNonIssueSummary()
|
||||
await createFullSummary()
|
||||
}
|
||||
|
||||
const createNonIssueSummary = async (): Promise<void> => {
|
||||
await createSummary(
|
||||
[],
|
||||
{forbidden: [], unresolved: [], unlicensed: []},
|
||||
[],
|
||||
defaultConfig,
|
||||
'non-issue-summary.md'
|
||||
)
|
||||
}
|
||||
|
||||
const createFullSummary = async (): Promise<void> => {
|
||||
const changes = [createTestChange()]
|
||||
const licenses: InvalidLicenseChanges = {
|
||||
forbidden: [
|
||||
createTestChange({
|
||||
name: 'underscore',
|
||||
version: '1.12.0',
|
||||
license: 'Apache 2.0'
|
||||
})
|
||||
],
|
||||
unresolved: [
|
||||
createTestChange({
|
||||
name: 'octoinvader',
|
||||
license: 'Non SPDX License'
|
||||
}),
|
||||
createTestChange({
|
||||
name: 'owner/action-1',
|
||||
license: 'XYZ-License',
|
||||
version: 'v1.2.2',
|
||||
manifest: '.github/workflows/action.yml'
|
||||
})
|
||||
],
|
||||
unlicensed: [
|
||||
createTestChange({
|
||||
name: 'my-other-dependency',
|
||||
license: null
|
||||
}),
|
||||
createTestChange({
|
||||
name: 'owner/action-2',
|
||||
version: 'main',
|
||||
license: null,
|
||||
manifest: '.github/workflows/action.yml'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
await createSummary(changes, licenses, [], defaultConfig, 'full-summary.md')
|
||||
}
|
||||
|
||||
async function createSummary(
|
||||
vulnerabilities: Changes,
|
||||
licenseIssues: InvalidLicenseChanges,
|
||||
denied: Change[],
|
||||
config: ConfigurationOptions,
|
||||
fileName: string
|
||||
): Promise<void> {
|
||||
summary.addSummaryToSummary(
|
||||
vulnerabilities,
|
||||
licenseIssues,
|
||||
denied,
|
||||
scorecard,
|
||||
config
|
||||
)
|
||||
summary.addChangeVulnerabilitiesToSummary(
|
||||
vulnerabilities,
|
||||
config.fail_on_severity
|
||||
)
|
||||
summary.addLicensesToSummary(licenseIssues, defaultConfig)
|
||||
|
||||
const allChanges = [
|
||||
...vulnerabilities,
|
||||
...licenseIssues.forbidden,
|
||||
...licenseIssues.unresolved,
|
||||
...licenseIssues.unlicensed
|
||||
]
|
||||
|
||||
summary.addScannedDependencies(allChanges)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
await fs.promises.writeFile(path.resolve(tmpDir, fileName), text, {
|
||||
flag: 'w'
|
||||
})
|
||||
core.summary.emptyBuffer()
|
||||
}
|
||||
|
||||
createExampleSummaries()
|
||||
@@ -1,102 +0,0 @@
|
||||
import * as github from '@actions/github'
|
||||
import * as core from '@actions/core'
|
||||
import * as githubUtils from '@actions/github/lib/utils'
|
||||
import * as retry from '@octokit/plugin-retry'
|
||||
import {RequestError} from '@octokit/request-error'
|
||||
import {ConfigurationOptions} from './schemas'
|
||||
|
||||
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry)
|
||||
const octo = new retryingOctokit(
|
||||
githubUtils.getOctokitOptions(core.getInput('repo-token', {required: true}))
|
||||
)
|
||||
|
||||
// Comment Marker to identify an existing comment to update, so we don't spam the PR with comments
|
||||
const COMMENT_MARKER = '<!-- dependency-review-pr-comment-marker -->'
|
||||
|
||||
export async function commentPr(
|
||||
summary: typeof core.summary,
|
||||
config: ConfigurationOptions
|
||||
): Promise<void> {
|
||||
const commentContent = summary.stringify()
|
||||
|
||||
core.setOutput('comment-content', commentContent)
|
||||
|
||||
if (
|
||||
!(
|
||||
config.comment_summary_in_pr === 'always' ||
|
||||
(config.comment_summary_in_pr === 'on-failure' &&
|
||||
process.exitCode === core.ExitCode.Failure)
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!github.context.payload.pull_request) {
|
||||
core.warning(
|
||||
'Not in the context of a pull request. Skipping comment creation.'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const commentBody = `${commentContent}\n\n${COMMENT_MARKER}`
|
||||
|
||||
try {
|
||||
const existingCommentId = await findCommentByMarker(COMMENT_MARKER)
|
||||
|
||||
if (existingCommentId) {
|
||||
await octo.rest.issues.updateComment({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
comment_id: existingCommentId,
|
||||
body: commentBody
|
||||
})
|
||||
} else {
|
||||
await octo.rest.issues.createComment({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
issue_number: github.context.payload.pull_request.number,
|
||||
body: commentBody
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof RequestError && error.status === 403) {
|
||||
core.warning(
|
||||
`Unable to write summary to pull-request. Make sure you are giving this workflow the permission 'pull-requests: write'.`
|
||||
)
|
||||
} else {
|
||||
if (error instanceof Error) {
|
||||
core.warning(
|
||||
`Unable to comment summary to pull-request, received error: ${error.message}`
|
||||
)
|
||||
} else {
|
||||
core.warning(
|
||||
'Unable to comment summary to pull-request: Unexpected fatal error'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function findCommentByMarker(
|
||||
commentBodyIncludes: string
|
||||
): Promise<number | undefined> {
|
||||
const commentsIterator = octo.paginate.iterator(
|
||||
octo.rest.issues.listComments,
|
||||
{
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
// We are already checking if we are in the context of a pull request in the caller
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
issue_number: github.context.payload.pull_request!.number
|
||||
}
|
||||
)
|
||||
|
||||
for await (const {data: comments} of commentsIterator) {
|
||||
const existingComment = comments.find(comment =>
|
||||
comment.body?.includes(commentBodyIncludes)
|
||||
)
|
||||
if (existingComment) return existingComment.id
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
+90
-200
@@ -3,91 +3,15 @@ import path from 'path'
|
||||
import YAML from 'yaml'
|
||||
import * as core from '@actions/core'
|
||||
import * as z from 'zod'
|
||||
import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas'
|
||||
import {isSPDXValid, octokitClient} from './utils'
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
import {
|
||||
ConfigurationOptions,
|
||||
ConfigurationOptionsSchema,
|
||||
SeveritySchema,
|
||||
SCOPES
|
||||
} from './schemas'
|
||||
import {isSPDXValid} from './utils'
|
||||
|
||||
type ConfigurationOptionsPartial = Partial<ConfigurationOptions>
|
||||
|
||||
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_dependencies_licenses = parseList(
|
||||
getOptionalInput('allow-dependencies-licenses')
|
||||
)
|
||||
const deny_packages = parseList(getOptionalInput('deny-packages'))
|
||||
const deny_groups = parseList(getOptionalInput('deny-groups'))
|
||||
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')
|
||||
const comment_summary_in_pr = getOptionalInput('comment-summary-in-pr')
|
||||
const retry_on_snapshot_warnings = getOptionalBoolean(
|
||||
'retry-on-snapshot-warnings'
|
||||
)
|
||||
const retry_on_snapshot_warnings_timeout = getOptionalNumber(
|
||||
'retry-on-snapshot-warnings-timeout'
|
||||
)
|
||||
const warn_only = getOptionalBoolean('warn-only')
|
||||
const show_openssf_scorecard = getOptionalBoolean('show-openssf-scorecard')
|
||||
const warn_on_openssf_scorecard_level = getOptionalNumber(
|
||||
'warn-on-openssf-scorecard-level'
|
||||
)
|
||||
|
||||
validatePURL(allow_dependencies_licenses)
|
||||
validateLicenses('allow-licenses', allow_licenses)
|
||||
validateLicenses('deny-licenses', deny_licenses)
|
||||
|
||||
const keys = {
|
||||
fail_on_severity,
|
||||
fail_on_scopes,
|
||||
allow_licenses,
|
||||
deny_licenses,
|
||||
deny_packages,
|
||||
deny_groups,
|
||||
allow_dependencies_licenses,
|
||||
allow_ghsas,
|
||||
license_check,
|
||||
vulnerability_check,
|
||||
base_ref,
|
||||
head_ref,
|
||||
comment_summary_in_pr,
|
||||
retry_on_snapshot_warnings,
|
||||
retry_on_snapshot_warnings_timeout,
|
||||
warn_only,
|
||||
show_openssf_scorecard,
|
||||
warn_on_openssf_scorecard_level
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(keys).filter(([_, value]) => value !== undefined)
|
||||
)
|
||||
}
|
||||
|
||||
function getOptionalNumber(name: string): number | undefined {
|
||||
const value = core.getInput(name)
|
||||
const parsed = z.string().regex(/^\d+$/).transform(Number).safeParse(value)
|
||||
return parsed.success ? parsed.data : undefined
|
||||
}
|
||||
type licenseKey = 'allow-licenses' | 'deny-licenses'
|
||||
|
||||
function getOptionalBoolean(name: string): boolean | undefined {
|
||||
const value = core.getInput(name)
|
||||
@@ -108,138 +32,104 @@ function parseList(list: string | undefined): string[] | undefined {
|
||||
}
|
||||
|
||||
function validateLicenses(
|
||||
key: 'allow-licenses' | 'deny-licenses',
|
||||
key: licenseKey,
|
||||
licenses: string[] | undefined
|
||||
): void {
|
||||
if (licenses === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const invalid_licenses = licenses.filter(license => !isSPDXValid(license))
|
||||
|
||||
if (invalid_licenses.length > 0) {
|
||||
throw new Error(`Invalid license(s) in ${key}: ${invalid_licenses}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function readConfigFile(
|
||||
filePath: string
|
||||
): Promise<ConfigurationOptionsPartial> {
|
||||
// match a remote config (e.g. 'owner/repo/filepath@someref')
|
||||
const format = new RegExp(
|
||||
'(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)'
|
||||
)
|
||||
|
||||
let data: string
|
||||
const pieces = format.exec(filePath)
|
||||
|
||||
try {
|
||||
if (pieces?.groups && pieces.length === 5) {
|
||||
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) {
|
||||
throw new Error(
|
||||
`Unable to fetch or parse config file: ${(error as Error).message}`
|
||||
`Invalid license(s) in ${key}: ${invalid_licenses.join(', ')}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfigFile(configData: string): ConfigurationOptionsPartial {
|
||||
export function readConfig(): ConfigurationOptions {
|
||||
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")
|
||||
}
|
||||
validateLicenses('allow-licenses', allow_licenses)
|
||||
validateLicenses('deny-licenses', deny_licenses)
|
||||
|
||||
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
|
||||
|
||||
const license_check = z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.parse(getOptionalBoolean('license-check'))
|
||||
const vulnerability_check = z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.parse(getOptionalBoolean('vulnerability-check'))
|
||||
if (license_check === false && vulnerability_check === false) {
|
||||
throw new Error("Can't disable both license-check and vulnerability-check")
|
||||
}
|
||||
|
||||
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,
|
||||
license_check,
|
||||
vulnerability_check,
|
||||
base_ref,
|
||||
head_ref
|
||||
}
|
||||
}
|
||||
|
||||
export function readConfigFile(filePath: string): ConfigurationOptions {
|
||||
let data
|
||||
|
||||
try {
|
||||
const data = YAML.parse(configData)
|
||||
|
||||
// These are the options that we support where the user can provide
|
||||
// either a YAML list or a comma-separated string.
|
||||
const listKeys = [
|
||||
'allow-licenses',
|
||||
'deny-licenses',
|
||||
'fail-on-scopes',
|
||||
'allow-ghsas',
|
||||
'allow-dependencies-licenses',
|
||||
'deny-packages',
|
||||
'deny-groups'
|
||||
]
|
||||
|
||||
for (const key of Object.keys(data)) {
|
||||
// strings can contain list values (e.g. 'MIT, Apache-2.0'). In this
|
||||
// case we need to parse that into a list (e.g. ['MIT', 'Apache-2.0']).
|
||||
if (listKeys.includes(key)) {
|
||||
const val = data[key]
|
||||
|
||||
if (typeof val === 'string') {
|
||||
data[key] = val.split(',').map(x => x.trim())
|
||||
}
|
||||
}
|
||||
|
||||
// perform SPDX validation
|
||||
if (key === 'allow-licenses' || key === 'deny-licenses') {
|
||||
validateLicenses(key, data[key])
|
||||
}
|
||||
|
||||
// validate purls from the allow-dependencies-licenses
|
||||
if (key === 'allow-dependencies-licenses') {
|
||||
validatePURL(data[key])
|
||||
}
|
||||
|
||||
// get rid of the ugly dashes from the actions conventions
|
||||
if (key.includes('-')) {
|
||||
data[key.replace(/-/g, '_')] = data[key]
|
||||
delete data[key]
|
||||
}
|
||||
}
|
||||
return data
|
||||
} catch (error) {
|
||||
data = fs.readFileSync(path.resolve(filePath), 'utf-8')
|
||||
} catch (error: unknown) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
data = YAML.parse(data)
|
||||
|
||||
async function getRemoteConfig(configOpts: {
|
||||
[key: string]: string
|
||||
}): Promise<string> {
|
||||
try {
|
||||
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')
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
function validatePURL(allow_dependencies_licenses: string[] | undefined): void {
|
||||
//validate that the provided elements of the string are in valid purl format
|
||||
if (allow_dependencies_licenses === undefined) {
|
||||
return
|
||||
}
|
||||
const invalid_purls = allow_dependencies_licenses.filter(
|
||||
purl => !PackageURL.fromString(purl)
|
||||
)
|
||||
|
||||
if (invalid_purls.length > 0) {
|
||||
throw new Error(
|
||||
`Invalid purl(s) in allow-dependencies-licenses: ${invalid_purls}`
|
||||
)
|
||||
}
|
||||
return
|
||||
const values = ConfigurationOptionsSchema.parse(data)
|
||||
return values
|
||||
}
|
||||
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
import {Change} from './schemas'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
export async function getDeniedChanges(
|
||||
changes: Change[],
|
||||
deniedPackages: string[],
|
||||
deniedGroups: string[]
|
||||
): Promise<Change[]> {
|
||||
const changesDenied: Change[] = []
|
||||
|
||||
let failed = false
|
||||
for (const change of changes) {
|
||||
change.name = change.name.toLowerCase()
|
||||
const packageUrl = change.package_url.toLowerCase().split('@')[0]
|
||||
|
||||
if (deniedPackages) {
|
||||
for (const denied of deniedPackages) {
|
||||
if (packageUrl === denied.split('@')[0].toLowerCase()) {
|
||||
changesDenied.push(change)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deniedGroups) {
|
||||
for (const denied of deniedGroups) {
|
||||
if (packageUrl.startsWith(denied.toLowerCase())) {
|
||||
changesDenied.push(change)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
core.setFailed('Dependency review detected denied packages.')
|
||||
} else {
|
||||
core.info('Dependency review did not detect any denied packages')
|
||||
}
|
||||
|
||||
return changesDenied
|
||||
}
|
||||
+5
-28
@@ -1,14 +1,9 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as githubUtils from '@actions/github/lib/utils'
|
||||
import * as retry from '@octokit/plugin-retry'
|
||||
import {
|
||||
ChangesSchema,
|
||||
ComparisonResponse,
|
||||
ComparisonResponseSchema
|
||||
} from './schemas'
|
||||
import {Changes, ChangesSchema} from './schemas'
|
||||
|
||||
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry)
|
||||
const SnapshotWarningsHeader = 'x-github-dependency-graph-snapshot-warnings'
|
||||
const octo = new retryingOctokit(
|
||||
githubUtils.getOctokitOptions(core.getInput('repo-token', {required: true}))
|
||||
)
|
||||
@@ -23,32 +18,14 @@ export async function compare({
|
||||
repo: string
|
||||
baseRef: string
|
||||
headRef: string
|
||||
}): Promise<ComparisonResponse> {
|
||||
let snapshot_warnings = ''
|
||||
}): Promise<Changes> {
|
||||
const changes = await octo.paginate(
|
||||
'GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}',
|
||||
{
|
||||
method: 'GET',
|
||||
url: '/repos/{owner}/{repo}/dependency-graph/compare/{basehead}',
|
||||
owner,
|
||||
repo,
|
||||
basehead: `${baseRef}...${headRef}`,
|
||||
per_page: 5
|
||||
},
|
||||
response => {
|
||||
if (
|
||||
response.headers[SnapshotWarningsHeader] &&
|
||||
typeof response.headers[SnapshotWarningsHeader] === 'string'
|
||||
) {
|
||||
snapshot_warnings = Buffer.from(
|
||||
response.headers[SnapshotWarningsHeader],
|
||||
'base64'
|
||||
).toString('utf-8')
|
||||
}
|
||||
return ChangesSchema.parse(response.data)
|
||||
basehead: `${baseRef}...${headRef}`
|
||||
}
|
||||
)
|
||||
return ComparisonResponseSchema.parse({
|
||||
changes,
|
||||
snapshot_warnings
|
||||
})
|
||||
return ChangesSchema.parse(changes)
|
||||
}
|
||||
|
||||
+13
-23
@@ -1,13 +1,5 @@
|
||||
import {Changes, Severity, SEVERITIES, Scope} from './schemas'
|
||||
|
||||
/**
|
||||
* Filters changes by a severity level. Only vulnerable
|
||||
* dependencies will be returned.
|
||||
*
|
||||
* @param severity - The severity level to filter by.
|
||||
* @param changes - The array of changes to filter.
|
||||
* @returns The filtered array of changes that match the specified severity level and have vulnerabilities.
|
||||
*/
|
||||
export function filterChangesBySeverity(
|
||||
severity: Severity,
|
||||
changes: Changes
|
||||
@@ -39,14 +31,7 @@ export function filterChangesBySeverity(
|
||||
filteredChanges = filteredChanges.filter(
|
||||
change => change.vulnerabilities.length > 0
|
||||
)
|
||||
|
||||
// only report vulnerability additions
|
||||
return filteredChanges.filter(
|
||||
change =>
|
||||
change.change_type === 'added' &&
|
||||
change.vulnerabilities !== undefined &&
|
||||
change.vulnerabilities.length > 0
|
||||
)
|
||||
return filteredChanges
|
||||
}
|
||||
|
||||
export function filterChangesByScopes(
|
||||
@@ -82,20 +67,25 @@ export function filterAllowedAdvisories(
|
||||
return changes
|
||||
}
|
||||
|
||||
const filteredChanges = changes.map(change => {
|
||||
const filteredChanges = changes.filter(change => {
|
||||
const noAdvisories =
|
||||
change.vulnerabilities === undefined ||
|
||||
change.vulnerabilities.length === 0
|
||||
|
||||
if (noAdvisories) {
|
||||
return change
|
||||
return true
|
||||
}
|
||||
const newChange = {...change}
|
||||
newChange.vulnerabilities = change.vulnerabilities.filter(
|
||||
vuln => !ghsas.includes(vuln.advisory_ghsa_id)
|
||||
)
|
||||
|
||||
return newChange
|
||||
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
|
||||
|
||||
+2
-5
@@ -21,19 +21,16 @@ export function getRefs(
|
||||
if (!base_ref && !head_ref) {
|
||||
throw new Error(
|
||||
'Both a base ref and head ref must be provided, either via the `base_ref`/`head_ref` ' +
|
||||
'config options, `base-ref`/`head-ref` workflow action options, or by running a ' +
|
||||
'`pull_request`/`pull_request_target` workflow.'
|
||||
'config options, or by running a `pull_request`/`pull_request_target` workflow.'
|
||||
)
|
||||
} else if (!base_ref) {
|
||||
throw new Error(
|
||||
'A base ref must be provided, either via the `base_ref` config option, ' +
|
||||
'`base-ref` workflow action option, or by running a ' +
|
||||
'`pull_request`/`pull_request_target` workflow.'
|
||||
'or by running a `pull_request`/`pull_request_target` workflow.'
|
||||
)
|
||||
} else if (!head_ref) {
|
||||
throw new Error(
|
||||
'A head ref must be provided, either via the `head_ref` config option, ' +
|
||||
'`head-ref` workflow action option, or by running a ' +
|
||||
'or by running a `pull_request`/`pull_request_target` workflow.'
|
||||
)
|
||||
}
|
||||
|
||||
+11
-50
@@ -1,74 +1,34 @@
|
||||
import * as core from '@actions/core'
|
||||
import spdxSatisfies from 'spdx-satisfies'
|
||||
import {Octokit} from 'octokit'
|
||||
import {Change, Changes} from './schemas'
|
||||
import {isSPDXValid, octokitClient} from './utils'
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
import {isSPDXValid} from './utils'
|
||||
|
||||
/**
|
||||
* Loops through a list of changes, filtering and returning the
|
||||
* ones that don't conform to the licenses allow/deny lists.
|
||||
* It will also filter out the changes which are defined in the licenseExclusions list.
|
||||
*
|
||||
* Keep in mind that we don't let users specify both an allow and a deny
|
||||
* list in their config files, so this code works under the assumption that
|
||||
* one of the two list parameters will be empty. If both lists are provided,
|
||||
* we will ignore the deny list.
|
||||
* @param {Change[]} changes The list of changes to filter.
|
||||
* @param { { allow?: string[], deny?: string[], licenseExclusions?: string[]}} licenses An object with `allow`/`deny`/`licenseExclusions` 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<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
|
||||
*/
|
||||
export type InvalidLicenseChangeTypes =
|
||||
| 'unlicensed'
|
||||
| 'unresolved'
|
||||
| 'forbidden'
|
||||
export type InvalidLicenseChanges = Record<InvalidLicenseChangeTypes, Changes>
|
||||
export async function getInvalidLicenseChanges(
|
||||
changes: Change[],
|
||||
licenses: {
|
||||
allow?: string[]
|
||||
deny?: string[]
|
||||
licenseExclusions?: string[]
|
||||
}
|
||||
): Promise<InvalidLicenseChanges> {
|
||||
): Promise<Record<string, Changes>> {
|
||||
const {allow, deny} = licenses
|
||||
const licenseExclusions = licenses.licenseExclusions?.map(
|
||||
(pkgUrl: string) => {
|
||||
return PackageURL.fromString(encodeURI(pkgUrl))
|
||||
}
|
||||
)
|
||||
|
||||
const groupedChanges = await groupChanges(changes)
|
||||
|
||||
// Takes the changes from the groupedChanges object and filters out the ones that are part of the exclusions list
|
||||
// It does by creating a new PackageURL object from the change and comparing it to the exclusions list
|
||||
groupedChanges.licensed = groupedChanges.licensed.filter(change => {
|
||||
if (change.package_url.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const changeAsPackageURL = PackageURL.fromString(
|
||||
encodeURI(change.package_url)
|
||||
)
|
||||
|
||||
// We want to find if the licenseExclussion list contains the PackageURL of the Change
|
||||
// If it does, we want to filter it out and therefore return false
|
||||
// If it doesn't, we want to keep it and therefore return true
|
||||
if (
|
||||
licenseExclusions !== null &&
|
||||
licenseExclusions !== undefined &&
|
||||
licenseExclusions.findIndex(
|
||||
exclusion =>
|
||||
exclusion.type === changeAsPackageURL.type &&
|
||||
exclusion.name === changeAsPackageURL.name
|
||||
) !== -1
|
||||
) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
})
|
||||
const licensedChanges: Changes = groupedChanges.licensed
|
||||
|
||||
const invalidLicenseChanges: InvalidLicenseChanges = {
|
||||
const invalidLicenseChanges: Record<string, Changes> = {
|
||||
unlicensed: groupedChanges.unlicensed,
|
||||
unresolved: [],
|
||||
forbidden: []
|
||||
@@ -116,11 +76,12 @@ 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 octokitClient().rest.licenses.getForRepo({
|
||||
owner,
|
||||
repo
|
||||
})
|
||||
const response = await octokit.rest.licenses.getForRepo({owner, repo})
|
||||
return response.data.license?.spdx_id ?? null
|
||||
} catch (_) {
|
||||
return null
|
||||
|
||||
+29
-198
@@ -3,13 +3,7 @@ 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,
|
||||
Changes,
|
||||
ConfigurationOptions,
|
||||
Scorecard
|
||||
} from './schemas'
|
||||
import {Change, Severity, Changes} from './schemas'
|
||||
import {readConfig} from '../src/config'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
@@ -17,156 +11,64 @@ import {
|
||||
filterAllowedAdvisories
|
||||
} from '../src/filter'
|
||||
import {getInvalidLicenseChanges} from './licenses'
|
||||
import {getScorecardLevels} from './scorecard'
|
||||
import * as summary from './summary'
|
||||
import {getRefs} from './git-refs'
|
||||
|
||||
import {groupDependenciesByManifest} from './utils'
|
||||
import {commentPr} from './comment-pr'
|
||||
import {getDeniedChanges} from './deny'
|
||||
|
||||
async function delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function getComparison(
|
||||
baseRef: string,
|
||||
headRef: string,
|
||||
retryOpts?: {
|
||||
retryUntil: number
|
||||
retryDelay: number
|
||||
}
|
||||
): ReturnType<typeof dependencyGraph.compare> {
|
||||
const comparison = await dependencyGraph.compare({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
baseRef,
|
||||
headRef
|
||||
})
|
||||
|
||||
if (comparison.snapshot_warnings.trim() !== '') {
|
||||
core.info(comparison.snapshot_warnings)
|
||||
if (retryOpts !== undefined) {
|
||||
if (retryOpts.retryUntil < Date.now()) {
|
||||
core.info(`Retry timeout exceeded. Proceeding...`)
|
||||
return comparison
|
||||
} else {
|
||||
core.info(`Retrying in ${retryOpts.retryDelay} seconds...`)
|
||||
await delay(retryOpts.retryDelay * 1000)
|
||||
return getComparison(baseRef, headRef, retryOpts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return comparison
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const config = await readConfig()
|
||||
|
||||
const config = readConfig()
|
||||
const refs = getRefs(config, github.context)
|
||||
|
||||
const comparison = await getComparison(
|
||||
refs.base,
|
||||
refs.head,
|
||||
config.retry_on_snapshot_warnings
|
||||
? {
|
||||
retryUntil:
|
||||
Date.now() + config.retry_on_snapshot_warnings_timeout * 1000,
|
||||
retryDelay: 10
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
|
||||
const changes = comparison.changes
|
||||
const snapshot_warnings = comparison.snapshot_warnings
|
||||
|
||||
if (!changes) {
|
||||
core.info('No Dependency Changes found. Skipping Dependency Review.')
|
||||
return
|
||||
}
|
||||
const changes = await dependencyGraph.compare({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
baseRef: refs.base,
|
||||
headRef: refs.head
|
||||
})
|
||||
|
||||
const minSeverity = config.fail_on_severity as Severity
|
||||
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
|
||||
|
||||
const filteredChanges = filterAllowedAdvisories(
|
||||
config.allow_ghsas,
|
||||
scopedChanges
|
||||
)
|
||||
|
||||
const failOnSeverityParams = config.fail_on_severity
|
||||
const warnOnly = config.warn_only
|
||||
let minSeverity: Severity = 'low'
|
||||
// If failOnSeverityParams is not set or warnOnly is true, the minSeverity is low, to allow all vulnerabilities to be reported as warnings
|
||||
if (failOnSeverityParams && !warnOnly) {
|
||||
minSeverity = failOnSeverityParams
|
||||
}
|
||||
|
||||
const vulnerableChanges = filterChangesBySeverity(
|
||||
const addedChanges = filterChangesBySeverity(
|
||||
minSeverity,
|
||||
filteredChanges
|
||||
).filter(
|
||||
change =>
|
||||
change.change_type === 'added' &&
|
||||
change.vulnerabilities !== undefined &&
|
||||
change.vulnerabilities.length > 0
|
||||
)
|
||||
|
||||
const invalidLicenseChanges = await getInvalidLicenseChanges(
|
||||
filteredChanges,
|
||||
{
|
||||
allow: config.allow_licenses,
|
||||
deny: config.deny_licenses,
|
||||
licenseExclusions: config.allow_dependencies_licenses
|
||||
deny: config.deny_licenses
|
||||
}
|
||||
)
|
||||
|
||||
core.debug(`Filtered Changes: ${JSON.stringify(filteredChanges)}`)
|
||||
core.debug(`Config Deny Packages: ${JSON.stringify(config)}`)
|
||||
|
||||
const deniedChanges = await getDeniedChanges(
|
||||
filteredChanges,
|
||||
config.deny_packages,
|
||||
config.deny_groups
|
||||
)
|
||||
|
||||
const scorecard = await getScorecardLevels(filteredChanges)
|
||||
|
||||
summary.addSummaryToSummary(
|
||||
vulnerableChanges,
|
||||
invalidLicenseChanges,
|
||||
deniedChanges,
|
||||
scorecard,
|
||||
config
|
||||
config.vulnerability_check ? addedChanges : null,
|
||||
config.license_check ? invalidLicenseChanges : null
|
||||
)
|
||||
|
||||
if (snapshot_warnings) {
|
||||
summary.addSnapshotWarnings(config, snapshot_warnings)
|
||||
}
|
||||
|
||||
if (config.vulnerability_check) {
|
||||
core.setOutput('vulnerable-changes', JSON.stringify(vulnerableChanges))
|
||||
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
|
||||
printVulnerabilitiesBlock(vulnerableChanges, minSeverity, warnOnly)
|
||||
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
|
||||
printVulnerabilitiesBlock(addedChanges, minSeverity)
|
||||
}
|
||||
if (config.license_check) {
|
||||
core.setOutput(
|
||||
'invalid-license-changes',
|
||||
JSON.stringify(invalidLicenseChanges)
|
||||
)
|
||||
summary.addLicensesToSummary(invalidLicenseChanges, config)
|
||||
printLicensesBlock(invalidLicenseChanges, warnOnly)
|
||||
}
|
||||
if (config.deny_packages || config.deny_groups) {
|
||||
core.setOutput('denied-changes', JSON.stringify(deniedChanges))
|
||||
summary.addDeniedToSummary(deniedChanges)
|
||||
printDeniedDependencies(deniedChanges, config)
|
||||
}
|
||||
if (config.show_openssf_scorecard) {
|
||||
summary.addScorecardToSummary(scorecard, config)
|
||||
printScorecardBlock(scorecard, config)
|
||||
createScorecardWarnings(scorecard, config)
|
||||
printLicensesBlock(invalidLicenseChanges)
|
||||
}
|
||||
|
||||
core.setOutput('dependency-changes', JSON.stringify(changes))
|
||||
summary.addScannedDependencies(changes)
|
||||
printScannedDependencies(changes)
|
||||
await commentPr(core.summary, config)
|
||||
} catch (error) {
|
||||
if (error instanceof RequestError && error.status === 404) {
|
||||
core.setFailed(
|
||||
@@ -174,7 +76,7 @@ async function run(): Promise<void> {
|
||||
)
|
||||
} else if (error instanceof RequestError && error.status === 403) {
|
||||
core.setFailed(
|
||||
`Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled along with GitHub Advanced Security on private repositories, see https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/settings/security_analysis`
|
||||
`Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled, see https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/settings/security_analysis`
|
||||
)
|
||||
} else {
|
||||
if (error instanceof Error) {
|
||||
@@ -190,25 +92,19 @@ async function run(): Promise<void> {
|
||||
|
||||
function printVulnerabilitiesBlock(
|
||||
addedChanges: Changes,
|
||||
minSeverity: Severity,
|
||||
warnOnly: boolean
|
||||
minSeverity: Severity
|
||||
): void {
|
||||
let vulFound = false
|
||||
let failed = false
|
||||
core.group('Vulnerabilities', async () => {
|
||||
if (addedChanges.length > 0) {
|
||||
for (const change of addedChanges) {
|
||||
printChangeVulnerabilities(change)
|
||||
}
|
||||
vulFound = true
|
||||
failed = true
|
||||
}
|
||||
|
||||
if (vulFound) {
|
||||
const msg = 'Dependency review detected vulnerable packages.'
|
||||
if (warnOnly) {
|
||||
core.warning(msg)
|
||||
} else {
|
||||
core.setFailed(msg)
|
||||
}
|
||||
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.`
|
||||
@@ -231,19 +127,13 @@ function printChangeVulnerabilities(change: Change): void {
|
||||
}
|
||||
|
||||
function printLicensesBlock(
|
||||
invalidLicenseChanges: Record<string, Changes>,
|
||||
warnOnly: boolean
|
||||
invalidLicenseChanges: Record<string, Changes>
|
||||
): void {
|
||||
core.group('Licenses', async () => {
|
||||
if (invalidLicenseChanges.forbidden.length > 0) {
|
||||
core.info('\nThe following dependencies have incompatible licenses:')
|
||||
printLicensesError(invalidLicenseChanges.forbidden)
|
||||
const msg = 'Dependency review detected incompatible licenses.'
|
||||
if (warnOnly) {
|
||||
core.warning(msg)
|
||||
} else {
|
||||
core.setFailed(msg)
|
||||
}
|
||||
core.setFailed('Dependency review detected incompatible licenses.')
|
||||
}
|
||||
if (invalidLicenseChanges.unresolved.length > 0) {
|
||||
core.warning(
|
||||
@@ -279,29 +169,6 @@ function printNullLicenses(changes: Changes): void {
|
||||
}
|
||||
}
|
||||
|
||||
function printScorecardBlock(
|
||||
scorecard: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
): void {
|
||||
core.group('Scorecard', async () => {
|
||||
if (scorecard) {
|
||||
for (const dependency of scorecard.dependencies) {
|
||||
if (
|
||||
dependency.scorecard?.score &&
|
||||
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
|
||||
) {
|
||||
core.info(
|
||||
`${styles.color.red.open}${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${dependency?.scorecard?.score}${styles.red.close}`
|
||||
)
|
||||
}
|
||||
core.info(
|
||||
`${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${dependency?.scorecard?.score}`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function renderSeverity(
|
||||
severity: 'critical' | 'high' | 'moderate' | 'low'
|
||||
): string {
|
||||
@@ -354,40 +221,4 @@ function printScannedDependencies(changes: Changes): void {
|
||||
})
|
||||
}
|
||||
|
||||
function printDeniedDependencies(
|
||||
changes: Change[],
|
||||
config: ConfigurationOptions
|
||||
): void {
|
||||
core.group('Denied', async () => {
|
||||
for (const denied of config.deny_packages) {
|
||||
core.info(`Config: ${denied}`)
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
core.info(`Change: ${change.name}@${change.version} is denied`)
|
||||
core.info(`Change: ${change.package_url} is denied`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function createScorecardWarnings(
|
||||
scorecards: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
): Promise<void> {
|
||||
// Iterate through the list of scorecards, and if the score is less than the threshold, send a warning
|
||||
for (const dependency of scorecards.dependencies) {
|
||||
if (
|
||||
dependency.scorecard?.score &&
|
||||
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
|
||||
) {
|
||||
core.warning(
|
||||
`${dependency.change.ecosystem}/${dependency.change.name} has an OpenSSF Scorecard of ${dependency.scorecard?.score}, which is less than this repository's threshold of ${config.warn_on_openssf_scorecard_level}.`,
|
||||
{
|
||||
title: 'OpenSSF Scorecard Warning'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
+11
-101
@@ -38,115 +38,25 @@ export const ConfigurationOptionsSchema = z
|
||||
.object({
|
||||
fail_on_severity: SeveritySchema,
|
||||
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
||||
allow_licenses: z.array(z.string()).optional(),
|
||||
deny_licenses: z.array(z.string()).optional(),
|
||||
allow_dependencies_licenses: z.array(z.string()).optional(),
|
||||
allow_licenses: z.array(z.string()).default([]),
|
||||
deny_licenses: z.array(z.string()).default([]),
|
||||
allow_ghsas: z.array(z.string()).default([]),
|
||||
deny_packages: z.array(z.string()).default([]),
|
||||
deny_groups: z.array(z.string()).default([]),
|
||||
license_check: z.boolean().default(true),
|
||||
vulnerability_check: z.boolean().default(true),
|
||||
config_file: z.string().optional(),
|
||||
base_ref: z.string().optional(),
|
||||
head_ref: z.string().optional(),
|
||||
retry_on_snapshot_warnings: z.boolean().default(false),
|
||||
retry_on_snapshot_warnings_timeout: z.number().default(120),
|
||||
show_openssf_scorecard: z.boolean().optional().default(true),
|
||||
warn_on_openssf_scorecard_level: z.number().default(3),
|
||||
comment_summary_in_pr: z
|
||||
.union([
|
||||
z.preprocess(
|
||||
val => (val === 'true' ? true : val === 'false' ? false : val),
|
||||
z.boolean()
|
||||
),
|
||||
z.enum(['always', 'never', 'on-failure'])
|
||||
])
|
||||
.default('never'),
|
||||
warn_only: z.boolean().default(false)
|
||||
})
|
||||
.transform(config => {
|
||||
if (config.comment_summary_in_pr === true) {
|
||||
config.comment_summary_in_pr = 'always'
|
||||
} else if (config.comment_summary_in_pr === false) {
|
||||
config.comment_summary_in_pr = 'never'
|
||||
}
|
||||
return config
|
||||
})
|
||||
.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"
|
||||
})
|
||||
}
|
||||
config_file: z.string().optional().default('false'),
|
||||
base_ref: z.string(),
|
||||
head_ref: z.string()
|
||||
})
|
||||
.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 ComparisonResponseSchema = z.object({
|
||||
changes: z.array(ChangeSchema),
|
||||
snapshot_warnings: z.string()
|
||||
})
|
||||
|
||||
export const ScorecardApiSchema = z.object({
|
||||
date: z.string(),
|
||||
repo: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
commit: z.string()
|
||||
})
|
||||
.nullish(),
|
||||
scorecard: z
|
||||
.object({
|
||||
version: z.string(),
|
||||
commit: z.string()
|
||||
})
|
||||
.nullish(),
|
||||
checks: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
documentation: z.object({
|
||||
shortDescription: z.string(),
|
||||
url: z.string()
|
||||
}),
|
||||
score: z.string(),
|
||||
reason: z.string(),
|
||||
details: z.array(z.string())
|
||||
})
|
||||
)
|
||||
.nullish(),
|
||||
score: z.number().nullish()
|
||||
})
|
||||
|
||||
export const ScorecardSchema = z.object({
|
||||
dependencies: z.array(
|
||||
z.object({
|
||||
change: ChangeSchema,
|
||||
scorecard: ScorecardApiSchema.nullish()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
export type Change = z.infer<typeof ChangeSchema>
|
||||
export type Changes = z.infer<typeof ChangesSchema>
|
||||
export type ComparisonResponse = z.infer<typeof ComparisonResponseSchema>
|
||||
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
|
||||
export type Severity = z.infer<typeof SeveritySchema>
|
||||
export type Scope = (typeof SCOPES)[number]
|
||||
export type Scorecard = z.infer<typeof ScorecardSchema>
|
||||
export type ScorecardApi = z.infer<typeof ScorecardApiSchema>
|
||||
export type Scope = typeof SCOPES[number]
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import {Change, Scorecard, ScorecardApi} from './schemas'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
export async function getScorecardLevels(
|
||||
changes: Change[]
|
||||
): Promise<Scorecard> {
|
||||
const data: Scorecard = {dependencies: []} as Scorecard
|
||||
for (const change of changes) {
|
||||
const ecosystem = change.ecosystem
|
||||
const packageName = change.name
|
||||
const version = change.version
|
||||
|
||||
//Get the project repository
|
||||
let repositoryUrl = change.source_repository_url
|
||||
//If the repository_url includes the protocol, remove it
|
||||
if (repositoryUrl?.startsWith('https://')) {
|
||||
repositoryUrl = repositoryUrl.replace('https://', '')
|
||||
}
|
||||
|
||||
// If GitHub API doesn't have the repository URL, query deps.dev for it.
|
||||
if (repositoryUrl) {
|
||||
// Call the deps.dev API to get the repository URL from there
|
||||
repositoryUrl = await getProjectUrl(ecosystem, packageName, version)
|
||||
}
|
||||
|
||||
// Get the scorecard API response from the scorecards API
|
||||
let scorecardApi: ScorecardApi | null = null
|
||||
if (repositoryUrl) {
|
||||
try {
|
||||
scorecardApi = await getScorecard(repositoryUrl)
|
||||
} catch (error: unknown) {
|
||||
core.debug(`Error querying for scorecard: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
data.dependencies.push({
|
||||
change,
|
||||
scorecard: scorecardApi
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
async function getScorecard(repositoryUrl: string): Promise<ScorecardApi> {
|
||||
const apiRoot = 'https://api.securityscorecards.dev/'
|
||||
let scorecardResponse: ScorecardApi = {} as ScorecardApi
|
||||
|
||||
const url = `${apiRoot}/projects/${repositoryUrl}`
|
||||
const response = await fetch(url)
|
||||
if (response.ok) {
|
||||
scorecardResponse = await response.json()
|
||||
} else {
|
||||
core.debug(`Couldn't get scorecard data for ${repositoryUrl}`)
|
||||
}
|
||||
return scorecardResponse
|
||||
}
|
||||
|
||||
export async function getProjectUrl(
|
||||
ecosystem: string,
|
||||
packageName: string,
|
||||
version: string
|
||||
): Promise<string> {
|
||||
core.debug(`Getting deps.dev data for ${packageName} ${version}`)
|
||||
const depsDevAPIRoot = 'https://api.deps.dev'
|
||||
const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`
|
||||
const response = await fetch(url)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.relatedProjects.length > 0) {
|
||||
return data.relatedProjects[0].projectKey.id
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
+62
-271
@@ -1,119 +1,50 @@
|
||||
import * as core from '@actions/core'
|
||||
import {ConfigurationOptions, Changes, Change, Scorecard} from './schemas'
|
||||
import {ConfigurationOptions, Changes} from './schemas'
|
||||
import {SummaryTableRow} from '@actions/core/lib/summary'
|
||||
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
|
||||
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
||||
|
||||
const icons = {
|
||||
check: '✅',
|
||||
cross: '❌',
|
||||
warning: '⚠️'
|
||||
}
|
||||
|
||||
export function addSummaryToSummary(
|
||||
vulnerableChanges: Changes,
|
||||
invalidLicenseChanges: InvalidLicenseChanges,
|
||||
deniedChanges: Changes,
|
||||
scorecard: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
addedPackages: Changes | null,
|
||||
invalidLicenseChanges: Record<string, Changes> | null
|
||||
): void {
|
||||
const scorecardWarnings = countScorecardWarnings(scorecard, config)
|
||||
const licenseIssues = countLicenseIssues(invalidLicenseChanges)
|
||||
|
||||
core.summary.addHeading('Dependency Review', 1)
|
||||
|
||||
if (
|
||||
vulnerableChanges.length === 0 &&
|
||||
licenseIssues === 0 &&
|
||||
deniedChanges.length === 0 &&
|
||||
scorecardWarnings === 0
|
||||
) {
|
||||
const issueTypes = [
|
||||
config.vulnerability_check ? 'vulnerabilities' : '',
|
||||
config.license_check ? 'license issues' : '',
|
||||
config.show_openssf_scorecard ? 'OpenSSF Scorecard issues' : ''
|
||||
]
|
||||
if (issueTypes.filter(Boolean).length === 0) {
|
||||
core.summary.addRaw(`${icons.check} No issues found.`)
|
||||
} else {
|
||||
core.summary.addRaw(
|
||||
`${icons.check} No ${issueTypes.filter(Boolean).join(' or ')} found.`
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
core.summary
|
||||
.addRaw('The following issues were found:')
|
||||
.addHeading('Dependency Review')
|
||||
.addRaw('We found:')
|
||||
.addList([
|
||||
...(config.vulnerability_check
|
||||
? [
|
||||
`${checkOrFailIcon(vulnerableChanges.length)} ${
|
||||
vulnerableChanges.length
|
||||
} vulnerable package(s)`
|
||||
]
|
||||
...(addedPackages
|
||||
? [`${addedPackages.length} vulnerable package(s)`]
|
||||
: []),
|
||||
...(config.license_check
|
||||
...(invalidLicenseChanges
|
||||
? [
|
||||
`${checkOrFailIcon(invalidLicenseChanges.forbidden.length)} ${
|
||||
invalidLicenseChanges.forbidden.length
|
||||
} package(s) with incompatible licenses`,
|
||||
`${checkOrFailIcon(invalidLicenseChanges.unresolved.length)} ${
|
||||
invalidLicenseChanges.unresolved.length
|
||||
} package(s) with invalid SPDX license definitions`,
|
||||
`${checkOrWarnIcon(invalidLicenseChanges.unlicensed.length)} ${
|
||||
invalidLicenseChanges.unlicensed.length
|
||||
} package(s) with unknown licenses.`
|
||||
]
|
||||
: []),
|
||||
...(deniedChanges.length > 0
|
||||
? [
|
||||
`${checkOrWarnIcon(deniedChanges.length)} ${
|
||||
deniedChanges.length
|
||||
} package(s) denied.`
|
||||
]
|
||||
: []),
|
||||
...(config.show_openssf_scorecard && scorecardWarnings > 0
|
||||
? [
|
||||
`${checkOrWarnIcon(scorecardWarnings)} ${scorecardWarnings ? scorecardWarnings : 'No'} packages with OpenSSF Scorecard issues.`
|
||||
`${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.`
|
||||
]
|
||||
: [])
|
||||
])
|
||||
.addRaw('See the Details below.')
|
||||
}
|
||||
|
||||
function countScorecardWarnings(
|
||||
scorecard: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
): number {
|
||||
return scorecard.dependencies.reduce(
|
||||
(total, dependency) =>
|
||||
total +
|
||||
(dependency.scorecard?.score &&
|
||||
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
|
||||
? 1
|
||||
: 0),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
export function addChangeVulnerabilitiesToSummary(
|
||||
vulnerableChanges: Changes,
|
||||
addedPackages: Changes,
|
||||
severity: string
|
||||
): void {
|
||||
if (vulnerableChanges.length === 0) {
|
||||
const rows: SummaryTableRow[] = []
|
||||
|
||||
const manifests = getManifestsSet(addedPackages)
|
||||
|
||||
core.summary
|
||||
.addHeading('Vulnerabilities')
|
||||
.addQuote(
|
||||
`Vulnerabilites were filtered by mininum severity <strong>${severity}</strong>.`
|
||||
)
|
||||
|
||||
if (addedPackages.length === 0) {
|
||||
core.summary.addQuote('No vulnerabilities found in added packages.')
|
||||
return
|
||||
}
|
||||
|
||||
const rows: SummaryTableRow[] = []
|
||||
|
||||
const manifests = getManifestsSet(vulnerableChanges)
|
||||
|
||||
core.summary.addHeading('Vulnerabilities', 2)
|
||||
|
||||
for (const manifest of manifests) {
|
||||
for (const change of vulnerableChanges.filter(
|
||||
for (const change of addedPackages.filter(
|
||||
pkg => pkg.manifest === manifest
|
||||
)) {
|
||||
let previous_package = ''
|
||||
@@ -141,7 +72,7 @@ export function addChangeVulnerabilitiesToSummary(
|
||||
previous_version = change.version
|
||||
}
|
||||
}
|
||||
core.summary.addHeading(`<em>${manifest}</em>`, 4).addTable([
|
||||
core.summary.addHeading(`<em>${manifest}</em>`, 3).addTable([
|
||||
[
|
||||
{data: 'Name', header: true},
|
||||
{data: 'Version', header: true},
|
||||
@@ -151,24 +82,13 @@ export function addChangeVulnerabilitiesToSummary(
|
||||
...rows
|
||||
])
|
||||
}
|
||||
|
||||
if (severity !== 'low') {
|
||||
core.summary.addQuote(
|
||||
`Only included vulnerabilities with severity <strong>${severity}</strong> or higher.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function addLicensesToSummary(
|
||||
invalidLicenseChanges: InvalidLicenseChanges,
|
||||
invalidLicenseChanges: Record<string, Changes>,
|
||||
config: ConfigurationOptions
|
||||
): void {
|
||||
if (countLicenseIssues(invalidLicenseChanges) === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
core.summary.addHeading('License Issues', 2)
|
||||
printLicenseViolations(invalidLicenseChanges)
|
||||
core.summary.addHeading('Licenses')
|
||||
|
||||
if (config.allow_licenses && config.allow_licenses.length > 0) {
|
||||
core.summary.addQuote(
|
||||
@@ -180,12 +100,10 @@ export function addLicensesToSummary(
|
||||
`<strong>Denied Licenses</strong>: ${config.deny_licenses.join(', ')}`
|
||||
)
|
||||
}
|
||||
if (config.allow_dependencies_licenses) {
|
||||
core.summary.addQuote(
|
||||
`<strong>Excluded from license check</strong>: ${config.allow_dependencies_licenses.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
|
||||
if (Object.values(invalidLicenseChanges).every(item => item.length === 0)) {
|
||||
core.summary.addQuote('No license violations detected.')
|
||||
return
|
||||
}
|
||||
|
||||
core.debug(
|
||||
@@ -195,43 +113,39 @@ export function addLicensesToSummary(
|
||||
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()
|
||||
|
||||
const licenseIssueTypes: InvalidLicenseChangeTypes[] = [
|
||||
'forbidden',
|
||||
'unresolved',
|
||||
'unlicensed'
|
||||
]
|
||||
if (changes.length > 0) {
|
||||
const rows: SummaryTableRow[] = []
|
||||
const manifests = getManifestsSet(changes)
|
||||
|
||||
const issueTypeNames: Record<InvalidLicenseChangeTypes, string> = {
|
||||
forbidden: 'Incompatible License',
|
||||
unresolved: 'Invalid SPDX License',
|
||||
unlicensed: 'Unknown License'
|
||||
}
|
||||
for (const manifest of manifests) {
|
||||
core.summary.addHeading(`<em>${manifest}</em>`, 4)
|
||||
|
||||
function printLicenseViolations(changes: InvalidLicenseChanges): void {
|
||||
const rowsGroupedByManifest: Record<string, SummaryTableRow[]> = {}
|
||||
|
||||
for (const issueType of licenseIssueTypes) {
|
||||
for (const change of changes[issueType]) {
|
||||
if (!rowsGroupedByManifest[change.manifest]) {
|
||||
rowsGroupedByManifest[change.manifest] = []
|
||||
for (const change of changes.filter(pkg => pkg.manifest === manifest)) {
|
||||
rows.push([
|
||||
renderUrl(change.source_repository_url, change.name),
|
||||
change.version,
|
||||
formatLicense(change.license)
|
||||
])
|
||||
}
|
||||
rowsGroupedByManifest[change.manifest].push([
|
||||
renderUrl(change.source_repository_url, change.name),
|
||||
change.version,
|
||||
formatLicense(change.license),
|
||||
issueTypeNames[issueType]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
for (const [manifest, rows] of Object.entries(rowsGroupedByManifest)) {
|
||||
core.summary.addHeading(`<em>${manifest}</em>`, 4)
|
||||
core.summary.addTable([
|
||||
['Package', 'Version', 'License', 'Issue Type'],
|
||||
...rows
|
||||
])
|
||||
core.summary.addTable([['Package', 'Version', 'License'], ...rows])
|
||||
}
|
||||
} else {
|
||||
core.summary.addQuote(`No ${heading.toLowerCase()} detected.`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +160,9 @@ export function addScannedDependencies(changes: Changes): void {
|
||||
const dependencies = groupDependenciesByManifest(changes)
|
||||
const manifests = dependencies.keys()
|
||||
|
||||
const summary = core.summary.addHeading('Scanned Manifest Files', 2)
|
||||
const summary = core.summary
|
||||
.addHeading('Scanned Dependencies')
|
||||
.addHeading(`We scanned ${dependencies.size} manifest files:`, 5)
|
||||
|
||||
for (const manifest of manifests) {
|
||||
const deps = dependencies.get(manifest)
|
||||
@@ -258,128 +174,3 @@ export function addScannedDependencies(changes: Changes): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function snapshotWarningRecommendation(
|
||||
config: ConfigurationOptions,
|
||||
warnings: string
|
||||
): string {
|
||||
const no_pr_snaps = warnings.includes(
|
||||
'No snapshots were found for the head SHA'
|
||||
)
|
||||
const retries_disabled = !config.retry_on_snapshot_warnings
|
||||
if (no_pr_snaps && retries_disabled) {
|
||||
return 'Ensure that dependencies are being submitted on PR branches and consider enabling <em>retry-on-snapshot-warnings</em>.'
|
||||
} else if (no_pr_snaps) {
|
||||
return 'Ensure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue.'
|
||||
} else if (retries_disabled) {
|
||||
return 'Consider enabling <em>retry-on-snapshot-warnings</em>.'
|
||||
}
|
||||
return 'Re-running this action after a short time may resolve the issue.'
|
||||
}
|
||||
|
||||
export function addScorecardToSummary(
|
||||
scorecard: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
): void {
|
||||
core.summary.addHeading('OpenSSF Scorecard', 2)
|
||||
if (scorecard.dependencies.length > 10) {
|
||||
core.summary.addRaw(`<details><summary>Scorecard details</summary>`, true)
|
||||
}
|
||||
core.summary.addRaw(
|
||||
`<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>`,
|
||||
true
|
||||
)
|
||||
for (const dependency of scorecard.dependencies) {
|
||||
core.debug('Adding scorecard to summary')
|
||||
core.debug(`Overall score ${dependency.scorecard?.score}`)
|
||||
|
||||
// Set the icon based on the overall score value
|
||||
let overallIcon = ''
|
||||
if (dependency.scorecard?.score) {
|
||||
overallIcon =
|
||||
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
|
||||
? ':warning:'
|
||||
: ':green_circle:'
|
||||
}
|
||||
|
||||
//Add a row for the dependency
|
||||
core.summary.addRaw(
|
||||
`<tr><td>${dependency.change.source_repository_url ? `<a href="https://${dependency.change.source_repository_url}">` : ''} ${dependency.change.ecosystem}/${dependency.change.name} ${dependency.change.source_repository_url ? `</a>` : ''}</td><td>${dependency.change.version}</td>
|
||||
<td>${overallIcon} ${dependency.scorecard?.score === undefined || dependency.scorecard?.score === null ? 'Unknown' : dependency.scorecard?.score}</td>`,
|
||||
false
|
||||
)
|
||||
|
||||
//Add details table in the last column
|
||||
if (dependency.scorecard?.checks !== undefined) {
|
||||
let detailsTable =
|
||||
'<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>'
|
||||
for (const check of dependency.scorecard?.checks || []) {
|
||||
const icon =
|
||||
parseFloat(check.score) < config.warn_on_openssf_scorecard_level
|
||||
? ':warning:'
|
||||
: ':green_circle:'
|
||||
|
||||
detailsTable += `<tr><td>${check.name}</td><td>${icon} ${check.score}</td><td>${check.reason}</td></tr>`
|
||||
}
|
||||
detailsTable += `</table>`
|
||||
core.summary.addRaw(
|
||||
`<td><details><summary>Details</summary>${detailsTable}</details></td></tr>`,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
core.summary.addRaw('<td>Unknown</td></tr>', true)
|
||||
}
|
||||
}
|
||||
core.summary.addRaw(`</table>`)
|
||||
if (scorecard.dependencies.length > 10) {
|
||||
core.summary.addRaw(`</details>`)
|
||||
}
|
||||
}
|
||||
|
||||
export function addSnapshotWarnings(
|
||||
config: ConfigurationOptions,
|
||||
warnings: string
|
||||
): void {
|
||||
core.summary.addHeading('Snapshot Warnings', 2)
|
||||
core.summary.addQuote(`${icons.warning}: ${warnings}`)
|
||||
const recommendation = snapshotWarningRecommendation(config, warnings)
|
||||
const docsLink =
|
||||
'See <a href="https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#best-practices-for-using-the-dependency-review-api-and-the-dependency-submission-api-together">the documentation</a> for more information and troubleshooting advice.'
|
||||
core.summary.addRaw(`${recommendation} ${docsLink}`)
|
||||
}
|
||||
|
||||
function countLicenseIssues(
|
||||
invalidLicenseChanges: InvalidLicenseChanges
|
||||
): number {
|
||||
return Object.values(invalidLicenseChanges).reduce(
|
||||
(acc, val) => acc + val.length,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
export function addDeniedToSummary(deniedChanges: Change[]): void {
|
||||
if (deniedChanges.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
core.summary.addHeading('Denied dependencies', 2)
|
||||
for (const change of deniedChanges) {
|
||||
core.summary.addHeading(`<em>Denied dependencies</em>`, 4)
|
||||
core.summary.addTable([
|
||||
['Package', 'Version', 'License'],
|
||||
[
|
||||
renderUrl(change.source_repository_url, change.name),
|
||||
change.version,
|
||||
change.license || ''
|
||||
]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
function checkOrFailIcon(count: number): string {
|
||||
return count === 0 ? icons.check : icons.cross
|
||||
}
|
||||
|
||||
function checkOrWarnIcon(count: number): string {
|
||||
return count === 0 ? icons.check : icons.warning
|
||||
}
|
||||
|
||||
+1
-31
@@ -1,5 +1,3 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Octokit} from 'octokit'
|
||||
import spdxParse from 'spdx-expression-parse'
|
||||
import {Changes} from './schemas'
|
||||
|
||||
@@ -8,9 +6,7 @@ export function groupDependenciesByManifest(
|
||||
): Map<string, Changes> {
|
||||
const dependencies: Map<string, Changes> = new Map()
|
||||
for (const change of changes) {
|
||||
// If the manifest is null or empty, give it a name now to avoid
|
||||
// breaking the HTML rendering later
|
||||
const manifestName = change.manifest || 'Unnamed Manifest'
|
||||
const manifestName = change.manifest
|
||||
|
||||
if (dependencies.get(manifestName) === undefined) {
|
||||
dependencies.set(manifestName, [])
|
||||
@@ -42,29 +38,3 @@ export function isSPDXValid(license: string): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isEnterprise(): boolean {
|
||||
const serverUrl = new URL(
|
||||
process.env['GITHUB_SERVER_URL'] ?? 'https://github.com'
|
||||
)
|
||||
return serverUrl.hostname.toLowerCase() !== 'github.com'
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
//baseUrl is required for GitHub Enterprise Server
|
||||
//https://github.com/octokit/octokit.js/blob/9c8fa89d5b0bc4ddbd6dec638db00a2f6c94c298/README.md?plain=1#L196
|
||||
if (isEnterprise()) {
|
||||
opts['baseUrl'] = new URL('api/v3', process.env['GITHUB_SERVER_URL'])
|
||||
}
|
||||
|
||||
return new Octokit(opts)
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib" /* Redirect output structure to the directory. */,
|
||||
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -3,9 +3,10 @@
|
||||
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
"outDir": "./lib" /* Redirect output structure to the directory. */,
|
||||
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user