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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

6
pnpm-lock.yaml generated
View File

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