Skip to content

Commit

Permalink
Merge pull request #251 from actions/sarahkemi/ghsa-allowlist
Browse files Browse the repository at this point in the history
Filter by vulnerability allow-list
  • Loading branch information
sarahkemi committed Sep 23, 2022
2 parents 2843194 + 716b322 commit 98f28eb
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 22 deletions.
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 GitHub Advisory IDs that can be skipped during detection.

**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])
})
3 changes: 3 additions & 0 deletions action.yml
Expand Up @@ -29,6 +29,9 @@ inputs:
deny-licenses:
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
required: false
allow-ghsas:
description: Comma-separated list of allowed Github Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
required: false
runs:
using: 'node16'
main: 'dist/index.js'
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

0 comments on commit 98f28eb

Please sign in to comment.