merge from main and fix code review comment from @juxtin

This commit is contained in:
tgrall
2024-01-28 10:16:07 +01:00
29 changed files with 3647 additions and 62427 deletions
+5 -5
View File
@@ -21,12 +21,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set Node.js 18.x - name: Set Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 20.x
cache: npm cache: npm
- name: Install dependencies - name: Install dependencies
@@ -47,7 +47,7 @@ jobs:
id: diff id: diff
# If index.js was different than expected, upload the expected version as an artifact # 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' }} if: ${{ failure() && steps.diff.conclusion == 'failure' }}
with: with:
name: dist name: dist
+6 -6
View File
@@ -14,10 +14,10 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
cache: npm cache: npm
- name: Install dependencies - name: Install dependencies
run: npm ci --ignore-scripts run: npm ci --ignore-scripts
@@ -27,10 +27,10 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
cache: npm cache: npm
- name: Install dependencies - name: Install dependencies
run: npm ci --ignore-scripts run: npm ci --ignore-scripts
+1 -1
View File
@@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@main uses: actions/dependency-review-action@main
+25
View File
@@ -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/[email protected]
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
View File
@@ -79,7 +79,7 @@ Here are a few things you can do that will increase the likelihood of your pull
- Write tests. - 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. - 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 ## 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: 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 tag -fa v4 -m "Updating v4 to 4.0.1"
git push origin v3 --force git push origin v4 --force
``` ```
## Resources ## Resources
+15 -11
View File
@@ -31,9 +31,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
``` ```
### GitHub Enterprise Server ### 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/[email protected]/admin/code-security/managing-github-advanced-security-for-your-enterprise/enabling-github-advanced-security-for-your-enterprise) Security](https://docs.github.com/en/[email protected]/admin/code-security/managing-github-advanced-security-for-your-enterprise/enabling-github-advanced-security-for-your-enterprise)
and [GitHub and [GitHub
Connect](https://docs.github.com/en/[email protected]/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect) Connect](https://docs.github.com/en/[email protected]/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 You can use the same workflow as above, replacing the `runs-on` value
with the label of any of your runners (the default label with the label of any of your runners (the default label
@@ -57,9 +57,9 @@ jobs:
runs-on: self-hosted runs-on: self-hosted
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
``` ```
## Configuration options ## Configuration options
@@ -67,7 +67,7 @@ jobs:
Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional. Configure this action by either 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 | | 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` | | `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 | | `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 | | `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 |
@@ -77,7 +77,11 @@ Configure this action by either inlining these options in your workflow file, or
| `vulnerability-check` | Enable or disable the vulnerability 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 | | `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 | | `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` | | `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` | | `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 \*not supported for use with GitHub Enterprise Server
@@ -103,9 +107,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
with: with:
fail-on-severity: moderate fail-on-severity: moderate
@@ -128,7 +132,7 @@ Start by specifying that you will be using an external configuration file:
```yaml ```yaml
- name: Dependency Review - name: Dependency Review
uses: actions/dependency-review-action@v2 uses: actions/dependency-review-action@v4
with: with:
config-file: './.github/dependency-review-config.yml' 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 ### 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. - 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**. - 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**.
+26 -1
View File
@@ -27,7 +27,6 @@ test('it reads custom configs', async () => {
}) })
test('it defaults to false for warn-only', async () => { test('it defaults to false for warn-only', async () => {
setInput('warn-only', 'false')
const config = await readConfig() const config = await readConfig()
expect(config.warn_only).toEqual(false) 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')
})
+166
View File
@@ -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/[email protected]',
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/[email protected]',
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/[email protected]',
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/[email protected]',
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
View File
@@ -19,7 +19,7 @@ const npmChange: Change = {
vulnerabilities: [ vulnerabilities: [
{ {
severity: 'critical', severity: 'critical',
advisory_ghsa_id: 'first-random_string', advisory_ghsa_id: 'vulnerable-ghsa-id',
advisory_summary: 'very dangerous', advisory_summary: 'very dangerous',
advisory_url: 'github.com/future-funk' advisory_url: 'github.com/future-funk'
} }
@@ -39,13 +39,13 @@ const rubyChange: Change = {
vulnerabilities: [ vulnerabilities: [
{ {
severity: 'moderate', severity: 'moderate',
advisory_ghsa_id: 'second-random_string', advisory_ghsa_id: 'moderate-ghsa-id',
advisory_summary: 'not so dangerous', advisory_summary: 'not so dangerous',
advisory_url: 'github.com/future-funk' advisory_url: 'github.com/future-funk'
}, },
{ {
severity: 'low', severity: 'low',
advisory_ghsa_id: 'third-random_string', advisory_ghsa_id: 'low-ghsa-id',
advisory_summary: 'dont page me', advisory_summary: 'dont page me',
advisory_url: 'github.com/future-funk' advisory_url: 'github.com/future-funk'
} }
@@ -65,6 +65,64 @@ const noVulnNpmChange: Change = {
vulnerabilities: [] vulnerabilities: []
} }
const lodashChange: Change = {
change_type: 'added',
manifest: 'package.json',
ecosystem: 'npm',
name: 'lodash',
version: '4.17.0',
package_url: 'pkg:npm/[email protected]',
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 () => { test('it properly filters changes by severity', async () => {
const changes = [npmChange, rubyChange] const changes = [npmChange, rubyChange]
let result = filterChangesBySeverity('high', changes) 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 () => { test('it properly filters changes with allowed vulnerabilities', async () => {
const changes = [npmChange, rubyChange, noVulnNpmChange] const changes = [npmChange, rubyChange, noVulnNpmChange]
let result = filterAllowedAdvisories(['notrealGHSAID'], changes) const fakeGHSAChanges = filterAllowedAdvisories(['notrealGHSAID'], changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange]) expect(fakeGHSAChanges).toEqual([npmChange, rubyChange, noVulnNpmChange])
})
result = filterAllowedAdvisories(['first-random_string'], changes) test('it properly filters only allowed vulnerabilities', async () => {
expect(result).toEqual([rubyChange, noVulnNpmChange]) const changes = [npmChange, rubyChange, noVulnNpmChange]
const oldVulns = [
...npmChange.vulnerabilities,
...rubyChange.vulnerabilities,
...noVulnNpmChange.vulnerabilities
]
result = filterAllowedAdvisories( const vulnerable = filterAllowedAdvisories(['vulnerable-ghsa-id'], changes)
['second-random_string', 'third-random_string'],
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 changes
) )
expect(result).toEqual([npmChange, noVulnNpmChange])
result = filterAllowedAdvisories( expect(result.map(change => change.name)).toEqual(
['first-random_string', 'second-random_string', 'third-random_string'], changes.map(change => change.name)
changes
) )
expect(result).toEqual([noVulnNpmChange]) })
// if we have a change with multiple vulnerabilities but only one is allowed, we still should not filter out that change test('it properly filters multiple GHSAs', async () => {
result = filterAllowedAdvisories(['second-random_string'], changes) const allowedGHSAs = ['vulnerable-ghsa-id', 'moderate-ghsa-id', 'low-ghsa-id']
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange]) 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)
}) })
+7 -4
View File
@@ -55,7 +55,7 @@ const pipChange: Change = {
ecosystem: 'pip', ecosystem: 'pip',
name: 'package-1', name: 'package-1',
version: '1.1.1', version: '1.1.1',
package_url: 'pkg:pip/[email protected]', package_url: 'pkg:pypi/[email protected]',
license: 'MIT', license: 'MIT',
source_repository_url: 'github.com/some-repo', source_repository_url: 'github.com/some-repo',
scope: 'runtime', 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 changes: Changes = [pipChange, npmChange, rubyChange]
const licensesConfig = { const licensesConfig = {
allow: ['BSD'], allow: ['BSD'],
licenseExclusions: ['pkg:pip/[email protected]', 'pkg:npm/[email protected]'] licenseExclusions: ['pkg:pypi/[email protected]', 'pkg:npm/[email protected]']
} }
const invalidLicenses = await getInvalidLicenseChanges( const invalidLicenses = await getInvalidLicenseChanges(
changes, 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 changes: Changes = [emptyPurlChange, npmChange, rubyChange]
const licensesConfig = { const licensesConfig = {
allow: ['BSD'], allow: ['BSD'],
licenseExclusions: ['pkg:pip/[email protected]', 'pkg:npm/[email protected]'] licenseExclusions: ['pkg:pypi/[email protected]', 'pkg:npm/[email protected]']
} }
const invalidLicenses = await getInvalidLicenseChanges( 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 changes: Changes = [pipChange, npmChange, rubyChange]
const licensesConfig = { const licensesConfig = {
allow: ['BSD'], allow: ['BSD'],
licenseExclusions: ['pkg:pip/[email protected]', 'pkg:npm/[email protected]'] licenseExclusions: [
'pkg:pypi/[email protected]',
'pkg:npm/[email protected]'
]
} }
const invalidLicenses = await getInvalidLicenseChanges( const invalidLicenses = await getInvalidLicenseChanges(
changes, changes,
+33 -4
View File
@@ -24,7 +24,11 @@ const defaultConfig: ConfigurationOptions = {
allow_ghsas: [], allow_ghsas: [],
allow_licenses: [], allow_licenses: [],
deny_licenses: [], deny_licenses: [],
deny_packages: [],
deny_groups: [],
comment_summary_in_pr: true, comment_summary_in_pr: true,
retry_on_snapshot_warnings: false,
retry_on_snapshot_warnings_timeout: 120,
warn_only: false warn_only: false
} }
@@ -71,6 +75,7 @@ test('prints headline as h1', () => {
summary.addSummaryToSummary( summary.addSummaryToSummary(
emptyChanges, emptyChanges,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
const text = core.summary.stringify() const text = core.summary.stringify()
@@ -82,6 +87,7 @@ test('only includes "No vulnerabilities or license issues found"-message if both
summary.addSummaryToSummary( summary.addSummaryToSummary(
emptyChanges, emptyChanges,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
const text = core.summary.stringify() 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', () => { test('only includes "No vulnerabilities found"-message if "license_check" is set to false and nothing was found', () => {
const config = {...defaultConfig, license_check: false} const config = {...defaultConfig, license_check: false}
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config) summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
config
)
const text = core.summary.stringify() const text = core.summary.stringify()
expect(text).toContain('✅ No vulnerabilities found.') 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', () => { 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} const config = {...defaultConfig, vulnerability_check: false}
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config) summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
config
)
const text = core.summary.stringify() const text = core.summary.stringify()
expect(text).toContain('✅ No license issues found.') expect(text).toContain('✅ No license issues found.')
@@ -109,6 +125,7 @@ test('groups dependencies with empty manifest paths together', () => {
summary.addSummaryToSummary( summary.addSummaryToSummary(
changesWithEmptyManifests, changesWithEmptyManifests,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
summary.addScannedDependencies(changesWithEmptyManifests) summary.addScannedDependencies(changesWithEmptyManifests)
@@ -125,6 +142,7 @@ test('does not include status section if nothing was found', () => {
summary.addSummaryToSummary( summary.addSummaryToSummary(
emptyChanges, emptyChanges,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
const text = core.summary.stringify() const text = core.summary.stringify()
@@ -143,7 +161,12 @@ test('includes count and status icons for all findings', () => {
unlicensed: [createTestChange(), createTestChange(), createTestChange()] unlicensed: [createTestChange(), createTestChange(), createTestChange()]
} }
summary.addSummaryToSummary(vulnerabilities, licenseIssues, defaultConfig) summary.addSummaryToSummary(
vulnerabilities,
licenseIssues,
emptyChanges,
defaultConfig
)
const text = core.summary.stringify() const text = core.summary.stringify()
expect(text).toContain('❌ 2 vulnerable package(s)') expect(text).toContain('❌ 2 vulnerable package(s)')
@@ -160,6 +183,7 @@ test('uses checkmarks for license issues if only vulnerabilities were found', ()
summary.addSummaryToSummary( summary.addSummaryToSummary(
vulnerabilities, vulnerabilities,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
@@ -179,7 +203,12 @@ test('uses checkmarks for vulnerabilities if only license issues were found', ()
unlicensed: [] unlicensed: []
} }
summary.addSummaryToSummary(emptyChanges, licenseIssues, defaultConfig) summary.addSummaryToSummary(
emptyChanges,
licenseIssues,
emptyChanges,
defaultConfig
)
const text = core.summary.stringify() const text = core.summary.stringify()
expect(text).toContain('✅ 0 vulnerable package(s)') expect(text).toContain('✅ 0 vulnerable package(s)')
+17 -3
View File
@@ -30,7 +30,7 @@ inputs:
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause") description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
required: false required: false
allow-dependencies-licenses: 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 required: false
allow-ghsas: allow-ghsas:
description: Comma-separated list of allowed GitHub Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679") description: Comma-separated list of allowed GitHub Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
@@ -45,13 +45,27 @@ inputs:
description: A boolean to determine if vulnerability checks should be performed description: A boolean to determine if vulnerability checks should be performed
required: false required: false
comment-summary-in-pr: 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 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: warn-only:
description: When set to `true` this action will not make the action to fail, this override the `fail-on-severity` parameter. description: When set to `true` this action will not make the action to fail, this override the `fail-on-severity` parameter.
required: false required: false
default: false default: false
runs: runs:
using: 'node16' using: 'node20'
main: 'dist/index.js' main: 'dist/index.js'
Generated Vendored
-57316
View File
File diff suppressed because one or more lines are too long
Generated Vendored
-1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
-1687
View File
File diff suppressed because it is too large Load Diff
Generated Vendored
-1
View File
File diff suppressed because one or more lines are too long
+88 -24
View File
@@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
``` ```
## Using an inline configuration ## Using an inline configuration
@@ -39,9 +39,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
with: with:
fail-on-severity: critical fail-on-severity: critical
deny-licenses: LGPL-2.0, BSD-2-Clause deny-licenses: LGPL-2.0, BSD-2-Clause
@@ -76,14 +76,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
with: with:
config-file: './.github/dependency-review-config.yml' 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. 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 runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
with: with:
config-file: 'github/octorepo/dependency-review-config.yml@main' 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. 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 runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
with: with:
config-file: 'github/octorepo-private/dependency-review-config.yml@main' 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 ## Getting the results of the action in the PR as a comment
@@ -155,13 +155,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
with: with:
fail-on-severity: critical fail-on-severity: critical
deny-licenses: LGPL-2.0, BSD-2-Clause deny-licenses: LGPL-2.0, BSD-2-Clause
comment-summary-in-pr: true comment-summary-in-pr: always
``` ```
## Exclude dependencies from the license check ## Exclude dependencies from the license check
@@ -183,14 +183,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
with: with:
fail-on-severity: critical fail-on-severity: critical
deny-licenses: LGPL-2.0, BSD-2-Clause deny-licenses: LGPL-2.0, BSD-2-Clause
comment-summary-in-pr: true comment-summary-in-pr: always
allow-dependencies-licenses: 'pkg:npm/loadash, pkg:pip/requests' allow-dependencies-licenses: 'pkg:npm/loadash, pkg:pypi/requests'
``` ```
If we were to use configuration file, the configuration would look like this: If we were to use configuration file, the configuration would look like this:
@@ -202,7 +202,7 @@ allow-licenses:
- 'BSD-2-Clause' - 'BSD-2-Clause'
allow-dependencies-licenses: allow-dependencies-licenses:
- 'pkg:npm/loadash' - 'pkg:npm/loadash'
- 'pkg:pip/requests' - 'pkg:pypi/requests'
``` ```
## Only check for vulnerabilities ## Only check for vulnerabilities
@@ -222,11 +222,75 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v3 uses: actions/dependency-review-action@v4
with: with:
fail-on-severity: critical fail-on-severity: critical
comment-summary-in-pr: true comment-summary-in-pr: always
license-check: false 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
+2818 -3210
View File
File diff suppressed because it is too large Load Diff
+27 -29
View File
@@ -1,6 +1,6 @@
{ {
"name": "dependency-review-action", "name": "dependency-review-action",
"version": "3.0.6", "version": "4.0.0",
"private": true, "private": true,
"description": "A GitHub Action for Dependency Review", "description": "A GitHub Action for Dependency Review",
"main": "lib/main.js", "main": "lib/main.js",
@@ -25,39 +25,37 @@
"author": "GitHub", "author": "GitHub",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.10.1",
"@actions/github": "^5.1.1", "@actions/github": "^6.0.0",
"@octokit/plugin-retry": "^5.0.2", "@octokit/plugin-retry": "^6.0.1",
"@octokit/request-error": "^2.1.0", "@octokit/request-error": "^5.0.1",
"@types/jest": "^29.5.11",
"ansi-styles": "^6.2.1", "ansi-styles": "^6.2.1",
"got": "^13.0.0", "got": "^14.0.0",
"nodemon": "^2.0.22", "jest": "^29.7.0",
"octokit": "^2.0.19", "octokit": "^3.1.2",
"packageurl-js": "^1.0.2", "packageurl-js": "^1.2.0",
"spdx-expression-parse": "^3.0.1", "spdx-expression-parse": "^3.0.1",
"spdx-satisfies": "^5.0.1", "spdx-satisfies": "^5.0.1",
"yaml": "^2.3.1", "ts-jest": "^29.1.1",
"zod": "^3.21.4" "yaml": "^2.3.4",
"zod": "^3.22.3"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.5.2", "@types/node": "^20",
"@types/node": "^16.18.35", "@types/spdx-expression-parse": "^3.0.4",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@types/spdx-satisfies": "^0.1.1",
"@typescript-eslint/parser": "^5.48.0", "@typescript-eslint/eslint-plugin": "^6.18.1",
"@types/spdx-expression-parse": "^3.0.2", "@typescript-eslint/parser": "^6.18.1",
"@types/spdx-satisfies": "^0.1.0", "@vercel/ncc": "^0.38.0",
"@typescript-eslint/eslint-plugin": "^5.59.9", "esbuild-register": "^3.5.0",
"@typescript-eslint/parser": "^5.59.9", "eslint": "^8.56.0",
"@vercel/ncc": "^0.36.1", "eslint-plugin-github": "^4.10.1",
"esbuild-register": "^3.4.2", "eslint-plugin-jest": "^27.6.3",
"eslint": "^8.41.0", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-github": "^4.8.0",
"eslint-plugin-jest": "^27.2.1",
"jest": "^27.5.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"nodemon": "^2.0.22", "nodemon": "^3.0.2",
"prettier": "2.8.8", "prettier": "3.1.1",
"ts-jest": "^27.1.4", "typescript": "^5.3.3"
"typescript": "^4.9.5"
} }
} }
+12 -6
View File
@@ -6,7 +6,7 @@
* npx ts-node scripts/create_summary.ts * 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 {createTestChange} from '../__tests__/fixtures/create-test-change'
import {InvalidLicenseChanges} from '../src/licenses' import {InvalidLicenseChanges} from '../src/licenses'
import * as fs from 'fs' import * as fs from 'fs'
@@ -22,13 +22,17 @@ const defaultConfig: ConfigurationOptions = {
allow_ghsas: [], allow_ghsas: [],
allow_licenses: ['MIT'], allow_licenses: ['MIT'],
deny_licenses: [], deny_licenses: [],
deny_packages: [],
deny_groups: [],
allow_dependencies_licenses: [ allow_dependencies_licenses: [
'pkg:npm/[email protected]', 'pkg:npm/[email protected]',
'pkg:pip/requests', 'pkg:pypi/requests',
'pkg:pip/certifi', 'pkg:pypi/certifi',
'pkg:pip/[email protected]' 'pkg:pypi/[email protected]'
], ],
comment_summary_in_pr: true, comment_summary_in_pr: true,
retry_on_snapshot_warnings: false,
retry_on_snapshot_warnings_timeout: 120,
warn_only: false warn_only: false
} }
@@ -45,6 +49,7 @@ const createNonIssueSummary = async (): Promise<void> => {
await createSummary( await createSummary(
[], [],
{forbidden: [], unresolved: [], unlicensed: []}, {forbidden: [], unresolved: [], unlicensed: []},
[],
defaultConfig, defaultConfig,
'non-issue-summary.md' '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( async function createSummary(
vulnerabilities: Changes, vulnerabilities: Changes,
licenseIssues: InvalidLicenseChanges, licenseIssues: InvalidLicenseChanges,
denied: Change[],
config: ConfigurationOptions, config: ConfigurationOptions,
fileName: string fileName: string
): Promise<void> { ): Promise<void> {
summary.addSummaryToSummary(vulnerabilities, licenseIssues, config) summary.addSummaryToSummary(vulnerabilities, licenseIssues, denied, config)
summary.addChangeVulnerabilitiesToSummary( summary.addChangeVulnerabilitiesToSummary(
vulnerabilities, vulnerabilities,
config.fail_on_severity config.fail_on_severity
+2 -2
View File
@@ -74,8 +74,8 @@ async function findCommentByMarker(
) )
for await (const {data: comments} of commentsIterator) { for await (const {data: comments} of commentsIterator) {
const existingComment = comments.find(comment => const existingComment = comments.find(
comment.body?.includes(commentBodyIncludes) comment => comment.body?.includes(commentBodyIncludes)
) )
if (existingComment) return existingComment.id if (existingComment) return existingComment.id
} }
+22 -2
View File
@@ -33,12 +33,20 @@ function readInlineConfig(): ConfigurationOptionsPartial {
const allow_dependencies_licenses = parseList( const allow_dependencies_licenses = parseList(
getOptionalInput('allow-dependencies-licenses') 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 allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
const license_check = getOptionalBoolean('license-check') const license_check = getOptionalBoolean('license-check')
const vulnerability_check = getOptionalBoolean('vulnerability-check') const vulnerability_check = getOptionalBoolean('vulnerability-check')
const base_ref = getOptionalInput('base-ref') const base_ref = getOptionalInput('base-ref')
const head_ref = getOptionalInput('head-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') const warn_only = getOptionalBoolean('warn-only')
validatePURL(allow_dependencies_licenses) validatePURL(allow_dependencies_licenses)
@@ -50,6 +58,8 @@ function readInlineConfig(): ConfigurationOptionsPartial {
fail_on_scopes, fail_on_scopes,
allow_licenses, allow_licenses,
deny_licenses, deny_licenses,
deny_packages,
deny_groups,
allow_dependencies_licenses, allow_dependencies_licenses,
allow_ghsas, allow_ghsas,
license_check, license_check,
@@ -57,6 +67,8 @@ function readInlineConfig(): ConfigurationOptionsPartial {
base_ref, base_ref,
head_ref, head_ref,
comment_summary_in_pr, comment_summary_in_pr,
retry_on_snapshot_warnings,
retry_on_snapshot_warnings_timeout,
warn_only 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 { function getOptionalBoolean(name: string): boolean | undefined {
const value = core.getInput(name) const value = core.getInput(name)
return value.length > 0 ? core.getBooleanInput(name) : undefined return value.length > 0 ? core.getBooleanInput(name) : undefined
@@ -139,7 +157,9 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
'deny-licenses', 'deny-licenses',
'fail-on-scopes', 'fail-on-scopes',
'allow-ghsas', 'allow-ghsas',
'allow-dependencies-licenses' 'allow-dependencies-licenses',
'deny-packages',
'deny-groups'
] ]
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
+42
View File
@@ -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
}
+2 -1
View File
@@ -31,7 +31,8 @@ export async function compare({
url: '/repos/{owner}/{repo}/dependency-graph/compare/{basehead}', url: '/repos/{owner}/{repo}/dependency-graph/compare/{basehead}',
owner, owner,
repo, repo,
basehead: `${baseRef}...${headRef}` basehead: `${baseRef}...${headRef}`,
per_page: 5
}, },
response => { response => {
if ( if (
+23 -13
View File
@@ -1,5 +1,13 @@
import {Changes, Severity, SEVERITIES, Scope} from './schemas' 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( export function filterChangesBySeverity(
severity: Severity, severity: Severity,
changes: Changes changes: Changes
@@ -31,7 +39,14 @@ export function filterChangesBySeverity(
filteredChanges = filteredChanges.filter( filteredChanges = filteredChanges.filter(
change => change.vulnerabilities.length > 0 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( export function filterChangesByScopes(
@@ -67,25 +82,20 @@ export function filterAllowedAdvisories(
return changes return changes
} }
const filteredChanges = changes.filter(change => { const filteredChanges = changes.map(change => {
const noAdvisories = const noAdvisories =
change.vulnerabilities === undefined || change.vulnerabilities === undefined ||
change.vulnerabilities.length === 0 change.vulnerabilities.length === 0
if (noAdvisories) { if (noAdvisories) {
return true return change
} }
const newChange = {...change}
newChange.vulnerabilities = change.vulnerabilities.filter(
vuln => !ghsas.includes(vuln.advisory_ghsa_id)
)
let allAllowedAdvisories = true return newChange
// 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 return filteredChanges
+4 -2
View File
@@ -32,7 +32,7 @@ export async function getInvalidLicenseChanges(
const {allow, deny} = licenses const {allow, deny} = licenses
const licenseExclusions = licenses.licenseExclusions?.map( const licenseExclusions = licenses.licenseExclusions?.map(
(pkgUrl: string) => { (pkgUrl: string) => {
return PackageURL.fromString(pkgUrl) return PackageURL.fromString(encodeURI(pkgUrl))
} }
) )
@@ -45,7 +45,9 @@ export async function getInvalidLicenseChanges(
return true 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 // We want to find if the licenseExclussion list contains the PackageURL of the Change
// If it does, we want to filter it out and therefore return false // If it does, we want to filter it out and therefore return false
+104 -43
View File
@@ -3,7 +3,7 @@ import * as dependencyGraph from './dependency-graph'
import * as github from '@actions/github' import * as github from '@actions/github'
import styles from 'ansi-styles' import styles from 'ansi-styles'
import {RequestError} from '@octokit/request-error' 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 {readConfig} from '../src/config'
import { import {
filterChangesBySeverity, filterChangesBySeverity,
@@ -16,6 +16,43 @@ import {getRefs} from './git-refs'
import {groupDependenciesByManifest} from './utils' import {groupDependenciesByManifest} from './utils'
import {commentPr} from './comment-pr' 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> { async function run(): Promise<void> {
try { try {
@@ -23,12 +60,18 @@ async function run(): Promise<void> {
const refs = getRefs(config, github.context) const refs = getRefs(config, github.context)
const comparison = await dependencyGraph.compare({ const comparison = await getComparison(
owner: github.context.repo.owner, refs.base,
repo: github.context.repo.repo, refs.head,
baseRef: refs.base, config.retry_on_snapshot_warnings
headRef: refs.head ? {
}) retryUntil:
Date.now() + config.retry_on_snapshot_warnings_timeout * 1000,
retryDelay: 10
}
: undefined
)
const changes = comparison.changes const changes = comparison.changes
const snapshot_warnings = comparison.snapshot_warnings const snapshot_warnings = comparison.snapshot_warnings
@@ -37,28 +80,24 @@ async function run(): Promise<void> {
return 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 scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
const filteredChanges = filterAllowedAdvisories( const filteredChanges = filterAllowedAdvisories(
config.allow_ghsas, config.allow_ghsas,
scopedChanges 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( const vulnerableChanges = filterChangesBySeverity(
minSeverity, minSeverity,
filteredChanges filteredChanges
).filter(
change =>
change.change_type === 'added' &&
change.vulnerabilities !== undefined &&
change.vulnerabilities.length > 0
) )
const invalidLicenseChanges = await getInvalidLicenseChanges( 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( summary.addSummaryToSummary(
vulnerableChanges, vulnerableChanges,
invalidLicenseChanges, invalidLicenseChanges,
deniedChanges,
config config
) )
if (snapshot_warnings) { if (snapshot_warnings) {
summary.addSnapshotWarnings(snapshot_warnings) summary.addSnapshotWarnings(config, snapshot_warnings)
} }
if (config.vulnerability_check) { if (config.vulnerability_check) {
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity) summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
printVulnerabilitiesBlock(vulnerableChanges, minSeverity, warnOnly) printVulnerabilitiesBlock(vulnerableChanges, minSeverity)
} }
if (config.license_check) { if (config.license_check) {
summary.addLicensesToSummary(invalidLicenseChanges, config) 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) summary.addScannedDependencies(changes)
printScannedDependencies(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) await commentPr(core.summary)
} }
} catch (error) { } catch (error) {
@@ -117,25 +174,19 @@ async function run(): Promise<void> {
function printVulnerabilitiesBlock( function printVulnerabilitiesBlock(
addedChanges: Changes, addedChanges: Changes,
minSeverity: Severity, minSeverity: Severity
warnOnly: boolean
): void { ): void {
let vulFound = false let failed = false
core.group('Vulnerabilities', async () => { core.group('Vulnerabilities', async () => {
if (addedChanges.length > 0) { if (addedChanges.length > 0) {
for (const change of addedChanges) { for (const change of addedChanges) {
printChangeVulnerabilities(change) printChangeVulnerabilities(change)
} }
vulFound = true failed = true
} }
if (vulFound) { if (failed) {
const msg = 'Dependency review detected vulnerable packages.' core.setFailed('Dependency review detected vulnerable packages.')
if (warnOnly) {
core.warning(msg)
} else {
core.setFailed(msg)
}
} else { } else {
core.info( core.info(
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.` `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( function printLicensesBlock(
invalidLicenseChanges: Record<string, Changes>, invalidLicenseChanges: Record<string, Changes>
warnOnly: boolean
): void { ): void {
core.group('Licenses', async () => { core.group('Licenses', async () => {
if (invalidLicenseChanges.forbidden.length > 0) { if (invalidLicenseChanges.forbidden.length > 0) {
core.info('\nThe following dependencies have incompatible licenses:') core.info('\nThe following dependencies have incompatible licenses:')
printLicensesError(invalidLicenseChanges.forbidden) printLicensesError(invalidLicenseChanges.forbidden)
const msg = 'Dependency review detected incompatible licenses.' core.setFailed('Dependency review detected incompatible licenses.')
if (warnOnly) {
core.warning(msg)
} else {
core.setFailed(msg)
}
} }
if (invalidLicenseChanges.unresolved.length > 0) { if (invalidLicenseChanges.unresolved.length > 0) {
core.warning( 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() run()
+22 -10
View File
@@ -1,15 +1,8 @@
import * as z from 'zod' 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 SEVERITIES = ['critical', 'high', 'moderate', 'low'] as const
export const SCOPES = ['unknown', 'runtime', 'development'] 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 SeveritySchema = z.enum(SEVERITIES).default('low')
export const ChangeSchema = z.object({ export const ChangeSchema = z.object({
@@ -43,20 +36,40 @@ export const PullRequestSchema = z.object({
export const ConfigurationOptionsSchema = z export const ConfigurationOptionsSchema = z
.object({ .object({
fail_on_severity: FailOnSeveritySchema, fail_on_severity: SeveritySchema,
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']), fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
allow_licenses: z.array(z.string()).optional(), allow_licenses: z.array(z.string()).optional(),
deny_licenses: z.array(z.string()).optional(), deny_licenses: z.array(z.string()).optional(),
allow_dependencies_licenses: z.array(z.string()).optional(), allow_dependencies_licenses: z.array(z.string()).optional(),
allow_ghsas: z.array(z.string()).default([]), allow_ghsas: z.array(z.string()).default([]),
deny_packages: z.array(z.string()).default([]),
deny_groups: z.array(z.string()).default([]),
license_check: z.boolean().default(true), license_check: z.boolean().default(true),
vulnerability_check: z.boolean().default(true), vulnerability_check: z.boolean().default(true),
config_file: z.string().optional(), config_file: z.string().optional(),
base_ref: z.string().optional(), base_ref: z.string().optional(),
head_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) 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) => { .superRefine((config, context) => {
if (config.allow_licenses && config.deny_licenses) { if (config.allow_licenses && config.deny_licenses) {
context.addIssue({ context.addIssue({
@@ -91,6 +104,5 @@ export type Change = z.infer<typeof ChangeSchema>
export type Changes = z.infer<typeof ChangesSchema> export type Changes = z.infer<typeof ChangesSchema>
export type ComparisonResponse = z.infer<typeof ComparisonResponseSchema> export type ComparisonResponse = z.infer<typeof ComparisonResponseSchema>
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema> export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
export type FailOnSeverity = z.infer<typeof FailOnSeveritySchema>
export type Severity = z.infer<typeof SeveritySchema> export type Severity = z.infer<typeof SeveritySchema>
export type Scope = (typeof SCOPES)[number] export type Scope = (typeof SCOPES)[number]
+54 -13
View File
@@ -1,5 +1,5 @@
import * as core from '@actions/core' 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 {SummaryTableRow} from '@actions/core/lib/summary'
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses' import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils' import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
@@ -13,13 +13,15 @@ const icons = {
export function addSummaryToSummary( export function addSummaryToSummary(
vulnerableChanges: Changes, vulnerableChanges: Changes,
invalidLicenseChanges: InvalidLicenseChanges, invalidLicenseChanges: InvalidLicenseChanges,
deniedChanges: Changes,
config: ConfigurationOptions config: ConfigurationOptions
): void { ): void {
core.summary.addHeading('Dependency Review', 1) core.summary.addHeading('Dependency Review', 1)
if ( if (
vulnerableChanges.length === 0 && vulnerableChanges.length === 0 &&
countLicenseIssues(invalidLicenseChanges) === 0 countLicenseIssues(invalidLicenseChanges) === 0 &&
deniedChanges.length === 0
) { ) {
if (!config.license_check) { if (!config.license_check) {
core.summary.addRaw(`${icons.check} No vulnerabilities found.`) core.summary.addRaw(`${icons.check} No vulnerabilities found.`)
@@ -56,6 +58,13 @@ export function addSummaryToSummary(
invalidLicenseChanges.unlicensed.length invalidLicenseChanges.unlicensed.length
} package(s) with unknown licenses.` } package(s) with unknown licenses.`
] ]
: []),
...(deniedChanges.length > 0
? [
`${checkOrWarnIcon(deniedChanges.length)} ${
deniedChanges.length
} package(s) denied.`
]
: []) : [])
]) ])
.addRaw('See the Details below.') .addRaw('See the Details below.')
@@ -222,21 +231,34 @@ export function addScannedDependencies(changes: Changes): void {
} }
} }
export function addSnapshotWarnings(warnings: string): void { function snapshotWarningRecommendation(
// For now, we want to ignore warnings that just complain config: ConfigurationOptions,
// about missing snapshots on the head SHA. This is a product warnings: string
// decision to avoid presenting warnings to users who simply ): string {
// don't use snapshots. const no_pr_snaps = warnings.includes(
const ignore_regex = new RegExp(/No.*snapshot.*found.*head.*/, 'i') 'No snapshots were found for the head SHA'
if (ignore_regex.test(warnings)) { )
return 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.addHeading('Snapshot Warnings', 2)
core.summary.addQuote(`${icons.warning}: ${warnings}`) core.summary.addQuote(`${icons.warning}: ${warnings}`)
core.summary.addRaw( const recommendation = snapshotWarningRecommendation(config, warnings)
'Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice.' 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( 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 { function checkOrFailIcon(count: number): string {
return count === 0 ? icons.check : icons.cross return count === 0 ? icons.check : icons.cross
} }