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 ,
2024-03-04 19:34:29 +00:00
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'
2022-08-01 21:07:02 +01:00
import * as summary from './summary'
2022-07-21 15:47:05 -04:00
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 {
2022-11-04 14:51:41 +00:00
const config = await readConfig ()
2023-04-06 09:37:42 +02:00
2022-07-21 15:47:05 -04: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
2023-03-02 07:47:09 +00: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
)
2024-01-28 10:16:07 +01:00
const failOnSeverityParams = config . fail_on_severity
const warnOnly = config . warn_only
2024-01-28 14:35:44 +01:00
let minSeverity : Severity = 'low'
2024-01-28 10:16:07 +01:00
// 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-08-01 21:07:02 +01:00
)
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
)
2024-06-07 10:00:37 -07:00
// generate informational scorecard entries for all added changes in the PR
const scorecardChanges = getScorecardChanges ( changes )
const scorecard = await getScorecardLevels ( scorecardChanges )
2024-03-12 20:47:25 +00:00
2024-06-03 15:42:37 -07:00
const minSummary = summary . addSummaryToSummary (
2024-05-06 00:26:50 +00:00
vulnerableChanges ,
invalidLicenseChanges ,
deniedChanges ,
scorecard ,
config
)
2022-10-28 22:15:44 +02:00
2023-03-22 21:13:20 +00:00
if ( snapshot_warnings ) {
2023-09-07 17:54:42 +00:00
summary . addSnapshotWarnings ( config , snapshot_warnings )
2023-03-22 21:13:20 +00:00
}
2024-11-19 13:15:15 -08:00
let issueFound = false
2024-08-19 07:30:39 -07:00
2022-10-28 21:59:30 +02:00
if ( config . vulnerability_check ) {
2024-03-02 04:55:58 +01:00
core . setOutput ( 'vulnerable-changes' , JSON . stringify ( vulnerableChanges ))
2023-02-28 12:27:55 +00:00
summary . addChangeVulnerabilitiesToSummary ( vulnerableChanges , minSeverity )
2024-11-19 13:15:15 -08:00
issueFound ||= printVulnerabilitiesBlock (
vulnerableChanges ,
minSeverity ,
warnOnly
)
2022-10-28 21:59:30 +02:00
}
if ( config . license_check ) {
2024-03-02 04:55:58 +01:00
core . setOutput (
'invalid-license-changes' ,
JSON . stringify ( invalidLicenseChanges )
)
2022-10-28 21:59:30 +02:00
summary . addLicensesToSummary ( invalidLicenseChanges , config )
2024-11-19 13:15:15 -08:00
issueFound ||= printLicensesBlock ( invalidLicenseChanges , warnOnly )
2022-10-28 21:59:30 +02:00
}
2023-08-07 14:04:41 +02:00
if ( config . deny_packages || config . deny_groups ) {
2024-03-02 04:55:58 +01:00
core . setOutput ( 'denied-changes' , JSON . stringify ( deniedChanges ))
2023-08-02 16:17:51 +02:00
summary . addDeniedToSummary ( deniedChanges )
2024-11-19 13:15:15 -08:00
issueFound ||= printDeniedDependencies ( deniedChanges , config )
2022-10-28 21:59:30 +02:00
}
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 )
2024-03-04 17:52:17 +00:00
printScorecardBlock ( scorecard , config )
2024-03-08 01:29:53 +00:00
createScorecardWarnings ( scorecard , config )
2024-03-04 17:52:17 +00:00
}
2022-08-01 21:07:02 +01:00
2024-03-02 04:55:58 +01:00
core . setOutput ( 'dependency-changes' , JSON . stringify ( changes ))
2024-09-13 14:10:13 +01:00
summary . addScannedFiles ( 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 ()
2024-06-04 12:14:40 -07:00
core . setOutput ( 'comment-content' , rendered )
2024-06-03 15:42:37 -07:00
// 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
2024-11-19 13:15:15 -08:00
await commentPr ( rendered , config , issueFound )
2022-03-31 18:31:39 +02:00
} catch ( error ) {
if ( error instanceof RequestError && error . status === 404 ) {
2022-05-11 19:53:29 +00:00
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 (
2023-01-05 17:26:46 +00:00
`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
}
2022-08-01 21:07:02 +01: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 ,
2024-01-28 14:54:14 +01:00
minSeverity : Severity ,
warnOnly : boolean
2024-11-19 13:15:15 -08:00
) : boolean {
let vulFound = false
2022-09-26 12:21:24 +02:00
core . group ( 'Vulnerabilities' , async () => {
2024-11-19 13:15:15 -08:00
for ( const change of addedChanges ) {
vulFound ||= printChangeVulnerabilities ( change )
2022-09-26 12:21:24 +02:00
}
2024-11-19 13:15:15 -08:00
if ( vulFound ) {
2024-01-28 14:54:14 +01:00
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.`
)
}
})
2024-11-19 13:15:15 -08:00
return vulFound
2022-09-26 12:21:24 +02:00
}
2024-11-19 13:15:15 -08:00
function printChangeVulnerabilities ( change : Change ) : boolean {
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 } ` )
}
2024-11-19 13:15:15 -08:00
return change . vulnerabilities . length > 0
2022-05-31 06:03:42 +02:00
}
2022-09-27 12:25:12 +02:00
function printLicensesBlock (
2024-01-28 14:54:28 +01:00
invalidLicenseChanges : Record < string , Changes >,
warnOnly : boolean
2024-11-19 13:15:15 -08:00
) : boolean {
let issueFound = false
2022-09-27 12:25:12 +02:00
core . group ( 'Licenses' , async () => {
2022-10-27 13:09:37 +00:00
if ( invalidLicenseChanges . forbidden . length > 0 ) {
2024-11-19 13:15:15 -08:00
issueFound = true
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 ) {
2024-11-19 13:15:15 -08:00
issueFound = true
2022-10-27 13:09:37 +00:00
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
})
2024-11-19 13:15:15 -08:00
return issueFound
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 ) {
2024-03-11 22:17:28 +00:00
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-11 22:17:28 +00:00
)
}
2024-03-03 05:24:07 +00:00
core . info (
2024-03-08 02:31:11 +00:00
` ${ 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 ]
2022-10-11 14:50:55 +02:00
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 (
2024-06-07 10:00:37 -07:00
changes : Changes ,
2023-08-02 16:17:51 +02:00
config : ConfigurationOptions
2024-11-19 13:15:15 -08:00
) : boolean {
let issueFound = false
2023-08-02 16:17:51 +02:00
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` )
}
2024-08-19 07:30:39 -07:00
if ( changes . length > 0 ) {
2024-11-19 13:15:15 -08:00
issueFound = true
2024-08-19 07:30:39 -07:00
core . setFailed ( 'Dependency review detected denied packages.' )
} else {
core . info ( 'Dependency review did not detect any denied packages' )
}
2023-08-02 16:17:51 +02:00
})
2024-11-19 13:15:15 -08:00
return issueFound
2023-08-02 16:17:51 +02:00
}
2024-06-07 10:00:37 -07:00
function getScorecardChanges ( changes : Changes ) : Changes {
const out : Changes = []
for ( const change of changes ) {
if ( change . change_type === 'added' ) {
out . push ( change )
}
}
return out
}
2024-03-08 02:31:11 +00:00
async function createScorecardWarnings (
2024-03-08 01:29:53 +00:00
scorecards : Scorecard ,
config : ConfigurationOptions
2024-03-08 02:31:11 +00:00
) : 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 02:31:11 +00:00
)
2024-03-08 01:29:53 +00:00
}
}
}
2022-03-31 18:31:39 +02:00
run ()