mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat(audit): add --ignore and --ignore-unfixable flags (#8474)
* feat(audit): add --ignore-vulnerabilities flag for CVE * fix: no need for changes * feat: add changeset * fix: need the GHSA id * docs: update changeset * test: fix * test: fix * feat(audit): --ignore and --ignore-unfixable * refactor: change output * fix: reading audit settings from pnpm-workspace.yaml * test: ignoring a list of cves * docs: add changeset --------- Co-authored-by: Ian Krieger <ian.krieger@gc.com@mac.lan> Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
5
.changeset/rare-cups-pull.md
Normal file
5
.changeset/rare-cups-pull.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/types": minor
|
||||
---
|
||||
|
||||
Export AuditConfig.
|
||||
18
.changeset/stupid-rules-build.md
Normal file
18
.changeset/stupid-rules-build.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-audit": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Added two new flags to the `pnpm audit` command, `--ignore` and `--ignore-unfixable` [#8474](https://github.com/pnpm/pnpm/pull/8474).
|
||||
|
||||
Ignore all vulnerabilities that have no solution:
|
||||
|
||||
```shell
|
||||
> pnpm audit --ignore-unfixable
|
||||
```
|
||||
|
||||
Provide a list of CVE's to ignore those specifically, even if they have a resolution.
|
||||
|
||||
```shell
|
||||
> pnpm audit --ignore=CVE-2021-1234 --ignore=CVE-2021-5678
|
||||
```
|
||||
@@ -13,6 +13,7 @@ import pick from 'ramda/src/pick'
|
||||
import pickBy from 'ramda/src/pickBy'
|
||||
import renderHelp from 'render-help'
|
||||
import { fix } from './fix'
|
||||
import { ignore } from './ignore'
|
||||
|
||||
// eslint-disable
|
||||
const AUDIT_LEVEL_NUMBER = {
|
||||
@@ -57,6 +58,8 @@ export function cliOptionsTypes (): Record<string, unknown> {
|
||||
'audit-level': ['low', 'moderate', 'high', 'critical'],
|
||||
fix: Boolean,
|
||||
'ignore-registry-errors': Boolean,
|
||||
ignore: [String, Array],
|
||||
'ignore-unfixable': Boolean,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +108,14 @@ export function help (): string {
|
||||
description: 'Use exit code 0 if the registry responds with an error. Useful when audit checks are used in CI. A build should fail because the registry has issues.',
|
||||
name: '--ignore-registry-errors',
|
||||
},
|
||||
{
|
||||
description: 'Ignore a vulnerability by CVE',
|
||||
name: '--ignore <vulnerability>',
|
||||
},
|
||||
{
|
||||
description: 'Ignore all CVEs with no resolution',
|
||||
name: '--ignore-unfixable',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -120,6 +131,8 @@ export type AuditOptions = Pick<UniversalOptions, 'dir'> & {
|
||||
json?: boolean
|
||||
lockfileDir?: string
|
||||
registries: Registries
|
||||
ignore?: string[]
|
||||
ignoreUnfixable?: boolean
|
||||
} & Pick<Config, 'auditConfig'
|
||||
| 'ca'
|
||||
| 'cert'
|
||||
@@ -213,6 +226,29 @@ The added overrides:
|
||||
${JSON.stringify(newOverrides, null, 2)}`,
|
||||
}
|
||||
}
|
||||
if (opts.ignore !== undefined || opts.ignoreUnfixable) {
|
||||
const newIgnores = await ignore({
|
||||
auditConfig: opts.auditConfig,
|
||||
auditReport,
|
||||
ignore: opts.ignore,
|
||||
ignoreUnfixable: opts.ignoreUnfixable === true,
|
||||
dir: opts.dir,
|
||||
rootProjectManifest: opts.rootProjectManifest,
|
||||
rootProjectManifestDir: opts.rootProjectManifestDir,
|
||||
workspaceDir: opts.workspaceDir ?? opts.rootProjectManifestDir,
|
||||
})
|
||||
if (newIgnores.length === 0) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
output: 'No new vulnerabilities were ignored',
|
||||
}
|
||||
}
|
||||
return {
|
||||
exitCode: 0,
|
||||
output: `${newIgnores.length} new vulnerabilities were ignored:
|
||||
${newIgnores.join('\n')}`,
|
||||
}
|
||||
}
|
||||
const vulnerabilities = auditReport.metadata.vulnerabilities
|
||||
const ignoredVulnerabilities: IgnoredAuditVulnerabilityCounts = {
|
||||
low: 0,
|
||||
@@ -222,7 +258,7 @@ ${JSON.stringify(newOverrides, null, 2)}`,
|
||||
}
|
||||
const totalVulnerabilityCount = Object.values(vulnerabilities)
|
||||
.reduce((sum: number, vulnerabilitiesCount: number) => sum + vulnerabilitiesCount, 0)
|
||||
const ignoreGhsas = opts.rootProjectManifest?.pnpm?.auditConfig?.ignoreGhsas
|
||||
const ignoreGhsas = opts.auditConfig?.ignoreGhsas
|
||||
if (ignoreGhsas) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
auditReport.advisories = pickBy(({ github_advisory_id, severity }) => {
|
||||
@@ -233,7 +269,7 @@ ${JSON.stringify(newOverrides, null, 2)}`,
|
||||
return false
|
||||
}, auditReport.advisories)
|
||||
}
|
||||
const ignoreCves = opts.rootProjectManifest?.pnpm?.auditConfig?.ignoreCves
|
||||
const ignoreCves = opts.auditConfig?.ignoreCves
|
||||
if (ignoreCves) {
|
||||
auditReport.advisories = pickBy(({ cves, severity }) => {
|
||||
if (cves.length === 0 || difference(cves, ignoreCves).length > 0) {
|
||||
|
||||
47
lockfile/plugin-commands-audit/src/ignore.ts
Normal file
47
lockfile/plugin-commands-audit/src/ignore.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { type AuditAdvisory, type AuditReport } from '@pnpm/audit'
|
||||
import { type ProjectManifest, type AuditConfig } from '@pnpm/types'
|
||||
import { writeSettings } from '@pnpm/config.config-writer'
|
||||
import difference from 'ramda/src/difference'
|
||||
|
||||
export interface IgnoreVulnerabilitiesOptions {
|
||||
dir: string
|
||||
ignore?: string[]
|
||||
ignoreUnfixable: boolean
|
||||
auditReport: AuditReport
|
||||
rootProjectManifest?: ProjectManifest
|
||||
rootProjectManifestDir: string
|
||||
workspaceDir: string
|
||||
auditConfig?: AuditConfig
|
||||
}
|
||||
|
||||
export async function ignore (opts: IgnoreVulnerabilitiesOptions): Promise<string[]> {
|
||||
const currentCves = opts?.auditConfig?.ignoreCves ?? []
|
||||
const currentUniqueCves = new Set(currentCves)
|
||||
const advisoryWthNoResolutions = filterAdvisoriesWithNoResolutions(Object.values(opts.auditReport.advisories))
|
||||
|
||||
if (opts.ignoreUnfixable) {
|
||||
Object.values(advisoryWthNoResolutions).forEach((advisory: AuditAdvisory) => {
|
||||
advisory.cves.forEach((cve) => currentUniqueCves.add(cve))
|
||||
})
|
||||
} else {
|
||||
opts.ignore?.forEach((cve) => currentUniqueCves.add(cve))
|
||||
}
|
||||
|
||||
const newIgnoreCves = currentUniqueCves.size > 0 ? Array.from(currentUniqueCves) : undefined
|
||||
const diffCve = difference(newIgnoreCves ?? [], currentCves)
|
||||
await writeSettings({
|
||||
...opts,
|
||||
updatedSettings: {
|
||||
auditConfig: {
|
||||
...opts.auditConfig,
|
||||
ignoreCves: newIgnoreCves,
|
||||
},
|
||||
},
|
||||
})
|
||||
return [...diffCve]
|
||||
}
|
||||
|
||||
function filterAdvisoriesWithNoResolutions (advisories: AuditAdvisory[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
return advisories.filter(({ patched_versions }) => patched_versions === '<0.0.0')
|
||||
}
|
||||
125
lockfile/plugin-commands-audit/test/ignore.ts
Normal file
125
lockfile/plugin-commands-audit/test/ignore.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import path from 'path'
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { audit } from '@pnpm/plugin-commands-audit'
|
||||
import nock from 'nock'
|
||||
import { sync as readYamlFile } from 'read-yaml-file'
|
||||
import * as responses from './utils/responses'
|
||||
|
||||
const f = fixtures(__dirname)
|
||||
const registries = {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
}
|
||||
const rawConfig = {
|
||||
registry: registries.default,
|
||||
}
|
||||
|
||||
test('ignores are added for vulnerable dependencies with no resolutions', async () => {
|
||||
const tmp = f.prepare('has-vulnerabilities')
|
||||
|
||||
nock(registries.default)
|
||||
.post('/-/npm/v1/security/audits')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
ignoreUnfixable: true,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
expect(output).toContain('2 new vulnerabilities were ignored')
|
||||
|
||||
const manifest = readYamlFile<any>(path.join(tmp, 'pnpm-workspace.yaml')) // eslint-disable-line
|
||||
const cveList = manifest.auditConfig?.ignoreCves
|
||||
expect(cveList?.length).toBe(2)
|
||||
expect(cveList).toStrictEqual(expect.arrayContaining(['CVE-2017-16115', 'CVE-2017-16024']))
|
||||
})
|
||||
|
||||
test('the specified vulnerabilities are ignored', async () => {
|
||||
const tmp = f.prepare('has-vulnerabilities')
|
||||
|
||||
nock(registries.default)
|
||||
.post('/-/npm/v1/security/audits')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
ignore: ['CVE-2017-16115'],
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
expect(output).toContain('1 new vulnerabilities were ignored')
|
||||
|
||||
const manifest = readYamlFile<any>(path.join(tmp, 'pnpm-workspace.yaml')) // eslint-disable-line
|
||||
expect(manifest.auditConfig?.ignoreCves).toStrictEqual(['CVE-2017-16115'])
|
||||
})
|
||||
|
||||
test('no ignores are added if no vulnerabilities are found', async () => {
|
||||
const tmp = f.prepare('fixture')
|
||||
|
||||
nock(registries.default)
|
||||
.post('/-/npm/v1/security/audits')
|
||||
.reply(200, responses.NO_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
ignoreUnfixable: true,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
expect(output).toBe('No new vulnerabilities were ignored')
|
||||
})
|
||||
|
||||
test('ignored CVEs are not duplicated', async () => {
|
||||
const tmp = f.prepare('has-vulnerabilities')
|
||||
const existingCves = [
|
||||
'CVE-2019-10742',
|
||||
'CVE-2020-7598',
|
||||
'CVE-2017-16115',
|
||||
'CVE-2017-16024',
|
||||
]
|
||||
|
||||
nock(registries.default)
|
||||
.post('/-/npm/v1/security/audits')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
auditLevel: 'moderate',
|
||||
auditConfig: {
|
||||
ignoreCves: existingCves,
|
||||
},
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
ignoreUnfixable: true,
|
||||
})
|
||||
expect(exitCode).toBe(0)
|
||||
expect(output).toBe('No new vulnerabilities were ignored')
|
||||
|
||||
const manifest = readYamlFile<any>(path.join(tmp, 'pnpm-workspace.yaml')) // eslint-disable-line
|
||||
expect(manifest.auditConfig?.ignoreCves).toStrictEqual(expect.arrayContaining(existingCves))
|
||||
})
|
||||
@@ -267,17 +267,14 @@ describe('plugin-commands-audit', () => {
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
rootProjectManifest: {
|
||||
pnpm: {
|
||||
auditConfig: {
|
||||
ignoreCves: [
|
||||
'CVE-2019-10742',
|
||||
'CVE-2020-28168',
|
||||
'CVE-2021-3749',
|
||||
'CVE-2020-7598',
|
||||
],
|
||||
},
|
||||
},
|
||||
rootProjectManifest: {},
|
||||
auditConfig: {
|
||||
ignoreCves: [
|
||||
'CVE-2019-10742',
|
||||
'CVE-2020-28168',
|
||||
'CVE-2021-3749',
|
||||
'CVE-2020-7598',
|
||||
],
|
||||
},
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
@@ -300,17 +297,14 @@ describe('plugin-commands-audit', () => {
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
rootProjectManifest: {
|
||||
pnpm: {
|
||||
auditConfig: {
|
||||
ignoreGhsas: [
|
||||
'GHSA-42xw-2xvc-qx8m',
|
||||
'GHSA-4w2v-q235-vp99',
|
||||
'GHSA-cph5-m8f7-6c5x',
|
||||
'GHSA-vh95-rmgr-6w4m',
|
||||
],
|
||||
},
|
||||
},
|
||||
rootProjectManifest: {},
|
||||
auditConfig: {
|
||||
ignoreGhsas: [
|
||||
'GHSA-42xw-2xvc-qx8m',
|
||||
'GHSA-4w2v-q235-vp99',
|
||||
'GHSA-cph5-m8f7-6c5x',
|
||||
'GHSA-vh95-rmgr-6w4m',
|
||||
],
|
||||
},
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
@@ -334,17 +328,14 @@ describe('plugin-commands-audit', () => {
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
rootProjectManifest: {
|
||||
pnpm: {
|
||||
auditConfig: {
|
||||
ignoreCves: [
|
||||
'CVE-2019-10742',
|
||||
'CVE-2020-28168',
|
||||
'CVE-2021-3749',
|
||||
'CVE-2020-7598',
|
||||
],
|
||||
},
|
||||
},
|
||||
rootProjectManifest: {},
|
||||
auditConfig: {
|
||||
ignoreCves: [
|
||||
'CVE-2019-10742',
|
||||
'CVE-2020-28168',
|
||||
'CVE-2021-3749',
|
||||
'CVE-2020-7598',
|
||||
],
|
||||
},
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
@@ -135,6 +135,11 @@ export type AllowedDeprecatedVersions = Record<string, string>
|
||||
|
||||
export type ConfigDependencies = Record<string, string>
|
||||
|
||||
export interface AuditConfig {
|
||||
ignoreCves?: string[]
|
||||
ignoreGhsas?: string[]
|
||||
}
|
||||
|
||||
export interface PnpmSettings {
|
||||
configDependencies?: ConfigDependencies
|
||||
neverBuiltDependencies?: string[]
|
||||
@@ -153,10 +158,7 @@ export interface PnpmSettings {
|
||||
updateConfig?: {
|
||||
ignoreDependencies?: string[]
|
||||
}
|
||||
auditConfig?: {
|
||||
ignoreCves?: string[]
|
||||
ignoreGhsas?: string[]
|
||||
}
|
||||
auditConfig?: AuditConfig
|
||||
requiredScripts?: string[]
|
||||
supportedArchitectures?: SupportedArchitectures
|
||||
executionEnv?: ExecutionEnv
|
||||
|
||||
Reference in New Issue
Block a user