Compare commits

..
Author SHA1 Message Date
Henri Maurer d3fa764646 fix 2023-06-14 10:38:45 +01:00
Henri Maurer 1856a6de19 fix 2023-06-14 10:26:22 +01:00
Henri Maurer 5573b58443 better logging 2023-06-14 10:24:40 +01:00
Henri Maurer c3c3c2e746 fix retry until 2023-06-14 10:12:19 +01:00
Henri Maurer f6f94a23a4 fix 2023-06-09 10:44:43 +01:00
Henri Maurer 50954e6a9a fix 2023-06-09 10:30:56 +01:00
Henri Maurer 66b6f67835 Add configs 2023-06-09 10:26:24 +01:00
Henri Maurer 1644401f8d rewrite retry logic 2023-06-08 18:11:13 +01:00
Henri Maurer 1a326fc7fa proceed even if warnings 2023-06-08 17:04:40 +01:00
Henri Maurer a82096e68a fix 2023-06-07 16:51:53 +01:00
Henri Maurer 90d3a94eb7 fix 2023-06-07 16:48:32 +01:00
Henri Maurer 9dde5949a8 retry every 10s 2023-06-07 16:39:16 +01:00
Henri Maurer cff142b535 includes_dependency_snapshots 2023-06-07 14:04:29 +01:00
Henri Maurer a4c5ac881a disable caching 2023-06-07 10:10:21 +01:00
Henri Maurer d35955ebf6 Prototype re-try on snapshot warnings 2023-06-06 16:44:27 +01:00
18 changed files with 3505 additions and 6177 deletions
+4 -6
View File
@@ -1,4 +1,4 @@
# dependency-review-action
# dependency-review-action
This action scans your pull requests for dependency changes, and will
raise an error if any vulnerabilities or invalid licenses are being introduced. The action is supported by an [API endpoint](https://docs.github.com/en/rest/reference/dependency-graph#dependency-review) that diffs the dependencies between any two revisions on your default branch.
@@ -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)
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)
are enabled, and that you have installed the [dependency-review-action](https://github.com/actions/dependency-review-action) on the server.
are enabled.
You can use the same workflow as above, replacing the `runs-on` value
with the label of any of your runners (the default label
@@ -77,9 +77,7 @@ 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` |
| `allow-dependencies-licenses`\* | Contains a list of packages that will be excluded from license checks. | Any package(s) in [purl](https://github.com/package-url/purl-spec) format | none |
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `always`, `on-failure`, `never` | `never` |
| `deny-packages` | Any number of packages to block in a PR. | Package(s) in [purl](https://github.com/package-url/purl-spec) format | empty |
| `deny-groups` | Any number of groups (namespaces) to block in a PR. | Namespace(s) in [purl](https://github.com/package-url/purl-spec) format (no package name, no version number) | empty |
| `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
@@ -146,7 +144,7 @@ For more examples of how to use this action and its configuration options, see t
### Considerations
- Checking for licenses is not supported on Enterprise Server as the API does not return license information.
- Checking for licenses is not supported on Enterprise Server.
- 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**.
-26
View File
@@ -171,29 +171,3 @@ 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
@@ -1,166 +0,0 @@
import {expect, jest, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
let getDeniedChanges: Function
const npmChange: Change = {
manifest: 'package.json',
change_type: 'added',
ecosystem: 'npm',
name: 'Reeuhq',
version: '1.0.2',
package_url: 'pkg:npm/[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:pip/[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)
})
+7 -32
View File
@@ -24,9 +24,9 @@ const defaultConfig: ConfigurationOptions = {
allow_ghsas: [],
allow_licenses: [],
deny_licenses: [],
deny_packages: [],
deny_groups: [],
comment_summary_in_pr: true
comment_summary_in_pr: true,
retry_on_snapshot_warnings: true,
retry_on_snapshot_warnings_timeout: 120
}
const changesWithEmptyManifests: Changes = [
@@ -72,7 +72,6 @@ test('prints headline as h1', () => {
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig
)
const text = core.summary.stringify()
@@ -84,7 +83,6 @@ test('only includes "No vulnerabilities or license issues found"-message if both
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig
)
const text = core.summary.stringify()
@@ -94,12 +92,7 @@ test('only includes "No vulnerabilities or license issues found"-message if both
test('only includes "No vulnerabilities found"-message if "license_check" is set to false and nothing was found', () => {
const config = {...defaultConfig, license_check: false}
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
config
)
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config)
const text = core.summary.stringify()
expect(text).toContain('✅ No vulnerabilities found.')
@@ -107,12 +100,7 @@ test('only includes "No vulnerabilities found"-message if "license_check" is set
test('only includes "No license issues found"-message if "vulnerability_check" is set to false and nothing was found', () => {
const config = {...defaultConfig, vulnerability_check: false}
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
config
)
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config)
const text = core.summary.stringify()
expect(text).toContain('✅ No license issues found.')
@@ -122,7 +110,6 @@ test('groups dependencies with empty manifest paths together', () => {
summary.addSummaryToSummary(
changesWithEmptyManifests,
emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig
)
summary.addScannedDependencies(changesWithEmptyManifests)
@@ -139,7 +126,6 @@ test('does not include status section if nothing was found', () => {
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig
)
const text = core.summary.stringify()
@@ -158,12 +144,7 @@ test('includes count and status icons for all findings', () => {
unlicensed: [createTestChange(), createTestChange(), createTestChange()]
}
summary.addSummaryToSummary(
vulnerabilities,
licenseIssues,
emptyChanges,
defaultConfig
)
summary.addSummaryToSummary(vulnerabilities, licenseIssues, defaultConfig)
const text = core.summary.stringify()
expect(text).toContain('❌ 2 vulnerable package(s)')
@@ -180,7 +161,6 @@ test('uses checkmarks for license issues if only vulnerabilities were found', ()
summary.addSummaryToSummary(
vulnerabilities,
emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig
)
@@ -200,12 +180,7 @@ test('uses checkmarks for vulnerabilities if only license issues were found', ()
unlicensed: []
}
summary.addSummaryToSummary(
emptyChanges,
licenseIssues,
emptyChanges,
defaultConfig
)
summary.addSummaryToSummary(emptyChanges, licenseIssues, defaultConfig)
const text = core.summary.stringify()
expect(text).toContain('✅ 0 vulnerable package(s)')
+7 -5
View File
@@ -45,14 +45,16 @@ inputs:
description: A boolean to determine if vulnerability checks should be performed
required: false
comment-summary-in-pr:
description: Determines if the summary is posted as a comment in the PR itself. Setting this to `always` or `on-failure` requires you to give the workflow the write permissions for pull-requests
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
deny-packages:
description: A comma-separated list of package URLs to deny (e.g. "pkg:npm/express, pkg:pip/pycrypto")
retry-on-snapshot-warnings:
description: Whether to retry on snapshot warnings
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:pip/pycrypto")
default: true
retry-on-snapshot-warnings-timeout:
description: How many seconds before bailing on retries
required: false
default: 120
runs:
using: 'node16'
main: 'dist/index.js'
Generated Vendored
+2185 -3778
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -31
View File
@@ -161,7 +161,7 @@ jobs:
with:
fail-on-severity: critical
deny-licenses: LGPL-2.0, BSD-2-Clause
comment-summary-in-pr: always
comment-summary-in-pr: true
```
## Exclude dependencies from the license check
@@ -189,7 +189,7 @@ jobs:
with:
fail-on-severity: critical
deny-licenses: LGPL-2.0, BSD-2-Clause
comment-summary-in-pr: always
comment-summary-in-pr: true
allow-dependencies-licenses: 'pkg:npm/loadash, pkg:pip/requests'
```
@@ -227,34 +227,6 @@ jobs:
uses: actions/dependency-review-action@v3
with:
fail-on-severity: critical
comment-summary-in-pr: always
comment-summary-in-pr: true
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@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v3
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'
```
+1204 -1967
View File
File diff suppressed because it is too large Load Diff
+13 -11
View File
@@ -1,6 +1,6 @@
{
"name": "dependency-review-action",
"version": "3.0.7",
"version": "3.0.6",
"private": true,
"description": "A GitHub Action for Dependency Review",
"main": "lib/main.js",
@@ -27,11 +27,12 @@
"dependencies": {
"@actions/core": "^1.10.0",
"@actions/github": "^5.1.1",
"@octokit/plugin-retry": "^5.0.4",
"@octokit/plugin-retry": "^5.0.0",
"@octokit/request-error": "^2.1.0",
"ansi-styles": "^6.2.1",
"got": "^13.0.0",
"octokit": "^2.1.0",
"nodemon": "^2.0.22",
"octokit": "^2.0.16",
"packageurl-js": "^1.0.2",
"spdx-expression-parse": "^3.0.1",
"spdx-satisfies": "^5.0.1",
@@ -40,21 +41,22 @@
},
"devDependencies": {
"@types/jest": "^27.5.2",
"@types/node": "^16.18.38",
"@types/node": "^16.18.34",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.0",
"@types/spdx-expression-parse": "^3.0.2",
"@types/spdx-satisfies": "^0.1.0",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.2.1",
"@typescript-eslint/eslint-plugin": "^5.59.8",
"@typescript-eslint/parser": "^5.59.8",
"@vercel/ncc": "^0.36.1",
"esbuild-register": "^3.4.2",
"eslint": "^8.47.0",
"eslint": "^8.41.0",
"eslint-plugin-github": "^4.8.0",
"eslint-plugin-jest": "^27.2.2",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-jest": "^27.2.1",
"jest": "^27.5.1",
"js-yaml": "^4.1.0",
"nodemon": "^3.0.1",
"prettier": "3.0.1",
"nodemon": "^2.0.22",
"prettier": "2.8.8",
"ts-jest": "^27.1.4",
"typescript": "^4.9.5"
}
+6 -8
View File
@@ -6,7 +6,7 @@
* npx ts-node scripts/create_summary.ts
*/
import {Change, Changes, ConfigurationOptions} from '../src/schemas'
import {Changes, ConfigurationOptions} from '../src/schemas'
import {createTestChange} from '../__tests__/fixtures/create-test-change'
import {InvalidLicenseChanges} from '../src/licenses'
import * as fs from 'fs'
@@ -22,15 +22,15 @@ const defaultConfig: ConfigurationOptions = {
allow_ghsas: [],
allow_licenses: ['MIT'],
deny_licenses: [],
deny_packages: [],
deny_groups: [],
allow_dependencies_licenses: [
'pkg:npm/[email protected]',
'pkg:pip/requests',
'pkg:pip/certifi',
'pkg:pip/[email protected]'
],
comment_summary_in_pr: 'never'
comment_summary_in_pr: true,
retry_on_snapshot_warnings: true,
retry_on_snapshot_warnings_timeout: 120
}
const tmpDir = path.resolve(__dirname, '../tmp')
@@ -46,7 +46,6 @@ const createNonIssueSummary = async (): Promise<void> => {
await createSummary(
[],
{forbidden: [], unresolved: [], unlicensed: []},
[],
defaultConfig,
'non-issue-summary.md'
)
@@ -88,17 +87,16 @@ const createFullSummary = async (): Promise<void> => {
]
}
await createSummary(changes, licenses, [], defaultConfig, 'full-summary.md')
await createSummary(changes, licenses, defaultConfig, 'full-summary.md')
}
async function createSummary(
vulnerabilities: Changes,
licenseIssues: InvalidLicenseChanges,
denied: Change[],
config: ConfigurationOptions,
fileName: string
): Promise<void> {
summary.addSummaryToSummary(vulnerabilities, licenseIssues, denied, config)
summary.addSummaryToSummary(vulnerabilities, licenseIssues, config)
summary.addChangeVulnerabilitiesToSummary(
vulnerabilities,
config.fail_on_severity
+2 -2
View File
@@ -74,8 +74,8 @@ async function findCommentByMarker(
)
for await (const {data: comments} of commentsIterator) {
const existingComment = comments.find(
comment => comment.body?.includes(commentBodyIncludes)
const existingComment = comments.find(comment =>
comment.body?.includes(commentBodyIncludes)
)
if (existingComment) return existingComment.id
}
+17 -9
View File
@@ -33,14 +33,18 @@ function readInlineConfig(): ConfigurationOptionsPartial {
const allow_dependencies_licenses = parseList(
getOptionalInput('allow-dependencies-licenses')
)
const deny_packages = parseList(getOptionalInput('deny-packages'))
const deny_groups = parseList(getOptionalInput('deny-groups'))
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
const license_check = getOptionalBoolean('license-check')
const vulnerability_check = getOptionalBoolean('vulnerability-check')
const base_ref = getOptionalInput('base-ref')
const head_ref = getOptionalInput('head-ref')
const comment_summary_in_pr = getOptionalInput('comment-summary-in-pr')
const comment_summary_in_pr = getOptionalBoolean('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'
)
validatePURL(allow_dependencies_licenses)
validateLicenses('allow-licenses', allow_licenses)
@@ -51,15 +55,15 @@ function readInlineConfig(): ConfigurationOptionsPartial {
fail_on_scopes,
allow_licenses,
deny_licenses,
deny_packages,
deny_groups,
allow_dependencies_licenses,
allow_ghsas,
license_check,
vulnerability_check,
base_ref,
head_ref,
comment_summary_in_pr
comment_summary_in_pr,
retry_on_snapshot_warnings,
retry_on_snapshot_warnings_timeout
}
return Object.fromEntries(
@@ -67,6 +71,12 @@ function readInlineConfig(): ConfigurationOptionsPartial {
)
}
function getOptionalNumber(name: string): number | undefined {
const value = core.getInput(name)
const parsed = z.string().regex(/^\d+$/).transform(Number).safeParse(value)
return parsed.success ? parsed.data : undefined
}
function getOptionalBoolean(name: string): boolean | undefined {
const value = core.getInput(name)
return value.length > 0 ? core.getBooleanInput(name) : undefined
@@ -141,9 +151,7 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
'deny-licenses',
'fail-on-scopes',
'allow-ghsas',
'allow-dependencies-licenses',
'deny-packages',
'deny-groups'
'allow-dependencies-licenses'
]
for (const key of Object.keys(data)) {
-42
View File
@@ -1,42 +0,0 @@
import {Change} from './schemas'
import * as core from '@actions/core'
export async function getDeniedChanges(
changes: Change[],
deniedPackages: string[],
deniedGroups: string[]
): Promise<Change[]> {
const changesDenied: Change[] = []
let failed = false
for (const change of changes) {
change.name = change.name.toLowerCase()
const packageUrl = change.package_url.toLowerCase().split('@')[0]
if (deniedPackages) {
for (const denied of deniedPackages) {
if (packageUrl === denied.split('@')[0].toLowerCase()) {
changesDenied.push(change)
failed = true
}
}
}
if (deniedGroups) {
for (const denied of deniedGroups) {
if (packageUrl.startsWith(denied.toLowerCase())) {
changesDenied.push(change)
failed = true
}
}
}
}
if (failed) {
core.setFailed('Dependency review detected denied packages.')
} else {
core.info('Dependency review did not detect any denied packages')
}
return changesDenied
}
+1 -1
View File
@@ -28,7 +28,7 @@ export async function compare({
const changes = await octo.paginate(
{
method: 'GET',
url: '/repos/{owner}/{repo}/dependency-graph/compare/{basehead}',
url: '/repos/{owner}/{repo}/dependency-graph/compare/{basehead}?includes_dependency_snapshots=true',
owner,
repo,
basehead: `${baseRef}...${headRef}`
+50 -43
View File
@@ -3,7 +3,7 @@ import * as dependencyGraph from './dependency-graph'
import * as github from '@actions/github'
import styles from 'ansi-styles'
import {RequestError} from '@octokit/request-error'
import {Change, Severity, Changes, ConfigurationOptions} from './schemas'
import {Change, Severity, Changes} from './schemas'
import {readConfig} from '../src/config'
import {
filterChangesBySeverity,
@@ -16,7 +16,42 @@ import {getRefs} from './git-refs'
import {groupDependenciesByManifest} from './utils'
import {commentPr} from './comment-pr'
import {getDeniedChanges} from './deny'
async function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function getComparison(
baseRef: string,
headRef: string,
retryOpts?: {
retryUntil: number
retryDelay: number
}
): ReturnType<typeof dependencyGraph.compare> {
const comparison = await dependencyGraph.compare({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
baseRef,
headRef
})
if (comparison.snapshot_warnings.trim() !== '') {
core.info(comparison.snapshot_warnings)
if (retryOpts !== undefined) {
if (retryOpts.retryUntil < Date.now()) {
core.info(`Retry timeout exceeded. Proceeding...`)
return comparison
} else {
core.info(`Retrying in ${retryOpts.retryDelay} seconds...`)
await delay(retryOpts.retryDelay * 1000)
return getComparison(baseRef, headRef, retryOpts)
}
}
}
return comparison
}
async function run(): Promise<void> {
try {
@@ -24,12 +59,18 @@ async function run(): Promise<void> {
const refs = getRefs(config, github.context)
const comparison = await dependencyGraph.compare({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
baseRef: refs.base,
headRef: refs.head
})
const comparison = await getComparison(
refs.base,
refs.head,
config.retry_on_snapshot_warnings
? {
retryUntil:
Date.now() + config.retry_on_snapshot_warnings_timeout * 1000,
retryDelay: 10
}
: undefined
)
const changes = comparison.changes
const snapshot_warnings = comparison.snapshot_warnings
@@ -64,19 +105,9 @@ async function run(): Promise<void> {
}
)
core.debug(`Filtered Changes: ${JSON.stringify(filteredChanges)}`)
core.debug(`Config Deny Packages: ${JSON.stringify(config)}`)
const deniedChanges = await getDeniedChanges(
filteredChanges,
config.deny_packages,
config.deny_groups
)
summary.addSummaryToSummary(
vulnerableChanges,
invalidLicenseChanges,
deniedChanges,
config
)
@@ -92,18 +123,10 @@ async function run(): Promise<void> {
summary.addLicensesToSummary(invalidLicenseChanges, config)
printLicensesBlock(invalidLicenseChanges)
}
if (config.deny_packages || config.deny_groups) {
summary.addDeniedToSummary(deniedChanges)
printDeniedDependencies(deniedChanges, config)
}
summary.addScannedDependencies(changes)
printScannedDependencies(changes)
if (
config.comment_summary_in_pr === 'always' ||
(config.comment_summary_in_pr === 'on-failure' &&
process.exitCode === core.ExitCode.Failure)
) {
if (config.comment_summary_in_pr) {
await commentPr(core.summary)
}
} catch (error) {
@@ -258,20 +281,4 @@ function printScannedDependencies(changes: Changes): void {
})
}
function printDeniedDependencies(
changes: Change[],
config: ConfigurationOptions
): void {
core.group('Denied', async () => {
for (const denied of config.deny_packages) {
core.info(`Config: ${denied}`)
}
for (const change of changes) {
core.info(`Change: ${change.name}@${change.version} is denied`)
core.info(`Change: ${change.package_url} is denied`)
}
})
}
run()
+3 -19
View File
@@ -42,30 +42,14 @@ export const ConfigurationOptionsSchema = z
deny_licenses: z.array(z.string()).optional(),
allow_dependencies_licenses: z.array(z.string()).optional(),
allow_ghsas: z.array(z.string()).default([]),
deny_packages: z.array(z.string()).default([]),
deny_groups: z.array(z.string()).default([]),
license_check: z.boolean().default(true),
vulnerability_check: z.boolean().default(true),
config_file: z.string().optional(),
base_ref: z.string().optional(),
head_ref: z.string().optional(),
comment_summary_in_pr: z
.union([
z.preprocess(
val => (val === 'true' ? true : val === 'false' ? false : val),
z.boolean()
),
z.enum(['always', 'never', 'on-failure'])
])
.default('never')
})
.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
comment_summary_in_pr: z.boolean().default(false),
retry_on_snapshot_warnings: z.boolean().default(true),
retry_on_snapshot_warnings_timeout: z.number().default(120)
})
.superRefine((config, context) => {
if (config.allow_licenses && config.deny_licenses) {
+2 -30
View File
@@ -1,5 +1,5 @@
import * as core from '@actions/core'
import {ConfigurationOptions, Changes, Change} from './schemas'
import {ConfigurationOptions, Changes} from './schemas'
import {SummaryTableRow} from '@actions/core/lib/summary'
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
@@ -13,15 +13,13 @@ const icons = {
export function addSummaryToSummary(
vulnerableChanges: Changes,
invalidLicenseChanges: InvalidLicenseChanges,
deniedChanges: Changes,
config: ConfigurationOptions
): void {
core.summary.addHeading('Dependency Review', 1)
if (
vulnerableChanges.length === 0 &&
countLicenseIssues(invalidLicenseChanges) === 0 &&
deniedChanges.length === 0
countLicenseIssues(invalidLicenseChanges) === 0
) {
if (!config.license_check) {
core.summary.addRaw(`${icons.check} No vulnerabilities found.`)
@@ -58,13 +56,6 @@ export function addSummaryToSummary(
invalidLicenseChanges.unlicensed.length
} package(s) with unknown licenses.`
]
: []),
...(deniedChanges.length > 0
? [
`${checkOrWarnIcon(deniedChanges.length)} ${
deniedChanges.length
} package(s) denied.`
]
: [])
])
.addRaw('See the Details below.')
@@ -257,25 +248,6 @@ function countLicenseIssues(
)
}
export function addDeniedToSummary(deniedChanges: Change[]): void {
if (deniedChanges.length === 0) {
return
}
core.summary.addHeading('Denied dependencies', 2)
for (const change of deniedChanges) {
core.summary.addHeading(`<em>Denied dependencies</em>`, 4)
core.summary.addTable([
['Package', 'Version', 'License'],
[
renderUrl(change.source_repository_url, change.name),
change.version,
change.license || ''
]
])
}
}
function checkOrFailIcon(count: number): string {
return count === 0 ? icons.check : icons.cross
}