2022-08-01 21:07:02 +01:00
import * as core from '@actions/core'
import { SummaryTableRow } from '@actions/core/lib/summary'
2023-03-02 07:43:23 +00:00
import { InvalidLicenseChanges , InvalidLicenseChangeTypes } from './licenses'
2024-09-13 14:10:13 +01:00
import { Change , Changes , ConfigurationOptions , Scorecard } from './schemas'
2022-09-27 12:25:12 +02:00
import { groupDependenciesByManifest , getManifestsSet , renderUrl } from './utils'
2022-08-01 21:07:02 +01:00
2023-02-22 14:05:52 +00:00
const icons = {
check : '✅' ,
cross : '❌' ,
warning : '⚠️'
}
2024-09-16 12:26:36 -07:00
const MAX_SCANNED_FILES_BYTES = 1048576
2024-06-03 19:17:51 -07:00
// generates the DR report summmary and caches it to the Action's core.summary.
// returns the DR summary string, ready to be posted as a PR comment if the
// final DR report is too large
2024-06-03 15:42:37 -07:00
export function addSummaryToSummary (
2024-05-06 00:26:50 +00:00
vulnerableChanges : Changes ,
invalidLicenseChanges : InvalidLicenseChanges ,
deniedChanges : Changes ,
scorecard : Scorecard ,
2024-06-04 11:25:38 -07:00
config : ConfigurationOptions
2024-05-06 00:26:50 +00:00
) : string {
2024-06-04 11:41:21 -07:00
const out : string [ ] = [ ]
2024-06-03 15:42:37 -07:00
2024-05-06 00:26:50 +00:00
const scorecardWarnings = countScorecardWarnings ( scorecard , config )
const licenseIssues = countLicenseIssues ( invalidLicenseChanges )
2024-06-03 15:42:37 -07:00
core . summary . addHeading ( 'Dependency Review' , 1 )
out . push ( '# Dependency Review' )
2024-05-06 00:26:50 +00:00
2025-08-14 14:25:52 +00:00
addDenyListsDeprecationWarningToSummary ( )
2024-05-06 00:26:50 +00:00
if (
vulnerableChanges . length === 0 &&
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' : ''
]
2024-06-03 15:42:37 -07:00
let msg = ''
2024-05-06 00:26:50 +00:00
if ( issueTypes . filter ( Boolean ) . length === 0 ) {
2024-06-03 15:42:37 -07:00
msg = ` ${ icons . check } No issues found. `
2024-05-06 00:26:50 +00:00
} else {
2024-06-03 15:42:37 -07:00
msg = ` ${ icons . check } No ${ issueTypes . filter ( Boolean ) . join ( ' or ' ) } found. `
2024-05-06 00:26:50 +00:00
}
2024-06-03 15:42:37 -07:00
core . summary . addRaw ( msg )
out . push ( msg )
return out . join ( '\n' )
}
2024-05-06 00:26:50 +00:00
2024-06-04 11:25:38 -07:00
const foundIssuesHeader = 'The following issues were found:'
2024-06-03 15:42:37 -07:00
core . summary . addRaw ( foundIssuesHeader )
out . push ( foundIssuesHeader )
2024-03-12 20:47:25 +00:00
2024-06-03 15:42:37 -07:00
const summaryList : string [ ] = [
2024-06-04 11:25:38 -07:00
. . . ( config . vulnerability_check
2024-06-03 15:42:37 -07:00
? [
` ${ checkOrFailIcon ( vulnerableChanges . length ) } ${
vulnerableChanges . length
} vulnerable package ( s ) `
]
: [ ] ) ,
. . . ( config . license_check
? [
` ${ checkOrFailIcon ( invalidLicenseChanges . forbidden . length ) } ${
invalidLicenseChanges . forbidden . length
} package ( s ) with incompatible licenses ` ,
` ${ checkOrFailIcon ( invalidLicenseChanges . unresolved . length ) } ${
invalidLicenseChanges . unresolved . length
} package ( s ) with invalid SPDX license definitions ` ,
` ${ checkOrWarnIcon ( invalidLicenseChanges . unlicensed . length ) } ${
invalidLicenseChanges . unlicensed . length
} package ( s ) with unknown licenses . `
]
: [ ] ) ,
. . . ( deniedChanges . length > 0
? [
` ${ checkOrWarnIcon ( deniedChanges . length ) } ${
deniedChanges . length
} package ( s ) denied . `
]
: [ ] ) ,
. . . ( config . show_openssf_scorecard && scorecardWarnings > 0
? [
` ${ checkOrWarnIcon ( scorecardWarnings ) } ${ scorecardWarnings ? scorecardWarnings : 'No' } packages with OpenSSF Scorecard issues. `
]
: [ ] )
2024-06-04 11:25:38 -07:00
]
2023-02-22 14:05:52 +00:00
2024-06-03 15:42:37 -07:00
core . summary . addList ( summaryList )
2024-06-04 11:41:21 -07:00
for ( const line of summaryList ) {
2024-06-04 11:45:43 -07:00
out . push ( ` * ${ line } ` )
2024-06-04 11:41:21 -07:00
}
2023-02-27 16:05:59 +00:00
2024-06-03 15:42:37 -07:00
core . summary . addRaw ( 'See the Details below.' )
2024-06-04 11:25:38 -07:00
out . push (
` \ n[View full job summary]( ${ process . env . GITHUB_SERVER_URL } / ${ process . env . GITHUB_REPOSITORY } /actions/runs/ ${ process . env . GITHUB_RUN_ID } ) `
)
2023-02-27 16:05:59 +00:00
2024-06-03 15:42:37 -07:00
return out . join ( '\n' )
2022-08-01 21:07:02 +01:00
}
2025-08-14 14:25:52 +00:00
function addDenyListsDeprecationWarningToSummary ( ) : void {
core . summary . addRaw (
` ${ icons . warning } The <em>deny-licenses</em> option is deprecated and will be removed in a future version, use <em>allow-licenses</em> instead.<br> `
)
}
2024-03-12 20:47:25 +00:00
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
)
}
2022-08-01 21:07:02 +01:00
export function addChangeVulnerabilitiesToSummary (
2023-02-28 12:28:20 +00:00
vulnerableChanges : Changes ,
2022-08-01 21:07:02 +01:00
severity : string
) : void {
2023-02-28 12:28:20 +00:00
if ( vulnerableChanges . length === 0 ) {
2023-02-27 16:05:59 +00:00
return
}
2022-08-01 21:07:02 +01:00
const rows : SummaryTableRow [ ] = [ ]
2023-02-28 12:28:20 +00:00
const manifests = getManifestsSet ( vulnerableChanges )
2022-08-01 21:07:02 +01:00
2023-02-28 12:28:20 +00:00
core . summary . addHeading ( 'Vulnerabilities' , 2 )
2022-08-01 21:07:02 +01:00
for ( const manifest of manifests ) {
2023-02-28 12:28:20 +00:00
for ( const change of vulnerableChanges . filter (
2022-08-01 21:07:02 +01:00
pkg = > pkg . manifest === manifest
) ) {
let previous_package = ''
let previous_version = ''
for ( const vuln of change . vulnerabilities ) {
const sameAsPrevious =
previous_package === change . name &&
previous_version === change . version
if ( ! sameAsPrevious ) {
rows . push ( [
renderUrl ( change . source_repository_url , change . name ) ,
change . version ,
renderUrl ( vuln . advisory_url , vuln . advisory_summary ) ,
vuln . severity
] )
} else {
rows . push ( [
{ data : '' , colspan : '2' } ,
renderUrl ( vuln . advisory_url , vuln . advisory_summary ) ,
vuln . severity
] )
}
previous_package = change . name
previous_version = change . version
}
}
2023-02-28 12:28:20 +00:00
core . summary . addHeading ( ` <em> ${ manifest } </em> ` , 4 ) . addTable ( [
2022-08-01 21:07:02 +01:00
[
{ data : 'Name' , header : true } ,
{ data : 'Version' , header : true } ,
{ data : 'Vulnerability' , header : true } ,
{ data : 'Severity' , header : true }
] ,
. . . rows
] )
}
2023-02-27 16:05:59 +00:00
if ( severity !== 'low' ) {
core . summary . addQuote (
` Only included vulnerabilities with severity <strong> ${ severity } </strong> or higher. `
)
}
2022-08-01 21:07:02 +01:00
}
export function addLicensesToSummary (
2023-02-28 11:08:39 +00:00
invalidLicenseChanges : InvalidLicenseChanges ,
2022-08-01 21:07:02 +01:00
config : ConfigurationOptions
) : void {
2023-02-28 11:08:39 +00:00
if ( countLicenseIssues ( invalidLicenseChanges ) === 0 ) {
return
}
2023-02-28 12:28:20 +00:00
core . summary . addHeading ( 'License Issues' , 2 )
2023-03-02 07:43:23 +00:00
printLicenseViolations ( invalidLicenseChanges )
2022-08-01 21:07:02 +01:00
if ( config . allow_licenses && config . allow_licenses . length > 0 ) {
core . summary . addQuote (
` <strong>Allowed Licenses</strong>: ${ config . allow_licenses . join ( ', ' ) } `
)
}
if ( config . deny_licenses && config . deny_licenses . length > 0 ) {
core . summary . addQuote (
` <strong>Denied Licenses</strong>: ${ config . deny_licenses . join ( ', ' ) } `
)
}
2023-03-08 12:38:34 +01:00
if ( config . allow_dependencies_licenses ) {
2023-04-06 10:04:48 +02:00
core . summary . addQuote (
` <strong>Excluded from license check</strong>: ${ config . allow_dependencies_licenses . join (
', '
) } `
)
2023-03-08 12:38:34 +01:00
}
2022-08-01 21:07:02 +01:00
2022-10-27 13:09:37 +00:00
core . debug (
` found ${ invalidLicenseChanges . unlicensed . length } unknown licenses `
)
2022-08-01 21:07:02 +01:00
2022-10-27 16:24:30 +00:00
core . debug (
` ${ invalidLicenseChanges . unresolved . length } licenses could not be validated `
)
}
2022-08-01 21:07:02 +01:00
2023-03-02 07:43:23 +00:00
const licenseIssueTypes : InvalidLicenseChangeTypes [ ] = [
'forbidden' ,
'unresolved' ,
'unlicensed'
]
2022-08-01 21:07:02 +01:00
2023-03-02 07:43:23 +00:00
const issueTypeNames : Record < InvalidLicenseChangeTypes , string > = {
forbidden : 'Incompatible License' ,
unresolved : 'Invalid SPDX License' ,
unlicensed : 'Unknown License'
}
2022-08-01 21:07:02 +01:00
2023-03-02 07:43:23 +00:00
function printLicenseViolations ( changes : InvalidLicenseChanges ) : void {
const rowsGroupedByManifest : Record < string , SummaryTableRow [ ] > = { }
2023-02-28 11:08:39 +00:00
2023-03-02 07:43:23 +00:00
for ( const issueType of licenseIssueTypes ) {
for ( const change of changes [ issueType ] ) {
if ( ! rowsGroupedByManifest [ change . manifest ] ) {
rowsGroupedByManifest [ change . manifest ] = [ ]
}
rowsGroupedByManifest [ change . manifest ] . push ( [
2023-02-28 11:08:39 +00:00
renderUrl ( change . source_repository_url , change . name ) ,
change . version ,
2023-03-02 07:43:23 +00:00
formatLicense ( change . license ) ,
issueTypeNames [ issueType ]
2023-02-28 11:08:39 +00:00
] )
2022-08-01 21:07:02 +01:00
}
2023-03-02 07:43:23 +00:00
}
2023-02-28 11:08:39 +00:00
2023-03-02 07:43:23 +00:00
for ( const [ manifest , rows ] of Object . entries ( rowsGroupedByManifest ) ) {
core . summary . addHeading ( ` <em> ${ manifest } </em> ` , 4 )
core . summary . addTable ( [
[ 'Package' , 'Version' , 'License' , 'Issue Type' ] ,
. . . rows
] )
2022-10-27 16:43:45 +00:00
}
}
function formatLicense ( license : string | null ) : string {
if ( license === null || license === 'NOASSERTION' ) {
return 'Null'
2022-08-01 21:07:02 +01:00
}
2022-10-27 16:43:45 +00:00
return license
2022-08-01 21:07:02 +01:00
}
2024-09-13 14:10:13 +01:00
export function addScannedFiles ( changes : Changes ) : void {
const manifests = Array . from (
groupDependenciesByManifest ( changes ) . keys ( )
) . sort ( )
2024-09-16 12:26:36 -07:00
let sf_size = 0
let trunc_at = - 1
for ( const [ index , entry ] of manifests . entries ( ) ) {
2024-09-16 12:42:35 -07:00
if ( sf_size + entry . length >= MAX_SCANNED_FILES_BYTES ) {
2024-09-16 12:26:36 -07:00
trunc_at = index
2024-09-16 12:42:35 -07:00
break
2024-09-16 12:26:36 -07:00
}
2024-09-16 12:42:35 -07:00
sf_size += entry . length
2024-09-16 12:26:36 -07:00
}
if ( trunc_at >= 0 ) {
2024-09-16 12:42:35 -07:00
// truncate the manifests list if it will overflow the summary output
2024-09-16 12:26:36 -07:00
manifests . slice ( 0 , trunc_at )
2024-09-16 12:42:35 -07:00
// if there's room between cutoff size and list size, add a warning
const size_diff = MAX_SCANNED_FILES_BYTES - sf_size
if ( size_diff < 12 ) {
manifests . push ( '(truncated)' )
}
2024-09-16 12:26:36 -07:00
}
2025-01-26 22:36:42 +09:00
const summary = core . summary . addHeading ( 'Scanned Files' , 2 )
if ( manifests . length === 0 ) {
summary . addRaw ( 'None' )
} else {
summary . addList ( manifests )
}
2022-09-26 19:14:04 +02:00
}
2023-02-22 14:05:52 +00:00
2023-09-07 17:54:42 +00:00
function snapshotWarningRecommendation (
config : ConfigurationOptions ,
warnings : string
) : string {
const no_pr_snaps = warnings . includes (
'No snapshots were found for the head SHA'
)
const retries_disabled = ! config . retry_on_snapshot_warnings
if ( no_pr_snaps && retries_disabled ) {
2023-09-07 18:00:57 +00:00
return 'Ensure that dependencies are being submitted on PR branches and consider enabling <em>retry-on-snapshot-warnings</em>.'
2023-09-07 17:54:42 +00:00
} else if ( no_pr_snaps ) {
return 'Ensure that dependencies are being submitted on PR branches. Re-running this action after a short time may resolve the issue.'
} else if ( retries_disabled ) {
2023-09-07 18:00:57 +00:00
return 'Consider enabling <em>retry-on-snapshot-warnings</em>.'
2023-09-07 17:54:42 +00:00
}
return 'Re-running this action after a short time may resolve the issue.'
}
2024-03-04 17:52:17 +00:00
export function addScorecardToSummary (
scorecard : Scorecard ,
config : ConfigurationOptions
) : void {
2025-01-25 23:28:54 +09:00
if ( scorecard . dependencies . length === 0 ) {
return
}
2024-03-04 17:52:17 +00:00
core . summary . addHeading ( 'OpenSSF Scorecard' , 2 )
2024-03-11 22:23:03 +00:00
if ( scorecard . dependencies . length > 10 ) {
core . summary . addRaw ( ` <details><summary>Scorecard details</summary> ` , true )
}
2024-03-04 18:38:53 +00:00
core . summary . addRaw (
` <table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr> ` ,
true
)
2024-03-04 17:52:17 +00:00
for ( const dependency of scorecard . dependencies ) {
2024-03-04 20:03:39 +00:00
core . debug ( 'Adding scorecard to summary' )
2024-03-06 14:43:49 +00:00
core . debug ( ` Overall score ${ dependency . scorecard ? . score } ` )
2024-03-04 20:03:39 +00:00
// Set the icon based on the overall score value
2024-03-04 20:07:08 +00:00
let overallIcon = ''
2024-03-12 20:47:25 +00:00
if ( dependency . scorecard ? . score ) {
2024-03-04 20:03:39 +00:00
overallIcon =
2024-03-06 14:43:49 +00:00
dependency . scorecard ? . score < config . warn_on_openssf_scorecard_level
2024-03-04 20:03:39 +00:00
? ':warning:'
: ':green_circle:'
}
//Add a row for the dependency
2024-03-04 18:28:43 +00:00
core . summary . addRaw (
2024-04-23 17:26:55 +00:00
` <tr><td> ${ dependency . change . source_repository_url ? ` <a href=" ${ dependency . change . source_repository_url } "> ` : '' } ${ dependency . change . ecosystem } / ${ dependency . change . name } ${ dependency . change . source_repository_url ? ` </a> ` : '' } </td><td> ${ dependency . change . version } </td>
2024-03-06 14:43:49 +00:00
< td > $ { overallIcon } $ { dependency . scorecard ? . score === undefined || dependency . scorecard ? . score === null ? 'Unknown' : dependency . scorecard ? . score } < / td > ` ,
2024-03-04 18:28:43 +00:00
false
)
2024-03-06 14:43:49 +00:00
//Add details table in the last column
if ( dependency . scorecard ? . checks !== undefined ) {
2024-03-04 19:34:29 +00:00
let detailsTable =
'<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>'
2024-03-06 14:43:49 +00:00
for ( const check of dependency . scorecard ? . checks || [ ] ) {
2024-03-04 20:03:39 +00:00
const icon =
parseFloat ( check . score ) < config . warn_on_openssf_scorecard_level
2024-03-04 19:38:52 +00:00
? ':warning:'
: ':green_circle:'
2024-03-04 19:34:29 +00:00
2024-03-04 20:07:08 +00:00
detailsTable += ` <tr><td> ${ check . name } </td><td> ${ icon } ${ check . score } </td><td> ${ check . reason } </td></tr> `
2024-03-04 19:34:29 +00:00
}
detailsTable += ` </table> `
core . summary . addRaw (
` <td><details><summary>Details</summary> ${ detailsTable } </details></td></tr> ` ,
true
)
} else {
core . summary . addRaw ( '<td>Unknown</td></tr>' , true )
2024-03-04 17:52:17 +00:00
}
}
2024-03-04 18:38:53 +00:00
core . summary . addRaw ( ` </table> ` )
2024-03-11 22:23:03 +00:00
if ( scorecard . dependencies . length > 10 ) {
core . summary . addRaw ( ` </details> ` )
}
2024-03-04 17:52:17 +00:00
}
2023-09-07 17:54:42 +00:00
export function addSnapshotWarnings (
config : ConfigurationOptions ,
warnings : string
) : void {
2023-03-22 21:13:20 +00:00
core . summary . addHeading ( 'Snapshot Warnings' , 2 )
core . summary . addQuote ( ` ${ icons . warning } : ${ warnings } ` )
2023-09-07 17:54:42 +00:00
const recommendation = snapshotWarningRecommendation ( config , warnings )
const docsLink =
'See <a href="https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#best-practices-for-using-the-dependency-review-api-and-the-dependency-submission-api-together">the documentation</a> for more information and troubleshooting advice.'
2023-09-07 18:00:57 +00:00
core . summary . addRaw ( ` ${ recommendation } ${ docsLink } ` )
2023-03-22 21:13:20 +00:00
}
2023-02-22 14:05:52 +00:00
function countLicenseIssues (
invalidLicenseChanges : InvalidLicenseChanges
) : number {
return Object . values ( invalidLicenseChanges ) . reduce (
( acc , val ) = > acc + val . length ,
0
)
}
2023-08-02 16:17:51 +02:00
export function addDeniedToSummary ( deniedChanges : Change [ ] ) : void {
if ( deniedChanges . length === 0 ) {
return
}
core . summary . addHeading ( 'Denied dependencies' , 2 )
for ( const change of deniedChanges ) {
core . summary . addHeading ( ` <em>Denied dependencies</em> ` , 4 )
core . summary . addTable ( [
[ 'Package' , 'Version' , 'License' ] ,
[
renderUrl ( change . source_repository_url , change . name ) ,
change . version ,
change . license || ''
]
] )
}
}
2023-02-27 16:05:59 +00:00
function checkOrFailIcon ( count : number ) : string {
2023-02-22 14:05:52 +00:00
return count === 0 ? icons.check : icons.cross
}
2023-02-27 16:05:59 +00:00
function checkOrWarnIcon ( count : number ) : string {
2023-02-22 14:05:52 +00:00
return count === 0 ? icons.check : icons.warning
}