Merge pull request #91 from actions/adding-config-file

Adding configuration options
This commit is contained in:
Federico Builes
2022-06-06 20:32:21 +02:00
committed by GitHub
15 changed files with 8754 additions and 31 deletions

8
.github/dependency-review.yml vendored Normal file
View 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"

View File

@@ -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
View 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
View 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])
})

View File

@@ -0,0 +1,4 @@
fail_on_severity: critical
allow_licenses:
- "BSD"
- "GPL 2"

View File

@@ -1,5 +0,0 @@
import {expect, test} from '@jest/globals'
test('tests things', async () => {
expect(true).toEqual(true)
})

8496
dist/index.js generated vendored

File diff suppressed because it is too large Load Diff

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

17
dist/licenses.txt generated vendored
View File

@@ -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
View File

@@ -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",

View File

@@ -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
View 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
View 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
}

View File

@@ -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 {

View File

@@ -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]