Merge pull request #106 from actions/adding-lists

Adding allow and deny lists
This commit is contained in:
Federico Builes
2022-06-14 04:45:37 +02:00
committed by GitHub
11 changed files with 270 additions and 15 deletions

13
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "0.1.0",
"configurations": [
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/.bin/jest", "--runInBand", "--coverage", "false"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}

View File

@@ -14,5 +14,18 @@ test('the default config path handles .yml and .yaml', async () => {
test('returns a default config when the config file was not found', async () => {
let options = readConfigFile('fixtures/i-dont-exist')
expect(options.fail_on_severity).toEqual('low')
expect(options.allow_licenses).toEqual([])
expect(options.allow_licenses).toEqual(undefined)
})
test('it reads config files with empty options', async () => {
let options = readConfigFile('./__tests__/fixtures/no-licenses-config.yml')
expect(options.fail_on_severity).toEqual('critical')
expect(options.allow_licenses).toEqual(undefined)
expect(options.deny_licenses).toEqual(undefined)
})
test('it raises an error if both an allow and denylist are specified', async () => {
expect(() =>
readConfigFile('./__tests__/fixtures/conflictive-config.yml')
).toThrow()
})

View File

@@ -8,7 +8,7 @@ let npmChange: Change = {
ecosystem: 'npm',
name: 'Reeuhq',
version: '1.0.2',
package_url: 'somepurl',
package_url: 'pkg:npm/reeuhq@1.0.2',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
vulnerabilities: [
@@ -27,7 +27,7 @@ let rubyChange: Change = {
ecosystem: 'rubygems',
name: 'actionsomething',
version: '3.2.0',
package_url: 'somerubypurl',
package_url: 'pkg:gem/actionsomething@3.2.0',
license: 'BSD',
source_repository_url: 'github.com/some-repo',
vulnerabilities: [

View File

@@ -0,0 +1,2 @@
allow_licenses: []
deny_licenses: []

View File

@@ -0,0 +1 @@
fail_on_severity: critical

View File

@@ -0,0 +1,70 @@
import {expect, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
import {getDeniedLicenseChanges} from '../src/licenses'
let npmChange: Change = {
manifest: 'package.json',
change_type: 'added',
ecosystem: 'npm',
name: 'Reeuhq',
version: '1.0.2',
package_url: 'pkg:npm/reeuhq@1.0.2',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
vulnerabilities: [
{
severity: 'critical',
advisory_ghsa_id: 'first-random_string',
advisory_summary: 'very dangerouns',
advisory_url: 'github.com/future-funk'
}
]
}
let rubyChange: Change = {
change_type: 'added',
manifest: 'Gemfile.lock',
ecosystem: 'rubygems',
name: 'actionsomething',
version: '3.2.0',
package_url: 'pkg:gem/actionsomething@3.2.0',
license: 'BSD',
source_repository_url: 'github.com/some-repo',
vulnerabilities: [
{
severity: 'moderate',
advisory_ghsa_id: 'second-random_string',
advisory_summary: 'not so dangerouns',
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'
}
]
}
test('it fails if a license outside the allow list is found', async () => {
const changes: Changes = [npmChange, rubyChange]
const invalidChanges = 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']})
expect(invalidChanges[0]).toBe(rubyChange)
})
// This is more of a "here's a behavior that might be surprising" than an actual
// 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, {
allow: [],
deny: ['BSD']
})
expect(invalidChanges.length).toBe(2)
})

81
dist/index.js generated vendored
View File

@@ -59,6 +59,52 @@ function compare({ owner, repo, baseRef, headRef }) {
exports.compare = compare;
/***/ }),
/***/ 3247:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getDeniedLicenseChanges = void 0;
/**
* Loops through a list of changes, filtering and returning the
* ones that don't conform to the licenses allow/deny lists.
*
* Keep in mind that we don't let users specify both an allow and a deny
* list in their config files, so this code works under the assumption that
* one of the two list parameters will be empty. If both lists are provided,
* 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} The list of denied changes.
*/
function getDeniedLicenseChanges(changes, licenses) {
let { allow, deny } = licenses;
let disallowed = [];
for (const change of changes) {
let license = change.license;
// TODO: be loud about unknown licenses
if (license === null) {
continue;
}
if (allow !== undefined) {
if (!allow.includes(license)) {
disallowed.push(change);
}
}
else if (deny !== undefined) {
if (deny.includes(license)) {
disallowed.push(change);
}
}
}
return disallowed;
}
exports.getDeniedLicenseChanges = getDeniedLicenseChanges;
/***/ }),
/***/ 3109:
@@ -110,6 +156,7 @@ const request_error_1 = __nccwpck_require__(537);
const schemas_1 = __nccwpck_require__(8774);
const config_1 = __nccwpck_require__(6373);
const filter_1 = __nccwpck_require__(8752);
const licenses_1 = __nccwpck_require__(3247);
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
@@ -126,6 +173,16 @@ function run() {
let config = (0, config_1.readConfigFile)();
let minSeverity = config.fail_on_severity;
let failed = false;
let licenses = {
allow: config.allow_licenses,
deny: config.deny_licenses
};
let licenseErrors = (0, licenses_1.getDeniedLicenseChanges)(changes, licenses);
if (licenseErrors.length > 0) {
printLicensesError(licenseErrors, licenses);
core.setFailed('Dependency review detected prohibited licenses.');
return;
}
let filteredChanges = (0, filter_1.filterChangesBySeverity)(minSeverity, changes);
for (const change of filteredChanges) {
if (change.change_type === 'added' &&
@@ -175,6 +232,23 @@ function renderSeverity(severity) {
}[severity];
return `${ansi_styles_1.default.color[color].open}(${severity} severity)${ansi_styles_1.default.color[color].close}`;
}
function printLicensesError(changes, licenses) {
if (changes.length == 0) {
return;
}
let { allow = [], deny = [] } = licenses;
core.info('Dependency review detected prohibited licenses.');
if (allow.length > 0) {
core.info('\nAllowed licenses: ' + allow.join(', ') + '\n');
}
if (deny.length > 0) {
core.info('\nDenied licenses: ' + deny.join(', ') + '\n');
}
core.info('The following dependencies have incompatible licenses:\n');
for (const change of changes) {
core.info(`${ansi_styles_1.default.bold.open}${change.manifest} » ${change.name}@${change.version}${ansi_styles_1.default.bold.close} License: ${ansi_styles_1.default.color.red.open}${change.license}${ansi_styles_1.default.color.red.close}`);
}
}
run();
@@ -13637,8 +13711,7 @@ exports.CONFIG_FILEPATH = './.github/dependency-review.yml';
function readConfigFile(filePath = exports.CONFIG_FILEPATH) {
// By default we want to fail on all severities and allow all licenses.
const defaultOptions = {
fail_on_severity: 'low',
allow_licenses: []
fail_on_severity: 'low'
};
let data;
try {
@@ -13652,9 +13725,7 @@ function readConfigFile(filePath = exports.CONFIG_FILEPATH) {
throw error;
}
}
const values = yaml_1.default.parse(data);
const parsed = schemas_1.ConfigurationOptionsSchema.parse(values);
return parsed;
return schemas_1.ConfigurationOptionsSchema.parse(yaml_1.default.parse(data));
}
exports.readConfigFile = readConfigFile;

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -10,8 +10,7 @@ export function readConfigFile(
): ConfigurationOptions {
// By default we want to fail on all severities and allow all licenses.
const defaultOptions: ConfigurationOptions = {
fail_on_severity: 'low',
allow_licenses: []
fail_on_severity: 'low'
}
let data
@@ -26,8 +25,5 @@ export function readConfigFile(
}
}
const values = YAML.parse(data)
const parsed = ConfigurationOptionsSchema.parse(values)
return parsed
return ConfigurationOptionsSchema.parse(YAML.parse(data))
}

44
src/licenses.ts Normal file
View File

@@ -0,0 +1,44 @@
import {Change, ChangeSchema} from './schemas'
/**
* Loops through a list of changes, filtering and returning the
* ones that don't conform to the licenses allow/deny lists.
*
* Keep in mind that we don't let users specify both an allow and a deny
* list in their config files, so this code works under the assumption that
* one of the two list parameters will be empty. If both lists are provided,
* 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} The list of denied changes.
*/
export function getDeniedLicenseChanges(
changes: Array<Change>,
licenses: {
allow?: Array<string>
deny?: Array<string>
}
): Array<Change> {
let {allow, deny} = licenses
let disallowed: Change[] = []
for (const change of changes) {
let license = change.license
// TODO: be loud about unknown licenses
if (license === null) {
continue
}
if (allow !== undefined) {
if (!allow.includes(license)) {
disallowed.push(change)
}
} else if (deny !== undefined) {
if (deny.includes(license)) {
disallowed.push(change)
}
}
}
return disallowed
}

View File

@@ -6,6 +6,7 @@ import {RequestError} from '@octokit/request-error'
import {Change, PullRequestSchema, Severity} from './schemas'
import {readConfigFile} from '../src/config'
import {filterChangesBySeverity} from '../src/filter'
import {getDeniedLicenseChanges} from './licenses'
async function run(): Promise<void> {
try {
@@ -30,6 +31,19 @@ async function run(): Promise<void> {
let minSeverity = config.fail_on_severity
let failed = false
let licenses = {
allow: config.allow_licenses,
deny: config.deny_licenses
}
let licenseErrors = getDeniedLicenseChanges(changes, licenses)
if (licenseErrors.length > 0) {
printLicensesError(licenseErrors, licenses)
core.setFailed('Dependency review detected prohibited licenses.')
return
}
let filteredChanges = filterChangesBySeverity(
minSeverity as Severity,
changes
@@ -99,4 +113,35 @@ function renderSeverity(
return `${styles.color[color].open}(${severity} severity)${styles.color[color].close}`
}
function printLicensesError(
changes: Array<Change>,
licenses: {
allow?: Array<string>
deny?: Array<string>
}
): void {
if (changes.length == 0) {
return
}
let {allow = [], deny = []} = licenses
core.info('Dependency review detected prohibited licenses.')
if (allow.length > 0) {
core.info('\nAllowed licenses: ' + allow.join(', ') + '\n')
}
if (deny.length > 0) {
core.info('\nDenied licenses: ' + deny.join(', ') + '\n')
}
core.info('The following dependencies have incompatible licenses:\n')
for (const change of changes) {
core.info(
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close} License: ${styles.color.red.open}${change.license}${styles.color.red.close}`
)
}
}
run()