Compare commits
15 Commits
juxtin/release
...
v4.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c155c5e85 | |||
| f3dac32d35 | |||
| d0d5cc3ec4 | |||
| 49fbbe0acb | |||
| e58c696e52 | |||
| 9b7c72ddcd | |||
| 7dcfabfea2 | |||
| 5f0808ffb1 | |||
| fcc66c23b3 | |||
| 1dd418bcb3 | |||
| 640617990f | |||
| 2034babb6b | |||
| 7e773b1e98 | |||
| a3460920cc | |||
| 0659a74c94 |
@@ -54,6 +54,52 @@ test('it raises an error if an empty allow list is specified', async () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('it successfully parses allow-dependencies-licenses', async () => {
|
||||
setInput(
|
||||
'allow-dependencies-licenses',
|
||||
'pkg:npm/@test/package@1.2.3,pkg:npm/example'
|
||||
)
|
||||
const config = await readConfig()
|
||||
expect(config.allow_dependencies_licenses).toEqual([
|
||||
'pkg:npm/@test/package@1.2.3',
|
||||
'pkg:npm/example'
|
||||
])
|
||||
})
|
||||
|
||||
test('it raises an error when an invalid package-url is used for allow-dependencies-licenses', async () => {
|
||||
setInput('allow-dependencies-licenses', 'not-a-purl')
|
||||
await expect(readConfig()).rejects.toThrow(`Error parsing package-url`)
|
||||
})
|
||||
|
||||
test('it raises an error when a nameless package-url is used for allow-dependencies-licenses', async () => {
|
||||
setInput('allow-dependencies-licenses', 'pkg:npm/@namespace/')
|
||||
await expect(readConfig()).rejects.toThrow(
|
||||
`Error parsing package-url: name is required`
|
||||
)
|
||||
})
|
||||
|
||||
test('it raises an error when an invalid package-url is used for deny-packages', async () => {
|
||||
setInput('deny-packages', 'not-a-purl')
|
||||
|
||||
await expect(readConfig()).rejects.toThrow(`Error parsing package-url`)
|
||||
})
|
||||
|
||||
test('it raises an error when a nameless package-url is used for deny-packages', async () => {
|
||||
setInput('deny-packages', 'pkg:npm/@namespace/')
|
||||
|
||||
await expect(readConfig()).rejects.toThrow(
|
||||
`Error parsing package-url: name is required`
|
||||
)
|
||||
})
|
||||
|
||||
test('it raises an error when an argument to deny-groups is missing a namespace', async () => {
|
||||
setInput('deny-groups', 'pkg:npm/my-fun-org')
|
||||
|
||||
await expect(readConfig()).rejects.toThrow(
|
||||
`package-url must have a namespace`
|
||||
)
|
||||
})
|
||||
|
||||
test('it raises an error when given an unknown severity', async () => {
|
||||
setInput('fail-on-severity', 'zombies')
|
||||
|
||||
|
||||
@@ -106,6 +106,25 @@ test('denies packages that match the deny group list exactly', async () => {
|
||||
expect(deniedChanges[0]).toBe(changes[1])
|
||||
})
|
||||
|
||||
test(`denies packages using the namespace from the name when there's no package_url`, async () => {
|
||||
const changes: Changes = [
|
||||
createTestChange({
|
||||
package_url: 'pkg:npm/org.test.pass/pass-this@1.0.0',
|
||||
ecosystem: 'npm'
|
||||
}),
|
||||
createTestChange({
|
||||
name: 'org.test:deny-this',
|
||||
package_url: '',
|
||||
ecosystem: 'maven'
|
||||
})
|
||||
]
|
||||
const deniedGroups = createTestPURLs(['pkg:maven/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 = createTestPURLs([
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {PackageURL} from 'packageurl-js'
|
||||
import {Change} from '../../src/schemas'
|
||||
import {createTestVulnerability} from './create-test-vulnerability'
|
||||
import {parsePURL} from '../../src/utils'
|
||||
import {PackageURL, parsePURL} from '../../src/purl'
|
||||
|
||||
const defaultNpmChange: Change = {
|
||||
change_type: 'added',
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {parsePURL} from '../src/purl'
|
||||
|
||||
test('parsePURL returns an error if the purl does not start with "pkg:"', () => {
|
||||
const purl = 'not-a-purl'
|
||||
const result = parsePURL(purl)
|
||||
expect(result.error).toEqual('package-url must start with "pkg:"')
|
||||
})
|
||||
|
||||
test('parsePURL returns an error if the purl does not contain a type', () => {
|
||||
const purl = 'pkg:/'
|
||||
const result = parsePURL(purl)
|
||||
expect(result.error).toEqual('package-url must contain a type')
|
||||
})
|
||||
|
||||
test('parsePURL returns an error if the purl does not contain a namespace or name', () => {
|
||||
const purl = 'pkg:ecosystem/'
|
||||
const result = parsePURL(purl)
|
||||
expect(result.type).toEqual('ecosystem')
|
||||
expect(result.error).toEqual('package-url must contain a namespace or name')
|
||||
})
|
||||
|
||||
test('parsePURL returns a PURL with the correct values in the happy case', () => {
|
||||
const purl = 'pkg:ecosystem/namespace/name@version'
|
||||
const result = parsePURL(purl)
|
||||
expect(result.type).toEqual('ecosystem')
|
||||
expect(result.namespace).toEqual('namespace')
|
||||
expect(result.name).toEqual('name')
|
||||
expect(result.version).toEqual('version')
|
||||
expect(result.original).toEqual(purl)
|
||||
expect(result.error).toBeNull()
|
||||
})
|
||||
|
||||
test('parsePURL table test', () => {
|
||||
const examples = [
|
||||
{
|
||||
purl: 'pkg:npm/@n4m3SPACE/Name@^1.2.3',
|
||||
expected: {
|
||||
type: 'npm',
|
||||
namespace: '@n4m3SPACE',
|
||||
name: 'Name',
|
||||
version: '^1.2.3',
|
||||
original: 'pkg:npm/@n4m3SPACE/Name@^1.2.3',
|
||||
error: null
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:npm/%40ns%20foo/n%40me@1.%2f2.3',
|
||||
expected: {
|
||||
type: 'npm',
|
||||
namespace: '@ns foo',
|
||||
name: 'n@me',
|
||||
version: '1./2.3',
|
||||
original: 'pkg:npm/%40ns%20foo/n%40me@1.%2f2.3',
|
||||
error: null
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:ecosystem/name@version',
|
||||
expected: {
|
||||
type: 'ecosystem',
|
||||
namespace: null,
|
||||
name: 'name',
|
||||
version: 'version',
|
||||
original: 'pkg:ecosystem/name@version',
|
||||
error: null
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:npm/namespace/',
|
||||
expected: {
|
||||
type: 'npm',
|
||||
namespace: 'namespace',
|
||||
name: null,
|
||||
version: null,
|
||||
original: 'pkg:npm/namespace/',
|
||||
error: null
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:ecosystem/name',
|
||||
expected: {
|
||||
type: 'ecosystem',
|
||||
namespace: null,
|
||||
name: 'name',
|
||||
version: null,
|
||||
original: 'pkg:ecosystem/name',
|
||||
error: null
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:/?',
|
||||
expected: {
|
||||
type: '',
|
||||
namespace: null,
|
||||
name: null,
|
||||
version: null,
|
||||
original: 'pkg:/?',
|
||||
error: 'package-url must contain a type'
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:ecosystem/#',
|
||||
expected: {
|
||||
type: 'ecosystem',
|
||||
namespace: null,
|
||||
name: null,
|
||||
version: null,
|
||||
original: 'pkg:ecosystem/#',
|
||||
error: 'package-url must contain a namespace or name'
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:ecosystem/name@version#subpath?attributes=123',
|
||||
expected: {
|
||||
type: 'ecosystem',
|
||||
namespace: null,
|
||||
name: 'name',
|
||||
version: 'version',
|
||||
original: 'pkg:ecosystem/name@version#subpath?attributes=123',
|
||||
error: null
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:ecosystem/name@version#subpath',
|
||||
expected: {
|
||||
type: 'ecosystem',
|
||||
namespace: null,
|
||||
name: 'name',
|
||||
version: 'version',
|
||||
original: 'pkg:ecosystem/name@version#subpath',
|
||||
error: null
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:ecosystem/namespace/name@version?attributes',
|
||||
expected: {
|
||||
type: 'ecosystem',
|
||||
namespace: 'namespace',
|
||||
name: 'name',
|
||||
version: 'version',
|
||||
original: 'pkg:ecosystem/namespace/name@version?attributes',
|
||||
error: null
|
||||
}
|
||||
},
|
||||
{
|
||||
purl: 'pkg:ecosystem/name#subpath?attributes',
|
||||
expected: {
|
||||
type: 'ecosystem',
|
||||
namespace: null,
|
||||
name: 'name',
|
||||
version: null,
|
||||
original: 'pkg:ecosystem/name#subpath?attributes',
|
||||
error: null
|
||||
}
|
||||
}
|
||||
]
|
||||
for (const example of examples) {
|
||||
const result = parsePURL(example.purl)
|
||||
expect(result).toEqual(example.expected)
|
||||
}
|
||||
})
|
||||
@@ -11,6 +11,7 @@ export function clearInputs(): void {
|
||||
'FAIL-ON-SEVERITY',
|
||||
'FAIL-ON-SCOPES',
|
||||
'ALLOW-LICENSES',
|
||||
'ALLOW-DEPENDENCIES-LICENSES',
|
||||
'DENY-LICENSES',
|
||||
'ALLOW-GHSAS',
|
||||
'LICENSE-CHECK',
|
||||
@@ -19,7 +20,9 @@ export function clearInputs(): void {
|
||||
'BASE-REF',
|
||||
'HEAD-REF',
|
||||
'COMMENT-SUMMARY-IN-PR',
|
||||
'WARN-ONLY'
|
||||
'WARN-ONLY',
|
||||
'DENY-GROUPS',
|
||||
'DENY-PACKAGES'
|
||||
]
|
||||
|
||||
// eslint-disable-next-line github/array-foreach
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ inputs:
|
||||
description: A comma-separated list of package URLs to deny (e.g. "pkg:npm/express, pkg:pypi/pycrypto"). If version specified, only deny matching packages and version; else, deny all regardless of version.
|
||||
required: false
|
||||
deny-groups:
|
||||
description: A comma-separated list of package URLs for group(s)/namespace(s) to deny (e.g. "pkg:npm/express, pkg:pypi/pycrypto")
|
||||
description: A comma-separated list of package URLs for group(s)/namespace(s) to deny (e.g. "pkg:npm/express/, pkg:pypi/pycrypto/"). Please note that the group name must be followed by a `/`.
|
||||
required: false
|
||||
retry-on-snapshot-warnings:
|
||||
description: Whether to retry on snapshot warnings
|
||||
|
||||
+360
-368
File diff suppressed because it is too large
Load Diff
+1
-1
File diff suppressed because one or more lines are too long
-22
@@ -1519,28 +1519,6 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
packageurl-js
|
||||
MIT
|
||||
Copyright (c) the purl authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
safe-buffer
|
||||
MIT
|
||||
The MIT License (MIT)
|
||||
|
||||
+2
-2
@@ -280,7 +280,7 @@ With the `deny-packages` option, you can exclude dependencies based on their PUR
|
||||
|
||||
Using the `deny-groups` option you can exclude dependencies by their group name/namespace. You can add multiple values separated by a comma.
|
||||
|
||||
In this example, we are excluding all versions of `pkg:maven/org.apache.logging.log4j:log4j-api` and only `2.23.0` of log4j-core `pkg:maven/org.apache.logging.log4j/log4j-core@2.23.0` from `maven` and all packages in the group `pkg:maven/com.bazaarvoice.maven`
|
||||
In this example, we are excluding all versions of `pkg:maven/org.apache.logging.log4j:log4j-api` and only `2.23.0` of log4j-core `pkg:maven/org.apache.logging.log4j/log4j-core@2.23.0` from `maven` and all packages in the group `pkg:maven/com.bazaarvoice.maven/`
|
||||
|
||||
```yaml
|
||||
name: 'Dependency Review'
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
deny-packages: 'pkg:maven/org.apache.logging.log4j/log4j-api,pkg:maven/org.apache.logging.log4j/log4j-core@2.23.0'
|
||||
deny-groups: 'pkg:maven/com.bazaarvoice.jolt'
|
||||
deny-groups: 'pkg:maven/com.bazaarvoice.jolt/'
|
||||
```
|
||||
|
||||
## Waiting for dependency submission jobs to complete
|
||||
|
||||
Generated
+2
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dependency-review-action",
|
||||
"version": "4.3.0",
|
||||
"version": "4.3.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dependency-review-action",
|
||||
"version": "4.3.0",
|
||||
"version": "4.3.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
@@ -18,7 +18,6 @@
|
||||
"got": "^14.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"octokit": "^3.1.2",
|
||||
"packageurl-js": "^1.2.0",
|
||||
"spdx-expression-parse": "^3.0.1",
|
||||
"spdx-satisfies": "^5.0.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
@@ -6651,11 +6650,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/packageurl-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.2.1.tgz",
|
||||
"integrity": "sha512-cZ6/MzuXaoFd16/k0WnwtI298UCaDHe/XlSh85SeOKbGZ1hq0xvNbx3ILyCMyk7uFQxl6scF3Aucj6/EO9NwcA=="
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dependency-review-action",
|
||||
"version": "4.3.0",
|
||||
"version": "4.3.2",
|
||||
"private": true,
|
||||
"description": "A GitHub Action for Dependency Review",
|
||||
"main": "lib/main.js",
|
||||
@@ -34,7 +34,6 @@
|
||||
"got": "^14.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"octokit": "^3.1.2",
|
||||
"packageurl-js": "^1.2.0",
|
||||
"spdx-expression-parse": "^3.0.1",
|
||||
"spdx-satisfies": "^5.0.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+21
-9
@@ -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
|
||||
}
|
||||
|
||||
+3
-5
@@ -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
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
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])
|
||||
namePlusRest = parts[2]
|
||||
}
|
||||
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
|
||||
}
|
||||
+57
-5
@@ -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(),
|
||||
|
||||
@@ -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