Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c77673abe0 |
@@ -100,5 +100,3 @@ Thumbs.db
|
|||||||
# Ignore built ts files
|
# Ignore built ts files
|
||||||
__tests__/runner/*
|
__tests__/runner/*
|
||||||
lib/**/*
|
lib/**/*
|
||||||
|
|
||||||
tmp
|
|
||||||
|
|||||||
@@ -66,25 +66,25 @@ 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 |
|
||||||
| `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` |
|
| `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 |
|
| `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` |
|
| `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` |
|
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
|
||||||
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
|
| `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` |
|
|
||||||
|
|
||||||
*not supported for use with GitHub Enterprise Server
|
*not supported for use with GitHub Enterprise Server
|
||||||
|
|
||||||
†will be supported with GitHub Enterprise Server 3.8
|
†will be supported with GitHub Enterprise Server 3.8
|
||||||
|
|
||||||
|
|
||||||
### Inline Configuration
|
### Inline Configuration
|
||||||
|
|
||||||
You can pass options to the Dependency Review GitHub Action using your workflow file.
|
You can pass options to the Dependency Review GitHub Action using your workflow file.
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
@@ -112,10 +112,10 @@ jobs:
|
|||||||
|
|
||||||
You can use an external configuration file to specify the settings for this action. It can be a local file or a file in an external repository. Refer to the following options for the specification.
|
You can use an external configuration file to specify the settings for this action. It can be a local file or a file in an external repository. Refer to the following options for the specification.
|
||||||
|
|
||||||
| Option | Usage | Possible values |
|
| Option | Usage | Possible values |
|
||||||
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `config-file` | A path to a file in the current repository or an external repository. Use this syntax for external files: `OWNER/REPOSITORY/FILENAME@BRANCH` | **Local file**: `./.github/dependency-review-config.yml` <br> **External repo**: `github/octorepo/dependency-review-config.yml@main` |
|
| `config-file` | A path to a file in the current repository or an external repository. Use this syntax for external files: `OWNER/REPOSITORY/FILENAME@BRANCH` | **Local file**: `./.github/dependency-review-config.yml` <br> **External repo**: `github/octorepo/dependency-review-config.yml@main` |
|
||||||
| `external-repo-token` | Specifies a token for fetching the configuration file. It is required if the file resides in a private external repository and for all GitHub Enterprise Server repositories. Create a token in [developer settings](https://github.com/settings/tokens). | Any token with `read` permissions to the repository hosting the config file. |
|
| `external-repo-token` | Specifies a token for fetching the configuration file. It is required if the file resides in a private external repository and for all GitHub Enterprise Server repositories. Create a token in [developer settings](https://github.com/settings/tokens). | Any token with `read` permissions to the repository hosting the config file. |
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ Start by specifying that you will be using an external configuration file:
|
|||||||
config-file: './.github/dependency-review-config.yml'
|
config-file: './.github/dependency-review-config.yml'
|
||||||
```
|
```
|
||||||
|
|
||||||
And then create the file in the path you just specified:
|
And then create the file in the path you just specified:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
fail-on-severity: 'critical'
|
fail-on-severity: 'critical'
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import * as Utils from '../src/utils'
|
|||||||
|
|
||||||
// GitHub Action inputs come in the form of environment variables
|
// GitHub Action inputs come in the form of environment variables
|
||||||
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
|
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
|
||||||
function setInput(input: string, value: string): void {
|
function setInput(input: string, value: string) {
|
||||||
process.env[`INPUT_${input.toUpperCase()}`] = value
|
process.env[`INPUT_${input.toUpperCase()}`] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want a clean ENV before each test. We use `delete`
|
// We want a clean ENV before each test. We use `delete`
|
||||||
// since we want `undefined` values and not empty strings.
|
// since we want `undefined` values and not empty strings.
|
||||||
function clearInputs(): void {
|
function clearInputs() {
|
||||||
const allowedOptions = [
|
const allowedOptions = [
|
||||||
'FAIL-ON-SEVERITY',
|
'FAIL-ON-SEVERITY',
|
||||||
'FAIL-ON-SCOPES',
|
'FAIL-ON-SCOPES',
|
||||||
@@ -22,11 +22,9 @@ function clearInputs(): void {
|
|||||||
'VULNERABILITY-CHECK',
|
'VULNERABILITY-CHECK',
|
||||||
'CONFIG-FILE',
|
'CONFIG-FILE',
|
||||||
'BASE-REF',
|
'BASE-REF',
|
||||||
'HEAD-REF',
|
'HEAD-REF'
|
||||||
'COMMENT-SUMMARY-IN-PR'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// eslint-disable-next-line github/array-foreach
|
|
||||||
allowedOptions.forEach(option => {
|
allowedOptions.forEach(option => {
|
||||||
delete process.env[`INPUT_${option.toUpperCase()}`]
|
delete process.env[`INPUT_${option.toUpperCase()}`]
|
||||||
})
|
})
|
||||||
@@ -239,7 +237,7 @@ test('it supports comma-separated lists', async () => {
|
|||||||
'config-file',
|
'config-file',
|
||||||
'./__tests__/fixtures/inline-license-config-sample.yml'
|
'./__tests__/fixtures/inline-license-config-sample.yml'
|
||||||
)
|
)
|
||||||
const config = await readConfig()
|
let config = await readConfig()
|
||||||
|
|
||||||
expect(config.allow_licenses).toEqual(['MIT', 'GPL-2.0-only'])
|
expect(config.allow_licenses).toEqual(['MIT', 'GPL-2.0-only'])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {expect, test} from '@jest/globals'
|
import {expect, test} from '@jest/globals'
|
||||||
import {Change} from '../src/schemas'
|
import {Change, Changes} from '../src/schemas'
|
||||||
import {
|
import {
|
||||||
filterChangesBySeverity,
|
filterChangesBySeverity,
|
||||||
filterChangesByScopes,
|
filterChangesByScopes,
|
||||||
filterAllowedAdvisories
|
filterAllowedAdvisories
|
||||||
} from '../src/filter'
|
} from '../src/filter'
|
||||||
|
|
||||||
const npmChange: Change = {
|
let npmChange: Change = {
|
||||||
manifest: 'package.json',
|
manifest: 'package.json',
|
||||||
change_type: 'added',
|
change_type: 'added',
|
||||||
ecosystem: 'npm',
|
ecosystem: 'npm',
|
||||||
@@ -26,7 +26,7 @@ const npmChange: Change = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const rubyChange: Change = {
|
let rubyChange: Change = {
|
||||||
change_type: 'added',
|
change_type: 'added',
|
||||||
manifest: 'Gemfile.lock',
|
manifest: 'Gemfile.lock',
|
||||||
ecosystem: 'rubygems',
|
ecosystem: 'rubygems',
|
||||||
@@ -52,7 +52,7 @@ const rubyChange: Change = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const noVulnNpmChange: Change = {
|
let noVulnNpmChange: Change = {
|
||||||
manifest: 'package.json',
|
manifest: 'package.json',
|
||||||
change_type: 'added',
|
change_type: 'added',
|
||||||
ecosystem: 'npm',
|
ecosystem: 'npm',
|
||||||
@@ -92,7 +92,7 @@ test('it properly filters changes by scope', async () => {
|
|||||||
|
|
||||||
test('it properly handles undefined advisory IDs', async () => {
|
test('it properly handles undefined advisory IDs', async () => {
|
||||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
||||||
const result = filterAllowedAdvisories(undefined, changes)
|
let result = filterAllowedAdvisories(undefined, changes)
|
||||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import {Change} from '../../src/schemas'
|
|
||||||
import {createTestVulnerability} from './create-test-vulnerability'
|
|
||||||
|
|
||||||
const defaultChange: Change = {
|
|
||||||
change_type: 'added',
|
|
||||||
manifest: 'package.json',
|
|
||||||
ecosystem: 'npm',
|
|
||||||
name: 'lodash',
|
|
||||||
version: '4.17.20',
|
|
||||||
package_url: 'pkg:npm/[email protected]',
|
|
||||||
license: 'MIT',
|
|
||||||
source_repository_url: 'https://github.com/lodash/lodash',
|
|
||||||
scope: 'runtime',
|
|
||||||
vulnerabilities: [
|
|
||||||
createTestVulnerability({
|
|
||||||
severity: 'high',
|
|
||||||
advisory_ghsa_id: 'GHSA-35jh-r3h4-6jhm',
|
|
||||||
advisory_summary: 'Command Injection in lodash',
|
|
||||||
advisory_url: 'https://github.com/advisories/GHSA-35jh-r3h4-6jhm'
|
|
||||||
}),
|
|
||||||
createTestVulnerability({
|
|
||||||
severity: 'moderate',
|
|
||||||
advisory_ghsa_id: 'GHSA-29mw-wpgm-hmr9',
|
|
||||||
advisory_summary:
|
|
||||||
'Regular Expression Denial of Service (ReDoS) in lodash',
|
|
||||||
advisory_url: 'https://github.com/advisories/GHSA-29mw-wpgm-hmr9'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const createTestChange = (overwrites: Partial<Change> = {}): Change => ({
|
|
||||||
...defaultChange,
|
|
||||||
...overwrites
|
|
||||||
})
|
|
||||||
|
|
||||||
export {createTestChange}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import {Change} from '../../src/schemas'
|
|
||||||
|
|
||||||
type Vulnerability = Change['vulnerabilities'][0]
|
|
||||||
|
|
||||||
const defaultTestVulnerability: Vulnerability = {
|
|
||||||
severity: 'high',
|
|
||||||
advisory_ghsa_id: 'GHSA-35jh-r3h4-6jhm',
|
|
||||||
advisory_summary: 'Command Injection in lodash',
|
|
||||||
advisory_url: 'https://github.com/advisories/GHSA-35jh-r3h4-6jhm'
|
|
||||||
}
|
|
||||||
|
|
||||||
const createTestVulnerability = (
|
|
||||||
overwrites: Partial<Vulnerability> = {}
|
|
||||||
): Vulnerability => ({
|
|
||||||
...defaultTestVulnerability,
|
|
||||||
...overwrites
|
|
||||||
})
|
|
||||||
|
|
||||||
export {createTestVulnerability}
|
|
||||||
@@ -3,7 +3,7 @@ import {Change, Changes} from '../src/schemas'
|
|||||||
|
|
||||||
let getInvalidLicenseChanges: Function
|
let getInvalidLicenseChanges: Function
|
||||||
|
|
||||||
const npmChange: Change = {
|
let npmChange: Change = {
|
||||||
manifest: 'package.json',
|
manifest: 'package.json',
|
||||||
change_type: 'added',
|
change_type: 'added',
|
||||||
ecosystem: 'npm',
|
ecosystem: 'npm',
|
||||||
@@ -23,7 +23,7 @@ const npmChange: Change = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const rubyChange: Change = {
|
let rubyChange: Change = {
|
||||||
change_type: 'added',
|
change_type: 'added',
|
||||||
manifest: 'Gemfile.lock',
|
manifest: 'Gemfile.lock',
|
||||||
ecosystem: 'rubygems',
|
ecosystem: 'rubygems',
|
||||||
@@ -63,7 +63,6 @@ const mockOctokit = {
|
|||||||
|
|
||||||
jest.mock('octokit', () => {
|
jest.mock('octokit', () => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
||||||
Octokit: class {
|
Octokit: class {
|
||||||
constructor() {
|
constructor() {
|
||||||
return mockOctokit
|
return mockOctokit
|
||||||
@@ -79,7 +78,6 @@ beforeEach(async () => {
|
|||||||
// true for BSD, false for all others
|
// true for BSD, false for all others
|
||||||
return jest.fn((license: string, _: string): boolean => license === 'BSD')
|
return jest.fn((license: string, _: string): boolean => license === 'BSD')
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
||||||
;({getInvalidLicenseChanges} = require('../src/licenses'))
|
;({getInvalidLicenseChanges} = require('../src/licenses'))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -142,7 +140,6 @@ test('it adds all licenses to unresolved if it is unable to determine the validi
|
|||||||
throw new Error('Some Error')
|
throw new Error('Some Error')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
||||||
;({getInvalidLicenseChanges} = require('../src/licenses'))
|
;({getInvalidLicenseChanges} = require('../src/licenses'))
|
||||||
const changes: Changes = [npmChange, rubyChange]
|
const changes: Changes = [npmChange, rubyChange]
|
||||||
const invalidLicenses = await getInvalidLicenseChanges(changes, {
|
const invalidLicenses = await getInvalidLicenseChanges(changes, {
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
import {expect, jest, test} from '@jest/globals'
|
|
||||||
import {Changes, ConfigurationOptions} from '../src/schemas'
|
|
||||||
import * as summary from '../src/summary'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import {createTestChange} from './fixtures/create-test-change'
|
|
||||||
import {createTestVulnerability} from './fixtures/create-test-vulnerability'
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
core.summary.emptyBuffer()
|
|
||||||
})
|
|
||||||
|
|
||||||
const emptyChanges: Changes = []
|
|
||||||
const emptyInvalidLicenseChanges = {
|
|
||||||
forbidden: [],
|
|
||||||
unresolved: [],
|
|
||||||
unlicensed: []
|
|
||||||
}
|
|
||||||
const defaultConfig: ConfigurationOptions = {
|
|
||||||
vulnerability_check: true,
|
|
||||||
license_check: true,
|
|
||||||
fail_on_severity: 'high',
|
|
||||||
fail_on_scopes: ['runtime'],
|
|
||||||
allow_ghsas: [],
|
|
||||||
allow_licenses: [],
|
|
||||||
deny_licenses: [],
|
|
||||||
comment_summary_in_pr: true
|
|
||||||
}
|
|
||||||
|
|
||||||
test('prints headline as h1', () => {
|
|
||||||
summary.addSummaryToSummary(
|
|
||||||
emptyChanges,
|
|
||||||
emptyInvalidLicenseChanges,
|
|
||||||
defaultConfig
|
|
||||||
)
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
|
|
||||||
expect(text).toContain('<h1>Dependency Review</h1>')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('only includes "No vulnerabilities or license issues found"-message if both are configured and nothing was found', () => {
|
|
||||||
summary.addSummaryToSummary(
|
|
||||||
emptyChanges,
|
|
||||||
emptyInvalidLicenseChanges,
|
|
||||||
defaultConfig
|
|
||||||
)
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
|
|
||||||
expect(text).toContain('✅ No vulnerabilities or license issues found.')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('only includes "No vulnerabilities found"-message if "license_check" is set to false and nothing was found', () => {
|
|
||||||
const config = {...defaultConfig, license_check: false}
|
|
||||||
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config)
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
|
|
||||||
expect(text).toContain('✅ No vulnerabilities found.')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('only includes "No license issues found"-message if "vulnerability_check" is set to false and nothing was found', () => {
|
|
||||||
const config = {...defaultConfig, vulnerability_check: false}
|
|
||||||
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config)
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
|
|
||||||
expect(text).toContain('✅ No license issues found.')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('does not include status section if nothing was found', () => {
|
|
||||||
summary.addSummaryToSummary(
|
|
||||||
emptyChanges,
|
|
||||||
emptyInvalidLicenseChanges,
|
|
||||||
defaultConfig
|
|
||||||
)
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
|
|
||||||
expect(text).not.toContain('The following issues were found:')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('includes count and status icons for all findings', () => {
|
|
||||||
const vulnerabilities = [
|
|
||||||
createTestChange({name: 'lodash'}),
|
|
||||||
createTestChange({name: 'underscore', package_url: 'test-url'})
|
|
||||||
]
|
|
||||||
const licenseIssues = {
|
|
||||||
forbidden: [createTestChange()],
|
|
||||||
unresolved: [createTestChange(), createTestChange()],
|
|
||||||
unlicensed: [createTestChange(), createTestChange(), createTestChange()]
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.addSummaryToSummary(vulnerabilities, licenseIssues, defaultConfig)
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toContain('❌ 2 vulnerable package(s)')
|
|
||||||
expect(text).toContain(
|
|
||||||
'❌ 2 package(s) with invalid SPDX license definitions'
|
|
||||||
)
|
|
||||||
expect(text).toContain('❌ 1 package(s) with incompatible licenses')
|
|
||||||
expect(text).toContain('⚠️ 3 package(s) with unknown licenses')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('uses checkmarks for license issues if only vulnerabilities were found', () => {
|
|
||||||
const vulnerabilities = [createTestChange()]
|
|
||||||
|
|
||||||
summary.addSummaryToSummary(
|
|
||||||
vulnerabilities,
|
|
||||||
emptyInvalidLicenseChanges,
|
|
||||||
defaultConfig
|
|
||||||
)
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toContain('❌ 1 vulnerable package(s)')
|
|
||||||
expect(text).toContain(
|
|
||||||
'✅ 0 package(s) with invalid SPDX license definitions'
|
|
||||||
)
|
|
||||||
expect(text).toContain('✅ 0 package(s) with incompatible licenses')
|
|
||||||
expect(text).toContain('✅ 0 package(s) with unknown licenses')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('uses checkmarks for vulnerabilities if only license issues were found', () => {
|
|
||||||
const licenseIssues = {
|
|
||||||
forbidden: [createTestChange()],
|
|
||||||
unresolved: [],
|
|
||||||
unlicensed: []
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.addSummaryToSummary(emptyChanges, licenseIssues, defaultConfig)
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toContain('✅ 0 vulnerable package(s)')
|
|
||||||
expect(text).toContain(
|
|
||||||
'✅ 0 package(s) with invalid SPDX license definitions'
|
|
||||||
)
|
|
||||||
expect(text).toContain('❌ 1 package(s) with incompatible licenses')
|
|
||||||
expect(text).toContain('✅ 0 package(s) with unknown licenses')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addChangeVulnerabilitiesToSummary() - only includes section if any vulnerabilites found', () => {
|
|
||||||
summary.addChangeVulnerabilitiesToSummary(emptyChanges, 'low')
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toEqual('')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addChangeVulnerabilitiesToSummary() - includes all vulnerabilities', () => {
|
|
||||||
const changes = [
|
|
||||||
createTestChange({name: 'lodash'}),
|
|
||||||
createTestChange({name: 'underscore', package_url: 'test-url'})
|
|
||||||
]
|
|
||||||
|
|
||||||
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toContain('<h2>Vulnerabilities</h2>')
|
|
||||||
expect(text).toContain('lodash')
|
|
||||||
expect(text).toContain('underscore')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addChangeVulnerabilitiesToSummary() - includes advisory url if available', () => {
|
|
||||||
const changes = [
|
|
||||||
createTestChange({
|
|
||||||
name: 'underscore',
|
|
||||||
vulnerabilities: [
|
|
||||||
createTestVulnerability({
|
|
||||||
advisory_summary: 'test-summary',
|
|
||||||
advisory_url: 'test-url'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
})
|
|
||||||
]
|
|
||||||
|
|
||||||
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toContain('lodash')
|
|
||||||
expect(text).toContain('<a href="test-url">test-summary</a>')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addChangeVulnerabilitiesToSummary() - groups vulnerabilities of a single package', () => {
|
|
||||||
const changes = [
|
|
||||||
createTestChange({
|
|
||||||
name: 'package-with-multiple-vulnerabilities',
|
|
||||||
vulnerabilities: [
|
|
||||||
createTestVulnerability({advisory_summary: 'test-summary-1'}),
|
|
||||||
createTestVulnerability({advisory_summary: 'test-summary-2'})
|
|
||||||
]
|
|
||||||
})
|
|
||||||
]
|
|
||||||
|
|
||||||
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text.match('package-with-multiple-vulnerabilities')).toHaveLength(1)
|
|
||||||
expect(text).toContain('test-summary-1')
|
|
||||||
expect(text).toContain('test-summary-2')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addChangeVulnerabilitiesToSummary() - prints severity statement if above low', () => {
|
|
||||||
const changes = [createTestChange()]
|
|
||||||
|
|
||||||
summary.addChangeVulnerabilitiesToSummary(changes, 'medium')
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toContain(
|
|
||||||
'Only included vulnerabilities with severity <strong>medium</strong> or higher.'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addChangeVulnerabilitiesToSummary() - does not print severity statment if it is set to "low"', () => {
|
|
||||||
const changes = [createTestChange()]
|
|
||||||
|
|
||||||
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).not.toContain('Only included vulnerabilities')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addLicensesToSummary() - does not include entire section if no license issues found', () => {
|
|
||||||
summary.addLicensesToSummary(emptyInvalidLicenseChanges, defaultConfig)
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toEqual('')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addLicensesToSummary() - includes all license issues in table', () => {
|
|
||||||
const licenseIssues = {
|
|
||||||
forbidden: [createTestChange()],
|
|
||||||
unresolved: [createTestChange(), createTestChange()],
|
|
||||||
unlicensed: [createTestChange(), createTestChange(), createTestChange()]
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.addLicensesToSummary(licenseIssues, defaultConfig)
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toContain('<h2>License Issues</h2>')
|
|
||||||
expect(text).toContain('<td>Incompatible License</td>')
|
|
||||||
expect(text).toContain('<td>Invalid SPDX License</td>')
|
|
||||||
expect(text).toContain('<td>Unknown License</td>')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addLicenseToSummary() - adds one table per manifest', () => {
|
|
||||||
const licenseIssues = {
|
|
||||||
forbidden: [
|
|
||||||
createTestChange({manifest: 'package.json'}),
|
|
||||||
createTestChange({manifest: '.github/workflows/test.yml'})
|
|
||||||
],
|
|
||||||
unresolved: [],
|
|
||||||
unlicensed: []
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.addLicensesToSummary(licenseIssues, defaultConfig)
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
|
|
||||||
expect(text).toContain('<h4><em>package.json</em></h4>')
|
|
||||||
expect(text).toContain('<h4><em>.github/workflows/test.yml</em></h4>')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addLicensesToSummary() - does not include specific license type sub-section if nothing is found', () => {
|
|
||||||
const licenseIssues = {
|
|
||||||
forbidden: [],
|
|
||||||
unlicensed: [],
|
|
||||||
unresolved: [createTestChange()]
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.addLicensesToSummary(licenseIssues, defaultConfig)
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).not.toContain('<td>Incompatible License</td>')
|
|
||||||
expect(text).not.toContain('<td>Unknown License</td>')
|
|
||||||
expect(text).toContain('<td>Invalid SPDX License</td>')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addLicensesToSummary() - includes list of configured allowed licenses', () => {
|
|
||||||
const licenseIssues = {
|
|
||||||
forbidden: [createTestChange()],
|
|
||||||
unresolved: [],
|
|
||||||
unlicensed: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: ConfigurationOptions = {
|
|
||||||
...defaultConfig,
|
|
||||||
allow_licenses: ['MIT', 'Apache-2.0']
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.addLicensesToSummary(licenseIssues, config)
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toContain('<strong>Allowed Licenses</strong>: MIT, Apache-2.0')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('addLicensesToSummary() - includes configured denied license', () => {
|
|
||||||
const licenseIssues = {
|
|
||||||
forbidden: [createTestChange()],
|
|
||||||
unresolved: [],
|
|
||||||
unlicensed: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const config: ConfigurationOptions = {
|
|
||||||
...defaultConfig,
|
|
||||||
deny_licenses: ['MIT']
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.addLicensesToSummary(licenseIssues, config)
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
expect(text).toContain('<strong>Denied Licenses</strong>: MIT')
|
|
||||||
})
|
|
||||||
@@ -41,9 +41,6 @@ inputs:
|
|||||||
vulnerability-check:
|
vulnerability-check:
|
||||||
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:
|
|
||||||
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
|
|
||||||
required: false
|
|
||||||
runs:
|
runs:
|
||||||
using: 'node16'
|
using: 'node16'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
|
|||||||
+265
-837
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Generated
+467
-1729
File diff suppressed because it is too large
Load Diff
+17
-17
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "dependency-review-action",
|
"name": "dependency-review-action",
|
||||||
"version": "3.0.3",
|
"version": "3.0.2",
|
||||||
"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",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.build.json",
|
"build": "tsc",
|
||||||
"format": "prettier --write '**/*.ts'",
|
"format": "prettier --write '**/*.ts'",
|
||||||
"format-check": "prettier --check '**/*.ts'",
|
"format-check": "prettier --check '**/*.ts'",
|
||||||
"lint": "eslint src/**/*.ts",
|
"lint": "eslint src/**/*.ts",
|
||||||
@@ -27,36 +27,36 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/github": "^5.1.1",
|
"@actions/github": "^5.1.1",
|
||||||
"@octokit/plugin-retry": "^4.1.1",
|
"@octokit/plugin-retry": "^4.0.3",
|
||||||
"@octokit/request-error": "^2.1.0",
|
"@octokit/request-error": "^2.1.0",
|
||||||
"ansi-styles": "^6.2.1",
|
"ansi-styles": "^6.2.1",
|
||||||
"got": "^12.6.0",
|
"got": "^12.5.3",
|
||||||
"nodemon": "^2.0.21",
|
"nodemon": "^2.0.20",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.11",
|
||||||
"spdx-expression-parse": "^3.0.1",
|
"spdx-expression-parse": "^3.0.1",
|
||||||
"spdx-satisfies": "^5.0.1",
|
"spdx-satisfies": "^5.0.1",
|
||||||
"yaml": "^2.2.1",
|
"yaml": "^2.2.1",
|
||||||
"zod": "^3.21.0"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/node": "^16.18.14",
|
"@types/node": "^16.18.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.48.0",
|
"@typescript-eslint/parser": "^5.48.0",
|
||||||
"@types/spdx-expression-parse": "^3.0.2",
|
"@types/spdx-expression-parse": "^3.0.2",
|
||||||
"@types/spdx-satisfies": "^0.1.0",
|
"@types/spdx-satisfies": "^0.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"@typescript-eslint/parser": "^5.53.0",
|
"@typescript-eslint/parser": "^5.47.1",
|
||||||
"@vercel/ncc": "^0.36.1",
|
"@vercel/ncc": "^0.36.0",
|
||||||
"esbuild-register": "^3.4.2",
|
"esbuild-register": "^3.4.2",
|
||||||
"eslint": "^8.35.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-plugin-github": "^4.6.1",
|
"eslint-plugin-github": "^4.6.0",
|
||||||
"eslint-plugin-jest": "^27.2.1",
|
"eslint-plugin-jest": "^27.2.1",
|
||||||
"jest": "^27.5.1",
|
"jest": "^27.5.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"nodemon": "^2.0.21",
|
"nodemon": "^2.0.20",
|
||||||
"prettier": "2.8.4",
|
"prettier": "2.8.2",
|
||||||
"ts-jest": "^27.1.4",
|
"ts-jest": "^27.1.4",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
/**
|
|
||||||
* This scripts creates example markdown files for the summary in the ./tmp folder.
|
|
||||||
* You can use it to preview changes to the summary.
|
|
||||||
*
|
|
||||||
* You can execute it like this:
|
|
||||||
* npx ts-node scripts/create_summary.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Changes, ConfigurationOptions} from '../src/schemas'
|
|
||||||
import {createTestChange} from '../__tests__/fixtures/create-test-change'
|
|
||||||
import {InvalidLicenseChanges} from '../src/licenses'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import * as summary from '../src/summary'
|
|
||||||
import * as path from 'path'
|
|
||||||
|
|
||||||
const defaultConfig: ConfigurationOptions = {
|
|
||||||
vulnerability_check: true,
|
|
||||||
license_check: true,
|
|
||||||
fail_on_severity: 'high',
|
|
||||||
fail_on_scopes: ['runtime'],
|
|
||||||
allow_ghsas: [],
|
|
||||||
allow_licenses: ['MIT'],
|
|
||||||
deny_licenses: [],
|
|
||||||
comment_summary_in_pr: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const tmpDir = path.resolve(__dirname, '../tmp')
|
|
||||||
|
|
||||||
const createExampleSummaries = async (): Promise<void> => {
|
|
||||||
await fs.promises.mkdir(tmpDir, {recursive: true})
|
|
||||||
|
|
||||||
await createNonIssueSummary()
|
|
||||||
await createFullSummary()
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNonIssueSummary = async (): Promise<void> => {
|
|
||||||
await createSummary(
|
|
||||||
[],
|
|
||||||
{forbidden: [], unresolved: [], unlicensed: []},
|
|
||||||
defaultConfig,
|
|
||||||
'non-issue-summary.md'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFullSummary = async (): Promise<void> => {
|
|
||||||
const changes = [createTestChange()]
|
|
||||||
const licenses: InvalidLicenseChanges = {
|
|
||||||
forbidden: [
|
|
||||||
createTestChange({
|
|
||||||
name: 'underscore',
|
|
||||||
version: '1.12.0',
|
|
||||||
license: 'Apache 2.0'
|
|
||||||
})
|
|
||||||
],
|
|
||||||
unresolved: [
|
|
||||||
createTestChange({
|
|
||||||
name: 'octoinvader',
|
|
||||||
license: 'Non SPDX License'
|
|
||||||
}),
|
|
||||||
createTestChange({
|
|
||||||
name: 'owner/action-1',
|
|
||||||
license: 'XYZ-License',
|
|
||||||
version: 'v1.2.2',
|
|
||||||
manifest: '.github/workflows/action.yml'
|
|
||||||
})
|
|
||||||
],
|
|
||||||
unlicensed: [
|
|
||||||
createTestChange({
|
|
||||||
name: 'my-other-dependency',
|
|
||||||
license: null
|
|
||||||
}),
|
|
||||||
createTestChange({
|
|
||||||
name: 'owner/action-2',
|
|
||||||
version: 'main',
|
|
||||||
license: null,
|
|
||||||
manifest: '.github/workflows/action.yml'
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
await createSummary(changes, licenses, defaultConfig, 'full-summary.md')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createSummary(
|
|
||||||
vulnerabilities: Changes,
|
|
||||||
licenseIssues: InvalidLicenseChanges,
|
|
||||||
config: ConfigurationOptions,
|
|
||||||
fileName: string
|
|
||||||
): Promise<void> {
|
|
||||||
summary.addSummaryToSummary(vulnerabilities, licenseIssues, config)
|
|
||||||
summary.addChangeVulnerabilitiesToSummary(
|
|
||||||
vulnerabilities,
|
|
||||||
config.fail_on_severity
|
|
||||||
)
|
|
||||||
summary.addLicensesToSummary(licenseIssues, defaultConfig)
|
|
||||||
|
|
||||||
const allChanges = [
|
|
||||||
...vulnerabilities,
|
|
||||||
...licenseIssues.forbidden,
|
|
||||||
...licenseIssues.unresolved,
|
|
||||||
...licenseIssues.unlicensed
|
|
||||||
]
|
|
||||||
|
|
||||||
summary.addScannedDependencies(allChanges)
|
|
||||||
|
|
||||||
const text = core.summary.stringify()
|
|
||||||
await fs.promises.writeFile(path.resolve(tmpDir, fileName), text, {
|
|
||||||
flag: 'w'
|
|
||||||
})
|
|
||||||
core.summary.emptyBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
createExampleSummaries()
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import * as github from '@actions/github'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import * as githubUtils from '@actions/github/lib/utils'
|
|
||||||
import * as retry from '@octokit/plugin-retry'
|
|
||||||
import {RequestError} from '@octokit/request-error'
|
|
||||||
|
|
||||||
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry)
|
|
||||||
const octo = new retryingOctokit(
|
|
||||||
githubUtils.getOctokitOptions(core.getInput('repo-token', {required: true}))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Comment Marker to identify an existing comment to update, so we don't spam the PR with comments
|
|
||||||
const COMMENT_MARKER = '<!-- dependency-review-pr-comment-marker -->'
|
|
||||||
|
|
||||||
export async function commentPr(summary: typeof core.summary): Promise<void> {
|
|
||||||
if (!github.context.payload.pull_request) {
|
|
||||||
core.warning(
|
|
||||||
'Not in the context of a pull request. Skipping comment creation.'
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const commentBody = `${summary.stringify()}\n\n${COMMENT_MARKER}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const existingCommentId = await findCommentByMarker(COMMENT_MARKER)
|
|
||||||
|
|
||||||
if (existingCommentId) {
|
|
||||||
await octo.rest.issues.updateComment({
|
|
||||||
owner: github.context.repo.owner,
|
|
||||||
repo: github.context.repo.repo,
|
|
||||||
comment_id: existingCommentId,
|
|
||||||
body: commentBody
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await octo.rest.issues.createComment({
|
|
||||||
owner: github.context.repo.owner,
|
|
||||||
repo: github.context.repo.repo,
|
|
||||||
issue_number: github.context.payload.pull_request.number,
|
|
||||||
body: commentBody
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof RequestError && error.status === 403) {
|
|
||||||
core.warning(
|
|
||||||
`Unable to write summary to pull-request. Make sure you are giving this workflow the permission 'pull-requests: write'.`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
core.warning(
|
|
||||||
`Unable to comment summary to pull-request, received error: ${error.message}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
core.warning(
|
|
||||||
'Unable to comment summary to pull-request: Unexpected fatal error'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findCommentByMarker(
|
|
||||||
commentBodyIncludes: string
|
|
||||||
): Promise<number | undefined> {
|
|
||||||
const commentsIterator = octo.paginate.iterator(
|
|
||||||
octo.rest.issues.listComments,
|
|
||||||
{
|
|
||||||
owner: github.context.repo.owner,
|
|
||||||
repo: github.context.repo.repo,
|
|
||||||
// We are already checking if we are in the context of a pull request in the caller
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
issue_number: github.context.payload.pull_request!.number
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
for await (const {data: comments} of commentsIterator) {
|
|
||||||
const existingComment = comments.find(comment =>
|
|
||||||
comment.body?.includes(commentBodyIncludes)
|
|
||||||
)
|
|
||||||
if (existingComment) return existingComment.id
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
+1
-3
@@ -34,7 +34,6 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
|||||||
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')
|
|
||||||
|
|
||||||
validateLicenses('allow-licenses', allow_licenses)
|
validateLicenses('allow-licenses', allow_licenses)
|
||||||
validateLicenses('deny-licenses', deny_licenses)
|
validateLicenses('deny-licenses', deny_licenses)
|
||||||
@@ -48,8 +47,7 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
|||||||
license_check,
|
license_check,
|
||||||
vulnerability_check,
|
vulnerability_check,
|
||||||
base_ref,
|
base_ref,
|
||||||
head_ref,
|
head_ref
|
||||||
comment_summary_in_pr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
|
|||||||
+2
-7
@@ -14,24 +14,19 @@ import {isSPDXValid, octokitClient} from './utils'
|
|||||||
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
|
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
|
||||||
* @returns {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
|
* @returns {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
|
||||||
*/
|
*/
|
||||||
export type InvalidLicenseChangeTypes =
|
|
||||||
| 'unlicensed'
|
|
||||||
| 'unresolved'
|
|
||||||
| 'forbidden'
|
|
||||||
export type InvalidLicenseChanges = Record<InvalidLicenseChangeTypes, Changes>
|
|
||||||
export async function getInvalidLicenseChanges(
|
export async function getInvalidLicenseChanges(
|
||||||
changes: Change[],
|
changes: Change[],
|
||||||
licenses: {
|
licenses: {
|
||||||
allow?: string[]
|
allow?: string[]
|
||||||
deny?: string[]
|
deny?: string[]
|
||||||
}
|
}
|
||||||
): Promise<InvalidLicenseChanges> {
|
): Promise<Record<string, Changes>> {
|
||||||
const {allow, deny} = licenses
|
const {allow, deny} = licenses
|
||||||
|
|
||||||
const groupedChanges = await groupChanges(changes)
|
const groupedChanges = await groupChanges(changes)
|
||||||
const licensedChanges: Changes = groupedChanges.licensed
|
const licensedChanges: Changes = groupedChanges.licensed
|
||||||
|
|
||||||
const invalidLicenseChanges: InvalidLicenseChanges = {
|
const invalidLicenseChanges: Record<string, Changes> = {
|
||||||
unlicensed: groupedChanges.unlicensed,
|
unlicensed: groupedChanges.unlicensed,
|
||||||
unresolved: [],
|
unresolved: [],
|
||||||
forbidden: []
|
forbidden: []
|
||||||
|
|||||||
+5
-15
@@ -15,7 +15,6 @@ import * as summary from './summary'
|
|||||||
import {getRefs} from './git-refs'
|
import {getRefs} from './git-refs'
|
||||||
|
|
||||||
import {groupDependenciesByManifest} from './utils'
|
import {groupDependenciesByManifest} from './utils'
|
||||||
import {commentPr} from './comment-pr'
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -29,11 +28,6 @@ async function run(): Promise<void> {
|
|||||||
headRef: refs.head
|
headRef: refs.head
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!changes) {
|
|
||||||
core.info('No Dependency Changes found. Skipping Dependency Review.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const minSeverity = config.fail_on_severity
|
const minSeverity = config.fail_on_severity
|
||||||
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
|
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
|
||||||
const filteredChanges = filterAllowedAdvisories(
|
const filteredChanges = filterAllowedAdvisories(
|
||||||
@@ -41,7 +35,7 @@ async function run(): Promise<void> {
|
|||||||
scopedChanges
|
scopedChanges
|
||||||
)
|
)
|
||||||
|
|
||||||
const vulnerableChanges = filterChangesBySeverity(
|
const addedChanges = filterChangesBySeverity(
|
||||||
minSeverity,
|
minSeverity,
|
||||||
filteredChanges
|
filteredChanges
|
||||||
).filter(
|
).filter(
|
||||||
@@ -60,14 +54,13 @@ async function run(): Promise<void> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
summary.addSummaryToSummary(
|
summary.addSummaryToSummary(
|
||||||
vulnerableChanges,
|
config.vulnerability_check ? addedChanges : null,
|
||||||
invalidLicenseChanges,
|
config.license_check ? invalidLicenseChanges : null
|
||||||
config
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (config.vulnerability_check) {
|
if (config.vulnerability_check) {
|
||||||
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
|
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
|
||||||
printVulnerabilitiesBlock(vulnerableChanges, minSeverity)
|
printVulnerabilitiesBlock(addedChanges, minSeverity)
|
||||||
}
|
}
|
||||||
if (config.license_check) {
|
if (config.license_check) {
|
||||||
summary.addLicensesToSummary(invalidLicenseChanges, config)
|
summary.addLicensesToSummary(invalidLicenseChanges, config)
|
||||||
@@ -76,9 +69,6 @@ async function run(): Promise<void> {
|
|||||||
|
|
||||||
summary.addScannedDependencies(changes)
|
summary.addScannedDependencies(changes)
|
||||||
printScannedDependencies(changes)
|
printScannedDependencies(changes)
|
||||||
if (config.comment_summary_in_pr) {
|
|
||||||
await commentPr(core.summary)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError && error.status === 404) {
|
if (error instanceof RequestError && error.status === 404) {
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
|
|||||||
+1
-2
@@ -45,8 +45,7 @@ export const ConfigurationOptionsSchema = z
|
|||||||
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)
|
|
||||||
})
|
})
|
||||||
.superRefine((config, context) => {
|
.superRefine((config, context) => {
|
||||||
if (config.allow_licenses && config.deny_licenses) {
|
if (config.allow_licenses && config.deny_licenses) {
|
||||||
|
|||||||
+62
-119
@@ -1,82 +1,50 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {ConfigurationOptions, Changes} from './schemas'
|
import {ConfigurationOptions, Changes} from './schemas'
|
||||||
import {SummaryTableRow} from '@actions/core/lib/summary'
|
import {SummaryTableRow} from '@actions/core/lib/summary'
|
||||||
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
|
|
||||||
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
||||||
|
|
||||||
const icons = {
|
|
||||||
check: '✅',
|
|
||||||
cross: '❌',
|
|
||||||
warning: '⚠️'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addSummaryToSummary(
|
export function addSummaryToSummary(
|
||||||
vulnerableChanges: Changes,
|
addedPackages: Changes | null,
|
||||||
invalidLicenseChanges: InvalidLicenseChanges,
|
invalidLicenseChanges: Record<string, Changes> | null
|
||||||
config: ConfigurationOptions
|
|
||||||
): void {
|
): void {
|
||||||
core.summary.addHeading('Dependency Review', 1)
|
|
||||||
|
|
||||||
if (
|
|
||||||
vulnerableChanges.length === 0 &&
|
|
||||||
countLicenseIssues(invalidLicenseChanges) === 0
|
|
||||||
) {
|
|
||||||
if (!config.license_check) {
|
|
||||||
core.summary.addRaw(`${icons.check} No vulnerabilities found.`)
|
|
||||||
} else if (!config.vulnerability_check) {
|
|
||||||
core.summary.addRaw(`${icons.check} No license issues found.`)
|
|
||||||
} else {
|
|
||||||
core.summary.addRaw(
|
|
||||||
`${icons.check} No vulnerabilities or license issues found.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
core.summary
|
core.summary
|
||||||
.addRaw('The following issues were found:')
|
.addHeading('Dependency Review')
|
||||||
|
.addRaw('We found:')
|
||||||
.addList([
|
.addList([
|
||||||
...(config.vulnerability_check
|
...(addedPackages
|
||||||
? [
|
? [`${addedPackages.length} vulnerable package(s)`]
|
||||||
`${checkOrFailIcon(vulnerableChanges.length)} ${
|
|
||||||
vulnerableChanges.length
|
|
||||||
} vulnerable package(s)`
|
|
||||||
]
|
|
||||||
: []),
|
: []),
|
||||||
...(config.license_check
|
...(invalidLicenseChanges
|
||||||
? [
|
? [
|
||||||
`${checkOrFailIcon(invalidLicenseChanges.forbidden.length)} ${
|
`${invalidLicenseChanges.unresolved.length} package(s) with invalid SPDX license definitions`,
|
||||||
invalidLicenseChanges.forbidden.length
|
`${invalidLicenseChanges.forbidden.length} package(s) with incompatible licenses`,
|
||||||
} package(s) with incompatible licenses`,
|
`${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.`
|
||||||
`${checkOrFailIcon(invalidLicenseChanges.unresolved.length)} ${
|
|
||||||
invalidLicenseChanges.unresolved.length
|
|
||||||
} package(s) with invalid SPDX license definitions`,
|
|
||||||
`${checkOrWarnIcon(invalidLicenseChanges.unlicensed.length)} ${
|
|
||||||
invalidLicenseChanges.unlicensed.length
|
|
||||||
} package(s) with unknown licenses.`
|
|
||||||
]
|
]
|
||||||
: [])
|
: [])
|
||||||
])
|
])
|
||||||
.addRaw('See the Details below.')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addChangeVulnerabilitiesToSummary(
|
export function addChangeVulnerabilitiesToSummary(
|
||||||
vulnerableChanges: Changes,
|
addedPackages: Changes,
|
||||||
severity: string
|
severity: string
|
||||||
): void {
|
): void {
|
||||||
if (vulnerableChanges.length === 0) {
|
const rows: SummaryTableRow[] = []
|
||||||
|
|
||||||
|
const manifests = getManifestsSet(addedPackages)
|
||||||
|
|
||||||
|
core.summary
|
||||||
|
.addHeading('Vulnerabilities')
|
||||||
|
.addQuote(
|
||||||
|
`Vulnerabilities were filtered by minimum severity <strong>${severity}</strong>.`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (addedPackages.length === 0) {
|
||||||
|
core.summary.addQuote('No vulnerabilities found in added packages.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows: SummaryTableRow[] = []
|
|
||||||
|
|
||||||
const manifests = getManifestsSet(vulnerableChanges)
|
|
||||||
|
|
||||||
core.summary.addHeading('Vulnerabilities', 2)
|
|
||||||
|
|
||||||
for (const manifest of manifests) {
|
for (const manifest of manifests) {
|
||||||
for (const change of vulnerableChanges.filter(
|
for (const change of addedPackages.filter(
|
||||||
pkg => pkg.manifest === manifest
|
pkg => pkg.manifest === manifest
|
||||||
)) {
|
)) {
|
||||||
let previous_package = ''
|
let previous_package = ''
|
||||||
@@ -104,7 +72,7 @@ export function addChangeVulnerabilitiesToSummary(
|
|||||||
previous_version = change.version
|
previous_version = change.version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
core.summary.addHeading(`<em>${manifest}</em>`, 4).addTable([
|
core.summary.addHeading(`<em>${manifest}</em>`, 3).addTable([
|
||||||
[
|
[
|
||||||
{data: 'Name', header: true},
|
{data: 'Name', header: true},
|
||||||
{data: 'Version', header: true},
|
{data: 'Version', header: true},
|
||||||
@@ -114,24 +82,13 @@ export function addChangeVulnerabilitiesToSummary(
|
|||||||
...rows
|
...rows
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (severity !== 'low') {
|
|
||||||
core.summary.addQuote(
|
|
||||||
`Only included vulnerabilities with severity <strong>${severity}</strong> or higher.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addLicensesToSummary(
|
export function addLicensesToSummary(
|
||||||
invalidLicenseChanges: InvalidLicenseChanges,
|
invalidLicenseChanges: Record<string, Changes>,
|
||||||
config: ConfigurationOptions
|
config: ConfigurationOptions
|
||||||
): void {
|
): void {
|
||||||
if (countLicenseIssues(invalidLicenseChanges) === 0) {
|
core.summary.addHeading('Licenses')
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
core.summary.addHeading('License Issues', 2)
|
|
||||||
printLicenseViolations(invalidLicenseChanges)
|
|
||||||
|
|
||||||
if (config.allow_licenses && config.allow_licenses.length > 0) {
|
if (config.allow_licenses && config.allow_licenses.length > 0) {
|
||||||
core.summary.addQuote(
|
core.summary.addQuote(
|
||||||
@@ -144,6 +101,11 @@ export function addLicensesToSummary(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.values(invalidLicenseChanges).every(item => item.length === 0)) {
|
||||||
|
core.summary.addQuote('No license violations detected.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
core.debug(
|
core.debug(
|
||||||
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
|
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
|
||||||
)
|
)
|
||||||
@@ -151,43 +113,39 @@ export function addLicensesToSummary(
|
|||||||
core.debug(
|
core.debug(
|
||||||
`${invalidLicenseChanges.unresolved.length} licenses could not be validated`
|
`${invalidLicenseChanges.unresolved.length} licenses could not be validated`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
printLicenseViolation(
|
||||||
|
'Incompatible Licenses',
|
||||||
|
invalidLicenseChanges.forbidden
|
||||||
|
)
|
||||||
|
printLicenseViolation('Unknown Licenses', invalidLicenseChanges.unlicensed)
|
||||||
|
printLicenseViolation(
|
||||||
|
'Invalid SPDX License Definitions',
|
||||||
|
invalidLicenseChanges.unresolved
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
function printLicenseViolation(heading: string, changes: Changes): void {
|
||||||
|
core.summary.addHeading(heading, 5).addSeparator()
|
||||||
|
|
||||||
const licenseIssueTypes: InvalidLicenseChangeTypes[] = [
|
if (changes.length > 0) {
|
||||||
'forbidden',
|
const rows: SummaryTableRow[] = []
|
||||||
'unresolved',
|
const manifests = getManifestsSet(changes)
|
||||||
'unlicensed'
|
|
||||||
]
|
|
||||||
|
|
||||||
const issueTypeNames: Record<InvalidLicenseChangeTypes, string> = {
|
for (const manifest of manifests) {
|
||||||
forbidden: 'Incompatible License',
|
core.summary.addHeading(`<em>${manifest}</em>`, 4)
|
||||||
unresolved: 'Invalid SPDX License',
|
|
||||||
unlicensed: 'Unknown License'
|
|
||||||
}
|
|
||||||
|
|
||||||
function printLicenseViolations(changes: InvalidLicenseChanges): void {
|
for (const change of changes.filter(pkg => pkg.manifest === manifest)) {
|
||||||
const rowsGroupedByManifest: Record<string, SummaryTableRow[]> = {}
|
rows.push([
|
||||||
|
renderUrl(change.source_repository_url, change.name),
|
||||||
for (const issueType of licenseIssueTypes) {
|
change.version,
|
||||||
for (const change of changes[issueType]) {
|
formatLicense(change.license)
|
||||||
if (!rowsGroupedByManifest[change.manifest]) {
|
])
|
||||||
rowsGroupedByManifest[change.manifest] = []
|
|
||||||
}
|
}
|
||||||
rowsGroupedByManifest[change.manifest].push([
|
|
||||||
renderUrl(change.source_repository_url, change.name),
|
|
||||||
change.version,
|
|
||||||
formatLicense(change.license),
|
|
||||||
issueTypeNames[issueType]
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [manifest, rows] of Object.entries(rowsGroupedByManifest)) {
|
core.summary.addTable([['Package', 'Version', 'License'], ...rows])
|
||||||
core.summary.addHeading(`<em>${manifest}</em>`, 4)
|
}
|
||||||
core.summary.addTable([
|
} else {
|
||||||
['Package', 'Version', 'License', 'Issue Type'],
|
core.summary.addQuote(`No ${heading.toLowerCase()} detected.`)
|
||||||
...rows
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +160,9 @@ export function addScannedDependencies(changes: Changes): void {
|
|||||||
const dependencies = groupDependenciesByManifest(changes)
|
const dependencies = groupDependenciesByManifest(changes)
|
||||||
const manifests = dependencies.keys()
|
const manifests = dependencies.keys()
|
||||||
|
|
||||||
const summary = core.summary.addHeading('Scanned Manifest Files', 2)
|
const summary = core.summary
|
||||||
|
.addHeading('Scanned Dependencies')
|
||||||
|
.addHeading(`We scanned ${dependencies.size} manifest files:`, 5)
|
||||||
|
|
||||||
for (const manifest of manifests) {
|
for (const manifest of manifests) {
|
||||||
const deps = dependencies.get(manifest)
|
const deps = dependencies.get(manifest)
|
||||||
@@ -214,20 +174,3 @@ export function addScannedDependencies(changes: Changes): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function countLicenseIssues(
|
|
||||||
invalidLicenseChanges: InvalidLicenseChanges
|
|
||||||
): number {
|
|
||||||
return Object.values(invalidLicenseChanges).reduce(
|
|
||||||
(acc, val) => acc + val.length,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkOrFailIcon(count: number): string {
|
|
||||||
return count === 0 ? icons.check : icons.cross
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkOrWarnIcon(count: number): string {
|
|
||||||
return count === 0 ? icons.check : icons.warning
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"include": ["src"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./lib" /* Redirect output structure to the directory. */,
|
|
||||||
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-1
@@ -3,9 +3,10 @@
|
|||||||
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||||
"outDir": "./lib" /* Redirect output structure to the directory. */,
|
"outDir": "./lib" /* Redirect output structure to the directory. */,
|
||||||
|
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
|
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user