Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter by vulnerability allow-list #251

Merged
merged 8 commits into from Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 26 additions & 6 deletions README.md
Expand Up @@ -75,7 +75,7 @@ A string representing the path to an external configuraton file. By
default external configuration files are not used.

**Possible values**: A string representing the absolute path to the
configuration file.
configuration file.

**Example**: `config-file: ./.github/dependency-review-config.yml`.

Expand All @@ -97,6 +97,7 @@ support. The default value is `development, runtime`.
**Inline example**: `fail-on-scopes: development, runtime`

**YAML example**:

```yaml
# this prevents scanning development dependencies
fail-on-scopes:
Expand All @@ -113,6 +114,7 @@ https://docs.github.com/en/rest/licenses.
**Inline example**: `allow-licenses: BSD-3-Clause, MIT`

**YAML example**:

```yaml
allow-licenses:
- BSD-3-Clause
Expand All @@ -130,12 +132,29 @@ https://docs.github.com/en/rest/licenses.
**Inline example**: `deny-licenses: LGPL-2.0, BSD-2-Clause`

**YAML example**:

```yaml
deny-licenses:
- LGPL-2.0
- BSD-2-Clause
```

### allow-ghsas

Add a custom list of GHSA ids you do not want the action to block on.
sarahkemi marked this conversation as resolved.
Show resolved Hide resolved

**Possible values**: Any valid advisory GHSA ids.

**Inline example**: `allow-ghsas: GHSA-abcd-1234-5679, GHSA-efgh-1234-5679`

**YAML example**:

```yaml
allow-ghsas:
- GHSA-abcd-1234-5679
- GHSA-efgh-1234-5679
```

### base-ref/head-ref

Provide custom git references for the git base/head when performing
Expand All @@ -146,6 +165,7 @@ this. The values need to be specified for all other event types.
**Possible values**: Any valid git ref(s) in your project.

**Example**:

```yaml
base-ref: 8bb8a58d6a4028b6c2e314d5caaf273f57644896
head-ref: 69af5638bf660cf218aad5709a4c100e42a2f37b
Expand All @@ -163,18 +183,18 @@ file:
- name: Dependency Review
uses: actions/dependency-review-action@v2
with:
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. **All of these fields are
optional**:

```yaml
fail-on-severity: "critical"
fail-on-severity: 'critical'
allow-licenses:
- "GPL-3.0"
- "BSD-3-Clause"
- "MIT"
- 'GPL-3.0'
- 'BSD-3-Clause'
- 'MIT'
```

### Inline Configuration
Expand Down
15 changes: 15 additions & 0 deletions __tests__/config.test.ts
Expand Up @@ -16,6 +16,7 @@ function clearInputs() {
'FAIL-ON-SCOPES',
'ALLOW-LICENSES',
'DENY-LICENSES',
'ALLOW-GHSAS',
'CONFIG-FILE',
'BASE-REF',
'HEAD-REF'
Expand Down Expand Up @@ -160,3 +161,17 @@ test('it raises an error when given invalid scope', async () => {
setInput('fail-on-scopes', 'runtime, zombies')
expect(() => readConfig()).toThrow()
})

test('it defaults to an empty GHSA allowlist', async () => {
const options = readConfig()
expect(options.allow_ghsas).toEqual(undefined)
})

test('it successfully parses GHSA allowlist', async () => {
setInput('allow-ghsas', 'GHSA-abcd-1234-5679, GHSA-efgh-1234-5679')
const options = readConfig()
expect(options.allow_ghsas).toEqual([
'GHSA-abcd-1234-5679',
'GHSA-efgh-1234-5679'
])
})
45 changes: 44 additions & 1 deletion __tests__/filter.test.ts
@@ -1,6 +1,10 @@
import {expect, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
import {filterChangesBySeverity, filterChangesByScopes} from '../src/filter'
import {
filterChangesBySeverity,
filterChangesByScopes,
filterOutAllowedAdvisories
} from '../src/filter'

let npmChange: Change = {
manifest: 'package.json',
Expand Down Expand Up @@ -48,6 +52,19 @@ let rubyChange: Change = {
]
}

let noVulnNpmChange: Change = {
manifest: 'package.json',
change_type: 'added',
ecosystem: 'npm',
name: 'helpful',
version: '1.0.0',
package_url: 'pkg:npm/helpful@1.0.0',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: []
}

test('it properly filters changes by severity', async () => {
const changes = [npmChange, rubyChange]
let result = filterChangesBySeverity('high', changes)
Expand All @@ -72,3 +89,29 @@ test('it properly filters changes by scope', async () => {
result = filterChangesByScopes(['runtime', 'development'], changes)
expect(result).toEqual([npmChange, rubyChange])
})

test('it properly filters changes with allowed vulnerabilities', async () => {
const changes = [npmChange, rubyChange, noVulnNpmChange]

let result = filterOutAllowedAdvisories(['notrealGHSAID'], changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])

result = filterOutAllowedAdvisories(['first-random_string'], changes)
expect(result).toEqual([rubyChange, noVulnNpmChange])

result = filterOutAllowedAdvisories(
['second-random_string', 'third-random_string'],
changes
)
expect(result).toEqual([npmChange, noVulnNpmChange])

result = filterOutAllowedAdvisories(
['first-random_string', 'second-random_string', 'third-random_string'],
changes
)
expect(result).toEqual([noVulnNpmChange])

// if we have a change with multiple vulnerabilities but only one is allowed, we still should not filter out that change
result = filterOutAllowedAdvisories(['second-random_string'], changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
})
41 changes: 34 additions & 7 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions src/config.ts
Expand Up @@ -47,21 +47,24 @@ export function readInlineConfig(): ConfigurationOptions {
.default(['runtime'])
.parse(parseList(getOptionalInput('fail-on-scopes')))

const allow_licenses = getOptionalInput('allow-licenses')
const deny_licenses = getOptionalInput('deny-licenses')
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
const deny_licenses = parseList(getOptionalInput('deny-licenses'))

if (allow_licenses !== undefined && deny_licenses !== undefined) {
throw new Error("Can't specify both allow_licenses and deny_licenses")
}

const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))

const base_ref = getOptionalInput('base-ref')
const head_ref = getOptionalInput('head-ref')

return {
fail_on_severity,
fail_on_scopes,
allow_licenses: parseList(allow_licenses),
deny_licenses: parseList(deny_licenses),
allow_licenses,
deny_licenses,
allow_ghsas,
base_ref,
head_ref
}
Expand Down
28 changes: 28 additions & 0 deletions src/filter.ts
Expand Up @@ -46,3 +46,31 @@ export function filterChangesByScopes(

return filteredChanges
}

export function filterOutAllowedAdvisories(
ghsas: string[],
changes: Changes
): Changes {
const filteredChanges = changes.filter(change => {
const noAdvisories =
change.vulnerabilities === undefined ||
change.vulnerabilities.length === 0

if (noAdvisories) {
return true
}

let allAllowedAdvisories = true
// if there's at least one advisory that is not allowlisted, we will keep the change
for (const vulnerability of change.vulnerabilities) {
if (!ghsas.includes(vulnerability.advisory_ghsa_id)) {
allAllowedAdvisories = false
}
if (!allAllowedAdvisories) {
return true
}
}
})

return filteredChanges
}
17 changes: 14 additions & 3 deletions src/main.ts
Expand Up @@ -5,7 +5,11 @@ import styles from 'ansi-styles'
import {RequestError} from '@octokit/request-error'
import {Change, Severity, Scope} from './schemas'
import {readConfig} from '../src/config'
import {filterChangesBySeverity, filterChangesByScopes} from '../src/filter'
import {
filterChangesBySeverity,
filterChangesByScopes,
filterOutAllowedAdvisories
} from '../src/filter'
import {getDeniedLicenseChanges} from './licenses'
import * as summary from './summary'
import {getRefs} from './git-refs'
Expand Down Expand Up @@ -34,9 +38,16 @@ async function run(): Promise<void> {

const scopedChanges = filterChangesByScopes(scopes as Scope[], changes)

const allowedGhsas: string[] = config.allow_ghsas || []

const filteredChanges = filterOutAllowedAdvisories(
allowedGhsas,
scopedChanges
)

const addedChanges = filterChangesBySeverity(
minSeverity as Severity,
scopedChanges
filteredChanges
).filter(
change =>
change.change_type === 'added' &&
Expand All @@ -45,7 +56,7 @@ async function run(): Promise<void> {
)

const [licenseErrors, unknownLicenses] = getDeniedLicenseChanges(
scopedChanges,
filteredChanges,
licenses
)

Expand Down
1 change: 1 addition & 0 deletions src/schemas.ts
Expand Up @@ -40,6 +40,7 @@ export const ConfigurationOptionsSchema = z
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
allow_licenses: z.array(z.string()).default([]),
deny_licenses: z.array(z.string()).default([]),
allow_ghsas: z.array(z.string()).default([]),
config_file: z.string().optional().default('false'),
base_ref: z.string(),
head_ref: z.string()
Expand Down