Merge branch 'main' into deps-dev-v3
This commit is contained in:
@@ -5,7 +5,6 @@ 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>
|
||||
|
||||
@@ -53,7 +52,6 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
'warn-on-openssf-scorecard-level'
|
||||
)
|
||||
|
||||
validatePURL(allow_dependencies_licenses)
|
||||
validateLicenses('allow-licenses', allow_licenses)
|
||||
validateLicenses('deny-licenses', deny_licenses)
|
||||
|
||||
@@ -184,11 +182,6 @@ 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]
|
||||
@@ -227,19 +220,3 @@ 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
|
||||
}
|
||||
|
||||
30
src/deny.ts
30
src/deny.ts
@@ -1,6 +1,6 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Change} from './schemas'
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
import {PackageURL, parsePURL} from './purl'
|
||||
|
||||
export async function getDeniedChanges(
|
||||
changes: Change[],
|
||||
@@ -11,12 +11,10 @@ export async function getDeniedChanges(
|
||||
|
||||
let hasDeniedPackage = false
|
||||
for (const change of changes) {
|
||||
const changedPackage = PackageURL.fromString(change.package_url)
|
||||
|
||||
for (const denied of deniedPackages) {
|
||||
if (
|
||||
(!denied.version || changedPackage.version === denied.version) &&
|
||||
changedPackage.name === denied.name
|
||||
(!denied.version || change.version === denied.version) &&
|
||||
change.name === denied.name
|
||||
) {
|
||||
changesDenied.push(change)
|
||||
hasDeniedPackage = true
|
||||
@@ -24,10 +22,13 @@ export async function getDeniedChanges(
|
||||
}
|
||||
|
||||
for (const denied of deniedGroups) {
|
||||
if (
|
||||
changedPackage.namespace &&
|
||||
changedPackage.namespace === denied.namespace
|
||||
) {
|
||||
const namespace = getNamespace(change)
|
||||
if (!denied.namespace) {
|
||||
core.error(
|
||||
`Denied group represented by '${denied.original}' does not have a namespace. The format should be 'pkg:<type>/<namespace>/'.`
|
||||
)
|
||||
}
|
||||
if (namespace && namespace === denied.namespace) {
|
||||
changesDenied.push(change)
|
||||
hasDeniedPackage = true
|
||||
}
|
||||
@@ -42,3 +43,14 @@ export async function getDeniedChanges(
|
||||
|
||||
return changesDenied
|
||||
}
|
||||
|
||||
export const getNamespace = (change: Change): string | null => {
|
||||
if (change.package_url) {
|
||||
return parsePURL(change.package_url).namespace
|
||||
}
|
||||
const matches = change.name.match(/([^:/]+)[:/]/)
|
||||
if (matches && matches.length > 1) {
|
||||
return matches[1]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import spdxSatisfies from 'spdx-satisfies'
|
||||
import {Change, Changes} from './schemas'
|
||||
import {isSPDXValid, octokitClient} from './utils'
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
import {parsePURL} from './purl'
|
||||
|
||||
/**
|
||||
* Loops through a list of changes, filtering and returning the
|
||||
@@ -32,7 +32,7 @@ export async function getInvalidLicenseChanges(
|
||||
const {allow, deny} = licenses
|
||||
const licenseExclusions = licenses.licenseExclusions?.map(
|
||||
(pkgUrl: string) => {
|
||||
return PackageURL.fromString(encodeURI(pkgUrl))
|
||||
return parsePURL(pkgUrl)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -45,9 +45,7 @@ export async function getInvalidLicenseChanges(
|
||||
return true
|
||||
}
|
||||
|
||||
const changeAsPackageURL = PackageURL.fromString(
|
||||
encodeURI(change.package_url)
|
||||
)
|
||||
const changeAsPackageURL = parsePURL(encodeURI(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
|
||||
|
||||
72
src/purl.ts
Normal file
72
src/purl.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
// the basic purl type, containing type, namespace, name, and version.
|
||||
// other than type, all fields are nullable. this is for maximum flexibility
|
||||
// at the cost of strict adherence to the package-url spec.
|
||||
export const PurlSchema = z.object({
|
||||
type: z.string(),
|
||||
namespace: z.string().nullable(),
|
||||
name: z.string().nullable(), // name is nullable for deny-groups
|
||||
version: z.string().nullable(),
|
||||
original: z.string(),
|
||||
error: z.string().nullable()
|
||||
})
|
||||
|
||||
export type PackageURL = z.infer<typeof PurlSchema>
|
||||
|
||||
const PURL_TYPE = /pkg:([a-zA-Z0-9-_]+)\/.*/
|
||||
|
||||
export function parsePURL(purl: string): PackageURL {
|
||||
const result: PackageURL = {
|
||||
type: '',
|
||||
namespace: null,
|
||||
name: null,
|
||||
version: null,
|
||||
original: purl,
|
||||
error: null
|
||||
}
|
||||
if (!purl.startsWith('pkg:')) {
|
||||
result.error = 'package-url must start with "pkg:"'
|
||||
return result
|
||||
}
|
||||
const type = purl.match(PURL_TYPE)
|
||||
if (!type) {
|
||||
result.error = 'package-url must contain a type'
|
||||
return result
|
||||
}
|
||||
result.type = type[1]
|
||||
const parts = purl.split('/')
|
||||
// the first 'part' should be 'pkg:ecosystem'
|
||||
if (parts.length < 2 || !parts[1]) {
|
||||
result.error = 'package-url must contain a namespace or name'
|
||||
return result
|
||||
}
|
||||
let namePlusRest: string
|
||||
if (parts.length === 2) {
|
||||
namePlusRest = parts[1]
|
||||
} else {
|
||||
result.namespace = decodeURIComponent(parts[1])
|
||||
// Add back the '/'s to the rest of the parts, in case there are any more.
|
||||
// This may violate the purl spec, but people do it and it can be parsed
|
||||
// without ambiguity.
|
||||
namePlusRest = parts.slice(2).join('/')
|
||||
}
|
||||
const name = namePlusRest.match(/([^@#?]+)[@#?]?.*/)
|
||||
if (!result.namespace && !name) {
|
||||
result.error = 'package-url must contain a namespace or name'
|
||||
return result
|
||||
}
|
||||
if (!name) {
|
||||
// we're done here
|
||||
return result
|
||||
}
|
||||
result.name = decodeURIComponent(name[1])
|
||||
const version = namePlusRest.match(/@([^#?]+)[#?]?.*/)
|
||||
if (!version) {
|
||||
return result
|
||||
}
|
||||
result.version = decodeURIComponent(version[1])
|
||||
|
||||
// we don't parse subpath or attributes, so we're done here
|
||||
return result
|
||||
}
|
||||
@@ -1,13 +1,65 @@
|
||||
import * as z from 'zod'
|
||||
import {parsePURL} from './utils'
|
||||
import {parsePURL} from './purl'
|
||||
|
||||
export const SEVERITIES = ['critical', 'high', 'moderate', 'low'] as const
|
||||
export const SCOPES = ['unknown', 'runtime', 'development'] as const
|
||||
|
||||
export const SeveritySchema = z.enum(SEVERITIES).default('low')
|
||||
|
||||
const PackageURL = z.string().transform(purlString => {
|
||||
return parsePURL(purlString)
|
||||
const PackageURL = z
|
||||
.string()
|
||||
.transform(purlString => {
|
||||
return parsePURL(purlString)
|
||||
})
|
||||
.superRefine((purl, context) => {
|
||||
if (purl.error) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Error parsing package-url: ${purl.error}`
|
||||
})
|
||||
}
|
||||
if (!purl.name) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Error parsing package-url: name is required`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const PackageURLWithNamespace = z
|
||||
.string()
|
||||
.transform(purlString => {
|
||||
return parsePURL(purlString)
|
||||
})
|
||||
.superRefine((purl, context) => {
|
||||
if (purl.error) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Error parsing purl: ${purl.error}`
|
||||
})
|
||||
}
|
||||
if (purl.namespace === null) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `package-url must have a namespace, and the namespace must be followed by '/'`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const PackageURLString = z.string().superRefine((value, context) => {
|
||||
const purl = parsePURL(value)
|
||||
if (purl.error) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Error parsing package-url: ${purl.error}`
|
||||
})
|
||||
}
|
||||
if (!purl.name) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Error parsing package-url: name is required`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const ChangeSchema = z.object({
|
||||
@@ -45,10 +97,10 @@ 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_dependencies_licenses: z.array(PackageURLString).optional(),
|
||||
allow_ghsas: z.array(z.string()).default([]),
|
||||
deny_packages: z.array(PackageURL).default([]),
|
||||
deny_groups: z.array(PackageURL).default([]),
|
||||
deny_groups: z.array(PackageURLWithNamespace).default([]),
|
||||
license_check: z.boolean().default(true),
|
||||
vulnerability_check: z.boolean().default(true),
|
||||
config_file: z.string().optional(),
|
||||
|
||||
19
src/utils.ts
19
src/utils.ts
@@ -1,7 +1,6 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Octokit} from 'octokit'
|
||||
import spdxParse from 'spdx-expression-parse'
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
import {Changes} from './schemas'
|
||||
|
||||
export function groupDependenciesByManifest(
|
||||
@@ -69,21 +68,3 @@ export function octokitClient(token = 'repo-token', required = true): Octokit {
|
||||
|
||||
return new Octokit(opts)
|
||||
}
|
||||
|
||||
export const parsePURL = (purlString: string): PackageURL => {
|
||||
try {
|
||||
return PackageURL.fromString(purlString)
|
||||
} catch (error) {
|
||||
if (
|
||||
(error as Error).message ===
|
||||
`purl is missing the required "name" component.`
|
||||
) {
|
||||
//packageurl-js does not support empty names, so will manually override it for deny-groups
|
||||
//https://github.com/package-url/packageurl-js/blob/master/src/package-url.js#L216
|
||||
const purl = PackageURL.fromString(`${purlString}TEMP_NAME`)
|
||||
purl.name = ''
|
||||
return purl
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user