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'
2022-09-15 20:03:27 +00:00
import { Change , Severity , Scope } 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 ,
filterOutAllowedAdvisories
} from '../src/filter'
2022-06-08 17:45:42 +02:00
import { getDeniedLicenseChanges } from './licenses'
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
async function run () : Promise < void > {
try {
2022-07-21 15:47:05 -04:00
const config = readConfig ()
const refs = getRefs ( config , github . context )
2022-03-31 18:31:39 +02:00
const changes = await dependencyGraph . compare ({
owner : github.context.repo.owner ,
repo : github.context.repo.repo ,
2022-07-21 15:47:05 -04:00
baseRef : refs.base ,
headRef : refs.head
2022-03-31 18:31:39 +02:00
})
2022-07-04 20:05:43 +09:00
const minSeverity = config . fail_on_severity
2022-03-31 18:31:39 +02:00
2022-07-04 20:05:43 +09:00
const licenses = {
2022-06-08 17:45:42 +02:00
allow : config.allow_licenses ,
deny : config.deny_licenses
}
2022-09-15 20:03:27 +00:00
const scopes = config . fail_on_scopes
const scopedChanges = filterChangesByScopes ( scopes as Scope [], changes )
2022-09-22 22:25:21 +00:00
const allowedGhsas : string [] = config . allow_ghsas || []
const filteredChanges = filterOutAllowedAdvisories (
allowedGhsas ,
scopedChanges
)
2022-08-01 21:07:02 +01:00
const addedChanges = filterChangesBySeverity (
2022-06-06 18:06:48 +02:00
minSeverity as Severity ,
2022-09-22 22:25:21 +00:00
filteredChanges
2022-08-01 21:07:02 +01:00
). filter (
change =>
2022-03-31 18:31:39 +02:00
change . change_type === 'added' &&
change . vulnerabilities !== undefined &&
change . vulnerabilities . length > 0
2022-08-01 21:07:02 +01:00
)
2022-03-31 18:31:39 +02:00
2022-07-04 20:05:43 +09:00
const [ licenseErrors , unknownLicenses ] = getDeniedLicenseChanges (
2022-09-22 22:25:21 +00:00
filteredChanges ,
2022-06-14 13:54:27 +02:00
licenses
)
2022-08-01 21:07:02 +01:00
summary . addSummaryToSummary ( addedChanges , licenseErrors , unknownLicenses )
summary . addChangeVulnerabilitiesToSummary ( addedChanges , minSeverity || '' )
summary . addLicensesToSummary ( licenseErrors , unknownLicenses , config )
2022-09-26 12:21:24 +02:00
printVulnerabilitiesBlock ( addedChanges , minSeverity || 'low' )
printLicensesBlock ( licenseErrors , unknownLicenses )
2022-09-22 16:49:45 +02:00
2022-09-26 12:21:24 +02:00
printScannedDependencies ( changes )
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 (
`Dependency review is not supported on this repository. Please ensure that Dependency graph is enabled, see https://github.com/ ${ github . context . repo . owner } / ${ github . context . repo . repo } /settings/security_analysis`
)
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 (
addedChanges : Change [],
minSeverity : Severity
) : void {
let failed = false
core . group ( 'Vulnerabilities' , async () => {
if ( addedChanges . length > 0 ) {
for ( const change of addedChanges ) {
printChangeVulnerabilities ( change )
}
failed = true
}
if ( failed ) {
core . setFailed ( 'Dependency review detected vulnerable packages.' )
} 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-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 . manifest } @ ${ change . version }${ styles . color [ color ]. close } `
2022-09-26 11:35:05 +02:00
}
2022-09-26 12:21:24 +02:00
function printScannedDependencies ( changes : Change []) : void {
core . group ( 'Dependency changes' , async () => {
// group changes by manifest
const dependencies : Record < string , Change [] > = {}
for ( const change of changes ) {
if ( dependencies [ change . manifest ] === undefined ) {
dependencies [ change . manifest ] = []
}
dependencies [ change . manifest ]. push ( change )
}
for ( const [ manifestName , manifestChanges ] of Object . entries (
dependencies
)) {
core . info ( `File: ${ styles . bold . open }${ manifestName }${ styles . bold . close } ` )
for ( const change of manifestChanges ) {
core . info ( ` ${ renderScannedDependency ( change ) } ` )
}
}
})
}
function printLicensesBlock (
licenseErrors : Change [],
unknownLicenses : Change []
) : void {
core . group ( 'Licenses' , async () => {
if ( licenseErrors . length > 0 ) {
printLicensesError ( licenseErrors )
core . setFailed ( 'Dependency review detected incompatible licenses.' )
}
printNullLicenses ( unknownLicenses )
})
}
2022-07-04 20:09:44 +09:00
function printLicensesError ( changes : Change []) : void {
if ( changes . length === 0 ) {
2022-06-08 17:45:42 +02:00
return
2022-06-06 18:06:48 +02:00
}
2022-06-14 13:54:27 +02:00
core . info ( '\nThe following dependencies have incompatible licenses:\n' )
2022-06-06 17:31:33 +02:00
for ( const change of changes ) {
core . info (
2022-06-06 17:46:22 +02:00
` ${ styles . bold . open }${ change . manifest } » ${ change . name } @ ${ change . version }${ styles . bold . close } – License: ${ styles . color . red . open }${ change . license }${ styles . color . red . close } `
2022-06-06 17:31:33 +02:00
)
}
}
2022-07-04 20:05:43 +09:00
function printNullLicenses ( changes : Change []) : void {
2022-06-16 06:03:16 +02:00
if ( changes . length === 0 ) {
return
}
2022-06-14 13:54:27 +02:00
core . info ( '\nWe could not detect a license for the following dependencies:\n' )
for ( const change of changes ) {
core . info (
` ${ styles . bold . open }${ change . manifest } » ${ change . name } @ ${ change . version }${ styles . bold . close } `
)
}
}
2022-03-31 18:31:39 +02:00
run ()