Merge pull request #309 from actions/codespace-actions-dependency-review-action-p79j7j9pxqrh669p
Add unresolved licenses section
This commit is contained in:
+57
-36
@@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+49
-45
@@ -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(', ')}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-5
@@ -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
@@ -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
@@ -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
@@ -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([
|
||||||
|
|||||||
Reference in New Issue
Block a user