Merge pull request #709 from jhutchings1/scorecard
Add support for calculating OpenSSF Scorecards
This commit is contained in:
@@ -83,6 +83,8 @@ Configure this action by either inlining these options in your workflow file, or
|
||||
| `retry-on-snapshot-warnings`\* | Enable or disable retrying the action every 10 seconds while waiting for dependency submission actions to complete. | `true`, `false` | `false` |
|
||||
| `retry-on-snapshot-warnings-timeout`\* | Maximum amount of time (in seconds) to retry the action while waiting for dependency submission actions to complete. | Any positive integer | 120 |
|
||||
| `warn-only`+ | When set to `true`, the action will log all vulnerabilities as warnings regardless of the severity, and the action will complete with a `success` status. This overrides the `fail-on-severity` option. | `true`, `false` | `false` |
|
||||
| `show-openssf-scorecard-levels` | When set to `true`, the action will output information about all the known OpenSSF Scorecard scores for the dependencies changed in this pull request. | `true`, `false` | `true` |
|
||||
| `warn-on-openssf-scorecard-level` | When `show-openssf-scorecard-levels` is set to `true`, this option lets you configure the threshold for when a score is considered too low and gets a :warning: warning in the CI. | Any positive integer | 3 |
|
||||
|
||||
\*not supported for use with GitHub Enterprise Server
|
||||
|
||||
|
||||
40
__tests__/scorecard.test.ts
Normal file
40
__tests__/scorecard.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
import {getScorecardLevels, getProjectUrl} from '../src/scorecard'
|
||||
|
||||
const npmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
name: 'type-is',
|
||||
version: '1.6.18',
|
||||
package_url: 'pkg:npm/type-is@1.6.18',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/jshttp/type-is',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: [
|
||||
{
|
||||
severity: 'critical',
|
||||
advisory_ghsa_id: 'first-random_string',
|
||||
advisory_summary: 'very dangerous',
|
||||
advisory_url: 'github.com/future-funk'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
test('Get scorecard from API', async () => {
|
||||
const changes: Changes = [npmChange]
|
||||
const scorecard = await getScorecardLevels(changes)
|
||||
expect(scorecard).not.toBeNull()
|
||||
expect(scorecard.dependencies).toHaveLength(1)
|
||||
expect(scorecard.dependencies[0].scorecard?.score).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Get project URL from deps.dev API', async () => {
|
||||
const result = await getProjectUrl(
|
||||
npmChange.ecosystem,
|
||||
npmChange.name,
|
||||
npmChange.version
|
||||
)
|
||||
expect(result).not.toBeNull()
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import {expect, jest, test} from '@jest/globals'
|
||||
import {Changes, ConfigurationOptions} from '../src/schemas'
|
||||
import {Changes, ConfigurationOptions, Scorecard} from '../src/schemas'
|
||||
import * as summary from '../src/summary'
|
||||
import * as core from '@actions/core'
|
||||
import {createTestChange} from './fixtures/create-test-change'
|
||||
@@ -16,6 +16,9 @@ const emptyInvalidLicenseChanges = {
|
||||
unresolved: [],
|
||||
unlicensed: []
|
||||
}
|
||||
const emptyScorecard: Scorecard = {
|
||||
dependencies: []
|
||||
}
|
||||
const defaultConfig: ConfigurationOptions = {
|
||||
vulnerability_check: true,
|
||||
license_check: true,
|
||||
@@ -29,7 +32,9 @@ const defaultConfig: ConfigurationOptions = {
|
||||
comment_summary_in_pr: true,
|
||||
retry_on_snapshot_warnings: false,
|
||||
retry_on_snapshot_warnings_timeout: 120,
|
||||
warn_only: false
|
||||
warn_only: false,
|
||||
warn_on_openssf_scorecard_level: 3,
|
||||
show_openssf_scorecard: false
|
||||
}
|
||||
|
||||
const changesWithEmptyManifests: Changes = [
|
||||
@@ -71,11 +76,32 @@ const changesWithEmptyManifests: Changes = [
|
||||
}
|
||||
]
|
||||
|
||||
const scorecard: Scorecard = {
|
||||
dependencies: [
|
||||
{
|
||||
change: {
|
||||
change_type: 'added',
|
||||
manifest: '',
|
||||
ecosystem: 'unknown',
|
||||
name: 'castore',
|
||||
version: '0.1.17',
|
||||
package_url: 'pkg:hex/castore@0.1.17',
|
||||
license: null,
|
||||
source_repository_url: null,
|
||||
scope: 'runtime',
|
||||
vulnerabilities: []
|
||||
},
|
||||
scorecard: null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
test('prints headline as h1', () => {
|
||||
summary.addSummaryToSummary(
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
scorecard,
|
||||
defaultConfig
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
@@ -88,6 +114,7 @@ test('only includes "No vulnerabilities or license issues found"-message if both
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
@@ -101,6 +128,7 @@ test('only includes "No vulnerabilities found"-message if "license_check" is set
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
config
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
@@ -114,6 +142,7 @@ test('only includes "No license issues found"-message if "vulnerability_check" i
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
config
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
@@ -126,6 +155,7 @@ test('groups dependencies with empty manifest paths together', () => {
|
||||
changesWithEmptyManifests,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
summary.addScannedDependencies(changesWithEmptyManifests)
|
||||
@@ -143,6 +173,7 @@ test('does not include status section if nothing was found', () => {
|
||||
emptyChanges,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
const text = core.summary.stringify()
|
||||
@@ -165,6 +196,7 @@ test('includes count and status icons for all findings', () => {
|
||||
vulnerabilities,
|
||||
licenseIssues,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
@@ -184,6 +216,7 @@ test('uses checkmarks for license issues if only vulnerabilities were found', ()
|
||||
vulnerabilities,
|
||||
emptyInvalidLicenseChanges,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
@@ -207,6 +240,7 @@ test('uses checkmarks for vulnerabilities if only license issues were found', ()
|
||||
emptyChanges,
|
||||
licenseIssues,
|
||||
emptyChanges,
|
||||
emptyScorecard,
|
||||
defaultConfig
|
||||
)
|
||||
|
||||
|
||||
@@ -65,6 +65,14 @@ inputs:
|
||||
description: When set to `true` this action will always complete with success, overriding the `fail-on-severity` parameter.
|
||||
required: false
|
||||
default: false
|
||||
show-openssf-scorecard:
|
||||
description: Show a summary of the OpenSSF Scorecard scores.
|
||||
required: false
|
||||
default: true
|
||||
warn-on-openssf-scorecard-level:
|
||||
description: Numeric threshold for the OpenSSF Scorecard score. If the score is below this threshold, the action will warn you.
|
||||
required: false
|
||||
default: 3
|
||||
outputs:
|
||||
comment-content:
|
||||
description: Prepared dependency report comment
|
||||
|
||||
314
dist/index.js
generated
vendored
314
dist/index.js
generated
vendored
@@ -565,6 +565,7 @@ const request_error_1 = __nccwpck_require__(537);
|
||||
const config_1 = __nccwpck_require__(6373);
|
||||
const filter_1 = __nccwpck_require__(134);
|
||||
const licenses_1 = __nccwpck_require__(3247);
|
||||
const scorecard_1 = __nccwpck_require__(307);
|
||||
const summary = __importStar(__nccwpck_require__(8608));
|
||||
const git_refs_1 = __nccwpck_require__(1086);
|
||||
const utils_1 = __nccwpck_require__(918);
|
||||
@@ -635,7 +636,8 @@ function run() {
|
||||
core.debug(`Filtered Changes: ${JSON.stringify(filteredChanges)}`);
|
||||
core.debug(`Config Deny Packages: ${JSON.stringify(config)}`);
|
||||
const deniedChanges = yield (0, deny_1.getDeniedChanges)(filteredChanges, config.deny_packages, config.deny_groups);
|
||||
summary.addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, config);
|
||||
const scorecard = yield (0, scorecard_1.getScorecardLevels)(filteredChanges);
|
||||
summary.addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config);
|
||||
if (snapshot_warnings) {
|
||||
summary.addSnapshotWarnings(config, snapshot_warnings);
|
||||
}
|
||||
@@ -651,6 +653,11 @@ function run() {
|
||||
summary.addDeniedToSummary(deniedChanges);
|
||||
printDeniedDependencies(deniedChanges, config);
|
||||
}
|
||||
if (config.show_openssf_scorecard) {
|
||||
summary.addScorecardToSummary(scorecard, config);
|
||||
printScorecardBlock(scorecard, config);
|
||||
createScorecardWarnings(scorecard, config);
|
||||
}
|
||||
summary.addScannedDependencies(changes);
|
||||
printScannedDependencies(changes);
|
||||
yield (0, comment_pr_1.commentPr)(core.summary, config);
|
||||
@@ -740,6 +747,20 @@ function printNullLicenses(changes) {
|
||||
core.info(`${ansi_styles_1.default.bold.open}${change.manifest} » ${change.name}@${change.version}${ansi_styles_1.default.bold.close}`);
|
||||
}
|
||||
}
|
||||
function printScorecardBlock(scorecard, config) {
|
||||
core.group('Scorecard', () => __awaiter(this, void 0, void 0, function* () {
|
||||
var _a, _b, _c, _d;
|
||||
if (scorecard) {
|
||||
for (const dependency of scorecard.dependencies) {
|
||||
if (((_a = dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score) &&
|
||||
((_b = dependency.scorecard) === null || _b === void 0 ? void 0 : _b.score) < config.warn_on_openssf_scorecard_level) {
|
||||
core.info(`${ansi_styles_1.default.color.red.open}${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${(_c = dependency === null || dependency === void 0 ? void 0 : dependency.scorecard) === null || _c === void 0 ? void 0 : _c.score}${ansi_styles_1.default.red.close}`);
|
||||
}
|
||||
core.info(`${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${(_d = dependency === null || dependency === void 0 ? void 0 : dependency.scorecard) === null || _d === void 0 ? void 0 : _d.score}`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
function renderSeverity(severity) {
|
||||
const color = {
|
||||
critical: 'red',
|
||||
@@ -787,6 +808,20 @@ function printDeniedDependencies(changes, config) {
|
||||
}
|
||||
}));
|
||||
}
|
||||
function createScorecardWarnings(scorecards, config) {
|
||||
var _a, _b, _c;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// Iterate through the list of scorecards, and if the score is less than the threshold, send a warning
|
||||
for (const dependency of scorecards.dependencies) {
|
||||
if (((_a = dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score) &&
|
||||
((_b = dependency.scorecard) === null || _b === void 0 ? void 0 : _b.score) < config.warn_on_openssf_scorecard_level) {
|
||||
core.warning(`${dependency.change.ecosystem}/${dependency.change.name} has an OpenSSF Scorecard of ${(_c = dependency.scorecard) === null || _c === void 0 ? void 0 : _c.score}, which is less than this repository's threshold of ${config.warn_on_openssf_scorecard_level}.`, {
|
||||
title: 'OpenSSF Scorecard Warning'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
run();
|
||||
|
||||
|
||||
@@ -821,7 +856,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
|
||||
exports.ScorecardSchema = exports.ScorecardApiSchema = exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
|
||||
const z = __importStar(__nccwpck_require__(3301));
|
||||
exports.SEVERITIES = ['critical', 'high', 'moderate', 'low'];
|
||||
exports.SCOPES = ['unknown', 'runtime', 'development'];
|
||||
@@ -868,6 +903,8 @@ exports.ConfigurationOptionsSchema = z
|
||||
head_ref: z.string().optional(),
|
||||
retry_on_snapshot_warnings: z.boolean().default(false),
|
||||
retry_on_snapshot_warnings_timeout: z.number().default(120),
|
||||
show_openssf_scorecard: z.boolean().optional().default(true),
|
||||
warn_on_openssf_scorecard_level: z.number().default(3),
|
||||
comment_summary_in_pr: z
|
||||
.union([
|
||||
z.preprocess(val => (val === 'true' ? true : val === 'false' ? false : val), z.boolean()),
|
||||
@@ -911,6 +948,152 @@ exports.ComparisonResponseSchema = z.object({
|
||||
changes: z.array(exports.ChangeSchema),
|
||||
snapshot_warnings: z.string()
|
||||
});
|
||||
exports.ScorecardApiSchema = z.object({
|
||||
date: z.string(),
|
||||
repo: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
commit: z.string()
|
||||
})
|
||||
.nullish(),
|
||||
scorecard: z
|
||||
.object({
|
||||
version: z.string(),
|
||||
commit: z.string()
|
||||
})
|
||||
.nullish(),
|
||||
checks: z
|
||||
.array(z.object({
|
||||
name: z.string(),
|
||||
documentation: z.object({
|
||||
shortDescription: z.string(),
|
||||
url: z.string()
|
||||
}),
|
||||
score: z.string(),
|
||||
reason: z.string(),
|
||||
details: z.array(z.string())
|
||||
}))
|
||||
.nullish(),
|
||||
score: z.number().nullish()
|
||||
});
|
||||
exports.ScorecardSchema = z.object({
|
||||
dependencies: z.array(z.object({
|
||||
change: exports.ChangeSchema,
|
||||
scorecard: exports.ScorecardApiSchema.nullish()
|
||||
}))
|
||||
});
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ 307:
|
||||
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.getProjectUrl = exports.getScorecardLevels = void 0;
|
||||
const core = __importStar(__nccwpck_require__(2186));
|
||||
function getScorecardLevels(changes) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const data = { dependencies: [] };
|
||||
for (const change of changes) {
|
||||
const ecosystem = change.ecosystem;
|
||||
const packageName = change.name;
|
||||
const version = change.version;
|
||||
//Get the project repository
|
||||
let repositoryUrl = change.source_repository_url;
|
||||
//If the repository_url includes the protocol, remove it
|
||||
if (repositoryUrl === null || repositoryUrl === void 0 ? void 0 : repositoryUrl.startsWith('https://')) {
|
||||
repositoryUrl = repositoryUrl.replace('https://', '');
|
||||
}
|
||||
// If GitHub API doesn't have the repository URL, query deps.dev for it.
|
||||
if (repositoryUrl) {
|
||||
// Call the deps.dev API to get the repository URL from there
|
||||
repositoryUrl = yield getProjectUrl(ecosystem, packageName, version);
|
||||
}
|
||||
// Get the scorecard API response from the scorecards API
|
||||
let scorecardApi = null;
|
||||
if (repositoryUrl) {
|
||||
try {
|
||||
scorecardApi = yield getScorecard(repositoryUrl);
|
||||
}
|
||||
catch (error) {
|
||||
core.debug(`Error querying for scorecard: ${error.message}`);
|
||||
}
|
||||
}
|
||||
data.dependencies.push({
|
||||
change,
|
||||
scorecard: scorecardApi
|
||||
});
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
exports.getScorecardLevels = getScorecardLevels;
|
||||
function getScorecard(repositoryUrl) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const apiRoot = 'https://api.securityscorecards.dev/';
|
||||
let scorecardResponse = {};
|
||||
const url = `${apiRoot}/projects/${repositoryUrl}`;
|
||||
const response = yield fetch(url);
|
||||
if (response.ok) {
|
||||
scorecardResponse = yield response.json();
|
||||
}
|
||||
else {
|
||||
core.debug(`Couldn't get scorecard data for ${repositoryUrl}`);
|
||||
}
|
||||
return scorecardResponse;
|
||||
});
|
||||
}
|
||||
function getProjectUrl(ecosystem, packageName, version) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
core.debug(`Getting deps.dev data for ${packageName} ${version}`);
|
||||
const depsDevAPIRoot = 'https://api.deps.dev';
|
||||
const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`;
|
||||
const response = yield fetch(url);
|
||||
if (response.ok) {
|
||||
const data = yield response.json();
|
||||
if (data.relatedProjects.length > 0) {
|
||||
return data.relatedProjects[0].projectKey.id;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}
|
||||
exports.getProjectUrl = getProjectUrl;
|
||||
|
||||
|
||||
/***/ }),
|
||||
@@ -944,7 +1127,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.addDeniedToSummary = exports.addSnapshotWarnings = exports.addScannedDependencies = exports.addLicensesToSummary = exports.addChangeVulnerabilitiesToSummary = exports.addSummaryToSummary = void 0;
|
||||
exports.addDeniedToSummary = exports.addSnapshotWarnings = exports.addScorecardToSummary = exports.addScannedDependencies = exports.addLicensesToSummary = exports.addChangeVulnerabilitiesToSummary = exports.addSummaryToSummary = void 0;
|
||||
const core = __importStar(__nccwpck_require__(2186));
|
||||
const utils_1 = __nccwpck_require__(918);
|
||||
const icons = {
|
||||
@@ -952,19 +1135,24 @@ const icons = {
|
||||
cross: '❌',
|
||||
warning: '⚠️'
|
||||
};
|
||||
function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, config) {
|
||||
function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config) {
|
||||
const scorecardWarnings = countScorecardWarnings(scorecard, config);
|
||||
const licenseIssues = countLicenseIssues(invalidLicenseChanges);
|
||||
core.summary.addHeading('Dependency Review', 1);
|
||||
if (vulnerableChanges.length === 0 &&
|
||||
countLicenseIssues(invalidLicenseChanges) === 0 &&
|
||||
deniedChanges.length === 0) {
|
||||
if (!config.license_check) {
|
||||
core.summary.addRaw(`${icons.check} No vulnerabilities found.`);
|
||||
}
|
||||
else if (!config.vulnerability_check) {
|
||||
core.summary.addRaw(`${icons.check} No license issues found.`);
|
||||
licenseIssues === 0 &&
|
||||
deniedChanges.length === 0 &&
|
||||
scorecardWarnings === 0) {
|
||||
const issueTypes = [
|
||||
config.vulnerability_check ? 'vulnerabilities' : '',
|
||||
config.license_check ? 'license issues' : '',
|
||||
config.show_openssf_scorecard ? 'OpenSSF Scorecard issues' : ''
|
||||
];
|
||||
if (issueTypes.filter(Boolean).length === 0) {
|
||||
core.summary.addRaw(`${icons.check} No issues found.`);
|
||||
}
|
||||
else {
|
||||
core.summary.addRaw(`${icons.check} No vulnerabilities or license issues found.`);
|
||||
core.summary.addRaw(`${icons.check} No ${issueTypes.filter(Boolean).join(' or ')} found.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -987,11 +1175,26 @@ function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedCha
|
||||
? [
|
||||
`${checkOrWarnIcon(deniedChanges.length)} ${deniedChanges.length} package(s) denied.`
|
||||
]
|
||||
: []),
|
||||
...(config.show_openssf_scorecard && scorecardWarnings > 0
|
||||
? [
|
||||
`${checkOrWarnIcon(scorecardWarnings)} ${scorecardWarnings ? scorecardWarnings : 'No'} packages with OpenSSF Scorecard issues.`
|
||||
]
|
||||
: [])
|
||||
])
|
||||
.addRaw('See the Details below.');
|
||||
}
|
||||
exports.addSummaryToSummary = addSummaryToSummary;
|
||||
function countScorecardWarnings(scorecard, config) {
|
||||
return scorecard.dependencies.reduce((total, dependency) => {
|
||||
var _a, _b;
|
||||
return total +
|
||||
(((_a = dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score) &&
|
||||
((_b = dependency.scorecard) === null || _b === void 0 ? void 0 : _b.score) < config.warn_on_openssf_scorecard_level
|
||||
? 1
|
||||
: 0);
|
||||
}, 0);
|
||||
}
|
||||
function addChangeVulnerabilitiesToSummary(vulnerableChanges, severity) {
|
||||
if (vulnerableChanges.length === 0) {
|
||||
return;
|
||||
@@ -1125,6 +1328,49 @@ function snapshotWarningRecommendation(config, warnings) {
|
||||
}
|
||||
return 'Re-running this action after a short time may resolve the issue.';
|
||||
}
|
||||
function addScorecardToSummary(scorecard, config) {
|
||||
var _a, _b, _c, _d, _e, _f, _g, _h;
|
||||
core.summary.addHeading('OpenSSF Scorecard', 2);
|
||||
if (scorecard.dependencies.length > 10) {
|
||||
core.summary.addRaw(`<details><summary>Scorecard details</summary>`, true);
|
||||
}
|
||||
core.summary.addRaw(`<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>`, true);
|
||||
for (const dependency of scorecard.dependencies) {
|
||||
core.debug('Adding scorecard to summary');
|
||||
core.debug(`Overall score ${(_a = dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score}`);
|
||||
// Set the icon based on the overall score value
|
||||
let overallIcon = '';
|
||||
if ((_b = dependency.scorecard) === null || _b === void 0 ? void 0 : _b.score) {
|
||||
overallIcon =
|
||||
((_c = dependency.scorecard) === null || _c === void 0 ? void 0 : _c.score) < config.warn_on_openssf_scorecard_level
|
||||
? ':warning:'
|
||||
: ':green_circle:';
|
||||
}
|
||||
//Add a row for the dependency
|
||||
core.summary.addRaw(`<tr><td>${dependency.change.source_repository_url ? `<a href="https://${dependency.change.source_repository_url}">` : ''} ${dependency.change.ecosystem}/${dependency.change.name} ${dependency.change.source_repository_url ? `</a>` : ''}</td><td>${dependency.change.version}</td>
|
||||
<td>${overallIcon} ${((_d = dependency.scorecard) === null || _d === void 0 ? void 0 : _d.score) === undefined || ((_e = dependency.scorecard) === null || _e === void 0 ? void 0 : _e.score) === null ? 'Unknown' : (_f = dependency.scorecard) === null || _f === void 0 ? void 0 : _f.score}</td>`, false);
|
||||
//Add details table in the last column
|
||||
if (((_g = dependency.scorecard) === null || _g === void 0 ? void 0 : _g.checks) !== undefined) {
|
||||
let detailsTable = '<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>';
|
||||
for (const check of ((_h = dependency.scorecard) === null || _h === void 0 ? void 0 : _h.checks) || []) {
|
||||
const icon = parseFloat(check.score) < config.warn_on_openssf_scorecard_level
|
||||
? ':warning:'
|
||||
: ':green_circle:';
|
||||
detailsTable += `<tr><td>${check.name}</td><td>${icon} ${check.score}</td><td>${check.reason}</td></tr>`;
|
||||
}
|
||||
detailsTable += `</table>`;
|
||||
core.summary.addRaw(`<td><details><summary>Details</summary>${detailsTable}</details></td></tr>`, true);
|
||||
}
|
||||
else {
|
||||
core.summary.addRaw('<td>Unknown</td></tr>', true);
|
||||
}
|
||||
}
|
||||
core.summary.addRaw(`</table>`);
|
||||
if (scorecard.dependencies.length > 10) {
|
||||
core.summary.addRaw(`</details>`);
|
||||
}
|
||||
}
|
||||
exports.addScorecardToSummary = addScorecardToSummary;
|
||||
function addSnapshotWarnings(config, warnings) {
|
||||
core.summary.addHeading('Snapshot Warnings', 2);
|
||||
core.summary.addQuote(`${icons.warning}: ${warnings}`);
|
||||
@@ -49488,6 +49734,8 @@ function readInlineConfig() {
|
||||
const retry_on_snapshot_warnings = getOptionalBoolean('retry-on-snapshot-warnings');
|
||||
const retry_on_snapshot_warnings_timeout = getOptionalNumber('retry-on-snapshot-warnings-timeout');
|
||||
const warn_only = getOptionalBoolean('warn-only');
|
||||
const show_openssf_scorecard = getOptionalBoolean('show-openssf-scorecard');
|
||||
const warn_on_openssf_scorecard_level = getOptionalNumber('warn-on-openssf-scorecard-level');
|
||||
validatePURL(allow_dependencies_licenses);
|
||||
validateLicenses('allow-licenses', allow_licenses);
|
||||
validateLicenses('deny-licenses', deny_licenses);
|
||||
@@ -49507,7 +49755,9 @@ function readInlineConfig() {
|
||||
comment_summary_in_pr,
|
||||
retry_on_snapshot_warnings,
|
||||
retry_on_snapshot_warnings_timeout,
|
||||
warn_only
|
||||
warn_only,
|
||||
show_openssf_scorecard,
|
||||
warn_on_openssf_scorecard_level
|
||||
};
|
||||
return Object.fromEntries(Object.entries(keys).filter(([_, value]) => value !== undefined));
|
||||
}
|
||||
@@ -49758,7 +50008,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
|
||||
exports.ScorecardSchema = exports.ScorecardApiSchema = exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
|
||||
const z = __importStar(__nccwpck_require__(3301));
|
||||
exports.SEVERITIES = ['critical', 'high', 'moderate', 'low'];
|
||||
exports.SCOPES = ['unknown', 'runtime', 'development'];
|
||||
@@ -49805,6 +50055,8 @@ exports.ConfigurationOptionsSchema = z
|
||||
head_ref: z.string().optional(),
|
||||
retry_on_snapshot_warnings: z.boolean().default(false),
|
||||
retry_on_snapshot_warnings_timeout: z.number().default(120),
|
||||
show_openssf_scorecard: z.boolean().optional().default(true),
|
||||
warn_on_openssf_scorecard_level: z.number().default(3),
|
||||
comment_summary_in_pr: z
|
||||
.union([
|
||||
z.preprocess(val => (val === 'true' ? true : val === 'false' ? false : val), z.boolean()),
|
||||
@@ -49848,6 +50100,40 @@ exports.ComparisonResponseSchema = z.object({
|
||||
changes: z.array(exports.ChangeSchema),
|
||||
snapshot_warnings: z.string()
|
||||
});
|
||||
exports.ScorecardApiSchema = z.object({
|
||||
date: z.string(),
|
||||
repo: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
commit: z.string()
|
||||
})
|
||||
.nullish(),
|
||||
scorecard: z
|
||||
.object({
|
||||
version: z.string(),
|
||||
commit: z.string()
|
||||
})
|
||||
.nullish(),
|
||||
checks: z
|
||||
.array(z.object({
|
||||
name: z.string(),
|
||||
documentation: z.object({
|
||||
shortDescription: z.string(),
|
||||
url: z.string()
|
||||
}),
|
||||
score: z.string(),
|
||||
reason: z.string(),
|
||||
details: z.array(z.string())
|
||||
}))
|
||||
.nullish(),
|
||||
score: z.number().nullish()
|
||||
});
|
||||
exports.ScorecardSchema = z.object({
|
||||
dependencies: z.array(z.object({
|
||||
change: exports.ChangeSchema,
|
||||
scorecard: exports.ScorecardApiSchema.nullish()
|
||||
}))
|
||||
});
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
* npx ts-node scripts/create_summary.ts
|
||||
*/
|
||||
|
||||
import {Change, Changes, ConfigurationOptions} from '../src/schemas'
|
||||
import {Change, Changes, ConfigurationOptions, Scorecard} from '../src/schemas'
|
||||
import {createTestChange} from '../__tests__/fixtures/create-test-change'
|
||||
import {InvalidLicenseChanges} from '../src/licenses'
|
||||
import * as fs from 'fs'
|
||||
@@ -33,7 +33,29 @@ const defaultConfig: ConfigurationOptions = {
|
||||
comment_summary_in_pr: true,
|
||||
retry_on_snapshot_warnings: false,
|
||||
retry_on_snapshot_warnings_timeout: 120,
|
||||
warn_only: false
|
||||
warn_only: false,
|
||||
warn_on_openssf_scorecard_level: 3,
|
||||
show_openssf_scorecard: true
|
||||
}
|
||||
|
||||
const scorecard: Scorecard = {
|
||||
dependencies: [
|
||||
{
|
||||
change: {
|
||||
change_type: 'added',
|
||||
manifest: '',
|
||||
ecosystem: 'unknown',
|
||||
name: 'castore',
|
||||
version: '0.1.17',
|
||||
package_url: 'pkg:hex/castore@0.1.17',
|
||||
license: null,
|
||||
source_repository_url: null,
|
||||
scope: 'runtime',
|
||||
vulnerabilities: []
|
||||
},
|
||||
scorecard: null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const tmpDir = path.resolve(__dirname, '../tmp')
|
||||
@@ -101,7 +123,13 @@ async function createSummary(
|
||||
config: ConfigurationOptions,
|
||||
fileName: string
|
||||
): Promise<void> {
|
||||
summary.addSummaryToSummary(vulnerabilities, licenseIssues, denied, config)
|
||||
summary.addSummaryToSummary(
|
||||
vulnerabilities,
|
||||
licenseIssues,
|
||||
denied,
|
||||
scorecard,
|
||||
config
|
||||
)
|
||||
summary.addChangeVulnerabilitiesToSummary(
|
||||
vulnerabilities,
|
||||
config.fail_on_severity
|
||||
|
||||
@@ -48,6 +48,10 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
'retry-on-snapshot-warnings-timeout'
|
||||
)
|
||||
const warn_only = getOptionalBoolean('warn-only')
|
||||
const show_openssf_scorecard = getOptionalBoolean('show-openssf-scorecard')
|
||||
const warn_on_openssf_scorecard_level = getOptionalNumber(
|
||||
'warn-on-openssf-scorecard-level'
|
||||
)
|
||||
|
||||
validatePURL(allow_dependencies_licenses)
|
||||
validateLicenses('allow-licenses', allow_licenses)
|
||||
@@ -69,7 +73,9 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
comment_summary_in_pr,
|
||||
retry_on_snapshot_warnings,
|
||||
retry_on_snapshot_warnings_timeout,
|
||||
warn_only
|
||||
warn_only,
|
||||
show_openssf_scorecard,
|
||||
warn_on_openssf_scorecard_level
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
|
||||
60
src/main.ts
60
src/main.ts
@@ -3,7 +3,13 @@ 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, Changes, ConfigurationOptions} from './schemas'
|
||||
import {
|
||||
Change,
|
||||
Severity,
|
||||
Changes,
|
||||
ConfigurationOptions,
|
||||
Scorecard
|
||||
} from './schemas'
|
||||
import {readConfig} from '../src/config'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
@@ -11,6 +17,7 @@ import {
|
||||
filterAllowedAdvisories
|
||||
} from '../src/filter'
|
||||
import {getInvalidLicenseChanges} from './licenses'
|
||||
import {getScorecardLevels} from './scorecard'
|
||||
import * as summary from './summary'
|
||||
import {getRefs} from './git-refs'
|
||||
|
||||
@@ -118,10 +125,13 @@ async function run(): Promise<void> {
|
||||
config.deny_groups
|
||||
)
|
||||
|
||||
const scorecard = await getScorecardLevels(filteredChanges)
|
||||
|
||||
summary.addSummaryToSummary(
|
||||
vulnerableChanges,
|
||||
invalidLicenseChanges,
|
||||
deniedChanges,
|
||||
scorecard,
|
||||
config
|
||||
)
|
||||
|
||||
@@ -141,6 +151,11 @@ async function run(): Promise<void> {
|
||||
summary.addDeniedToSummary(deniedChanges)
|
||||
printDeniedDependencies(deniedChanges, config)
|
||||
}
|
||||
if (config.show_openssf_scorecard) {
|
||||
summary.addScorecardToSummary(scorecard, config)
|
||||
printScorecardBlock(scorecard, config)
|
||||
createScorecardWarnings(scorecard, config)
|
||||
}
|
||||
|
||||
summary.addScannedDependencies(changes)
|
||||
printScannedDependencies(changes)
|
||||
@@ -257,6 +272,29 @@ function printNullLicenses(changes: Changes): void {
|
||||
}
|
||||
}
|
||||
|
||||
function printScorecardBlock(
|
||||
scorecard: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
): void {
|
||||
core.group('Scorecard', async () => {
|
||||
if (scorecard) {
|
||||
for (const dependency of scorecard.dependencies) {
|
||||
if (
|
||||
dependency.scorecard?.score &&
|
||||
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
|
||||
) {
|
||||
core.info(
|
||||
`${styles.color.red.open}${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${dependency?.scorecard?.score}${styles.red.close}`
|
||||
)
|
||||
}
|
||||
core.info(
|
||||
`${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${dependency?.scorecard?.score}`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function renderSeverity(
|
||||
severity: 'critical' | 'high' | 'moderate' | 'low'
|
||||
): string {
|
||||
@@ -325,4 +363,24 @@ function printDeniedDependencies(
|
||||
})
|
||||
}
|
||||
|
||||
async function createScorecardWarnings(
|
||||
scorecards: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
): Promise<void> {
|
||||
// Iterate through the list of scorecards, and if the score is less than the threshold, send a warning
|
||||
for (const dependency of scorecards.dependencies) {
|
||||
if (
|
||||
dependency.scorecard?.score &&
|
||||
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
|
||||
) {
|
||||
core.warning(
|
||||
`${dependency.change.ecosystem}/${dependency.change.name} has an OpenSSF Scorecard of ${dependency.scorecard?.score}, which is less than this repository's threshold of ${config.warn_on_openssf_scorecard_level}.`,
|
||||
{
|
||||
title: 'OpenSSF Scorecard Warning'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
@@ -51,6 +51,8 @@ export const ConfigurationOptionsSchema = z
|
||||
head_ref: z.string().optional(),
|
||||
retry_on_snapshot_warnings: z.boolean().default(false),
|
||||
retry_on_snapshot_warnings_timeout: z.number().default(120),
|
||||
show_openssf_scorecard: z.boolean().optional().default(true),
|
||||
warn_on_openssf_scorecard_level: z.number().default(3),
|
||||
comment_summary_in_pr: z
|
||||
.union([
|
||||
z.preprocess(
|
||||
@@ -100,9 +102,51 @@ export const ComparisonResponseSchema = z.object({
|
||||
snapshot_warnings: z.string()
|
||||
})
|
||||
|
||||
export const ScorecardApiSchema = z.object({
|
||||
date: z.string(),
|
||||
repo: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
commit: z.string()
|
||||
})
|
||||
.nullish(),
|
||||
scorecard: z
|
||||
.object({
|
||||
version: z.string(),
|
||||
commit: z.string()
|
||||
})
|
||||
.nullish(),
|
||||
checks: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
documentation: z.object({
|
||||
shortDescription: z.string(),
|
||||
url: z.string()
|
||||
}),
|
||||
score: z.string(),
|
||||
reason: z.string(),
|
||||
details: z.array(z.string())
|
||||
})
|
||||
)
|
||||
.nullish(),
|
||||
score: z.number().nullish()
|
||||
})
|
||||
|
||||
export const ScorecardSchema = z.object({
|
||||
dependencies: z.array(
|
||||
z.object({
|
||||
change: ChangeSchema,
|
||||
scorecard: ScorecardApiSchema.nullish()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
export type Change = z.infer<typeof ChangeSchema>
|
||||
export type Changes = z.infer<typeof ChangesSchema>
|
||||
export type ComparisonResponse = z.infer<typeof ComparisonResponseSchema>
|
||||
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
|
||||
export type Severity = z.infer<typeof SeveritySchema>
|
||||
export type Scope = (typeof SCOPES)[number]
|
||||
export type Scorecard = z.infer<typeof ScorecardSchema>
|
||||
export type ScorecardApi = z.infer<typeof ScorecardApiSchema>
|
||||
|
||||
73
src/scorecard.ts
Normal file
73
src/scorecard.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import {Change, Scorecard, ScorecardApi} from './schemas'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
export async function getScorecardLevels(
|
||||
changes: Change[]
|
||||
): Promise<Scorecard> {
|
||||
const data: Scorecard = {dependencies: []} as Scorecard
|
||||
for (const change of changes) {
|
||||
const ecosystem = change.ecosystem
|
||||
const packageName = change.name
|
||||
const version = change.version
|
||||
|
||||
//Get the project repository
|
||||
let repositoryUrl = change.source_repository_url
|
||||
//If the repository_url includes the protocol, remove it
|
||||
if (repositoryUrl?.startsWith('https://')) {
|
||||
repositoryUrl = repositoryUrl.replace('https://', '')
|
||||
}
|
||||
|
||||
// If GitHub API doesn't have the repository URL, query deps.dev for it.
|
||||
if (repositoryUrl) {
|
||||
// Call the deps.dev API to get the repository URL from there
|
||||
repositoryUrl = await getProjectUrl(ecosystem, packageName, version)
|
||||
}
|
||||
|
||||
// Get the scorecard API response from the scorecards API
|
||||
let scorecardApi: ScorecardApi | null = null
|
||||
if (repositoryUrl) {
|
||||
try {
|
||||
scorecardApi = await getScorecard(repositoryUrl)
|
||||
} catch (error: unknown) {
|
||||
core.debug(`Error querying for scorecard: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
data.dependencies.push({
|
||||
change,
|
||||
scorecard: scorecardApi
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
async function getScorecard(repositoryUrl: string): Promise<ScorecardApi> {
|
||||
const apiRoot = 'https://api.securityscorecards.dev/'
|
||||
let scorecardResponse: ScorecardApi = {} as ScorecardApi
|
||||
|
||||
const url = `${apiRoot}/projects/${repositoryUrl}`
|
||||
const response = await fetch(url)
|
||||
if (response.ok) {
|
||||
scorecardResponse = await response.json()
|
||||
} else {
|
||||
core.debug(`Couldn't get scorecard data for ${repositoryUrl}`)
|
||||
}
|
||||
return scorecardResponse
|
||||
}
|
||||
|
||||
export async function getProjectUrl(
|
||||
ecosystem: string,
|
||||
packageName: string,
|
||||
version: string
|
||||
): Promise<string> {
|
||||
core.debug(`Getting deps.dev data for ${packageName} ${version}`)
|
||||
const depsDevAPIRoot = 'https://api.deps.dev'
|
||||
const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`
|
||||
const response = await fetch(url)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.relatedProjects.length > 0) {
|
||||
return data.relatedProjects[0].projectKey.id
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
103
src/summary.ts
103
src/summary.ts
@@ -1,5 +1,5 @@
|
||||
import * as core from '@actions/core'
|
||||
import {ConfigurationOptions, Changes, Change} from './schemas'
|
||||
import {ConfigurationOptions, Changes, Change, Scorecard} from './schemas'
|
||||
import {SummaryTableRow} from '@actions/core/lib/summary'
|
||||
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
|
||||
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
||||
@@ -14,22 +14,30 @@ export function addSummaryToSummary(
|
||||
vulnerableChanges: Changes,
|
||||
invalidLicenseChanges: InvalidLicenseChanges,
|
||||
deniedChanges: Changes,
|
||||
scorecard: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
): void {
|
||||
const scorecardWarnings = countScorecardWarnings(scorecard, config)
|
||||
const licenseIssues = countLicenseIssues(invalidLicenseChanges)
|
||||
|
||||
core.summary.addHeading('Dependency Review', 1)
|
||||
|
||||
if (
|
||||
vulnerableChanges.length === 0 &&
|
||||
countLicenseIssues(invalidLicenseChanges) === 0 &&
|
||||
deniedChanges.length === 0
|
||||
licenseIssues === 0 &&
|
||||
deniedChanges.length === 0 &&
|
||||
scorecardWarnings === 0
|
||||
) {
|
||||
if (!config.license_check) {
|
||||
core.summary.addRaw(`${icons.check} No vulnerabilities found.`)
|
||||
} else if (!config.vulnerability_check) {
|
||||
core.summary.addRaw(`${icons.check} No license issues found.`)
|
||||
const issueTypes = [
|
||||
config.vulnerability_check ? 'vulnerabilities' : '',
|
||||
config.license_check ? 'license issues' : '',
|
||||
config.show_openssf_scorecard ? 'OpenSSF Scorecard issues' : ''
|
||||
]
|
||||
if (issueTypes.filter(Boolean).length === 0) {
|
||||
core.summary.addRaw(`${icons.check} No issues found.`)
|
||||
} else {
|
||||
core.summary.addRaw(
|
||||
`${icons.check} No vulnerabilities or license issues found.`
|
||||
`${icons.check} No ${issueTypes.filter(Boolean).join(' or ')} found.`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,11 +73,31 @@ export function addSummaryToSummary(
|
||||
deniedChanges.length
|
||||
} package(s) denied.`
|
||||
]
|
||||
: []),
|
||||
...(config.show_openssf_scorecard && scorecardWarnings > 0
|
||||
? [
|
||||
`${checkOrWarnIcon(scorecardWarnings)} ${scorecardWarnings ? scorecardWarnings : 'No'} packages with OpenSSF Scorecard issues.`
|
||||
]
|
||||
: [])
|
||||
])
|
||||
.addRaw('See the Details below.')
|
||||
}
|
||||
|
||||
function countScorecardWarnings(
|
||||
scorecard: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
): number {
|
||||
return scorecard.dependencies.reduce(
|
||||
(total, dependency) =>
|
||||
total +
|
||||
(dependency.scorecard?.score &&
|
||||
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
|
||||
? 1
|
||||
: 0),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
export function addChangeVulnerabilitiesToSummary(
|
||||
vulnerableChanges: Changes,
|
||||
severity: string
|
||||
@@ -249,6 +277,65 @@ function snapshotWarningRecommendation(
|
||||
return 'Re-running this action after a short time may resolve the issue.'
|
||||
}
|
||||
|
||||
export function addScorecardToSummary(
|
||||
scorecard: Scorecard,
|
||||
config: ConfigurationOptions
|
||||
): void {
|
||||
core.summary.addHeading('OpenSSF Scorecard', 2)
|
||||
if (scorecard.dependencies.length > 10) {
|
||||
core.summary.addRaw(`<details><summary>Scorecard details</summary>`, true)
|
||||
}
|
||||
core.summary.addRaw(
|
||||
`<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>`,
|
||||
true
|
||||
)
|
||||
for (const dependency of scorecard.dependencies) {
|
||||
core.debug('Adding scorecard to summary')
|
||||
core.debug(`Overall score ${dependency.scorecard?.score}`)
|
||||
|
||||
// Set the icon based on the overall score value
|
||||
let overallIcon = ''
|
||||
if (dependency.scorecard?.score) {
|
||||
overallIcon =
|
||||
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
|
||||
? ':warning:'
|
||||
: ':green_circle:'
|
||||
}
|
||||
|
||||
//Add a row for the dependency
|
||||
core.summary.addRaw(
|
||||
`<tr><td>${dependency.change.source_repository_url ? `<a href="https://${dependency.change.source_repository_url}">` : ''} ${dependency.change.ecosystem}/${dependency.change.name} ${dependency.change.source_repository_url ? `</a>` : ''}</td><td>${dependency.change.version}</td>
|
||||
<td>${overallIcon} ${dependency.scorecard?.score === undefined || dependency.scorecard?.score === null ? 'Unknown' : dependency.scorecard?.score}</td>`,
|
||||
false
|
||||
)
|
||||
|
||||
//Add details table in the last column
|
||||
if (dependency.scorecard?.checks !== undefined) {
|
||||
let detailsTable =
|
||||
'<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>'
|
||||
for (const check of dependency.scorecard?.checks || []) {
|
||||
const icon =
|
||||
parseFloat(check.score) < config.warn_on_openssf_scorecard_level
|
||||
? ':warning:'
|
||||
: ':green_circle:'
|
||||
|
||||
detailsTable += `<tr><td>${check.name}</td><td>${icon} ${check.score}</td><td>${check.reason}</td></tr>`
|
||||
}
|
||||
detailsTable += `</table>`
|
||||
core.summary.addRaw(
|
||||
`<td><details><summary>Details</summary>${detailsTable}</details></td></tr>`,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
core.summary.addRaw('<td>Unknown</td></tr>', true)
|
||||
}
|
||||
}
|
||||
core.summary.addRaw(`</table>`)
|
||||
if (scorecard.dependencies.length > 10) {
|
||||
core.summary.addRaw(`</details>`)
|
||||
}
|
||||
}
|
||||
|
||||
export function addSnapshotWarnings(
|
||||
config: ConfigurationOptions,
|
||||
warnings: string
|
||||
|
||||
Reference in New Issue
Block a user