Compare commits
45 Commits
parse-spdx
...
v2.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
| fd675ced9c | |||
| f7d03d8b76 | |||
| 7e41a6f1ee | |||
| 4c0961eff6 | |||
| d1e9a12830 | |||
| 2e3713aab8 | |||
| ba9d7c1389 | |||
| 0cd2781117 | |||
| 129f0ad973 | |||
| 0a88a4704b | |||
| 18069caed8 | |||
| 61cee4b12b | |||
| 94670a1af8 | |||
| 577d9714ad | |||
| 9ce6cb532b | |||
| 0b980b1ccd | |||
| bc5f6c2f39 | |||
| 9c96258789 | |||
| f076f221f4 | |||
| 88b817ec8d | |||
| 2dd6c6a3d7 | |||
| 1d9bfbbddf | |||
| f632f5f79d | |||
| ee42a6512f | |||
| 6f58092362 | |||
| b81bfe53ce | |||
| 5679c0f8be | |||
| 2018b3e66f | |||
| 463890c1ed | |||
| c9b9d23e75 | |||
| 4c14cfe593 | |||
| 5b70fe08e7 | |||
| 81216f689b | |||
| afbc15c97f | |||
| 8d974c4ee8 | |||
| cdad98596a | |||
| 0a0eb39992 | |||
| df3ceaf7f0 | |||
| 1997789b86 | |||
| 584e620d09 | |||
| 1fa34689ad | |||
| de2814d20e | |||
| eabc27054f | |||
| b486e073e9 | |||
| 03321307df |
@@ -3,7 +3,7 @@ import {Change, Changes} from '../src/schemas'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
filterChangesByScopes,
|
||||
filterOutAllowedAdvisories
|
||||
filterAllowedAdvisories
|
||||
} from '../src/filter'
|
||||
|
||||
let npmChange: Change = {
|
||||
@@ -90,28 +90,34 @@ test('it properly filters changes by scope', async () => {
|
||||
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 () => {
|
||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
||||
|
||||
let result = filterOutAllowedAdvisories(['notrealGHSAID'], changes)
|
||||
let result = filterAllowedAdvisories(['notrealGHSAID'], changes)
|
||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
|
||||
result = filterOutAllowedAdvisories(['first-random_string'], changes)
|
||||
result = filterAllowedAdvisories(['first-random_string'], changes)
|
||||
expect(result).toEqual([rubyChange, noVulnNpmChange])
|
||||
|
||||
result = filterOutAllowedAdvisories(
|
||||
result = filterAllowedAdvisories(
|
||||
['second-random_string', 'third-random_string'],
|
||||
changes
|
||||
)
|
||||
expect(result).toEqual([npmChange, noVulnNpmChange])
|
||||
|
||||
result = filterOutAllowedAdvisories(
|
||||
result = filterAllowedAdvisories(
|
||||
['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)
|
||||
result = filterAllowedAdvisories(['second-random_string'], changes)
|
||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {expect, jest, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
import {getDeniedLicenseChanges} from '../src/licenses'
|
||||
|
||||
@@ -48,15 +48,41 @@ 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 () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: ['BSD']
|
||||
})
|
||||
expect(invalidChanges[0]).toBe(npmChange)
|
||||
})
|
||||
|
||||
test('it fails if a license inside the deny list is found', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const [invalidChanges] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
|
||||
const [invalidChanges] = await getDeniedLicenseChanges(changes, {
|
||||
deny: ['BSD']
|
||||
})
|
||||
expect(invalidChanges[0]).toBe(rubyChange)
|
||||
})
|
||||
|
||||
@@ -64,7 +90,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.
|
||||
test('it fails all license checks when allow is provided an empty array', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
let [invalidChanges, _] = getDeniedLicenseChanges(changes, {
|
||||
let [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: [],
|
||||
deny: ['BSD']
|
||||
})
|
||||
@@ -76,7 +102,9 @@ test('it does not fail if a license outside the allow list is found in removed c
|
||||
{...npmChange, change_type: 'removed'},
|
||||
{...rubyChange, change_type: 'removed'}
|
||||
]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: ['BSD']
|
||||
})
|
||||
expect(invalidChanges).toStrictEqual([])
|
||||
})
|
||||
|
||||
@@ -85,7 +113,9 @@ test('it does not fail if a license inside the deny list is found in removed cha
|
||||
{...npmChange, change_type: 'removed'},
|
||||
{...rubyChange, change_type: 'removed'}
|
||||
]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
deny: ['BSD']
|
||||
})
|
||||
expect(invalidChanges).toStrictEqual([])
|
||||
})
|
||||
|
||||
@@ -95,6 +125,47 @@ test('it fails if a license outside the allow list is found in both of added and
|
||||
npmChange,
|
||||
{...rubyChange, change_type: 'removed'}
|
||||
]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: ['BSD']
|
||||
})
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
+16646
-100
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1049
File diff suppressed because it is too large
Load Diff
Generated
+1974
-251
File diff suppressed because it is too large
Load Diff
+12
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dependency-review-action",
|
||||
"version": "2.4.0",
|
||||
"version": "2.5.0",
|
||||
"private": true,
|
||||
"description": "A GitHub Action for Dependency Review",
|
||||
"main": "lib/main.js",
|
||||
@@ -29,21 +29,23 @@
|
||||
"@actions/github": "^5.1.1",
|
||||
"@octokit/plugin-retry": "^3.0.9",
|
||||
"@octokit/request-error": "^3.0.1",
|
||||
"ansi-styles": "^6.1.1",
|
||||
"got": "^12.5.1",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"got": "^12.5.2",
|
||||
"nodemon": "^2.0.20",
|
||||
"yaml": "^2.1.2",
|
||||
"octokit": "^2.0.7",
|
||||
"yaml": "^2.1.3",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.63",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.11.65",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"esbuild-register": "^3.3.3",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-plugin-github": "^4.3.7",
|
||||
"eslint-plugin-jest": "^27.0.4",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-plugin-github": "^4.4.0",
|
||||
"eslint-plugin-jest": "^27.1.1",
|
||||
"jest": "^27.5.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"nodemon": "^2.0.20",
|
||||
|
||||
+10
-2
@@ -51,12 +51,20 @@ export function filterChangesByScopes(
|
||||
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,
|
||||
changes: Changes
|
||||
): Changes {
|
||||
if (ghsas === undefined) {
|
||||
return []
|
||||
return changes
|
||||
}
|
||||
|
||||
const filteredChanges = changes.filter(change => {
|
||||
|
||||
+65
-4
@@ -1,3 +1,5 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Octokit} from 'octokit'
|
||||
import {Change} from './schemas'
|
||||
|
||||
/**
|
||||
@@ -10,21 +12,27 @@ import {Change} from './schemas'
|
||||
* we will ignore the deny list.
|
||||
* @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.
|
||||
* @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
|
||||
* @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
|
||||
*/
|
||||
export function getDeniedLicenseChanges(
|
||||
export async function getDeniedLicenseChanges(
|
||||
changes: Change[],
|
||||
licenses: {
|
||||
allow?: string[]
|
||||
deny?: string[]
|
||||
}
|
||||
): [Change[], Change[]] {
|
||||
): Promise<[Change[], Change[]]> {
|
||||
const {allow, deny} = licenses
|
||||
|
||||
const disallowed: Change[] = []
|
||||
const unknown: Change[] = []
|
||||
|
||||
for (const change of changes) {
|
||||
const consolidatedChanges = changes.some(
|
||||
({source_repository_url, license}) => !license && source_repository_url
|
||||
)
|
||||
? await setGHLicenses(changes)
|
||||
: changes
|
||||
|
||||
for (const change of consolidatedChanges) {
|
||||
if (change.change_type === 'removed') {
|
||||
continue
|
||||
}
|
||||
@@ -47,3 +55,56 @@ export function getDeniedLicenseChanges(
|
||||
|
||||
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
@@ -8,7 +8,7 @@ import {readConfig} from '../src/config'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
filterChangesByScopes,
|
||||
filterOutAllowedAdvisories
|
||||
filterAllowedAdvisories
|
||||
} from '../src/filter'
|
||||
import {getDeniedLicenseChanges} from './licenses'
|
||||
import * as summary from './summary'
|
||||
@@ -30,7 +30,7 @@ async function run(): Promise<void> {
|
||||
|
||||
const minSeverity = config.fail_on_severity as Severity
|
||||
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
|
||||
const filteredChanges = filterOutAllowedAdvisories(
|
||||
const filteredChanges = filterAllowedAdvisories(
|
||||
config.allow_ghsas,
|
||||
scopedChanges
|
||||
)
|
||||
@@ -45,7 +45,7 @@ async function run(): Promise<void> {
|
||||
change.vulnerabilities.length > 0
|
||||
)
|
||||
|
||||
const [licenseErrors, unknownLicenses] = getDeniedLicenseChanges(
|
||||
const [licenseErrors, unknownLicenses] = await getDeniedLicenseChanges(
|
||||
filteredChanges,
|
||||
{
|
||||
allow: config.allow_licenses,
|
||||
@@ -192,7 +192,7 @@ function renderScannedDependency(change: Change): string {
|
||||
} as const
|
||||
)[changeType]
|
||||
|
||||
return `${styles.color[color].open}${icon} ${change.manifest}@${change.version}${styles.color[color].close}`
|
||||
return `${styles.color[color].open}${icon} ${change.name}@${change.version}${styles.color[color].close}`
|
||||
}
|
||||
|
||||
function printScannedDependencies(changes: Changes): void {
|
||||
|
||||
Reference in New Issue
Block a user