Replace packageurl-js with our own implementation

This commit is contained in:
Justin Holguín
2024-04-27 20:54:20 +00:00
committed by GitHub
parent 2034babb6b
commit 640617990f
16 changed files with 484 additions and 542 deletions

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
View 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
}

View File

@@ -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

View File

@@ -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`
}