Compare commits

..

1 Commits

Author SHA1 Message Date
Federico Builes 77a34f96cc Fixing typo in config.test.ts. 2022-10-31 07:46:08 +01:00
59 changed files with 24823 additions and 48459 deletions
-37
View File
@@ -1,37 +0,0 @@
---
name: Bug report
about: Create a bug report to help us improve
title: "[BUG] "
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Action version**
What version of the action are you using in your workflow?
_Note: if you're not running the [latest release](https://github.com/actions/dependency-review-action/releases/latest) please try that first!_
**Examples**
If possible, please link to a public example of the issue that you're encountering, or a copy of the workflow that you're using to run the action.
If you have encountered a problem with a specific package (e.g. issue with license or attributions data) please share details about the package, as well as a link to the manifest where it's being referenced.
**Additional context**
Add any other context about the problem here.
-5
View File
@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: GitHub Security Bug Bounty
url: https://bounty.github.com/
about: If you believe that you've found a security issue, please report security vulnerabilities here.
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. e.g. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -1,7 +0,0 @@
## Purpose
_Describe the purpose of this pull request_
## Related Issues
_What issues does this PR close or relate to?_
+2 -13
View File
@@ -3,23 +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']
groups:
minor-updates:
update-types:
- 'minor'
- 'patch'
exclude-patterns:
- '*spdx*'
# Pull out any updates to spdx definitions and parsing as a priority PR
spdx-licenses:
patterns:
- '*spdx*'
+5 -9
View File
@@ -16,21 +16,17 @@ on:
- '**.md'
workflow_dispatch:
permissions:
contents: read
jobs:
check-dist:
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
@@ -50,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
+6 -9
View File
@@ -10,17 +10,14 @@ on:
paths-ignore:
- '**.md'
permissions:
contents: read
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
@@ -30,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
-48
View File
@@ -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', 'actions', 'ruby' ]
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}}"
+2 -3
View File
@@ -1,5 +1,4 @@
name: 'Dependency Review'
on: [pull_request]
permissions:
@@ -10,6 +9,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Dependency Review
uses: ./
uses: actions/dependency-review-action@main
-31
View File
@@ -1,31 +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.1.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 for 180 days. You can: comment on the PR or remove the stale label to hold stalebot 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 stalebot. Please see CONTRIBUTING.md for more policy details."
stale-pr-label: "Stale"
close-pr-message: "👋 This pull request has been closed by stalebot because it has been open with no activity for over 180 days. Please see CONTRIBUTING.md for more policy details."
stale-issue-label: "Stale"
stale-issue-message: "👋 This issue has been marked as stale because it has been open with no activity for 180 days. You can: comment on the issue or remove the stale label to hold stalebot off for a while, add the `Keep` label to hold stale off permanently, or do nothing. If you do nothing, this issue will be closed eventually by the stalebot. Please see CONTRIBUTING.md for more policy details."
close-issue-message: "👋 This issue has been closed by stalebot because it has been open with no activity for over 180 days. Please see CONTRIBUTING.md for more policy details."
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
-2
View File
@@ -100,5 +100,3 @@ Thumbs.db
# Ignore built ts files
__tests__/runner/*
lib/**/*
tmp
+40 -64
View File
@@ -4,52 +4,41 @@
[pr]: https://github.com/actions/dependency-review-action/compare
[code-of-conduct]: CODE_OF_CONDUCT.md
Hi there! We're thrilled that you'd like to contribute to this project.
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
Contributions to this project are
[released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license)
to the public under the [project's open source license](LICENSE).
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
Please note that this project is released with a [Contributor Code of
Conduct][code-of-conduct]. By participating in this project you agree
to abide by its terms.
## Bug reports and other issues
### How it works
If you've encountered a problem, please let us know by [submitting an issue](https://github.com/actions/dependency-review-action/issues/new)!
This Action makes an authenticated query to the Dependency Graph Diff
API endpoint (`GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}`)
to find out the set of added and removed dependencies for each manifest.
## Enhancements and feature requests
### Bootstrapping the project
If you've got an idea for a new feature or a significant change to the code or its dependencies, please submit as [an issue](https://github.com/actions/dependency-review-action/issues/new) so that the community can see it, and we can discuss it there. We may not be able to respond to every single issue, but will make a best effort!
```
git clone https://github.com/actions/dependency-review-action.git
cd dependency-review-action
npm install
```
If you'd like to make a contribution yourself, we ask that before significant effort is put into code changes, that we have agreement that the change aligns with our strategy for the action. Since this is a verified Action owned by GitHub we want to make sure that contributions are high quality, and that they maintain consistency with the rest of the action's behavior.
### Running the tests
1. Create an [issue discussing the idea](https://github.com/actions/dependency-review-action/issues/new), so that we can discuss it there.
2. If we agree to incorporate the idea into the action, please write-up a high level summary of the approach that you plan to take so we can review
```
npm run test
```
## Stalebot
_Note_: We don't have any useful tests yet, contributions are welcome!
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.
## Development lifecycle
Ready to contribute to `dependency-review-action`? Here is some information to help you get started.
### High level overview of the action
This action makes an authenticated query to the [Dependency Review API](https://docs.github.com/en/rest/dependency-graph/dependency-review) endpoint (`GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}`) to find out the set of added and removed dependencies for each manifest.
The action then evaluates the differences between the pushes based on the rules defined in the action configuration, and summarizes the differences and any violations of the rules you have defined as a comment in the pull request that triggered it and the action outputs.
### Local Development
Before you begin, you need to have [Node.js](https://nodejs.org/en/) installed, minimum version 20.
#### Bootstrapping the project
0. [Fork][fork] and clone the repository
1. Change to the working directory: `cd dependency-review-action`
2. Install the dependencies: `npm install`
3. Make sure the tests pass on your machine: `npm run test`
#### Manually testing for vulnerabilities
## Local Development
It is recommended to have atleast [Node 18](https://nodejs.org/en/) installed.
We have a script to scan a given PR for vulnerabilities, this will
help you test your local changes. Make sure to [grab a Personal Access Token (PAT)](https://github.com/settings/tokens) before proceeding (you'll need `repo` permissions for private repos):
@@ -64,7 +53,7 @@ $ GITHUB_TOKEN=<token> ./scripts/scan_pr <pr_url>
Like this:
```sh
$ GITHUB_TOKEN=<token> ./scripts/scan_pr https://github.com/actions/dependency-review-action/pull/3
$ GITHUB_TOKEN=my-secret-token ./scripts/scan_pr https://github.com/actions/dependency-review-action/pull/3
```
[Configuration options](README.md#configuration-options) can be set by
@@ -75,37 +64,26 @@ passing an external YAML [configuration file](README.md#configuration-file) to t
$ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_url>
```
#### Running unit tests
## Submitting a pull request
```
npm run test
```
_Note_: We don't have a very comprehensive test suite, so any contributions to the existing tests are welcome!
### Submitting a pull request
1. Create a new branch: `git checkout -b my-branch-name`
2. Make your change, add tests, and make sure the tests still pass
3. Make sure to build and package before pushing: `npm run build && npm run package`
4. Push to your fork and [submit a pull request][pr]
0. [Fork][fork] and clone the repository
1. Configure and install the dependencies: `npm install`
2. Make sure the tests pass on your machine: `npm run test`
3. Create a new branch: `git checkout -b my-branch-name`
4. Make your change, add tests, and make sure the tests still pass
5. Make sure to build and package before pushing: `npm run build && npm run package`
6. Push to your fork and [submit a pull request][pr]
7. Pat your self on the back and wait for your pull request to be reviewed and merged.
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
- 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](docs/examples.md)
- Link to a sample PR in a custom repository running your version of the Action.
- Please be responsive to any questions and feedback that you get from a maintainer of the repo!
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
## Cutting a new release
<details>
_Note: these instructions are for maintainers_
1. Update the version number in [package.json](https://github.com/actions/dependency-review-action/blob/main/package.json) and run `npm i` to update the lockfile.
1. Update the version number in [package.json](https://github.com/actions/dependency-review-action/blob/main/package.json).
1. Go to [Draft a new
release](https://github.com/actions/dependency-review-action/releases/new)
in the Releases page.
@@ -134,14 +112,12 @@ 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
```
</details>
## Resources
- [Creating JavaScript GitHub actions](https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action)
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)
+256 -210
View File
@@ -1,276 +1,322 @@
# dependency-review-action
- [dependency-review-action](#dependency-review-action)
- [Overview](#overview)
- [Viewing the results](#viewing-the-results)
- [Installation](#installation)
- [Installation (standard)](#installation-standard)
- [Installation (GitHub Enterprise Server)](#installation-github-enterprise-server)
- [Configuration](#configuration)
- [Configuration options](#configuration-options)
- [Configuration methods](#configuration-methods)
- [Option 1: Using inline configuration](#option-1-using-inline-configuration)
- [Option 2: Using an external configuration file](#option-2-using-an-external-configuration-file)
- [`OTHER` in license strings](#other-in-license-strings)
- [Further information](#further-information)
- [Using dependency review action to block a pull request from being merged](#using-dependency-review-action-to-block-a-pull-request-from-being-merged)
- [Outputs](#outputs)
- [Getting help](#getting-help)
- [Contributing](#contributing)
- [License](#license)
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/en/rest/reference/dependency-graph#dependency-review) that diffs the dependencies between any two revisions.
## Overview
The action is available for all public repositories, as well as private repositories that have GitHub Advanced Security licensed.
The dependency review 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/en/rest/dependency-graph/dependency-review?apiVersion=2022-11-28) that diffs the dependencies between any two revisions on your default branch.
You can see the results on the job logs
The action is available for:
<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">
- Public repositories
- Private repositories with a [GitHub Advanced Security](https://docs.github.com/en/enterprise-cloud@latest/get-started/learning-about-github/about-github-advanced-security) license.
or on the job summary
### Viewing the results
When the action runs, you can see the results on:
- The **job logs** page.
1. Go to the **Actions** tab for the repository and select the relevant workflow run.
1. Then under "Jobs", click **dependency review**.
<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">
- The **job summary** page.
1. Go to the **Actions** tab for the repository and select the relevant workflow run.
1. Click **Summary**, then scroll to "dependency-review 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
- [Installation (standard)](#installation)
- [Installation (GitHub Enterprise Server)](#installation-github-enterprise-server)
#### Installation (standard)
You can install the action on any public repository, or any organization-owned private repository, provided the organization has a GitHub Advanced Security license.
**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:
```yaml
name: 'Dependency Review'
on: [pull_request]
```yaml
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
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
```
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2
```
#### Installation (GitHub Enterprise Server)
### GitHub Enterprise Server
You can install the action on repositories on GitHub Enterprise Server.
This action is available in Enterprise Server starting with version 3.6. Make sure
[GitHub Advanced
Security](https://docs.github.com/en/enterprise-server@3.6/admin/code-security/managing-github-advanced-security-for-your-enterprise/enabling-github-advanced-security-for-your-enterprise)
and [GitHub
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.
1. Ensure [GitHub Advanced Security](https://docs.github.com/en/enterprise-server@latest/admin/code-security/managing-github-advanced-security-for-your-enterprise/enabling-github-advanced-security-for-your-enterprise) and [GitHub Connect](https://docs.github.com/en/enterprise-server@latest/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect) are enabled for the enterprise.
2. Ensure you have installed the [dependency-review-action](https://github.com/actions/dependency-review-action) on the server.
3. Add a new YAML workflow to your `.github/workflows` folder:
You can use the same workflow as above, replacing the `runs-on` value
with the label of any of your runners (the default label
is `self-hosted`):
```yaml
name: 'Dependency Review'
on: [pull_request]
```yaml
# ...
permissions:
contents: read
jobs:
dependency-review:
runs-on: self-hosted
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
```
4. In the workflow file, replace the `runs-on` value with the label of any of your runners. (The default value is `self-hosted`.)
jobs:
dependency-review:
runs-on: self-hosted
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2
```
## Configuration
- [Configuration options](#configuration-options)
- [Configuration methods](#configuration-methods)
Configure this action by either using an external configuration file,
or by inlining these options in your workflow file.
### Configuration options
## Configuration Options
There are various configuration options you can use to specify settings for the dependency review action.
### config-file
All configuration options are optional.
A string representing the path to an external configuration file. By
default external configuration files are not used.
| 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`\* | ⚠️ This option is deprecated for possible removal in the next major release. See [Deprecate the deny-licenses option #938](https://github.com/actions/dependency-review-action/issues/938) for more information. <br> 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. With each execution, a new comment will overwrite the existing one. | `always`, `on-failure`, `never` | `never` |
| `deny-packages` | Any number of packages to block in a PR. This option will match on the exact version provided. If no version is provided, the option will treat the specified package as a wildcard and deny all versions. | 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` | 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 |
**Possible values**: A string representing the absolute path to the
configuration file.
> [!NOTE]
>
> - \* Not supported for use with GitHub Enterprise Server. (Checking for licenses is not supported on GitHub Enterprise Server because the API does not return license information.)
> - \+ When `warn-only` is set to `true`, all vulnerabilities, independently of the severity, will be reported as warnings and the action will not fail.
> - The `allow-licenses` and `deny-licenses` options are mutually exclusive; an error will be raised if you provide both.
> - If we can't detect the license for a dependency **we will inform you, but the action won't fail**.
**Example**: `config-file: ./.github/dependency-review-config.yml`.
### Configuration methods
### fail-on-severity
To specify settings for the dependency review action, you can choose from two options:
Configure the severity level for alerting. See "[Vulnerability Severity](https://github.com/actions/dependency-review-action#vulnerability-severity)".
- [Option 1: Inline the configuration options]() in your workflow file.
- [Option 2: Reference an external configuration file]() in your workflow file.
**Possible values**: `critical`, `high`, `moderate`, `low`.
#### Option 1: Using inline configuration
**Example**: `fail-on-severity: moderate`.
You can pass configuration options to the dependency review action using your workflow file.
### fail-on-scopes
1. In the same YAML workflow file you created during installation, use the `with:` key to specify your chosen settings:
A list of strings representing the build environments you want to
support. The default value is `development, runtime`.
```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: moderate
**Possible values**: `development`, `runtime`, `unknown`
# Use comma-separated names to pass list arguments:
deny-licenses: LGPL-2.0, BSD-2-Clause
```
**Inline example**: `fail-on-scopes: development, runtime`
#### Option 2: Using an external configuration file
**YAML example**:
You can use an external configuration file to specify settings for this action. The file can be a local file or a file in an external repository.
```yaml
# this prevents scanning development dependencies
fail-on-scopes:
- runtime
```
1. In the same YAML workflow file you created during installation, use `config-file` to specify that you are using an external configuration file.
### allow-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:
config-file: './.github/dependency-review-config.yml'
```
Only allow the licenses that comply with the expressions in this list. See "[Licenses](https://github.com/actions/dependency-review-action#licenses)".
| 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` |
**Possible values**: A list of of [SPDX-compliant license identifiers](https://spdx.org/licenses/).
2. Optionally, if the file resides in a private external repository, and for all GitHub Enterprise Server repositories, use `external-repo-token` to specify a token for fetching the file.
**Inline example**: `allow-licenses: BSD-3-Clause, LGPL-2.1 OR MIT OR BSD-3-Clause`
```yaml
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
config-file: 'github/octorepo/dependency-review-config.yml@main'
external-repo-token: 'ghp_123456789abcde'
```
**YAML example**:
| Option | Usage | Possible values |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `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. |
```yaml
allow-licenses:
- BSD-3-Clause
- LGPL-2.1
- MIT
- BSD-3-Clause
```
3. Create the configuration file in the path you specified for `config-file`.
4. In the configuration file, specify your chosen settings.
```yaml
fail-on-severity: 'critical'
allow-licenses:
- 'GPL-3.0'
- 'BSD-3-Clause'
- 'MIT'
```
### deny-licenses
#### `OTHER` in license strings
Add a custom list of licenses you want to block. See
"[Licenses](https://github.com/actions/dependency-review-action#licenses)".
License data comes from [ClearlyDefined](https://clearlydefined.io) and you may sometimes see licenses displayed with the string `OTHER` in them. ClearlyDefined [defines OTHER](https://docs.clearlydefined.io/docs/curation/curation-guidelines) as:
**Possible values**: Any valid set of [SPDX licenses](https://spdx.org/licenses/).
> This indicates that a human confirmed that there is license information in the file but that the license is not an SPDX-identified license.
**Inline example**: `deny-licenses: LGPL-2.0, GPL-2.0+ WITH Bison-exception-2.2`
`OTHER` is not a valid [SPDX license identifier](https://spdx.org/licenses/), so we convert `OTHER` in a license string into `LicenseRef-clearlydefined-OTHER`, which _is_ valid in SPDX. If you want to add that to the deny or allow list, be sure to add `LicenseRef-clearlydefined-OTHER` to this list, because that is what we'll actually be comparing.
**YAML example**:
#### Further information
```yaml
deny-licenses:
- LGPL-2.0
- GPL-2.0+ WITH Bison-exception-2.2
```
- For more examples of how to use this action and its configuration options, see the [examples](docs/examples.md) page.
- For general information about dependency review on GitHub, see "[About dependency review](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review)" in the GitHub Docs documentation.
### allow-ghsas
## Using dependency review action to block a pull request from being merged
Add a custom list of GitHub Advisory IDs that can be skipped during detection.
You can configure your repository to block a pull request from being merged if the pull request fails the dependency review action check. To do this, the repository owner must configure branch protection settings that require the check to pass before merging. For more information, see "[Require status checks before merging](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)" in GitHub Docs documentation.
**Possible values**: Any valid advisory GHSA ids.
## Outputs
**Inline example**: `allow-ghsas: GHSA-abcd-1234-5679, GHSA-efgh-1234-5679`
Dependency review action can create [outputs](https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs), so that data from its execution can be used by other jobs in a workflow.
**YAML example**:
- `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.
```yaml
allow-ghsas:
- GHSA-abcd-1234-5679
- GHSA-efgh-1234-5679
```
> [!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).
>
> If you use these outputs in a run-step, you must store the output data in an environment 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.
### 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
Action using your workflow file. Here's an example of what the full
file would look like:
```yaml
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: Dependency Review
uses: actions/dependency-review-action@v2
with:
fail-on-severity: moderate
# Use comma-separated names to pass list arguments:
deny-licenses: LGPL-2.0, BSD-2-Clause
```
### Vulnerability Severity
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`.
This example will only fail on pull requests with `critical` and `high` vulnerabilities:
```yaml
- name: Dependency Review
uses: actions/dependency-review-action@v2
with:
fail-on-severity: high
```
### Dependency Scoping
By default the action will only fail on `runtime` dependencies that have vulnerabilities or unacceptable licenses, ignoring `development` dependencies. You can override this behavior with the `fail-on-scopes` option, which will allow you to list the specific dependency scopes you care about. The possible values are: `unknown`, `runtime`, and `development`. Note: Filtering by scope will not be supported on Enterprise Server just yet, as the REST API's introduction of `scope` will be released in an upcoming Enterprise Server version. We will treat all dependencies on Enterprise Server as having a `runtime` scope and thus will not be filtered away.
```yaml
- name: Dependency Review
uses: actions/dependency-review-action@v2
with:
fail-on-scopes: runtime, development
```
### Licenses
You can set the action to fail on pull requests based on the licenses of the dependencies
they introduce. With `allow-licenses` you can define the list of licenses
your repository will accept. Alternatively, you can use `deny-licenses` to only
forbid a subset of licenses. These options are not supported on 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.
- 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/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
+1 -1
View File
@@ -1,3 +1,3 @@
If you discover a security issue in this repo, please submit it through the [GitHub Security Bug Bounty](https://bounty.github.com/)
If you discover a security issue in this repo, please submit it through the [GitHub Security Bug Bounty](https://hackerone.com/github)
Thanks for helping make GitHub Actions safe for everyone.
+137 -207
View File
@@ -1,112 +1,81 @@
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 spdx from '../src/spdx'
import {setInput, clearInputs} from './test-helpers'
import * as Utils from '../src/utils'
// 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)
})
beforeEach(() => {
clearInputs()
})
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', 'ISC, GPL-2.0')
setInput('allow-licenses', ' BSD, GPL 2')
const config = await readConfig()
expect(config.fail_on_severity).toEqual('critical')
expect(config.allow_licenses).toEqual(['ISC', 'GPL-2.0'])
})
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-3-Clause')
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'
)
})
test('it successfully parses allow-dependencies-licenses', async () => {
setInput(
'allow-dependencies-licenses',
'pkg:npm/@test/package@1.2.3,pkg:npm/example'
)
const config = await readConfig()
expect(config.allow_dependencies_licenses).toEqual([
'pkg:npm/@test/package@1.2.3',
'pkg:npm/example'
])
})
test('it raises an error when an invalid package-url is used for allow-dependencies-licenses', async () => {
setInput('allow-dependencies-licenses', 'not-a-purl')
await expect(readConfig()).rejects.toThrow(`Error parsing package-url`)
})
test('it raises an error when a nameless package-url is used for allow-dependencies-licenses', async () => {
setInput('allow-dependencies-licenses', 'pkg:npm/@namespace/')
await expect(readConfig()).rejects.toThrow(
`Error parsing package-url: name is required`
)
})
test('it raises an error when an invalid package-url is used for deny-packages', async () => {
setInput('deny-packages', 'not-a-purl')
await expect(readConfig()).rejects.toThrow(`Error parsing package-url`)
})
test('it raises an error when a nameless package-url is used for deny-packages', async () => {
setInput('deny-packages', 'pkg:npm/@namespace/')
await expect(readConfig()).rejects.toThrow(
`Error parsing package-url: name is required`
)
})
test('it raises an error when an argument to deny-groups is missing a namespace', async () => {
setInput('deny-groups', 'pkg:npm/my-fun-org')
await expect(readConfig()).rejects.toThrow(
`package-url must have a namespace`
)
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'
})
@@ -115,200 +84,161 @@ 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()
})
const pullRequestLikeEvents = ['pull_request', 'pull_request_target']
test.each(pullRequestLikeEvents)(
'it uses the given refs even when the event is %s',
async eventName => {
setInput('base-ref', 'a-custom-base-ref')
setInput('head-ref', 'a-custom-head-ref')
const refs = getRefs(await readConfig(), {
payload: {
pull_request: {
number: 42,
base: {sha: 'pr-base-ref'},
head: {sha: 'pr-head-ref'}
}
},
eventName
})
expect(refs.base).toEqual('a-custom-base-ref')
expect(refs.head).toEqual('a-custom-head-ref')
}
)
test.each(pullRequestLikeEvents)(
'it uses the event refs when the event is %s and no refs are provided in config',
async eventName => {
const refs = getRefs(await readConfig(), {
payload: {
pull_request: {
number: 42,
base: {sha: 'pr-base-ref'},
head: {sha: 'pr-head-ref'}
}
},
eventName
})
expect(refs.base).toEqual('pr-base-ref')
expect(refs.head).toEqual('pr-head-ref')
}
)
test('it uses the given refs even when the event is merge_group', async () => {
setInput('base-ref', 'a-custom-base-ref')
setInput('head-ref', 'a-custom-head-ref')
const refs = getRefs(await readConfig(), {
payload: {
merge_group: {
base_sha: 'pr-base-ref',
head_sha: 'pr-head-ref'
}
},
eventName: 'merge_group'
})
expect(refs.base).toEqual('a-custom-base-ref')
expect(refs.head).toEqual('a-custom-head-ref')
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('it uses the event refs when the event is merge_group and no refs are provided in config', async () => {
const refs = getRefs(await readConfig(), {
payload: {
merge_group: {
base_sha: 'pr-base-ref',
head_sha: 'pr-head-ref'
}
},
eventName: 'merge_group'
})
expect(refs.base).toEqual('pr-base-ref')
expect(refs.head).toEqual('pr-head-ref')
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', () => {
beforeAll(() => {
jest.spyOn(Utils, 'isSPDXValid').mockReturnValue(false)
})
test('it raises an error for invalid licenses in allow-licenses', async () => {
setInput('allow-licenses', ' BSD-YOLO, GPL-2.0')
await expect(readConfig()).rejects.toThrow(
'Invalid license(s) in allow-licenses: BSD-YOLO'
)
setInput('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', ' GPL-2.0, BSD-YOLO, Apache-2.0, ToIll')
await expect(readConfig()).rejects.toThrow(
'Invalid license(s) in deny-licenses: BSD-YOLO, ToIll'
)
setInput('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')
})
-195
View File
@@ -1,195 +0,0 @@
import {expect, jest, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
import {createTestChange, createTestPURLs} from './fixtures/create-test-change'
import {getDeniedChanges} from '../src/deny'
jest.mock('@actions/core')
const mockOctokit = {
rest: {
licenses: {
getForRepo: jest
.fn()
.mockReturnValue({data: {license: {spdx_id: 'AGPL'}}})
}
}
}
let npmChange: Change
let rubyChange: Change
let pipChange: Change
let mvnChange: Change
jest.mock('octokit', () => {
return {
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
Octokit: class {
constructor() {
return mockOctokit
}
}
}
})
beforeEach(async () => {
jest.resetModules()
npmChange = createTestChange({ecosystem: 'npm'})
rubyChange = createTestChange({ecosystem: 'rubygems'})
pipChange = createTestChange({ecosystem: 'pip'})
mvnChange = createTestChange({ecosystem: 'maven'})
})
test('denies packages from the deny packages list', async () => {
const changes: Changes = [npmChange, rubyChange]
const deniedPackages = createTestPURLs(['pkg:gem/actionsomething@3.2.0'])
const deniedChanges = await getDeniedChanges(changes, deniedPackages)
expect(deniedChanges[0]).toBe(rubyChange)
expect(deniedChanges.length).toEqual(1)
})
test('denies packages only for the specified version from deny packages list', async () => {
const deniedPackageWithDifferentVersion = createTestPURLs([
'pkg:npm/lodash@1.2.3'
])
const changes: Changes = [npmChange]
const deniedChanges = await getDeniedChanges(
changes,
deniedPackageWithDifferentVersion
)
expect(deniedChanges.length).toEqual(0)
})
test('if no specified version from deny packages list, it will treat package as wildcard and deny all versions', async () => {
const changes: Changes = [
createTestChange({name: 'lodash', version: '1.2.3'}),
createTestChange({name: 'lodash', version: '4.5.6'}),
createTestChange({name: 'lodash', version: '7.8.9'})
]
const denyAllLodashVersions = createTestPURLs(['pkg:npm/lodash'])
const deniedChanges = await getDeniedChanges(changes, denyAllLodashVersions)
expect(deniedChanges.length).toEqual(3)
})
test('denies packages from the deny group list', async () => {
const changes: Changes = [mvnChange, rubyChange]
const deniedGroups = createTestPURLs(['pkg:maven/org.apache.logging.log4j/'])
const deniedChanges = await getDeniedChanges(changes, [], deniedGroups)
expect(deniedChanges[0]).toBe(mvnChange)
expect(deniedChanges.length).toEqual(1)
})
test('denies packages that match the deny group list exactly', async () => {
const changes: Changes = [
createTestChange({
package_url: 'pkg:npm/org.test.pass/pass-this@1.0.0',
ecosystem: 'npm'
}),
createTestChange({
package_url: 'pkg:npm/org.test/deny-this@1.0.0',
ecosystem: 'npm'
})
]
const deniedGroups = createTestPURLs(['pkg:npm/org.test/'])
const deniedChanges = await getDeniedChanges(changes, [], deniedGroups)
expect(deniedChanges.length).toEqual(1)
expect(deniedChanges[0]).toBe(changes[1])
})
test(`denies packages using the namespace from the name when there's no package_url`, async () => {
const changes: Changes = [
createTestChange({
package_url: 'pkg:npm/org.test.pass/pass-this@1.0.0',
ecosystem: 'npm'
}),
createTestChange({
name: 'org.test:deny-this',
package_url: '',
ecosystem: 'maven'
})
]
const deniedGroups = createTestPURLs(['pkg:maven/org.test/'])
const deniedChanges = await getDeniedChanges(changes, [], deniedGroups)
expect(deniedChanges.length).toEqual(1)
expect(deniedChanges[0]).toBe(changes[1])
})
test('allows packages not defined in the deny packages and groups list', async () => {
const changes: Changes = [npmChange, pipChange]
const deniedPackages = createTestPURLs([
'pkg:gem/package-not-in-changes@1.0.0'
])
const deniedGroups = createTestPURLs(['pkg:maven/group.not.in.changes/'])
const deniedChanges = await getDeniedChanges(
changes,
deniedPackages,
deniedGroups
)
expect(deniedChanges.length).toEqual(0)
})
test('deny packages does not prevent removal of denied packages', async () => {
const changes: Changes = [
createTestChange({
change_type: 'added',
name: 'deny-by-name-and-version',
version: '1.0.0',
ecosystem: 'npm'
}),
createTestChange({
change_type: 'removed',
name: 'pass-by-name-and-version',
version: '1.0.0',
ecosystem: 'npm'
}),
createTestChange({
change_type: 'added',
name: 'deny-by-name',
version: '1.0.0',
ecosystem: 'npm'
}),
createTestChange({
change_type: 'removed',
name: 'pass-by-name',
version: '1.0.0',
ecosystem: 'npm'
}),
createTestChange({
change_type: 'added',
package_url: 'pkg:npm/org.test.deny.by.namespace/only@1.0.0',
ecosystem: 'npm'
}),
createTestChange({
change_type: 'removed',
package_url: 'pkg:npm/org.test.pass.by.namespace/only@1.0.0',
ecosystem: 'npm'
})
]
const deniedPackages = createTestPURLs([
'pkg:npm/org.test.deny.by/deny-by-name-and-version@1.0.0',
'pkg:npm/org.test.pass.by/pass-by-name-and-version@1.0.0',
'pkg:npm/org.test.deny.by/deny-by-name',
'pkg:npm/org.test.pass.by/pass-by-name'
])
const deniedGroups = createTestPURLs([
'pkg:npm/org.test.deny.by.namespace/',
'pkg:npm/org.test.pass.by.namespace/'
])
const deniedChanges = await getDeniedChanges(
changes,
deniedPackages,
deniedGroups
)
expect(deniedChanges.length).toEqual(3)
expect(deniedChanges[0]).toBe(changes[0])
expect(deniedChanges[1]).toBe(changes[2])
expect(deniedChanges[2]).toBe(changes[4])
})
-29
View File
@@ -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)
}
})
-107
View File
@@ -1,107 +0,0 @@
import {expect, test, beforeEach} from '@jest/globals'
import {readConfig} from '../src/config'
import * as spdx from '../src/spdx'
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
}
}
}
})
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-3-Clause', 'GPL-2.0'])
})
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
View File
@@ -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])
})
+2 -2
View File
@@ -1,4 +1,4 @@
fail_on_severity: critical
allow_licenses:
- 'BSD-3-Clause'
- 'GPL-2.0'
- "BSD"
- "GPL 2"
@@ -1,2 +0,0 @@
fail_on_severity: critical
allow_licenses: []
-126
View File
@@ -1,126 +0,0 @@
import {Change} from '../../src/schemas'
import {createTestVulnerability} from './create-test-vulnerability'
import {PackageURL, parsePURL} from '../../src/purl'
const defaultNpmChange: 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 defaultRubyChange: 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 defaultPipChange: 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 defaultMavenChange: 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'
}
]
}
const ecosystemToDefaultChange: {[key: string]: Change} = {
npm: defaultNpmChange,
rubygems: defaultRubyChange,
pip: defaultPipChange,
maven: defaultMavenChange
}
const createTestChange = (overwrites: Partial<Change> = {}): Change => {
const ecosystem = overwrites.ecosystem || 'npm'
return {
...ecosystemToDefaultChange[ecosystem],
...overwrites
}
}
const createTestPURLs = (list: string[]): PackageURL[] => {
return list.map(purl => {
return parsePURL(purl)
})
}
export {createTestChange, createTestPURLs}
@@ -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
+35 -199
View File
@@ -1,8 +1,9 @@
import {expect, jest, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
import {getInvalidLicenseChanges} from '../src/licenses'
const npmChange: Change = {
let getInvalidLicenseChanges: Function
let npmChange: Change = {
manifest: 'package.json',
change_type: 'added',
ecosystem: 'npm',
@@ -22,14 +23,14 @@ const npmChange: Change = {
]
}
const rubyChange: Change = {
let 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-3-Clause',
license: 'BSD',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
@@ -48,72 +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'
}
]
}
const complexLicenseChange: 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 AND Apache-2.0',
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 unlicensedChange: Change = {
change_type: 'added',
manifest: '.github/workflows/ci.yml',
ecosystem: 'actions',
name: 'foo-org/actions-repo/.github/workflows/some-action.yml',
version: '1.1.1',
package_url:
'pkg:githubactions/foo-org/actions-repo/.github/workflows/some-action.yml@1.1.1',
license: null,
source_repository_url: 'github.com/some-repo',
scope: 'development',
vulnerabilities: []
}
jest.mock('@actions/core')
const mockOctokit = {
@@ -128,7 +63,6 @@ const mockOctokit = {
jest.mock('octokit', () => {
return {
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
Octokit: class {
constructor() {
return mockOctokit
@@ -139,58 +73,41 @@ jest.mock('octokit', () => {
beforeEach(async () => {
jest.resetModules()
jest.doMock('spdx-satisfies', () => {
// mock spdx-satisfies return value
// true for BSD, false for all others
return jest.fn((license: string, _: string): boolean => license === 'BSD')
})
;({getInvalidLicenseChanges} = require('../src/licenses'))
})
test('it adds license outside the allow list to forbidden changes', async () => {
const changes: Changes = [
npmChange, // MIT license
rubyChange // BSD license
]
const changes: Changes = [npmChange, rubyChange]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD-3-Clause']
allow: ['BSD']
})
expect(forbidden[0]).toBe(npmChange)
expect(forbidden.length).toEqual(1)
})
test('it adds license inside the deny list to forbidden changes', async () => {
const changes: Changes = [
npmChange, // MIT license
rubyChange // BSD license
]
const changes: Changes = [npmChange, rubyChange]
const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['BSD-3-Clause']
deny: ['BSD']
})
expect(forbidden[0]).toBe(rubyChange)
expect(forbidden.length).toEqual(1)
})
test('it handles allowed complex licenses', async () => {
const changes: Changes = [
complexLicenseChange // MIT AND Apache-2.0 license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['MIT', 'Apache-2.0']
// 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).toEqual(0)
})
test('it handles complex licenses not all on the allow list', async () => {
const changes: Changes = [
complexLicenseChange // MIT AND Apache-2.0 license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['MIT']
})
expect(forbidden.length).toEqual(1)
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 () => {
@@ -199,7 +116,7 @@ test('it does not add license outside the allow list to forbidden changes if it
{...rubyChange, change_type: 'removed'}
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD-3-Clause']
allow: ['BSD']
})
expect(forbidden).toStrictEqual([])
})
@@ -210,7 +127,7 @@ test('it does not add license inside the deny list to forbidden changes if it is
{...rubyChange, change_type: 'removed'}
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['BSD-3-Clause']
deny: ['BSD']
})
expect(forbidden).toStrictEqual([])
})
@@ -222,87 +139,28 @@ test('it adds license outside the allow list to forbidden changes if it is in bo
{...rubyChange, change_type: 'removed'}
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD-3-Clause']
allow: ['BSD']
})
expect(forbidden).toStrictEqual([npmChange])
})
test('it adds all licenses to unresolved if it is unable to determine the validity', async () => {
const changes: Changes = [
{...npmChange, license: 'Foo'},
{...rubyChange, license: 'Bar'}
]
jest.resetModules() // reset module set in before
jest.doMock('spdx-satisfies', () => {
return jest.fn((_first: string, _second: string) => {
throw new Error('Some Error')
})
})
;({getInvalidLicenseChanges} = require('../src/licenses'))
const changes: Changes = [npmChange, rubyChange]
const invalidLicenses = await getInvalidLicenseChanges(changes, {
allow: ['Apache-2.0']
allow: ['BSD']
})
expect(invalidLicenses.forbidden.length).toEqual(0)
expect(invalidLicenses.unlicensed.length).toEqual(0)
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-3-Clause'],
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-3-Clause'],
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-3-Clause'],
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)
})
test('it does not fail if there is a license expression in the allow list', async () => {
const changes: Changes = [
{...npmChange, license: 'MIT AND Apache-2.0'},
{...rubyChange, license: 'BSD-3-Clause'}
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD-3-Clause', 'MIT AND Apache-2.0', 'MIT', 'Apache-2.0']
})
expect(forbidden.length).toEqual(0)
})
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 = {
@@ -340,26 +198,4 @@ describe('GH License API fallback', () => {
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unlicensed.length).toEqual(0)
})
test('it does not call licenses API if the package is excluded', async () => {
const {unlicensed} = await getInvalidLicenseChanges([unlicensedChange], {
licenseExclusions: [
'pkg:githubactions/foo-org/actions-repo/.github/workflows/some-action.yml'
]
})
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unlicensed.length).toEqual(0)
})
test('it checks namespaces when doing exclusions', async () => {
const {unlicensed} = await getInvalidLicenseChanges([unlicensedChange], {
licenseExclusions: [
'pkg:githubactions/bar-org/actions-repo/.github/workflows/some-action.yml'
]
})
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unlicensed.length).toEqual(1)
})
})
-186
View File
@@ -1,186 +0,0 @@
import {expect, test} from '@jest/globals'
import {parsePURL} from '../src/purl'
test('parsePURL returns an error if the purl does not start with "pkg:"', () => {
const purl = 'not-a-purl'
const result = parsePURL(purl)
expect(result.error).toEqual('package-url must start with "pkg:"')
})
test('parsePURL returns an error if the purl does not contain a type', () => {
const purl = 'pkg:/'
const result = parsePURL(purl)
expect(result.error).toEqual('package-url must contain a type')
})
test('parsePURL returns an error if the purl does not contain a namespace or name', () => {
const purl = 'pkg:ecosystem/'
const result = parsePURL(purl)
expect(result.type).toEqual('ecosystem')
expect(result.error).toEqual('package-url must contain a namespace or name')
})
test('parsePURL returns a PURL with the correct values in the happy case', () => {
const purl = 'pkg:ecosystem/namespace/name@version'
const result = parsePURL(purl)
expect(result.type).toEqual('ecosystem')
expect(result.namespace).toEqual('namespace')
expect(result.name).toEqual('name')
expect(result.version).toEqual('version')
expect(result.original).toEqual(purl)
expect(result.error).toBeNull()
})
test('parsePURL table test', () => {
const examples = [
{
purl: 'pkg:npm/@n4m3SPACE/Name@^1.2.3',
expected: {
type: 'npm',
namespace: '@n4m3SPACE',
name: 'Name',
version: '^1.2.3',
original: 'pkg:npm/@n4m3SPACE/Name@^1.2.3',
error: null
}
},
{
purl: 'pkg:golang/gopkg.in/DataDog/dd-trace-go.v1@1.63.1',
// Note: this purl is technically invalid, but we can still parse it
expected: {
type: 'golang',
namespace: 'gopkg.in',
name: 'DataDog/dd-trace-go.v1',
version: '1.63.1',
original: 'pkg:golang/gopkg.in/DataDog/dd-trace-go.v1@1.63.1',
error: null
}
},
{
purl: 'pkg:golang/github.com/pelletier/go-toml/v2',
// Note: this purl is technically invalid, but we can still parse it
expected: {
type: 'golang',
namespace: 'github.com',
name: 'pelletier/go-toml/v2',
version: null,
original: 'pkg:golang/github.com/pelletier/go-toml/v2',
error: null
}
},
{
purl: 'pkg:npm/%40ns%20foo/n%40me@1.%2f2.3',
expected: {
type: 'npm',
namespace: '@ns foo',
name: 'n@me',
version: '1./2.3',
original: 'pkg:npm/%40ns%20foo/n%40me@1.%2f2.3',
error: null
}
},
{
purl: 'pkg:ecosystem/name@version',
expected: {
type: 'ecosystem',
namespace: null,
name: 'name',
version: 'version',
original: 'pkg:ecosystem/name@version',
error: null
}
},
{
purl: 'pkg:npm/namespace/',
expected: {
type: 'npm',
namespace: 'namespace',
name: null,
version: null,
original: 'pkg:npm/namespace/',
error: null
}
},
{
purl: 'pkg:ecosystem/name',
expected: {
type: 'ecosystem',
namespace: null,
name: 'name',
version: null,
original: 'pkg:ecosystem/name',
error: null
}
},
{
purl: 'pkg:/?',
expected: {
type: '',
namespace: null,
name: null,
version: null,
original: 'pkg:/?',
error: 'package-url must contain a type'
}
},
{
purl: 'pkg:ecosystem/#',
expected: {
type: 'ecosystem',
namespace: null,
name: null,
version: null,
original: 'pkg:ecosystem/#',
error: 'package-url must contain a namespace or name'
}
},
{
purl: 'pkg:ecosystem/name@version#subpath?attributes=123',
expected: {
type: 'ecosystem',
namespace: null,
name: 'name',
version: 'version',
original: 'pkg:ecosystem/name@version#subpath?attributes=123',
error: null
}
},
{
purl: 'pkg:ecosystem/name@version#subpath',
expected: {
type: 'ecosystem',
namespace: null,
name: 'name',
version: 'version',
original: 'pkg:ecosystem/name@version#subpath',
error: null
}
},
{
purl: 'pkg:ecosystem/namespace/name@version?attributes',
expected: {
type: 'ecosystem',
namespace: 'namespace',
name: 'name',
version: 'version',
original: 'pkg:ecosystem/namespace/name@version?attributes',
error: null
}
},
{
purl: 'pkg:ecosystem/name#subpath?attributes',
expected: {
type: 'ecosystem',
namespace: null,
name: 'name',
version: null,
original: 'pkg:ecosystem/name#subpath?attributes',
error: null
}
}
]
for (const example of examples) {
const result = parsePURL(example.purl)
expect(result).toEqual(example.expected)
}
})
-61
View File
@@ -1,61 +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'
}
]
}
const actionsChange: Change = {
manifest: 'workflow.yml',
change_type: 'added',
ecosystem: 'actions',
name: 'actions/checkout/',
version: 'v3',
package_url: 'pkg:githubactions/actions@v3',
license: 'MIT',
source_repository_url: 'null',
scope: 'runtime',
vulnerabilities: []
}
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()
})
test('Handles Actions special case', async () => {
const changes: Changes = [actionsChange]
const result = await getScorecardLevels(changes)
expect(result).not.toBeNull()
expect(result.dependencies).toHaveLength(1)
expect(result.dependencies[0].scorecard?.score).toBeGreaterThan(0)
})
-326
View File
@@ -1,326 +0,0 @@
import {expect, test, describe} from '@jest/globals'
import * as spdx from '../src/spdx'
describe('satisfiesAny', () => {
const units = [
{
candidate: 'MIT',
licenses: ['MIT'],
expected: true
},
{
candidate: 'MIT OR Apache-2.0',
licenses: ['MIT', 'Apache-2.0'],
expected: true
},
{
candidate: '(MIT AND ISC) OR Apache-2.0',
licenses: ['MIT', 'Apache-2.0'],
expected: true
},
{
candidate: 'MIT AND Apache-2.0',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
{
candidate: 'MIT AND BSD-3-Clause',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
// missing params, case sensitivity, syntax problems,
// or unknown licenses will return 'false'
{
candidate: 'MIT OR',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
{
candidate: '',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
{
candidate: 'MIT OR (Apache-2.0 AND ISC)',
licenses: [],
expected: false
},
{
candidate: 'MIT AND (ISC',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
{
candidate: 'MIT OR ISC',
licenses: ['MiT'],
expected: false
},
{
candidate: 'MIT AND OTHER',
licenses: ['MIT'],
expected: false
},
{
candidate: 'MIT OR OTHER',
licenses: ['MIT', 'LicenseRef-clearlydefined-OTHER'],
expected: true
}
]
for (const unit of units) {
const got: boolean = spdx.satisfiesAny(unit.candidate, unit.licenses)
test(`should return ${unit.expected} for ("${unit.candidate}", "${unit.licenses}")`, () => {
expect(got).toBe(unit.expected)
})
}
})
describe('satisfiesAll', () => {
const units = [
{
candidate: 'MIT',
licenses: ['MIT'],
expected: true
},
{
candidate: 'Apache-2.0',
licenses: ['MIT', 'ISC', 'Apache-2.0'],
expected: false
},
{
candidate: 'MIT AND Apache-2.0',
licenses: ['MIT', 'Apache-2.0'],
expected: true
},
{
candidate: '(MIT OR ISC) AND Apache-2.0',
licenses: ['MIT', 'Apache-2.0'],
expected: true
},
{
candidate: 'MIT OR BSD-3-Clause',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
{
candidate: 'BSD-3-Clause OR ISC',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
{
candidate: '(MIT AND ISC) OR Apache-2.0',
licenses: ['MIT', 'ISC'],
expected: true
},
// missing params, case sensitivity, syntax problems,
// or unknown licenses will return 'false'
{
candidate: 'MIT OR',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
{
candidate: '',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
{
candidate: 'MIT OR (Apache-2.0 AND ISC)',
licenses: [],
expected: false
},
{
candidate: 'MIT AND (ISC',
licenses: ['MIT', 'Apache-2.0'],
expected: false
},
{
candidate: 'MIT OR ISC',
licenses: ['MiT'],
expected: false
},
{
candidate: 'MIT AND OTHER',
licenses: ['MIT'],
expected: false
},
{
candidate: 'MIT AND OTHER',
licenses: ['MIT', 'LicenseRef-clearlydefined-OTHER'],
expected: true
}
]
for (const unit of units) {
const got: boolean = spdx.satisfiesAll(unit.candidate, unit.licenses)
test(`should return ${unit.expected} for ("${unit.candidate}", "${unit.licenses}")`, () => {
expect(got).toBe(unit.expected)
})
}
})
describe('satisfies', () => {
const units = [
{
candidate: 'MIT',
allowList: ['MIT'],
expected: true
},
{
candidate: 'Apache-2.0',
allowList: ['MIT'],
expected: false
},
{
candidate: 'MIT OR Apache-2.0',
allowList: ['MIT'],
expected: true
},
{
candidate: 'MIT OR Apache-2.0',
allowList: ['Apache-2.0'],
expected: true
},
{
candidate: 'MIT OR Apache-2.0',
allowList: ['BSD-3-Clause'],
expected: false
},
{
candidate: 'MIT OR Apache-2.0',
allowList: ['Apache-2.0', 'BSD-3-Clause'],
expected: true
},
{
candidate: 'MIT AND Apache-2.0',
allowList: ['MIT', 'Apache-2.0'],
expected: true
},
{
candidate: 'MIT OR Apache-2.0',
allowList: ['MIT', 'Apache-2.0'],
expected: true
},
{
candidate: 'ISC OR (MIT AND Apache-2.0)',
allowList: ['MIT', 'Apache-2.0'],
expected: true
},
// missing params, case sensitivity, syntax problems,
// or unknown licenses will return 'false'
{
candidate: 'MIT',
allowList: ['MiT'],
expected: false
},
{
candidate: 'MIT AND (ISC OR',
allowList: ['MIT'],
expected: false
},
{
candidate: 'MIT OR ISC OR Apache-2.0',
allowList: [],
expected: false
},
{
candidate: '',
allowList: ['BSD-3-Clause', 'ISC', 'MIT'],
expected: false
},
{
candidate: 'MIT OR OTHER',
allowList: ['MIT', 'LicenseRef-clearlydefined-OTHER'],
expected: true
},
{
candidate: '(Apache-2.0 AND OTHER) OR (MIT AND OTHER)',
allowList: ['Apache-2.0', 'LicenseRef-clearlydefined-OTHER'],
expected: true
}
]
for (const unit of units) {
const got: boolean = spdx.satisfies(unit.candidate, unit.allowList)
test(`should return ${unit.expected} for ("${unit.candidate}", "${unit.allowList}")`, () => {
expect(got).toBe(unit.expected)
})
}
})
describe('isValid', () => {
const units = [
{
candidate: 'MIT',
expected: true
},
{
candidate: 'MIT AND BSD-3-Clause',
expected: true
},
{
candidate: '(MIT AND ISC) OR BSD-3-Clause',
expected: true
},
{
candidate: 'NOASSERTION',
expected: false
},
{
candidate: 'Foobar',
expected: false
},
{
candidate: '',
expected: false
},
{
candidate: 'MIT AND OTHER',
expected: true
}
]
for (const unit of units) {
const got: boolean = spdx.isValid(unit.candidate)
test(`should return ${unit.expected} for ("${unit.candidate}")`, () => {
expect(got).toBe(unit.expected)
})
}
})
describe('cleanInvalidSPDX', () => {
const units = [
{
candidate: 'MIT',
expected: 'MIT'
},
{
candidate: 'OTHER',
expected: 'LicenseRef-clearlydefined-OTHER'
},
{
candidate: 'LicenseRef-clearlydefined-OTHER',
expected: 'LicenseRef-clearlydefined-OTHER'
},
{
candidate: 'OTHER AND MIT',
expected: 'LicenseRef-clearlydefined-OTHER AND MIT'
},
{
candidate: 'MIT AND OTHER',
expected: 'MIT AND LicenseRef-clearlydefined-OTHER'
},
{
candidate: 'MIT AND SomethingElse-OTHER',
expected: 'MIT AND SomethingElse-OTHER'
}
]
for (const unit of units) {
const got: string = spdx.cleanInvalidSPDX(unit.candidate)
test(`should return ${unit.expected} for ("${unit.candidate}")`, () => {
expect(got).toBe(unit.expected)
})
}
})
-486
View File
@@ -1,486 +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('does not add deprecation warning for deny-licenses option if not set', () => {
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
scorecard,
defaultConfig
)
const text = core.summary.stringify()
expect(text).not.toContain('deny-licenses')
})
test('adds deprecation warning for deny-licenses option if set', () => {
const config = {...defaultConfig, deny_licenses: ['MIT']}
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
scorecard,
config
)
const text = core.summary.stringify()
expect(text).toContain('deny-licenses')
})
test('returns minimal summary formatted for posting as a PR comment', () => {
const OLD_ENV = process.env
const changes: Changes = [
createTestChange({name: 'lodash', version: '1.2.3'}),
createTestChange({name: 'colors', version: '2.3.4'}),
createTestChange({name: '@foo/bar', version: '*'})
]
process.env.GITHUB_SERVER_URL = 'https://github.com'
process.env.GITHUB_REPOSITORY = 'owner/repo'
process.env.GITHUB_RUN_ID = 'abc-123-xyz'
const minSummary: string = summary.addSummaryToSummary(
changes,
emptyInvalidLicenseChanges,
emptyChanges,
scorecard,
defaultConfig
)
process.env = OLD_ENV
// note: no Actions context values in unit test env
const expected = `
# Dependency Review
The following issues were found:
* ❌ 3 vulnerable package(s)
* ✅ 0 package(s) with incompatible licenses
* ✅ 0 package(s) with invalid SPDX license definitions
* ✅ 0 package(s) with unknown licenses.
[View full job summary](https://github.com/owner/repo/actions/runs/abc-123-xyz)
`.trim()
expect(minSummary).toEqual(expected)
})
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.addScannedFiles(changesWithEmptyManifests)
const text = core.summary.stringify()
expect(text).toContain('Unnamed Manifest')
expect(text).toContain('python/dist-info/METADATA')
})
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')
})
-32
View File
@@ -1,32 +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',
'ALLOW-DEPENDENCIES-LICENSES',
'DENY-LICENSES',
'ALLOW-GHSAS',
'LICENSE-CHECK',
'VULNERABILITY-CHECK',
'CONFIG-FILE',
'BASE-REF',
'HEAD-REF',
'COMMENT-SUMMARY-IN-PR',
'WARN-ONLY',
'DENY-GROUPS',
'DENY-PACKAGES'
]
// eslint-disable-next-line github/array-foreach
allowedOptions.forEach(option => {
delete process.env[`INPUT_${option.toUpperCase()}`]
})
}
+5 -61
View File
@@ -1,13 +1,3 @@
# IMPORTANT
#
# Avoid setting default values for configuration options in
# this file, they will overwrite external configurations.
#
# If you are trying to find out the default value for a config
# option please take a look at the README or src/schemas.ts.
#
# If you are adding an option, make sure the Zod definition
# contains a default value.
name: 'Dependency Review'
description: 'Prevent the introduction of dependencies with known vulnerabilities'
author: 'GitHub'
@@ -19,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
@@ -29,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")
@@ -37,57 +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"). If version specified, only deny matching packages and version; else, deny all regardless of version.
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/"). Please note that the group name must be followed by a `/`.
required: false
retry-on-snapshot-warnings:
description: Whether to retry on snapshot warnings
required: false
retry-on-snapshot-warnings-timeout:
description: Number of seconds to wait before stopping snapshot retries.
required: false
warn-only:
description: When set to `true` this action will always complete with success, overriding the `fail-on-severity` parameter.
required: false
show-openssf-scorecard:
description: Show a summary of the OpenSSF Scorecard scores.
required: false
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
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'
Generated Vendored
+14458 -40324
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+158 -104
View File
@@ -10,18 +10,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.
@actions/exec
MIT
The MIT License (MIT)
Copyright 2019 GitHub
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.
@actions/github
MIT
The MIT License (MIT)
@@ -59,40 +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.
@actions/io
MIT
The MIT License (MIT)
Copyright 2019 GitHub
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.
@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
@@ -368,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
@@ -538,30 +481,15 @@ 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.
@onebeyond/spdx-license-satisfies
@vercel/ncc
MIT
The MIT License
Copyright 2018 ZEIT, Inc.
Copyright (c) spdx-satisfies.js contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
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.
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
@@ -1118,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
@@ -1131,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)
@@ -1484,7 +1461,7 @@ lru-cache
ISC
The ISC License
Copyright (c) 2010-2023 Isaac Z. Schlueter and Contributors
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
@@ -1524,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
@@ -1695,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)
@@ -1720,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
@@ -1781,6 +1762,60 @@ Permission to use, copy, modify, and/or distribute this software for any purpose
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
uuid
MIT
The MIT License (MIT)
Copyright (c) 2010-2020 Robert Kieffer and other 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.
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) 20152016 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
@@ -1800,6 +1835,25 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
yallist
ISC
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
yaml
ISC
Copyright Eemeli Aro <eemeli@gmail.com>
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
-340
View File
@@ -1,340 +0,0 @@
# Examples of 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 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/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 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/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
With the `deny-packages` option, you can exclude dependencies based on their PURL (Package URL). If a specific version is provided, the action will deny packages matching that version. When no version is specified, the action treats it as a wildcard, denying all matching packages regardless of version. Multiple values can be added, separated by 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 all versions of `pkg:maven/org.apache.logging.log4j:log4j-api` and only `2.23.0` of log4j-core `pkg:maven/org.apache.logging.log4j/log4j-core@2.23.0` 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@2.23.0'
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
+9376 -3349
View File
File diff suppressed because it is too large Load Diff
+28 -33
View File
@@ -1,11 +1,11 @@
{
"name": "dependency-review-action",
"version": "4.7.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,41 +25,36 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.1",
"@octokit/plugin-retry": "^6.1.0",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "12.5.0",
"@onebeyond/spdx-license-satisfies": "^1.0.1",
"@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.4.7",
"jest": "^29.7.0",
"octokit": "^3.1.2",
"got": "^12.5.2",
"nodemon": "^2.0.20",
"octokit": "^2.0.10",
"spdx-expression-parse": "^3.0.1",
"spdx-satisfies": "^6.0.0",
"ts-jest": "^29.4.1",
"yaml": "^2.8.1",
"zod": "^3.24.1"
"spdx-satisfies": "^5.0.1",
"yaml": "^2.1.3",
"zod": "^3.19.1"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20",
"@types/spdx-expression-parse": "^3.0.4",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vercel/ncc": "^0.38.3",
"esbuild-register": "^3.6.0",
"eslint": "^8.57.0",
"eslint-plugin-github": "^4.10.2",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-prettier": "^5.5.4",
"@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.10",
"prettier": "3.6.2",
"typescript": "^5.9.2"
},
"overrides": {
"cross-spawn": ">=7.0.5",
"@octokit/request-error@5.0.1": "5.1.1"
"nodemon": "^2.0.20",
"prettier": "2.7.1",
"ts-jest": "^27.1.4",
"typescript": "^4.8.4"
}
}
-155
View File
@@ -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.addScannedFiles(allChanges)
const text = core.summary.stringify()
await fs.promises.writeFile(path.resolve(tmpDir, fileName), text, {
flag: 'w'
})
core.summary.emptyBuffer()
}
createExampleSummaries()
-100
View File
@@ -1,100 +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'
export const MAX_COMMENT_LENGTH = 65536
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(
commentContent: string,
config: ConfigurationOptions,
issueFound: boolean
): Promise<void> {
if (
!(
config.comment_summary_in_pr === 'always' ||
(config.comment_summary_in_pr === 'on-failure' && issueFound)
)
) {
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
}
+87 -177
View File
@@ -3,90 +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 {octokitClient} from './utils'
import {isValid} from './spdx'
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'
)
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)
@@ -107,14 +32,13 @@ 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 => !isValid(license))
const invalid_licenses = licenses.filter(license => !isSPDXValid(license))
if (invalid_licenses.length > 0) {
throw new Error(
@@ -123,103 +47,89 @@ function validateLicenses(
}
}
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}`
)
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()
}
}
function parseConfigFile(configData: string): ConfigurationOptionsPartial {
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])
}
// 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]
}
}
const values = ConfigurationOptionsSchema.parse(data)
return values
}
-51
View File
@@ -1,51 +0,0 @@
import * as core from '@actions/core'
import {Change} from './schemas'
import {PackageURL, parsePURL} from './purl'
export async function getDeniedChanges(
changes: Change[],
deniedPackages: PackageURL[] = [],
deniedGroups: PackageURL[] = []
): Promise<Change[]> {
const changesDenied: Change[] = []
for (const change of changes) {
if (change.change_type === 'removed') {
continue
}
for (const denied of deniedPackages) {
if (
(!denied.version || change.version === denied.version) &&
change.name === denied.name
) {
changesDenied.push(change)
}
}
for (const denied of deniedGroups) {
const namespace = getNamespace(change)
if (!denied.namespace) {
core.error(
`Denied group represented by '${denied.original}' does not have a namespace. The format should be 'pkg:<type>/<namespace>/'.`
)
}
if (namespace && namespace === denied.namespace) {
changesDenied.push(change)
}
}
}
return changesDenied
}
export const getNamespace = (change: Change): string | null => {
if (change.package_url) {
return parsePURL(change.package_url).namespace
}
const matches = change.name.match(/([^:/]+)[:/]/)
if (matches && matches.length > 1) {
return matches[1]
}
return null
}
+5 -28
View File
@@ -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
View File
@@ -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
+12 -28
View File
@@ -1,53 +1,37 @@
import {
PullRequestSchema,
ConfigurationOptions,
MergeGroupSchema
} from './schemas'
import {PullRequestSchema, ConfigurationOptions} from './schemas'
export function getRefs(
config: ConfigurationOptions,
context: {
payload: {pull_request?: unknown; merge_group?: unknown}
eventName: string
}
context: {payload: {pull_request?: unknown}; eventName: string}
): {base: string; head: string} {
let base_ref = config.base_ref
let head_ref = config.head_ref
// If possible, source default base & head refs from the GitHub event.
// The base/head ref from the config take priority, if provided.
if (!base_ref && !head_ref) {
if (
context.eventName === 'pull_request' ||
context.eventName === 'pull_request_target'
) {
const pull_request = PullRequestSchema.parse(context.payload.pull_request)
base_ref = base_ref || pull_request.base.sha
head_ref = head_ref || pull_request.head.sha
} else if (context.eventName === 'merge_group') {
const merge_group = MergeGroupSchema.parse(context.payload.merge_group)
base_ref = base_ref || merge_group.base_sha
head_ref = head_ref || merge_group.head_sha
}
if (
context.eventName === 'pull_request' ||
context.eventName === 'pull_request_target'
) {
const pull_request = PullRequestSchema.parse(context.payload.pull_request)
base_ref = base_ref || pull_request.base.sha
head_ref = head_ref || pull_request.head.sha
}
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`/`merge_group` 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`/`merge_group` 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`/`merge_group` workflow.'
'or by running a `pull_request`/`pull_request_target` workflow.'
)
}
+25 -82
View File
@@ -1,55 +1,34 @@
import * as core from '@actions/core'
import spdxSatisfies from 'spdx-satisfies'
import {Octokit} from 'octokit'
import {Change, Changes} from './schemas'
import {octokitClient} from './utils'
import {parsePURL, PackageURL} from './purl'
import * as spdx from './spdx'
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> {
const deny = licenses.deny
let allow = licenses.allow
// Filter out elements of the allow list that include AND
// or OR because the list should be simple license IDs and
// not expressions.
allow = allow?.filter(license => {
return !license.includes(' AND ') && !license.includes(' OR ')
})
const licenseExclusions = licenses.licenseExclusions?.map(
(pkgUrl: string) => {
return parsePURL(pkgUrl)
}
)
const groupedChanges = await groupChanges(changes, licenseExclusions)
): Promise<Record<string, Changes>> {
const {allow, deny} = licenses
const groupedChanges = await groupChanges(changes)
const licensedChanges: Changes = groupedChanges.licensed
const invalidLicenseChanges: InvalidLicenseChanges = {
const invalidLicenseChanges: Record<string, Changes> = {
unlicensed: groupedChanges.unlicensed,
unresolved: [],
forbidden: []
@@ -70,19 +49,15 @@ export async function getInvalidLicenseChanges(
} else if (validityCache.get(license) === undefined) {
try {
if (allow !== undefined) {
if (spdx.isValid(license)) {
const found = spdx.satisfies(license, allow)
validityCache.set(license, found)
} else {
invalidLicenseChanges.unresolved.push(change)
}
const found = allow.find(spdxExpression =>
spdxSatisfies(license, spdxExpression)
)
validityCache.set(license, found !== undefined)
} else if (deny !== undefined) {
if (spdx.isValid(license)) {
const found = spdx.satisfiesAny(license, deny)
validityCache.set(license, !found)
} else {
invalidLicenseChanges.unresolved.push(change)
}
const found = deny.find(spdxExpression =>
spdxSatisfies(license, spdxExpression)
)
validityCache.set(license, found === undefined)
}
} catch (err) {
invalidLicenseChanges.unresolved.push(change)
@@ -101,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
@@ -148,55 +124,22 @@ const setGHLicenses = async (changes: Change[]): Promise<Change[]> => {
return Promise.all(updatedChanges)
}
// Currently Dependency Graph licenses are truncated to 255 characters
// This possibly makes them invalid spdx ids
const truncatedDGLicense = (license: string): boolean =>
license.length === 255 && !spdx.isValid(license)
license.length === 255 && !isSPDXValid(license)
async function groupChanges(
changes: Changes,
licenseExclusions: PackageURL[] | null = null
changes: Changes
): Promise<Record<string, Changes>> {
const result: Record<string, Changes> = {
licensed: [],
unlicensed: []
}
let candidateChanges = changes
// If a package is excluded from license checking, we don't bother trying to
// fetch the license for it and we leave it off of the `licensed` and
// `unlicensed` lists.
if (licenseExclusions !== null && licenseExclusions !== undefined) {
candidateChanges = candidateChanges.filter(change => {
if (change.package_url.length === 0) {
return true
}
const changeAsPackageURL = parsePURL(encodeURI(change.package_url))
// We want to find if the licenseExclusion 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.findIndex(
exclusion =>
exclusion.type === changeAsPackageURL.type &&
exclusion.namespace === changeAsPackageURL.namespace &&
exclusion.name === changeAsPackageURL.name
) !== -1
) {
return false
} else {
return true
}
})
}
const ghChanges = []
for (const change of candidateChanges) {
for (const change of changes) {
if (change.change_type === 'removed') {
continue
}
+42 -262
View File
@@ -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,178 +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, MAX_COMMENT_LENGTH} 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
summary.addSummaryToSummary(
config.vulnerability_check ? addedChanges : null,
config.license_check ? invalidLicenseChanges : null
)
// generate informational scorecard entries for all added changes in the PR
const scorecardChanges = getScorecardChanges(changes)
const scorecard = await getScorecardLevels(scorecardChanges)
const minSummary = summary.addSummaryToSummary(
vulnerableChanges,
invalidLicenseChanges,
deniedChanges,
scorecard,
config
)
if (snapshot_warnings) {
summary.addSnapshotWarnings(config, snapshot_warnings)
}
let issueFound = false
if (config.vulnerability_check) {
core.setOutput('vulnerable-changes', JSON.stringify(vulnerableChanges))
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
issueFound ||= await 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)
issueFound ||= await printLicensesBlock(invalidLicenseChanges, warnOnly)
}
if (config.deny_packages || config.deny_groups) {
core.setOutput('denied-changes', JSON.stringify(deniedChanges))
summary.addDeniedToSummary(deniedChanges)
issueFound ||= await 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.addScannedFiles(changes)
summary.addScannedDependencies(changes)
printScannedDependencies(changes)
// include full summary in output; Actions will truncate if oversized
let rendered = core.summary.stringify()
core.setOutput('comment-content', rendered)
// if the summary is oversized, replace with minimal version
if (rendered.length >= MAX_COMMENT_LENGTH) {
core.debug(
'The comment was too big for the GitHub API. Falling back on a minimum comment'
)
rendered = minSummary
}
// update the PR comment if needed with the right-sized summary
await commentPr(rendered, config, issueFound)
} catch (error) {
if (error instanceof RequestError && error.status === 404) {
core.setFailed(
@@ -196,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 ${github.context.serverUrl}/${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) {
@@ -210,36 +90,30 @@ async function run(): Promise<void> {
}
}
async function printVulnerabilitiesBlock(
function printVulnerabilitiesBlock(
addedChanges: Changes,
minSeverity: Severity,
warnOnly: boolean
): Promise<boolean> {
return core.group('Vulnerabilities', async () => {
let vulFound = false
for (const change of addedChanges) {
vulFound ||= printChangeVulnerabilities(change)
minSeverity: Severity
): void {
let failed = false
core.group('Vulnerabilities', async () => {
if (addedChanges.length > 0) {
for (const change of addedChanges) {
printChangeVulnerabilities(change)
}
failed = true
}
if (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.`
)
}
return vulFound
})
}
function printChangeVulnerabilities(change: Change): boolean {
function printChangeVulnerabilities(change: Change): void {
for (const vuln of change.vulnerabilities) {
core.info(
`${styles.bold.open}${change.manifest} » ${change.name}@${
@@ -250,29 +124,18 @@ function printChangeVulnerabilities(change: Change): boolean {
)
core.info(`${vuln.advisory_url}`)
}
return change.vulnerabilities.length > 0
}
async function printLicensesBlock(
invalidLicenseChanges: Record<string, Changes>,
warnOnly: boolean
): Promise<boolean> {
return core.group('Licenses', async () => {
let issueFound = false
function printLicensesBlock(
invalidLicenseChanges: Record<string, Changes>
): void {
core.group('Licenses', async () => {
if (invalidLicenseChanges.forbidden.length > 0) {
issueFound = true
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) {
issueFound = true
core.warning(
'\nThe validity of the licenses of the dependencies below could not be determined. Ensure that they are valid SPDX licenses:'
)
@@ -282,8 +145,6 @@ async function printLicensesBlock(
)
}
printNullLicenses(invalidLicenseChanges.unlicensed)
return issueFound
})
}
@@ -308,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 {
@@ -383,62 +221,4 @@ function printScannedDependencies(changes: Changes): void {
})
}
async function printDeniedDependencies(
changes: Changes,
config: ConfigurationOptions
): Promise<boolean> {
return core.group('Denied', async () => {
let issueFound = false
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`)
}
if (changes.length > 0) {
issueFound = true
core.setFailed('Dependency review detected denied packages.')
} else {
core.info('Dependency review did not detect any denied packages')
}
return issueFound
})
}
function getScorecardChanges(changes: Changes): Changes {
const out: Changes = []
for (const change of changes) {
if (change.change_type === 'added') {
out.push(change)
}
}
return out
}
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()
-72
View File
@@ -1,72 +0,0 @@
import * as z from 'zod'
// the basic purl type, containing type, namespace, name, and version.
// other than type, all fields are nullable. this is for maximum flexibility
// at the cost of strict adherence to the package-url spec.
export const PurlSchema = z.object({
type: z.string(),
namespace: z.string().nullable(),
name: z.string().nullable(), // name is nullable for deny-groups
version: z.string().nullable(),
original: z.string(),
error: z.string().nullable()
})
export type PackageURL = z.infer<typeof PurlSchema>
const PURL_TYPE = /pkg:([a-zA-Z0-9-_]+)\/.*/
export function parsePURL(purl: string): PackageURL {
const result: PackageURL = {
type: '',
namespace: null,
name: null,
version: null,
original: purl,
error: null
}
if (!purl.startsWith('pkg:')) {
result.error = 'package-url must start with "pkg:"'
return result
}
const type = purl.match(PURL_TYPE)
if (!type) {
result.error = 'package-url must contain a type'
return result
}
result.type = type[1]
const parts = purl.split('/')
// the first 'part' should be 'pkg:ecosystem'
if (parts.length < 2 || !parts[1]) {
result.error = 'package-url must contain a namespace or name'
return result
}
let namePlusRest: string
if (parts.length === 2) {
namePlusRest = parts[1]
} else {
result.namespace = decodeURIComponent(parts[1])
// Add back the '/'s to the rest of the parts, in case there are any more.
// This may violate the purl spec, but people do it and it can be parsed
// without ambiguity.
namePlusRest = parts.slice(2).join('/')
}
const name = namePlusRest.match(/([^@#?]+)[@#?]?.*/)
if (!result.namespace && !name) {
result.error = 'package-url must contain a namespace or name'
return result
}
if (!name) {
// we're done here
return result
}
result.name = decodeURIComponent(name[1])
const version = namePlusRest.match(/@([^#?]+)[#?]?.*/)
if (!version) {
return result
}
result.version = decodeURIComponent(version[1])
// we don't parse subpath or attributes, so we're done here
return result
}
+11 -163
View File
@@ -1,67 +1,10 @@
import * as z from 'zod'
import {parsePURL} from './purl'
export const SEVERITIES = ['critical', 'high', 'moderate', 'low'] as const
export const SCOPES = ['unknown', 'runtime', 'development'] as const
export const SeveritySchema = z.enum(SEVERITIES).default('low')
const PackageURL = z
.string()
.transform(purlString => {
return parsePURL(purlString)
})
.superRefine((purl, context) => {
if (purl.error) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Error parsing package-url: ${purl.error}`
})
}
if (!purl.name) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Error parsing package-url: name is required`
})
}
})
const PackageURLWithNamespace = z
.string()
.transform(purlString => {
return parsePURL(purlString)
})
.superRefine((purl, context) => {
if (purl.error) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Error parsing purl: ${purl.error}`
})
}
if (purl.namespace === null) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `package-url must have a namespace, and the namespace must be followed by '/'`
})
}
})
const PackageURLString = z.string().superRefine((value, context) => {
const purl = parsePURL(value)
if (purl.error) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Error parsing package-url: ${purl.error}`
})
}
if (!purl.name) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Error parsing package-url: name is required`
})
}
})
export const ChangeSchema = z.object({
change_type: z.enum(['added', 'removed']),
manifest: z.string(),
@@ -91,124 +34,29 @@ export const PullRequestSchema = z.object({
head: z.object({sha: z.string()})
})
export const MergeGroupSchema = z.object({
base_sha: z.string(),
head_sha: z.string()
})
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(PackageURLString).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(PackageURL).default([]),
deny_groups: z.array(PackageURLWithNamespace).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]
-81
View File
@@ -1,81 +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://', '')
}
// Handle the special case for GitHub Actions, where the repository URL is null
if (ecosystem === 'actions') {
// The package name for GitHub Actions in the API is in the format `owner/repo/`, so we can use that to get the repository URL
// If the package name has more than 2 slashes, it's referencing a sub-action, and we need to strip the last part out
const parts = packageName.split('/')
repositoryUrl = `github.com/${parts[0]}/${parts[1]}` // e.g. github.com/actions/checkout
}
// 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}/v3/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 ''
}
-4
View File
@@ -1,4 +0,0 @@
declare module 'spdx-satisfies' {
function spdxSatisfies(candidate: string, allowList: string[]): boolean
export = spdxSatisfies
}
-66
View File
@@ -1,66 +0,0 @@
import * as spdxlib from '@onebeyond/spdx-license-satisfies'
import spdxSatisfies from 'spdx-satisfies'
import parse from 'spdx-expression-parse'
/*
* NOTE: spdx-license-satisfies methods depend on spdx-expression-parse
* which throws errors in the presence of any syntax trouble, unknown
* license tokens, case sensitivity problems etc. to simplify handling
* you should pre-screen inputs to the satisfies* methods using isValid
*/
// accepts a pair of well-formed SPDX expressions. the
// candidate is tested against the constraint
export function satisfies(candidateExpr: string, allowList: string[]): boolean {
candidateExpr = cleanInvalidSPDX(candidateExpr)
try {
return spdxSatisfies(candidateExpr, allowList)
} catch (_) {
return false
}
}
// accepts an SPDX expression and a non-empty list of licenses (not expressions)
export function satisfiesAny(
candidateExpr: string,
licenses: string[]
): boolean {
candidateExpr = cleanInvalidSPDX(candidateExpr)
try {
return spdxlib.satisfiesAny(candidateExpr, licenses)
} catch (_) {
return false
}
}
// accepts an SPDX expression and a non-empty list of licenses (not expressions)
export function satisfiesAll(
candidateExpr: string,
licenses: string[]
): boolean {
candidateExpr = cleanInvalidSPDX(candidateExpr)
try {
return spdxlib.satisfiesAll(candidateExpr, licenses)
} catch (_) {
return false
}
}
// accepts any SPDX expression
export function isValid(spdxExpr: string): boolean {
spdxExpr = cleanInvalidSPDX(spdxExpr)
try {
parse(spdxExpr)
return true
} catch (_) {
return false
}
}
const replaceOtherRegex = /(?<![\w-])OTHER(?![\w-])/g
// adjusts license expressions to not include the invalid `OTHER`
// which ClearlyDefined adds to license strings
export function cleanInvalidSPDX(spdxExpr: string): string {
return spdxExpr.replace(replaceOtherRegex, 'LicenseRef-clearlydefined-OTHER')
}
+79 -342
View File
@@ -1,153 +1,50 @@
import * as core from '@actions/core'
import {ConfigurationOptions, Changes} from './schemas'
import {SummaryTableRow} from '@actions/core/lib/summary'
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
import {Change, Changes, ConfigurationOptions, Scorecard} from './schemas'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
const icons = {
check: '✅',
cross: '❌',
warning: '⚠️'
}
const MAX_SCANNED_FILES_BYTES = 1048576
// generates the DR report summmary and caches it to the Action's core.summary.
// returns the DR summary string, ready to be posted as a PR comment if the
// final DR report is too large
export function addSummaryToSummary(
vulnerableChanges: Changes,
invalidLicenseChanges: InvalidLicenseChanges,
deniedChanges: Changes,
scorecard: Scorecard,
config: ConfigurationOptions
): string {
if (config.deny_licenses && config.deny_licenses.length > 0) {
addDenyListsDeprecationWarningToSummary()
}
const out: string[] = []
const scorecardWarnings = countScorecardWarnings(scorecard, config)
const licenseIssues = countLicenseIssues(invalidLicenseChanges)
core.summary.addHeading('Dependency Review', 1)
out.push('# Dependency Review')
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' : ''
]
let msg = ''
if (issueTypes.filter(Boolean).length === 0) {
msg = `${icons.check} No issues found.`
} else {
msg = `${icons.check} No ${issueTypes.filter(Boolean).join(' or ')} found.`
}
core.summary.addRaw(msg)
out.push(msg)
return out.join('\n')
}
const foundIssuesHeader = 'The following issues were found:'
core.summary.addRaw(foundIssuesHeader)
out.push(foundIssuesHeader)
const summaryList: string[] = [
...(config.vulnerability_check
? [
`${checkOrFailIcon(vulnerableChanges.length)} ${
vulnerableChanges.length
} vulnerable package(s)`
]
: []),
...(config.license_check
? [
`${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.`
]
: [])
]
core.summary.addList(summaryList)
for (const line of summaryList) {
out.push(`* ${line}`)
}
core.summary.addRaw('See the Details below.')
out.push(
`\n[View full job summary](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})`
)
return out.join('\n')
}
function addDenyListsDeprecationWarningToSummary(): void {
core.summary.addRaw(
`${icons.warning} <strong>Deprecation Warning</strong>: The <em>deny-licenses</em> option is deprecated for possible removal in the next major release. For more information, see actions/dependency-review-action/issues/938.`,
true
)
}
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
)
addedPackages: Changes | null,
invalidLicenseChanges: Record<string, Changes> | null
): void {
core.summary
.addHeading('Dependency Review')
.addRaw('We found:')
.addList([
...(addedPackages
? [`${addedPackages.length} vulnerable package(s)`]
: []),
...(invalidLicenseChanges
? [
`${invalidLicenseChanges.unresolved.length} package(s) with invalid SPDX license definitions`,
`${invalidLicenseChanges.forbidden.length} package(s) with incompatible licenses`,
`${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.`
]
: [])
])
}
export function addChangeVulnerabilitiesToSummary(
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 = ''
@@ -175,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},
@@ -185,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(
@@ -214,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(
@@ -229,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.`)
}
}
@@ -276,164 +156,21 @@ function formatLicense(license: string | null): string {
return license
}
export function addScannedFiles(changes: Changes): void {
const manifests = Array.from(
groupDependenciesByManifest(changes).keys()
).sort()
export function addScannedDependencies(changes: Changes): void {
const dependencies = groupDependenciesByManifest(changes)
const manifests = dependencies.keys()
let sf_size = 0
let trunc_at = -1
const summary = core.summary
.addHeading('Scanned Dependencies')
.addHeading(`We scanned ${dependencies.size} manifest files:`, 5)
for (const [index, entry] of manifests.entries()) {
if (sf_size + entry.length >= MAX_SCANNED_FILES_BYTES) {
trunc_at = index
break
}
sf_size += entry.length
}
if (trunc_at >= 0) {
// truncate the manifests list if it will overflow the summary output
manifests.slice(0, trunc_at)
// if there's room between cutoff size and list size, add a warning
const size_diff = MAX_SCANNED_FILES_BYTES - sf_size
if (size_diff < 12) {
manifests.push('(truncated)')
}
}
const summary = core.summary.addHeading('Scanned Files', 2)
if (manifests.length === 0) {
summary.addRaw('None')
} else {
summary.addList(manifests)
}
}
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 {
if (scorecard.dependencies.length === 0) {
return
}
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="${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
for (const manifest of manifests) {
const deps = dependencies.get(manifest)
if (deps) {
const dependencyNames = deps.map(
dependency => `<li>${dependency.name}@${dependency.version}</li>`
)
} else {
core.summary.addRaw('<td>Unknown</td></tr>', true)
summary.addDetails(manifest, `<ul>${dependencyNames.join('')}</ul>`)
}
}
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
}
+8 -28
View File
@@ -1,5 +1,4 @@
import * as core from '@actions/core'
import {Octokit} from 'octokit'
import spdxParse from 'spdx-expression-parse'
import {Changes} from './schemas'
export function groupDependenciesByManifest(
@@ -7,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, [])
@@ -33,28 +30,11 @@ export function renderUrl(url: string | null, text: string): string {
}
}
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
export function isSPDXValid(license: string): boolean {
try {
spdxParse(license)
return true
} catch (_) {
return false
}
//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)
}
-8
View File
@@ -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. */
}
}
+3 -4
View File
@@ -3,11 +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'. */,
"typeRoots": [ "./node_modules/@types", "./types" ],
"types": [ "node", "jest", "spdx-license-satisfies" ]
"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"]
}
-16
View File
@@ -1,16 +0,0 @@
declare module '@onebeyond/spdx-license-satisfies' {
export function satisfies(
candidateExpr: string,
constraintExpr: string
): boolean
export function satisfiesAny(
candidateExpr: string,
licenses: string[]
): boolean
export function satisfiesAll(
candidateExpr: string,
licenses: string[]
): boolean
}