Replace packageurl-js with our own implementation
This commit is contained in:
@@ -5,7 +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'
|
||||
import {parsePURL} from './purl'
|
||||
|
||||
type ConfigurationOptionsPartial = Partial<ConfigurationOptions>
|
||||
|
||||
@@ -233,7 +233,7 @@ function validatePURL(allow_dependencies_licenses: string[] | undefined): void {
|
||||
return
|
||||
}
|
||||
const invalid_purls = allow_dependencies_licenses.filter(
|
||||
purl => !isPURLValid(purl)
|
||||
purl => !parsePURL(purl).error
|
||||
)
|
||||
|
||||
if (invalid_purls.length > 0) {
|
||||
@@ -243,11 +243,3 @@ function validatePURL(allow_dependencies_licenses: string[] | undefined): void {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const isPURLValid = (purl: string): boolean => {
|
||||
try {
|
||||
return PackageURL.fromString(purl) !== null
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
37
src/deny.ts
37
src/deny.ts
@@ -1,7 +1,6 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Change} from './schemas'
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
import {parsePURL} from './utils'
|
||||
import {PackageURL, parsePURL} from './purl'
|
||||
|
||||
export async function getDeniedChanges(
|
||||
changes: Change[],
|
||||
@@ -24,6 +23,11 @@ export async function getDeniedChanges(
|
||||
|
||||
for (const denied of deniedGroups) {
|
||||
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
|
||||
@@ -40,30 +44,13 @@ export async function getDeniedChanges(
|
||||
return changesDenied
|
||||
}
|
||||
|
||||
// getNamespace returns the namespace associated with the given change.
|
||||
// it tries to get this from the package_url member, but that won't exist
|
||||
// for all changes, so as a fallback it may create a new purl based on the
|
||||
// ecosystem and name associated with the change, then extract the namespace
|
||||
// from that.
|
||||
// returns '' if there is no namespace.
|
||||
export const getNamespace = (change: Change): string => {
|
||||
let purl_str: string
|
||||
export const getNamespace = (change: Change): string | null => {
|
||||
if (change.package_url) {
|
||||
purl_str = change.package_url
|
||||
} else {
|
||||
purl_str = `pkg:${change.ecosystem}/${change.name}`
|
||||
return parsePURL(change.package_url).namespace
|
||||
}
|
||||
|
||||
try {
|
||||
const purl = parsePURL(purl_str)
|
||||
const namespace = purl.namespace
|
||||
if (namespace === undefined || namespace === null) {
|
||||
return ''
|
||||
} else {
|
||||
return namespace
|
||||
}
|
||||
} catch (e) {
|
||||
core.error(`Error parsing purl '${purl_str}': ${e}`)
|
||||
return ''
|
||||
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, parsePURL} from './utils'
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
import {isSPDXValid, octokitClient} from './utils'
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
65
src/purl.ts
Normal file
65
src/purl.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as z from 'zod'
|
||||
|
||||
// the basic purl type, containing ecosystem, namespace, name, and version.
|
||||
// other than ecosystem, 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_ECOSYSTEM = /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 = 'purl must start with "pkg:"'
|
||||
return result
|
||||
}
|
||||
const ecosystem = purl.match(PURL_ECOSYSTEM)
|
||||
if (ecosystem === null) {
|
||||
result.error = 'purl must contain an ecosystem'
|
||||
return result
|
||||
}
|
||||
result.type = ecosystem[1]
|
||||
const parts = purl.split('/')
|
||||
// the first 'part' should be 'pkg:ecosystem'
|
||||
if (parts.length < 2 || parts[1].length === 0) {
|
||||
result.error = 'purl must contain a namespace or name'
|
||||
return result
|
||||
}
|
||||
let namePlusRest: string
|
||||
if (parts.length === 2) {
|
||||
namePlusRest = parts[1]
|
||||
} else {
|
||||
result.namespace = decodeURIComponent(parts[1])
|
||||
namePlusRest = parts[2]
|
||||
}
|
||||
const name = namePlusRest.match(/([^@#?]+)[@#?]?.*/)
|
||||
if (name === null) {
|
||||
// we're done here
|
||||
return result
|
||||
}
|
||||
result.name = decodeURIComponent(name[1])
|
||||
const version = namePlusRest.match(/@([^#?]+)[#?]?.*/)
|
||||
if (version === null) {
|
||||
return result
|
||||
}
|
||||
result.version = decodeURIComponent(version[1])
|
||||
|
||||
// we don't parse subpath or attributes, so we're done here
|
||||
return result
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
|
||||
45
src/utils.ts
45
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,47 +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 fixedPurlString = addTempName(purlString)
|
||||
const purl = PackageURL.fromString(fixedPurlString)
|
||||
purl.name = ''
|
||||
return purl
|
||||
} else if ((error as Error).message === `version must be percent-encoded`) {
|
||||
core.error(
|
||||
`Version must be percent-encoded. Removing version from purl: '${purlString}.`
|
||||
)
|
||||
const fixedPurlString = removeVersion(purlString)
|
||||
const purl = parsePURL(fixedPurlString)
|
||||
purl.version = ''
|
||||
return purl
|
||||
}
|
||||
core.error(`Error parsing purl: ${purlString}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const removeVersion = (purlString: string): string => {
|
||||
// sometimes these errors are actually caused by a final '/', so try removing that first
|
||||
if (purlString.endsWith('/')) {
|
||||
return purlString.substring(0, purlString.length - 1)
|
||||
}
|
||||
const idx = purlString.lastIndexOf('@')
|
||||
return purlString.substring(0, idx)
|
||||
}
|
||||
|
||||
export const addTempName = (purlString: string): string => {
|
||||
if (purlString.endsWith('/')) {
|
||||
return `${purlString}TEMP_NAME`
|
||||
}
|
||||
return `${purlString}/TEMP_NAME`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user