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 blocking dependency changes by scopes #243

Merged
merged 8 commits into from Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 25 additions & 12 deletions README.md
Expand Up @@ -38,7 +38,7 @@ jobs:

### GitHub Enterprise Server

This action is available in GHES starting with version 3.6. Make sure
This action is available in Enterprise Server starting with version 3.6. Make sure
[GitHub Advanced
Security](https://docs.github.com/en/enterprise-server@3.6/admin/code-security/managing-github-advanced-security-for-your-enterprise/enabling-github-advanced-security-for-your-enterprise)
and [GitHub
Expand All @@ -50,7 +50,6 @@ with the label of any of your runners (the default label
is `self-hosted`):

```yaml

# ...

jobs:
Expand Down Expand Up @@ -86,11 +85,14 @@ jobs:
# Possible values: "critical", "high", "moderate", "low"
# fail-on-severity: critical
#
# Possible values in comma separated list: "unknown", "runtime", or "development"
# fail-on-scopes: runtime, development
#
# Possible values: Any available git ref
# base-ref: ${{ github.event.pull_request.base.ref }}
# head-ref: ${{ github.event.pull_request.head.ref }}
#
# You can only include one of these two options: `allow-licenses` and `deny-licenses`. These options are not supported on GHES.
# You can only include one of these two options: `allow-licenses` and `deny-licenses`. These options are not supported on Enterprise Server.
#
# Possible values: Any `spdx_id` value(s) from https://docs.github.com/en/rest/licenses
# allow-licenses: GPL-3.0, BSD-3-Clause, MIT
Expand Down Expand Up @@ -120,12 +122,23 @@ This example will only fail on pull requests with `critical` and `high` vulnerab
fail-on-severity: high
```

### Dependency Scoping

By default the action will only fail on `runtime` dependencies that have vulnerabilities or unacceptable licenses, ignoring `development` dependencies. You can override this behavior with the `fail-on-scopes` option, which will allow you to list the specific dependency scopes you care about. The possible values are: `unknown`, `runtime`, and `development`. Note: Filtering by scope will not be supported on Enterprise Server just yet, as the REST API's introduction of `scope` will be released in an upcoming Enterprise Server version. We will treat all dependencies on Enterprise Server as having a `runtime` scope and thus will not be filtered away.

```yaml
- name: Dependency Review
uses: actions/dependency-review-action@v2
with:
fail-on-scopes: runtime, development
```

### 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. These options are not supported on GHES.
forbid a subset of licenses. These options are not supported on Enterprise Server.

You can use the [Licenses
API](https://docs.github.com/en/rest/licenses) to see the full list of
Expand All @@ -150,14 +163,14 @@ to filter. A couple of examples:

**Important**

* Checking for licenses is not supported on GHES.
* 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**.
- Checking for licenses is not supported on Enterprise Server.
- 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

Expand Down
20 changes: 20 additions & 0 deletions __tests__/config.test.ts
Expand Up @@ -13,6 +13,7 @@ function setInput(input: string, value: string) {
function clearInputs() {
const allowedOptions = [
'FAIL-ON-SEVERITY',
'FAIL-ON-SCOPES',
'ALLOW-LICENSES',
'DENY-LICENSES',
'BASE-REF',
Expand Down Expand Up @@ -82,3 +83,22 @@ test('it raises an error when no refs are provided and the event is not a pull r
})
).toThrow()
})

test('it defaults to runtime scope', async () => {
const options = readConfig()
expect(options.fail_on_scopes).toEqual(['runtime'])
})
test('it parses custom scopes preference', async () => {
setInput('fail-on-scopes', 'runtime, development')
let options = readConfig()
expect(options.fail_on_scopes).toEqual(['runtime', 'development'])

clearInputs()
setInput('fail-on-scopes', 'development')
options = readConfig()
expect(options.fail_on_scopes).toEqual(['development'])
})
test('it raises an error when given invalid scope', async () => {
setInput('fail-on-scopes', 'runtime, zombies')
expect(() => readConfig()).toThrow()
})
17 changes: 16 additions & 1 deletion __tests__/filter.test.ts
@@ -1,6 +1,6 @@
import {expect, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
import {filterChangesBySeverity} from '../src/filter'
import {filterChangesBySeverity, filterChangesByScopes} from '../src/filter'

let npmChange: Change = {
manifest: 'package.json',
Expand All @@ -11,6 +11,7 @@ let npmChange: Change = {
package_url: 'pkg:npm/reeuhq@1.0.2',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'critical',
Expand All @@ -30,6 +31,7 @@ let rubyChange: Change = {
package_url: 'pkg:gem/actionsomething@3.2.0',
license: 'BSD',
source_repository_url: 'github.com/some-repo',
scope: 'development',
vulnerabilities: [
{
severity: 'moderate',
Expand Down Expand Up @@ -57,3 +59,16 @@ test('it properly filters changes by severity', async () => {
result = filterChangesBySeverity('critical', changes)
expect(changes).toEqual([npmChange, rubyChange])
})

test('it properly filters changes by scope', async () => {
const changes = [npmChange, rubyChange]

let result = filterChangesByScopes(['runtime'], changes)
expect(result).toEqual([npmChange])

result = filterChangesByScopes(['development'], changes)
expect(result).toEqual([rubyChange])

result = filterChangesByScopes(['runtime', 'development'], changes)
expect(result).toEqual([npmChange, rubyChange])
})
2 changes: 2 additions & 0 deletions __tests__/licenses.test.ts
Expand Up @@ -11,6 +11,7 @@ let npmChange: Change = {
package_url: 'pkg:npm/reeuhq@1.0.2',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'critical',
Expand All @@ -30,6 +31,7 @@ let rubyChange: Change = {
package_url: 'pkg:gem/actionsomething@3.2.0',
license: 'BSD',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'moderate',
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Expand Up @@ -10,6 +10,10 @@ inputs:
description: Don't block PRs below this severity. Possible values are `low`, `moderate`, `high`, `critical`.
required: false
default: 'low'
fail-on-scopes:
description: Dependency scopes to block PRs on. Comma-separated list. Possible values are 'unknown', 'runtime', and 'development' (e.g. "runtime, development")
required: false
default: 'runtime'
base-ref:
description: The base git ref to be used for this check. Has a default value when the workflow event is `pull_request` or `pull_request_target`. Must be provided otherwise.
required: false
Expand Down
48 changes: 39 additions & 9 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.

19 changes: 16 additions & 3 deletions src/config.ts
@@ -1,17 +1,29 @@
import * as core from '@actions/core'
import * as z from 'zod'
import {ConfigurationOptions, SEVERITIES} from './schemas'
import {ConfigurationOptions, SEVERITIES, SCOPES} from './schemas'

function getOptionalInput(name: string): string | undefined {
const value = core.getInput(name)
return value.length > 0 ? value : undefined
}

function parseList(list: string | undefined): string[] | undefined {
if (list === undefined) {
return list
} else {
return list.split(',').map(x => x.trim())
}
}

export function readConfig(): ConfigurationOptions {
const fail_on_severity = z
.enum(SEVERITIES)
.default('low')
.parse(getOptionalInput('fail-on-severity'))
const fail_on_scopes = z
.array(z.enum(SCOPES))
.default(['runtime'])
.parse(parseList(getOptionalInput('fail-on-scopes')))
const allow_licenses = getOptionalInput('allow-licenses')
const deny_licenses = getOptionalInput('deny-licenses')

Expand All @@ -24,8 +36,9 @@ export function readConfig(): ConfigurationOptions {

return {
fail_on_severity,
allow_licenses: allow_licenses?.split(',').map(x => x.trim()),
deny_licenses: deny_licenses?.split(',').map(x => x.trim()),
fail_on_scopes,
allow_licenses: parseList(allow_licenses),
deny_licenses: parseList(deny_licenses),
base_ref,
head_ref
}
Expand Down
15 changes: 14 additions & 1 deletion src/filter.ts
@@ -1,4 +1,4 @@
import {Changes, Severity, SEVERITIES} from './schemas'
import {Changes, Severity, SEVERITIES, Scope} from './schemas'

export function filterChangesBySeverity(
severity: Severity,
Expand Down Expand Up @@ -33,3 +33,16 @@ export function filterChangesBySeverity(
)
return filteredChanges
}

export function filterChangesByScopes(
scopes: Scope[],
changes: Changes
): Changes {
const filteredChanges = changes.filter(change => {
// if there is no scope on the change (Enterprise Server API for now), we will assume it is a runtime scope
const scope = change.scope || 'runtime'
return scopes.includes(scope)
})

return filteredChanges
}