2022-08-01 21:07:02 +01:00
import * as core from '@actions/core'
2024-03-04 17:52:17 +00:00
import { ConfigurationOptions , Changes , Change , Scorecard } from './schemas'
2022-08-01 21:07:02 +01:00
import { SummaryTableRow } from '@actions/core/lib/summary'
2023-03-02 07:43:23 +00:00
import { InvalidLicenseChanges , InvalidLicenseChangeTypes } from './licenses'
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 : '⚠️'
}
2022-08-01 21:07:02 +01:00
export function addSummaryToSummary (
2023-02-28 12:28:20 +00:00
vulnerableChanges : Changes ,
2023-02-22 14:05:52 +00:00
invalidLicenseChanges : InvalidLicenseChanges ,
2023-08-02 15:48:28 +02:00
deniedChanges : Changes ,
2024-03-12 20:47:25 +00:00
scorecard : Scorecard ,
2023-02-22 14:05:52 +00:00
config : ConfigurationOptions
2022-08-01 21:07:02 +01:00
) : void {
2024-03-12 20:47:25 +00:00
const scorecardWarnings = countScorecardWarnings ( scorecard , config )
const licenseIssues = countLicenseIssues ( invalidLicenseChanges )
2023-02-28 12:28:20 +00:00
core . summary . addHeading ( 'Dependency Review' , 1 )
2023-02-22 14:05:52 +00:00
if (
2023-02-28 12:28:20 +00:00
vulnerableChanges . length === 0 &&
2024-03-12 20:47:25 +00:00
licenseIssues === 0 &&
deniedChanges . length === 0 &&
scorecardWarnings === 0
2023-02-22 14:05:52 +00:00
) {
2024-03-12 20:47:25 +00:00
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. ` )
2023-02-22 14:05:52 +00:00
} else {
core . summary . addRaw (
2024-03-12 21:32:27 +00:00
` ${ icons . check } No ${ issueTypes . filter ( Boolean ) . join ( ' or ' ) } found. `
2023-02-22 14:05:52 +00:00
)
}
2023-02-27 16:05:59 +00:00
return
2023-02-22 14:05:52 +00:00
}
2023-02-27 16:05:59 +00:00
core . summary
. addRaw ( 'The following issues were found:' )
. addList ( [
. . . ( config . vulnerability_check
? [
2023-02-28 12:28:20 +00:00
` ${ checkOrFailIcon ( vulnerableChanges . length ) } ${
vulnerableChanges . length
2023-02-27 16:05:59 +00:00
} vulnerable package ( s ) `
]
: [ ] ) ,
. . . ( config . license_check
? [
` ${ checkOrFailIcon ( invalidLicenseChanges . forbidden . length ) } ${
invalidLicenseChanges . forbidden . length
} package ( s ) with incompatible licenses ` ,
2023-02-28 12:28:20 +00:00
` ${ checkOrFailIcon ( invalidLicenseChanges . unresolved . length ) } ${
invalidLicenseChanges . unresolved . length
} package ( s ) with invalid SPDX license definitions ` ,
2023-02-27 16:05:59 +00:00
` ${ checkOrWarnIcon ( invalidLicenseChanges . unlicensed . length ) } ${
invalidLicenseChanges . unlicensed . length
} package ( s ) with unknown licenses . `
]
2023-08-02 16:17:51 +02:00
: [ ] ) ,
. . . ( deniedChanges . length > 0
? [
` ${ checkOrWarnIcon ( deniedChanges . length ) } ${
deniedChanges . length
} package ( s ) denied . `
]
2024-03-12 20:47:25 +00:00
: [ ] ) ,
. . . ( config . show_openssf_scorecard && scorecardWarnings > 0
? [
` ${ checkOrWarnIcon ( scorecardWarnings ) } ${ scorecardWarnings ? scorecardWarnings : 'No' } packages with OpenSSF Scorecard issues. `
]
2023-02-27 16:05:59 +00:00
: [ ] )
] )
2023-02-28 12:28:20 +00:00
. addRaw ( 'See the Details below.' )
2022-08-01 21:07:02 +01:00
}
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
}
2022-09-26 19:14:04 +02:00
export function addScannedDependencies ( changes : Changes ) : void {
const dependencies = groupDependenciesByManifest ( changes )
const manifests = dependencies . keys ( )
2023-02-28 12:28:20 +00:00
const summary = core . summary . addHeading ( 'Scanned Manifest Files' , 2 )
2022-09-26 19:14:04 +02:00
for ( const manifest of manifests ) {
const deps = dependencies . get ( manifest )
if ( deps ) {
const dependencyNames = deps . map (
2022-09-27 11:52:15 +02:00
dependency = > ` <li> ${ dependency . name } @ ${ dependency . version } </li> `
2022-09-26 19:14:04 +02:00
)
2022-10-27 15:19:32 +00:00
summary . addDetails ( manifest , ` <ul> ${ dependencyNames . join ( '' ) } </ul> ` )
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 {
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-03-08 02:31:11 +00:00
` <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>
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
}