Merge pull request #112 from actions/move-config-file
Move configuration file location
This commit is contained in:
8
.github/dependency-review.yml
vendored
8
.github/dependency-review.yml
vendored
@@ -1,8 +0,0 @@
|
||||
fail_on_severity: low
|
||||
allow_licenses:
|
||||
- 'GPL 3.0'
|
||||
- 'BSD 3 Clause'
|
||||
- 'MIT'
|
||||
#deny_licenses:
|
||||
# - "LGPL 2.0"
|
||||
# - "BSD 2 Clause"
|
||||
97
README.md
97
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
This action scans your pull requests for dependency changes and will raise an error if any new dependencies have existing vulnerabilities. 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.
|
||||
|
||||
The action is available for all public repositories, as well as private repositories that have Github Advanced Security licensed.
|
||||
The action is available for all public repositories, as well as private repositories that have Github Advanced Security licensed.
|
||||
|
||||
<img width="854" alt="Screen Shot 2022-03-31 at 1 10 51 PM" src="https://user-images.githubusercontent.com/2161/161042286-b22d7dd3-13cb-458d-8744-ce70ed9bf562.png">
|
||||
|
||||
@@ -25,10 +25,99 @@ jobs:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v1
|
||||
uses: actions/dependency-review-action@v2
|
||||
```
|
||||
|
||||
Please keep in mind that you need a GitHub Advanced Security license if you're running this Action on private repos.
|
||||
Please keep in mind that you need a GitHub Advanced Security license if you're running this action on private repos.
|
||||
|
||||
## Configuration
|
||||
You can pass additional options to the Dependency Review
|
||||
Action using your workflow file. Here's an example workflow with
|
||||
all the possible configurations:
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
with:
|
||||
# Possible values: "critical", "high", "moderate", "low"
|
||||
# fail-on-severity: critical
|
||||
#
|
||||
# You can only can only include one of these two options: `allow-licenses` and `deny-licences`
|
||||
#
|
||||
# Possible values: Any `spdx_id` value(s) from https://docs.github.com/en/rest/licenses
|
||||
# allow-licenses: GPL-3.0, BSD-3-Clause, MIT
|
||||
#
|
||||
# Possible values: Any `spdx_id` value(s) from https://docs.github.com/en/rest/licenses
|
||||
# deny-licenses: LGPL-2.0, BSD-2-Clause
|
||||
```
|
||||
|
||||
### Vulnerability Severity
|
||||
|
||||
By default the action will fail on any pull request that contains a
|
||||
vulnerable dependency, regardless of the severity level. You can override this behavior by
|
||||
using the `fail-on-severity` option, which will cause a failure on any pull requests that introduce vulnerabilities of the specified severity level or higher. The possible values are: `critical`, `high`, `moderate`, or `low`. The
|
||||
action defaults to `low`.
|
||||
|
||||
This example will only fail on pull requests with `critical` and `high` vulnerabilities:
|
||||
|
||||
```yaml
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
with:
|
||||
fail-on-severity: high
|
||||
```
|
||||
|
||||
### Licenses
|
||||
|
||||
You can set the action to fail on pull requests based on the licenses of the dependencies
|
||||
they introduce. With `allow-licenses` you can define the list of licenses
|
||||
your repository will accept. Alternatively, you can use `deny-licenses` to only
|
||||
forbid a subset of licenses.
|
||||
|
||||
You can use the [Licenses
|
||||
API](https://docs.github.com/en/rest/licenses) to see the full list of
|
||||
supported licenses. Use the `spdx_id` field for every license you want
|
||||
to filter. A couple of examples:
|
||||
|
||||
```yaml
|
||||
# only allow MIT-licensed dependents
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
with:
|
||||
allow-licenses: MIT
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Block Apache 1.1 and 2.0 licensed dependents
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
with:
|
||||
deny-licenses: Apache-1.1, Apache-2.0
|
||||
```
|
||||
|
||||
**Important**
|
||||
|
||||
* The action will only accept one of the two parameters; an error will
|
||||
be raised if you provide both.
|
||||
* By default both parameters are empty (no license checking is
|
||||
performed).
|
||||
* We don't have license information for all of your dependents. If we
|
||||
can't detect the license for a dependency **we will inform you, but the
|
||||
action won't fail**.
|
||||
|
||||
## Blocking pull requests
|
||||
|
||||
The Dependency Review GitHub Action check will only block a pull request from being merged if the repository owner has required the check to pass before merging. For more information, see the [documentation on protected branches](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches#require-status-checks-before-merging).
|
||||
|
||||
## Getting help
|
||||
|
||||
@@ -37,7 +126,7 @@ issue](https://github.com/actions/dependency-review-action/issues/new/choose).
|
||||
|
||||
## Contributing
|
||||
|
||||
We are grateful for any contributions made to this project.
|
||||
We are grateful for any contributions made to this project.
|
||||
|
||||
Please read [CONTRIBUTING.MD](https://github.com/actions/dependency-review-action/blob/main/CONTRIBUTING.md) to get started.
|
||||
|
||||
|
||||
@@ -1,31 +1,53 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {readConfigFile} from '../src/config'
|
||||
import {expect, test, beforeEach} from '@jest/globals'
|
||||
import {readConfig} from '../src/config'
|
||||
|
||||
test('reads the config file', async () => {
|
||||
let options = readConfigFile('./__tests__/fixtures/config-allow-sample.yml')
|
||||
// GitHub Action inputs come in the form of environment variables
|
||||
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
|
||||
function setInput(input: string, value: string) {
|
||||
process.env[`INPUT_${input.toUpperCase()}`] = value
|
||||
}
|
||||
|
||||
// We want a clean ENV before each test. We use `delete`
|
||||
// since we want `undefined` values and not empty strings.
|
||||
function clearInputs() {
|
||||
delete process.env['INPUT_FAIL-ON-SEVERITY']
|
||||
delete process.env['INPUT_ALLOW-LICENSES']
|
||||
delete process.env['INPUT_DENY-LICENSES']
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearInputs()
|
||||
})
|
||||
|
||||
test('it defaults to low severity', async () => {
|
||||
const options = readConfig()
|
||||
expect(options.fail_on_severity).toEqual('low')
|
||||
})
|
||||
|
||||
test('it reads custom configs', async () => {
|
||||
setInput('fail-on-severity', 'critical')
|
||||
setInput('allow-licenses', ' BSD, GPL 2')
|
||||
|
||||
const options = readConfig()
|
||||
expect(options.fail_on_severity).toEqual('critical')
|
||||
expect(options.allow_licenses).toEqual(['BSD', 'GPL 2'])
|
||||
})
|
||||
|
||||
test('the default config path handles .yml and .yaml', async () => {
|
||||
expect(true).toEqual(true)
|
||||
})
|
||||
test('it defaults to empty allow/deny lists ', async () => {
|
||||
const options = readConfig()
|
||||
|
||||
test('returns a default config when the config file was not found', async () => {
|
||||
let options = readConfigFile('fixtures/i-dont-exist')
|
||||
expect(options.fail_on_severity).toEqual('low')
|
||||
expect(options.allow_licenses).toEqual(undefined)
|
||||
})
|
||||
|
||||
test('it reads config files with empty options', async () => {
|
||||
let options = readConfigFile('./__tests__/fixtures/no-licenses-config.yml')
|
||||
expect(options.fail_on_severity).toEqual('critical')
|
||||
expect(options.allow_licenses).toEqual(undefined)
|
||||
expect(options.deny_licenses).toEqual(undefined)
|
||||
})
|
||||
|
||||
test('it raises an error if both an allow and denylist are specified', async () => {
|
||||
expect(() =>
|
||||
readConfigFile('./__tests__/fixtures/conflictive-config.yml')
|
||||
).toThrow()
|
||||
setInput('allow-licenses', 'MIT')
|
||||
setInput('deny-licenses', 'BSD')
|
||||
|
||||
expect(() => readConfig()).toThrow()
|
||||
})
|
||||
|
||||
test('it raises an error when given an unknown severity', async () => {
|
||||
setInput('fail-on-severity', 'zombies')
|
||||
expect(() => readConfig()).toThrow()
|
||||
})
|
||||
|
||||
@@ -48,13 +48,13 @@ let rubyChange: Change = {
|
||||
|
||||
test('it fails if a license outside the allow list is found', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const invalidChanges = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
expect(invalidChanges[0]).toBe(npmChange)
|
||||
})
|
||||
|
||||
test('it fails if a license inside the deny list is found', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const invalidChanges = getDeniedLicenseChanges(changes, {deny: ['BSD']})
|
||||
const [invalidChanges] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
|
||||
expect(invalidChanges[0]).toBe(rubyChange)
|
||||
})
|
||||
|
||||
@@ -62,7 +62,7 @@ test('it fails if a license inside the deny list is found', async () => {
|
||||
// thing we want in the system. Please remove this test after refactoring.
|
||||
test('it fails all license checks when allow is provided an empty array', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
let invalidChanges = getDeniedLicenseChanges(changes, {
|
||||
let [invalidChanges, _] = getDeniedLicenseChanges(changes, {
|
||||
allow: [],
|
||||
deny: ['BSD']
|
||||
})
|
||||
|
||||
12
action.yml
12
action.yml
@@ -3,9 +3,19 @@ description: 'Prevent the introduction of dependencies with known vulnerabilitie
|
||||
author: 'GitHub'
|
||||
inputs:
|
||||
repo-token:
|
||||
description: 'Token for the repository. Can be passed in using `{{ secrets.GITHUB_TOKEN }}`.'
|
||||
description: Token for the repository. Can be passed in using `{{ secrets.GITHUB_TOKEN }}`.
|
||||
required: false
|
||||
default: ${{ github.token }}
|
||||
fail-on-severity:
|
||||
description: Don't block PRs below this severity. Possible values are `low`, `moderate`, `high`, `critical`.
|
||||
required: false
|
||||
default: 'low'
|
||||
allow-licenses:
|
||||
description: Comma-separated list of allowed licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
||||
required: false
|
||||
deny-licenses:
|
||||
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
||||
required: false
|
||||
runs:
|
||||
using: 'node16'
|
||||
main: 'dist/index.js'
|
||||
|
||||
8385
dist/index.js
generated
vendored
8385
dist/index.js
generated
vendored
File diff suppressed because it is too large
Load Diff
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
17
dist/licenses.txt
generated
vendored
17
dist/licenses.txt
generated
vendored
@@ -684,23 +684,6 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
yaml
|
||||
ISC
|
||||
Copyright Eemeli Aro <eemeli@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
||||
|
||||
|
||||
zod
|
||||
MIT
|
||||
MIT License
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
import * as fs from 'fs'
|
||||
import YAML from 'yaml'
|
||||
import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas'
|
||||
import path from 'path'
|
||||
import * as core from '@actions/core'
|
||||
import * as z from 'zod'
|
||||
import {ConfigurationOptions, SEVERITIES} from './schemas'
|
||||
|
||||
export const CONFIG_FILEPATH = './.github/dependency-review.yml'
|
||||
|
||||
export function readConfigFile(
|
||||
filePath: string = CONFIG_FILEPATH
|
||||
): ConfigurationOptions {
|
||||
// By default we want to fail on all severities and allow all licenses.
|
||||
const defaultOptions: ConfigurationOptions = {
|
||||
fail_on_severity: 'low'
|
||||
}
|
||||
|
||||
let data
|
||||
|
||||
try {
|
||||
data = fs.readFileSync(path.resolve(filePath), 'utf-8')
|
||||
} catch (error: any) {
|
||||
if (error.code && error.code === 'ENOENT') {
|
||||
return defaultOptions
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return ConfigurationOptionsSchema.parse(YAML.parse(data))
|
||||
function getOptionalInput(name: string): string | undefined {
|
||||
const value = core.getInput(name)
|
||||
return value.length > 0 ? value : undefined
|
||||
}
|
||||
|
||||
export function readConfig(): ConfigurationOptions {
|
||||
const fail_on_severity = z
|
||||
.enum(SEVERITIES)
|
||||
.default('low')
|
||||
.parse(getOptionalInput('fail-on-severity'))
|
||||
const allow_licenses = getOptionalInput('allow-licenses')
|
||||
const deny_licenses = getOptionalInput('deny-licenses')
|
||||
|
||||
if (allow_licenses !== undefined && deny_licenses !== undefined) {
|
||||
throw new Error("Can't specify both allow_licenses and deny_licenses")
|
||||
}
|
||||
|
||||
return {
|
||||
fail_on_severity,
|
||||
allow_licenses: allow_licenses?.split(',').map(x => x.trim()),
|
||||
deny_licenses: deny_licenses?.split(',').map(x => x.trim())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {Change, ChangeSchema} from './schemas'
|
||||
* we will ignore the deny list.
|
||||
* @param {Change[]} changes The list of changes to filter.
|
||||
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
|
||||
* @returns {Array<Change} The list of denied changes.
|
||||
* @returns {[Array<Change>, Array<Change]} A tuple where the first element is the list of denied changes and the second one is the list of changes with unknown licenses
|
||||
*/
|
||||
export function getDeniedLicenseChanges(
|
||||
changes: Array<Change>,
|
||||
@@ -18,15 +18,17 @@ export function getDeniedLicenseChanges(
|
||||
allow?: Array<string>
|
||||
deny?: Array<string>
|
||||
}
|
||||
): Array<Change> {
|
||||
): [Array<Change>, Array<Change>] {
|
||||
let {allow, deny} = licenses
|
||||
|
||||
let disallowed: Change[] = []
|
||||
let unknown: Change[] = []
|
||||
|
||||
for (const change of changes) {
|
||||
let license = change.license
|
||||
// TODO: be loud about unknown licenses
|
||||
if (license === null) {
|
||||
unknown.push(change)
|
||||
continue
|
||||
}
|
||||
if (allow !== undefined) {
|
||||
@@ -40,5 +42,5 @@ export function getDeniedLicenseChanges(
|
||||
}
|
||||
}
|
||||
|
||||
return disallowed
|
||||
return [disallowed, unknown]
|
||||
}
|
||||
|
||||
48
src/main.ts
48
src/main.ts
@@ -4,7 +4,7 @@ import * as github from '@actions/github'
|
||||
import styles from 'ansi-styles'
|
||||
import {RequestError} from '@octokit/request-error'
|
||||
import {Change, PullRequestSchema, Severity} from './schemas'
|
||||
import {readConfigFile} from '../src/config'
|
||||
import {readConfig} from '../src/config'
|
||||
import {filterChangesBySeverity} from '../src/filter'
|
||||
import {getDeniedLicenseChanges} from './licenses'
|
||||
|
||||
@@ -27,7 +27,7 @@ async function run(): Promise<void> {
|
||||
headRef: pull_request.head.sha
|
||||
})
|
||||
|
||||
let config = readConfigFile()
|
||||
let config = readConfig()
|
||||
let minSeverity = config.fail_on_severity
|
||||
let failed = false
|
||||
|
||||
@@ -36,14 +36,6 @@ async function run(): Promise<void> {
|
||||
deny: config.deny_licenses
|
||||
}
|
||||
|
||||
let licenseErrors = getDeniedLicenseChanges(changes, licenses)
|
||||
|
||||
if (licenseErrors.length > 0) {
|
||||
printLicensesError(licenseErrors, licenses)
|
||||
core.setFailed('Dependency review detected prohibited licenses.')
|
||||
return
|
||||
}
|
||||
|
||||
let filteredChanges = filterChangesBySeverity(
|
||||
minSeverity as Severity,
|
||||
changes
|
||||
@@ -60,11 +52,22 @@ async function run(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
let [licenseErrors, unknownLicenses] = getDeniedLicenseChanges(
|
||||
changes,
|
||||
licenses
|
||||
)
|
||||
|
||||
if (licenseErrors.length > 0) {
|
||||
printLicensesError(licenseErrors, licenses)
|
||||
printNullLicenses(unknownLicenses)
|
||||
core.setFailed('Dependency review detected incompatible licenses.')
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
throw new Error('Dependency review detected vulnerable packages.')
|
||||
core.setFailed('Dependency review detected vulnerable packages.')
|
||||
} else {
|
||||
core.info(
|
||||
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or above.`
|
||||
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -126,17 +129,7 @@ function printLicensesError(
|
||||
|
||||
let {allow = [], deny = []} = licenses
|
||||
|
||||
core.info('Dependency review detected prohibited licenses.')
|
||||
|
||||
if (allow.length > 0) {
|
||||
core.info('\nAllowed licenses: ' + allow.join(', ') + '\n')
|
||||
}
|
||||
|
||||
if (deny.length > 0) {
|
||||
core.info('\nDenied licenses: ' + deny.join(', ') + '\n')
|
||||
}
|
||||
|
||||
core.info('The following dependencies have incompatible licenses:\n')
|
||||
core.info('\nThe following dependencies have incompatible licenses:\n')
|
||||
for (const change of changes) {
|
||||
core.info(
|
||||
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close} – License: ${styles.color.red.open}${change.license}${styles.color.red.close}`
|
||||
@@ -144,4 +137,13 @@ function printLicensesError(
|
||||
}
|
||||
}
|
||||
|
||||
function printNullLicenses(changes: Array<Change>): void {
|
||||
core.info('\nWe could not detect a license for the following dependencies:\n')
|
||||
for (const change of changes) {
|
||||
core.info(
|
||||
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
@@ -39,7 +39,7 @@ export const ConfigurationOptionsSchema = z
|
||||
.partial()
|
||||
.refine(
|
||||
obj => !(obj.allow_licenses && obj.deny_licenses),
|
||||
"Can't specify both allow_licenses and deny_licenses"
|
||||
"Your workflow file has both an allow_licenses list and deny_licenses list, but you can only set one or the other."
|
||||
)
|
||||
|
||||
export const ChangesSchema = z.array(ChangeSchema)
|
||||
|
||||
Reference in New Issue
Block a user