Compare commits

..
1 Commits
Author SHA1 Message Date
Federico Builes 1ade604b58 adding spdx-satisfies 2022-10-04 15:46:53 +02:00
13 changed files with 543 additions and 20158 deletions
+1
View File
@@ -0,0 +1 @@
fail-on-severity: low
+16 -24
View File
@@ -1,5 +1,4 @@
# Contributing # Contributing
[fork]: https://github.com/actions/dependency-review-action/fork [fork]: https://github.com/actions/dependency-review-action/fork
[pr]: https://github.com/actions/dependency-review-action/compare [pr]: https://github.com/actions/dependency-review-action/compare
[code-of-conduct]: CODE_OF_CONDUCT.md [code-of-conduct]: CODE_OF_CONDUCT.md
@@ -10,6 +9,7 @@ Contributions to this project are
[released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license)
to the public under the [project's open source license](LICENSE). to the public under the [project's open source license](LICENSE).
Please note that this project is released with a [Contributor Code of Please note that this project is released with a [Contributor Code of
Conduct][code-of-conduct]. By participating in this project you agree Conduct][code-of-conduct]. By participating in this project you agree
to abide by its terms. to abide by its terms.
@@ -20,6 +20,7 @@ This Action makes an authenticated query to the Dependency Graph Diff
API endpoint (`GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}`) API endpoint (`GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}`)
to find out the set of added and removed dependencies for each manifest. to find out the set of added and removed dependencies for each manifest.
### Bootstrapping the project ### Bootstrapping the project
``` ```
@@ -34,7 +35,7 @@ npm install
npm run test npm run test
``` ```
_Note_: We don't have any useful tests yet, contributions are welcome! *Note*: We don't have any useful tests yet, contributions are welcome!
## Local Development ## Local Development
@@ -55,24 +56,16 @@ Like this:
$ GITHUB_TOKEN=my-secret-token ./scripts/scan_pr https://github.com/actions/dependency-review-action/pull/3 $ GITHUB_TOKEN=my-secret-token ./scripts/scan_pr https://github.com/actions/dependency-review-action/pull/3
``` ```
[Configuration options](README.md#configuration-options) can be set by
passing an external YAML [configuration file](README.md#configuration-file) to the
`scan_pr` script with the `-c`/`--config-file` option:
```sh
$ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_url>
```
## Submitting a pull request ## Submitting a pull request
0. [Fork][fork] and clone the repository 0. [Fork][fork] and clone the repository
1. Configure and install the dependencies: `npm install` 0. Configure and install the dependencies: `npm install`
2. Make sure the tests pass on your machine: `npm run test` 0. Make sure the tests pass on your machine: `npm run test`
3. Create a new branch: `git checkout -b my-branch-name` 0. Create a new branch: `git checkout -b my-branch-name`
4. Make your change, add tests, and make sure the tests still pass 0. Make your change, add tests, and make sure the tests still pass
5. Make sure to build and package before pushing: `npm run build && npm run package` 0. Make sure to build and package before pushing: `npm run build && npm run package`
6. Push to your fork and [submit a pull request][pr] 0. Push to your fork and [submit a pull request][pr]
7. Pat your self on the back and wait for your pull request to be reviewed and merged. 0. Pat your self on the back and wait for your pull request to be reviewed and merged.
Here are a few things you can do that will increase the likelihood of your pull request being accepted: Here are a few things you can do that will increase the likelihood of your pull request being accepted:
@@ -84,21 +77,21 @@ Here are a few things you can do that will increase the likelihood of your pull
1. Update the version number in [package.json](https://github.com/actions/dependency-review-action/blob/main/package.json). 1. Update the version number in [package.json](https://github.com/actions/dependency-review-action/blob/main/package.json).
1. Go to [Draft a new 1. Go to [Draft a new
release](https://github.com/actions/dependency-review-action/releases/new) release](https://github.com/actions/dependency-review-action/releases/new)
in the Releases page. in the Releases page.
1. Make sure that the `Publish this Action to the GitHub Marketplace` 2. Make sure that the `Publish this Action to the GitHub Marketplace`
checkbox is enabled checkbox is enabled
<img width="481" alt="Screenshot 2022-06-15 at 12 08 19" src="https://user-images.githubusercontent.com/2161/173822484-4b60d8b4-c674-4bff-b5ff-b0c4a3650ab7.png"> <img width="481" alt="Screenshot 2022-06-15 at 12 08 19" src="https://user-images.githubusercontent.com/2161/173822484-4b60d8b4-c674-4bff-b5ff-b0c4a3650ab7.png">
3. Click "Choose a tag" and then "Create new tag", where the tag name 3. Click "Choose a tag" and then "Create new tag", where the tag name
will be your version prefixed by a `v` (e.g. `v1.2.3`). will be your version prefixed by a `v` (e.g. `v1.2.3`).
4. Use a version number for the release title (e.g. "1.2.3"). 4. Use a version number for the release title (e.g. "1.2.3").
<img width="700" alt="Screenshot 2022-06-15 at 12 08 36" src="https://user-images.githubusercontent.com/2161/173822548-33ab3432-d679-4dc1-adf8-b50fdaf47de3.png"> <img width="700" alt="Screenshot 2022-06-15 at 12 08 36" src="https://user-images.githubusercontent.com/2161/173822548-33ab3432-d679-4dc1-adf8-b50fdaf47de3.png">
5. Add your release notes. If this is a major version make sure to 5. Add your release notes. If this is a major version make sure to
include a small description of the biggest changes in the new version. include a small description of the biggest changes in the new version.
6. Click "Publish Release". 6. Click "Publish Release".
You now have a tag and release using the semver version you used You now have a tag and release using the semver version you used
@@ -109,7 +102,6 @@ automatically getting all the
minor/patch updates. minor/patch updates.
To do this just checkout `main`, force-create a new annotated tag, and push it: To do this just checkout `main`, force-create a new annotated tag, and push it:
``` ```
git tag -fa v2 -m "Updating v2 to 2.3.4" git tag -fa v2 -m "Updating v2 to 2.3.4"
git push origin v2 --force git push origin v2 --force
+6 -12
View File
@@ -3,7 +3,7 @@ import {Change, Changes} from '../src/schemas'
import { import {
filterChangesBySeverity, filterChangesBySeverity,
filterChangesByScopes, filterChangesByScopes,
filterAllowedAdvisories filterOutAllowedAdvisories
} from '../src/filter' } from '../src/filter'
let npmChange: Change = { let npmChange: Change = {
@@ -90,34 +90,28 @@ test('it properly filters changes by scope', async () => {
expect(result).toEqual([npmChange, rubyChange]) expect(result).toEqual([npmChange, rubyChange])
}) })
test('it properly handles undefined advisory IDs', async () => {
const changes = [npmChange, rubyChange, noVulnNpmChange]
let result = filterAllowedAdvisories(undefined, changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
})
test('it properly filters changes with allowed vulnerabilities', async () => { test('it properly filters changes with allowed vulnerabilities', async () => {
const changes = [npmChange, rubyChange, noVulnNpmChange] const changes = [npmChange, rubyChange, noVulnNpmChange]
let result = filterAllowedAdvisories(['notrealGHSAID'], changes) let result = filterOutAllowedAdvisories(['notrealGHSAID'], changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange]) expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
result = filterAllowedAdvisories(['first-random_string'], changes) result = filterOutAllowedAdvisories(['first-random_string'], changes)
expect(result).toEqual([rubyChange, noVulnNpmChange]) expect(result).toEqual([rubyChange, noVulnNpmChange])
result = filterAllowedAdvisories( result = filterOutAllowedAdvisories(
['second-random_string', 'third-random_string'], ['second-random_string', 'third-random_string'],
changes changes
) )
expect(result).toEqual([npmChange, noVulnNpmChange]) expect(result).toEqual([npmChange, noVulnNpmChange])
result = filterAllowedAdvisories( result = filterOutAllowedAdvisories(
['first-random_string', 'second-random_string', 'third-random_string'], ['first-random_string', 'second-random_string', 'third-random_string'],
changes changes
) )
expect(result).toEqual([noVulnNpmChange]) 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 // if we have a change with multiple vulnerabilities but only one is allowed, we still should not filter out that change
result = filterAllowedAdvisories(['second-random_string'], changes) result = filterOutAllowedAdvisories(['second-random_string'], changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange]) expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
}) })
+7 -78
View File
@@ -1,4 +1,4 @@
import {expect, jest, test} from '@jest/globals' import {expect, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas' import {Change, Changes} from '../src/schemas'
import {getDeniedLicenseChanges} from '../src/licenses' import {getDeniedLicenseChanges} from '../src/licenses'
@@ -48,41 +48,15 @@ let rubyChange: Change = {
] ]
} }
jest.mock('@actions/core')
const mockOctokit = {
rest: {
licenses: {
getForRepo: jest
.fn()
.mockReturnValue({data: {license: {spdx_id: 'AGPL'}}})
}
}
}
jest.mock('octokit', () => {
return {
Octokit: class {
constructor() {
return mockOctokit
}
}
}
})
test('it fails if a license outside the allow list is found', async () => { test('it fails if a license outside the allow list is found', async () => {
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
allow: ['BSD']
})
expect(invalidChanges[0]).toBe(npmChange) expect(invalidChanges[0]).toBe(npmChange)
}) })
test('it fails if a license inside the deny list is found', async () => { test('it fails if a license inside the deny list is found', async () => {
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
const [invalidChanges] = await getDeniedLicenseChanges(changes, { const [invalidChanges] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
deny: ['BSD']
})
expect(invalidChanges[0]).toBe(rubyChange) expect(invalidChanges[0]).toBe(rubyChange)
}) })
@@ -90,7 +64,7 @@ test('it fails if a license inside the deny list is found', async () => {
// thing we want in the system. Please remove this test after refactoring. // thing we want in the system. Please remove this test after refactoring.
test('it fails all license checks when allow is provided an empty array', async () => { test('it fails all license checks when allow is provided an empty array', async () => {
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
let [invalidChanges, _] = await getDeniedLicenseChanges(changes, { let [invalidChanges, _] = getDeniedLicenseChanges(changes, {
allow: [], allow: [],
deny: ['BSD'] deny: ['BSD']
}) })
@@ -102,9 +76,7 @@ test('it does not fail if a license outside the allow list is found in removed c
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
allow: ['BSD']
})
expect(invalidChanges).toStrictEqual([]) expect(invalidChanges).toStrictEqual([])
}) })
@@ -113,9 +85,7 @@ test('it does not fail if a license inside the deny list is found in removed cha
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const [invalidChanges, _] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
deny: ['BSD']
})
expect(invalidChanges).toStrictEqual([]) expect(invalidChanges).toStrictEqual([])
}) })
@@ -125,47 +95,6 @@ test('it fails if a license outside the allow list is found in both of added and
npmChange, npmChange,
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
allow: ['BSD']
})
expect(invalidChanges).toStrictEqual([npmChange]) expect(invalidChanges).toStrictEqual([npmChange])
}) })
describe('GH License API fallback', () => {
test('it calls licenses endpoint if atleast one of the changes has null license and valid source_repository_url', async () => {
const nullLicenseChange = {
...npmChange,
license: null,
source_repository_url: 'http://github.com/some-owner/some-repo'
}
const [_, unknownChanges] = await getDeniedLicenseChanges(
[nullLicenseChange, rubyChange],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).toHaveBeenNthCalledWith(1, {
owner: 'some-owner',
repo: 'some-repo'
})
expect(unknownChanges.length).toEqual(0)
})
test('it does not call licenses API endpoint for change with null license and invalid source_repository_url ', async () => {
const [_, unknownChanges] = await getDeniedLicenseChanges(
[{...npmChange, license: null}],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unknownChanges.length).toEqual(1)
})
test('it does not call licenses API endpoint if licenses for all changes are present', async () => {
const [_, unknownChanges] = await getDeniedLicenseChanges(
[npmChange, rubyChange],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unknownChanges.length).toEqual(0)
})
})
Generated Vendored
+100 -16633
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
Generated Vendored
-1049
View File
File diff suppressed because it is too large Load Diff
+387 -2235
View File
File diff suppressed because it is too large Load Diff
+12 -13
View File
@@ -1,6 +1,6 @@
{ {
"name": "dependency-review-action", "name": "dependency-review-action",
"version": "2.5.1", "version": "2.4.0",
"private": true, "private": true,
"description": "A GitHub Action for Dependency Review", "description": "A GitHub Action for Dependency Review",
"main": "lib/main.js", "main": "lib/main.js",
@@ -28,24 +28,23 @@
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
"@actions/github": "^5.1.1", "@actions/github": "^5.1.1",
"@octokit/plugin-retry": "^3.0.9", "@octokit/plugin-retry": "^3.0.9",
"@octokit/request-error": "^3.0.2", "@octokit/request-error": "^3.0.1",
"ansi-styles": "^6.2.1", "ansi-styles": "^6.1.1",
"got": "^12.5.2", "got": "^12.5.1",
"nodemon": "^2.0.20", "nodemon": "^2.0.20",
"octokit": "^2.0.9", "spdx-satisfies": "^5.0.1",
"yaml": "^2.1.3", "yaml": "^2.1.2",
"zod": "^3.19.1" "zod": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.5.2", "@types/node": "^16.11.63",
"@types/node": "^16.18.0", "@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/parser": "^5.38.1",
"@typescript-eslint/parser": "^5.40.1",
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.34.0",
"esbuild-register": "^3.3.3", "esbuild-register": "^3.3.3",
"eslint": "^8.26.0", "eslint": "^8.24.0",
"eslint-plugin-github": "^4.4.0", "eslint-plugin-github": "^4.3.7",
"eslint-plugin-jest": "^27.1.3", "eslint-plugin-jest": "^27.0.4",
"jest": "^27.5.1", "jest": "^27.5.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"nodemon": "^2.0.20", "nodemon": "^2.0.20",
+3 -34
View File
@@ -3,52 +3,22 @@ require 'json'
require 'tempfile' require 'tempfile'
require 'open3' require 'open3'
require 'bundler/inline' require 'bundler/inline'
require 'optparse'
gemfile do gemfile do
source 'https://rubygems.org' source 'https://rubygems.org'
gem 'octokit' gem 'octokit'
end end
config_file = nil
github_token = ENV["GITHUB_TOKEN"] github_token = ENV["GITHUB_TOKEN"]
if !github_token || github_token.empty? if !github_token || github_token.empty?
puts "Please set the GITHUB_TOKEN environment variable" puts "Please set the GITHUB_TOKEN environment variable"
exit -1 exit -1
end end
op = OptionParser.new do |opts| arg = /(?<repo_nwo>[\w\-]+\/[\w\-]+)\/pull\/(?<pr_number>\d+)/.match(ARGV[0])
usage = <<EOF
Run Dependency Review on a repository.
\e[1mUsage:\e[22m
scripts/scan_pr [options] <pr_url>
\e[1mExample:\e[22m
scripts/scan_pr https://github.com/actions/dependency-review-action/pull/294
EOF
opts.banner = usage
opts.on('-c', '--config-file <FILE>', 'Use an external configuration file') do |cf|
config_file = cf
end
opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end
op.parse!
# make sure we have a NWO somewhere in the parameters
arg = /(?<repo_nwo>[\w\-]+\/[\w\-]+)\/pull\/(?<pr_number>\d+)/.match(ARGV.join(" "))
if arg.nil? if arg.nil?
puts op puts "Usage: script/scan_pr <pr_url>"
exit -1 exit -1
end end
@@ -63,8 +33,7 @@ event_file.write("{ \"pull_request\": #{pr.to_h.to_json}}")
event_file.close event_file.close
action_inputs = { action_inputs = {
"repo-token": github_token, "repo-token" => github_token
"config-file": config_file
} }
dev_cmd_env = { dev_cmd_env = {
+2 -10
View File
@@ -51,20 +51,12 @@ export function filterChangesByScopes(
return filteredChanges return filteredChanges
} }
/** export function filterOutAllowedAdvisories(
* Filter out changes that are allowed by the allow_ghsas config
* option. We want to remove these changes before we do any
* processing.
* @param ghsas - list of GHSA IDs to allow
* @param changes - list of changes to filter
* @returns a list of changes with the allowed GHSAs removed
*/
export function filterAllowedAdvisories(
ghsas: string[] | undefined, ghsas: string[] | undefined,
changes: Changes changes: Changes
): Changes { ): Changes {
if (ghsas === undefined) { if (ghsas === undefined) {
return changes return []
} }
const filteredChanges = changes.filter(change => { const filteredChanges = changes.filter(change => {
+4 -65
View File
@@ -1,5 +1,3 @@
import * as core from '@actions/core'
import {Octokit} from 'octokit'
import {Change} from './schemas' import {Change} from './schemas'
/** /**
@@ -12,27 +10,21 @@ import {Change} from './schemas'
* we will ignore the deny list. * we will ignore the deny list.
* @param {Change[]} changes The list of changes to filter. * @param {Change[]} changes The list of changes to filter.
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses. * @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
* @returns {Promise<[Array.<Change>, Array.<Change>]>} A promise to a 2 element tuple. The first element is the list of denied changes and the second one is the list of changes with unknown licenses * @returns {[Array<Change>, Array<Change]} A tuple where the first element is the list of denied changes and the second one is the list of changes with unknown licenses
*/ */
export async function getDeniedLicenseChanges( export function getDeniedLicenseChanges(
changes: Change[], changes: Change[],
licenses: { licenses: {
allow?: string[] allow?: string[]
deny?: string[] deny?: string[]
} }
): Promise<[Change[], Change[]]> { ): [Change[], Change[]] {
const {allow, deny} = licenses const {allow, deny} = licenses
const disallowed: Change[] = [] const disallowed: Change[] = []
const unknown: Change[] = [] const unknown: Change[] = []
const consolidatedChanges = changes.some( for (const change of changes) {
({source_repository_url, license}) => !license && source_repository_url
)
? await setGHLicenses(changes)
: changes
for (const change of consolidatedChanges) {
if (change.change_type === 'removed') { if (change.change_type === 'removed') {
continue continue
} }
@@ -55,56 +47,3 @@ export async function getDeniedLicenseChanges(
return [disallowed, unknown] return [disallowed, unknown]
} }
const fetchGHLicense = async (
owner: string,
repo: string
): Promise<string | null> => {
const octokit = new Octokit({
auth: core.getInput('repo-token', {required: true})
})
try {
const response = await octokit.rest.licenses.getForRepo({owner, repo})
return response.data.license?.spdx_id ?? null
} catch (_) {
return null
}
}
const parseGitHubURL = (url: string): {owner: string; repo: string} | null => {
try {
const parsed = new URL(url)
if (parsed.host !== 'github.com') {
return null
}
const components = parsed.pathname.split('/')
if (components.length < 3) {
return null
}
return {owner: components[1], repo: components[2]}
} catch (_) {
return null
}
}
const setGHLicenses = async (changes: Change[]): Promise<Change[]> => {
const updatedChanges = changes.map(async change => {
if (change.license !== null || change.source_repository_url === null) {
return change
}
const githubUrl = parseGitHubURL(change.source_repository_url)
if (githubUrl === null) {
return change
}
return {
...change,
license: await fetchGHLicense(githubUrl.owner, githubUrl.repo)
}
})
return Promise.all(updatedChanges)
}
+4 -4
View File
@@ -8,7 +8,7 @@ import {readConfig} from '../src/config'
import { import {
filterChangesBySeverity, filterChangesBySeverity,
filterChangesByScopes, filterChangesByScopes,
filterAllowedAdvisories filterOutAllowedAdvisories
} from '../src/filter' } from '../src/filter'
import {getDeniedLicenseChanges} from './licenses' import {getDeniedLicenseChanges} from './licenses'
import * as summary from './summary' import * as summary from './summary'
@@ -30,7 +30,7 @@ async function run(): Promise<void> {
const minSeverity = config.fail_on_severity as Severity const minSeverity = config.fail_on_severity as Severity
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes) const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
const filteredChanges = filterAllowedAdvisories( const filteredChanges = filterOutAllowedAdvisories(
config.allow_ghsas, config.allow_ghsas,
scopedChanges scopedChanges
) )
@@ -45,7 +45,7 @@ async function run(): Promise<void> {
change.vulnerabilities.length > 0 change.vulnerabilities.length > 0
) )
const [licenseErrors, unknownLicenses] = await getDeniedLicenseChanges( const [licenseErrors, unknownLicenses] = getDeniedLicenseChanges(
filteredChanges, filteredChanges,
{ {
allow: config.allow_licenses, allow: config.allow_licenses,
@@ -192,7 +192,7 @@ function renderScannedDependency(change: Change): string {
} as const } as const
)[changeType] )[changeType]
return `${styles.color[color].open}${icon} ${change.name}@${change.version}${styles.color[color].close}` return `${styles.color[color].open}${icon} ${change.manifest}@${change.version}${styles.color[color].close}`
} }
function printScannedDependencies(changes: Changes): void { function printScannedDependencies(changes: Changes): void {