From a318e62c6c5bedf19be93e61b8cb627162b77023 Mon Sep 17 00:00:00 2001 From: Brandon Teng Date: Tue, 16 Apr 2024 12:44:51 -0500 Subject: [PATCH] using packageurl-js to parse packages and groups from config --- __tests__/deny.test.ts | 59 ++++++++++++++++++------ __tests__/fixtures/create-test-change.ts | 18 +++++++- src/config.ts | 23 +++++++-- src/deny.ts | 21 +++++---- src/schemas.ts | 13 +++++- src/utils.ts | 19 ++++++++ 6 files changed, 122 insertions(+), 31 deletions(-) diff --git a/__tests__/deny.test.ts b/__tests__/deny.test.ts index e5888b9..d46fd77 100644 --- a/__tests__/deny.test.ts +++ b/__tests__/deny.test.ts @@ -1,6 +1,10 @@ import {expect, jest, test} from '@jest/globals' import {Change, Changes} from '../src/schemas' -import {createTestChange} from './fixtures/create-test-change' +import { + createTestChange, + createTestGroupPURLs, + createTestPackagePURLs +} from './fixtures/create-test-change' import {getDeniedChanges} from '../src/deny' jest.mock('@actions/core') @@ -47,20 +51,24 @@ beforeEach(async () => { test('denies packages from the deny packages list', async () => { const changes: Changes = [npmChange, rubyChange] - const deniedChanges = await getDeniedChanges(changes, [ + const deniedPackages = createTestPackagePURLs([ 'pkg:gem/actionsomething@3.2.0' ]) + const deniedChanges = await getDeniedChanges(changes, deniedPackages) expect(deniedChanges[0]).toBe(rubyChange) expect(deniedChanges.length).toEqual(1) }) test('denies packages only for the specified version from deny packages list', async () => { - const packageWithDifferentVersion = 'pkg:npm/lodash@1.2.3' - const changes: Changes = [npmChange] - const deniedChanges = await getDeniedChanges(changes, [ - packageWithDifferentVersion + const deniedPackageWithDifferentVersion = createTestPackagePURLs([ + 'pkg:npm/lodash@1.2.3' ]) + const changes: Changes = [npmChange] + const deniedChanges = await getDeniedChanges( + changes, + deniedPackageWithDifferentVersion + ) expect(deniedChanges.length).toEqual(0) }) @@ -71,30 +79,51 @@ test('if no specified version from deny packages list, it will treat package as createTestChange({name: 'lodash', version: '4.5.6'}), createTestChange({name: 'lodash', version: '7.8.9'}) ] - const denyAllLodashVersions = 'pkg:npm/lodash' - const deniedChanges = await getDeniedChanges(changes, [denyAllLodashVersions]) + const denyAllLodashVersions = createTestPackagePURLs(['pkg:npm/lodash']) + const deniedChanges = await getDeniedChanges(changes, denyAllLodashVersions) expect(deniedChanges.length).toEqual(3) }) test('denies packages from the deny group list', async () => { const changes: Changes = [mvnChange, rubyChange] - const deniedChanges = await getDeniedChanges( - changes, - [], - ['pkg:maven/org.apache.logging.log4j'] - ) + const deniedGroups = createTestGroupPURLs([ + 'pkg:maven/org.apache.logging.log4j/' + ]) + const deniedChanges = await getDeniedChanges(changes, [], deniedGroups) expect(deniedChanges[0]).toBe(mvnChange) expect(deniedChanges.length).toEqual(1) }) +test('denies packages that match the deny group list exactly', async () => { + const changes: Changes = [ + createTestChange({ + package_url: 'pkg:npm/org.test.pass/pass-this@1.0.0', + ecosystem: 'npm' + }), + createTestChange({ + package_url: 'pkg:npm/org.test/deny-this@1.0.0', + ecosystem: 'npm' + }) + ] + const deniedGroups = createTestGroupPURLs(['pkg:npm/org.test/']) + const deniedChanges = await getDeniedChanges(changes, [], deniedGroups) + + expect(deniedChanges.length).toEqual(1) + expect(deniedChanges[0]).toBe(changes[1]) +}) + test('allows packages not defined in the deny packages and groups list', async () => { const changes: Changes = [npmChange, pipChange] + const deniedPackages = createTestPackagePURLs([ + 'pkg:gem/package-not-in-changes@1.0.0' + ]) + const deniedGroups = createTestGroupPURLs(['pkg:maven/group.not.in.changes/']) const deniedChanges = await getDeniedChanges( changes, - ['pkg:gem/not-in-list@1.0.0'], - ['pkg:maven:org.apache.logging.not-in-list'] + deniedPackages, + deniedGroups ) expect(deniedChanges.length).toEqual(0) diff --git a/__tests__/fixtures/create-test-change.ts b/__tests__/fixtures/create-test-change.ts index 30d5653..992c011 100644 --- a/__tests__/fixtures/create-test-change.ts +++ b/__tests__/fixtures/create-test-change.ts @@ -1,5 +1,7 @@ +import {PackageURL} from 'packageurl-js' import {Change} from '../../src/schemas' import {createTestVulnerability} from './create-test-vulnerability' +import {parseGroupPURL} from '../../src/utils' const defaultNpmChange: Change = { change_type: 'added', @@ -116,4 +118,18 @@ const createTestChange = (overwrites: Partial = {}): Change => { } } -export {createTestChange} +const createTestPackagePURLs = (list: string[]): PackageURL[] => { + return list.map(purl => { + return PackageURL.fromString(purl) + }) +} + +const createTestGroupPURLs = (list: string[]): PackageURL[] => { + return list + .map(purl => { + return parseGroupPURL(purl) + }) + .filter((purl): purl is PackageURL => purl !== undefined) +} + +export {createTestChange, createTestPackagePURLs, createTestGroupPURLs} diff --git a/src/config.ts b/src/config.ts index 33336f3..a2b8471 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,7 +4,7 @@ import YAML from 'yaml' import * as core from '@actions/core' import * as z from 'zod' import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas' -import {isSPDXValid, octokitClient} from './utils' +import {isSPDXValid, octokitClient, parseGroupPURL} from './utils' import {PackageURL} from 'packageurl-js' type ConfigurationOptionsPartial = Partial @@ -33,8 +33,8 @@ function readInlineConfig(): ConfigurationOptionsPartial { const allow_dependencies_licenses = parseList( getOptionalInput('allow-dependencies-licenses') ) - const deny_packages = parseList(getOptionalInput('deny-packages')) - const deny_groups = parseList(getOptionalInput('deny-groups')) + const deny_packages = getPackagePURLs(getOptionalInput('deny-packages')) + const deny_groups = getGroupPURLs(getOptionalInput('deny-groups')) const allow_ghsas = parseList(getOptionalInput('allow-ghsas')) const license_check = getOptionalBoolean('license-check') const vulnerability_check = getOptionalBoolean('vulnerability-check') @@ -107,6 +107,23 @@ function parseList(list: string | undefined): string[] | undefined { } } +function getPackagePURLs(list: string | undefined): PackageURL[] | undefined { + if (list) { + return list + .split(',') + .map(packageString => PackageURL.fromString(packageString.trim())) + } +} + +function getGroupPURLs(list: string | undefined): PackageURL[] | undefined { + if (list) { + return list + .split(',') + .map(packageString => parseGroupPURL(packageString.trim())) + .filter((purl): purl is PackageURL => purl !== undefined) + } +} + function validateLicenses( key: 'allow-licenses' | 'deny-licenses', licenses: string[] | undefined diff --git a/src/deny.ts b/src/deny.ts index 60c9bf3..2ad4a77 100644 --- a/src/deny.ts +++ b/src/deny.ts @@ -1,24 +1,22 @@ -import {Change} from './schemas' import * as core from '@actions/core' +import {Change} from './schemas' +import {PackageURL} from 'packageurl-js' export async function getDeniedChanges( changes: Change[], - deniedPackages: string[] = [], - deniedGroups: string[] = [] + deniedPackages: PackageURL[] = [], + deniedGroups: PackageURL[] = [] ): Promise { const changesDenied: Change[] = [] let hasDeniedPackage = false for (const change of changes) { - const [packageName, packageVersion] = change.package_url - .toLowerCase() - .split('@') + const changedPackage = PackageURL.fromString(change.package_url) for (const denied of deniedPackages) { - const [deniedName, deniedVersion] = denied.toLowerCase().split('@') if ( - (!deniedVersion || packageVersion === deniedVersion) && - packageName === deniedName + (!denied.version || changedPackage.version === denied.version) && + changedPackage.name === denied.name ) { changesDenied.push(change) hasDeniedPackage = true @@ -26,7 +24,10 @@ export async function getDeniedChanges( } for (const denied of deniedGroups) { - if (packageName.startsWith(denied.toLowerCase())) { + if ( + changedPackage.namespace && + changedPackage.namespace === denied.namespace + ) { changesDenied.push(change) hasDeniedPackage = true } diff --git a/src/schemas.ts b/src/schemas.ts index 2df50fc..b590529 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -5,6 +5,15 @@ export const SCOPES = ['unknown', 'runtime', 'development'] as const export const SeveritySchema = z.enum(SEVERITIES).default('low') +export const PackageURLSchema = z.object({ + type: z.string(), + namespace: z.string(), + name: z.string(), + version: z.string(), + qualifiers: z.record(z.string()).nullable(), + subpath: z.string().nullable() +}) + export const ChangeSchema = z.object({ change_type: z.enum(['added', 'removed']), manifest: z.string(), @@ -42,8 +51,8 @@ export const ConfigurationOptionsSchema = z deny_licenses: z.array(z.string()).optional(), allow_dependencies_licenses: z.array(z.string()).optional(), allow_ghsas: z.array(z.string()).default([]), - deny_packages: z.array(z.string()).default([]), - deny_groups: z.array(z.string()).default([]), + deny_packages: z.array(PackageURLSchema).default([]), + deny_groups: z.array(PackageURLSchema).default([]), license_check: z.boolean().default(true), vulnerability_check: z.boolean().default(true), config_file: z.string().optional(), diff --git a/src/utils.ts b/src/utils.ts index 3939839..d0fe628 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ 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( @@ -68,3 +69,21 @@ export function octokitClient(token = 'repo-token', required = true): Octokit { return new Octokit(opts) } + +export const parseGroupPURL = (purlString: string): PackageURL | undefined => { + try { + return PackageURL.fromString(purlString) + } catch (error) { + if ( + (error as Error).message === + `purl is missing the required "name" component.` + ) { + //package-url-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 + } + } + return undefined +}