Files
dependency-review-action/src/main.ts
T

408 lines
12 KiB
TypeScript
Raw Normal View History

2022-03-31 18:31:39 +02:00
import * as core from '@actions/core'
import * as dependencyGraph from './dependency-graph'
import * as github from '@actions/github'
import styles from 'ansi-styles'
2022-06-01 12:09:11 +02:00
import {RequestError} from '@octokit/request-error'
2024-03-03 05:24:07 +00:00
import {
Change,
Severity,
Changes,
ConfigurationOptions,
Scorecard
2024-03-03 05:24:07 +00:00
} from './schemas'
2022-06-13 19:55:08 +02:00
import {readConfig} from '../src/config'
2022-09-22 22:25:21 +00:00
import {
filterChangesBySeverity,
filterChangesByScopes,
2022-10-11 15:17:34 +02:00
filterAllowedAdvisories
2022-09-22 22:25:21 +00:00
} from '../src/filter'
2022-10-27 13:09:37 +00:00
import {getInvalidLicenseChanges} from './licenses'
2024-03-02 22:37:50 +00:00
import {getScorecardLevels} from './scorecard'
import * as summary from './summary'
import {getRefs} from './git-refs'
2022-03-31 18:31:39 +02:00
2022-09-27 12:25:12 +02:00
import {groupDependenciesByManifest} from './utils'
2024-06-04 11:25:38 -07:00
import {commentPr, MAX_COMMENT_LENGTH} from './comment-pr'
2023-08-07 14:04:41 +02:00
import {getDeniedChanges} from './deny'
2022-09-27 12:25:12 +02:00
2023-06-07 16:51:53 +01:00
async function delay(ms: number): Promise<void> {
2023-06-06 16:44:27 +01:00
return new Promise(resolve => setTimeout(resolve, ms))
}
2023-06-08 18:11:13 +01:00
async function getComparison(
baseRef: string,
headRef: string,
2023-06-09 10:26:24 +01:00
retryOpts?: {
retryUntil: number
2023-06-08 18:11:13 +01:00
retryDelay: number
}
): ReturnType<typeof dependencyGraph.compare> {
const comparison = await dependencyGraph.compare({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
baseRef,
headRef
})
2023-06-14 10:38:45 +01:00
if (comparison.snapshot_warnings.trim() !== '') {
core.info(comparison.snapshot_warnings)
if (retryOpts !== undefined) {
if (retryOpts.retryUntil < Date.now()) {
core.info(`Retry timeout exceeded. Proceeding...`)
return comparison
} else {
core.info(`Retrying in ${retryOpts.retryDelay} seconds...`)
await delay(retryOpts.retryDelay * 1000)
return getComparison(baseRef, headRef, retryOpts)
}
2023-06-14 10:24:40 +01:00
}
2023-06-08 18:11:13 +01:00
}
2023-06-14 10:38:45 +01:00
2023-06-08 18:11:13 +01:00
return comparison
}
2022-09-27 12:25:12 +02:00
2022-03-31 18:31:39 +02:00
async function run(): Promise<void> {
try {
const config = await readConfig()
2023-04-06 09:37:42 +02:00
const refs = getRefs(config, github.context)
2022-03-31 18:31:39 +02:00
2023-06-09 10:26:24 +01:00
const comparison = await getComparison(
refs.base,
refs.head,
config.retry_on_snapshot_warnings
? {
retryUntil:
Date.now() + config.retry_on_snapshot_warnings_timeout * 1000,
retryDelay: 10
}
: undefined
)
2023-06-08 18:11:13 +01:00
2023-03-22 21:13:20 +00:00
const changes = comparison.changes
const snapshot_warnings = comparison.snapshot_warnings
2022-03-31 18:31:39 +02:00
if (!changes) {
core.info('No Dependency Changes found. Skipping Dependency Review.')
return
}
2023-06-13 09:29:10 +02:00
2022-09-27 12:25:12 +02:00
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
2023-11-24 14:37:30 +01:00
2022-10-11 15:17:34 +02:00
const filteredChanges = filterAllowedAdvisories(
2022-09-27 12:25:12 +02:00
config.allow_ghsas,
2022-09-22 22:25:21 +00:00
scopedChanges
)
const failOnSeverityParams = config.fail_on_severity
const warnOnly = config.warn_only
let minSeverity: Severity = 'low'
// If failOnSeverityParams is not set or warnOnly is true, the minSeverity is low, to allow all vulnerabilities to be reported as warnings
if (failOnSeverityParams && !warnOnly) {
minSeverity = failOnSeverityParams
}
2023-02-28 12:27:55 +00:00
const vulnerableChanges = filterChangesBySeverity(
2022-09-27 12:25:12 +02:00
minSeverity,
2022-09-22 22:25:21 +00:00
filteredChanges
)
2022-03-31 18:31:39 +02:00
2022-10-27 13:09:37 +00:00
const invalidLicenseChanges = await getInvalidLicenseChanges(
2022-09-22 22:25:21 +00:00
filteredChanges,
2022-09-27 12:25:12 +02:00
{
allow: config.allow_licenses,
2023-03-08 12:38:34 +01:00
deny: config.deny_licenses,
2023-04-06 09:37:42 +02:00
licenseExclusions: config.allow_dependencies_licenses
2022-09-27 12:25:12 +02:00
}
2022-06-14 13:54:27 +02:00
)
2023-08-08 16:51:40 +02:00
core.debug(`Filtered Changes: ${JSON.stringify(filteredChanges)}`)
core.debug(`Config Deny Packages: ${JSON.stringify(config)}`)
2023-08-02 15:48:28 +02:00
const deniedChanges = await getDeniedChanges(
filteredChanges,
2023-08-07 14:04:41 +02:00
config.deny_packages,
config.deny_groups
2023-08-02 15:48:28 +02:00
)
const scorecard = await getScorecardLevels(filteredChanges)
2024-06-03 15:42:37 -07:00
const minSummary = summary.addSummaryToSummary(
vulnerableChanges,
invalidLicenseChanges,
deniedChanges,
scorecard,
config
)
2023-03-22 21:13:20 +00:00
if (snapshot_warnings) {
summary.addSnapshotWarnings(config, snapshot_warnings)
2023-03-22 21:13:20 +00:00
}
if (config.vulnerability_check) {
core.setOutput('vulnerable-changes', JSON.stringify(vulnerableChanges))
2023-02-28 12:27:55 +00:00
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
printVulnerabilitiesBlock(vulnerableChanges, minSeverity, warnOnly)
}
if (config.license_check) {
core.setOutput(
'invalid-license-changes',
JSON.stringify(invalidLicenseChanges)
)
summary.addLicensesToSummary(invalidLicenseChanges, config)
2024-01-28 14:54:28 +01:00
printLicensesBlock(invalidLicenseChanges, warnOnly)
}
2023-08-07 14:04:41 +02:00
if (config.deny_packages || config.deny_groups) {
core.setOutput('denied-changes', JSON.stringify(deniedChanges))
2023-08-02 16:17:51 +02:00
summary.addDeniedToSummary(deniedChanges)
printDeniedDependencies(deniedChanges, config)
}
2024-03-04 18:28:43 +00:00
if (config.show_openssf_scorecard) {
2024-03-04 18:38:53 +00:00
summary.addScorecardToSummary(scorecard, config)
printScorecardBlock(scorecard, config)
2024-03-08 01:29:53 +00:00
createScorecardWarnings(scorecard, config)
}
core.setOutput('dependency-changes', JSON.stringify(changes))
summary.addScannedDependencies(changes)
2022-09-26 12:21:24 +02:00
printScannedDependencies(changes)
2024-06-03 15:42:37 -07:00
// include full summary in output; Actions will truncate if oversized
let rendered = core.summary.stringify()
core.setOutput('content-comment', rendered)
// if the summary is oversized, replace with minimal version
if (rendered.length >= MAX_COMMENT_LENGTH) {
core.debug(
'The comment was too big for the GitHub API. Falling back on a minimum comment'
)
rendered = minSummary
}
// update the PR comment if needed with the right-sized summary
await commentPr(rendered, config)
2022-03-31 18:31:39 +02:00
} catch (error) {
if (error instanceof RequestError && error.status === 404) {
core.setFailed(
`Dependency review could not obtain dependency data for the specified owner, repository, or revision range.`
)
} else if (error instanceof RequestError && error.status === 403) {
2022-03-31 18:31:39 +02:00
core.setFailed(
`Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled along with GitHub Advanced Security on private repositories, see https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/settings/security_analysis`
2022-03-31 18:31:39 +02:00
)
2022-05-23 11:45:36 -07:00
} else {
if (error instanceof Error) {
core.setFailed(error.message)
} else {
core.setFailed('Unexpected fatal error')
}
2022-03-31 18:31:39 +02:00
}
} finally {
await core.summary.write()
2022-03-31 18:31:39 +02:00
}
}
2022-09-26 12:21:24 +02:00
function printVulnerabilitiesBlock(
2022-10-27 13:09:37 +00:00
addedChanges: Changes,
minSeverity: Severity,
warnOnly: boolean
2022-09-26 12:21:24 +02:00
): void {
let vulFound = false
2022-09-26 12:21:24 +02:00
core.group('Vulnerabilities', async () => {
if (addedChanges.length > 0) {
for (const change of addedChanges) {
printChangeVulnerabilities(change)
}
vulFound = true
2022-09-26 12:21:24 +02:00
}
if (vulFound) {
const msg = 'Dependency review detected vulnerable packages.'
if (warnOnly) {
core.warning(msg)
} else {
core.setFailed(msg)
}
2022-09-26 12:21:24 +02:00
} else {
core.info(
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`
)
}
})
}
2022-07-04 20:09:44 +09:00
function printChangeVulnerabilities(change: Change): void {
2022-06-01 05:36:46 +02:00
for (const vuln of change.vulnerabilities) {
2022-05-31 06:03:42 +02:00
core.info(
2022-06-01 12:09:11 +02:00
`${styles.bold.open}${change.manifest} » ${change.name}@${
change.version
2022-05-31 06:03:42 +02:00
}${styles.bold.close} ${vuln.advisory_summary} ${renderSeverity(
vuln.severity
)}`
)
core.info(` ↪ ${vuln.advisory_url}`)
}
}
2022-09-27 12:25:12 +02:00
function printLicensesBlock(
2024-01-28 14:54:28 +01:00
invalidLicenseChanges: Record<string, Changes>,
warnOnly: boolean
2022-09-27 12:25:12 +02:00
): void {
core.group('Licenses', async () => {
2022-10-27 13:09:37 +00:00
if (invalidLicenseChanges.forbidden.length > 0) {
2022-10-27 16:24:30 +00:00
core.info('\nThe following dependencies have incompatible licenses:')
2022-10-27 13:09:37 +00:00
printLicensesError(invalidLicenseChanges.forbidden)
2024-01-28 14:54:28 +01:00
const msg = 'Dependency review detected incompatible licenses.'
if (warnOnly) {
core.warning(msg)
} else {
core.setFailed(msg)
}
2022-09-27 12:25:12 +02:00
}
2022-10-27 13:09:37 +00:00
if (invalidLicenseChanges.unresolved.length > 0) {
core.warning(
2022-10-28 11:23:05 +02:00
'\nThe validity of the licenses of the dependencies below could not be determined. Ensure that they are valid SPDX licenses:'
2022-10-27 13:09:37 +00:00
)
printLicensesError(invalidLicenseChanges.unresolved)
core.setFailed(
'Dependency review could not detect the validity of all licenses.'
)
}
printNullLicenses(invalidLicenseChanges.unlicensed)
2022-09-27 12:25:12 +02:00
})
}
2022-10-27 13:09:37 +00:00
function printLicensesError(changes: Changes): void {
2022-09-27 12:25:12 +02:00
for (const change of changes) {
core.info(
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close} License: ${styles.color.red.open}${change.license}${styles.color.red.close}`
)
}
}
2022-10-27 13:09:37 +00:00
function printNullLicenses(changes: Changes): void {
2022-09-27 12:25:12 +02:00
if (changes.length === 0) {
return
}
2022-10-27 16:24:30 +00:00
core.info('\nWe could not detect a license for the following dependencies:')
2022-09-27 12:25:12 +02:00
for (const change of changes) {
core.info(
`${styles.bold.open}${change.manifest} » ${change.name}@${change.version}${styles.bold.close}`
)
}
}
2024-03-03 05:24:07 +00:00
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(
2024-03-13 16:23:23 +00:00
`${styles.color.red.open}${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${dependency?.scorecard?.score}${styles.red.close}`
)
}
2024-03-03 05:24:07 +00:00
core.info(
`${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${dependency?.scorecard?.score}`
2024-03-03 05:24:07 +00:00
)
}
}
})
}
2022-03-31 18:31:39 +02:00
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}`
}
2022-09-26 12:01:39 +02:00
function renderScannedDependency(change: Change): string {
const changeType: string = change.change_type
if (changeType !== 'added' && changeType !== 'removed') {
throw new Error(`Unexpected change type: ${changeType}`)
}
2022-09-26 11:35:05 +02:00
const color = (
{
added: 'green',
removed: 'red'
} as const
2022-09-26 12:01:39 +02:00
)[changeType]
const icon = (
{
added: '+',
removed: '-'
} as const
)[changeType]
return `${styles.color[color].open}${icon} ${change.name}@${change.version}${styles.color[color].close}`
2022-09-26 11:35:05 +02:00
}
2022-09-26 19:13:25 +02:00
function printScannedDependencies(changes: Changes): void {
2022-09-26 12:41:16 +02:00
core.group('Dependency Changes', async () => {
2022-09-27 12:25:12 +02:00
const dependencies = groupDependenciesByManifest(changes)
2022-09-26 12:21:24 +02:00
2022-09-26 12:41:16 +02:00
for (const manifestName of dependencies.keys()) {
const manifestChanges = dependencies.get(manifestName) || []
2022-09-26 12:21:24 +02:00
core.info(`File: ${styles.bold.open}${manifestName}${styles.bold.close}`)
for (const change of manifestChanges) {
core.info(`${renderScannedDependency(change)}`)
}
}
})
}
2023-08-02 16:17:51 +02:00
function printDeniedDependencies(
changes: Change[],
config: ConfigurationOptions
): void {
core.group('Denied', async () => {
2023-08-07 14:04:41 +02:00
for (const denied of config.deny_packages) {
2023-08-02 16:17:51 +02:00
core.info(`Config: ${denied}`)
}
for (const change of changes) {
core.info(`Change: ${change.name}@${change.version} is denied`)
core.info(`Change: ${change.package_url} is denied`)
}
})
}
async function createScorecardWarnings(
2024-03-08 01:29:53 +00:00
scorecards: Scorecard,
config: ConfigurationOptions
): Promise<void> {
2024-03-08 01:29:53 +00:00
// 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
) {
2024-03-11 22:05:19 +00:00
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'
}
)
2024-03-08 01:29:53 +00:00
}
}
}
2022-03-31 18:31:39 +02:00
run()