Merge pull request #91 from actions/adding-config-file
Adding configuration options
This commit is contained in:
8
.github/dependency-review.yml
vendored
Normal file
8
.github/dependency-review.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
fail_on_severity: low
|
||||
allow_licenses:
|
||||
- 'GPL 3.0'
|
||||
- 'BSD 3 Clause'
|
||||
- 'MIT'
|
||||
#deny_licenses:
|
||||
# - "LGPL 2.0"
|
||||
# - "BSD 2 Clause"
|
||||
29
README.md
29
README.md
@@ -28,7 +28,34 @@ jobs:
|
||||
uses: actions/dependency-review-action@v1
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
The Dependency Review Action uses a YAML configuration file. It
|
||||
expects this file to be named `dependency-review.yml`, inside your
|
||||
`.github/` directory.
|
||||
|
||||
Here's a sample configuration file:
|
||||
|
||||
```yaml
|
||||
fail_on_severity: low
|
||||
```
|
||||
|
||||
[Here](https://github.com/actions/dependency-review-action/blob/main/.github/dependency-review.yml)
|
||||
you can see an example of the configuration file we use for this repository.
|
||||
|
||||
### Severity
|
||||
|
||||
By default this Action blocks any pull request that contains a
|
||||
vulnerability of any severity level. You can override this behavior by
|
||||
setting an option in your configuration file:
|
||||
|
||||
```yaml
|
||||
# choose one of: 'critical', 'high', 'moderate' or 'low'
|
||||
fail_on_severity: high
|
||||
```
|
||||
|
||||
## Getting help
|
||||
|
||||
|
||||
18
__tests__/config.test.ts
Normal file
18
__tests__/config.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {readConfigFile} from '../src/config'
|
||||
|
||||
test('reads the config file', async () => {
|
||||
let options = readConfigFile('./__tests__/fixtures/config-allow-sample.yml')
|
||||
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('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([])
|
||||
})
|
||||
59
__tests__/filter.test.ts
Normal file
59
__tests__/filter.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
import {filterChangesBySeverity} from '../src/filter'
|
||||
|
||||
let npmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
name: 'Reeuhq',
|
||||
version: '1.0.2',
|
||||
package_url: 'somepurl',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'first-random_string',
|
||||
advisory_summary: 'very dangerouns',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
let rubyChange: Change = {
|
||||
change_type: 'added',
|
||||
manifest: 'Gemfile.lock',
|
||||
ecosystem: 'rubygems',
|
||||
name: 'actionsomething',
|
||||
version: '3.2.0',
|
||||
package_url: 'somerubypurl',
|
||||
license: 'BSD',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'moderate',
|
||||
advisory_ghsa_id: 'second-random_string',
|
||||
advisory_summary: 'not so dangerouns',
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
test('it properly filters changes by severity', async () => {
|
||||
const changes = [npmChange, rubyChange]
|
||||
let result = filterChangesBySeverity('high', changes)
|
||||
expect(result).toEqual([npmChange])
|
||||
|
||||
result = filterChangesBySeverity('low', changes)
|
||||
expect(changes).toEqual([npmChange, rubyChange])
|
||||
|
||||
result = filterChangesBySeverity('critical', changes)
|
||||
expect(changes).toEqual([npmChange, rubyChange])
|
||||
})
|
||||
4
__tests__/fixtures/config-allow-sample.yml
Normal file
4
__tests__/fixtures/config-allow-sample.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
fail_on_severity: critical
|
||||
allow_licenses:
|
||||
- "BSD"
|
||||
- "GPL 2"
|
||||
@@ -1,5 +0,0 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
|
||||
test('tests things', async () => {
|
||||
expect(true).toEqual(true)
|
||||
})
|
||||
8496
dist/index.js
generated
vendored
8496
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,6 +684,23 @@ 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
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -16,6 +16,7 @@
|
||||
"ansi-styles": "^6.1.0",
|
||||
"got": "^12.1.0",
|
||||
"nodemon": "^2.0.16",
|
||||
"yaml": "^2.1.0",
|
||||
"zod": "^3.17.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -7435,6 +7436,14 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.0.tgz",
|
||||
"integrity": "sha512-OuAINfTsoJrY5H7CBWnKZhX6nZciXBydrMtTHr1dC4nP40X5jyTIVlogZHxSlVZM8zSgXRfgZGsaHF4+pV+JRw==",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
@@ -13046,6 +13055,11 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"yaml": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.0.tgz",
|
||||
"integrity": "sha512-OuAINfTsoJrY5H7CBWnKZhX6nZciXBydrMtTHr1dC4nP40X5jyTIVlogZHxSlVZM8zSgXRfgZGsaHF4+pV+JRw=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"ansi-styles": "^6.1.0",
|
||||
"got": "^12.1.0",
|
||||
"nodemon": "^2.0.16",
|
||||
"yaml": "^2.1.0",
|
||||
"zod": "^3.17.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -50,4 +51,4 @@
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.7.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/config.ts
Normal file
33
src/config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as fs from 'fs'
|
||||
import YAML from 'yaml'
|
||||
import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas'
|
||||
import path from 'path'
|
||||
|
||||
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',
|
||||
allow_licenses: []
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const values = YAML.parse(data)
|
||||
const parsed = ConfigurationOptionsSchema.parse(values)
|
||||
|
||||
return parsed
|
||||
}
|
||||
36
src/filter.ts
Normal file
36
src/filter.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {Changes} from './schemas'
|
||||
import {Severity, SEVERITIES} from './schemas'
|
||||
|
||||
export function filterChangesBySeverity(
|
||||
severity: Severity,
|
||||
changes: Changes
|
||||
): Changes {
|
||||
const severityIdx = SEVERITIES.indexOf(severity)
|
||||
let filteredChanges = []
|
||||
for (let change of changes) {
|
||||
if (
|
||||
change === undefined ||
|
||||
change.vulnerabilities === undefined ||
|
||||
change.vulnerabilities.length === 0
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
let fChange = {
|
||||
...change,
|
||||
vulnerabilities: change.vulnerabilities.filter(vuln => {
|
||||
const vulnIdx = SEVERITIES.indexOf(vuln.severity)
|
||||
if (vulnIdx <= severityIdx) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
filteredChanges.push(fChange)
|
||||
}
|
||||
|
||||
// don't want to deal with changes with no vulnerabilities
|
||||
filteredChanges = filteredChanges.filter(
|
||||
change => change.vulnerabilities.length > 0
|
||||
)
|
||||
return filteredChanges
|
||||
}
|
||||
41
src/main.ts
41
src/main.ts
@@ -3,7 +3,9 @@ import * as dependencyGraph from './dependency-graph'
|
||||
import * as github from '@actions/github'
|
||||
import styles from 'ansi-styles'
|
||||
import {RequestError} from '@octokit/request-error'
|
||||
import {PullRequestSchema} from './schemas'
|
||||
import {Change, PullRequestSchema, Severity} from './schemas'
|
||||
import {readConfigFile} from '../src/config'
|
||||
import {filterChangesBySeverity} from '../src/filter'
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
@@ -24,24 +26,22 @@ async function run(): Promise<void> {
|
||||
headRef: pull_request.head.sha
|
||||
})
|
||||
|
||||
let config = readConfigFile()
|
||||
let minSeverity = config.fail_on_severity
|
||||
let failed = false
|
||||
|
||||
for (const change of changes) {
|
||||
let filteredChanges = filterChangesBySeverity(
|
||||
minSeverity as Severity,
|
||||
changes
|
||||
)
|
||||
|
||||
for (const change of filteredChanges) {
|
||||
if (
|
||||
change.change_type === 'added' &&
|
||||
change.vulnerabilities !== undefined &&
|
||||
change.vulnerabilities.length > 0
|
||||
) {
|
||||
for (const vuln of change.vulnerabilities) {
|
||||
core.info(
|
||||
`${styles.bold.open}${change.manifest} » ${change.name}@${
|
||||
change.version
|
||||
}${styles.bold.close} – ${vuln.advisory_summary} ${renderSeverity(
|
||||
vuln.severity
|
||||
)}`
|
||||
)
|
||||
core.info(` ↪ ${vuln.advisory_url}`)
|
||||
}
|
||||
printChangeVulnerabilities(change)
|
||||
failed = true
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,9 @@ async function run(): Promise<void> {
|
||||
if (failed) {
|
||||
throw new Error('Dependency review detected vulnerable packages.')
|
||||
} else {
|
||||
core.info('Dependency review did not detect any vulnerable packages.')
|
||||
core.info(
|
||||
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or above.`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof RequestError && error.status === 404) {
|
||||
@@ -70,6 +72,19 @@ async function run(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function printChangeVulnerabilities(change: Change) {
|
||||
for (const vuln of change.vulnerabilities) {
|
||||
core.info(
|
||||
`${styles.bold.open}${change.manifest} » ${change.name}@${
|
||||
change.version
|
||||
}${styles.bold.close} – ${vuln.advisory_summary} ${renderSeverity(
|
||||
vuln.severity
|
||||
)}`
|
||||
)
|
||||
core.info(` ↪ ${vuln.advisory_url}`)
|
||||
}
|
||||
}
|
||||
|
||||
function renderSeverity(
|
||||
severity: 'critical' | 'high' | 'moderate' | 'low'
|
||||
): string {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
const ChangeSchema = z.object({
|
||||
export const SEVERITIES = ['critical', 'high', 'moderate', 'low'] as const
|
||||
|
||||
export const ChangeSchema = z.object({
|
||||
change_type: z.enum(['added', 'removed']),
|
||||
manifest: z.string(),
|
||||
ecosystem: z.string(),
|
||||
@@ -19,6 +21,7 @@ const ChangeSchema = z.object({
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.default([])
|
||||
})
|
||||
|
||||
export const PullRequestSchema = z.object({
|
||||
@@ -27,6 +30,21 @@ export const PullRequestSchema = z.object({
|
||||
head: z.object({sha: z.string()})
|
||||
})
|
||||
|
||||
export const ConfigurationOptionsSchema = z
|
||||
.object({
|
||||
fail_on_severity: z.enum(SEVERITIES).default('low'),
|
||||
allow_licenses: z.array(z.string()).default([]),
|
||||
deny_licenses: z.array(z.string()).default([])
|
||||
})
|
||||
.partial()
|
||||
.refine(
|
||||
obj => !(obj.allow_licenses && obj.deny_licenses),
|
||||
"Can't specify both allow_licenses and deny_licenses"
|
||||
)
|
||||
|
||||
export const ChangesSchema = z.array(ChangeSchema)
|
||||
|
||||
export type Change = z.infer<typeof ChangeSchema>
|
||||
export type Changes = z.infer<typeof ChangesSchema>
|
||||
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
|
||||
export type Severity = typeof SEVERITIES[number]
|
||||
|
||||
Reference in New Issue
Block a user