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

178 lines
5.2 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,
2022-09-21 16:50:02 +02:00
SeveritySchema,
SCOPES
} from './schemas'
2022-11-04 09:05:45 +00:00
import {isSPDXValid, octokitClient} from './utils'
2022-10-26 09:01:43 +00:00
type licenseKey = 'allow-licenses' | 'deny-licenses'
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
}
2022-05-12 18:05:14 +02:00
2022-09-15 18:48:58 +00:00
function parseList(list: string | undefined): string[] | undefined {
if (list === undefined) {
return list
} else {
return list.split(',').map(x => x.trim())
}
}
2022-10-26 09:01:43 +00:00
function validateLicenses(
key: licenseKey,
licenses: string[] | undefined
): void {
if (licenses === undefined) {
return
}
2022-10-27 13:09:37 +00:00
const invalid_licenses = licenses.filter(license => !isSPDXValid(license))
2022-10-26 09:01:43 +00:00
if (invalid_licenses.length > 0) {
throw new Error(
`Invalid license(s) in ${key}: ${invalid_licenses.join(', ')}`
)
}
}
2022-11-04 09:05:45 +00:00
export async function readConfig(): Promise<ConfigurationOptions> {
const externalToken = getOptionalInput('config-repository-token')
const remoteConfigFile = getOptionalInput('remote-config-file')
const repoConfigFile = getOptionalInput('config-file')
const inlineConfig = readInlineConfig()
let remoteConfig: ConfigurationOptions = {}
let repoConfig: ConfigurationOptions = {}
if (externalToken !== undefined) {
if (remoteConfigFile === undefined) {
throw new Error('Missing required parameter: remote-config-file.')
}
remoteConfig = readConfigFile(await getRemoteConfig(remoteConfigFile))
}
2022-11-04 09:05:45 +00:00
if (repoConfigFile !== undefined) {
repoConfig = readConfigFile(getRepoConfig(repoConfigFile))
}
// the reasoning behind reading the inline config when an external
// config file is provided is that we still want to allow users to
// pass inline options in the presence of an external config file.
// TO DO check order of precedence
return {...inlineConfig, ...remoteConfig, ...repoConfig}
}
export function readInlineConfig(): ConfigurationOptions {
const fail_on_severity = SeveritySchema.parse(
getOptionalInput('fail-on-severity')
)
2022-09-15 18:48:58 +00:00
const fail_on_scopes = z
.array(z.enum(SCOPES))
.default(['runtime'])
.parse(parseList(getOptionalInput('fail-on-scopes')))
2022-09-21 16:50:02 +02:00
2022-09-22 21:34:18 +00:00
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
2022-05-12 18:05:14 +02:00
if (allow_licenses !== undefined && deny_licenses !== undefined) {
throw new Error("Can't specify both allow_licenses and deny_licenses")
2022-06-13 19:55:08 +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-09-22 21:34:18 +00:00
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
const license_check = z
.boolean()
.default(true)
.parse(getOptionalBoolean('license-check'))
const vulnerability_check = z
.boolean()
.default(true)
.parse(getOptionalBoolean('vulnerability-check'))
2022-10-28 22:08:55 +02:00
if (license_check === false && vulnerability_check === false) {
throw new Error("Can't disable both license-check and vulnerability-check")
}
const base_ref = getOptionalInput('base-ref')
const head_ref = getOptionalInput('head-ref')
return {
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
2022-06-13 19:55:08 +02:00
}
}
2022-09-16 14:30:57 +02:00
2022-11-04 09:05:45 +00:00
export function readConfigFile(configData: string): ConfigurationOptions {
const data = YAML.parse(configData)
for (const key of Object.keys(data)) {
2022-10-26 09:01:43 +00:00
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]
}
}
const values = ConfigurationOptionsSchema.parse(data)
2022-09-16 14:30:57 +02:00
return values
}
2022-11-04 09:05:45 +00:00
function getRepoConfig(filePath: string): string {
try {
return fs.readFileSync(path.resolve(filePath), 'utf-8')
} catch (error) {
throw error
}
}
async function getRemoteConfig(configFile: string): Promise<string> {
const format = new RegExp(
'(?<owner>[^/]+)/(?<repo>[^/]+)/(?<path>[^@]+)@(?<ref>.*)'
)
const pieces = format.exec(configFile)
if (pieces === null || pieces.groups === undefined || pieces.length < 5) {
throw new Error('Invalid remote config file format.')
}
try {
const {data} = await octokitClient(
'config-repository-token'
).rest.repos.getContent({
mediaType: {
format: 'raw'
},
owner: pieces.groups.owner,
repo: pieces.groups.repo,
path: pieces.groups.path,
ref: pieces.groups.ref
})
if (data === undefined) {
throw new Error('Invalid content')
}
2022-11-04 10:08:00 +00: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.
return z.string().parse(data as unknown)
2022-11-04 09:05:45 +00:00
} catch (error) {
throw error
}
}