Compare commits

...
34 Commits
Author SHA1 Message Date
Federico Builes 468485fc8e Clean up the main script a bit. 2022-09-27 12:25:12 +02:00
Federico Builes 46c9f79a1f Create utils.ts file for helper functions. 2022-09-27 12:23:05 +02:00
Federico Builes cd3f55e8f9 Add all the dependencies to the review summary too. 2022-09-27 11:52:15 +02:00
Federico Builes 0515f5cb39 Adding a skeleton for scanned dependencies in the summary. 2022-09-26 19:14:04 +02:00
Federico Builes 2d1d679f58 Move manifest grouping outside main.ts 2022-09-26 19:13:25 +02:00
Federico Builes a3563a05bc Use a set instead of raw JS objects. 2022-09-26 12:41:16 +02:00
Federico Builes 8a20ddbf25 try adding 3 sections 2022-09-26 12:21:24 +02:00
Federico Builes 2a646668d9 adding dist 2022-09-26 12:03:34 +02:00
Federico Builes 60be833ffd Update manifest formatting in output. 2022-09-26 12:01:39 +02:00
Federico Builes edc501a219 adding dist 2022-09-26 11:41:40 +02:00
Federico Builes 000837f2ac Don't nest groups. 2022-09-26 11:41:02 +02:00
Federico Builes 89f99d150a adding colors to the dep output 2022-09-26 11:35:05 +02:00
Federico Builes 0ed41eff02 Merge branch 'main' into add-scanned-deps 2022-09-26 11:34:43 +02:00
Federico Builes dbe70eb550 updating gitignore 2022-09-26 11:29:22 +02:00
Federico Builes 78c7c01396 Merge branch 'main' into add-scanned-deps
# Conflicts:
#	dist/index.js.map
2022-09-26 08:47:23 +02:00
Federico BuilesandGitHub 89a5c76329 Merge pull request #254 from actions/dependabot/npm_and_yarn/actions/github-5.1.0
Bump @actions/github from 5.0.3 to 5.1.0
2022-09-26 08:46:18 +02:00
Federico Builes 4a6d691283 adding dist 2022-09-26 08:45:09 +02:00
Federico BuilesandGitHub b58d457243 Merge pull request #253 from actions/dependabot/npm_and_yarn/types/node-16.11.60
Bump @types/node from 16.11.59 to 16.11.60
2022-09-26 08:42:47 +02:00
Federico BuilesandGitHub cc033856be Merge pull request #255 from actions/dependabot/npm_and_yarn/eslint-8.24.0
Bump eslint from 8.23.1 to 8.24.0
2022-09-26 08:04:38 +02:00
dependabot[bot]andGitHub 8595e805a5 Bump eslint from 8.23.1 to 8.24.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.23.1 to 8.24.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.23.1...v8.24.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-26 01:55:41 +00:00
dependabot[bot]andGitHub fa10a7f0d6 Bump @actions/github from 5.0.3 to 5.1.0
Bumps [@actions/github](https://github.com/actions/toolkit/tree/HEAD/packages/github) from 5.0.3 to 5.1.0.
- [Release notes](https://github.com/actions/toolkit/releases)
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/github/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/github)

---
updated-dependencies:
- dependency-name: "@actions/github"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-26 01:54:35 +00:00
dependabot[bot]andGitHub 6755d8aa71 Bump @types/node from 16.11.59 to 16.11.60
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 16.11.59 to 16.11.60.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-26 01:54:19 +00:00
Sarah AladetanandGitHub 375c537008 Updating to 2.4.0 2022-09-23 13:07:20 -07:00
Sarah AladetanandGitHub 98f28ebe06 Merge pull request #251 from actions/sarahkemi/ghsa-allowlist
Filter by vulnerability allow-list
2022-09-23 13:06:41 -07:00
Sarah Aladetan 716b322ec9 add allow-ghsas input to action.yml 2022-09-23 19:59:39 +00:00
12ae1bd550 Update wording in README.md
Co-authored-by: Federico Builes <[email protected]>
2022-09-23 12:32:46 -07:00
Sarah Aladetan bcb52636bd build and package allow-ghsas 2022-09-22 22:58:43 +00:00
Sarah Aladetan 241ff73141 add doc on allow-ghsas to readme 2022-09-22 22:44:17 +00:00
Sarah Aladetan 062b749663 revise ghsa filter 2022-09-22 22:36:34 +00:00
Sarah Aladetan 4f00b72b84 filter allowed ghsas in action flow 2022-09-22 22:25:21 +00:00
Sarah Aladetan 602f968ea2 create a filter for vulns that are on the allowlist 2022-09-22 21:36:26 +00:00
Sarah Aladetan bd61ea0d9e create config option for ghsa allowlist 2022-09-22 21:34:18 +00:00
Federico Builes 8ec13c1f01 adding dist 2022-09-22 16:52:03 +02:00
Federico Builes 723ec8c0d3 Try showing information about the scanned dependencies. 2022-09-22 16:49:45 +02:00
15 changed files with 496 additions and 164 deletions
+1
View File
@@ -1,4 +1,5 @@
event.json
.ruby-version
# Dependency directory
node_modules
+26 -6
View File
@@ -75,7 +75,7 @@ A string representing the path to an external configuraton file. By
default external configuration files are not used.
**Possible values**: A string representing the absolute path to the
configuration file.
configuration file.
**Example**: `config-file: ./.github/dependency-review-config.yml`.
@@ -97,6 +97,7 @@ support. The default value is `development, runtime`.
**Inline example**: `fail-on-scopes: development, runtime`
**YAML example**:
```yaml
# this prevents scanning development dependencies
fail-on-scopes:
@@ -113,6 +114,7 @@ https://docs.github.com/en/rest/licenses.
**Inline example**: `allow-licenses: BSD-3-Clause, MIT`
**YAML example**:
```yaml
allow-licenses:
- BSD-3-Clause
@@ -130,12 +132,29 @@ https://docs.github.com/en/rest/licenses.
**Inline example**: `deny-licenses: LGPL-2.0, BSD-2-Clause`
**YAML example**:
```yaml
deny-licenses:
- LGPL-2.0
- BSD-2-Clause
```
### allow-ghsas
Add a custom list of GitHub Advisory IDs that can be skipped during detection.
**Possible values**: Any valid advisory GHSA ids.
**Inline example**: `allow-ghsas: GHSA-abcd-1234-5679, GHSA-efgh-1234-5679`
**YAML example**:
```yaml
allow-ghsas:
- GHSA-abcd-1234-5679
- GHSA-efgh-1234-5679
```
### base-ref/head-ref
Provide custom git references for the git base/head when performing
@@ -146,6 +165,7 @@ this. The values need to be specified for all other event types.
**Possible values**: Any valid git ref(s) in your project.
**Example**:
```yaml
base-ref: 8bb8a58d6a4028b6c2e314d5caaf273f57644896
head-ref: 69af5638bf660cf218aad5709a4c100e42a2f37b
@@ -163,18 +183,18 @@ file:
- name: Dependency Review
uses: actions/dependency-review-action@v2
with:
config-file: "./.github/dependency-review-config.yml"
config-file: './.github/dependency-review-config.yml'
```
And then create the file in the path you just specified. **All of these fields are
optional**:
```yaml
fail-on-severity: "critical"
fail-on-severity: 'critical'
allow-licenses:
- "GPL-3.0"
- "BSD-3-Clause"
- "MIT"
- 'GPL-3.0'
- 'BSD-3-Clause'
- 'MIT'
```
### Inline Configuration
+15
View File
@@ -16,6 +16,7 @@ function clearInputs() {
'FAIL-ON-SCOPES',
'ALLOW-LICENSES',
'DENY-LICENSES',
'ALLOW-GHSAS',
'CONFIG-FILE',
'BASE-REF',
'HEAD-REF'
@@ -160,3 +161,17 @@ test('it raises an error when given invalid scope', async () => {
setInput('fail-on-scopes', 'runtime, zombies')
expect(() => readConfig()).toThrow()
})
test('it defaults to an empty GHSA allowlist', async () => {
const options = readConfig()
expect(options.allow_ghsas).toEqual(undefined)
})
test('it successfully parses GHSA allowlist', async () => {
setInput('allow-ghsas', 'GHSA-abcd-1234-5679, GHSA-efgh-1234-5679')
const options = readConfig()
expect(options.allow_ghsas).toEqual([
'GHSA-abcd-1234-5679',
'GHSA-efgh-1234-5679'
])
})
+44 -1
View File
@@ -1,6 +1,10 @@
import {expect, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
import {filterChangesBySeverity, filterChangesByScopes} from '../src/filter'
import {
filterChangesBySeverity,
filterChangesByScopes,
filterOutAllowedAdvisories
} from '../src/filter'
let npmChange: Change = {
manifest: 'package.json',
@@ -48,6 +52,19 @@ let rubyChange: Change = {
]
}
let noVulnNpmChange: Change = {
manifest: 'package.json',
change_type: 'added',
ecosystem: 'npm',
name: 'helpful',
version: '1.0.0',
package_url: 'pkg:npm/[email protected]',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: []
}
test('it properly filters changes by severity', async () => {
const changes = [npmChange, rubyChange]
let result = filterChangesBySeverity('high', changes)
@@ -72,3 +89,29 @@ test('it properly filters changes by scope', async () => {
result = filterChangesByScopes(['runtime', 'development'], changes)
expect(result).toEqual([npmChange, rubyChange])
})
test('it properly filters changes with allowed vulnerabilities', async () => {
const changes = [npmChange, rubyChange, noVulnNpmChange]
let result = filterOutAllowedAdvisories(['notrealGHSAID'], changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
result = filterOutAllowedAdvisories(['first-random_string'], changes)
expect(result).toEqual([rubyChange, noVulnNpmChange])
result = filterOutAllowedAdvisories(
['second-random_string', 'third-random_string'],
changes
)
expect(result).toEqual([npmChange, noVulnNpmChange])
result = filterOutAllowedAdvisories(
['first-random_string', 'second-random_string', 'third-random_string'],
changes
)
expect(result).toEqual([noVulnNpmChange])
// if we have a change with multiple vulnerabilities but only one is allowed, we still should not filter out that change
result = filterOutAllowedAdvisories(['second-random_string'], changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
})
+3
View File
@@ -29,6 +29,9 @@ inputs:
deny-licenses:
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
required: false
allow-ghsas:
description: Comma-separated list of allowed Github Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
required: false
runs:
using: 'node16'
main: 'dist/index.js'
Generated Vendored
+165 -52
View File
@@ -203,6 +203,7 @@ const filter_1 = __nccwpck_require__(8752);
const licenses_1 = __nccwpck_require__(3247);
const summary = __importStar(__nccwpck_require__(8608));
const git_refs_1 = __nccwpck_require__(1086);
const utils_1 = __nccwpck_require__(918);
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
@@ -215,37 +216,22 @@ function run() {
headRef: refs.head
});
const minSeverity = config.fail_on_severity;
let failed = false;
const licenses = {
allow: config.allow_licenses,
deny: config.deny_licenses
};
const scopes = config.fail_on_scopes;
const scopedChanges = (0, filter_1.filterChangesByScopes)(scopes, changes);
const addedChanges = (0, filter_1.filterChangesBySeverity)(minSeverity, scopedChanges).filter(change => change.change_type === 'added' &&
const scopedChanges = (0, filter_1.filterChangesByScopes)(config.fail_on_scopes, changes);
const filteredChanges = (0, filter_1.filterOutAllowedAdvisories)(config.allow_ghsas, scopedChanges);
const addedChanges = (0, filter_1.filterChangesBySeverity)(minSeverity, filteredChanges).filter(change => change.change_type === 'added' &&
change.vulnerabilities !== undefined &&
change.vulnerabilities.length > 0);
const [licenseErrors, unknownLicenses] = (0, licenses_1.getDeniedLicenseChanges)(scopedChanges, licenses);
const [licenseErrors, unknownLicenses] = (0, licenses_1.getDeniedLicenseChanges)(filteredChanges, {
allow: config.allow_licenses,
deny: config.deny_licenses
});
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses);
if (addedChanges.length > 0) {
for (const change of addedChanges) {
printChangeVulnerabilities(change);
}
failed = true;
}
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity || '');
if (licenseErrors.length > 0) {
printLicensesError(licenseErrors);
core.setFailed('Dependency review detected incompatible licenses.');
}
printNullLicenses(unknownLicenses);
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity);
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config);
if (failed) {
core.setFailed('Dependency review detected vulnerable packages.');
}
else {
core.info(`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`);
}
summary.addScannedDependencies(changes);
printVulnerabilitiesBlock(addedChanges, minSeverity);
printLicensesBlock(licenseErrors, unknownLicenses);
printScannedDependencies(changes);
}
catch (error) {
if (error instanceof request_error_1.RequestError && error.status === 404) {
@@ -268,20 +254,37 @@ function run() {
}
});
}
function printVulnerabilitiesBlock(addedChanges, minSeverity) {
let failed = false;
core.group('Vulnerabilities', () => __awaiter(this, void 0, void 0, function* () {
if (addedChanges.length > 0) {
for (const change of addedChanges) {
printChangeVulnerabilities(change);
}
failed = true;
}
if (failed) {
core.setFailed('Dependency review detected vulnerable packages.');
}
else {
core.info(`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`);
}
}));
}
function printChangeVulnerabilities(change) {
for (const vuln of change.vulnerabilities) {
core.info(`${ansi_styles_1.default.bold.open}${change.manifest} » ${change.name}@${change.version}${ansi_styles_1.default.bold.close} ${vuln.advisory_summary} ${renderSeverity(vuln.severity)}`);
core.info(`${vuln.advisory_url}`);
}
}
function renderSeverity(severity) {
const color = {
critical: 'red',
high: 'red',
moderate: 'yellow',
low: 'grey'
}[severity];
return `${ansi_styles_1.default.color[color].open}(${severity} severity)${ansi_styles_1.default.color[color].close}`;
function printLicensesBlock(licenseErrors, unknownLicenses) {
core.group('Licenses', () => __awaiter(this, void 0, void 0, function* () {
if (licenseErrors.length > 0) {
printLicensesError(licenseErrors);
core.setFailed('Dependency review detected incompatible licenses.');
}
printNullLicenses(unknownLicenses);
}));
}
function printLicensesError(changes) {
if (changes.length === 0) {
@@ -301,6 +304,42 @@ function printNullLicenses(changes) {
core.info(`${ansi_styles_1.default.bold.open}${change.manifest} » ${change.name}@${change.version}${ansi_styles_1.default.bold.close}`);
}
}
function renderSeverity(severity) {
const color = {
critical: 'red',
high: 'red',
moderate: 'yellow',
low: 'grey'
}[severity];
return `${ansi_styles_1.default.color[color].open}(${severity} severity)${ansi_styles_1.default.color[color].close}`;
}
function renderScannedDependency(change) {
const changeType = change.change_type;
if (changeType !== 'added' && changeType !== 'removed') {
throw new Error(`Unexpected change type: ${changeType}`);
}
const color = {
added: 'green',
removed: 'red'
}[changeType];
const icon = {
added: '+',
removed: '-'
}[changeType];
return `${ansi_styles_1.default.color[color].open}${icon} ${change.manifest}@${change.version}${ansi_styles_1.default.color[color].close}`;
}
function printScannedDependencies(changes) {
core.group('Dependency Changes', () => __awaiter(this, void 0, void 0, function* () {
const dependencies = (0, utils_1.groupDependenciesByManifest)(changes);
for (const manifestName of dependencies.keys()) {
const manifestChanges = dependencies.get(manifestName) || [];
core.info(`File: ${ansi_styles_1.default.bold.open}${manifestName}${ansi_styles_1.default.bold.close}`);
for (const change of manifestChanges) {
core.info(`${renderScannedDependency(change)}`);
}
}
}));
}
run();
@@ -371,6 +410,7 @@ exports.ConfigurationOptionsSchema = z
fail_on_scopes: z.array(z.enum(exports.SCOPES)).default(['runtime']),
allow_licenses: z.array(z.string()).default([]),
deny_licenses: z.array(z.string()).default([]),
allow_ghsas: z.array(z.string()).default([]),
config_file: z.string().optional().default('false'),
base_ref: z.string(),
head_ref: z.string()
@@ -411,8 +451,9 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
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 utils_1 = __nccwpck_require__(918);
function addSummaryToSummary(addedPackages, licenseErrors, unknownLicenses) {
core.summary
.addHeading('Dependency Review')
@@ -421,7 +462,7 @@ function addSummaryToSummary(addedPackages, licenseErrors, unknownLicenses) {
exports.addSummaryToSummary = addSummaryToSummary;
function addChangeVulnerabilitiesToSummary(addedPackages, severity) {
const rows = [];
const manifests = getManifests(addedPackages);
const manifests = (0, utils_1.getManifestsSet)(addedPackages);
core.summary
.addHeading('Vulnerabilities')
.addQuote(`Vulnerabilites were filtered by mininum severity <strong>${severity}</strong>.`);
@@ -438,16 +479,16 @@ function addChangeVulnerabilitiesToSummary(addedPackages, severity) {
previous_version === change.version;
if (!sameAsPrevious) {
rows.push([
renderUrl(change.source_repository_url, change.name),
(0, utils_1.renderUrl)(change.source_repository_url, change.name),
change.version,
renderUrl(vuln.advisory_url, vuln.advisory_summary),
(0, utils_1.renderUrl)(vuln.advisory_url, vuln.advisory_summary),
vuln.severity
]);
}
else {
rows.push([
{ data: '', colspan: '2' },
renderUrl(vuln.advisory_url, vuln.advisory_summary),
(0, utils_1.renderUrl)(vuln.advisory_url, vuln.advisory_summary),
vuln.severity
]);
}
@@ -481,13 +522,13 @@ function addLicensesToSummary(licenseErrors, unknownLicenses, config) {
}
if (licenseErrors.length > 0) {
const rows = [];
const manifests = getManifests(licenseErrors);
const manifests = (0, utils_1.getManifestsSet)(licenseErrors);
core.summary.addHeading('Incompatible Licenses', 3).addSeparator();
for (const manifest of manifests) {
core.summary.addHeading(`<em>${manifest}</em>`, 4);
for (const change of licenseErrors.filter(pkg => pkg.manifest === manifest)) {
rows.push([
renderUrl(change.source_repository_url, change.name),
(0, utils_1.renderUrl)(change.source_repository_url, change.name),
change.version,
change.license || ''
]);
@@ -501,14 +542,14 @@ function addLicensesToSummary(licenseErrors, unknownLicenses, config) {
core.debug(`found ${unknownLicenses.length} unknown licenses`);
if (unknownLicenses.length > 0) {
const rows = [];
const manifests = getManifests(unknownLicenses);
const manifests = (0, utils_1.getManifestsSet)(unknownLicenses);
core.debug(`found ${manifests.entries.length} manifests for unknown licenses`);
core.summary.addHeading('Unknown Licenses', 3).addSeparator();
for (const manifest of manifests) {
core.summary.addHeading(`<em>${manifest}</em>`, 4);
for (const change of unknownLicenses.filter(pkg => pkg.manifest === manifest)) {
rows.push([
renderUrl(change.source_repository_url, change.name),
(0, utils_1.renderUrl)(change.source_repository_url, change.name),
change.version
]);
}
@@ -517,9 +558,49 @@ function addLicensesToSummary(licenseErrors, unknownLicenses, config) {
}
}
exports.addLicensesToSummary = addLicensesToSummary;
function getManifests(changes) {
function addScannedDependencies(changes) {
const dependencies = (0, utils_1.groupDependenciesByManifest)(changes);
const manifests = dependencies.keys();
const summary = core.summary
.addHeading('Scanned Dependencies')
.addRaw(`We scanned ${dependencies.size} manifest files:`);
for (const manifest of manifests) {
const deps = dependencies.get(manifest);
if (deps) {
const dependencyNames = deps.map(dependency => `<li>${dependency.name}@${dependency.version}</li>`);
summary.addRaw(`<h3>${manifest}</h3><ul>${dependencyNames.join('')}</ul>`);
}
}
}
exports.addScannedDependencies = addScannedDependencies;
/***/ }),
/***/ 918:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.renderUrl = exports.getManifestsSet = exports.groupDependenciesByManifest = void 0;
function groupDependenciesByManifest(changes) {
var _a;
const dependencies = new Map();
for (const change of changes) {
const manifestName = change.manifest;
if (dependencies.get(manifestName) === undefined) {
dependencies.set(manifestName, []);
}
(_a = dependencies.get(manifestName)) === null || _a === void 0 ? void 0 : _a.push(change);
}
return dependencies;
}
exports.groupDependenciesByManifest = groupDependenciesByManifest;
function getManifestsSet(changes) {
return new Set(changes.flatMap(c => c.manifest));
}
exports.getManifestsSet = getManifestsSet;
function renderUrl(url, text) {
if (url) {
return `<a href="${url}">${text}</a>`;
@@ -528,6 +609,7 @@ function renderUrl(url, text) {
return text;
}
}
exports.renderUrl = renderUrl;
/***/ }),
@@ -1606,8 +1688,9 @@ exports.context = new Context.Context();
* @param token the repo PAT or GITHUB_TOKEN
* @param options other options to set
*/
function getOctokit(token, options) {
return new utils_1.GitHub(utils_1.getOctokitOptions(token, options));
function getOctokit(token, options, ...additionalPlugins) {
const GitHubWithPlugins = utils_1.GitHub.plugin(...additionalPlugins);
return new GitHubWithPlugins(utils_1.getOctokitOptions(token, options));
}
exports.getOctokit = getOctokit;
//# sourceMappingURL=github.js.map
@@ -14971,18 +15054,20 @@ function readInlineConfig() {
.array(z.enum(schemas_1.SCOPES))
.default(['runtime'])
.parse(parseList(getOptionalInput('fail-on-scopes')));
const allow_licenses = getOptionalInput('allow-licenses');
const deny_licenses = getOptionalInput('deny-licenses');
const allow_licenses = parseList(getOptionalInput('allow-licenses'));
const deny_licenses = parseList(getOptionalInput('deny-licenses'));
if (allow_licenses !== undefined && deny_licenses !== undefined) {
throw new Error("Can't specify both allow_licenses and deny_licenses");
}
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'));
const base_ref = getOptionalInput('base-ref');
const head_ref = getOptionalInput('head-ref');
return {
fail_on_severity,
fail_on_scopes,
allow_licenses: parseList(allow_licenses),
deny_licenses: parseList(deny_licenses),
allow_licenses,
deny_licenses,
allow_ghsas,
base_ref,
head_ref
};
@@ -15018,7 +15103,7 @@ exports.readConfigFile = readConfigFile;
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.filterChangesByScopes = exports.filterChangesBySeverity = void 0;
exports.filterOutAllowedAdvisories = exports.filterChangesByScopes = exports.filterChangesBySeverity = void 0;
const schemas_1 = __nccwpck_require__(1129);
function filterChangesBySeverity(severity, changes) {
const severityIdx = schemas_1.SEVERITIES.indexOf(severity);
@@ -15043,6 +15128,9 @@ function filterChangesBySeverity(severity, changes) {
}
exports.filterChangesBySeverity = filterChangesBySeverity;
function filterChangesByScopes(scopes, changes) {
if (scopes === undefined) {
return [];
}
const filteredChanges = changes.filter(change => {
// if there is no scope on the change (Enterprise Server API for now), we will assume it is a runtime scope
const scope = change.scope || 'runtime';
@@ -15051,6 +15139,30 @@ function filterChangesByScopes(scopes, changes) {
return filteredChanges;
}
exports.filterChangesByScopes = filterChangesByScopes;
function filterOutAllowedAdvisories(ghsas, changes) {
if (ghsas === undefined) {
return [];
}
const filteredChanges = changes.filter(change => {
const noAdvisories = change.vulnerabilities === undefined ||
change.vulnerabilities.length === 0;
if (noAdvisories) {
return true;
}
let allAllowedAdvisories = true;
// if there's at least one advisory that is not allowlisted, we will keep the change
for (const vulnerability of change.vulnerabilities) {
if (!ghsas.includes(vulnerability.advisory_ghsa_id)) {
allAllowedAdvisories = false;
}
if (!allAllowedAdvisories) {
return true;
}
}
});
return filteredChanges;
}
exports.filterOutAllowedAdvisories = filterOutAllowedAdvisories;
/***/ }),
@@ -15120,6 +15232,7 @@ exports.ConfigurationOptionsSchema = z
fail_on_scopes: z.array(z.enum(exports.SCOPES)).default(['runtime']),
allow_licenses: z.array(z.string()).default([]),
deny_licenses: z.array(z.string()).default([]),
allow_ghsas: z.array(z.string()).default([]),
config_file: z.string().optional().default('false'),
base_ref: z.string(),
head_ref: z.string()
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+31 -31
View File
@@ -1,16 +1,16 @@
{
"name": "dependency-review-action",
"version": "2.3.0",
"version": "2.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "dependency-review-action",
"version": "2.3.0",
"version": "2.4.0",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/github": "^5.0.3",
"@actions/github": "^5.1.0",
"@octokit/plugin-retry": "^3.0.9",
"@octokit/request-error": "^3.0.1",
"ansi-styles": "^6.1.1",
@@ -20,12 +20,12 @@
"zod": "^3.19.1"
},
"devDependencies": {
"@types/node": "^16.11.59",
"@types/node": "^16.11.60",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"@vercel/ncc": "^0.34.0",
"esbuild-register": "^3.3.3",
"eslint": "^8.23.1",
"eslint": "^8.24.0",
"eslint-plugin-github": "^4.3.7",
"eslint-plugin-jest": "^27.0.4",
"jest": "^27.5.1",
@@ -46,9 +46,9 @@
}
},
"node_modules/@actions/github": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.3.tgz",
"integrity": "sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.0.tgz",
"integrity": "sha512-tuI80F7JQIhg77ZTTgUAPpVD7ZnP9oHSPN8xw7LOwtA4vEMbAjWJNbmLBfV7xua7r016GyjzWLuec5cs8f/a8A==",
"dependencies": {
"@actions/http-client": "^2.0.1",
"@octokit/core": "^3.6.0",
@@ -705,9 +705,9 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz",
"integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==",
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz",
"integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
@@ -1430,9 +1430,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "16.11.59",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.59.tgz",
"integrity": "sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw==",
"version": "16.11.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.60.tgz",
"integrity": "sha512-kYIYa1D1L+HDv5M5RXQeEu1o0FKA6yedZIoyugm/MBPROkLpX4L7HRxMrPVyo8bnvjpW/wDlqFNGzXNMb7AdRw==",
"dev": true
},
"node_modules/@types/prettier": {
@@ -2776,13 +2776,13 @@
}
},
"node_modules/eslint": {
"version": "8.23.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz",
"integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz",
"integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==",
"dev": true,
"dependencies": {
"@eslint/eslintrc": "^1.3.2",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/config-array": "^0.10.5",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"@humanwhocodes/module-importer": "^1.0.1",
"ajv": "^6.10.0",
@@ -7201,9 +7201,9 @@
}
},
"@actions/github": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.3.tgz",
"integrity": "sha512-myjA/pdLQfhUGLtRZC/J4L1RXOG4o6aYdiEq+zr5wVVKljzbFld+xv10k1FX6IkIJtNxbAq44BdwSNpQ015P0A==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@actions/github/-/github-5.1.0.tgz",
"integrity": "sha512-tuI80F7JQIhg77ZTTgUAPpVD7ZnP9oHSPN8xw7LOwtA4vEMbAjWJNbmLBfV7xua7r016GyjzWLuec5cs8f/a8A==",
"requires": {
"@actions/http-client": "^2.0.1",
"@octokit/core": "^3.6.0",
@@ -7705,9 +7705,9 @@
}
},
"@humanwhocodes/config-array": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz",
"integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==",
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz",
"integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==",
"dev": true,
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
@@ -8312,9 +8312,9 @@
"dev": true
},
"@types/node": {
"version": "16.11.59",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.59.tgz",
"integrity": "sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw==",
"version": "16.11.60",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.60.tgz",
"integrity": "sha512-kYIYa1D1L+HDv5M5RXQeEu1o0FKA6yedZIoyugm/MBPROkLpX4L7HRxMrPVyo8bnvjpW/wDlqFNGzXNMb7AdRw==",
"dev": true
},
"@types/prettier": {
@@ -9287,13 +9287,13 @@
}
},
"eslint": {
"version": "8.23.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz",
"integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==",
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz",
"integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==",
"dev": true,
"requires": {
"@eslint/eslintrc": "^1.3.2",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/config-array": "^0.10.5",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"@humanwhocodes/module-importer": "^1.0.1",
"ajv": "^6.10.0",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "dependency-review-action",
"version": "2.3.0",
"version": "2.4.0",
"private": true,
"description": "A GitHub Action for Dependency Review",
"main": "lib/main.js",
@@ -26,7 +26,7 @@
"license": "MIT",
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/github": "^5.0.3",
"@actions/github": "^5.1.0",
"@octokit/plugin-retry": "^3.0.9",
"@octokit/request-error": "^3.0.1",
"ansi-styles": "^6.1.1",
@@ -36,12 +36,12 @@
"zod": "^3.19.1"
},
"devDependencies": {
"@types/node": "^16.11.59",
"@types/node": "^16.11.60",
"@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0",
"@vercel/ncc": "^0.34.0",
"esbuild-register": "^3.3.3",
"eslint": "^8.23.1",
"eslint": "^8.24.0",
"eslint-plugin-github": "^4.3.7",
"eslint-plugin-jest": "^27.0.4",
"jest": "^27.5.1",
+7 -4
View File
@@ -47,21 +47,24 @@ export function readInlineConfig(): ConfigurationOptions {
.default(['runtime'])
.parse(parseList(getOptionalInput('fail-on-scopes')))
const allow_licenses = getOptionalInput('allow-licenses')
const deny_licenses = getOptionalInput('deny-licenses')
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
if (allow_licenses !== undefined && deny_licenses !== undefined) {
throw new Error("Can't specify both allow_licenses and deny_licenses")
}
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
const base_ref = getOptionalInput('base-ref')
const head_ref = getOptionalInput('head-ref')
return {
fail_on_severity,
fail_on_scopes,
allow_licenses: parseList(allow_licenses),
deny_licenses: parseList(deny_licenses),
allow_licenses,
deny_licenses,
allow_ghsas,
base_ref,
head_ref
}
+37 -1
View File
@@ -35,9 +35,13 @@ export function filterChangesBySeverity(
}
export function filterChangesByScopes(
scopes: Scope[],
scopes: Scope[] | undefined,
changes: Changes
): Changes {
if (scopes === undefined) {
return []
}
const filteredChanges = changes.filter(change => {
// if there is no scope on the change (Enterprise Server API for now), we will assume it is a runtime scope
const scope = change.scope || 'runtime'
@@ -46,3 +50,35 @@ export function filterChangesByScopes(
return filteredChanges
}
export function filterOutAllowedAdvisories(
ghsas: string[] | undefined,
changes: Changes
): Changes {
if (ghsas === undefined) {
return []
}
const filteredChanges = changes.filter(change => {
const noAdvisories =
change.vulnerabilities === undefined ||
change.vulnerabilities.length === 0
if (noAdvisories) {
return true
}
let allAllowedAdvisories = true
// if there's at least one advisory that is not allowlisted, we will keep the change
for (const vulnerability of change.vulnerabilities) {
if (!ghsas.includes(vulnerability.advisory_ghsa_id)) {
allAllowedAdvisories = false
}
if (!allAllowedAdvisories) {
return true
}
}
})
return filteredChanges
}
+112 -53
View File
@@ -3,13 +3,19 @@ import * as dependencyGraph from './dependency-graph'
import * as github from '@actions/github'
import styles from 'ansi-styles'
import {RequestError} from '@octokit/request-error'
import {Change, Severity, Scope} from './schemas'
import {Change, Severity, Changes} from './schemas'
import {readConfig} from '../src/config'
import {filterChangesBySeverity, filterChangesByScopes} from '../src/filter'
import {
filterChangesBySeverity,
filterChangesByScopes,
filterOutAllowedAdvisories
} from '../src/filter'
import {getDeniedLicenseChanges} from './licenses'
import * as summary from './summary'
import {getRefs} from './git-refs'
import {groupDependenciesByManifest} from './utils'
async function run(): Promise<void> {
try {
const config = readConfig()
@@ -22,21 +28,16 @@ async function run(): Promise<void> {
headRef: refs.head
})
const minSeverity = config.fail_on_severity
let failed = false
const licenses = {
allow: config.allow_licenses,
deny: config.deny_licenses
}
const scopes = config.fail_on_scopes
const scopedChanges = filterChangesByScopes(scopes as Scope[], changes)
const minSeverity = config.fail_on_severity as Severity
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
const filteredChanges = filterOutAllowedAdvisories(
config.allow_ghsas,
scopedChanges
)
const addedChanges = filterChangesBySeverity(
minSeverity as Severity,
scopedChanges
minSeverity,
filteredChanges
).filter(
change =>
change.change_type === 'added' &&
@@ -45,37 +46,21 @@ async function run(): Promise<void> {
)
const [licenseErrors, unknownLicenses] = getDeniedLicenseChanges(
scopedChanges,
licenses
filteredChanges,
{
allow: config.allow_licenses,
deny: config.deny_licenses
}
)
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses)
if (addedChanges.length > 0) {
for (const change of addedChanges) {
printChangeVulnerabilities(change)
}
failed = true
}
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity || '')
if (licenseErrors.length > 0) {
printLicensesError(licenseErrors)
core.setFailed('Dependency review detected incompatible licenses.')
}
printNullLicenses(unknownLicenses)
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config)
summary.addScannedDependencies(changes)
if (failed) {
core.setFailed('Dependency review detected vulnerable packages.')
} else {
core.info(
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`
)
}
printVulnerabilitiesBlock(addedChanges, minSeverity)
printLicensesBlock(licenseErrors, unknownLicenses)
printScannedDependencies(changes)
} catch (error) {
if (error instanceof RequestError && error.status === 404) {
core.setFailed(
@@ -97,6 +82,29 @@ async function run(): Promise<void> {
}
}
function printVulnerabilitiesBlock(
addedChanges: Change[],
minSeverity: Severity
): void {
let failed = false
core.group('Vulnerabilities', async () => {
if (addedChanges.length > 0) {
for (const change of addedChanges) {
printChangeVulnerabilities(change)
}
failed = true
}
if (failed) {
core.setFailed('Dependency review detected vulnerable packages.')
} else {
core.info(
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`
)
}
})
}
function printChangeVulnerabilities(change: Change): void {
for (const vuln of change.vulnerabilities) {
core.info(
@@ -110,18 +118,17 @@ function printChangeVulnerabilities(change: Change): void {
}
}
function renderSeverity(
severity: 'critical' | 'high' | 'moderate' | 'low'
): string {
const color = (
{
critical: 'red',
high: 'red',
moderate: 'yellow',
low: 'grey'
} as const
)[severity]
return `${styles.color[color].open}(${severity} severity)${styles.color[color].close}`
function printLicensesBlock(
licenseErrors: Change[],
unknownLicenses: Change[]
): void {
core.group('Licenses', async () => {
if (licenseErrors.length > 0) {
printLicensesError(licenseErrors)
core.setFailed('Dependency review detected incompatible licenses.')
}
printNullLicenses(unknownLicenses)
})
}
function printLicensesError(changes: Change[]): void {
@@ -150,4 +157,56 @@ function printNullLicenses(changes: Change[]): void {
}
}
function renderSeverity(
severity: 'critical' | 'high' | 'moderate' | 'low'
): string {
const color = (
{
critical: 'red',
high: 'red',
moderate: 'yellow',
low: 'grey'
} as const
)[severity]
return `${styles.color[color].open}(${severity} severity)${styles.color[color].close}`
}
function renderScannedDependency(change: Change): string {
const changeType: string = change.change_type
if (changeType !== 'added' && changeType !== 'removed') {
throw new Error(`Unexpected change type: ${changeType}`)
}
const color = (
{
added: 'green',
removed: 'red'
} as const
)[changeType]
const icon = (
{
added: '+',
removed: '-'
} as const
)[changeType]
return `${styles.color[color].open}${icon} ${change.manifest}@${change.version}${styles.color[color].close}`
}
function printScannedDependencies(changes: Changes): void {
core.group('Dependency Changes', async () => {
const dependencies = groupDependenciesByManifest(changes)
for (const manifestName of dependencies.keys()) {
const manifestChanges = dependencies.get(manifestName) || []
core.info(`File: ${styles.bold.open}${manifestName}${styles.bold.close}`)
for (const change of manifestChanges) {
core.info(`${renderScannedDependency(change)}`)
}
}
})
}
run()
+1
View File
@@ -40,6 +40,7 @@ export const ConfigurationOptionsSchema = z
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
allow_licenses: z.array(z.string()).default([]),
deny_licenses: z.array(z.string()).default([]),
allow_ghsas: z.array(z.string()).default([]),
config_file: z.string().optional().default('false'),
base_ref: z.string(),
head_ref: z.string()
+19 -11
View File
@@ -1,6 +1,7 @@
import * as core from '@actions/core'
import {ConfigurationOptions, Change, Changes} from './schemas'
import {SummaryTableRow} from '@actions/core/lib/summary'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
export function addSummaryToSummary(
addedPackages: Changes,
@@ -20,7 +21,7 @@ export function addChangeVulnerabilitiesToSummary(
): void {
const rows: SummaryTableRow[] = []
const manifests = getManifests(addedPackages)
const manifests = getManifestsSet(addedPackages)
core.summary
.addHeading('Vulnerabilities')
@@ -99,7 +100,7 @@ export function addLicensesToSummary(
if (licenseErrors.length > 0) {
const rows: SummaryTableRow[] = []
const manifests = getManifests(licenseErrors)
const manifests = getManifestsSet(licenseErrors)
core.summary.addHeading('Incompatible Licenses', 3).addSeparator()
@@ -125,7 +126,7 @@ export function addLicensesToSummary(
if (unknownLicenses.length > 0) {
const rows: SummaryTableRow[] = []
const manifests = getManifests(unknownLicenses)
const manifests = getManifestsSet(unknownLicenses)
core.debug(
`found ${manifests.entries.length} manifests for unknown licenses`
@@ -150,14 +151,21 @@ export function addLicensesToSummary(
}
}
function getManifests(changes: Changes): Set<string> {
return new Set(changes.flatMap(c => c.manifest))
}
export function addScannedDependencies(changes: Changes): void {
const dependencies = groupDependenciesByManifest(changes)
const manifests = dependencies.keys()
function renderUrl(url: string | null, text: string): string {
if (url) {
return `<a href="${url}">${text}</a>`
} else {
return text
const summary = core.summary
.addHeading('Scanned Dependencies')
.addRaw(`We scanned ${dependencies.size} manifest files:`)
for (const manifest of manifests) {
const deps = dependencies.get(manifest)
if (deps) {
const dependencyNames = deps.map(
dependency => `<li>${dependency.name}@${dependency.version}</li>`
)
summary.addRaw(`<h3>${manifest}</h3><ul>${dependencyNames.join('')}</ul>`)
}
}
}
+30
View File
@@ -0,0 +1,30 @@
import {Changes} from './schemas'
export function groupDependenciesByManifest(
changes: Changes
): Map<string, Changes> {
const dependencies: Map<string, Changes> = new Map()
for (const change of changes) {
const manifestName = change.manifest
if (dependencies.get(manifestName) === undefined) {
dependencies.set(manifestName, [])
}
dependencies.get(manifestName)?.push(change)
}
return dependencies
}
export function getManifestsSet(changes: Changes): Set<string> {
return new Set(changes.flatMap(c => c.manifest))
}
export function renderUrl(url: string | null, text: string): string {
if (url) {
return `<a href="${url}">${text}</a>`
} else {
return text
}
}