add new parameter warn_only

This commit is contained in:
tgrall
2023-06-12 11:26:44 +02:00
23 changed files with 6541 additions and 9314 deletions
+32 -2
View File
@@ -5,6 +5,7 @@ import * as core from '@actions/core'
import * as z from 'zod'
import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas'
import {isSPDXValid, octokitClient} from './utils'
import {PackageURL} from 'packageurl-js'
type ConfigurationOptionsPartial = Partial<ConfigurationOptions>
@@ -29,13 +30,18 @@ function readInlineConfig(): ConfigurationOptionsPartial {
const fail_on_scopes = parseList(getOptionalInput('fail-on-scopes'))
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
const allow_dependencies_licenses = parseList(
getOptionalInput('allow-dependencies-licenses')
)
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
const license_check = getOptionalBoolean('license-check')
const vulnerability_check = getOptionalBoolean('vulnerability-check')
const base_ref = getOptionalInput('base-ref')
const head_ref = getOptionalInput('head-ref')
const comment_summary_in_pr = getOptionalBoolean('comment-summary-in-pr')
const warn_only = getOptionalBoolean('comment-summary-in-pr')
validatePURL(allow_dependencies_licenses)
validateLicenses('allow-licenses', allow_licenses)
validateLicenses('deny-licenses', deny_licenses)
@@ -44,12 +50,14 @@ function readInlineConfig(): ConfigurationOptionsPartial {
fail_on_scopes,
allow_licenses,
deny_licenses,
allow_dependencies_licenses,
allow_ghsas,
license_check,
vulnerability_check,
base_ref,
head_ref,
comment_summary_in_pr
comment_summary_in_pr,
warn_only
}
return Object.fromEntries(
@@ -130,7 +138,8 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
'allow-licenses',
'deny-licenses',
'fail-on-scopes',
'allow-ghsas'
'allow-ghsas',
'allow-dependencies-licenses'
]
for (const key of Object.keys(data)) {
@@ -149,6 +158,11 @@ function parseConfigFile(configData: string): ConfigurationOptionsPartial {
validateLicenses(key, data[key])
}
// validate purls from the allow-dependencies-licenses
if (key === 'allow-dependencies-licenses') {
validatePURL(data[key])
}
// get rid of the ugly dashes from the actions conventions
if (key.includes('-')) {
data[key.replace(/-/g, '_')] = data[key]
@@ -187,3 +201,19 @@ async function getRemoteConfig(configOpts: {
throw new Error('Error fetching remote config file')
}
}
function validatePURL(allow_dependencies_licenses: string[] | undefined): void {
//validate that the provided elements of the string are in valid purl format
if (allow_dependencies_licenses === undefined) {
return
}
const invalid_purls = allow_dependencies_licenses.filter(
purl => !PackageURL.fromString(purl)
)
if (invalid_purls.length > 0) {
throw new Error(
`Invalid purl(s) in allow-dependencies-licenses: ${invalid_purls}`
)
}
return
}
+26 -4
View File
@@ -1,9 +1,14 @@
import * as core from '@actions/core'
import * as githubUtils from '@actions/github/lib/utils'
import * as retry from '@octokit/plugin-retry'
import {Changes, ChangesSchema} from './schemas'
import {
ChangesSchema,
ComparisonResponse,
ComparisonResponseSchema
} from './schemas'
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry)
const SnapshotWarningsHeader = 'x-github-dependency-graph-snapshot-warnings'
const octo = new retryingOctokit(
githubUtils.getOctokitOptions(core.getInput('repo-token', {required: true}))
)
@@ -18,14 +23,31 @@ export async function compare({
repo: string
baseRef: string
headRef: string
}): Promise<Changes> {
}): Promise<ComparisonResponse> {
let snapshot_warnings = ''
const changes = await octo.paginate(
'GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}',
{
method: 'GET',
url: '/repos/{owner}/{repo}/dependency-graph/compare/{basehead}',
owner,
repo,
basehead: `${baseRef}...${headRef}`
},
response => {
if (
response.headers[SnapshotWarningsHeader] &&
typeof response.headers[SnapshotWarningsHeader] === 'string'
) {
snapshot_warnings = Buffer.from(
response.headers[SnapshotWarningsHeader],
'base64'
).toString('utf-8')
}
return ChangesSchema.parse(response.data)
}
)
return ChangesSchema.parse(changes)
return ComparisonResponseSchema.parse({
changes,
snapshot_warnings
})
}
+36 -1
View File
@@ -1,17 +1,19 @@
import spdxSatisfies from 'spdx-satisfies'
import {Change, Changes} from './schemas'
import {isSPDXValid, octokitClient} from './utils'
import {PackageURL} from 'packageurl-js'
/**
* Loops through a list of changes, filtering and returning the
* ones that don't conform to the licenses allow/deny lists.
* It will also filter out the changes which are defined in the licenseExclusions list.
*
* Keep in mind that we don't let users specify both an allow and a deny
* list in their config files, so this code works under the assumption that
* one of the two list parameters will be empty. If both lists are provided,
* we will ignore the deny list.
* @param {Change[]} changes The list of changes to filter.
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
* @param { { allow?: string[], deny?: string[], licenseExclusions?: string[]}} licenses An object with `allow`/`deny`/`licenseExclusions` keys, each containing a list of licenses.
* @returns {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
*/
export type InvalidLicenseChangeTypes =
@@ -24,11 +26,44 @@ export async function getInvalidLicenseChanges(
licenses: {
allow?: string[]
deny?: string[]
licenseExclusions?: string[]
}
): Promise<InvalidLicenseChanges> {
const {allow, deny} = licenses
const licenseExclusions = licenses.licenseExclusions?.map(
(pkgUrl: string) => {
return PackageURL.fromString(pkgUrl)
}
)
const groupedChanges = await groupChanges(changes)
// Takes the changes from the groupedChanges object and filters out the ones that are part of the exclusions list
// It does by creating a new PackageURL object from the change and comparing it to the exclusions list
groupedChanges.licensed = groupedChanges.licensed.filter(change => {
if (change.package_url.length === 0) {
return true
}
const changeAsPackageURL = PackageURL.fromString(change.package_url)
// We want to find if the licenseExclussion list contains the PackageURL of the Change
// If it does, we want to filter it out and therefore return false
// If it doesn't, we want to keep it and therefore return true
if (
licenseExclusions !== null &&
licenseExclusions !== undefined &&
licenseExclusions.findIndex(
exclusion =>
exclusion.type === changeAsPackageURL.type &&
exclusion.name === changeAsPackageURL.name
) !== -1
) {
return false
} else {
return true
}
})
const licensedChanges: Changes = groupedChanges.licensed
const invalidLicenseChanges: InvalidLicenseChanges = {
+14 -5
View File
@@ -16,28 +16,32 @@ import {getRefs} from './git-refs'
import {groupDependenciesByManifest} from './utils'
import {commentPr} from './comment-pr'
import { warn } from 'console'
async function run(): Promise<void> {
try {
const config = await readConfig()
const refs = getRefs(config, github.context)
const changes = await dependencyGraph.compare({
const comparison = await dependencyGraph.compare({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
baseRef: refs.base,
headRef: refs.head
})
const changes = comparison.changes
const snapshot_warnings = comparison.snapshot_warnings
if (!changes) {
core.info('No Dependency Changes found. Skipping Dependency Review.')
return
}
// config.fail_on_severity
config.fail_on_severity
const failOnSeverityParams = config.fail_on_severity
const failOnVulnerability = config.fail_on_severity !== 'none'
const failOnVulnerability = !config.warn_only // if warn only is true the system should not fail on vulnerabilities
let minSeverity: Severity = 'low'
if (failOnSeverityParams !== 'none') {
if (failOnSeverityParams) {
minSeverity = failOnSeverityParams
}
@@ -61,7 +65,8 @@ async function run(): Promise<void> {
filteredChanges,
{
allow: config.allow_licenses,
deny: config.deny_licenses
deny: config.deny_licenses,
licenseExclusions: config.allow_dependencies_licenses
}
)
@@ -71,6 +76,10 @@ async function run(): Promise<void> {
config
)
if (snapshot_warnings) {
summary.addSnapshotWarnings(snapshot_warnings)
}
if (config.vulnerability_check) {
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
printVulnerabilitiesBlock(
+9 -3
View File
@@ -4,8 +4,7 @@ export const FAIL_ON_SEVERITIES = [
'critical',
'high',
'moderate',
'low',
'none'
'low'
] as const
export const SEVERITIES = ['critical', 'high', 'moderate', 'low'] as const
export const SCOPES = ['unknown', 'runtime', 'development'] as const
@@ -48,13 +47,15 @@ export const ConfigurationOptionsSchema = z
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
allow_licenses: z.array(z.string()).optional(),
deny_licenses: z.array(z.string()).optional(),
allow_dependencies_licenses: z.array(z.string()).optional(),
allow_ghsas: z.array(z.string()).default([]),
license_check: z.boolean().default(true),
vulnerability_check: z.boolean().default(true),
config_file: z.string().optional(),
base_ref: z.string().optional(),
head_ref: z.string().optional(),
comment_summary_in_pr: z.boolean().default(false)
comment_summary_in_pr: z.boolean().default(false),
warn_only: z.boolean().default(false)
})
.superRefine((config, context) => {
if (config.allow_licenses && config.deny_licenses) {
@@ -81,9 +82,14 @@ export const ConfigurationOptionsSchema = z
})
export const ChangesSchema = z.array(ChangeSchema)
export const ComparisonResponseSchema = z.object({
changes: z.array(ChangeSchema),
snapshot_warnings: z.string()
})
export type Change = z.infer<typeof ChangeSchema>
export type Changes = z.infer<typeof ChangesSchema>
export type ComparisonResponse = z.infer<typeof ComparisonResponseSchema>
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
export type FailOnSeverity = z.infer<typeof FailOnSeveritySchema>
export type Severity = z.infer<typeof SeveritySchema>
+24
View File
@@ -143,6 +143,13 @@ export function addLicensesToSummary(
`<strong>Denied Licenses</strong>: ${config.deny_licenses.join(', ')}`
)
}
if (config.allow_dependencies_licenses) {
core.summary.addQuote(
`<strong>Excluded from license check</strong>: ${config.allow_dependencies_licenses.join(
', '
)}`
)
}
core.debug(
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
@@ -215,6 +222,23 @@ export function addScannedDependencies(changes: Changes): void {
}
}
export function addSnapshotWarnings(warnings: string): void {
// For now, we want to ignore warnings that just complain
// about missing snapshots on the head SHA. This is a product
// decision to avoid presenting warnings to users who simply
// don't use snapshots.
const ignore_regex = new RegExp(/No.*snapshot.*found.*head.*/, 'i')
if (ignore_regex.test(warnings)) {
return
}
core.summary.addHeading('Snapshot Warnings', 2)
core.summary.addQuote(`${icons.warning}: ${warnings}`)
core.summary.addRaw(
'Re-running this action after a short time may resolve the issue. See the documentation for more information and troubleshooting advice.'
)
}
function countLicenseIssues(
invalidLicenseChanges: InvalidLicenseChanges
): number {
+3 -1
View File
@@ -8,7 +8,9 @@ export function groupDependenciesByManifest(
): Map<string, Changes> {
const dependencies: Map<string, Changes> = new Map()
for (const change of changes) {
const manifestName = change.manifest
// If the manifest is null or empty, give it a name now to avoid
// breaking the HTML rendering later
const manifestName = change.manifest || 'Unnamed Manifest'
if (dependencies.get(manifestName) === undefined) {
dependencies.set(manifestName, [])