Files
dependency-review-action/src/config.ts
T

190 lines
5.3 KiB
TypeScript
Raw Normal View History

2022-09-16 14:30:57 +02:00
import * as fs from 'fs'
import path from 'path'
import YAML from 'yaml'
2022-06-13 19:55:08 +02:00
import * as core from '@actions/core'
import * as z from 'zod'
import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas'
2022-11-04 09:05:45 +00:00
import {isSPDXValid, octokitClient} from './utils'
2022-10-26 09:01:43 +00:00
type ConfigurationOptionsPartial = Partial<ConfigurationOptions>
2022-11-04 09:05:45 +00:00
export async function readConfig(): Promise<ConfigurationOptions> {
const inlineConfig = readInlineConfig()
const configFile = getOptionalInput('config-file')
if (configFile !== undefined) {
const externalConfig = await readConfigFile(configFile)
2022-11-08 10:52:30 +00:00
return ConfigurationOptionsSchema.parse({
...externalConfig,
...inlineConfig
})
}
return ConfigurationOptionsSchema.parse(inlineConfig)
}
2022-11-08 09:53:36 +00:00
function readInlineConfig(): ConfigurationOptionsPartial {
const fail_on_severity = getOptionalInput('fail-on-severity')
const fail_on_scopes = parseList(getOptionalInput('fail-on-scopes'))
2022-09-22 21:34:18 +00:00
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
2022-11-09 13:16:53 +01:00
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')
2022-05-12 18:05:14 +02:00
2022-10-26 09:01:43 +00:00
validateLicenses('allow-licenses', allow_licenses)
validateLicenses('deny-licenses', deny_licenses)
2022-06-13 19:55:08 +02:00
2022-11-09 13:16:53 +01:00
const keys = {
2022-09-22 22:45:27 +00:00
fail_on_severity,
fail_on_scopes,
allow_licenses,
deny_licenses,
allow_ghsas,
license_check,
vulnerability_check,
2022-09-22 22:45:27 +00:00
base_ref,
head_ref,
comment_summary_in_pr
2022-06-13 19:55:08 +02:00
}
return Object.fromEntries(
2022-11-09 13:16:53 +01:00
Object.entries(keys).filter(([_, value]) => value !== undefined)
)
}
2022-09-16 14:30:57 +02:00
2022-11-08 09:53:36 +00:00
function getOptionalBoolean(name: string): boolean | undefined {
const value = core.getInput(name)
return value.length > 0 ? core.getBooleanInput(name) : undefined
}
function getOptionalInput(name: string): string | undefined {
const value = core.getInput(name)
return value.length > 0 ? value : undefined
}
function parseList(list: string | undefined): string[] | undefined {
if (list === undefined) {
return list
} else {
return list.split(',').map(x => x.trim())
}
}
function validateLicenses(
key: 'allow-licenses' | 'deny-licenses',
licenses: string[] | undefined
): void {
if (licenses === undefined) {
return
}
2022-11-15 22:29:00 +01:00
const invalid_licenses = licenses.filter(license => !isSPDXValid(license))
2022-11-08 09:53:36 +00:00
if (invalid_licenses.length > 0) {
2022-11-15 22:29:00 +01:00
throw new Error(`Invalid license(s) in ${key}: ${invalid_licenses}`)
2022-11-08 09:53:36 +00:00
}
}
async function readConfigFile(
filePath: string
): Promise<ConfigurationOptionsPartial> {
2022-11-09 13:17:12 +01:00
// match a remote config (e.g. 'owner/repo/filepath@someref')
const format = new RegExp(
'(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)'
)
2022-11-09 13:17:12 +01:00
let data: string
const pieces = format.exec(filePath)
2022-11-09 13:17:12 +01:00
try {
if (pieces?.groups && pieces.length === 5) {
data = await getRemoteConfig({
owner: pieces.groups.owner,
repo: pieces.groups.repo,
path: pieces.groups.path,
ref: pieces.groups.ref
})
} else {
data = fs.readFileSync(path.resolve(filePath), 'utf-8')
}
return parseConfigFile(data)
} catch (error) {
2022-11-15 22:29:00 +01:00
throw new Error(
`Unable to fetch or parse config file: ${(error as Error).message}`
)
}
}
2022-11-08 09:53:36 +00:00
function parseConfigFile(configData: string): ConfigurationOptionsPartial {
try {
const data = YAML.parse(configData)
2022-11-15 22:29:00 +01:00
// These are the options that we support where the user can provide
// either a YAML list or a comma-separated string.
const listKeys = [
'allow-licenses',
'deny-licenses',
'fail-on-scopes',
'allow-ghsas'
]
for (const key of Object.keys(data)) {
2022-11-15 22:29:00 +01:00
// strings can contain list values (e.g. 'MIT, Apache-2.0'). In this
// case we need to parse that into a list (e.g. ['MIT', 'Apache-2.0']).
if (listKeys.includes(key)) {
const val = data[key]
if (typeof val === 'string') {
data[key] = val.split(',').map(x => x.trim())
}
}
2022-11-15 22:29:00 +01:00
// perform SPDX validation
if (key === 'allow-licenses' || key === 'deny-licenses') {
validateLicenses(key, data[key])
}
// get rid of the ugly dashes from the actions conventions
if (key.includes('-')) {
data[key.replace(/-/g, '_')] = data[key]
delete data[key]
}
}
return data
} catch (error) {
throw error
}
2022-09-16 14:30:57 +02:00
}
2022-11-04 09:05:45 +00:00
async function getRemoteConfig(configOpts: {
[key: string]: string
}): Promise<string> {
2022-11-04 09:05:45 +00:00
try {
const {data} = await octokitClient(
2022-11-08 11:16:26 +00:00
'external-repo-token',
false
2022-11-04 09:05:45 +00:00
).rest.repos.getContent({
mediaType: {
format: 'raw'
},
owner: configOpts.owner,
repo: configOpts.repo,
path: configOpts.path,
ref: configOpts.ref
2022-11-04 09:05:45 +00:00
})
2022-11-09 13:17:12 +01:00
// When using mediaType.format = 'raw', the response.data is a string
// but this is not reflected in the return type of getContent, so we're
// casting the return value to a string.
2022-11-04 10:08:00 +00:00
return z.string().parse(data as unknown)
2022-11-04 09:05:45 +00:00
} catch (error) {
core.debug(error as string)
throw new Error('Error fetching remote config file')
2022-11-04 09:05:45 +00:00
}
}