import * as core from '@actions/core' import * as dependencyGraph from './dependency-graph' import * as github from '@actions/github' import styles from 'ansi-styles' import {RequestError} from '@octokit/request-error' import {Change, Severity, Scope} from './schemas' import {readConfig} from '../src/config' import { filterChangesBySeverity, filterChangesByScopes, filterOutAllowedAdvisories } from '../src/filter' import {getDeniedLicenseChanges} from './licenses' import * as summary from './summary' import {getRefs} from './git-refs' async function run(): Promise { try { let failed = false const config = readConfig() const refs = getRefs(config, github.context) const changes = await dependencyGraph.compare({ owner: github.context.repo.owner, repo: github.context.repo.repo, baseRef: refs.base, headRef: refs.head }) const minSeverity = config.fail_on_severity const licenses = { allow: config.allow_licenses, deny: config.deny_licenses } const scopes = config.fail_on_scopes const scopedChanges = filterChangesByScopes(scopes as Scope[], changes) const allowedGhsas: string[] = config.allow_ghsas || [] const filteredChanges = filterOutAllowedAdvisories( allowedGhsas, scopedChanges ) const addedChanges = filterChangesBySeverity( minSeverity as Severity, filteredChanges ).filter( change => change.change_type === 'added' && change.vulnerabilities !== undefined && change.vulnerabilities.length > 0 ) const [licenseErrors, unknownLicenses] = getDeniedLicenseChanges( filteredChanges, licenses ) summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses) if (addedChanges.length > 0) { for (const change of addedChanges) { printChangeVulnerabilities(change) } failed = true } summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity || '') if (licenseErrors.length > 0) { printLicensesError(licenseErrors) core.setFailed('Dependency review detected incompatible licenses.') } printNullLicenses(unknownLicenses) summary.addLicensesToSummary(licenseErrors, unknownLicenses, config) printScannedDependencies(changes) 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.` ) } } 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) { 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` ) } else { if (error instanceof Error) { core.setFailed(error.message) } else { core.setFailed('Unexpected fatal error') } } } finally { await core.summary.write() } } function printChangeVulnerabilities(change: Change): void { for (const vuln of change.vulnerabilities) { core.info( `${styles.bold.open}${change.manifest} » ${change.name}@${ change.version }${styles.bold.close} – ${vuln.advisory_summary} ${renderSeverity( vuln.severity )}` ) core.info(` ↪ ${vuln.advisory_url}`) } } 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}` } function renderChangeType( change_type: 'added' | 'modified' | 'removed' ): string { const color = ( { added: 'green', modified: 'yellow', removed: 'red' } as const )[change_type] return `${styles.color[color].open}${change_type}${styles.color[color].close}` } function printLicensesError(changes: Change[]): void { if (changes.length === 0) { return } core.info('\nThe following dependencies have incompatible licenses:\n') 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}` ) } } function printNullLicenses(changes: Change[]): void { if (changes.length === 0) { return } 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}` ) } } function printScannedDependencies(changes: Change[]): void { core.group('Dependency changes', async () => { // group changes by manifest const dependencies: Record = {} 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(`${styles.bold.open}${manifestName}`) for (const change of manifestChanges) { core.info( `${renderChangeType(change.change_type)}\t ${change.name}@${ change.version }` ) } } }) } run()