merge from main and fix code review comment from @juxtin
This commit is contained in:
@@ -21,12 +21,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
- name: Set Node.js 20.x
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
id: diff
|
||||
|
||||
# If index.js was different than expected, upload the expected version as an artifact
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||
with:
|
||||
name: dist
|
||||
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
@@ -27,10 +27,10 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@main
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Close stale PRs
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "00 0 * * *" # runs at 00:00 daily
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9.0.0
|
||||
name: Clean up stale PRs
|
||||
with:
|
||||
stale-pr-message: "👋 This pull request has been marked as stale because it has been open with no activity. You can: comment on the issue or remove the stale label to hold stale off for a while, add the `Keep` label to hold stale off permanently, or do nothing. If you do nothing, this pull request will be closed eventually by the stale bot. Please see CONTRIBUTING.md for more policy details."
|
||||
stale-pr-label: "Stale"
|
||||
exempt-pr-labels: "Keep" # a "Keep" label will keep the PR 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: -1 # prevents issues from being tagged by the bot
|
||||
days-before-issue-close: -1 # prevents issues from being closed by the bot
|
||||
exempt-assignees: 'advanced-security-dependency-graph'
|
||||
ascending: true
|
||||
+3
-3
@@ -79,7 +79,7 @@ Here are a few things you can do that will increase the likelihood of your pull
|
||||
|
||||
- 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](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
- Write a [good commit message](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
|
||||
## Cutting a new release
|
||||
|
||||
@@ -112,8 +112,8 @@ minor/patch updates.
|
||||
To do this just checkout `main`, force-create a new annotated tag, and push it:
|
||||
|
||||
```
|
||||
git tag -fa v3 -m "Updating v3 to 3.0.1"
|
||||
git push origin v3 --force
|
||||
git tag -fa v4 -m "Updating v4 to 4.0.1"
|
||||
git push origin v4 --force
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -31,9 +31,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
```
|
||||
|
||||
### GitHub Enterprise Server
|
||||
@@ -43,7 +43,7 @@ This action is available in Enterprise Server starting with version 3.6. Make su
|
||||
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.
|
||||
are enabled, and that you have installed the [dependency-review-action](https://github.com/actions/dependency-review-action) on the server.
|
||||
|
||||
You can use the same workflow as above, replacing the `runs-on` value
|
||||
with the label of any of your runners (the default label
|
||||
@@ -57,28 +57,32 @@ jobs:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
```
|
||||
|
||||
## Configuration options
|
||||
|
||||
Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional.
|
||||
|
||||
| Option | Usage | Possible values | Default value |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- |
|
||||
| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` |
|
||||
| `allow-licenses`\* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||
| `deny-licenses`\* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||
| `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. | `runtime`, `development`, `unknown` | `runtime` |
|
||||
| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none |
|
||||
| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` |
|
||||
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
|
||||
| `allow-dependencies-licenses`\* | Contains a list of packages that will be excluded from license checks. | Any package(s) in [purl](https://github.com/package-url/purl-spec) format | none |
|
||||
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
|
||||
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `true`, `false` | `false` |
|
||||
| `warn-only`+ | Enable or disable the action to fail, when set to `true` the action will log all vulnarabilities as warning. | `true`, `false` | `false` |
|
||||
| Option | Usage | Possible values | Default value |
|
||||
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------- |
|
||||
| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` |
|
||||
| `allow-licenses`\* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||
| `deny-licenses`\* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
|
||||
| `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. | `runtime`, `development`, `unknown` | `runtime` |
|
||||
| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none |
|
||||
| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` |
|
||||
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
|
||||
| `allow-dependencies-licenses`\* | Contains a list of packages that will be excluded from license checks. | Any package(s) in [purl](https://github.com/package-url/purl-spec) format | none |
|
||||
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
|
||||
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `always`, `on-failure`, `never` | `never` |
|
||||
| `deny-packages` | Any number of packages to block in a PR. | Package(s) in [purl](https://github.com/package-url/purl-spec) format | empty |
|
||||
| `deny-groups` | Any number of groups (namespaces) to block in a PR. | Namespace(s) in [purl](https://github.com/package-url/purl-spec) format (no package name, no version number) | empty |
|
||||
| `retry-on-snapshot-warnings`\* | Enable or disable retrying the action every 10 seconds while waiting for dependency submission actions to complete. | `true`, `false` | `false` |
|
||||
| `retry-on-snapshot-warnings-timeout`\* | Maximum amount of time (in seconds) to retry the action while waiting for dependency submission actions to complete. | Any positive integer | 120 |
|
||||
| `warn-only`+ | Enable or disable the action to fail, when set to `true` the action will log all vulnarabilities as warning. | `true`, `false` | `false` |
|
||||
|
||||
\*not supported for use with GitHub Enterprise Server
|
||||
|
||||
@@ -103,9 +107,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
|
||||
@@ -128,7 +132,7 @@ Start by specifying that you will be using an external configuration file:
|
||||
|
||||
```yaml
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
config-file: './.github/dependency-review-config.yml'
|
||||
```
|
||||
@@ -148,7 +152,7 @@ For more examples of how to use this action and its configuration options, see t
|
||||
|
||||
### Considerations
|
||||
|
||||
- Checking for licenses is not supported on Enterprise Server.
|
||||
- Checking for licenses is not supported on Enterprise Server as the API does not return license information.
|
||||
- The action will only accept one of the two `license` parameters; an error will be raised if you provide both.
|
||||
- We don't have license information for all of your dependents. If we can't detect the license for a dependency **we will inform you, but the action won't fail**.
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ test('it reads custom configs', async () => {
|
||||
})
|
||||
|
||||
test('it defaults to false for warn-only', async () => {
|
||||
setInput('warn-only', 'false')
|
||||
const config = await readConfig()
|
||||
expect(config.warn_only).toEqual(false)
|
||||
})
|
||||
@@ -177,3 +176,29 @@ describe('licenses that are not valid SPDX licenses', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import {expect, jest, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
|
||||
let getDeniedChanges: Function
|
||||
|
||||
const npmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
name: 'Reeuhq',
|
||||
version: '1.0.2',
|
||||
package_url: 'pkg:npm/reeuhq@1.0.2',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'first-random_string',
|
||||
advisory_summary: 'very dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const rubyChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'Gemfile.lock',
|
||||
ecosystem: 'rubygems',
|
||||
name: 'actionsomething',
|
||||
version: '3.2.0',
|
||||
package_url: 'pkg:gem/actionsomething@3.2.0',
|
||||
license: 'BSD',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
},
|
||||
{
|
||||
severity: 'low',
|
||||
advisory_ghsa_id: 'third-random_string',
|
||||
advisory_summary: 'dont page me',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const pipChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'requirements.txt',
|
||||
ecosystem: 'pip',
|
||||
name: 'package-1',
|
||||
version: '1.1.1',
|
||||
package_url: 'pkg:pypi/package-1@1.1.1',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
},
|
||||
{
|
||||
severity: 'low',
|
||||
advisory_ghsa_id: 'third-random_string',
|
||||
advisory_summary: 'dont page me',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const mvnChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'pom.xml',
|
||||
ecosystem: 'maven',
|
||||
name: 'org.apache.logging.log4j:log4j-core',
|
||||
version: '2.15.0',
|
||||
package_url: 'pkg:maven/org.apache.logging.log4j/log4j-core@2.14.7',
|
||||
license: 'Apache-2.0',
|
||||
source_repository_url:
|
||||
'https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core',
|
||||
scope: 'unknown',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
jest.mock('@actions/core')
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
licenses: {
|
||||
getForRepo: jest
|
||||
.fn()
|
||||
.mockReturnValue({data: {license: {spdx_id: 'AGPL'}}})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('octokit', () => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
Octokit: class {
|
||||
constructor() {
|
||||
return mockOctokit
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetModules()
|
||||
jest.doMock('spdx-satisfies', () => {
|
||||
// mock spdx-satisfies return value
|
||||
// true for BSD, false for all others
|
||||
return jest.fn((license: string, _: string): boolean => license === 'BSD')
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
;({getDeniedChanges} = require('../src/deny'))
|
||||
})
|
||||
|
||||
test('it adds packages in the deny packages list', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const deniedChanges = await getDeniedChanges(
|
||||
changes,
|
||||
['pkg:gem/actionsomething'],
|
||||
[]
|
||||
)
|
||||
|
||||
expect(deniedChanges[0]).toBe(rubyChange)
|
||||
expect(deniedChanges.length).toEqual(1)
|
||||
})
|
||||
|
||||
test('it adds packages in the deny group list', async () => {
|
||||
const changes: Changes = [mvnChange, rubyChange]
|
||||
const deniedChanges = await getDeniedChanges(
|
||||
changes,
|
||||
[],
|
||||
['pkg:maven/org.apache.logging.log4j']
|
||||
)
|
||||
|
||||
expect(deniedChanges[0]).toBe(mvnChange)
|
||||
expect(deniedChanges.length).toEqual(1)
|
||||
})
|
||||
|
||||
test('it adds packages outside of the deny lists', async () => {
|
||||
const changes: Changes = [npmChange, pipChange]
|
||||
const deniedChanges = await getDeniedChanges(
|
||||
changes,
|
||||
['pkg:gem/actionsomething'],
|
||||
['pkg:maven:org.apache.logging.log4j']
|
||||
)
|
||||
|
||||
expect(deniedChanges.length).toEqual(0)
|
||||
})
|
||||
+111
-17
@@ -19,7 +19,7 @@ const npmChange: Change = {
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'first-random_string',
|
||||
advisory_ghsa_id: 'vulnerable-ghsa-id',
|
||||
advisory_summary: 'very dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
@@ -39,13 +39,13 @@ const rubyChange: Change = {
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_ghsa_id: 'moderate-ghsa-id',
|
||||
advisory_summary: 'not so dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
},
|
||||
{
|
||||
severity: 'low',
|
||||
advisory_ghsa_id: 'third-random_string',
|
||||
advisory_ghsa_id: 'low-ghsa-id',
|
||||
advisory_summary: 'dont page me',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
@@ -65,6 +65,64 @@ 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)
|
||||
@@ -99,25 +157,61 @@ test('it properly handles undefined advisory IDs', async () => {
|
||||
test('it properly filters changes with allowed vulnerabilities', async () => {
|
||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
||||
|
||||
let result = filterAllowedAdvisories(['notrealGHSAID'], changes)
|
||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
const fakeGHSAChanges = filterAllowedAdvisories(['notrealGHSAID'], changes)
|
||||
expect(fakeGHSAChanges).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
})
|
||||
|
||||
result = filterAllowedAdvisories(['first-random_string'], changes)
|
||||
expect(result).toEqual([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(
|
||||
['second-random_string', 'third-random_string'],
|
||||
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'],
|
||||
changes
|
||||
)
|
||||
expect(result).toEqual([npmChange, noVulnNpmChange])
|
||||
|
||||
result = filterAllowedAdvisories(
|
||||
['first-random_string', 'second-random_string', 'third-random_string'],
|
||||
changes
|
||||
expect(result.map(change => change.name)).toEqual(
|
||||
changes.map(change => change.name)
|
||||
)
|
||||
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])
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ const pipChange: Change = {
|
||||
ecosystem: 'pip',
|
||||
name: 'package-1',
|
||||
version: '1.1.1',
|
||||
package_url: 'pkg:pip/package-1@1.1.1',
|
||||
package_url: 'pkg:pypi/package-1@1.1.1',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
@@ -183,7 +183,7 @@ test('it does not filter out changes that are on the exclusions list', async ()
|
||||
const changes: Changes = [pipChange, npmChange, rubyChange]
|
||||
const licensesConfig = {
|
||||
allow: ['BSD'],
|
||||
licenseExclusions: ['pkg:pip/package-1@1.1.1', 'pkg:npm/reeuhq@1.0.2']
|
||||
licenseExclusions: ['pkg:pypi/package-1@1.1.1', 'pkg:npm/reeuhq@1.0.2']
|
||||
}
|
||||
const invalidLicenses = await getInvalidLicenseChanges(
|
||||
changes,
|
||||
@@ -199,7 +199,7 @@ test('it does not fail when the packages dont have a valid PURL', async () => {
|
||||
const changes: Changes = [emptyPurlChange, npmChange, rubyChange]
|
||||
const licensesConfig = {
|
||||
allow: ['BSD'],
|
||||
licenseExclusions: ['pkg:pip/package-1@1.1.1', 'pkg:npm/reeuhq@1.0.2']
|
||||
licenseExclusions: ['pkg:pypi/package-1@1.1.1', 'pkg:npm/reeuhq@1.0.2']
|
||||
}
|
||||
|
||||
const invalidLicenses = await getInvalidLicenseChanges(
|
||||
@@ -213,7 +213,10 @@ test('it does filters out changes if they are not on the exclusions list', async
|
||||
const changes: Changes = [pipChange, npmChange, rubyChange]
|
||||
const licensesConfig = {
|
||||
allow: ['BSD'],
|
||||
licenseExclusions: ['pkg:pip/notmypackage-1@1.1.1', 'pkg:npm/alsonot@1.0.2']
|
||||
licenseExclusions: [
|
||||
'pkg:pypi/notmypackage-1@1.1.1',
|
||||
'pkg:npm/alsonot@1.0.2'
|
||||
]
|
||||
}
|
||||
const invalidLicenses = await getInvalidLicenseChanges(
|
||||
changes,
|
||||
|
||||
@@ -24,7 +24,11 @@ const defaultConfig: ConfigurationOptions = {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -71,6 +75,7 @@ test('prints headline as h1', () => {
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
defaultConfig
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
@@ -82,6 +87,7 @@ test('only includes "No vulnerabilities or license issues found"-message if both
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
defaultConfig
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
@@ -91,7 +97,12 @@ test('only includes "No vulnerabilities or license issues found"-message if both
|
||||
|
||||
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, config)
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
config
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
|
||||
expect(text).toContain('✅ No vulnerabilities found.')
|
||||
@@ -99,7 +110,12 @@ test('only includes "No vulnerabilities found"-message if "license_check" is set
|
||||
|
||||
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, config)
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
config
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
|
||||
expect(text).toContain('✅ No license issues found.')
|
||||
@@ -109,6 +125,7 @@ test('groups dependencies with empty manifest paths together', () => {
|
||||
summary.addSummaryToSummary(
|
||||
changesWithEmptyManifests,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
defaultConfig
|
||||
)
|
||||
summary.addScannedDependencies(changesWithEmptyManifests)
|
||||
@@ -125,6 +142,7 @@ test('does not include status section if nothing was found', () => {
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
defaultConfig
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
@@ -143,7 +161,12 @@ test('includes count and status icons for all findings', () => {
|
||||
unlicensed: [createTestChange(), createTestChange(), createTestChange()]
|
||||
}
|
||||
|
||||
summary.addSummaryToSummary(vulnerabilities, licenseIssues, defaultConfig)
|
||||
summary.addSummaryToSummary(
|
||||
vulnerabilities,
|
||||
licenseIssues,
|
||||
emptyChanges,
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('❌ 2 vulnerable package(s)')
|
||||
@@ -160,6 +183,7 @@ test('uses checkmarks for license issues if only vulnerabilities were found', ()
|
||||
summary.addSummaryToSummary(
|
||||
vulnerabilities,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
@@ -179,7 +203,12 @@ test('uses checkmarks for vulnerabilities if only license issues were found', ()
|
||||
unlicensed: []
|
||||
}
|
||||
|
||||
summary.addSummaryToSummary(emptyChanges, licenseIssues, defaultConfig)
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
licenseIssues,
|
||||
emptyChanges,
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('✅ 0 vulnerable package(s)')
|
||||
|
||||
+17
-3
@@ -30,7 +30,7 @@ inputs:
|
||||
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:pip/pycrypto"). These dependencies will be permitted to use any license, no matter what license policy is enforced otherwise.
|
||||
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")
|
||||
@@ -45,13 +45,27 @@ inputs:
|
||||
description: A boolean to determine if vulnerability checks should be performed
|
||||
required: false
|
||||
comment-summary-in-pr:
|
||||
description: A boolean to determine if the report should be posted as a comment in the PR itself. Setting this to true requires you to give the workflow the write permissions for pull-requests
|
||||
description: Determines if the summary is posted as a comment in the PR itself. Setting this to `always` or `on-failure` requires you to give the workflow the write permissions for pull-requests
|
||||
required: false
|
||||
deny-packages:
|
||||
description: A comma-separated list of package URLs to deny (e.g. "pkg:npm/express, pkg:pypi/pycrypto")
|
||||
required: false
|
||||
deny-groups:
|
||||
description: A comma-separated list of package URLs for group(s)/namespace(s) to deny (e.g. "pkg:npm/express, pkg:pypi/pycrypto")
|
||||
required: false
|
||||
retry-on-snapshot-warnings:
|
||||
description: Whether to retry on snapshot warnings
|
||||
required: false
|
||||
default: false
|
||||
retry-on-snapshot-warnings-timeout:
|
||||
description: Number of seconds to wait before stopping snapshot retries.
|
||||
required: false
|
||||
default: 120
|
||||
warn-only:
|
||||
description: When set to `true` this action will not make the action to fail, this override the `fail-on-severity` parameter.
|
||||
required: false
|
||||
default: false
|
||||
|
||||
runs:
|
||||
using: 'node16'
|
||||
using: 'node20'
|
||||
main: 'dist/index.js'
|
||||
|
||||
-57316
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-1687
File diff suppressed because it is too large
Load Diff
-1
File diff suppressed because one or more lines are too long
+88
-24
@@ -18,9 +18,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
```
|
||||
|
||||
## Using an inline configuration
|
||||
@@ -39,9 +39,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
@@ -76,14 +76,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
config-file: './.github/dependency-review-config.yml'
|
||||
```
|
||||
|
||||
## Using a configuration file from a external repository
|
||||
## 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.
|
||||
|
||||
@@ -103,14 +103,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
config-file: 'github/octorepo/dependency-review-config.yml@main'
|
||||
```
|
||||
|
||||
## Using a configuration file from a external repository with a personal access token
|
||||
## 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.
|
||||
|
||||
@@ -130,12 +130,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
config-file: 'github/octorepo-private/dependency-review-config.yml@main'
|
||||
config-file-token: ${{ secrets.GITHUB_TOKEN }} # or a personal access token
|
||||
external-repo-token: ${{ secrets.GITHUB_TOKEN }} # or a personal access token
|
||||
```
|
||||
|
||||
## Getting the results of the action in the PR as a comment
|
||||
@@ -155,13 +155,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
comment-summary-in-pr: true
|
||||
comment-summary-in-pr: always
|
||||
```
|
||||
|
||||
## Exclude dependencies from the license check
|
||||
@@ -183,14 +183,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
comment-summary-in-pr: true
|
||||
allow-dependencies-licenses: 'pkg:npm/loadash, pkg:pip/requests'
|
||||
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:
|
||||
@@ -202,7 +202,7 @@ allow-licenses:
|
||||
- 'BSD-2-Clause'
|
||||
allow-dependencies-licenses:
|
||||
- 'pkg:npm/loadash'
|
||||
- 'pkg:pip/requests'
|
||||
- 'pkg:pypi/requests'
|
||||
```
|
||||
|
||||
## Only check for vulnerabilities
|
||||
@@ -222,11 +222,75 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
comment-summary-in-pr: true
|
||||
comment-summary-in-pr: always
|
||||
license-check: false
|
||||
```
|
||||
|
||||
## Exclude dependencies from their name or groups
|
||||
|
||||
Using the `deny-packages` option you can exclude dependencies by their PURL. You can add multiple values separated by a commas.
|
||||
Using the `deny-groups` option you can exclude dependencies by their group name/namespace. You can add multiple values separated by a comma.
|
||||
|
||||
In this example, we are excluding `pkg:maven/org.apache.logging.log4j:log4j-api` and `pkg:maven/org.apache.logging.log4j/log4j-core` from `maven` and all packages in the group `pkg:maven/com.bazaarvoice.maven`
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
deny-packages: 'pkg:maven/org.apache.logging.log4j/log4j-api,pkg:maven/org.apache.logging.log4j/log4j-core'
|
||||
deny-groups: 'pkg:maven/com.bazaarvoice.jolt'
|
||||
```
|
||||
|
||||
## Waiting for dependency submission jobs to complete
|
||||
|
||||
When possible, this action will [include dependencies submitted through the dependency submission API][DSAPI]. In this case,
|
||||
it's important for the action not to complete until all of the relevant dependencies have been submitted for both the base
|
||||
and head commits.
|
||||
|
||||
When this action runs before one or more of the dependency submission actions, there will be an unequal number of dependency
|
||||
snapshots between the base and head commits. For example, there may be one snapshot available for the tip of `main` and none
|
||||
for the PR branch. In that case, the API response will contain a "snapshot warning" explaining the discrepancy.
|
||||
|
||||
In this example, when the action encounters one of these warnings it will retry every 10 seconds after that for 60 seconds
|
||||
or until there is no warning in the response.
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
retry-on-snapshot-warnings: true
|
||||
retry-on-snapshot-warnings-timeout: 60
|
||||
```
|
||||
|
||||
[DSAPI]: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#best-practices-for-using-the-dependency-review-api-and-the-dependency-submission-api-together
|
||||
|
||||
Generated
+2819
-3211
File diff suppressed because it is too large
Load Diff
+27
-29
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dependency-review-action",
|
||||
"version": "3.0.6",
|
||||
"version": "4.0.0",
|
||||
"private": true,
|
||||
"description": "A GitHub Action for Dependency Review",
|
||||
"main": "lib/main.js",
|
||||
@@ -25,39 +25,37 @@
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1",
|
||||
"@octokit/plugin-retry": "^5.0.2",
|
||||
"@octokit/request-error": "^2.1.0",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@octokit/plugin-retry": "^6.0.1",
|
||||
"@octokit/request-error": "^5.0.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"got": "^13.0.0",
|
||||
"nodemon": "^2.0.22",
|
||||
"octokit": "^2.0.19",
|
||||
"packageurl-js": "^1.0.2",
|
||||
"got": "^14.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"octokit": "^3.1.2",
|
||||
"packageurl-js": "^1.2.0",
|
||||
"spdx-expression-parse": "^3.0.1",
|
||||
"spdx-satisfies": "^5.0.1",
|
||||
"yaml": "^2.3.1",
|
||||
"zod": "^3.21.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"yaml": "^2.3.4",
|
||||
"zod": "^3.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.35",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@types/spdx-expression-parse": "^3.0.2",
|
||||
"@types/spdx-satisfies": "^0.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.9",
|
||||
"@typescript-eslint/parser": "^5.59.9",
|
||||
"@vercel/ncc": "^0.36.1",
|
||||
"esbuild-register": "^3.4.2",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-plugin-github": "^4.8.0",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"jest": "^27.5.1",
|
||||
"@types/node": "^20",
|
||||
"@types/spdx-expression-parse": "^3.0.4",
|
||||
"@types/spdx-satisfies": "^0.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"@vercel/ncc": "^0.38.0",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-github": "^4.10.1",
|
||||
"eslint-plugin-jest": "^27.6.3",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"nodemon": "^2.0.22",
|
||||
"prettier": "2.8.8",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.9.5"
|
||||
"nodemon": "^3.0.2",
|
||||
"prettier": "3.1.1",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* npx ts-node scripts/create_summary.ts
|
||||
*/
|
||||
|
||||
import {Changes, ConfigurationOptions} from '../src/schemas'
|
||||
import {Change, Changes, ConfigurationOptions} from '../src/schemas'
|
||||
import {createTestChange} from '../__tests__/fixtures/create-test-change'
|
||||
import {InvalidLicenseChanges} from '../src/licenses'
|
||||
import * as fs from 'fs'
|
||||
@@ -22,13 +22,17 @@ const defaultConfig: ConfigurationOptions = {
|
||||
allow_ghsas: [],
|
||||
allow_licenses: ['MIT'],
|
||||
deny_licenses: [],
|
||||
deny_packages: [],
|
||||
deny_groups: [],
|
||||
allow_dependencies_licenses: [
|
||||
'pkg:npm/express@4.17.1',
|
||||
'pkg:pip/requests',
|
||||
'pkg:pip/certifi',
|
||||
'pkg:pip/pycrypto@2.6.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
|
||||
}
|
||||
|
||||
@@ -45,6 +49,7 @@ const createNonIssueSummary = async (): Promise<void> => {
|
||||
await createSummary(
|
||||
[],
|
||||
{forbidden: [], unresolved: [], unlicensed: []},
|
||||
[],
|
||||
defaultConfig,
|
||||
'non-issue-summary.md'
|
||||
)
|
||||
@@ -86,16 +91,17 @@ const createFullSummary = async (): Promise<void> => {
|
||||
]
|
||||
}
|
||||
|
||||
await createSummary(changes, licenses, defaultConfig, 'full-summary.md')
|
||||
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, config)
|
||||
summary.addSummaryToSummary(vulnerabilities, licenseIssues, denied, config)
|
||||
summary.addChangeVulnerabilitiesToSummary(
|
||||
vulnerabilities,
|
||||
config.fail_on_severity
|
||||
|
||||
+2
-2
@@ -74,8 +74,8 @@ async function findCommentByMarker(
|
||||
)
|
||||
|
||||
for await (const {data: comments} of commentsIterator) {
|
||||
const existingComment = comments.find(comment =>
|
||||
comment.body?.includes(commentBodyIncludes)
|
||||
const existingComment = comments.find(
|
||||
comment => comment.body?.includes(commentBodyIncludes)
|
||||
)
|
||||
if (existingComment) return existingComment.id
|
||||
}
|
||||
|
||||
+22
-2
@@ -33,12 +33,20 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
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 = getOptionalBoolean('comment-summary-in-pr')
|
||||
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')
|
||||
|
||||
validatePURL(allow_dependencies_licenses)
|
||||
@@ -50,6 +58,8 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
fail_on_scopes,
|
||||
allow_licenses,
|
||||
deny_licenses,
|
||||
deny_packages,
|
||||
deny_groups,
|
||||
allow_dependencies_licenses,
|
||||
allow_ghsas,
|
||||
license_check,
|
||||
@@ -57,6 +67,8 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
base_ref,
|
||||
head_ref,
|
||||
comment_summary_in_pr,
|
||||
retry_on_snapshot_warnings,
|
||||
retry_on_snapshot_warnings_timeout,
|
||||
warn_only
|
||||
}
|
||||
|
||||
@@ -65,6 +77,12 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function getOptionalBoolean(name: string): boolean | undefined {
|
||||
const value = core.getInput(name)
|
||||
return value.length > 0 ? core.getBooleanInput(name) : undefined
|
||||
@@ -139,7 +157,9 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
|
||||
'deny-licenses',
|
||||
'fail-on-scopes',
|
||||
'allow-ghsas',
|
||||
'allow-dependencies-licenses'
|
||||
'allow-dependencies-licenses',
|
||||
'deny-packages',
|
||||
'deny-groups'
|
||||
]
|
||||
|
||||
for (const key of Object.keys(data)) {
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import {Change} from './schemas'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
export async function getDeniedChanges(
|
||||
changes: Change[],
|
||||
deniedPackages: string[],
|
||||
deniedGroups: string[]
|
||||
): Promise<Change[]> {
|
||||
const changesDenied: Change[] = []
|
||||
|
||||
let failed = false
|
||||
for (const change of changes) {
|
||||
change.name = change.name.toLowerCase()
|
||||
const packageUrl = change.package_url.toLowerCase().split('@')[0]
|
||||
|
||||
if (deniedPackages) {
|
||||
for (const denied of deniedPackages) {
|
||||
if (packageUrl === denied.split('@')[0].toLowerCase()) {
|
||||
changesDenied.push(change)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deniedGroups) {
|
||||
for (const denied of deniedGroups) {
|
||||
if (packageUrl.startsWith(denied.toLowerCase())) {
|
||||
changesDenied.push(change)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
core.setFailed('Dependency review detected denied packages.')
|
||||
} else {
|
||||
core.info('Dependency review did not detect any denied packages')
|
||||
}
|
||||
|
||||
return changesDenied
|
||||
}
|
||||
@@ -31,7 +31,8 @@ export async function compare({
|
||||
url: '/repos/{owner}/{repo}/dependency-graph/compare/{basehead}',
|
||||
owner,
|
||||
repo,
|
||||
basehead: `${baseRef}...${headRef}`
|
||||
basehead: `${baseRef}...${headRef}`,
|
||||
per_page: 5
|
||||
},
|
||||
response => {
|
||||
if (
|
||||
|
||||
+23
-13
@@ -1,5 +1,13 @@
|
||||
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
|
||||
@@ -31,7 +39,14 @@ export function filterChangesBySeverity(
|
||||
filteredChanges = filteredChanges.filter(
|
||||
change => change.vulnerabilities.length > 0
|
||||
)
|
||||
return filteredChanges
|
||||
|
||||
// only report vulnerability additions
|
||||
return filteredChanges.filter(
|
||||
change =>
|
||||
change.change_type === 'added' &&
|
||||
change.vulnerabilities !== undefined &&
|
||||
change.vulnerabilities.length > 0
|
||||
)
|
||||
}
|
||||
|
||||
export function filterChangesByScopes(
|
||||
@@ -67,25 +82,20 @@ export function filterAllowedAdvisories(
|
||||
return changes
|
||||
}
|
||||
|
||||
const filteredChanges = changes.filter(change => {
|
||||
const filteredChanges = changes.map(change => {
|
||||
const noAdvisories =
|
||||
change.vulnerabilities === undefined ||
|
||||
change.vulnerabilities.length === 0
|
||||
|
||||
if (noAdvisories) {
|
||||
return true
|
||||
return change
|
||||
}
|
||||
const newChange = {...change}
|
||||
newChange.vulnerabilities = change.vulnerabilities.filter(
|
||||
vuln => !ghsas.includes(vuln.advisory_ghsa_id)
|
||||
)
|
||||
|
||||
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 newChange
|
||||
})
|
||||
|
||||
return filteredChanges
|
||||
|
||||
+4
-2
@@ -32,7 +32,7 @@ export async function getInvalidLicenseChanges(
|
||||
const {allow, deny} = licenses
|
||||
const licenseExclusions = licenses.licenseExclusions?.map(
|
||||
(pkgUrl: string) => {
|
||||
return PackageURL.fromString(pkgUrl)
|
||||
return PackageURL.fromString(encodeURI(pkgUrl))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -45,7 +45,9 @@ export async function getInvalidLicenseChanges(
|
||||
return true
|
||||
}
|
||||
|
||||
const changeAsPackageURL = PackageURL.fromString(change.package_url)
|
||||
const changeAsPackageURL = PackageURL.fromString(
|
||||
encodeURI(change.package_url)
|
||||
)
|
||||
|
||||
// We want to find if the licenseExclussion list contains the PackageURL of the Change
|
||||
// If it does, we want to filter it out and therefore return false
|
||||
|
||||
+104
-43
@@ -3,7 +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} from './schemas'
|
||||
import {Change, Severity, Changes, ConfigurationOptions} from './schemas'
|
||||
import {readConfig} from '../src/config'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
@@ -16,6 +16,43 @@ import {getRefs} from './git-refs'
|
||||
|
||||
import {groupDependenciesByManifest} from './utils'
|
||||
import {commentPr} from './comment-pr'
|
||||
import {getDeniedChanges} from './deny'
|
||||
|
||||
async function delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
async function getComparison(
|
||||
baseRef: string,
|
||||
headRef: string,
|
||||
retryOpts?: {
|
||||
retryUntil: number
|
||||
retryDelay: number
|
||||
}
|
||||
): ReturnType<typeof dependencyGraph.compare> {
|
||||
const comparison = await dependencyGraph.compare({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
baseRef,
|
||||
headRef
|
||||
})
|
||||
|
||||
if (comparison.snapshot_warnings.trim() !== '') {
|
||||
core.info(comparison.snapshot_warnings)
|
||||
if (retryOpts !== undefined) {
|
||||
if (retryOpts.retryUntil < Date.now()) {
|
||||
core.info(`Retry timeout exceeded. Proceeding...`)
|
||||
return comparison
|
||||
} else {
|
||||
core.info(`Retrying in ${retryOpts.retryDelay} seconds...`)
|
||||
await delay(retryOpts.retryDelay * 1000)
|
||||
return getComparison(baseRef, headRef, retryOpts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return comparison
|
||||
}
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
@@ -23,12 +60,18 @@ async function run(): Promise<void> {
|
||||
|
||||
const refs = getRefs(config, github.context)
|
||||
|
||||
const comparison = await dependencyGraph.compare({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
baseRef: refs.base,
|
||||
headRef: refs.head
|
||||
})
|
||||
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
|
||||
|
||||
@@ -37,28 +80,24 @@ async function run(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
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 scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
|
||||
|
||||
const filteredChanges = filterAllowedAdvisories(
|
||||
config.allow_ghsas,
|
||||
scopedChanges
|
||||
)
|
||||
|
||||
let minSeverity = config.fail_on_severity
|
||||
const failOnSeverityParams = config.fail_on_severity
|
||||
const warnOnly = config.warn_only
|
||||
// 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(
|
||||
minSeverity,
|
||||
filteredChanges
|
||||
).filter(
|
||||
change =>
|
||||
change.change_type === 'added' &&
|
||||
change.vulnerabilities !== undefined &&
|
||||
change.vulnerabilities.length > 0
|
||||
)
|
||||
|
||||
const invalidLicenseChanges = await getInvalidLicenseChanges(
|
||||
@@ -70,28 +109,46 @@ async function run(): Promise<void> {
|
||||
}
|
||||
)
|
||||
|
||||
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(
|
||||
vulnerableChanges,
|
||||
invalidLicenseChanges,
|
||||
deniedChanges,
|
||||
config
|
||||
)
|
||||
|
||||
if (snapshot_warnings) {
|
||||
summary.addSnapshotWarnings(snapshot_warnings)
|
||||
summary.addSnapshotWarnings(config, snapshot_warnings)
|
||||
}
|
||||
|
||||
if (config.vulnerability_check) {
|
||||
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
|
||||
printVulnerabilitiesBlock(vulnerableChanges, minSeverity, warnOnly)
|
||||
printVulnerabilitiesBlock(vulnerableChanges, minSeverity)
|
||||
}
|
||||
if (config.license_check) {
|
||||
summary.addLicensesToSummary(invalidLicenseChanges, config)
|
||||
printLicensesBlock(invalidLicenseChanges, warnOnly)
|
||||
printLicensesBlock(invalidLicenseChanges)
|
||||
}
|
||||
if (config.deny_packages || config.deny_groups) {
|
||||
summary.addDeniedToSummary(deniedChanges)
|
||||
printDeniedDependencies(deniedChanges, config)
|
||||
}
|
||||
|
||||
summary.addScannedDependencies(changes)
|
||||
printScannedDependencies(changes)
|
||||
if (config.comment_summary_in_pr) {
|
||||
if (
|
||||
config.comment_summary_in_pr === 'always' ||
|
||||
(config.comment_summary_in_pr === 'on-failure' &&
|
||||
process.exitCode === core.ExitCode.Failure)
|
||||
) {
|
||||
await commentPr(core.summary)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -117,25 +174,19 @@ async function run(): Promise<void> {
|
||||
|
||||
function printVulnerabilitiesBlock(
|
||||
addedChanges: Changes,
|
||||
minSeverity: Severity,
|
||||
warnOnly: boolean
|
||||
minSeverity: Severity
|
||||
): void {
|
||||
let vulFound = false
|
||||
let failed = false
|
||||
core.group('Vulnerabilities', async () => {
|
||||
if (addedChanges.length > 0) {
|
||||
for (const change of addedChanges) {
|
||||
printChangeVulnerabilities(change)
|
||||
}
|
||||
vulFound = true
|
||||
failed = true
|
||||
}
|
||||
|
||||
if (vulFound) {
|
||||
const msg = 'Dependency review detected vulnerable packages.'
|
||||
if (warnOnly) {
|
||||
core.warning(msg)
|
||||
} else {
|
||||
core.setFailed(msg)
|
||||
}
|
||||
if (failed) {
|
||||
core.setFailed('Dependency review detected vulnerable packages.')
|
||||
} else {
|
||||
core.info(
|
||||
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`
|
||||
@@ -158,19 +209,13 @@ function printChangeVulnerabilities(change: Change): void {
|
||||
}
|
||||
|
||||
function printLicensesBlock(
|
||||
invalidLicenseChanges: Record<string, Changes>,
|
||||
warnOnly: boolean
|
||||
invalidLicenseChanges: Record<string, Changes>
|
||||
): void {
|
||||
core.group('Licenses', async () => {
|
||||
if (invalidLicenseChanges.forbidden.length > 0) {
|
||||
core.info('\nThe following dependencies have incompatible licenses:')
|
||||
printLicensesError(invalidLicenseChanges.forbidden)
|
||||
const msg = 'Dependency review detected incompatible licenses.'
|
||||
if (warnOnly) {
|
||||
core.warning(msg)
|
||||
} else {
|
||||
core.setFailed(msg)
|
||||
}
|
||||
core.setFailed('Dependency review detected incompatible licenses.')
|
||||
}
|
||||
if (invalidLicenseChanges.unresolved.length > 0) {
|
||||
core.warning(
|
||||
@@ -258,4 +303,20 @@ function printScannedDependencies(changes: Changes): void {
|
||||
})
|
||||
}
|
||||
|
||||
function printDeniedDependencies(
|
||||
changes: Change[],
|
||||
config: ConfigurationOptions
|
||||
): void {
|
||||
core.group('Denied', async () => {
|
||||
for (const denied of config.deny_packages) {
|
||||
core.info(`Config: ${denied}`)
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
core.info(`Change: ${change.name}@${change.version} is denied`)
|
||||
core.info(`Change: ${change.package_url} is denied`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
+22
-10
@@ -1,15 +1,8 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
export const FAIL_ON_SEVERITIES = [
|
||||
'critical',
|
||||
'high',
|
||||
'moderate',
|
||||
'low'
|
||||
] as const
|
||||
export const SEVERITIES = ['critical', 'high', 'moderate', 'low'] as const
|
||||
export const SCOPES = ['unknown', 'runtime', 'development'] as const
|
||||
|
||||
export const FailOnSeveritySchema = z.enum(FAIL_ON_SEVERITIES).default('low')
|
||||
export const SeveritySchema = z.enum(SEVERITIES).default('low')
|
||||
|
||||
export const ChangeSchema = z.object({
|
||||
@@ -43,20 +36,40 @@ export const PullRequestSchema = z.object({
|
||||
|
||||
export const ConfigurationOptionsSchema = z
|
||||
.object({
|
||||
fail_on_severity: FailOnSeveritySchema,
|
||||
fail_on_severity: SeveritySchema,
|
||||
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
||||
allow_licenses: z.array(z.string()).optional(),
|
||||
deny_licenses: z.array(z.string()).optional(),
|
||||
allow_dependencies_licenses: z.array(z.string()).optional(),
|
||||
allow_ghsas: z.array(z.string()).default([]),
|
||||
deny_packages: z.array(z.string()).default([]),
|
||||
deny_groups: z.array(z.string()).default([]),
|
||||
license_check: z.boolean().default(true),
|
||||
vulnerability_check: z.boolean().default(true),
|
||||
config_file: z.string().optional(),
|
||||
base_ref: z.string().optional(),
|
||||
head_ref: z.string().optional(),
|
||||
comment_summary_in_pr: z.boolean().default(false),
|
||||
retry_on_snapshot_warnings: z.boolean().default(false),
|
||||
retry_on_snapshot_warnings_timeout: z.number().default(120),
|
||||
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({
|
||||
@@ -91,6 +104,5 @@ 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 FailOnSeverity = z.infer<typeof FailOnSeveritySchema>
|
||||
export type Severity = z.infer<typeof SeveritySchema>
|
||||
export type Scope = (typeof SCOPES)[number]
|
||||
|
||||
+54
-13
@@ -1,5 +1,5 @@
|
||||
import * as core from '@actions/core'
|
||||
import {ConfigurationOptions, Changes} from './schemas'
|
||||
import {ConfigurationOptions, Changes, Change} from './schemas'
|
||||
import {SummaryTableRow} from '@actions/core/lib/summary'
|
||||
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
|
||||
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
||||
@@ -13,13 +13,15 @@ const icons = {
|
||||
export function addSummaryToSummary(
|
||||
vulnerableChanges: Changes,
|
||||
invalidLicenseChanges: InvalidLicenseChanges,
|
||||
deniedChanges: Changes,
|
||||
config: ConfigurationOptions
|
||||
): void {
|
||||
core.summary.addHeading('Dependency Review', 1)
|
||||
|
||||
if (
|
||||
vulnerableChanges.length === 0 &&
|
||||
countLicenseIssues(invalidLicenseChanges) === 0
|
||||
countLicenseIssues(invalidLicenseChanges) === 0 &&
|
||||
deniedChanges.length === 0
|
||||
) {
|
||||
if (!config.license_check) {
|
||||
core.summary.addRaw(`${icons.check} No vulnerabilities found.`)
|
||||
@@ -56,6 +58,13 @@ export function addSummaryToSummary(
|
||||
invalidLicenseChanges.unlicensed.length
|
||||
} package(s) with unknown licenses.`
|
||||
]
|
||||
: []),
|
||||
...(deniedChanges.length > 0
|
||||
? [
|
||||
`${checkOrWarnIcon(deniedChanges.length)} ${
|
||||
deniedChanges.length
|
||||
} package(s) denied.`
|
||||
]
|
||||
: [])
|
||||
])
|
||||
.addRaw('See the Details below.')
|
||||
@@ -222,21 +231,34 @@ export function addScannedDependencies(changes: Changes): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function addSnapshotWarnings(warnings: string): void {
|
||||
// For now, we want to ignore warnings that just complain
|
||||
// about missing snapshots on the head SHA. This is a product
|
||||
// decision to avoid presenting warnings to users who simply
|
||||
// don't use snapshots.
|
||||
const ignore_regex = new RegExp(/No.*snapshot.*found.*head.*/, 'i')
|
||||
if (ignore_regex.test(warnings)) {
|
||||
return
|
||||
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 addSnapshotWarnings(
|
||||
config: ConfigurationOptions,
|
||||
warnings: string
|
||||
): void {
|
||||
core.summary.addHeading('Snapshot Warnings', 2)
|
||||
core.summary.addQuote(`${icons.warning}: ${warnings}`)
|
||||
core.summary.addRaw(
|
||||
'Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice.'
|
||||
)
|
||||
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(
|
||||
@@ -248,6 +270,25 @@ function countLicenseIssues(
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user