Merge pull request #544 from adrienpessu/main

Add an option to deny packages or groups of packages
This commit is contained in:
Federico Builes
2023-08-09 15:25:21 +02:00
committed by GitHub
13 changed files with 502 additions and 29 deletions
+3 -1
View File
@@ -67,7 +67,7 @@ jobs:
Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional. Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional.
| Option | Usage | Possible values | Default value | | Option | Usage | Possible values | Default value |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- | |---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|---------------|
| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` | | `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` |
| `allow-licenses`\* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none | | `allow-licenses`\* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
| `deny-licenses`\* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none | | `deny-licenses`\* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
@@ -78,6 +78,8 @@ Configure this action by either inlining these options in your workflow file, or
| `allow-dependencies-licenses`\* | Contains a list of packages that will be excluded from license checks. | Any package(s) in [purl](https://github.com/package-url/purl-spec) format | none | | `allow-dependencies-licenses`\* | Contains a list of packages that will be excluded from license checks. | Any package(s) in [purl](https://github.com/package-url/purl-spec) format | none |
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none | | `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `true`, `false` | `false` | | `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `true`, `false` | `false` |
| `deny-packages` | Any number of packages to block in a PR. | Package(s) in [purl](https://github.com/package-url/purl-spec) format | empty |
| `deny-groups` | Any number of groups (namespaces) to block in a PR. | Namespace(s) in [purl](https://github.com/package-url/purl-spec) format (no package name, no version number) | empty |
\*not supported for use with GitHub Enterprise Server \*not supported for use with GitHub Enterprise Server
+166
View File
@@ -0,0 +1,166 @@
import {expect, jest, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
let getDeniedChanges: Function
const npmChange: Change = {
manifest: 'package.json',
change_type: 'added',
ecosystem: 'npm',
name: 'Reeuhq',
version: '1.0.2',
package_url: 'pkg:npm/[email protected]',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'critical',
advisory_ghsa_id: 'first-random_string',
advisory_summary: 'very dangerous',
advisory_url: 'github.com/future-funk'
}
]
}
const rubyChange: Change = {
change_type: 'added',
manifest: 'Gemfile.lock',
ecosystem: 'rubygems',
name: 'actionsomething',
version: '3.2.0',
package_url: 'pkg:gem/[email protected]',
license: 'BSD',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'moderate',
advisory_ghsa_id: 'second-random_string',
advisory_summary: 'not so dangerous',
advisory_url: 'github.com/future-funk'
},
{
severity: 'low',
advisory_ghsa_id: 'third-random_string',
advisory_summary: 'dont page me',
advisory_url: 'github.com/future-funk'
}
]
}
const pipChange: Change = {
change_type: 'added',
manifest: 'requirements.txt',
ecosystem: 'pip',
name: 'package-1',
version: '1.1.1',
package_url: 'pkg:pip/[email protected]',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'moderate',
advisory_ghsa_id: 'second-random_string',
advisory_summary: 'not so dangerous',
advisory_url: 'github.com/future-funk'
},
{
severity: 'low',
advisory_ghsa_id: 'third-random_string',
advisory_summary: 'dont page me',
advisory_url: 'github.com/future-funk'
}
]
}
const mvnChange: Change = {
change_type: 'added',
manifest: 'pom.xml',
ecosystem: 'maven',
name: 'org.apache.logging.log4j:log4j-core',
version: '2.15.0',
package_url: 'pkg:maven/org.apache.logging.log4j/[email protected]',
license: 'Apache-2.0',
source_repository_url:
'https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core',
scope: 'unknown',
vulnerabilities: [
{
severity: 'critical',
advisory_ghsa_id: 'second-random_string',
advisory_summary: 'not so dangerous',
advisory_url: 'github.com/future-funk'
}
]
}
jest.mock('@actions/core')
const mockOctokit = {
rest: {
licenses: {
getForRepo: jest
.fn()
.mockReturnValue({data: {license: {spdx_id: 'AGPL'}}})
}
}
}
jest.mock('octokit', () => {
return {
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
Octokit: class {
constructor() {
return mockOctokit
}
}
}
})
beforeEach(async () => {
jest.resetModules()
jest.doMock('spdx-satisfies', () => {
// mock spdx-satisfies return value
// true for BSD, false for all others
return jest.fn((license: string, _: string): boolean => license === 'BSD')
})
// eslint-disable-next-line @typescript-eslint/no-require-imports
;({getDeniedChanges} = require('../src/deny'))
})
test('it adds packages in the deny packages list', async () => {
const changes: Changes = [npmChange, rubyChange]
const deniedChanges = await getDeniedChanges(
changes,
['pkg:gem/actionsomething'],
[]
)
expect(deniedChanges[0]).toBe(rubyChange)
expect(deniedChanges.length).toEqual(1)
})
test('it adds packages in the deny group list', async () => {
const changes: Changes = [mvnChange, rubyChange]
const deniedChanges = await getDeniedChanges(
changes,
[],
['pkg:maven/org.apache.logging.log4j']
)
expect(deniedChanges[0]).toBe(mvnChange)
expect(deniedChanges.length).toEqual(1)
})
test('it adds packages outside of the deny lists', async () => {
const changes: Changes = [npmChange, pipChange]
const deniedChanges = await getDeniedChanges(
changes,
['pkg:gem/actionsomething'],
['pkg:maven:org.apache.logging.log4j']
)
expect(deniedChanges.length).toEqual(0)
})
+31 -4
View File
@@ -24,6 +24,8 @@ const defaultConfig: ConfigurationOptions = {
allow_ghsas: [], allow_ghsas: [],
allow_licenses: [], allow_licenses: [],
deny_licenses: [], deny_licenses: [],
deny_packages: [],
deny_groups: [],
comment_summary_in_pr: true comment_summary_in_pr: true
} }
@@ -70,6 +72,7 @@ test('prints headline as h1', () => {
summary.addSummaryToSummary( summary.addSummaryToSummary(
emptyChanges, emptyChanges,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
const text = core.summary.stringify() const text = core.summary.stringify()
@@ -81,6 +84,7 @@ test('only includes "No vulnerabilities or license issues found"-message if both
summary.addSummaryToSummary( summary.addSummaryToSummary(
emptyChanges, emptyChanges,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
const text = core.summary.stringify() const text = core.summary.stringify()
@@ -90,7 +94,12 @@ test('only includes "No vulnerabilities or license issues found"-message if both
test('only includes "No vulnerabilities found"-message if "license_check" is set to false and nothing was found', () => { test('only includes "No vulnerabilities found"-message if "license_check" is set to false and nothing was found', () => {
const config = {...defaultConfig, license_check: false} const config = {...defaultConfig, license_check: false}
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config) summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
config
)
const text = core.summary.stringify() const text = core.summary.stringify()
expect(text).toContain('✅ No vulnerabilities found.') expect(text).toContain('✅ No vulnerabilities found.')
@@ -98,7 +107,12 @@ test('only includes "No vulnerabilities found"-message if "license_check" is set
test('only includes "No license issues found"-message if "vulnerability_check" is set to false and nothing was found', () => { test('only includes "No license issues found"-message if "vulnerability_check" is set to false and nothing was found', () => {
const config = {...defaultConfig, vulnerability_check: false} const config = {...defaultConfig, vulnerability_check: false}
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config) summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
config
)
const text = core.summary.stringify() const text = core.summary.stringify()
expect(text).toContain('✅ No license issues found.') expect(text).toContain('✅ No license issues found.')
@@ -108,6 +122,7 @@ test('groups dependencies with empty manifest paths together', () => {
summary.addSummaryToSummary( summary.addSummaryToSummary(
changesWithEmptyManifests, changesWithEmptyManifests,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
summary.addScannedDependencies(changesWithEmptyManifests) summary.addScannedDependencies(changesWithEmptyManifests)
@@ -124,6 +139,7 @@ test('does not include status section if nothing was found', () => {
summary.addSummaryToSummary( summary.addSummaryToSummary(
emptyChanges, emptyChanges,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
const text = core.summary.stringify() const text = core.summary.stringify()
@@ -142,7 +158,12 @@ test('includes count and status icons for all findings', () => {
unlicensed: [createTestChange(), createTestChange(), createTestChange()] unlicensed: [createTestChange(), createTestChange(), createTestChange()]
} }
summary.addSummaryToSummary(vulnerabilities, licenseIssues, defaultConfig) summary.addSummaryToSummary(
vulnerabilities,
licenseIssues,
emptyChanges,
defaultConfig
)
const text = core.summary.stringify() const text = core.summary.stringify()
expect(text).toContain('❌ 2 vulnerable package(s)') expect(text).toContain('❌ 2 vulnerable package(s)')
@@ -159,6 +180,7 @@ test('uses checkmarks for license issues if only vulnerabilities were found', ()
summary.addSummaryToSummary( summary.addSummaryToSummary(
vulnerabilities, vulnerabilities,
emptyInvalidLicenseChanges, emptyInvalidLicenseChanges,
emptyChanges,
defaultConfig defaultConfig
) )
@@ -178,7 +200,12 @@ test('uses checkmarks for vulnerabilities if only license issues were found', ()
unlicensed: [] unlicensed: []
} }
summary.addSummaryToSummary(emptyChanges, licenseIssues, defaultConfig) summary.addSummaryToSummary(
emptyChanges,
licenseIssues,
emptyChanges,
defaultConfig
)
const text = core.summary.stringify() const text = core.summary.stringify()
expect(text).toContain('✅ 0 vulnerable package(s)') expect(text).toContain('✅ 0 vulnerable package(s)')
+6
View File
@@ -47,6 +47,12 @@ inputs:
comment-summary-in-pr: comment-summary-in-pr:
description: A boolean to determine if the report should be posted as a comment in the PR itself. Setting this to true requires you to give the workflow the write permissions for pull-requests description: A boolean to determine if the report should be posted as a comment in the PR itself. Setting this to true requires you to give the workflow the write permissions for pull-requests
required: false required: false
deny-packages:
description: A comma-separated list of package URLs to deny (e.g. "pkg:npm/express, pkg:pip/pycrypto")
required: false
deny-groups:
description: A comma-separated list of package URLs for group(s)/namespace(s) to deny (e.g. "pkg:npm/express, pkg:pip/pycrypto")
required: false
runs: runs:
using: 'node16' using: 'node16'
main: 'dist/index.js' main: 'dist/index.js'
Generated Vendored
+136 -5
View File
@@ -135,6 +135,84 @@ function findCommentByMarker(commentBodyIncludes) {
} }
/***/ }),
/***/ 2134:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getDeniedChanges = void 0;
const core = __importStar(__nccwpck_require__(2186));
function getDeniedChanges(changes, deniedPackages, deniedGroups) {
return __awaiter(this, void 0, void 0, function* () {
const changesDenied = [];
let failed = false;
for (const change of changes) {
change.name = change.name.toLowerCase();
const packageUrl = change.package_url.toLowerCase().split('@')[0];
if (deniedPackages) {
for (const denied of deniedPackages) {
if (packageUrl === denied.split('@')[0].toLowerCase()) {
changesDenied.push(change);
failed = true;
}
}
}
if (deniedGroups) {
for (const denied of deniedGroups) {
if (packageUrl.startsWith(denied.toLowerCase())) {
changesDenied.push(change);
failed = true;
}
}
}
}
if (failed) {
core.setFailed('Dependency review detected denied packages.');
}
else {
core.info('Dependency review did not detect any denied packages');
}
return changesDenied;
});
}
exports.getDeniedChanges = getDeniedChanges;
/***/ }), /***/ }),
/***/ 4966: /***/ 4966:
@@ -485,6 +563,7 @@ const summary = __importStar(__nccwpck_require__(8608));
const git_refs_1 = __nccwpck_require__(1086); const git_refs_1 = __nccwpck_require__(1086);
const utils_1 = __nccwpck_require__(918); const utils_1 = __nccwpck_require__(918);
const comment_pr_1 = __nccwpck_require__(5842); const comment_pr_1 = __nccwpck_require__(5842);
const deny_1 = __nccwpck_require__(2134);
function run() { function run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
@@ -513,7 +592,10 @@ function run() {
deny: config.deny_licenses, deny: config.deny_licenses,
licenseExclusions: config.allow_dependencies_licenses licenseExclusions: config.allow_dependencies_licenses
}); });
summary.addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, config); core.debug(`Filtered Changes: ${JSON.stringify(filteredChanges)}`);
core.debug(`Config Deny Packages: ${JSON.stringify(config)}`);
const deniedChanges = yield (0, deny_1.getDeniedChanges)(filteredChanges, config.deny_packages, config.deny_groups);
summary.addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, config);
if (snapshot_warnings) { if (snapshot_warnings) {
summary.addSnapshotWarnings(snapshot_warnings); summary.addSnapshotWarnings(snapshot_warnings);
} }
@@ -525,6 +607,10 @@ function run() {
summary.addLicensesToSummary(invalidLicenseChanges, config); summary.addLicensesToSummary(invalidLicenseChanges, config);
printLicensesBlock(invalidLicenseChanges); printLicensesBlock(invalidLicenseChanges);
} }
if (config.deny_packages || config.deny_groups) {
summary.addDeniedToSummary(deniedChanges);
printDeniedDependencies(deniedChanges, config);
}
summary.addScannedDependencies(changes); summary.addScannedDependencies(changes);
printScannedDependencies(changes); printScannedDependencies(changes);
if (config.comment_summary_in_pr) { if (config.comment_summary_in_pr) {
@@ -640,6 +726,17 @@ function printScannedDependencies(changes) {
} }
})); }));
} }
function printDeniedDependencies(changes, config) {
core.group('Denied', () => __awaiter(this, void 0, void 0, function* () {
for (const denied of config.deny_packages) {
core.info(`Config: ${denied}`);
}
for (const change of changes) {
core.info(`Change: ${change.name}@${change.version} is denied`);
core.info(`Change: ${change.package_url} is denied`);
}
}));
}
run(); run();
@@ -712,6 +809,8 @@ exports.ConfigurationOptionsSchema = z
deny_licenses: z.array(z.string()).optional(), deny_licenses: z.array(z.string()).optional(),
allow_dependencies_licenses: z.array(z.string()).optional(), allow_dependencies_licenses: z.array(z.string()).optional(),
allow_ghsas: z.array(z.string()).default([]), allow_ghsas: z.array(z.string()).default([]),
deny_packages: z.array(z.string()).default([]),
deny_groups: z.array(z.string()).default([]),
license_check: z.boolean().default(true), license_check: z.boolean().default(true),
vulnerability_check: z.boolean().default(true), vulnerability_check: z.boolean().default(true),
config_file: z.string().optional(), config_file: z.string().optional(),
@@ -778,7 +877,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.addSnapshotWarnings = exports.addScannedDependencies = exports.addLicensesToSummary = exports.addChangeVulnerabilitiesToSummary = exports.addSummaryToSummary = void 0; exports.addDeniedToSummary = exports.addSnapshotWarnings = exports.addScannedDependencies = exports.addLicensesToSummary = exports.addChangeVulnerabilitiesToSummary = exports.addSummaryToSummary = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const utils_1 = __nccwpck_require__(918); const utils_1 = __nccwpck_require__(918);
const icons = { const icons = {
@@ -786,10 +885,11 @@ const icons = {
cross: '❌', cross: '❌',
warning: '⚠️' warning: '⚠️'
}; };
function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, config) { function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, config) {
core.summary.addHeading('Dependency Review', 1); core.summary.addHeading('Dependency Review', 1);
if (vulnerableChanges.length === 0 && if (vulnerableChanges.length === 0 &&
countLicenseIssues(invalidLicenseChanges) === 0) { countLicenseIssues(invalidLicenseChanges) === 0 &&
deniedChanges.length === 0) {
if (!config.license_check) { if (!config.license_check) {
core.summary.addRaw(`${icons.check} No vulnerabilities found.`); core.summary.addRaw(`${icons.check} No vulnerabilities found.`);
} }
@@ -815,6 +915,11 @@ function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, config) {
`${checkOrFailIcon(invalidLicenseChanges.unresolved.length)} ${invalidLicenseChanges.unresolved.length} package(s) with invalid SPDX license definitions`, `${checkOrFailIcon(invalidLicenseChanges.unresolved.length)} ${invalidLicenseChanges.unresolved.length} package(s) with invalid SPDX license definitions`,
`${checkOrWarnIcon(invalidLicenseChanges.unlicensed.length)} ${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.` `${checkOrWarnIcon(invalidLicenseChanges.unlicensed.length)} ${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.`
] ]
: []),
...(deniedChanges.length > 0
? [
`${checkOrWarnIcon(deniedChanges.length)} ${deniedChanges.length} package(s) denied.`
]
: []) : [])
]) ])
.addRaw('See the Details below.'); .addRaw('See the Details below.');
@@ -956,6 +1061,24 @@ exports.addSnapshotWarnings = addSnapshotWarnings;
function countLicenseIssues(invalidLicenseChanges) { function countLicenseIssues(invalidLicenseChanges) {
return Object.values(invalidLicenseChanges).reduce((acc, val) => acc + val.length, 0); return Object.values(invalidLicenseChanges).reduce((acc, val) => acc + val.length, 0);
} }
function addDeniedToSummary(deniedChanges) {
if (deniedChanges.length === 0) {
return;
}
core.summary.addHeading('Denied dependencies', 2);
for (const change of deniedChanges) {
core.summary.addHeading(`<em>Denied dependencies</em>`, 4);
core.summary.addTable([
['Package', 'Version', 'License'],
[
(0, utils_1.renderUrl)(change.source_repository_url, change.name),
change.version,
change.license || ''
]
]);
}
}
exports.addDeniedToSummary = addDeniedToSummary;
function checkOrFailIcon(count) { function checkOrFailIcon(count) {
return count === 0 ? icons.check : icons.cross; return count === 0 ? icons.check : icons.cross;
} }
@@ -47821,6 +47944,8 @@ function readInlineConfig() {
const allow_licenses = parseList(getOptionalInput('allow-licenses')); const allow_licenses = parseList(getOptionalInput('allow-licenses'));
const deny_licenses = parseList(getOptionalInput('deny-licenses')); const deny_licenses = parseList(getOptionalInput('deny-licenses'));
const allow_dependencies_licenses = parseList(getOptionalInput('allow-dependencies-licenses')); const allow_dependencies_licenses = parseList(getOptionalInput('allow-dependencies-licenses'));
const deny_packages = parseList(getOptionalInput('deny-packages'));
const deny_groups = parseList(getOptionalInput('deny-groups'));
const allow_ghsas = parseList(getOptionalInput('allow-ghsas')); const allow_ghsas = parseList(getOptionalInput('allow-ghsas'));
const license_check = getOptionalBoolean('license-check'); const license_check = getOptionalBoolean('license-check');
const vulnerability_check = getOptionalBoolean('vulnerability-check'); const vulnerability_check = getOptionalBoolean('vulnerability-check');
@@ -47835,6 +47960,8 @@ function readInlineConfig() {
fail_on_scopes, fail_on_scopes,
allow_licenses, allow_licenses,
deny_licenses, deny_licenses,
deny_packages,
deny_groups,
allow_dependencies_licenses, allow_dependencies_licenses,
allow_ghsas, allow_ghsas,
license_check, license_check,
@@ -47905,7 +48032,9 @@ function parseConfigFile(configData) {
'deny-licenses', 'deny-licenses',
'fail-on-scopes', 'fail-on-scopes',
'allow-ghsas', 'allow-ghsas',
'allow-dependencies-licenses' 'allow-dependencies-licenses',
'deny-packages',
'deny-groups'
]; ];
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
// strings can contain list values (e.g. 'MIT, Apache-2.0'). In this // strings can contain list values (e.g. 'MIT, Apache-2.0'). In this
@@ -48119,6 +48248,8 @@ exports.ConfigurationOptionsSchema = z
deny_licenses: z.array(z.string()).optional(), deny_licenses: z.array(z.string()).optional(),
allow_dependencies_licenses: z.array(z.string()).optional(), allow_dependencies_licenses: z.array(z.string()).optional(),
allow_ghsas: z.array(z.string()).default([]), allow_ghsas: z.array(z.string()).default([]),
deny_packages: z.array(z.string()).default([]),
deny_groups: z.array(z.string()).default([]),
license_check: z.boolean().default(true), license_check: z.boolean().default(true),
vulnerability_check: z.boolean().default(true), vulnerability_check: z.boolean().default(true),
config_file: z.string().optional(), config_file: z.string().optional(),
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+28
View File
@@ -230,3 +230,31 @@ jobs:
comment-summary-in-pr: true comment-summary-in-pr: true
license-check: false license-check: false
``` ```
## Exclude dependencies from their name or groups
Using the `deny-packages` option you can exclude dependencies by their PURL. You can add multiple values separated by a commas.
Using the `deny-groups` option you can exclude dependencies by their group name/namespace. You can add multiple values separated by a comma.
In this example, we are excluding `pkg:maven/org.apache.logging.log4j:log4j-api` and `pkg:maven/org.apache.logging.log4j/log4j-core` from `maven` and all packages in the group `pkg:maven/com.bazaarvoice.maven`
```yaml
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v3
with:
deny-packages: 'pkg:maven/org.apache.logging.log4j/log4j-api,pkg:maven/org.apache.logging.log4j/log4j-core'
deny-groups: 'pkg:maven/com.bazaarvoice.jolt'
```
+7 -3
View File
@@ -6,7 +6,7 @@
* npx ts-node scripts/create_summary.ts * npx ts-node scripts/create_summary.ts
*/ */
import {Changes, ConfigurationOptions} from '../src/schemas' import {Change, Changes, ConfigurationOptions} from '../src/schemas'
import {createTestChange} from '../__tests__/fixtures/create-test-change' import {createTestChange} from '../__tests__/fixtures/create-test-change'
import {InvalidLicenseChanges} from '../src/licenses' import {InvalidLicenseChanges} from '../src/licenses'
import * as fs from 'fs' import * as fs from 'fs'
@@ -22,6 +22,8 @@ const defaultConfig: ConfigurationOptions = {
allow_ghsas: [], allow_ghsas: [],
allow_licenses: ['MIT'], allow_licenses: ['MIT'],
deny_licenses: [], deny_licenses: [],
deny_packages: [],
deny_groups: [],
allow_dependencies_licenses: [ allow_dependencies_licenses: [
'pkg:npm/[email protected]', 'pkg:npm/[email protected]',
'pkg:pip/requests', 'pkg:pip/requests',
@@ -44,6 +46,7 @@ const createNonIssueSummary = async (): Promise<void> => {
await createSummary( await createSummary(
[], [],
{forbidden: [], unresolved: [], unlicensed: []}, {forbidden: [], unresolved: [], unlicensed: []},
[],
defaultConfig, defaultConfig,
'non-issue-summary.md' 'non-issue-summary.md'
) )
@@ -85,16 +88,17 @@ const createFullSummary = async (): Promise<void> => {
] ]
} }
await createSummary(changes, licenses, defaultConfig, 'full-summary.md') await createSummary(changes, licenses, [], defaultConfig, 'full-summary.md')
} }
async function createSummary( async function createSummary(
vulnerabilities: Changes, vulnerabilities: Changes,
licenseIssues: InvalidLicenseChanges, licenseIssues: InvalidLicenseChanges,
denied: Change[],
config: ConfigurationOptions, config: ConfigurationOptions,
fileName: string fileName: string
): Promise<void> { ): Promise<void> {
summary.addSummaryToSummary(vulnerabilities, licenseIssues, config) summary.addSummaryToSummary(vulnerabilities, licenseIssues, denied, config)
summary.addChangeVulnerabilitiesToSummary( summary.addChangeVulnerabilitiesToSummary(
vulnerabilities, vulnerabilities,
config.fail_on_severity config.fail_on_severity
+7 -1
View File
@@ -33,6 +33,8 @@ function readInlineConfig(): ConfigurationOptionsPartial {
const allow_dependencies_licenses = parseList( const allow_dependencies_licenses = parseList(
getOptionalInput('allow-dependencies-licenses') getOptionalInput('allow-dependencies-licenses')
) )
const deny_packages = parseList(getOptionalInput('deny-packages'))
const deny_groups = parseList(getOptionalInput('deny-groups'))
const allow_ghsas = parseList(getOptionalInput('allow-ghsas')) const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
const license_check = getOptionalBoolean('license-check') const license_check = getOptionalBoolean('license-check')
const vulnerability_check = getOptionalBoolean('vulnerability-check') const vulnerability_check = getOptionalBoolean('vulnerability-check')
@@ -49,6 +51,8 @@ function readInlineConfig(): ConfigurationOptionsPartial {
fail_on_scopes, fail_on_scopes,
allow_licenses, allow_licenses,
deny_licenses, deny_licenses,
deny_packages,
deny_groups,
allow_dependencies_licenses, allow_dependencies_licenses,
allow_ghsas, allow_ghsas,
license_check, license_check,
@@ -137,7 +141,9 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
'deny-licenses', 'deny-licenses',
'fail-on-scopes', 'fail-on-scopes',
'allow-ghsas', 'allow-ghsas',
'allow-dependencies-licenses' 'allow-dependencies-licenses',
'deny-packages',
'deny-groups'
] ]
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
+42
View File
@@ -0,0 +1,42 @@
import {Change} from './schemas'
import * as core from '@actions/core'
export async function getDeniedChanges(
changes: Change[],
deniedPackages: string[],
deniedGroups: string[]
): Promise<Change[]> {
const changesDenied: Change[] = []
let failed = false
for (const change of changes) {
change.name = change.name.toLowerCase()
const packageUrl = change.package_url.toLowerCase().split('@')[0]
if (deniedPackages) {
for (const denied of deniedPackages) {
if (packageUrl === denied.split('@')[0].toLowerCase()) {
changesDenied.push(change)
failed = true
}
}
}
if (deniedGroups) {
for (const denied of deniedGroups) {
if (packageUrl.startsWith(denied.toLowerCase())) {
changesDenied.push(change)
failed = true
}
}
}
}
if (failed) {
core.setFailed('Dependency review detected denied packages.')
} else {
core.info('Dependency review did not detect any denied packages')
}
return changesDenied
}
+32 -1
View File
@@ -3,7 +3,7 @@ import * as dependencyGraph from './dependency-graph'
import * as github from '@actions/github' import * as github from '@actions/github'
import styles from 'ansi-styles' import styles from 'ansi-styles'
import {RequestError} from '@octokit/request-error' import {RequestError} from '@octokit/request-error'
import {Change, Severity, Changes} from './schemas' import {Change, Severity, Changes, ConfigurationOptions} from './schemas'
import {readConfig} from '../src/config' import {readConfig} from '../src/config'
import { import {
filterChangesBySeverity, filterChangesBySeverity,
@@ -16,6 +16,7 @@ import {getRefs} from './git-refs'
import {groupDependenciesByManifest} from './utils' import {groupDependenciesByManifest} from './utils'
import {commentPr} from './comment-pr' import {commentPr} from './comment-pr'
import {getDeniedChanges} from './deny'
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
@@ -63,9 +64,19 @@ async function run(): Promise<void> {
} }
) )
core.debug(`Filtered Changes: ${JSON.stringify(filteredChanges)}`)
core.debug(`Config Deny Packages: ${JSON.stringify(config)}`)
const deniedChanges = await getDeniedChanges(
filteredChanges,
config.deny_packages,
config.deny_groups
)
summary.addSummaryToSummary( summary.addSummaryToSummary(
vulnerableChanges, vulnerableChanges,
invalidLicenseChanges, invalidLicenseChanges,
deniedChanges,
config config
) )
@@ -81,6 +92,10 @@ async function run(): Promise<void> {
summary.addLicensesToSummary(invalidLicenseChanges, config) summary.addLicensesToSummary(invalidLicenseChanges, config)
printLicensesBlock(invalidLicenseChanges) printLicensesBlock(invalidLicenseChanges)
} }
if (config.deny_packages || config.deny_groups) {
summary.addDeniedToSummary(deniedChanges)
printDeniedDependencies(deniedChanges, config)
}
summary.addScannedDependencies(changes) summary.addScannedDependencies(changes)
printScannedDependencies(changes) printScannedDependencies(changes)
@@ -239,4 +254,20 @@ function printScannedDependencies(changes: Changes): void {
}) })
} }
function printDeniedDependencies(
changes: Change[],
config: ConfigurationOptions
): void {
core.group('Denied', async () => {
for (const denied of config.deny_packages) {
core.info(`Config: ${denied}`)
}
for (const change of changes) {
core.info(`Change: ${change.name}@${change.version} is denied`)
core.info(`Change: ${change.package_url} is denied`)
}
})
}
run() run()
+2
View File
@@ -42,6 +42,8 @@ export const ConfigurationOptionsSchema = z
deny_licenses: z.array(z.string()).optional(), deny_licenses: z.array(z.string()).optional(),
allow_dependencies_licenses: z.array(z.string()).optional(), allow_dependencies_licenses: z.array(z.string()).optional(),
allow_ghsas: z.array(z.string()).default([]), allow_ghsas: z.array(z.string()).default([]),
deny_packages: z.array(z.string()).default([]),
deny_groups: z.array(z.string()).default([]),
license_check: z.boolean().default(true), license_check: z.boolean().default(true),
vulnerability_check: z.boolean().default(true), vulnerability_check: z.boolean().default(true),
config_file: z.string().optional(), config_file: z.string().optional(),
+30 -2
View File
@@ -1,5 +1,5 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {ConfigurationOptions, Changes} from './schemas' import {ConfigurationOptions, Changes, Change} from './schemas'
import {SummaryTableRow} from '@actions/core/lib/summary' import {SummaryTableRow} from '@actions/core/lib/summary'
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses' import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils' import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
@@ -13,13 +13,15 @@ const icons = {
export function addSummaryToSummary( export function addSummaryToSummary(
vulnerableChanges: Changes, vulnerableChanges: Changes,
invalidLicenseChanges: InvalidLicenseChanges, invalidLicenseChanges: InvalidLicenseChanges,
deniedChanges: Changes,
config: ConfigurationOptions config: ConfigurationOptions
): void { ): void {
core.summary.addHeading('Dependency Review', 1) core.summary.addHeading('Dependency Review', 1)
if ( if (
vulnerableChanges.length === 0 && vulnerableChanges.length === 0 &&
countLicenseIssues(invalidLicenseChanges) === 0 countLicenseIssues(invalidLicenseChanges) === 0 &&
deniedChanges.length === 0
) { ) {
if (!config.license_check) { if (!config.license_check) {
core.summary.addRaw(`${icons.check} No vulnerabilities found.`) core.summary.addRaw(`${icons.check} No vulnerabilities found.`)
@@ -56,6 +58,13 @@ export function addSummaryToSummary(
invalidLicenseChanges.unlicensed.length invalidLicenseChanges.unlicensed.length
} package(s) with unknown licenses.` } package(s) with unknown licenses.`
] ]
: []),
...(deniedChanges.length > 0
? [
`${checkOrWarnIcon(deniedChanges.length)} ${
deniedChanges.length
} package(s) denied.`
]
: []) : [])
]) ])
.addRaw('See the Details below.') .addRaw('See the Details below.')
@@ -248,6 +257,25 @@ function countLicenseIssues(
) )
} }
export function addDeniedToSummary(deniedChanges: Change[]): void {
if (deniedChanges.length === 0) {
return
}
core.summary.addHeading('Denied dependencies', 2)
for (const change of deniedChanges) {
core.summary.addHeading(`<em>Denied dependencies</em>`, 4)
core.summary.addTable([
['Package', 'Version', 'License'],
[
renderUrl(change.source_repository_url, change.name),
change.version,
change.license || ''
]
])
}
}
function checkOrFailIcon(count: number): string { function checkOrFailIcon(count: number): string {
return count === 0 ? icons.check : icons.cross return count === 0 ? icons.check : icons.cross
} }