feat: pnpm audit --fix (#3598)

close #3067
This commit is contained in:
Zoltan Kochan
2021-07-19 23:14:51 +03:00
committed by GitHub
parent 0554d9201e
commit a5f698290c
14 changed files with 129 additions and 11 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/plugin-commands-audit": minor
"pnpm": minor
---
New command added: `pnpm audit --fix`. This command adds overrides to `package.json` that force versions of packages that do not have the vulnerabilities.

View File

@@ -29,11 +29,14 @@
},
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/plugin-commands-audit#readme",
"devDependencies": {
"@pnpm/test-fixtures": "workspace:*",
"@pnpm/types": "workspace:7.3.0",
"@types/ramda": "0.27.39",
"@types/zkochan__table": "npm:@types/table@6.0.0",
"load-json-file": "^6.2.0",
"nock": "12.0.3",
"strip-ansi": "^6.0.0"
"strip-ansi": "^6.0.0",
"tempy": "^1.0.0"
},
"dependencies": {
"@pnpm/audit": "workspace:2.1.6",
@@ -42,6 +45,7 @@
"@pnpm/constants": "workspace:5.0.0",
"@pnpm/error": "workspace:2.0.0",
"@pnpm/lockfile-file": "workspace:4.1.0",
"@pnpm/read-project-manifest": "workspace:2.0.4",
"@zkochan/table": "^1.0.0",
"chalk": "^4.1.0",
"ramda": "^0.27.1",

View File

@@ -9,6 +9,7 @@ import { table } from '@zkochan/table'
import chalk from 'chalk'
import pick from 'ramda/src/pick'
import renderHelp from 'render-help'
import fix from './fix'
// eslint-disable
const AUDIT_LEVEL_NUMBER = {
@@ -39,6 +40,7 @@ export function cliOptionsTypes () {
'registry',
], allTypes),
'audit-level': ['low', 'moderate', 'high', 'critical'],
fix: Boolean,
'ignore-registry-errors': Boolean,
}
}
@@ -58,6 +60,10 @@ export function help () {
title: 'Options',
list: [
{
description: 'Add overrides to the package.json file in order to force non-vulnerable versions of the dependencies',
name: '--fix',
},
{
description: 'Output audit report in JSON format',
name: '--json',
@@ -95,6 +101,7 @@ export function help () {
export async function handler (
opts: Pick<UniversalOptions, 'dir'> & {
auditLevel?: 'low' | 'moderate' | 'high' | 'critical'
fix?: boolean
ignoreRegistryErrors?: boolean
json?: boolean
lockfileDir?: string
@@ -131,6 +138,23 @@ export async function handler (
}
}
}
if (opts.fix) {
const newOverrides = await fix(opts.dir, auditReport)
if (Object.values(newOverrides).length === 0) {
return {
exitCode: 0,
output: 'No fixes were made',
}
}
return {
exitCode: 0,
output: `${Object.values(newOverrides).length} overrides were added to package.json to fix vulnerabilities.
Run "pnpm install" to apply the fixes.
The added overrides:
${JSON.stringify(newOverrides, null, 2)}`,
}
}
const vulnerabilities = auditReport.metadata.vulnerabilities
const totalVulnerabilityCount = Object.values(vulnerabilities)
.reduce((sum: number, vulnerabilitiesCount: number) => sum + vulnerabilitiesCount, 0)

View File

@@ -0,0 +1,27 @@
import { AuditReport, AuditAdvisory } from '@pnpm/audit'
import readProjectManifest from '@pnpm/read-project-manifest'
import fromPairs from 'ramda/src/fromPairs'
export default async function fix (dir: string, auditReport: AuditReport) {
const { manifest, writeProjectManifest } = await readProjectManifest(dir)
const vulnOverrides = createOverrides(Object.values(auditReport.advisories))
if (Object.values(vulnOverrides).length === 0) return vulnOverrides
await writeProjectManifest({
...manifest,
pnpm: {
...manifest.pnpm,
overrides: {
...manifest.pnpm?.overrides,
...vulnOverrides,
},
},
})
return vulnOverrides
}
function createOverrides (advisories: AuditAdvisory[]) {
return fromPairs(advisories.map((advisory) => [
`${advisory.module_name}@${advisory.vulnerable_versions}`,
advisory.patched_versions,
]))
}

View File

@@ -0,0 +1,43 @@
import path from 'path'
import { copyFixture } from '@pnpm/test-fixtures'
import { ProjectManifest } from '@pnpm/types'
import { audit } from '@pnpm/plugin-commands-audit'
import loadJsonFile from 'load-json-file'
import tempy from 'tempy'
test('overrides are added for vulnerable dependencies', async () => {
const tmp = tempy.directory()
await copyFixture('has-vulnerabilities', tmp, __dirname)
const { exitCode, output } = await audit.handler({
auditLevel: 'moderate',
dir: tmp,
fix: true,
registries: {
default: 'https://registry.npmjs.org/',
},
})
expect(exitCode).toBe(0)
expect(output).toMatch(/Run "pnpm install"/)
const manifest = await loadJsonFile<ProjectManifest>(path.join(tmp, 'package.json'))
expect(manifest.pnpm?.overrides?.['axios@<0.18.1']).toBe('>=0.18.1')
})
test('no overrides are added if no vulnerabilities are found', async () => {
const tmp = tempy.directory()
await copyFixture('fixture', tmp, __dirname)
const { exitCode, output } = await audit.handler({
auditLevel: 'moderate',
dir: tmp,
fix: true,
registries: {
default: 'https://registry.npmjs.org/',
},
})
expect(exitCode).toBe(0)
expect(output).toBe('No fixes were made')
})

View File

@@ -8,7 +8,7 @@ const skipOnNode10 = process.version.split('.')[0] === 'v10' ? test.skip : test
// The audits give different results on Node 10, for some reason
skipOnNode10('audit', async () => {
const { output, exitCode } = await audit.handler({
dir: path.join(__dirname, 'packages/has-vulnerabilities'),
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
registries: {
default: 'https://registry.npmjs.org/',
},
@@ -19,7 +19,7 @@ skipOnNode10('audit', async () => {
test('audit --dev', async () => {
const { output, exitCode } = await audit.handler({
dir: path.join(__dirname, 'packages/has-vulnerabilities'),
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
dev: true,
production: false,
registries: {
@@ -34,7 +34,7 @@ test('audit --dev', async () => {
test('audit --audit-level', async () => {
const { output, exitCode } = await audit.handler({
auditLevel: 'moderate',
dir: path.join(__dirname, 'packages/has-vulnerabilities'),
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
registries: {
default: 'https://registry.npmjs.org/',
},
@@ -58,7 +58,7 @@ test('audit: no vulnerabilities', async () => {
test('audit --json', async () => {
const { output, exitCode } = await audit.handler({
dir: path.join(__dirname, 'packages/has-vulnerabilities'),
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
json: true,
registries: {
default: 'https://registry.npmjs.org/',
@@ -73,7 +73,7 @@ test('audit --json', async () => {
test.skip('audit does not exit with code 1 if the found vulnerabilities are having lower severity then what we asked for', async () => {
const { output, exitCode } = await audit.handler({
auditLevel: 'high',
dir: path.join(__dirname, 'packages/has-vulnerabilities'),
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
dev: true,
registries: {
default: 'https://registry.npmjs.org/',
@@ -91,7 +91,7 @@ test('audit does not exit with code 1 if the registry responds with a non-200 re
.post('/-/npm/v1/security/audits')
.reply(500, { message: 'Something bad happened' })
const { output, exitCode } = await audit.handler({
dir: path.join(__dirname, 'packages/has-vulnerabilities'),
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
dev: true,
ignoreRegistryErrors: true,
production: false,

View File

@@ -9,6 +9,9 @@
"../../typings/**/*.d.ts"
],
"references": [
{
"path": "../../privatePackages/test-fixtures"
},
{
"path": "../audit"
},
@@ -27,6 +30,9 @@
{
"path": "../lockfile-file"
},
{
"path": "../read-project-manifest"
},
{
"path": "../types"
}

8
pnpm-lock.yaml generated
View File

@@ -1722,15 +1722,19 @@ importers:
'@pnpm/error': workspace:2.0.0
'@pnpm/lockfile-file': workspace:4.1.0
'@pnpm/plugin-commands-audit': 'link:'
'@pnpm/read-project-manifest': workspace:2.0.4
'@pnpm/test-fixtures': workspace:*
'@pnpm/types': workspace:7.3.0
'@types/ramda': 0.27.39
'@types/zkochan__table': npm:@types/table@6.0.0
'@zkochan/table': ^1.0.0
chalk: ^4.1.0
load-json-file: ^6.2.0
nock: 12.0.3
ramda: ^0.27.1
render-help: ^1.0.1
strip-ansi: ^6.0.0
tempy: ^1.0.0
dependencies:
'@pnpm/audit': link:../audit
'@pnpm/cli-utils': link:../cli-utils
@@ -1738,17 +1742,21 @@ importers:
'@pnpm/constants': link:../constants
'@pnpm/error': link:../error
'@pnpm/lockfile-file': link:../lockfile-file
'@pnpm/read-project-manifest': link:../read-project-manifest
'@zkochan/table': 1.0.0
chalk: 4.1.1
ramda: 0.27.1
render-help: 1.0.2
devDependencies:
'@pnpm/plugin-commands-audit': 'link:'
'@pnpm/test-fixtures': link:../../privatePackages/test-fixtures
'@pnpm/types': link:../types
'@types/ramda': 0.27.39
'@types/zkochan__table': /@types/table/6.0.0
load-json-file: 6.2.0
nock: 12.0.3
strip-ansi: 6.0.0
tempy: 1.0.1
packages/plugin-commands-import:
specifiers:

View File

@@ -5,14 +5,14 @@ import ncpCB from 'ncp'
const ncp = promisify(ncpCB)
export async function copyFixture (fixtureName: string, dest: string) {
const fixturePath = pathToLocalPkg(fixtureName)
export async function copyFixture (fixtureName: string, dest: string, searchFromDir?: string) {
const fixturePath = pathToLocalPkg(fixtureName, searchFromDir)
if (!fixturePath) throw new Error(`${fixtureName} not found`)
return ncp(fixturePath, dest)
}
export function pathToLocalPkg (pkgName: string) {
let dir = __dirname
export function pathToLocalPkg (pkgName: string, _dir?: string) {
let dir = _dir ?? __dirname
const { root } = path.parse(dir)
while (true) {
const checkDir = path.join(dir, 'fixtures', pkgName)