fix: audit fix should update overrides in pnpm-workspace.yaml (#9371)

This commit is contained in:
Zoltan Kochan
2025-04-03 11:18:31 +02:00
committed by GitHub
parent 013285a01b
commit 01f2bcfa9b
9 changed files with 109 additions and 78 deletions

View File

@@ -0,0 +1,7 @@
---
"@pnpm/plugin-commands-audit": patch
"@pnpm/config": patch
"pnpm": patch
---
`pnpm audit --fix` should update the overrides in `pnpm-workspace.yaml`.

View File

@@ -28,7 +28,7 @@ export type OptionsFromRootManifest = {
patchedDependencies?: Record<string, string>
peerDependencyRules?: PeerDependencyRules
supportedArchitectures?: SupportedArchitectures
} & Pick<PnpmSettings, 'configDependencies'>
} & Pick<PnpmSettings, 'configDependencies' | 'auditConfig'>
export function getOptionsFromRootManifest (manifestDir: string, manifest: ProjectManifest): OptionsFromRootManifest {
const settings: OptionsFromRootManifest = getOptionsFromPnpmSettings(manifestDir, {
@@ -36,6 +36,7 @@ export function getOptionsFromRootManifest (manifestDir: string, manifest: Proje
'allowedDeprecatedVersions',
'allowNonAppliedPatches',
'allowUnusedPatches',
'auditConfig',
'configDependencies',
'ignoredBuiltDependencies',
'ignoredOptionalDependencies',

View File

@@ -35,6 +35,7 @@
"@pnpm/audit": "workspace:*",
"@pnpm/cli-utils": "workspace:*",
"@pnpm/config": "workspace:*",
"@pnpm/config.config-writer": "workspace:*",
"@pnpm/constants": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/lockfile.fs": "workspace:*",
@@ -55,6 +56,7 @@
"@types/zkochan__table": "catalog:",
"load-json-file": "catalog:",
"nock": "catalog:",
"read-yaml-file": "catalog:",
"tempy": "catalog:"
},
"engines": {

View File

@@ -113,37 +113,41 @@ export function help (): string {
})
}
export async function handler (
opts: Pick<UniversalOptions, 'dir'> & {
auditLevel?: 'low' | 'moderate' | 'high' | 'critical'
fix?: boolean
ignoreRegistryErrors?: boolean
json?: boolean
lockfileDir?: string
registries: Registries
} & Pick<Config, 'ca'
| 'cert'
| 'httpProxy'
| 'httpsProxy'
| 'key'
| 'localAddress'
| 'maxSockets'
| 'noProxy'
| 'strictSsl'
| 'fetchRetries'
| 'fetchRetryMaxtimeout'
| 'fetchRetryMintimeout'
| 'fetchRetryFactor'
| 'fetchTimeout'
| 'production'
| 'dev'
| 'optional'
| 'userConfig'
| 'rawConfig'
| 'rootProjectManifest'
| 'virtualStoreDirMaxLength'
>
): Promise<{ exitCode: number, output: string }> {
export type AuditOptions = Pick<UniversalOptions, 'dir'> & {
auditLevel?: 'low' | 'moderate' | 'high' | 'critical'
fix?: boolean
ignoreRegistryErrors?: boolean
json?: boolean
lockfileDir?: string
registries: Registries
} & Pick<Config, 'auditConfig'
| 'ca'
| 'cert'
| 'httpProxy'
| 'httpsProxy'
| 'key'
| 'localAddress'
| 'maxSockets'
| 'noProxy'
| 'strictSsl'
| 'fetchRetries'
| 'fetchRetryMaxtimeout'
| 'fetchRetryMintimeout'
| 'fetchRetryFactor'
| 'fetchTimeout'
| 'production'
| 'dev'
| 'overrides'
| 'optional'
| 'userConfig'
| 'rawConfig'
| 'rootProjectManifest'
| 'rootProjectManifestDir'
| 'virtualStoreDirMaxLength'
| 'workspaceDir'
>
export async function handler (opts: AuditOptions): Promise<{ exitCode: number, output: string }> {
const lockfileDir = opts.lockfileDir ?? opts.dir
const lockfile = await readWantedLockfile(lockfileDir, { ignoreIncompatible: true })
if (lockfile == null) {
@@ -193,7 +197,7 @@ export async function handler (
throw err
}
if (opts.fix) {
const newOverrides = await fix(opts.dir, auditReport)
const newOverrides = await fix(auditReport, opts)
if (Object.values(newOverrides).length === 0) {
return {
exitCode: 0,

View File

@@ -1,20 +1,21 @@
import { type AuditReport, type AuditAdvisory } from '@pnpm/audit'
import { readProjectManifest } from '@pnpm/read-project-manifest'
import { writeSettings } from '@pnpm/config.config-writer'
import difference from 'ramda/src/difference'
import { type AuditOptions } from './audit'
export async function fix (dir: string, auditReport: AuditReport): Promise<Record<string, string>> {
const { manifest, writeProjectManifest } = await readProjectManifest(dir)
const vulnOverrides = createOverrides(Object.values(auditReport.advisories), manifest.pnpm?.auditConfig?.ignoreCves)
export async function fix (auditReport: AuditReport, opts: AuditOptions): Promise<Record<string, string>> {
const vulnOverrides = createOverrides(Object.values(auditReport.advisories), opts.auditConfig?.ignoreCves)
if (Object.values(vulnOverrides).length === 0) return vulnOverrides
await writeProjectManifest({
...manifest,
pnpm: {
...manifest.pnpm,
await writeSettings({
updatedSettings: {
overrides: {
...manifest.pnpm?.overrides,
...opts.overrides,
...vulnOverrides,
},
},
rootProjectManifest: opts.rootProjectManifest,
rootProjectManifestDir: opts.rootProjectManifestDir,
workspaceDir: opts.workspaceDir ?? opts.rootProjectManifestDir,
})
return vulnOverrides
}

View File

@@ -1,9 +1,7 @@
import path from 'path'
import { fixtures } from '@pnpm/test-fixtures'
import { type ProjectManifest } from '@pnpm/types'
import { audit } from '@pnpm/plugin-commands-audit'
import { readProjectManifest } from '@pnpm/read-project-manifest'
import loadJsonFile from 'load-json-file'
import { sync as readYamlFile } from 'read-yaml-file'
import nock from 'nock'
import * as responses from './utils/responses'
@@ -25,6 +23,7 @@ test('overrides are added for vulnerable dependencies', async () => {
const { exitCode, output } = await audit.handler({
auditLevel: 'moderate',
dir: tmp,
rootProjectManifestDir: tmp,
fix: true,
userConfig: {},
rawConfig,
@@ -35,9 +34,9 @@ test('overrides are added for vulnerable dependencies', async () => {
expect(exitCode).toBe(0)
expect(output).toMatch(/Run "pnpm install"/)
const manifest = loadJsonFile.sync<ProjectManifest>(path.join(tmp, 'package.json'))
expect(manifest.pnpm?.overrides?.['axios@<=0.18.0']).toBe('>=0.18.1')
expect(manifest.pnpm?.overrides?.['sync-exec@>=0.0.0']).toBeFalsy()
const manifest = readYamlFile<{ overrides?: Record<string, string> }>(path.join(tmp, 'pnpm-workspace.yaml'))
expect(manifest.overrides?.['axios@<=0.18.0']).toBe('>=0.18.1')
expect(manifest.overrides?.['sync-exec@>=0.0.0']).toBeFalsy()
})
test('no overrides are added if no vulnerabilities are found', async () => {
@@ -50,6 +49,7 @@ test('no overrides are added if no vulnerabilities are found', async () => {
const { exitCode, output } = await audit.handler({
auditLevel: 'moderate',
dir: tmp,
rootProjectManifestDir: tmp,
fix: true,
userConfig: {},
rawConfig,
@@ -63,21 +63,6 @@ test('no overrides are added if no vulnerabilities are found', async () => {
test('CVEs found in the allow list are not added as overrides', async () => {
const tmp = f.prepare('has-vulnerabilities')
{
const { manifest, writeProjectManifest } = await readProjectManifest(tmp)
manifest.pnpm = {
...manifest.pnpm,
auditConfig: {
ignoreCves: [
'CVE-2019-10742',
'CVE-2020-28168',
'CVE-2021-3749',
'CVE-2020-7598',
],
},
}
await writeProjectManifest(manifest)
}
nock(registries.default)
.post('/-/npm/v1/security/audits')
@@ -85,7 +70,16 @@ test('CVEs found in the allow list are not added as overrides', async () => {
const { exitCode, output } = await audit.handler({
auditLevel: 'moderate',
auditConfig: {
ignoreCves: [
'CVE-2019-10742',
'CVE-2020-28168',
'CVE-2021-3749',
'CVE-2020-7598',
],
},
dir: tmp,
rootProjectManifestDir: tmp,
fix: true,
userConfig: {},
rawConfig,
@@ -95,9 +89,9 @@ test('CVEs found in the allow list are not added as overrides', async () => {
expect(exitCode).toBe(0)
expect(output).toMatch(/Run "pnpm install"/)
const manifest = loadJsonFile.sync<ProjectManifest>(path.join(tmp, 'package.json'))
expect(manifest.pnpm?.overrides?.['axios@<=0.18.0']).toBeFalsy()
expect(manifest.pnpm?.overrides?.['axios@<0.21.1']).toBeFalsy()
expect(manifest.pnpm?.overrides?.['minimist@<0.2.1']).toBeFalsy()
expect(manifest.pnpm?.overrides?.['url-parse@<1.5.6']).toBeTruthy()
const manifest = readYamlFile<{ overrides?: Record<string, string> }>(path.join(tmp, 'pnpm-workspace.yaml'))
expect(manifest.overrides?.['axios@<=0.18.0']).toBeFalsy()
expect(manifest.overrides?.['axios@<0.21.1']).toBeFalsy()
expect(manifest.overrides?.['minimist@<0.2.1']).toBeFalsy()
expect(manifest.overrides?.['url-parse@<1.5.6']).toBeTruthy()
})

View File

@@ -67,11 +67,12 @@ export const DEFAULT_OPTS = {
}
describe('plugin-commands-audit', () => {
const hasVulnerabilitiesDir = f.find('has-vulnerabilities')
beforeAll(async () => {
await install.handler({
...DEFAULT_OPTS,
frozenLockfile: true,
dir: f.find('has-vulnerabilities'),
dir: hasVulnerabilitiesDir,
})
})
test('audit', async () => {
@@ -80,7 +81,8 @@ describe('plugin-commands-audit', () => {
.reply(200, responses.ALL_VULN_RESP)
const { output, exitCode } = await audit.handler({
dir: f.find('has-vulnerabilities'),
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
userConfig: {},
rawConfig,
registries,
@@ -96,7 +98,8 @@ describe('plugin-commands-audit', () => {
.reply(200, responses.DEV_VULN_ONLY_RESP)
const { output, exitCode } = await audit.handler({
dir: f.find('has-vulnerabilities'),
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
dev: true,
production: false,
userConfig: {},
@@ -116,7 +119,8 @@ describe('plugin-commands-audit', () => {
const { output, exitCode } = await audit.handler({
auditLevel: 'moderate',
dir: f.find('has-vulnerabilities'),
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
userConfig: {},
rawConfig,
registries,
@@ -133,7 +137,8 @@ describe('plugin-commands-audit', () => {
.reply(200, responses.NO_VULN_RESP)
const { output, exitCode } = await audit.handler({
dir: f.find('has-outdated-deps'),
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
userConfig: {},
rawConfig,
registries,
@@ -150,7 +155,8 @@ describe('plugin-commands-audit', () => {
.reply(200, responses.ALL_VULN_RESP)
const { output, exitCode } = await audit.handler({
dir: f.find('has-vulnerabilities'),
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
json: true,
userConfig: {},
rawConfig,
@@ -170,7 +176,8 @@ describe('plugin-commands-audit', () => {
const { output, exitCode } = await audit.handler({
auditLevel: 'high',
dir: f.find('has-vulnerabilities'),
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
userConfig: {},
rawConfig,
dev: true,
@@ -188,7 +195,8 @@ describe('plugin-commands-audit', () => {
.post('/-/npm/v1/security/audits')
.reply(500, { message: 'Something bad happened' })
const { output, exitCode } = await audit.handler({
dir: f.find('has-vulnerabilities'),
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
dev: true,
fetchRetries: 0,
ignoreRegistryErrors: true,
@@ -211,7 +219,8 @@ describe('plugin-commands-audit', () => {
.reply(200, responses.NO_VULN_RESP)
const { output, exitCode } = await audit.handler({
dir: f.find('has-outdated-deps'),
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
userConfig: {},
rawConfig: {
registry: registries.default,
@@ -231,7 +240,8 @@ describe('plugin-commands-audit', () => {
.reply(404, {})
await expect(audit.handler({
dir: f.find('has-vulnerabilities'),
dir: hasVulnerabilitiesDir,
rootProjectManifestDir: hasVulnerabilitiesDir,
dev: true,
fetchRetries: 0,
ignoreRegistryErrors: false,
@@ -253,6 +263,7 @@ describe('plugin-commands-audit', () => {
const { exitCode, output } = await audit.handler({
auditLevel: 'moderate',
dir: tmp,
rootProjectManifestDir: tmp,
userConfig: {},
rawConfig,
registries,
@@ -285,6 +296,7 @@ describe('plugin-commands-audit', () => {
const { exitCode, output } = await audit.handler({
auditLevel: 'moderate',
dir: tmp,
rootProjectManifestDir: tmp,
userConfig: {},
rawConfig,
registries,
@@ -317,6 +329,7 @@ describe('plugin-commands-audit', () => {
const { exitCode, output } = await audit.handler({
auditLevel: 'moderate',
dir: tmp,
rootProjectManifestDir: tmp,
json: true,
userConfig: {},
rawConfig,

View File

@@ -20,6 +20,9 @@
{
"path": "../../config/config"
},
{
"path": "../../config/config-writer"
},
{
"path": "../../network/auth-header"
},

6
pnpm-lock.yaml generated
View File

@@ -3518,6 +3518,9 @@ importers:
'@pnpm/config':
specifier: workspace:*
version: link:../../config/config
'@pnpm/config.config-writer':
specifier: workspace:*
version: link:../../config/config-writer
'@pnpm/constants':
specifier: workspace:*
version: link:../../packages/constants
@@ -3573,6 +3576,9 @@ importers:
nock:
specifier: 'catalog:'
version: 13.3.4
read-yaml-file:
specifier: 'catalog:'
version: 2.1.0
tempy:
specifier: 'catalog:'
version: 1.0.1