Merge pull request #309 from actions/codespace-actions-dependency-review-action-p79j7j9pxqrh669p

Add unresolved licenses section
This commit is contained in:
cnagadya
2022-10-27 15:43:28 +02:00
committed by GitHub
7 changed files with 171 additions and 139 deletions
+57 -36
View File
@@ -1,6 +1,7 @@
import {expect, jest, test} from '@jest/globals' import {expect, jest, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas' import {Change, Changes} from '../src/schemas'
import {getDeniedLicenseChanges} from '../src/licenses'
let getInvalidLicenseChanges: Function
let npmChange: Change = { let npmChange: Change = {
manifest: 'package.json', manifest: 'package.json',
@@ -49,15 +50,6 @@ let rubyChange: Change = {
} }
jest.mock('@actions/core') jest.mock('@actions/core')
jest.mock('spdx-satisfies', () => {
return {
__esModule: true,
// hack to coerce / mock spdx-satisfies to return value
// true for BSD, false for all others
// affects only deny_licenses and allow_licenses checks
default: (license: string, _: string): boolean => license === 'BSD'
}
})
const mockOctokit = { const mockOctokit = {
rest: { rest: {
@@ -79,65 +71,94 @@ jest.mock('octokit', () => {
} }
}) })
test('it fails if a license outside the allow list is found', async () => { beforeEach(async () => {
const changes: Changes = [npmChange, rubyChange] jest.resetModules()
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { jest.doMock('spdx-satisfies', () => {
allow: ['BSD'] // mock spdx-satisfies return value
// true for BSD, false for all others
return jest.fn((license: string, _: string): boolean => license === 'BSD')
}) })
expect(invalidChanges[0]).toBe(npmChange) ;({getInvalidLicenseChanges} = require('../src/licenses'))
}) })
test('it fails if a license inside the deny list is found', async () => { test('it adds license outside the allow list to forbidden changes', async () => {
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
const [invalidChanges] = await getDeniedLicenseChanges(changes, { const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD']
})
expect(forbidden[0]).toBe(npmChange)
expect(forbidden.length).toEqual(1)
})
test('it adds license inside the deny list to forbidden changes', async () => {
const changes: Changes = [npmChange, rubyChange]
const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['BSD'] deny: ['BSD']
}) })
expect(invalidChanges[0]).toBe(rubyChange) expect(forbidden[0]).toBe(rubyChange)
expect(forbidden.length).toEqual(1)
}) })
// This is more of a "here's a behavior that might be surprising" than an actual // 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. // 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 adds all licenses to forbidden changes when allow is provided an empty array', async () => {
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
let [invalidChanges, _] = await getDeniedLicenseChanges(changes, { let {forbidden} = await getInvalidLicenseChanges(changes, {
allow: [], allow: [],
deny: ['BSD'] deny: ['BSD']
}) })
expect(invalidChanges.length).toBe(2) expect(forbidden.length).toBe(2)
}) })
test('it does not fail if a license outside the allow list is found in removed changes', async () => { test('it does not add license outside the allow list to forbidden changes if it is in removed changes', async () => {
const changes: Changes = [ const changes: Changes = [
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD'] allow: ['BSD']
}) })
expect(invalidChanges).toStrictEqual([]) expect(forbidden).toStrictEqual([])
}) })
test('it does not fail if a license inside the deny list is found in removed changes', async () => { test('it does not add license inside the deny list to forbidden changes if it is in removed changes', async () => {
const changes: Changes = [ const changes: Changes = [
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['BSD'] deny: ['BSD']
}) })
expect(invalidChanges).toStrictEqual([]) expect(forbidden).toStrictEqual([])
}) })
test('it fails if a license outside the allow list is found in both of added and removed changes', async () => { test('it adds license outside the allow list to forbidden changes if it is in both added and removed changes', async () => {
const changes: Changes = [ const changes: Changes = [
{...npmChange, change_type: 'removed'}, {...npmChange, change_type: 'removed'},
npmChange, npmChange,
{...rubyChange, change_type: 'removed'} {...rubyChange, change_type: 'removed'}
] ]
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, { const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD'] allow: ['BSD']
}) })
expect(invalidChanges).toStrictEqual([npmChange]) expect(forbidden).toStrictEqual([npmChange])
})
test('it adds all licenses to unresolved if it is unable to determine the validity', async () => {
jest.resetModules() // reset module set in before
jest.doMock('spdx-satisfies', () => {
return jest.fn((_first: string, _second: string) => {
throw new Error('Some Error')
})
})
;({getInvalidLicenseChanges} = require('../src/licenses'))
const changes: Changes = [npmChange, rubyChange]
const invalidLicenses = await getInvalidLicenseChanges(changes, {
allow: ['BSD']
})
expect(invalidLicenses.forbidden.length).toEqual(0)
expect(invalidLicenses.unlicensed.length).toEqual(0)
expect(invalidLicenses.unresolved.length).toEqual(2)
}) })
describe('GH License API fallback', () => { describe('GH License API fallback', () => {
@@ -147,7 +168,7 @@ describe('GH License API fallback', () => {
license: null, license: null,
source_repository_url: 'http://github.com/some-owner/some-repo' source_repository_url: 'http://github.com/some-owner/some-repo'
} }
const [_, unknownChanges] = await getDeniedLicenseChanges( const {unlicensed} = await getInvalidLicenseChanges(
[nullLicenseChange, rubyChange], [nullLicenseChange, rubyChange],
{} {}
) )
@@ -156,25 +177,25 @@ describe('GH License API fallback', () => {
owner: 'some-owner', owner: 'some-owner',
repo: 'some-repo' repo: 'some-repo'
}) })
expect(unknownChanges.length).toEqual(0) expect(unlicensed.length).toEqual(0)
}) })
test('it does not call licenses API endpoint for change with null license and invalid source_repository_url ', async () => { test('it does not call licenses API endpoint for change with null license and invalid source_repository_url ', async () => {
const [_, unknownChanges] = await getDeniedLicenseChanges( const {unlicensed} = await getInvalidLicenseChanges(
[{...npmChange, license: null}], [{...npmChange, license: null}],
{} {}
) )
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled() expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unknownChanges.length).toEqual(1) expect(unlicensed.length).toEqual(1)
}) })
test('it does not call licenses API endpoint if licenses for all changes are present', async () => { test('it does not call licenses API endpoint if licenses for all changes are present', async () => {
const [_, unknownChanges] = await getDeniedLicenseChanges( const {unlicensed} = await getInvalidLicenseChanges(
[npmChange, rubyChange], [npmChange, rubyChange],
{} {}
) )
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled() expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unknownChanges.length).toEqual(0) expect(unlicensed.length).toEqual(0)
}) })
}) })
Generated Vendored
+49 -45
View File
@@ -143,7 +143,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getDeniedLicenseChanges = void 0; exports.getInvalidLicenseChanges = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const spdx_satisfies_1 = __importDefault(__nccwpck_require__(4424)); const spdx_satisfies_1 = __importDefault(__nccwpck_require__(4424));
const octokit_1 = __nccwpck_require__(7467); const octokit_1 = __nccwpck_require__(7467);
@@ -158,23 +158,29 @@ const utils_1 = __nccwpck_require__(918);
* 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 {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
*/ */
function getDeniedLicenseChanges(changes, licenses) { function getInvalidLicenseChanges(changes, licenses) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const { allow, deny } = licenses; const { allow, deny } = licenses;
const groupedChanges = yield groupChanges(changes); const groupedChanges = yield groupChanges(changes);
const unlicensedChanges = groupedChanges.unlicensed;
const licensedChanges = groupedChanges.licensed; const licensedChanges = groupedChanges.licensed;
const forbiddenLicenseChanges = []; const invalidLicenseChanges = {
unlicensed: groupedChanges.unlicensed,
unresolved: [],
forbidden: []
};
const validityCache = new Map(); const validityCache = new Map();
for (const change of licensedChanges) { for (const change of licensedChanges) {
// should never happen since licensedChanges have licenses. Look into Intersection Types
const license = change.license; const license = change.license;
// should never happen since licensedChanges always have licenses but license is nullable in changes schema
if (license === null) { if (license === null) {
continue; continue;
} }
if (validityCache.get(license) === undefined) { if (license === 'NOASSERTION') {
invalidLicenseChanges.unlicensed.push(change);
}
else if (validityCache.get(license) === undefined) {
try { try {
if (allow !== undefined) { if (allow !== undefined) {
const found = allow.find(spdxExpression => (0, spdx_satisfies_1.default)(license, spdxExpression)); const found = allow.find(spdxExpression => (0, spdx_satisfies_1.default)(license, spdxExpression));
@@ -185,22 +191,18 @@ function getDeniedLicenseChanges(changes, licenses) {
validityCache.set(license, found === undefined); validityCache.set(license, found === undefined);
} }
} }
catch (_) { catch (err) {
// eslint-disable-next-line no-console invalidLicenseChanges.unresolved.push(change);
console.log(`Invalid spdx license ${license} for ${change.name}`);
} }
} }
// TODO: Verify spdxSatisfies is working as expected as currently:
// spdxSatisfies("MIT", "MIT AND (GPL-2.0 OR ISC)") => true
// spdxSatisfies("MIT AND (GPL-2.0 OR ISC)", "MIT") => false
if (validityCache.get(license) === false) { if (validityCache.get(license) === false) {
forbiddenLicenseChanges.push(change); invalidLicenseChanges.forbidden.push(change);
} }
} }
return [forbiddenLicenseChanges, unlicensedChanges]; return invalidLicenseChanges;
}); });
} }
exports.getDeniedLicenseChanges = getDeniedLicenseChanges; exports.getInvalidLicenseChanges = getInvalidLicenseChanges;
const fetchGHLicense = (owner, repo) => __awaiter(void 0, void 0, void 0, function* () { const fetchGHLicense = (owner, repo) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b; var _a, _b;
const octokit = new octokit_1.Octokit({ const octokit = new octokit_1.Octokit({
@@ -362,16 +364,16 @@ function run() {
const addedChanges = (0, filter_1.filterChangesBySeverity)(minSeverity, filteredChanges).filter(change => change.change_type === 'added' && const addedChanges = (0, filter_1.filterChangesBySeverity)(minSeverity, filteredChanges).filter(change => change.change_type === 'added' &&
change.vulnerabilities !== undefined && change.vulnerabilities !== undefined &&
change.vulnerabilities.length > 0); change.vulnerabilities.length > 0);
const [licenseErrors, unknownLicenses] = yield (0, licenses_1.getDeniedLicenseChanges)(filteredChanges, { const invalidLicenseChanges = yield (0, licenses_1.getInvalidLicenseChanges)(filteredChanges, {
allow: config.allow_licenses, allow: config.allow_licenses,
deny: config.deny_licenses deny: config.deny_licenses
}); });
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses); summary.addSummaryToSummary(addedChanges, invalidLicenseChanges);
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity); summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity);
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config); summary.addLicensesToSummary(invalidLicenseChanges, config);
summary.addScannedDependencies(changes); summary.addScannedDependencies(changes);
printVulnerabilitiesBlock(addedChanges, minSeverity); printVulnerabilitiesBlock(addedChanges, minSeverity);
printLicensesBlock(licenseErrors, unknownLicenses); printLicensesBlock(invalidLicenseChanges);
printScannedDependencies(changes); printScannedDependencies(changes);
} }
catch (error) { catch (error) {
@@ -418,20 +420,22 @@ function printChangeVulnerabilities(change) {
core.info(`${vuln.advisory_url}`); core.info(`${vuln.advisory_url}`);
} }
} }
function printLicensesBlock(licenseErrors, unknownLicenses) { function printLicensesBlock(invalidLicenseChanges) {
core.group('Licenses', () => __awaiter(this, void 0, void 0, function* () { core.group('Licenses', () => __awaiter(this, void 0, void 0, function* () {
if (licenseErrors.length > 0) { if (invalidLicenseChanges.forbidden.length > 0) {
printLicensesError(licenseErrors); core.info('\nThe following dependencies have incompatible licenses:\n');
printLicensesError(invalidLicenseChanges.forbidden);
core.setFailed('Dependency review detected incompatible licenses.'); core.setFailed('Dependency review detected incompatible licenses.');
} }
printNullLicenses(unknownLicenses); if (invalidLicenseChanges.unresolved.length > 0) {
core.warning('\nThe validity of the licenses of the dependecies below could not be determine. Ensure that they are valid spdx licenses:\n');
printLicensesError(invalidLicenseChanges.unresolved);
core.setFailed('Dependency review could not detect the validity of all licenses.');
}
printNullLicenses(invalidLicenseChanges.unlicensed);
})); }));
} }
function printLicensesError(changes) { function printLicensesError(changes) {
if (changes.length === 0) {
return;
}
core.info('\nThe following dependencies have incompatible licenses:\n');
for (const change of changes) { 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}`); 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}`);
} }
@@ -595,10 +599,12 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.addScannedDependencies = exports.addLicensesToSummary = exports.addChangeVulnerabilitiesToSummary = exports.addSummaryToSummary = void 0; 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);
function addSummaryToSummary(addedPackages, licenseErrors, unknownLicenses) { function addSummaryToSummary(addedPackages, invalidLicenseChanges) {
core.summary core.summary.addHeading('Dependency Review').addRaw(`We found:
.addHeading('Dependency Review') - ${addedPackages.length} vulnerable package(s),
.addRaw(`We found ${addedPackages.length} vulnerable package(s), ${licenseErrors.length} package(s) with incompatible licenses, and ${unknownLicenses.length} package(s) with unknown licenses.`); - ${invalidLicenseChanges.unresolved.length} package(s) with unprocessable licenses,
- ${invalidLicenseChanges.forbidden.length} package(s) with incompatible licenses, and
- ${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.`);
} }
exports.addSummaryToSummary = addSummaryToSummary; exports.addSummaryToSummary = addSummaryToSummary;
function addChangeVulnerabilitiesToSummary(addedPackages, severity) { function addChangeVulnerabilitiesToSummary(addedPackages, severity) {
@@ -649,7 +655,7 @@ function addChangeVulnerabilitiesToSummary(addedPackages, severity) {
} }
} }
exports.addChangeVulnerabilitiesToSummary = addChangeVulnerabilitiesToSummary; exports.addChangeVulnerabilitiesToSummary = addChangeVulnerabilitiesToSummary;
function addLicensesToSummary(licenseErrors, unknownLicenses, config) { function addLicensesToSummary(invalidLicenseChanges, config) {
core.summary.addHeading('Licenses'); core.summary.addHeading('Licenses');
if (config.allow_licenses && config.allow_licenses.length > 0) { if (config.allow_licenses && config.allow_licenses.length > 0) {
core.summary.addQuote(`<strong>Allowed Licenses</strong>: ${config.allow_licenses.join(', ')}`); core.summary.addQuote(`<strong>Allowed Licenses</strong>: ${config.allow_licenses.join(', ')}`);
@@ -657,17 +663,18 @@ function addLicensesToSummary(licenseErrors, unknownLicenses, config) {
if (config.deny_licenses && config.deny_licenses.length > 0) { if (config.deny_licenses && config.deny_licenses.length > 0) {
core.summary.addQuote(`<strong>Denied Licenses</strong>: ${config.deny_licenses.join(', ')}`); core.summary.addQuote(`<strong>Denied Licenses</strong>: ${config.deny_licenses.join(', ')}`);
} }
if (licenseErrors.length === 0 && unknownLicenses.length === 0) { if (invalidLicenseChanges.forbidden.length === 0 &&
invalidLicenseChanges.unlicensed.length === 0) {
core.summary.addQuote('No license violations detected.'); core.summary.addQuote('No license violations detected.');
return; return;
} }
if (licenseErrors.length > 0) { if (invalidLicenseChanges.forbidden.length > 0) {
const rows = []; const rows = [];
const manifests = (0, utils_1.getManifestsSet)(licenseErrors); const manifests = (0, utils_1.getManifestsSet)(invalidLicenseChanges.forbidden);
core.summary.addHeading('Incompatible Licenses', 3).addSeparator(); core.summary.addHeading('Incompatible Licenses', 3).addSeparator();
for (const manifest of manifests) { for (const manifest of manifests) {
core.summary.addHeading(`<em>${manifest}</em>`, 4); core.summary.addHeading(`<em>${manifest}</em>`, 4);
for (const change of licenseErrors.filter(pkg => pkg.manifest === manifest)) { for (const change of invalidLicenseChanges.forbidden.filter(pkg => pkg.manifest === manifest)) {
rows.push([ rows.push([
(0, utils_1.renderUrl)(change.source_repository_url, change.name), (0, utils_1.renderUrl)(change.source_repository_url, change.name),
change.version, change.version,
@@ -680,15 +687,15 @@ function addLicensesToSummary(licenseErrors, unknownLicenses, config) {
else { else {
core.summary.addQuote('No license violations detected.'); core.summary.addQuote('No license violations detected.');
} }
core.debug(`found ${unknownLicenses.length} unknown licenses`); core.debug(`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`);
if (unknownLicenses.length > 0) { if (invalidLicenseChanges.unlicensed.length > 0) {
const rows = []; const rows = [];
const manifests = (0, utils_1.getManifestsSet)(unknownLicenses); const manifests = (0, utils_1.getManifestsSet)(invalidLicenseChanges.unlicensed);
core.debug(`found ${manifests.entries.length} manifests for unknown licenses`); core.debug(`found ${manifests.entries.length} manifests for unknown licenses`);
core.summary.addHeading('Unknown Licenses', 3).addSeparator(); core.summary.addHeading('Unknown Licenses', 3).addSeparator();
for (const manifest of manifests) { for (const manifest of manifests) {
core.summary.addHeading(`<em>${manifest}</em>`, 4); core.summary.addHeading(`<em>${manifest}</em>`, 4);
for (const change of unknownLicenses.filter(pkg => pkg.manifest === manifest)) { for (const change of invalidLicenseChanges.unlicensed.filter(pkg => pkg.manifest === manifest)) {
rows.push([ rows.push([
(0, utils_1.renderUrl)(change.source_repository_url, change.name), (0, utils_1.renderUrl)(change.source_repository_url, change.name),
change.version change.version
@@ -27403,14 +27410,11 @@ function parseList(list) {
return list.split(',').map(x => x.trim()); return list.split(',').map(x => x.trim());
} }
} }
function getInvalidLicenses(licenses) {
return licenses.filter(license => !(0, utils_1.isSPDXValid)(license));
}
function validateLicenses(key, licenses) { function validateLicenses(key, licenses) {
if (licenses === undefined) { if (licenses === undefined) {
return; return;
} }
const invalid_licenses = getInvalidLicenses(licenses); const invalid_licenses = licenses.filter(license => !(0, utils_1.isSPDXValid)(license));
if (invalid_licenses.length > 0) { if (invalid_licenses.length > 0) {
throw new Error(`Invalid license(s) in ${key}: ${invalid_licenses.join(', ')}`); throw new Error(`Invalid license(s) in ${key}: ${invalid_licenses.join(', ')}`);
} }
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -5
View File
@@ -26,10 +26,6 @@ function parseList(list: string | undefined): string[] | undefined {
} }
} }
function getInvalidLicenses(licenses: string[]): string[] {
return licenses.filter(license => !isSPDXValid(license))
}
function validateLicenses( function validateLicenses(
key: licenseKey, key: licenseKey,
licenses: string[] | undefined licenses: string[] | undefined
@@ -37,7 +33,7 @@ function validateLicenses(
if (licenses === undefined) { if (licenses === undefined) {
return return
} }
const invalid_licenses = getInvalidLicenses(licenses) const invalid_licenses = licenses.filter(license => !isSPDXValid(license))
if (invalid_licenses.length > 0) { if (invalid_licenses.length > 0) {
throw new Error( throw new Error(
+18 -16
View File
@@ -15,32 +15,39 @@ import {isSPDXValid} from './utils'
* 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 {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
*/ */
export async function getDeniedLicenseChanges( export async function getInvalidLicenseChanges(
changes: Change[], changes: Change[],
licenses: { licenses: {
allow?: string[] allow?: string[]
deny?: string[] deny?: string[]
} }
): Promise<[Change[], Change[]]> { ): Promise<Record<string, Changes>> {
const {allow, deny} = licenses const {allow, deny} = licenses
const groupedChanges = await groupChanges(changes) const groupedChanges = await groupChanges(changes)
const unlicensedChanges: Changes = groupedChanges.unlicensed
const licensedChanges: Changes = groupedChanges.licensed const licensedChanges: Changes = groupedChanges.licensed
const forbiddenLicenseChanges: Changes = [] const invalidLicenseChanges: Record<string, Changes> = {
unlicensed: groupedChanges.unlicensed,
unresolved: [],
forbidden: []
}
const validityCache = new Map<string, boolean>() const validityCache = new Map<string, boolean>()
for (const change of licensedChanges) { for (const change of licensedChanges) {
// should never happen since licensedChanges have licenses. Look into Intersection Types
const license = change.license const license = change.license
// should never happen since licensedChanges always have licenses but license is nullable in changes schema
if (license === null) { if (license === null) {
continue continue
} }
if (validityCache.get(license) === undefined) { if (license === 'NOASSERTION') {
invalidLicenseChanges.unlicensed.push(change)
} else if (validityCache.get(license) === undefined) {
try { try {
if (allow !== undefined) { if (allow !== undefined) {
const found = allow.find(spdxExpression => const found = allow.find(spdxExpression =>
@@ -53,22 +60,17 @@ export async function getDeniedLicenseChanges(
) )
validityCache.set(license, found === undefined) validityCache.set(license, found === undefined)
} }
} catch (_) { } catch (err) {
// eslint-disable-next-line no-console invalidLicenseChanges.unresolved.push(change)
console.log(`Invalid spdx license ${license} for ${change.name}`)
} }
} }
// TODO: Verify spdxSatisfies is working as expected as currently:
// spdxSatisfies("MIT", "MIT AND (GPL-2.0 OR ISC)") => true
// spdxSatisfies("MIT AND (GPL-2.0 OR ISC)", "MIT") => false
if (validityCache.get(license) === false) { if (validityCache.get(license) === false) {
forbiddenLicenseChanges.push(change) invalidLicenseChanges.forbidden.push(change)
} }
} }
return [forbiddenLicenseChanges, unlicensedChanges] return invalidLicenseChanges
} }
const fetchGHLicense = async ( const fetchGHLicense = async (
+22 -18
View File
@@ -10,7 +10,7 @@ import {
filterChangesByScopes, filterChangesByScopes,
filterAllowedAdvisories filterAllowedAdvisories
} from '../src/filter' } from '../src/filter'
import {getDeniedLicenseChanges} from './licenses' import {getInvalidLicenseChanges} from './licenses'
import * as summary from './summary' import * as summary from './summary'
import {getRefs} from './git-refs' import {getRefs} from './git-refs'
@@ -45,7 +45,7 @@ async function run(): Promise<void> {
change.vulnerabilities.length > 0 change.vulnerabilities.length > 0
) )
const [licenseErrors, unknownLicenses] = await getDeniedLicenseChanges( const invalidLicenseChanges = await getInvalidLicenseChanges(
filteredChanges, filteredChanges,
{ {
allow: config.allow_licenses, allow: config.allow_licenses,
@@ -53,13 +53,13 @@ async function run(): Promise<void> {
} }
) )
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses) summary.addSummaryToSummary(addedChanges, invalidLicenseChanges)
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity) summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config) summary.addLicensesToSummary(invalidLicenseChanges, config)
summary.addScannedDependencies(changes) summary.addScannedDependencies(changes)
printVulnerabilitiesBlock(addedChanges, minSeverity) printVulnerabilitiesBlock(addedChanges, minSeverity)
printLicensesBlock(licenseErrors, unknownLicenses) printLicensesBlock(invalidLicenseChanges)
printScannedDependencies(changes) printScannedDependencies(changes)
} catch (error) { } catch (error) {
if (error instanceof RequestError && error.status === 404) { if (error instanceof RequestError && error.status === 404) {
@@ -83,7 +83,7 @@ async function run(): Promise<void> {
} }
function printVulnerabilitiesBlock( function printVulnerabilitiesBlock(
addedChanges: Change[], addedChanges: Changes,
minSeverity: Severity minSeverity: Severity
): void { ): void {
let failed = false let failed = false
@@ -119,24 +119,28 @@ function printChangeVulnerabilities(change: Change): void {
} }
function printLicensesBlock( function printLicensesBlock(
licenseErrors: Change[], invalidLicenseChanges: Record<string, Changes>
unknownLicenses: Change[]
): void { ): void {
core.group('Licenses', async () => { core.group('Licenses', async () => {
if (licenseErrors.length > 0) { if (invalidLicenseChanges.forbidden.length > 0) {
printLicensesError(licenseErrors) core.info('\nThe following dependencies have incompatible licenses:\n')
printLicensesError(invalidLicenseChanges.forbidden)
core.setFailed('Dependency review detected incompatible licenses.') core.setFailed('Dependency review detected incompatible licenses.')
} }
printNullLicenses(unknownLicenses) if (invalidLicenseChanges.unresolved.length > 0) {
core.warning(
'\nThe validity of the licenses of the dependecies below could not be determine. Ensure that they are valid spdx licenses:\n'
)
printLicensesError(invalidLicenseChanges.unresolved)
core.setFailed(
'Dependency review could not detect the validity of all licenses.'
)
}
printNullLicenses(invalidLicenseChanges.unlicensed)
}) })
} }
function printLicensesError(changes: Change[]): void { function printLicensesError(changes: Changes): void {
if (changes.length === 0) {
return
}
core.info('\nThe following dependencies have incompatible licenses:\n')
for (const change of changes) { for (const change of changes) {
core.info( 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}` `${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close} License: ${styles.color.red.open}${change.license}${styles.color.red.close}`
@@ -144,7 +148,7 @@ function printLicensesError(changes: Change[]): void {
} }
} }
function printNullLicenses(changes: Change[]): void { function printNullLicenses(changes: Changes): void {
if (changes.length === 0) { if (changes.length === 0) {
return return
} }
+22 -17
View File
@@ -1,17 +1,18 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {ConfigurationOptions, Change, Changes} from './schemas' import {ConfigurationOptions, Changes} from './schemas'
import {SummaryTableRow} from '@actions/core/lib/summary' import {SummaryTableRow} from '@actions/core/lib/summary'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils' import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
export function addSummaryToSummary( export function addSummaryToSummary(
addedPackages: Changes, addedPackages: Changes,
licenseErrors: Change[], invalidLicenseChanges: Record<string, Changes>
unknownLicenses: Change[]
): void { ): void {
core.summary core.summary.addHeading('Dependency Review').addRaw(
.addHeading('Dependency Review') `We found:
.addRaw( - ${addedPackages.length} vulnerable package(s),
`We found ${addedPackages.length} vulnerable package(s), ${licenseErrors.length} package(s) with incompatible licenses, and ${unknownLicenses.length} package(s) with unknown licenses.` - ${invalidLicenseChanges.unresolved.length} package(s) with unprocessable licenses,
- ${invalidLicenseChanges.forbidden.length} package(s) with incompatible licenses, and
- ${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.`
) )
} }
@@ -76,8 +77,7 @@ export function addChangeVulnerabilitiesToSummary(
} }
export function addLicensesToSummary( export function addLicensesToSummary(
licenseErrors: Change[], invalidLicenseChanges: Record<string, Changes>,
unknownLicenses: Change[],
config: ConfigurationOptions config: ConfigurationOptions
): void { ): void {
core.summary.addHeading('Licenses') core.summary.addHeading('Licenses')
@@ -93,21 +93,24 @@ export function addLicensesToSummary(
) )
} }
if (licenseErrors.length === 0 && unknownLicenses.length === 0) { if (
invalidLicenseChanges.forbidden.length === 0 &&
invalidLicenseChanges.unlicensed.length === 0
) {
core.summary.addQuote('No license violations detected.') core.summary.addQuote('No license violations detected.')
return return
} }
if (licenseErrors.length > 0) { if (invalidLicenseChanges.forbidden.length > 0) {
const rows: SummaryTableRow[] = [] const rows: SummaryTableRow[] = []
const manifests = getManifestsSet(licenseErrors) const manifests = getManifestsSet(invalidLicenseChanges.forbidden)
core.summary.addHeading('Incompatible Licenses', 3).addSeparator() core.summary.addHeading('Incompatible Licenses', 3).addSeparator()
for (const manifest of manifests) { for (const manifest of manifests) {
core.summary.addHeading(`<em>${manifest}</em>`, 4) core.summary.addHeading(`<em>${manifest}</em>`, 4)
for (const change of licenseErrors.filter( for (const change of invalidLicenseChanges.forbidden.filter(
pkg => pkg.manifest === manifest pkg => pkg.manifest === manifest
)) { )) {
rows.push([ rows.push([
@@ -122,11 +125,13 @@ export function addLicensesToSummary(
core.summary.addQuote('No license violations detected.') core.summary.addQuote('No license violations detected.')
} }
core.debug(`found ${unknownLicenses.length} unknown licenses`) core.debug(
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
)
if (unknownLicenses.length > 0) { if (invalidLicenseChanges.unlicensed.length > 0) {
const rows: SummaryTableRow[] = [] const rows: SummaryTableRow[] = []
const manifests = getManifestsSet(unknownLicenses) const manifests = getManifestsSet(invalidLicenseChanges.unlicensed)
core.debug( core.debug(
`found ${manifests.entries.length} manifests for unknown licenses` `found ${manifests.entries.length} manifests for unknown licenses`
@@ -137,7 +142,7 @@ export function addLicensesToSummary(
for (const manifest of manifests) { for (const manifest of manifests) {
core.summary.addHeading(`<em>${manifest}</em>`, 4) core.summary.addHeading(`<em>${manifest}</em>`, 4)
for (const change of unknownLicenses.filter( for (const change of invalidLicenseChanges.unlicensed.filter(
pkg => pkg.manifest === manifest pkg => pkg.manifest === manifest
)) { )) {
rows.push([ rows.push([