using packageurl-js to parse packages and groups from config

This commit is contained in:
Brandon Teng
2024-04-16 12:44:51 -05:00
parent 061f471b83
commit a318e62c6c
6 changed files with 122 additions and 31 deletions
+44 -15
View File
@@ -1,6 +1,10 @@
import {expect, jest, test} from '@jest/globals' import {expect, jest, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas' 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' import {getDeniedChanges} from '../src/deny'
jest.mock('@actions/core') jest.mock('@actions/core')
@@ -47,20 +51,24 @@ beforeEach(async () => {
test('denies packages from the deny packages list', async () => { test('denies packages from the deny packages list', async () => {
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
const deniedChanges = await getDeniedChanges(changes, [ const deniedPackages = createTestPackagePURLs([
'pkg:gem/[email protected]' 'pkg:gem/[email protected]'
]) ])
const deniedChanges = await getDeniedChanges(changes, deniedPackages)
expect(deniedChanges[0]).toBe(rubyChange) expect(deniedChanges[0]).toBe(rubyChange)
expect(deniedChanges.length).toEqual(1) expect(deniedChanges.length).toEqual(1)
}) })
test('denies packages only for the specified version from deny packages list', async () => { test('denies packages only for the specified version from deny packages list', async () => {
const packageWithDifferentVersion = 'pkg:npm/[email protected]' const deniedPackageWithDifferentVersion = createTestPackagePURLs([
const changes: Changes = [npmChange] 'pkg:npm/[email protected]'
const deniedChanges = await getDeniedChanges(changes, [
packageWithDifferentVersion
]) ])
const changes: Changes = [npmChange]
const deniedChanges = await getDeniedChanges(
changes,
deniedPackageWithDifferentVersion
)
expect(deniedChanges.length).toEqual(0) 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: '4.5.6'}),
createTestChange({name: 'lodash', version: '7.8.9'}) createTestChange({name: 'lodash', version: '7.8.9'})
] ]
const denyAllLodashVersions = 'pkg:npm/lodash' const denyAllLodashVersions = createTestPackagePURLs(['pkg:npm/lodash'])
const deniedChanges = await getDeniedChanges(changes, [denyAllLodashVersions]) const deniedChanges = await getDeniedChanges(changes, denyAllLodashVersions)
expect(deniedChanges.length).toEqual(3) expect(deniedChanges.length).toEqual(3)
}) })
test('denies packages from the deny group list', async () => { test('denies packages from the deny group list', async () => {
const changes: Changes = [mvnChange, rubyChange] const changes: Changes = [mvnChange, rubyChange]
const deniedChanges = await getDeniedChanges( const deniedGroups = createTestGroupPURLs([
changes, 'pkg:maven/org.apache.logging.log4j/'
[], ])
['pkg:maven/org.apache.logging.log4j'] const deniedChanges = await getDeniedChanges(changes, [], deniedGroups)
)
expect(deniedChanges[0]).toBe(mvnChange) expect(deniedChanges[0]).toBe(mvnChange)
expect(deniedChanges.length).toEqual(1) 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/[email protected]',
ecosystem: 'npm'
}),
createTestChange({
package_url: 'pkg:npm/org.test/[email protected]',
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 () => { test('allows packages not defined in the deny packages and groups list', async () => {
const changes: Changes = [npmChange, pipChange] const changes: Changes = [npmChange, pipChange]
const deniedPackages = createTestPackagePURLs([
'pkg:gem/[email protected]'
])
const deniedGroups = createTestGroupPURLs(['pkg:maven/group.not.in.changes/'])
const deniedChanges = await getDeniedChanges( const deniedChanges = await getDeniedChanges(
changes, changes,
['pkg:gem/[email protected]'], deniedPackages,
['pkg:maven:org.apache.logging.not-in-list'] deniedGroups
) )
expect(deniedChanges.length).toEqual(0) expect(deniedChanges.length).toEqual(0)
+17 -1
View File
@@ -1,5 +1,7 @@
import {PackageURL} from 'packageurl-js'
import {Change} from '../../src/schemas' import {Change} from '../../src/schemas'
import {createTestVulnerability} from './create-test-vulnerability' import {createTestVulnerability} from './create-test-vulnerability'
import {parseGroupPURL} from '../../src/utils'
const defaultNpmChange: Change = { const defaultNpmChange: Change = {
change_type: 'added', change_type: 'added',
@@ -116,4 +118,18 @@ const createTestChange = (overwrites: Partial<Change> = {}): 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}
+20 -3
View File
@@ -4,7 +4,7 @@ import YAML from 'yaml'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as z from 'zod' import * as z from 'zod'
import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas' import {ConfigurationOptions, ConfigurationOptionsSchema} from './schemas'
import {isSPDXValid, octokitClient} from './utils' import {isSPDXValid, octokitClient, parseGroupPURL} from './utils'
import {PackageURL} from 'packageurl-js' import {PackageURL} from 'packageurl-js'
type ConfigurationOptionsPartial = Partial<ConfigurationOptions> type ConfigurationOptionsPartial = Partial<ConfigurationOptions>
@@ -33,8 +33,8 @@ function readInlineConfig(): ConfigurationOptionsPartial {
const allow_dependencies_licenses = parseList( const allow_dependencies_licenses = parseList(
getOptionalInput('allow-dependencies-licenses') getOptionalInput('allow-dependencies-licenses')
) )
const deny_packages = parseList(getOptionalInput('deny-packages')) const deny_packages = getPackagePURLs(getOptionalInput('deny-packages'))
const deny_groups = parseList(getOptionalInput('deny-groups')) const deny_groups = getGroupPURLs(getOptionalInput('deny-groups'))
const allow_ghsas = parseList(getOptionalInput('allow-ghsas')) const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
const license_check = getOptionalBoolean('license-check') const license_check = getOptionalBoolean('license-check')
const vulnerability_check = getOptionalBoolean('vulnerability-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( function validateLicenses(
key: 'allow-licenses' | 'deny-licenses', key: 'allow-licenses' | 'deny-licenses',
licenses: string[] | undefined licenses: string[] | undefined
+11 -10
View File
@@ -1,24 +1,22 @@
import {Change} from './schemas'
import * as core from '@actions/core' import * as core from '@actions/core'
import {Change} from './schemas'
import {PackageURL} from 'packageurl-js'
export async function getDeniedChanges( export async function getDeniedChanges(
changes: Change[], changes: Change[],
deniedPackages: string[] = [], deniedPackages: PackageURL[] = [],
deniedGroups: string[] = [] deniedGroups: PackageURL[] = []
): Promise<Change[]> { ): Promise<Change[]> {
const changesDenied: Change[] = [] const changesDenied: Change[] = []
let hasDeniedPackage = false let hasDeniedPackage = false
for (const change of changes) { for (const change of changes) {
const [packageName, packageVersion] = change.package_url const changedPackage = PackageURL.fromString(change.package_url)
.toLowerCase()
.split('@')
for (const denied of deniedPackages) { for (const denied of deniedPackages) {
const [deniedName, deniedVersion] = denied.toLowerCase().split('@')
if ( if (
(!deniedVersion || packageVersion === deniedVersion) && (!denied.version || changedPackage.version === denied.version) &&
packageName === deniedName changedPackage.name === denied.name
) { ) {
changesDenied.push(change) changesDenied.push(change)
hasDeniedPackage = true hasDeniedPackage = true
@@ -26,7 +24,10 @@ export async function getDeniedChanges(
} }
for (const denied of deniedGroups) { for (const denied of deniedGroups) {
if (packageName.startsWith(denied.toLowerCase())) { if (
changedPackage.namespace &&
changedPackage.namespace === denied.namespace
) {
changesDenied.push(change) changesDenied.push(change)
hasDeniedPackage = true hasDeniedPackage = true
} }
+11 -2
View File
@@ -5,6 +5,15 @@ export const SCOPES = ['unknown', 'runtime', 'development'] as const
export const SeveritySchema = z.enum(SEVERITIES).default('low') 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({ export const ChangeSchema = z.object({
change_type: z.enum(['added', 'removed']), change_type: z.enum(['added', 'removed']),
manifest: z.string(), manifest: z.string(),
@@ -42,8 +51,8 @@ export const ConfigurationOptionsSchema = z
deny_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(z.string()).optional(),
allow_ghsas: z.array(z.string()).default([]), allow_ghsas: z.array(z.string()).default([]),
deny_packages: z.array(z.string()).default([]), deny_packages: z.array(PackageURLSchema).default([]),
deny_groups: z.array(z.string()).default([]), deny_groups: z.array(PackageURLSchema).default([]),
license_check: z.boolean().default(true), license_check: z.boolean().default(true),
vulnerability_check: z.boolean().default(true), vulnerability_check: z.boolean().default(true),
config_file: z.string().optional(), config_file: z.string().optional(),
+19
View File
@@ -1,6 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {Octokit} from 'octokit' import {Octokit} from 'octokit'
import spdxParse from 'spdx-expression-parse' import spdxParse from 'spdx-expression-parse'
import {PackageURL} from 'packageurl-js'
import {Changes} from './schemas' import {Changes} from './schemas'
export function groupDependenciesByManifest( export function groupDependenciesByManifest(
@@ -68,3 +69,21 @@ export function octokitClient(token = 'repo-token', required = true): Octokit {
return new Octokit(opts) 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
}