mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-12 10:11:42 -04:00
6
.changeset/eight-weeks-double.md
Normal file
6
.changeset/eight-weeks-double.md
Normal 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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
27
packages/plugin-commands-audit/src/fix.ts
Normal file
27
packages/plugin-commands-audit/src/fix.ts
Normal 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,
|
||||
]))
|
||||
}
|
||||
43
packages/plugin-commands-audit/test/fix.ts
Normal file
43
packages/plugin-commands-audit/test/fix.ts
Normal 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')
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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
8
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user