Merge pull request #106 from actions/adding-lists
Adding allow and deny lists
This commit is contained in:
13
.vscode/launch.json
vendored
Normal file
13
.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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: [
|
||||
|
||||
2
__tests__/fixtures/conflictive-config.yml
Normal file
2
__tests__/fixtures/conflictive-config.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
allow_licenses: []
|
||||
deny_licenses: []
|
||||
1
__tests__/fixtures/no-licenses-config.yml
Normal file
1
__tests__/fixtures/no-licenses-config.yml
Normal file
@@ -0,0 +1 @@
|
||||
fail_on_severity: critical
|
||||
70
__tests__/licenses.test.ts
Normal file
70
__tests__/licenses.test.ts
Normal 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
81
dist/index.js
generated
vendored
@@ -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
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -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
44
src/licenses.ts
Normal 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
|
||||
}
|
||||
45
src/main.ts
45
src/main.ts
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user