mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-29 03:26:25 -04:00
feat(audit): add fix update mode (#10341)
* feat(audit): add fix update mode Add the ability to fix vulnerabilities by updating packages in the lockfile instead of adding overrides. * revert: remove audit-registry parameter * fix: properly invoke audit command recursively on workspace * fix: negative weight version priority & top-level pinned dep updating * refactor: apply packageVulnerabilityAudit version preferences earlier * chore: update changeset * fix: vulnerability penalties are greater than direct dep weight * test: use nock on mock registry directly * fix: exit with 1 if it can't resolve all vulnerabilities to match npm * fix: properly update workspace top-level pinned vulnerable dependencies * fix: update lockfile * fix: update vulnerabilities in catalogs * chore: sync pnpm-lock.yaml with main
This commit is contained in:
12
.changeset/stupid-carpets-teach.md
Normal file
12
.changeset/stupid-carpets-teach.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-installation": minor
|
||||
"@pnpm/resolve-dependencies": minor
|
||||
"@pnpm/plugin-commands-audit": minor
|
||||
"@pnpm/npm-resolver": minor
|
||||
"@pnpm/core": minor
|
||||
"@pnpm/types": minor
|
||||
"@pnpm-private/updater": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
Add the ability to fix vulnerabilities by updating packages in the lockfile instead of adding overrides.
|
||||
@@ -269,6 +269,7 @@ async function updateManifest (workspaceDir: string, manifest: ProjectManifest,
|
||||
case '@pnpm/plugin-commands-script-runners':
|
||||
case '@pnpm/plugin-commands-store':
|
||||
case '@pnpm/plugin-commands-deploy':
|
||||
case '@pnpm/plugin-commands-audit':
|
||||
case CLI_PKG_NAME:
|
||||
case '@pnpm/core': {
|
||||
preset = '@pnpm/jest-config/with-registry'
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
"noent",
|
||||
"nonexec",
|
||||
"noninjected",
|
||||
"nonvulnerable",
|
||||
"nopadding",
|
||||
"noproxy",
|
||||
"nosystem",
|
||||
|
||||
@@ -40,20 +40,27 @@
|
||||
"@pnpm/constants": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/lockfile.fs": "workspace:*",
|
||||
"@pnpm/lockfile.types": "workspace:*",
|
||||
"@pnpm/lockfile.utils": "workspace:*",
|
||||
"@pnpm/lockfile.walker": "workspace:*",
|
||||
"@pnpm/network.auth-header": "workspace:*",
|
||||
"@pnpm/plugin-commands-installation": "workspace:*",
|
||||
"@pnpm/read-project-manifest": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"@zkochan/table": "catalog:",
|
||||
"chalk": "catalog:",
|
||||
"memoize": "catalog:",
|
||||
"ramda": "catalog:",
|
||||
"render-help": "catalog:"
|
||||
"render-help": "catalog:",
|
||||
"semver": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/filter-workspace-packages": "workspace:*",
|
||||
"@pnpm/plugin-commands-audit": "workspace:*",
|
||||
"@pnpm/plugin-commands-installation": "workspace:*",
|
||||
"@pnpm/registry-mock": "catalog:",
|
||||
"@pnpm/test-fixtures": "workspace:*",
|
||||
"@types/ramda": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@types/zkochan__table": "catalog:",
|
||||
"load-json-file": "catalog:",
|
||||
"nock": "catalog:",
|
||||
@@ -64,6 +71,6 @@
|
||||
"node": ">=22.13"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "@pnpm/jest-config"
|
||||
"preset": "@pnpm/jest-config/with-registry"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { audit, type AuditLevelNumber, type AuditLevelString, type AuditReport, type AuditVulnerabilityCounts, type IgnoredAuditVulnerabilityCounts } from '@pnpm/audit'
|
||||
import { audit, type AuditLevelNumber, type AuditLevelString, type AuditReport, type AuditAdvisory, type AuditVulnerabilityCounts, type IgnoredAuditVulnerabilityCounts } from '@pnpm/audit'
|
||||
import { createGetAuthHeaderByURI } from '@pnpm/network.auth-header'
|
||||
import { docsUrl, TABLE_OPTIONS } from '@pnpm/cli-utils'
|
||||
import { type Config, types as allTypes, type UniversalOptions } from '@pnpm/config'
|
||||
@@ -6,14 +6,15 @@ import { WANTED_LOCKFILE } from '@pnpm/constants'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { readEnvLockfile, readWantedLockfile } from '@pnpm/lockfile.fs'
|
||||
import type { Registries } from '@pnpm/types'
|
||||
import { update, type InstallCommandOptions } from '@pnpm/plugin-commands-installation'
|
||||
import { table } from '@zkochan/table'
|
||||
import chalk, { type ChalkInstance } from 'chalk'
|
||||
import { difference, pick, pickBy } from 'ramda'
|
||||
import { renderHelp } from 'render-help'
|
||||
import { fix } from './fix.js'
|
||||
import { fixWithUpdate, type FixWithUpdateResult } from './fixWithUpdate.js'
|
||||
import { ignore } from './ignore.js'
|
||||
|
||||
// eslint-disable
|
||||
const AUDIT_LEVEL_NUMBER = {
|
||||
low: 0,
|
||||
moderate: 1,
|
||||
@@ -37,14 +38,12 @@ const AUDIT_TABLE_OPTIONS = {
|
||||
},
|
||||
},
|
||||
}
|
||||
// eslint-enable
|
||||
|
||||
const MAX_PATHS_COUNT = 3
|
||||
|
||||
export const rcOptionsTypes = cliOptionsTypes
|
||||
|
||||
export function cliOptionsTypes (): Record<string, unknown> {
|
||||
export function rcOptionsTypes (): Record<string, unknown> {
|
||||
return {
|
||||
...update.rcOptionsTypes(),
|
||||
...pick([
|
||||
'dev',
|
||||
'json',
|
||||
@@ -54,13 +53,25 @@ export function cliOptionsTypes (): Record<string, unknown> {
|
||||
'registry',
|
||||
], allTypes),
|
||||
'audit-level': ['low', 'moderate', 'high', 'critical'],
|
||||
fix: Boolean,
|
||||
// For fix, use String instead of a list of allowed string values.
|
||||
// Otherwise, an unexpected value will get coerced to true because of the Boolean type.
|
||||
fix: [String, Boolean],
|
||||
'ignore-registry-errors': Boolean,
|
||||
ignore: [String, Array],
|
||||
'ignore-unfixable': Boolean,
|
||||
}
|
||||
}
|
||||
|
||||
export function cliOptionsTypes (): Record<string, unknown> {
|
||||
return {
|
||||
...pick([
|
||||
'recursive',
|
||||
'workspace',
|
||||
], update.cliOptionsTypes()),
|
||||
...rcOptionsTypes(),
|
||||
}
|
||||
}
|
||||
|
||||
export const shorthands: Record<string, string> = {
|
||||
D: '--dev',
|
||||
P: '--production',
|
||||
@@ -77,8 +88,8 @@ export function help (): string {
|
||||
|
||||
list: [
|
||||
{
|
||||
description: 'Add overrides to the package.json file in order to force non-vulnerable versions of the dependencies',
|
||||
name: '--fix',
|
||||
description: 'Fix the audited vulnerabilities using the specified method: "override" or "update". "override" adds overrides to the package.json file in order to force non-vulnerable versions of the dependencies. "update" attempts to update the vulnerable packages in the lockfile to non-vulnerable versions. If no method is specified, "override" is used by default.',
|
||||
name: '--fix [method]',
|
||||
},
|
||||
{
|
||||
description: 'Output audit report in JSON format',
|
||||
@@ -123,7 +134,7 @@ export function help (): string {
|
||||
}
|
||||
|
||||
export type AuditOptions = Pick<UniversalOptions, 'dir'> & {
|
||||
fix?: boolean
|
||||
fix?: boolean | 'override' | 'update'
|
||||
ignoreRegistryErrors?: boolean
|
||||
json?: boolean
|
||||
lockfileDir?: string
|
||||
@@ -156,7 +167,9 @@ export type AuditOptions = Pick<UniversalOptions, 'dir'> & {
|
||||
| 'rootProjectManifestDir'
|
||||
| 'virtualStoreDirMaxLength'
|
||||
| 'workspaceDir'
|
||||
>
|
||||
> & InstallCommandOptions
|
||||
|
||||
const DEFAULT_FIX_METHOD = 'override'
|
||||
|
||||
export async function handler (opts: AuditOptions): Promise<{ exitCode: number, output: string }> {
|
||||
const lockfileDir = opts.lockfileDir ?? opts.dir
|
||||
@@ -209,7 +222,24 @@ export async function handler (opts: AuditOptions): Promise<{ exitCode: number,
|
||||
|
||||
throw err
|
||||
}
|
||||
if (opts.fix) {
|
||||
let fixMethod: 'update' | 'override' | undefined
|
||||
if (opts.fix === 'update' || opts.fix === 'override') {
|
||||
fixMethod = opts.fix
|
||||
} else if (opts.fix === true) {
|
||||
fixMethod = DEFAULT_FIX_METHOD
|
||||
} else if (!opts.fix) {
|
||||
fixMethod = undefined
|
||||
} else {
|
||||
throw new PnpmError('INVALID_FIX_OPTION', `Invalid value for --fix: ${opts.fix as string}. Should be one of "override" or "update"`)
|
||||
}
|
||||
if (fixMethod === 'update') {
|
||||
const result = await fixWithUpdate(auditReport, { ...opts, include })
|
||||
return {
|
||||
exitCode: result.remaining.length > 0 ? 1 : 0,
|
||||
output: formatFixWithUpdateOutput(result, auditReport),
|
||||
}
|
||||
}
|
||||
if (fixMethod === 'override') {
|
||||
const newOverrides = await fix(auditReport, opts)
|
||||
if (Object.values(newOverrides).length === 0) {
|
||||
return {
|
||||
@@ -327,3 +357,57 @@ function reportSummary (vulnerabilities: AuditVulnerabilityCounts, totalVulnerab
|
||||
.join(' | ')
|
||||
}`
|
||||
}
|
||||
|
||||
export function formatFixWithUpdateOutput (result: FixWithUpdateResult, auditReport: AuditReport): string {
|
||||
const output: string[] = []
|
||||
|
||||
interface IdAndAdvisory {
|
||||
id: number
|
||||
advisory?: AuditAdvisory
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the given array of advisory IDs by severity descending
|
||||
*/
|
||||
function sortBySeverity (ids: number[]): IdAndAdvisory[] {
|
||||
return ids.map(id => ({ id, advisory: auditReport.advisories[id] })).sort((a, b) => {
|
||||
const aValue = a.advisory ? AUDIT_LEVEL_NUMBER[a.advisory.severity] : -1
|
||||
const bValue = b.advisory ? AUDIT_LEVEL_NUMBER[b.advisory.severity] : -1
|
||||
return bValue - aValue
|
||||
})
|
||||
}
|
||||
|
||||
const fixed = sortBySeverity(result.fixed)
|
||||
const remaining = sortBySeverity(result.remaining)
|
||||
|
||||
const fixedString = fixed.length === 1 ? 'vulnerability was fixed' : 'vulnerabilities were fixed'
|
||||
const remainingString = remaining.length === 1 ? 'vulnerability remains' : 'vulnerabilities remain'
|
||||
|
||||
output.push(`${chalk.green(fixed.length)} ${fixedString}, ${chalk.red(remaining.length)} ${remainingString}.`)
|
||||
|
||||
function summarizeAdvisory (fixed: boolean, { id, advisory }: IdAndAdvisory): string {
|
||||
if (advisory) {
|
||||
const color = fixed ? chalk.green : AUDIT_COLOR[advisory.severity]
|
||||
return `- (${color(advisory.severity)}) "${color(advisory.title)}" ${chalk.blue(advisory.module_name)}`
|
||||
}
|
||||
return `- Advisory with ID ${id} (details not found in the audit report)`
|
||||
}
|
||||
|
||||
if (fixed.length > 0) {
|
||||
output.push('\nThe fixed vulnerabilities are:')
|
||||
for (const f of fixed) {
|
||||
output.push(summarizeAdvisory(true, f))
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining.length > 0) {
|
||||
output.push('\nThe remaining vulnerabilities are:')
|
||||
for (const r of remaining) {
|
||||
output.push(summarizeAdvisory(false, r))
|
||||
}
|
||||
}
|
||||
|
||||
// Add trailing newline
|
||||
output.push('')
|
||||
return output.join('\n')
|
||||
}
|
||||
|
||||
139
lockfile/plugin-commands-audit/src/fixWithUpdate.ts
Normal file
139
lockfile/plugin-commands-audit/src/fixWithUpdate.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { AuditReport } from '@pnpm/audit'
|
||||
import type {
|
||||
VulnerabilitySeverity,
|
||||
PackageVulnerability,
|
||||
PackageVulnerabilityAudit,
|
||||
DependenciesField,
|
||||
} from '@pnpm/types'
|
||||
import { update } from '@pnpm/plugin-commands-installation'
|
||||
import semver from 'semver'
|
||||
import { WANTED_LOCKFILE } from '@pnpm/constants'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { readWantedLockfile } from '@pnpm/lockfile.fs'
|
||||
import type { AuditOptions } from './audit.js'
|
||||
import { lockfileToPackages } from './lockfileToPackages.js'
|
||||
|
||||
interface ExtendedPackageVulnerability {
|
||||
vulnerability: PackageVulnerability
|
||||
id: number
|
||||
semverRange?: semver.Range
|
||||
}
|
||||
|
||||
export interface FixWithUpdateResult {
|
||||
// IDs of packages that were fixed
|
||||
fixed: number[]
|
||||
// IDs of packages that could not be fixed
|
||||
remaining: number[]
|
||||
}
|
||||
|
||||
export type FixWithUpdateOptions = AuditOptions & {
|
||||
include?: { [dependenciesField in DependenciesField]: boolean }
|
||||
}
|
||||
|
||||
export async function fixWithUpdate (auditReport: AuditReport, opts: FixWithUpdateOptions): Promise<FixWithUpdateResult> {
|
||||
const vulnerabilitiesByPackage = new Map<string, ExtendedPackageVulnerability[]>()
|
||||
const unfixableVulnerabilities = new Map<string, Set<number>>()
|
||||
for (const advisory of Object.values(auditReport.advisories)) {
|
||||
let packageVulnerabilities = vulnerabilitiesByPackage.get(advisory.module_name)
|
||||
if (!packageVulnerabilities) {
|
||||
packageVulnerabilities = []
|
||||
vulnerabilitiesByPackage.set(advisory.module_name, packageVulnerabilities)
|
||||
}
|
||||
const severity: VulnerabilitySeverity = advisory.severity
|
||||
const versionRange = advisory.vulnerable_versions
|
||||
if (versionRange === '>=0.0.0' || versionRange === '*') {
|
||||
// skip unfixable vulnerabilities
|
||||
let unfixableForPackage = unfixableVulnerabilities.get(advisory.module_name)
|
||||
if (!unfixableForPackage) {
|
||||
unfixableForPackage = new Set()
|
||||
unfixableVulnerabilities.set(advisory.module_name, unfixableForPackage)
|
||||
}
|
||||
unfixableForPackage.add(advisory.id)
|
||||
continue
|
||||
}
|
||||
packageVulnerabilities.push({
|
||||
vulnerability: {
|
||||
versionRange,
|
||||
severity,
|
||||
},
|
||||
id: advisory.id,
|
||||
})
|
||||
}
|
||||
|
||||
const packageVulnerabilityAudit: PackageVulnerabilityAudit = {
|
||||
isVulnerable (packageName: string, version: string): boolean {
|
||||
const vulnerabilities = vulnerabilitiesByPackage.get(packageName)
|
||||
if (!vulnerabilities) return false
|
||||
for (const vulnerabilityWithRange of vulnerabilities) {
|
||||
let { semverRange } = vulnerabilityWithRange
|
||||
if (!semverRange) {
|
||||
semverRange = new semver.Range(vulnerabilityWithRange.vulnerability.versionRange)
|
||||
vulnerabilityWithRange.semverRange = semverRange
|
||||
}
|
||||
if (semver.satisfies(version, semverRange)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
getVulnerabilities (): Map<string, PackageVulnerability[]> {
|
||||
const allVulnerabilities = new Map<string, PackageVulnerability[]>()
|
||||
for (const [pkgName, vulnerabilities] of vulnerabilitiesByPackage) {
|
||||
allVulnerabilities.set(pkgName, vulnerabilities.map(v => v.vulnerability))
|
||||
}
|
||||
return allVulnerabilities
|
||||
},
|
||||
}
|
||||
|
||||
await update.handler({
|
||||
...opts,
|
||||
packageVulnerabilityAudit,
|
||||
}, [])
|
||||
|
||||
const lockfileDir = opts.lockfileDir ?? opts.dir
|
||||
const lockfile = await readWantedLockfile(lockfileDir, { ignoreIncompatible: true })
|
||||
if (lockfile == null) {
|
||||
throw new PnpmError('AUDIT_NO_LOCKFILE', `No ${WANTED_LOCKFILE} found after update: Cannot report fixed vulnerabilities`)
|
||||
}
|
||||
const updatedPackages = lockfileToPackages(lockfile, { include: opts.include })
|
||||
|
||||
const fixed: number[] = []
|
||||
const remaining: number[] = []
|
||||
|
||||
for (const [pkgName, vulnerabilities] of vulnerabilitiesByPackage) {
|
||||
const updatedVersions = updatedPackages.get(pkgName)
|
||||
if (!updatedVersions) {
|
||||
fixed.push(...vulnerabilities.map(v => v.id))
|
||||
continue
|
||||
}
|
||||
for (const vulnerability of vulnerabilities) {
|
||||
let wasFixed = true
|
||||
for (const updatedVersion of updatedVersions) {
|
||||
let { semverRange } = vulnerability
|
||||
if (!semverRange) {
|
||||
semverRange = new semver.Range(vulnerability.vulnerability.versionRange)
|
||||
vulnerability.semverRange = semverRange
|
||||
}
|
||||
if (semver.satisfies(updatedVersion, semverRange)) {
|
||||
wasFixed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (wasFixed) {
|
||||
fixed.push(vulnerability.id)
|
||||
} else {
|
||||
remaining.push(vulnerability.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [pkgName, unfixableIds] of unfixableVulnerabilities) {
|
||||
if (updatedPackages.has(pkgName)) {
|
||||
remaining.push(...unfixableIds)
|
||||
} else {
|
||||
fixed.push(...unfixableIds)
|
||||
}
|
||||
}
|
||||
|
||||
return { fixed, remaining }
|
||||
}
|
||||
31
lockfile/plugin-commands-audit/src/lockfileToPackages.ts
Normal file
31
lockfile/plugin-commands-audit/src/lockfileToPackages.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { LockfileObject } from '@pnpm/lockfile.types'
|
||||
import { nameVerFromPkgSnapshot } from '@pnpm/lockfile.utils'
|
||||
import { lockfileWalkerGroupImporterSteps, type LockfileWalkerStep } from '@pnpm/lockfile.walker'
|
||||
import type { DependenciesField, ProjectId } from '@pnpm/types'
|
||||
|
||||
export function lockfileToPackages (
|
||||
lockfile: LockfileObject,
|
||||
opts: {
|
||||
include?: { [dependenciesField in DependenciesField]: boolean }
|
||||
}
|
||||
): Map<string, Set<string>> {
|
||||
const importerWalkers = lockfileWalkerGroupImporterSteps(lockfile, Object.keys(lockfile.importers) as ProjectId[], { include: opts?.include })
|
||||
const packages = new Map<string, Set<string>>()
|
||||
for (const importerWalker of importerWalkers) {
|
||||
addPackages(packages, importerWalker.step)
|
||||
}
|
||||
return packages
|
||||
}
|
||||
|
||||
function addPackages (packages: Map<string, Set<string>>, step: LockfileWalkerStep) {
|
||||
for (const { depPath, pkgSnapshot, next } of step.dependencies) {
|
||||
const { name, version } = nameVerFromPkgSnapshot(depPath, pkgSnapshot)
|
||||
if (version != null) {
|
||||
if (!packages.has(name)) {
|
||||
packages.set(name, new Set())
|
||||
}
|
||||
packages.get(name)!.add(version)
|
||||
}
|
||||
addPackages(packages, next())
|
||||
}
|
||||
}
|
||||
@@ -4,31 +4,23 @@ import { audit } from '@pnpm/plugin-commands-audit'
|
||||
import { readYamlFileSync } from 'read-yaml-file'
|
||||
import nock from 'nock'
|
||||
import * as responses from './utils/responses/index.js'
|
||||
import { AUDIT_REGISTRY_OPTS, AUDIT_REGISTRY } from './utils/options.js'
|
||||
|
||||
const f = fixtures(import.meta.dirname)
|
||||
const registries = {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
}
|
||||
const rawConfig = {
|
||||
registry: registries.default,
|
||||
}
|
||||
|
||||
test('overrides are added for vulnerable dependencies', async () => {
|
||||
const tmp = f.prepare('has-vulnerabilities')
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
@@ -42,19 +34,16 @@ test('overrides are added for vulnerable dependencies', async () => {
|
||||
test('no overrides are added if no vulnerabilities are found', async () => {
|
||||
const tmp = f.prepare('fixture')
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.NO_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
@@ -64,11 +53,12 @@ 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')
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
auditConfig: {
|
||||
ignoreCves: [
|
||||
@@ -81,10 +71,6 @@ test('CVEs found in the allow list are not added as overrides', async () => {
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
expect(exitCode).toBe(0)
|
||||
expect(output).toMatch(/Run "pnpm install"/)
|
||||
|
||||
795
lockfile/plugin-commands-audit/test/fixWithUpdate.ts
Normal file
795
lockfile/plugin-commands-audit/test/fixWithUpdate.ts
Normal file
@@ -0,0 +1,795 @@
|
||||
import { join } from 'path'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { fixtures } from '@pnpm/test-fixtures'
|
||||
import { audit } from '@pnpm/plugin-commands-audit'
|
||||
import { readWantedLockfile } from '@pnpm/lockfile.fs'
|
||||
import { readProjectManifest } from '@pnpm/read-project-manifest'
|
||||
import { filterPackagesFromDir } from '@pnpm/filter-workspace-packages'
|
||||
import type { DepPath } from '@pnpm/types'
|
||||
import { addDistTag } from '@pnpm/registry-mock'
|
||||
import chalk from 'chalk'
|
||||
import nock from 'nock'
|
||||
import { readYamlFileSync } from 'read-yaml-file'
|
||||
import { MOCK_REGISTRY, MOCK_REGISTRY_OPTS } from './utils/options.js'
|
||||
|
||||
const f = fixtures(import.meta.dirname)
|
||||
|
||||
describe('audit fix with update', () => {
|
||||
afterEach(() => nock.cleanAll())
|
||||
test('top-level vulnerability is fixed by updating the vulnerable package', async () => {
|
||||
const tmp = f.prepare('update-single-depth-2')
|
||||
|
||||
const originalPkgId = '@pnpm.e2e/pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedPkgId = '@pnpm.e2e/pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const { manifest: originalManifest } = await readProjectManifest(tmp)
|
||||
expect(originalManifest).toBeTruthy()
|
||||
expect(originalManifest.dependencies).toBeDefined()
|
||||
expect(originalManifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.0.0')
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'top-level-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(1)} vulnerability was fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('high')}) "${chalk.green('Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/pkg-with-1-dep')}
|
||||
`)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
const { manifest } = await readProjectManifest(tmp)
|
||||
expect(manifest).toBeTruthy()
|
||||
expect(manifest.dependencies).toBeDefined()
|
||||
expect(manifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.1.0')
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId)
|
||||
expect(packagesArray).toContain(expectedPkgId)
|
||||
|
||||
// All other packages should remain the same
|
||||
for (const pkgId of Object.keys(originalLockfile!.packages!)) {
|
||||
if (pkgId === originalPkgId) continue
|
||||
expect(packagesArray).toContain(pkgId)
|
||||
}
|
||||
})
|
||||
|
||||
test('top-level pinned vulnerability is fixed by updating the vulnerable package', async () => {
|
||||
const tmp = f.prepare('update-single-pinned')
|
||||
|
||||
const originalPkgId = '@pnpm.e2e/pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedPkgId = '@pnpm.e2e/pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const originalDepPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedDepPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const { manifest: originalManifest } = await readProjectManifest(tmp)
|
||||
expect(originalManifest).toBeTruthy()
|
||||
expect(originalManifest.dependencies).toBeDefined()
|
||||
expect(originalManifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('100.0.0')
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId]).toBeUndefined()
|
||||
expect(originalLockfile!.packages![originalDepPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedDepPkgId]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'top-level-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(1)} vulnerability was fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('high')}) "${chalk.green('Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/pkg-with-1-dep')}
|
||||
`)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
const { manifest } = await readProjectManifest(tmp)
|
||||
expect(manifest).toBeTruthy()
|
||||
expect(manifest.dependencies).toBeDefined()
|
||||
expect(manifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('100.1.0')
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId)
|
||||
expect(packagesArray).toContain(expectedPkgId)
|
||||
|
||||
// The vulnerable dependency's dependencies should also be updated
|
||||
expect(packagesArray).not.toContain(originalDepPkgId)
|
||||
expect(packagesArray).toContain(expectedDepPkgId)
|
||||
|
||||
// All other packages should remain the same
|
||||
for (const pkgId of Object.keys(originalLockfile!.packages!)) {
|
||||
if (pkgId === originalPkgId) continue
|
||||
if (pkgId === originalDepPkgId) continue
|
||||
expect(packagesArray).toContain(pkgId)
|
||||
}
|
||||
})
|
||||
|
||||
test('depth 2 vulnerability is fixed by updating the vulnerable package', async () => {
|
||||
const tmp = f.prepare('update-single-depth-2')
|
||||
|
||||
const originalPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'depth-2-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(1)} vulnerability was fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('high')}) "${chalk.green('Title: mock vulnerability in @pnpm.e2e/dep-of-pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/dep-of-pkg-with-1-dep')}
|
||||
`)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId)
|
||||
expect(packagesArray).toContain(expectedPkgId)
|
||||
|
||||
// All other packages should remain the same
|
||||
for (const pkgId of Object.keys(originalLockfile!.packages!)) {
|
||||
if (pkgId === originalPkgId) continue
|
||||
expect(packagesArray).toContain(pkgId)
|
||||
}
|
||||
})
|
||||
|
||||
test('depth 3 vulnerability is fixed by updating the vulnerable package', async () => {
|
||||
const tmp = f.prepare('update-single-depth-3')
|
||||
|
||||
const originalPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'depth-3-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(1)} vulnerability was fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('high')}) "${chalk.green('Title: mock vulnerability in @pnpm.e2e/dep-of-pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/dep-of-pkg-with-1-dep')}
|
||||
`)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId)
|
||||
expect(packagesArray).toContain(expectedPkgId)
|
||||
|
||||
// All other packages should remain the same
|
||||
for (const pkgId of Object.keys(originalLockfile!.packages!)) {
|
||||
if (pkgId === originalPkgId) continue
|
||||
expect(packagesArray).toContain(pkgId)
|
||||
}
|
||||
})
|
||||
|
||||
test('unfixable vulnerability remains unresolved', async () => {
|
||||
const tmp = f.prepare('update-single-depth-2')
|
||||
|
||||
const pkgId = '@pnpm.e2e/pkg-with-1-dep@100.0.0' as DepPath
|
||||
|
||||
const { manifest: originalManifest } = await readProjectManifest(tmp)
|
||||
expect(originalManifest).toBeTruthy()
|
||||
expect(originalManifest.dependencies).toBeDefined()
|
||||
expect(originalManifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.0.0')
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![pkgId]).toBeDefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'unfixable-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(0)} vulnerabilities were fixed, ${chalk.red(1)} vulnerability remains.
|
||||
|
||||
The remaining vulnerabilities are:
|
||||
- (${chalk.bold.red('high')}) "${chalk.bold.red('Title: unfixable vulnerability in @pnpm.e2e/pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/pkg-with-1-dep')}
|
||||
`)
|
||||
expect(exitCode).toBe(1)
|
||||
|
||||
// The manifest should remain unchanged
|
||||
const { manifest } = await readProjectManifest(tmp)
|
||||
expect(manifest).toBeTruthy()
|
||||
expect(manifest.dependencies).toBeDefined()
|
||||
expect(manifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.0.0')
|
||||
|
||||
// The lockfile should remain unchanged
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// All packages should remain the same
|
||||
for (const pkgId of Object.keys(originalLockfile!.packages!)) {
|
||||
expect(packagesArray).toContain(pkgId)
|
||||
}
|
||||
})
|
||||
|
||||
test('vulnerable package with multiple versions is updated', async () => {
|
||||
await addDistTag({ package: 'form-data', version: '4.0.4', distTag: 'latest' })
|
||||
|
||||
const tmp = f.prepare('update-multiple')
|
||||
|
||||
const originalPkgId1 = 'form-data@3.0.1' as DepPath
|
||||
const originalPkgId2 = 'form-data@4.0.0' as DepPath
|
||||
const expectedPkgId1 = 'form-data@3.0.4' as DepPath
|
||||
const expectedPkgId2 = 'form-data@4.0.4' as DepPath
|
||||
|
||||
const { manifest: originalManifest } = await readProjectManifest(tmp)
|
||||
expect(originalManifest).toBeTruthy()
|
||||
expect(originalManifest.dependencies).toBeDefined()
|
||||
expect(originalManifest.dependencies?.['form-data']).toBe('^3.0.1')
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId1]).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId2]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId1]).toBeUndefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId2]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'form-data-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(2)} vulnerabilities were fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('critical')}) "${chalk.green('form-data uses unsafe random function in form-data for choosing boundary')}" ${chalk.blue('form-data')}
|
||||
- (${chalk.green('critical')}) "${chalk.green('form-data uses unsafe random function in form-data for choosing boundary')}" ${chalk.blue('form-data')}
|
||||
`)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
const { manifest } = await readProjectManifest(tmp)
|
||||
expect(manifest).toBeTruthy()
|
||||
expect(manifest.dependencies).toBeDefined()
|
||||
expect(manifest.dependencies?.['form-data']).toBe('^3.0.4')
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId1)
|
||||
expect(packagesArray).not.toContain(originalPkgId2)
|
||||
expect(packagesArray).toContain(expectedPkgId1)
|
||||
expect(packagesArray).toContain(expectedPkgId2)
|
||||
|
||||
// All other packages should remain the same
|
||||
for (const pkgId of Object.keys(originalLockfile!.packages!)) {
|
||||
if (pkgId === originalPkgId1 || pkgId === originalPkgId2) continue
|
||||
expect(packagesArray).toContain(pkgId)
|
||||
}
|
||||
})
|
||||
|
||||
test('top-level workspace subpackage vulnerability is fixed by recursive update from root', async () => {
|
||||
const tmp = f.prepare('update-workspace-depth-2')
|
||||
|
||||
const originalPkgId = '@pnpm.e2e/pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedPkgId = '@pnpm.e2e/pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const subPkgDir = join(tmp, 'packages', 'sub-pkg')
|
||||
|
||||
const { manifest: originalManifest } = await readProjectManifest(subPkgDir)
|
||||
expect(originalManifest).toBeTruthy()
|
||||
expect(originalManifest.dependencies).toBeDefined()
|
||||
expect(originalManifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.0.0')
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'top-level-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(tmp, [], {
|
||||
workspaceDir: tmp,
|
||||
prefix: tmp,
|
||||
})
|
||||
expect(allProjects).toHaveLength(2)
|
||||
expect(new Set(allProjects.map(p => p.manifest.name))).toEqual(new Set(['update-workspace-depth-2', 'sub-pkg']))
|
||||
expect(allProjectsGraph).toBeTruthy()
|
||||
expect(selectedProjectsGraph).toEqual(allProjectsGraph)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
workspaceDir: tmp,
|
||||
lockfileDir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(1)} vulnerability was fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('high')}) "${chalk.green('Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/pkg-with-1-dep')}
|
||||
`)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
const { manifest } = await readProjectManifest(subPkgDir)
|
||||
expect(manifest).toBeTruthy()
|
||||
expect(manifest.dependencies).toBeDefined()
|
||||
expect(manifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.1.0')
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId)
|
||||
expect(packagesArray).toContain(expectedPkgId)
|
||||
|
||||
// All other packages should remain the same
|
||||
for (const pkgId of Object.keys(originalLockfile!.packages!)) {
|
||||
if (pkgId === originalPkgId) continue
|
||||
expect(packagesArray).toContain(pkgId)
|
||||
}
|
||||
})
|
||||
|
||||
test('depth 2 workspace subpackage vulnerability is fixed by recursive update from root', async () => {
|
||||
const tmp = f.prepare('update-workspace-depth-2')
|
||||
|
||||
const originalPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'depth-2-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(tmp, [], {
|
||||
workspaceDir: tmp,
|
||||
prefix: tmp,
|
||||
})
|
||||
expect(allProjects).toHaveLength(2)
|
||||
expect(new Set(allProjects.map(p => p.manifest.name))).toEqual(new Set(['update-workspace-depth-2', 'sub-pkg']))
|
||||
expect(allProjectsGraph).toBeTruthy()
|
||||
expect(selectedProjectsGraph).toEqual(allProjectsGraph)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
workspaceDir: tmp,
|
||||
lockfileDir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
expect(output).toBe(`${chalk.green(1)} vulnerability was fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('high')}) "${chalk.green('Title: mock vulnerability in @pnpm.e2e/dep-of-pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/dep-of-pkg-with-1-dep')}
|
||||
`)
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId)
|
||||
expect(packagesArray).toContain(expectedPkgId)
|
||||
|
||||
// All other packages should remain the same
|
||||
for (const pkgId of Object.keys(originalLockfile!.packages!)) {
|
||||
if (pkgId === originalPkgId) continue
|
||||
expect(packagesArray).toContain(pkgId)
|
||||
}
|
||||
})
|
||||
|
||||
test('top-level pinned workspace subpackage vulnerability is fixed by recursive update from root', async () => {
|
||||
const tmp = f.prepare('update-workspace-pinned')
|
||||
|
||||
const originalPkgId = '@pnpm.e2e/pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedPkgId = '@pnpm.e2e/pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const originalDepPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedDepPkgId = '@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const subPkgDir = join(tmp, 'packages', 'sub-pkg')
|
||||
|
||||
const { manifest: originalManifest } = await readProjectManifest(subPkgDir)
|
||||
expect(originalManifest).toBeTruthy()
|
||||
expect(originalManifest.dependencies).toBeDefined()
|
||||
expect(originalManifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('100.0.0')
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId]).toBeUndefined()
|
||||
expect(originalLockfile!.packages![originalDepPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedDepPkgId]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'top-level-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(tmp, [], {
|
||||
workspaceDir: tmp,
|
||||
prefix: tmp,
|
||||
})
|
||||
expect(allProjects).toHaveLength(2)
|
||||
expect(new Set(allProjects.map(p => p.manifest.name))).toEqual(new Set(['update-workspace-pinned', 'sub-pkg']))
|
||||
expect(allProjectsGraph).toBeTruthy()
|
||||
expect(selectedProjectsGraph).toEqual(allProjectsGraph)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
workspaceDir: tmp,
|
||||
lockfileDir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(1)} vulnerability was fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('high')}) "${chalk.green('Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/pkg-with-1-dep')}
|
||||
`)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
const { manifest } = await readProjectManifest(subPkgDir)
|
||||
expect(manifest).toBeTruthy()
|
||||
expect(manifest.dependencies).toBeDefined()
|
||||
expect(manifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('100.1.0')
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId)
|
||||
expect(packagesArray).toContain(expectedPkgId)
|
||||
|
||||
// The vulnerable dependency's dependencies should also be updated
|
||||
expect(packagesArray).not.toContain(originalDepPkgId)
|
||||
expect(packagesArray).toContain(expectedDepPkgId)
|
||||
|
||||
// All other packages should remain the same
|
||||
for (const pkgId of Object.keys(originalLockfile!.packages!)) {
|
||||
if (pkgId === originalPkgId) continue
|
||||
if (pkgId === originalDepPkgId) continue
|
||||
expect(packagesArray).toContain(pkgId)
|
||||
}
|
||||
})
|
||||
|
||||
test('top-level pinned workspace catalog vulnerability is fixed by updating the catalog entry', async () => {
|
||||
const tmp = f.prepare('update-workspace-catalog-pinned')
|
||||
|
||||
const originalPkgId = '@pnpm.e2e/pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedPkgId = '@pnpm.e2e/pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const subPkgDir = join(tmp, 'packages', 'sub-pkg')
|
||||
|
||||
// Verify the sub-package uses catalog: protocol
|
||||
const { manifest: originalManifest } = await readProjectManifest(subPkgDir)
|
||||
expect(originalManifest).toBeTruthy()
|
||||
expect(originalManifest.dependencies).toBeDefined()
|
||||
expect(originalManifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('catalog:')
|
||||
|
||||
// Verify the workspace catalog has the pinned version
|
||||
const originalWorkspaceManifest = readYamlFileSync<{ catalog?: Record<string, string> }>(join(tmp, 'pnpm-workspace.yaml'))
|
||||
expect(originalWorkspaceManifest.catalog?.['@pnpm.e2e/pkg-with-1-dep']).toBe('100.0.0')
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'top-level-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(tmp, [], {
|
||||
workspaceDir: tmp,
|
||||
prefix: tmp,
|
||||
})
|
||||
expect(allProjects).toHaveLength(2)
|
||||
expect(new Set(allProjects.map(p => p.manifest.name))).toEqual(new Set(['update-workspace-catalog-pinned', 'sub-pkg']))
|
||||
expect(allProjectsGraph).toBeTruthy()
|
||||
expect(selectedProjectsGraph).toEqual(allProjectsGraph)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
workspaceDir: tmp,
|
||||
lockfileDir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
catalogs: {
|
||||
default: { '@pnpm.e2e/pkg-with-1-dep': '100.0.0' },
|
||||
},
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(1)} vulnerability was fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('high')}) "${chalk.green('Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/pkg-with-1-dep')}
|
||||
`)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
// The sub-package manifest should still use catalog: protocol
|
||||
const { manifest } = await readProjectManifest(subPkgDir)
|
||||
expect(manifest).toBeTruthy()
|
||||
expect(manifest.dependencies).toBeDefined()
|
||||
expect(manifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('catalog:')
|
||||
|
||||
// The workspace catalog should be updated to the fixed version
|
||||
const workspaceManifest = readYamlFileSync<{ catalog?: Record<string, string> }>(join(tmp, 'pnpm-workspace.yaml'))
|
||||
expect(workspaceManifest.catalog?.['@pnpm.e2e/pkg-with-1-dep']).toBe('100.1.0')
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId)
|
||||
expect(packagesArray).toContain(expectedPkgId)
|
||||
})
|
||||
|
||||
test('top-level workspace catalog vulnerability is fixed by updating the catalog entry', async () => {
|
||||
const tmp = f.prepare('update-workspace-catalog')
|
||||
|
||||
const originalPkgId = '@pnpm.e2e/pkg-with-1-dep@100.0.0' as DepPath
|
||||
const expectedPkgId = '@pnpm.e2e/pkg-with-1-dep@100.1.0' as DepPath
|
||||
|
||||
const subPkgDir = join(tmp, 'packages', 'sub-pkg')
|
||||
|
||||
// Verify the sub-package uses catalog: protocol
|
||||
const { manifest: originalManifest } = await readProjectManifest(subPkgDir)
|
||||
expect(originalManifest).toBeTruthy()
|
||||
expect(originalManifest.dependencies).toBeDefined()
|
||||
expect(originalManifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('catalog:')
|
||||
|
||||
// Verify the workspace catalog has the ranged version
|
||||
const originalWorkspaceManifest = readYamlFileSync<{ catalog?: Record<string, string> }>(join(tmp, 'pnpm-workspace.yaml'))
|
||||
expect(originalWorkspaceManifest.catalog?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.0.0')
|
||||
|
||||
const originalLockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(originalLockfile).toBeTruthy()
|
||||
expect(originalLockfile!.packages).toBeDefined()
|
||||
expect(originalLockfile!.packages![originalPkgId]).toBeDefined()
|
||||
expect(originalLockfile!.packages![expectedPkgId]).toBeUndefined()
|
||||
|
||||
const mockResponse = await readFile(join(tmp, 'responses', 'top-level-vulnerability.json'), 'utf-8')
|
||||
expect(mockResponse).toBeTruthy()
|
||||
|
||||
nock(MOCK_REGISTRY, { allowUnmocked: true })
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, mockResponse)
|
||||
|
||||
const {
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
} = await filterPackagesFromDir(tmp, [], {
|
||||
workspaceDir: tmp,
|
||||
prefix: tmp,
|
||||
})
|
||||
expect(allProjects).toHaveLength(2)
|
||||
expect(new Set(allProjects.map(p => p.manifest.name))).toEqual(new Set(['update-workspace-catalog', 'sub-pkg']))
|
||||
expect(allProjectsGraph).toBeTruthy()
|
||||
expect(selectedProjectsGraph).toEqual(allProjectsGraph)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...MOCK_REGISTRY_OPTS,
|
||||
dir: tmp,
|
||||
workspaceDir: tmp,
|
||||
lockfileDir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
allProjects,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
catalogs: {
|
||||
default: { '@pnpm.e2e/pkg-with-1-dep': '^100.0.0' },
|
||||
},
|
||||
auditLevel: 'moderate',
|
||||
fix: 'update',
|
||||
lockfileOnly: true,
|
||||
})
|
||||
|
||||
expect(output).toBe(`${chalk.green(1)} vulnerability was fixed, ${chalk.red(0)} vulnerabilities remain.
|
||||
|
||||
The fixed vulnerabilities are:
|
||||
- (${chalk.green('high')}) "${chalk.green('Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep')}" ${chalk.blue('@pnpm.e2e/pkg-with-1-dep')}
|
||||
`)
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
// The sub-package manifest should still use catalog: protocol
|
||||
const { manifest } = await readProjectManifest(subPkgDir)
|
||||
expect(manifest).toBeTruthy()
|
||||
expect(manifest.dependencies).toBeDefined()
|
||||
expect(manifest.dependencies?.['@pnpm.e2e/pkg-with-1-dep']).toBe('catalog:')
|
||||
|
||||
// The workspace catalog should be updated, preserving the ^ range style
|
||||
const workspaceManifest = readYamlFileSync<{ catalog?: Record<string, string> }>(join(tmp, 'pnpm-workspace.yaml'))
|
||||
expect(workspaceManifest.catalog?.['@pnpm.e2e/pkg-with-1-dep']).toBe('^100.1.0')
|
||||
|
||||
const lockfile = await readWantedLockfile(tmp, { ignoreIncompatible: true })
|
||||
expect(lockfile).toBeTruthy()
|
||||
expect(lockfile!.packages).toBeDefined()
|
||||
const packagesArray = Object.keys(lockfile!.packages!)
|
||||
|
||||
// The vulnerable dependency should be updated
|
||||
expect(packagesArray).not.toContain(originalPkgId)
|
||||
expect(packagesArray).toContain(expectedPkgId)
|
||||
})
|
||||
})
|
||||
8
lockfile/plugin-commands-audit/test/fixtures/update-multiple/package.json
vendored
Normal file
8
lockfile/plugin-commands-audit/test/fixtures/update-multiple/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "update-multiple",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"form-data": "^3.0.1"
|
||||
}
|
||||
}
|
||||
100
lockfile/plugin-commands-audit/test/fixtures/update-multiple/pnpm-lock.yaml
generated
vendored
Normal file
100
lockfile/plugin-commands-audit/test/fixtures/update-multiple/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
axios:
|
||||
specifier: ^1.8.2
|
||||
version: 1.8.2
|
||||
form-data:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
|
||||
packages:
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
axios@1.8.2:
|
||||
resolution: {integrity: sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
follow-redirects@1.15.11:
|
||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
form-data@3.0.1:
|
||||
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
axios@1.8.2:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.0
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
|
||||
form-data@3.0.1:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
form-data@4.0.0:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
126
lockfile/plugin-commands-audit/test/fixtures/update-multiple/responses/form-data-vulnerability.json
vendored
Normal file
126
lockfile/plugin-commands-audit/test/fixtures/update-multiple/responses/form-data-vulnerability.json
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 1109538,
|
||||
"path": ".>axios>form-data",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "form-data",
|
||||
"target": "4.0.5",
|
||||
"depth": 3
|
||||
},
|
||||
{
|
||||
"action": "review",
|
||||
"module": "form-data",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 1109539,
|
||||
"path": ".>form-data",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"1109538": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "4.0.0",
|
||||
"paths": [
|
||||
".>axios>form-data"
|
||||
]
|
||||
}
|
||||
],
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"references": "- https://github.com/form-data/form-data/security/advisories/GHSA-fjxv-7rqg-78g4\n- https://nvd.nist.gov/vuln/detail/CVE-2025-7783\n- https://github.com/form-data/form-data/commit/3d1723080e6577a66f17f163ecd345a21d8d0fd0\n- https://github.com/benweissmann/CVE-2025-7783-poc\n- https://lists.debian.org/debian-lts-announce/2025/07/msg00023.html\n- https://github.com/advisories/GHSA-fjxv-7rqg-78g4",
|
||||
"created": "2025-07-21T19:04:54.000Z",
|
||||
"id": 1109538,
|
||||
"npm_advisory_id": null,
|
||||
"overview": "### Summary\n\nform-data uses `Math.random()` to select a boundary value for multipart form-encoded data. This can lead to a security issue if an attacker:\n1. can observe other values produced by Math.random in the target application, and\n2. can control one field of a request made using form-data\n\nBecause the values of Math.random() are pseudo-random and predictable (see: https://blog.securityevaluators.com/hacking-the-javascript-lottery-80cc437e3b7f), an attacker who can observe a few sequential values can determine the state of the PRNG and predict future values, includes those used to generate form-data's boundary value. The allows the attacker to craft a value that contains a boundary value, allowing them to inject additional parameters into the request.\n\nThis is largely the same vulnerability as was [recently found in `undici`](https://hackerone.com/reports/2913312) by [`parrot409`](https://hackerone.com/parrot409?type=user) -- I'm not affiliated with that researcher but want to give credit where credit is due! My PoC is largely based on their work.\n\n### Details\n\nThe culprit is this line here: https://github.com/form-data/form-data/blob/426ba9ac440f95d1998dac9a5cd8d738043b048f/lib/form_data.js#L347\n\nAn attacker who is able to predict the output of Math.random() can predict this boundary value, and craft a payload that contains the boundary value, followed by another, fully attacker-controlled field. This is roughly equivalent to any sort of improper escaping vulnerability, with the caveat that the attacker must find a way to observe other Math.random() values generated by the application to solve for the state of the PRNG. However, Math.random() is used in all sorts of places that might be visible to an attacker (including by form-data itself, if the attacker can arrange for the vulnerable application to make a request to an attacker-controlled server using form-data, such as a user-controlled webhook -- the attacker could observe the boundary values from those requests to observe the Math.random() outputs). A common example would be a `x-request-id` header added by the server. These sorts of headers are often used for distributed tracing, to correlate errors across the frontend and backend. `Math.random()` is a fine place to get these sorts of IDs (in fact, [opentelemetry uses Math.random for this purpose](https://github.com/open-telemetry/opentelemetry-js/blob/2053f0d3a44631ade77ea04f656056a2c8a2ae76/packages/opentelemetry-sdk-trace-base/src/platform/node/RandomIdGenerator.ts#L22))\n\n### PoC\n\nPoC here: https://github.com/benweissmann/CVE-2025-7783-poc\n\nInstructions are in that repo. It's based on the PoC from https://hackerone.com/reports/2913312 but simplified somewhat; the vulnerable application has a more direct side-channel from which to observe Math.random() values (a separate endpoint that happens to include a randomly-generated request ID). \n\n### Impact\n\nFor an application to be vulnerable, it must:\n- Use `form-data` to send data including user-controlled data to some other system. The attacker must be able to do something malicious by adding extra parameters (that were not intended to be user-controlled) to this request. Depending on the target system's handling of repeated parameters, the attacker might be able to overwrite values in addition to appending values (some multipart form handlers deal with repeats by overwriting values instead of representing them as an array)\n- Reveal values of Math.random(). It's easiest if the attacker can observe multiple sequential values, but more complex math could recover the PRNG state to some degree of confidence with non-sequential values. \n\nIf an application is vulnerable, this allows an attacker to make arbitrary requests to internal systems.",
|
||||
"reported_by": null,
|
||||
"title": "form-data uses unsafe random function in form-data for choosing boundary",
|
||||
"metadata": null,
|
||||
"cves": [
|
||||
"CVE-2025-7783"
|
||||
],
|
||||
"access": "public",
|
||||
"severity": "critical",
|
||||
"module_name": "form-data",
|
||||
"vulnerable_versions": ">=4.0.0 <4.0.4",
|
||||
"github_advisory_id": "GHSA-fjxv-7rqg-78g4",
|
||||
"recommendation": "Upgrade to version 4.0.4 or later",
|
||||
"patched_versions": ">=4.0.4",
|
||||
"updated": "2025-11-03T21:34:09.000Z",
|
||||
"cvss": {
|
||||
"score": 0,
|
||||
"vectorString": null
|
||||
},
|
||||
"cwe": [
|
||||
"CWE-330"
|
||||
],
|
||||
"url": "https://github.com/advisories/GHSA-fjxv-7rqg-78g4"
|
||||
},
|
||||
"1109539": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "3.0.1",
|
||||
"paths": [
|
||||
".>form-data"
|
||||
]
|
||||
}
|
||||
],
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"references": "- https://github.com/form-data/form-data/security/advisories/GHSA-fjxv-7rqg-78g4\n- https://nvd.nist.gov/vuln/detail/CVE-2025-7783\n- https://github.com/form-data/form-data/commit/3d1723080e6577a66f17f163ecd345a21d8d0fd0\n- https://github.com/benweissmann/CVE-2025-7783-poc\n- https://lists.debian.org/debian-lts-announce/2025/07/msg00023.html\n- https://github.com/advisories/GHSA-fjxv-7rqg-78g4",
|
||||
"created": "2025-07-21T19:04:54.000Z",
|
||||
"id": 1109539,
|
||||
"npm_advisory_id": null,
|
||||
"overview": "### Summary\n\nform-data uses `Math.random()` to select a boundary value for multipart form-encoded data. This can lead to a security issue if an attacker:\n1. can observe other values produced by Math.random in the target application, and\n2. can control one field of a request made using form-data\n\nBecause the values of Math.random() are pseudo-random and predictable (see: https://blog.securityevaluators.com/hacking-the-javascript-lottery-80cc437e3b7f), an attacker who can observe a few sequential values can determine the state of the PRNG and predict future values, includes those used to generate form-data's boundary value. The allows the attacker to craft a value that contains a boundary value, allowing them to inject additional parameters into the request.\n\nThis is largely the same vulnerability as was [recently found in `undici`](https://hackerone.com/reports/2913312) by [`parrot409`](https://hackerone.com/parrot409?type=user) -- I'm not affiliated with that researcher but want to give credit where credit is due! My PoC is largely based on their work.\n\n### Details\n\nThe culprit is this line here: https://github.com/form-data/form-data/blob/426ba9ac440f95d1998dac9a5cd8d738043b048f/lib/form_data.js#L347\n\nAn attacker who is able to predict the output of Math.random() can predict this boundary value, and craft a payload that contains the boundary value, followed by another, fully attacker-controlled field. This is roughly equivalent to any sort of improper escaping vulnerability, with the caveat that the attacker must find a way to observe other Math.random() values generated by the application to solve for the state of the PRNG. However, Math.random() is used in all sorts of places that might be visible to an attacker (including by form-data itself, if the attacker can arrange for the vulnerable application to make a request to an attacker-controlled server using form-data, such as a user-controlled webhook -- the attacker could observe the boundary values from those requests to observe the Math.random() outputs). A common example would be a `x-request-id` header added by the server. These sorts of headers are often used for distributed tracing, to correlate errors across the frontend and backend. `Math.random()` is a fine place to get these sorts of IDs (in fact, [opentelemetry uses Math.random for this purpose](https://github.com/open-telemetry/opentelemetry-js/blob/2053f0d3a44631ade77ea04f656056a2c8a2ae76/packages/opentelemetry-sdk-trace-base/src/platform/node/RandomIdGenerator.ts#L22))\n\n### PoC\n\nPoC here: https://github.com/benweissmann/CVE-2025-7783-poc\n\nInstructions are in that repo. It's based on the PoC from https://hackerone.com/reports/2913312 but simplified somewhat; the vulnerable application has a more direct side-channel from which to observe Math.random() values (a separate endpoint that happens to include a randomly-generated request ID). \n\n### Impact\n\nFor an application to be vulnerable, it must:\n- Use `form-data` to send data including user-controlled data to some other system. The attacker must be able to do something malicious by adding extra parameters (that were not intended to be user-controlled) to this request. Depending on the target system's handling of repeated parameters, the attacker might be able to overwrite values in addition to appending values (some multipart form handlers deal with repeats by overwriting values instead of representing them as an array)\n- Reveal values of Math.random(). It's easiest if the attacker can observe multiple sequential values, but more complex math could recover the PRNG state to some degree of confidence with non-sequential values. \n\nIf an application is vulnerable, this allows an attacker to make arbitrary requests to internal systems.",
|
||||
"reported_by": null,
|
||||
"title": "form-data uses unsafe random function in form-data for choosing boundary",
|
||||
"metadata": null,
|
||||
"cves": [
|
||||
"CVE-2025-7783"
|
||||
],
|
||||
"access": "public",
|
||||
"severity": "critical",
|
||||
"module_name": "form-data",
|
||||
"vulnerable_versions": ">=3.0.0 <3.0.4",
|
||||
"github_advisory_id": "GHSA-fjxv-7rqg-78g4",
|
||||
"recommendation": "Upgrade to version 3.0.4 or later",
|
||||
"patched_versions": ">=3.0.4",
|
||||
"updated": "2025-11-03T21:34:09.000Z",
|
||||
"cvss": {
|
||||
"score": 0,
|
||||
"vectorString": null
|
||||
},
|
||||
"cwe": [
|
||||
"CWE-330"
|
||||
],
|
||||
"url": "https://github.com/advisories/GHSA-fjxv-7rqg-78g4"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 0,
|
||||
"critical": 2
|
||||
},
|
||||
"dependencies": 11,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 11
|
||||
}
|
||||
}
|
||||
8
lockfile/plugin-commands-audit/test/fixtures/update-single-depth-2/package.json
vendored
Normal file
8
lockfile/plugin-commands-audit/test/fixtures/update-single-depth-2/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "update-single-depth-2",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/bar": "^100.0.0",
|
||||
"@pnpm.e2e/pkg-with-1-dep": "^100.0.0"
|
||||
}
|
||||
}
|
||||
37
lockfile/plugin-commands-audit/test/fixtures/update-single-depth-2/pnpm-lock.yaml
generated
vendored
Normal file
37
lockfile/plugin-commands-audit/test/fixtures/update-single-depth-2/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@pnpm.e2e/bar':
|
||||
specifier: ^100.0.0
|
||||
version: 100.0.0
|
||||
'@pnpm.e2e/pkg-with-1-dep':
|
||||
specifier: ^100.0.0
|
||||
version: 100.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm.e2e/bar@100.0.0':
|
||||
resolution: {integrity: sha512-ZiNxDXOAD0K8nguFnyz3TmV8L9kz6xipLTQIWbIe7ojrRtjblv+NDt8oh5TPObUN92pDnWIEiRVh+ZjN+JWygg==}
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-atUXGBNAbym4OioYcKt1qTjiH23CSfZ1K2N8JgCUewSE5gY/i9YoK7Ez6+CuEZbH+O3R+HKNrRIaZfnkv/93tg==}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-aoavufOsqVp0D9pbv/dJRTYKFkmL8O1Pvj9lnWDFuAbNQnwWCDvxp5Dem12+DArhHmTJjGl3RccUATXLJxe1iQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm.e2e/bar@100.0.0': {}
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': {}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
dependencies:
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep': 100.0.0
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 123456,
|
||||
"path": ".>@pnpm.e2e/pkg-with-1-dep>@pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "@pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"target": "100.0.0",
|
||||
"depth": 3
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
".>@pnpm.e2e/pkg-with-1-dep>@pnpm.e2e/dep-of-pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": "<100.1.0",
|
||||
"module_name": "@pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": ">=100.1.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "Upgrade to version 100.1.0 or later",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: mock vulnerability in @pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: mock vulnerability in @pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 2,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 123456,
|
||||
"path": ".>@pnpm.e2e/pkg-with-1-dep",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"target": "100.0.0",
|
||||
"depth": 2
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
".>@pnpm.e2e/pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": "<100.1.0",
|
||||
"module_name": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": ">=100.1.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "Upgrade to version 100.1.0 or later",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 2,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"actions": [],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
".>@pnpm.e2e/pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": ">=0.0.0",
|
||||
"module_name": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": "<0.0.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "No fix available",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: unfixable vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: unfixable vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 2,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 2
|
||||
}
|
||||
}
|
||||
7
lockfile/plugin-commands-audit/test/fixtures/update-single-depth-3/package.json
vendored
Normal file
7
lockfile/plugin-commands-audit/test/fixtures/update-single-depth-3/package.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "update-single-depth-3",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/parent-of-pkg-with-1-dep": "^1.0.0"
|
||||
}
|
||||
}
|
||||
36
lockfile/plugin-commands-audit/test/fixtures/update-single-depth-3/pnpm-lock.yaml
generated
vendored
Normal file
36
lockfile/plugin-commands-audit/test/fixtures/update-single-depth-3/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@pnpm.e2e/parent-of-pkg-with-1-dep':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-atUXGBNAbym4OioYcKt1qTjiH23CSfZ1K2N8JgCUewSE5gY/i9YoK7Ez6+CuEZbH+O3R+HKNrRIaZfnkv/93tg==}
|
||||
|
||||
'@pnpm.e2e/parent-of-pkg-with-1-dep@1.0.0':
|
||||
resolution: {integrity: sha512-u2S6Vmz7W3aeIQL4N0g1GGjyD1YIYqj0vqLwvpddmsrc4KBkJLGliK2Knc+2PAM27DoILKXonQ9fQLoqzuF8bA==}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-aoavufOsqVp0D9pbv/dJRTYKFkmL8O1Pvj9lnWDFuAbNQnwWCDvxp5Dem12+DArhHmTJjGl3RccUATXLJxe1iQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': {}
|
||||
|
||||
'@pnpm.e2e/parent-of-pkg-with-1-dep@1.0.0':
|
||||
dependencies:
|
||||
'@pnpm.e2e/pkg-with-1-dep': 100.0.0
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
dependencies:
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep': 100.0.0
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 123456,
|
||||
"path": ".>@pnpm.e2e/parent-of-pkg-with-1-dep>@pnpm.e2e/pkg-with-1-dep>@pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "@pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"target": "100.0.0",
|
||||
"depth": 4
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
".>@pnpm.e2e/parent-of-pkg-with-1-dep>@pnpm.e2e/pkg-with-1-dep>@pnpm.e2e/dep-of-pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": "<100.1.0",
|
||||
"module_name": "@pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": ">=100.1.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "Upgrade to version 100.1.0 or later",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: mock vulnerability in @pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: mock vulnerability in @pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 3,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 3
|
||||
}
|
||||
}
|
||||
8
lockfile/plugin-commands-audit/test/fixtures/update-single-pinned/package.json
vendored
Normal file
8
lockfile/plugin-commands-audit/test/fixtures/update-single-pinned/package.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "update-single-pinned",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/bar": "100.0.0",
|
||||
"@pnpm.e2e/pkg-with-1-dep": "100.0.0"
|
||||
}
|
||||
}
|
||||
37
lockfile/plugin-commands-audit/test/fixtures/update-single-pinned/pnpm-lock.yaml
generated
vendored
Normal file
37
lockfile/plugin-commands-audit/test/fixtures/update-single-pinned/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@pnpm.e2e/bar':
|
||||
specifier: 100.0.0
|
||||
version: 100.0.0
|
||||
'@pnpm.e2e/pkg-with-1-dep':
|
||||
specifier: 100.0.0
|
||||
version: 100.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm.e2e/bar@100.0.0':
|
||||
resolution: {integrity: sha512-ZiNxDXOAD0K8nguFnyz3TmV8L9kz6xipLTQIWbIe7ojrRtjblv+NDt8oh5TPObUN92pDnWIEiRVh+ZjN+JWygg==}
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-atUXGBNAbym4OioYcKt1qTjiH23CSfZ1K2N8JgCUewSE5gY/i9YoK7Ez6+CuEZbH+O3R+HKNrRIaZfnkv/93tg==}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-aoavufOsqVp0D9pbv/dJRTYKFkmL8O1Pvj9lnWDFuAbNQnwWCDvxp5Dem12+DArhHmTJjGl3RccUATXLJxe1iQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm.e2e/bar@100.0.0': {}
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': {}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
dependencies:
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep': 100.0.0
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 123456,
|
||||
"path": ".>@pnpm.e2e/pkg-with-1-dep",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"target": "100.0.0",
|
||||
"depth": 2
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
".>@pnpm.e2e/pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": "<100.1.0",
|
||||
"module_name": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": ">=100.1.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "Upgrade to version 100.1.0 or later",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 2,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 2
|
||||
}
|
||||
}
|
||||
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog-pinned/package.json
vendored
Normal file
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog-pinned/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "update-workspace-catalog-pinned",
|
||||
"version": "1.0.0",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "sub-pkg",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/pkg-with-1-dep": "catalog:"
|
||||
}
|
||||
}
|
||||
37
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog-pinned/pnpm-lock.yaml
generated
vendored
Normal file
37
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog-pinned/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
catalogs:
|
||||
default:
|
||||
'@pnpm.e2e/pkg-with-1-dep':
|
||||
specifier: 100.0.0
|
||||
version: 100.0.0
|
||||
|
||||
importers:
|
||||
|
||||
.: {}
|
||||
|
||||
packages/sub-pkg:
|
||||
dependencies:
|
||||
'@pnpm.e2e/pkg-with-1-dep':
|
||||
specifier: 'catalog:'
|
||||
version: 100.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-atUXGBNAbym4OioYcKt1qTjiH23CSfZ1K2N8JgCUewSE5gY/i9YoK7Ez6+CuEZbH+O3R+HKNrRIaZfnkv/93tg==}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-aoavufOsqVp0D9pbv/dJRTYKFkmL8O1Pvj9lnWDFuAbNQnwWCDvxp5Dem12+DArhHmTJjGl3RccUATXLJxe1iQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': {}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
dependencies:
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep': 100.0.0
|
||||
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
|
||||
catalog:
|
||||
'@pnpm.e2e/pkg-with-1-dep': 100.0.0
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 123456,
|
||||
"path": "packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"target": "100.0.0",
|
||||
"depth": 2
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
"packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": "<100.1.0",
|
||||
"module_name": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": ">=100.1.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "Upgrade to version 100.1.0 or later",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 2,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 2
|
||||
}
|
||||
}
|
||||
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog/package.json
vendored
Normal file
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "update-workspace-catalog",
|
||||
"version": "1.0.0",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "sub-pkg",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/pkg-with-1-dep": "catalog:"
|
||||
}
|
||||
}
|
||||
37
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog/pnpm-lock.yaml
generated
vendored
Normal file
37
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
catalogs:
|
||||
default:
|
||||
'@pnpm.e2e/pkg-with-1-dep':
|
||||
specifier: ^100.0.0
|
||||
version: 100.0.0
|
||||
|
||||
importers:
|
||||
|
||||
.: {}
|
||||
|
||||
packages/sub-pkg:
|
||||
dependencies:
|
||||
'@pnpm.e2e/pkg-with-1-dep':
|
||||
specifier: 'catalog:'
|
||||
version: 100.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-atUXGBNAbym4OioYcKt1qTjiH23CSfZ1K2N8JgCUewSE5gY/i9YoK7Ez6+CuEZbH+O3R+HKNrRIaZfnkv/93tg==}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-aoavufOsqVp0D9pbv/dJRTYKFkmL8O1Pvj9lnWDFuAbNQnwWCDvxp5Dem12+DArhHmTJjGl3RccUATXLJxe1iQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': {}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
dependencies:
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep': 100.0.0
|
||||
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog/pnpm-workspace.yaml
vendored
Normal file
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-catalog/pnpm-workspace.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
|
||||
catalog:
|
||||
'@pnpm.e2e/pkg-with-1-dep': ^100.0.0
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 123456,
|
||||
"path": "packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"target": "100.0.0",
|
||||
"depth": 2
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
"packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": "<100.1.0",
|
||||
"module_name": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": ">=100.1.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "Upgrade to version 100.1.0 or later",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 2,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 2
|
||||
}
|
||||
}
|
||||
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-depth-2/package.json
vendored
Normal file
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-depth-2/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "update-workspace-depth-2",
|
||||
"version": "1.0.0",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "sub-pkg",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/pkg-with-1-dep": "^100.0.0"
|
||||
}
|
||||
}
|
||||
31
lockfile/plugin-commands-audit/test/fixtures/update-workspace-depth-2/pnpm-lock.yaml
generated
vendored
Normal file
31
lockfile/plugin-commands-audit/test/fixtures/update-workspace-depth-2/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.: {}
|
||||
|
||||
packages/sub-pkg:
|
||||
dependencies:
|
||||
'@pnpm.e2e/pkg-with-1-dep':
|
||||
specifier: ^100.0.0
|
||||
version: 100.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-atUXGBNAbym4OioYcKt1qTjiH23CSfZ1K2N8JgCUewSE5gY/i9YoK7Ez6+CuEZbH+O3R+HKNrRIaZfnkv/93tg==}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-aoavufOsqVp0D9pbv/dJRTYKFkmL8O1Pvj9lnWDFuAbNQnwWCDvxp5Dem12+DArhHmTJjGl3RccUATXLJxe1iQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': {}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
dependencies:
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep': 100.0.0
|
||||
2
lockfile/plugin-commands-audit/test/fixtures/update-workspace-depth-2/pnpm-workspace.yaml
vendored
Normal file
2
lockfile/plugin-commands-audit/test/fixtures/update-workspace-depth-2/pnpm-workspace.yaml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 123456,
|
||||
"path": "packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep>@pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "@pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"target": "100.0.0",
|
||||
"depth": 3
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
"packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep>@pnpm.e2e/dep-of-pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": "<100.1.0",
|
||||
"module_name": "@pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": ">=100.1.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "Upgrade to version 100.1.0 or later",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: mock vulnerability in @pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: mock vulnerability in @pnpm.e2e/dep-of-pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 2,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 123456,
|
||||
"path": "packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"target": "100.0.0",
|
||||
"depth": 2
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
"packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": "<100.1.0",
|
||||
"module_name": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": ">=100.1.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "Upgrade to version 100.1.0 or later",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 2,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 2
|
||||
}
|
||||
}
|
||||
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-pinned/package.json
vendored
Normal file
5
lockfile/plugin-commands-audit/test/fixtures/update-workspace-pinned/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "update-workspace-pinned",
|
||||
"version": "1.0.0",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "sub-pkg",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@pnpm.e2e/pkg-with-1-dep": "100.0.0"
|
||||
}
|
||||
}
|
||||
31
lockfile/plugin-commands-audit/test/fixtures/update-workspace-pinned/pnpm-lock.yaml
generated
vendored
Normal file
31
lockfile/plugin-commands-audit/test/fixtures/update-workspace-pinned/pnpm-lock.yaml
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.: {}
|
||||
|
||||
packages/sub-pkg:
|
||||
dependencies:
|
||||
'@pnpm.e2e/pkg-with-1-dep':
|
||||
specifier: 100.0.0
|
||||
version: 100.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-atUXGBNAbym4OioYcKt1qTjiH23CSfZ1K2N8JgCUewSE5gY/i9YoK7Ez6+CuEZbH+O3R+HKNrRIaZfnkv/93tg==}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
resolution: {integrity: sha512-aoavufOsqVp0D9pbv/dJRTYKFkmL8O1Pvj9lnWDFuAbNQnwWCDvxp5Dem12+DArhHmTJjGl3RccUATXLJxe1iQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': {}
|
||||
|
||||
'@pnpm.e2e/pkg-with-1-dep@100.0.0':
|
||||
dependencies:
|
||||
'@pnpm.e2e/dep-of-pkg-with-1-dep': 100.0.0
|
||||
2
lockfile/plugin-commands-audit/test/fixtures/update-workspace-pinned/pnpm-workspace.yaml
vendored
Normal file
2
lockfile/plugin-commands-audit/test/fixtures/update-workspace-pinned/pnpm-workspace.yaml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"action": "update",
|
||||
"resolves": [
|
||||
{
|
||||
"id": 123456,
|
||||
"path": "packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep",
|
||||
"dev": false,
|
||||
"optional": false,
|
||||
"bundled": false
|
||||
}
|
||||
],
|
||||
"module": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"target": "100.0.0",
|
||||
"depth": 2
|
||||
}
|
||||
],
|
||||
"advisories": {
|
||||
"123456": {
|
||||
"findings": [
|
||||
{
|
||||
"version": "100.0.0",
|
||||
"paths": [
|
||||
"packages__sub-pkg>@pnpm.e2e/pkg-with-1-dep"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": null,
|
||||
"vulnerable_versions": "<100.1.0",
|
||||
"module_name": "@pnpm.e2e/pkg-with-1-dep",
|
||||
"severity": "high",
|
||||
"github_advisory_id": "GHSA-mock-mock-mock",
|
||||
"cves": [],
|
||||
"access": "public",
|
||||
"patched_versions": ">=100.1.0",
|
||||
"updated": "2025-12-19T19:00:00.000Z",
|
||||
"recommendation": "Upgrade to version 100.1.0 or later",
|
||||
"cwe": "",
|
||||
"found_by": null,
|
||||
"deleted": null,
|
||||
"id": 123456,
|
||||
"references": "",
|
||||
"created": "2025-12-19T19:00:00.000Z",
|
||||
"reported_by": null,
|
||||
"title": "Title: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"npm_advisory_id": null,
|
||||
"overview": "Overview: mock vulnerability in @pnpm.e2e/pkg-with-1-dep",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
},
|
||||
"muted": [],
|
||||
"metadata": {
|
||||
"vulnerabilities": {
|
||||
"info": 0,
|
||||
"low": 0,
|
||||
"moderate": 0,
|
||||
"high": 1,
|
||||
"critical": 0
|
||||
},
|
||||
"dependencies": 2,
|
||||
"devDependencies": 0,
|
||||
"optionalDependencies": 0,
|
||||
"totalDependencies": 2
|
||||
}
|
||||
}
|
||||
@@ -4,31 +4,23 @@ import { audit } from '@pnpm/plugin-commands-audit'
|
||||
import nock from 'nock'
|
||||
import { readYamlFileSync } from 'read-yaml-file'
|
||||
import * as responses from './utils/responses/index.js'
|
||||
import { AUDIT_REGISTRY_OPTS, AUDIT_REGISTRY } from './utils/options.js'
|
||||
|
||||
const f = fixtures(import.meta.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)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
ignoreUnfixable: true,
|
||||
})
|
||||
|
||||
@@ -44,19 +36,16 @@ test('ignores are added for vulnerable dependencies with no resolutions', async
|
||||
test('the specified vulnerabilities are ignored', async () => {
|
||||
const tmp = f.prepare('has-vulnerabilities')
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
ignore: ['CVE-2017-16115'],
|
||||
})
|
||||
|
||||
@@ -70,19 +59,16 @@ test('the specified vulnerabilities are ignored', async () => {
|
||||
test('no ignores are added if no vulnerabilities are found', async () => {
|
||||
const tmp = f.prepare('fixture')
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.NO_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
ignoreUnfixable: true,
|
||||
})
|
||||
|
||||
@@ -99,11 +85,12 @@ test('ignored CVEs are not duplicated', async () => {
|
||||
'CVE-2017-16024',
|
||||
]
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
auditConfig: {
|
||||
ignoreCves: existingCves,
|
||||
@@ -111,10 +98,6 @@ test('ignored CVEs are not duplicated', async () => {
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
fix: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: 120,
|
||||
ignoreUnfixable: true,
|
||||
})
|
||||
expect(exitCode).toBe(0)
|
||||
|
||||
@@ -6,65 +6,9 @@ import { AuditEndpointNotExistsError } from '@pnpm/audit'
|
||||
import nock from 'nock'
|
||||
import { stripVTControlCharacters as stripAnsi } from 'util'
|
||||
import * as responses from './utils/responses/index.js'
|
||||
import { DEFAULT_OPTS, AUDIT_REGISTRY_OPTS, AUDIT_REGISTRY } from './utils/options.js'
|
||||
|
||||
const f = fixtures(path.join(import.meta.dirname, 'fixtures'))
|
||||
const registries = {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
}
|
||||
const rawConfig = {
|
||||
registry: registries.default,
|
||||
}
|
||||
export const DEFAULT_OPTS = {
|
||||
argv: {
|
||||
original: [],
|
||||
},
|
||||
bail: true,
|
||||
bin: 'node_modules/.bin',
|
||||
ca: undefined,
|
||||
cacheDir: '../cache',
|
||||
cert: undefined,
|
||||
excludeLinksFromLockfile: false,
|
||||
extraEnv: {},
|
||||
cliOptions: {},
|
||||
fetchRetries: 2,
|
||||
fetchRetryFactor: 90,
|
||||
fetchRetryMaxtimeout: 90,
|
||||
fetchRetryMintimeout: 10,
|
||||
filter: [] as string[],
|
||||
httpsProxy: undefined,
|
||||
include: {
|
||||
dependencies: true,
|
||||
devDependencies: true,
|
||||
optionalDependencies: true,
|
||||
},
|
||||
key: undefined,
|
||||
linkWorkspacePackages: true,
|
||||
localAddress: undefined,
|
||||
lock: false,
|
||||
lockStaleDuration: 90,
|
||||
networkConcurrency: 16,
|
||||
offline: false,
|
||||
pending: false,
|
||||
pnpmfile: ['./.pnpmfile.cjs'],
|
||||
pnpmHomeDir: '',
|
||||
preferWorkspacePackages: true,
|
||||
proxy: undefined,
|
||||
rawConfig,
|
||||
rawLocalConfig: {},
|
||||
registries,
|
||||
rootProjectManifestDir: '',
|
||||
// registry: REGISTRY,
|
||||
sort: true,
|
||||
storeDir: '../store',
|
||||
strictSsl: false,
|
||||
userAgent: 'pnpm',
|
||||
userConfig: {},
|
||||
useRunningStoreServer: false,
|
||||
useStoreServer: false,
|
||||
workspaceConcurrency: 4,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
peersSuffixMaxLength: 1000,
|
||||
}
|
||||
|
||||
describe('plugin-commands-audit', () => {
|
||||
const hasVulnerabilitiesDir = f.prepare('has-vulnerabilities')
|
||||
@@ -75,37 +19,34 @@ describe('plugin-commands-audit', () => {
|
||||
dir: hasVulnerabilitiesDir,
|
||||
})
|
||||
})
|
||||
afterEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
test('audit', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
expect(exitCode).toBe(1)
|
||||
expect(stripAnsi(output)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('audit --dev', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.DEV_VULN_ONLY_RESP)
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
dev: true,
|
||||
production: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(1)
|
||||
@@ -113,18 +54,15 @@ describe('plugin-commands-audit', () => {
|
||||
})
|
||||
|
||||
test('audit --audit-level', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(1)
|
||||
@@ -132,17 +70,14 @@ describe('plugin-commands-audit', () => {
|
||||
})
|
||||
|
||||
test('audit: no vulnerabilities', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.NO_VULN_RESP)
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(stripAnsi(output)).toBe('No known vulnerabilities found\n')
|
||||
@@ -150,18 +85,15 @@ describe('plugin-commands-audit', () => {
|
||||
})
|
||||
|
||||
test('audit --json', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
json: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
const json = JSON.parse(output)
|
||||
@@ -170,19 +102,16 @@ describe('plugin-commands-audit', () => {
|
||||
})
|
||||
|
||||
test.skip('audit does not exit with code 1 if the found vulnerabilities are having lower severity then what we asked for', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.DEV_VULN_ONLY_RESP)
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'high',
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
dev: true,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
@@ -191,20 +120,17 @@ describe('plugin-commands-audit', () => {
|
||||
})
|
||||
|
||||
test('audit --json respects audit-level', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.DEV_VULN_ONLY_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'critical',
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
json: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
dev: true,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
@@ -213,20 +139,17 @@ describe('plugin-commands-audit', () => {
|
||||
})
|
||||
|
||||
test('audit --json filters advisories by audit-level', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.DEV_VULN_ONLY_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'high',
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
json: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
dev: true,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(1)
|
||||
@@ -240,46 +163,41 @@ describe('plugin-commands-audit', () => {
|
||||
})
|
||||
|
||||
test('audit does not exit with code 1 if the registry responds with a non-200 response and ignoreRegistryErrors is used', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(500, { message: 'Something bad happened' })
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits')
|
||||
.reply(500, { message: 'Fallback failed too' })
|
||||
const { output, exitCode } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
dev: true,
|
||||
fetchRetries: 0,
|
||||
ignoreRegistryErrors: true,
|
||||
production: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
expect(stripAnsi(output)).toBe(`The audit endpoint (at ${registries.default}-/npm/v1/security/audits/quick) responded with 500: {"message":"Something bad happened"}. Fallback endpoint (at ${registries.default}-/npm/v1/security/audits) responded with 500: {"message":"Fallback failed too"}`)
|
||||
expect(stripAnsi(output)).toBe(`The audit endpoint (at ${AUDIT_REGISTRY}-/npm/v1/security/audits/quick) responded with 500: {"message":"Something bad happened"}. Fallback endpoint (at ${AUDIT_REGISTRY}-/npm/v1/security/audits) responded with 500: {"message":"Fallback failed too"}`)
|
||||
})
|
||||
|
||||
test('audit sends authToken', async () => {
|
||||
nock(registries.default, {
|
||||
nock(AUDIT_REGISTRY, {
|
||||
reqheaders: { authorization: 'Bearer 123' },
|
||||
})
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.NO_VULN_RESP)
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
userConfig: {},
|
||||
rawConfig: {
|
||||
registry: registries.default,
|
||||
[`${registries.default.replace(/^https?:/, '')}:_authToken`]: '123',
|
||||
registry: AUDIT_REGISTRY,
|
||||
[`${AUDIT_REGISTRY.replace(/^https?:/, '')}:_authToken`]: '123',
|
||||
},
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(stripAnsi(output)).toBe('No known vulnerabilities found\n')
|
||||
@@ -287,41 +205,36 @@ describe('plugin-commands-audit', () => {
|
||||
})
|
||||
|
||||
test('audit endpoint does not exist', async () => {
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(404, {})
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits')
|
||||
.reply(404, {})
|
||||
|
||||
await expect(audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
dir: hasVulnerabilitiesDir,
|
||||
rootProjectManifestDir: hasVulnerabilitiesDir,
|
||||
dev: true,
|
||||
fetchRetries: 0,
|
||||
ignoreRegistryErrors: false,
|
||||
production: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})).rejects.toThrow(AuditEndpointNotExistsError)
|
||||
})
|
||||
|
||||
test('audit: CVEs in ignoreCves do not show up', async () => {
|
||||
const tmp = f.prepare('has-vulnerabilities')
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
rootProjectManifest: {},
|
||||
auditConfig: {
|
||||
ignoreCves: [
|
||||
@@ -331,7 +244,6 @@ describe('plugin-commands-audit', () => {
|
||||
'CVE-2020-7598',
|
||||
],
|
||||
},
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(1)
|
||||
@@ -341,17 +253,15 @@ describe('plugin-commands-audit', () => {
|
||||
test('audit: CVEs in ignoreGhsas do not show up', async () => {
|
||||
const tmp = f.prepare('has-vulnerabilities')
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
rootProjectManifest: {},
|
||||
auditConfig: {
|
||||
ignoreGhsas: [
|
||||
@@ -361,7 +271,6 @@ describe('plugin-commands-audit', () => {
|
||||
'GHSA-vh95-rmgr-6w4m',
|
||||
],
|
||||
},
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(1)
|
||||
@@ -371,18 +280,16 @@ describe('plugin-commands-audit', () => {
|
||||
test('audit: CVEs in ignoreCves do not show up when JSON output is used', async () => {
|
||||
const tmp = f.prepare('has-vulnerabilities')
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
json: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
rootProjectManifest: {},
|
||||
auditConfig: {
|
||||
ignoreCves: [
|
||||
@@ -392,7 +299,6 @@ describe('plugin-commands-audit', () => {
|
||||
'CVE-2020-7598',
|
||||
],
|
||||
},
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(1)
|
||||
|
||||
@@ -5,34 +5,26 @@ import { readProjectManifest } from '@pnpm/read-project-manifest'
|
||||
import { readYamlFileSync } from 'read-yaml-file'
|
||||
import nock from 'nock'
|
||||
import * as responses from './utils/responses/index.js'
|
||||
import { AUDIT_REGISTRY_OPTS, AUDIT_REGISTRY } from './utils/options.js'
|
||||
|
||||
const f = fixtures(import.meta.dirname)
|
||||
const registries = {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
}
|
||||
const rawConfig = {
|
||||
registry: registries.default,
|
||||
}
|
||||
|
||||
test('overrides with references (via $) are preserved during audit --fix', async () => {
|
||||
const tmp = f.prepare('preserve-reference-overrides')
|
||||
|
||||
nock(registries.default)
|
||||
nock(AUDIT_REGISTRY)
|
||||
.post('/-/npm/v1/security/audits/quick')
|
||||
.reply(200, responses.ALL_VULN_RESP)
|
||||
|
||||
const { manifest: initialManifest } = await readProjectManifest(tmp)
|
||||
|
||||
const { exitCode, output } = await audit.handler({
|
||||
...AUDIT_REGISTRY_OPTS,
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
rootProjectManifestDir: tmp,
|
||||
rootProjectManifest: initialManifest,
|
||||
fix: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
overrides: {
|
||||
'is-positive': '1.0.0',
|
||||
},
|
||||
|
||||
83
lockfile/plugin-commands-audit/test/utils/options.ts
Normal file
83
lockfile/plugin-commands-audit/test/utils/options.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
|
||||
|
||||
const registries = {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
}
|
||||
const rawConfig = {
|
||||
registry: registries.default,
|
||||
}
|
||||
export const DEFAULT_OPTS = {
|
||||
argv: {
|
||||
original: [],
|
||||
},
|
||||
bail: true,
|
||||
bin: 'node_modules/.bin',
|
||||
ca: undefined,
|
||||
cacheDir: '../cache',
|
||||
cert: undefined,
|
||||
excludeLinksFromLockfile: false,
|
||||
extraEnv: {},
|
||||
cliOptions: {},
|
||||
fetchRetries: 2,
|
||||
fetchRetryFactor: 90,
|
||||
fetchRetryMaxtimeout: 90,
|
||||
fetchRetryMintimeout: 10,
|
||||
filter: [] as string[],
|
||||
httpsProxy: undefined,
|
||||
include: {
|
||||
dependencies: true,
|
||||
devDependencies: true,
|
||||
optionalDependencies: true,
|
||||
},
|
||||
key: undefined,
|
||||
linkWorkspacePackages: true,
|
||||
localAddress: undefined,
|
||||
lock: false,
|
||||
lockStaleDuration: 90,
|
||||
networkConcurrency: 16,
|
||||
offline: false,
|
||||
pending: false,
|
||||
pnpmfile: ['./.pnpmfile.cjs'],
|
||||
pnpmHomeDir: '',
|
||||
preferWorkspacePackages: true,
|
||||
proxy: undefined,
|
||||
rawConfig,
|
||||
rawLocalConfig: {},
|
||||
registries,
|
||||
rootProjectManifestDir: '',
|
||||
registry: registries.default,
|
||||
sort: true,
|
||||
storeDir: '../store',
|
||||
strictSsl: false,
|
||||
userAgent: 'pnpm',
|
||||
userConfig: {},
|
||||
useRunningStoreServer: false,
|
||||
useStoreServer: false,
|
||||
workspaceConcurrency: 4,
|
||||
virtualStoreDirMaxLength: process.platform === 'win32' ? 60 : 120,
|
||||
peersSuffixMaxLength: 1000,
|
||||
}
|
||||
|
||||
export const AUDIT_REGISTRY = 'http://audit.registry/'
|
||||
export const AUDIT_REGISTRY_OPTS = {
|
||||
...DEFAULT_OPTS,
|
||||
registry: AUDIT_REGISTRY,
|
||||
registries: {
|
||||
default: AUDIT_REGISTRY,
|
||||
},
|
||||
rawConfig: {
|
||||
registry: AUDIT_REGISTRY,
|
||||
},
|
||||
}
|
||||
|
||||
export const MOCK_REGISTRY = `http://localhost:${REGISTRY_MOCK_PORT}`
|
||||
export const MOCK_REGISTRY_OPTS = {
|
||||
...DEFAULT_OPTS,
|
||||
registry: MOCK_REGISTRY,
|
||||
registries: {
|
||||
default: MOCK_REGISTRY,
|
||||
},
|
||||
rawConfig: {
|
||||
registry: MOCK_REGISTRY,
|
||||
},
|
||||
}
|
||||
@@ -40,11 +40,23 @@
|
||||
{
|
||||
"path": "../../pkg-manifest/read-project-manifest"
|
||||
},
|
||||
{
|
||||
"path": "../../workspace/filter-workspace-packages"
|
||||
},
|
||||
{
|
||||
"path": "../audit"
|
||||
},
|
||||
{
|
||||
"path": "../fs"
|
||||
},
|
||||
{
|
||||
"path": "../types"
|
||||
},
|
||||
{
|
||||
"path": "../utils"
|
||||
},
|
||||
{
|
||||
"path": "../walker"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -44,3 +44,32 @@ export type PinnedVersion =
|
||||
| 'major'
|
||||
|
||||
export type IgnoredBuilds = Set<DepPath>
|
||||
|
||||
export interface PackageVulnerabilityAudit {
|
||||
/**
|
||||
* Check if the given package version is vulnerable.
|
||||
*/
|
||||
isVulnerable: (packageName: string, version: string) => boolean
|
||||
/**
|
||||
* Get all vulnerabilities for all packages.
|
||||
* @returns A map where the keys are package names and the values are arrays of vulnerabilities for those packages.
|
||||
*/
|
||||
getVulnerabilities: () => Map<string, PackageVulnerability[]>
|
||||
}
|
||||
|
||||
export interface PackageVulnerability {
|
||||
/**
|
||||
* A semver version range that indicates which versions are vulnerable
|
||||
*/
|
||||
versionRange: string
|
||||
/**
|
||||
* The severity of the vulnerability
|
||||
*/
|
||||
severity: VulnerabilitySeverity
|
||||
}
|
||||
|
||||
export type VulnerabilitySeverity =
|
||||
| 'low'
|
||||
| 'moderate'
|
||||
| 'high'
|
||||
| 'critical'
|
||||
|
||||
@@ -18,6 +18,7 @@ import type {
|
||||
ReadPackageHook,
|
||||
Registries,
|
||||
TrustPolicy,
|
||||
PackageVulnerabilityAudit,
|
||||
} from '@pnpm/types'
|
||||
import type { CustomResolver, CustomFetcher, PreResolutionHookContext } from '@pnpm/hooks.types'
|
||||
import { parseOverrides, type VersionOverride } from '@pnpm/parse-overrides'
|
||||
@@ -174,6 +175,7 @@ export interface StrictInstallOptions {
|
||||
trustPolicy?: TrustPolicy
|
||||
trustPolicyExclude?: string[]
|
||||
trustPolicyIgnoreAfter?: number
|
||||
packageVulnerabilityAudit?: PackageVulnerabilityAudit
|
||||
blockExoticSubdeps?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -557,6 +557,41 @@ export async function mutateModules (
|
||||
includeDirect: opts.includeDirect,
|
||||
})
|
||||
.map((wantedDependency) => ({ ...wantedDependency, updateSpec: true }))
|
||||
if (opts.packageVulnerabilityAudit) {
|
||||
for (const dep of wantedDependencies) {
|
||||
let specifier: string | undefined = dep.bareSpecifier
|
||||
const catalogName = specifier ? parseCatalogProtocol(specifier) : null
|
||||
if (catalogName != null) {
|
||||
const catalogResult = resolveFromCatalog(opts.catalogs, { alias: dep.alias, bareSpecifier: specifier! })
|
||||
specifier = matchCatalogResolveResult(catalogResult, pickCatalogSpecifier)
|
||||
}
|
||||
const validVersion = semver.valid(specifier)
|
||||
// Only proceed if the specifier is a pinned version, not a range
|
||||
if (!validVersion) continue
|
||||
if (opts.packageVulnerabilityAudit.isVulnerable(dep.alias, validVersion)) {
|
||||
// If the current version is pinned and vulnerable, expand the specifier to a range
|
||||
// that will allow updating to a non-vulnerable, semver-compatible version, if available.
|
||||
if (catalogName != null && opts.catalogs?.[catalogName]) {
|
||||
// If a catalog is used, update the catalog entry so the resolver can find a
|
||||
// non-vulnerable version. The package.json keeps "catalog:" and the workspace manifest
|
||||
// gets updated.
|
||||
opts.catalogs = {
|
||||
...opts.catalogs,
|
||||
[catalogName]: {
|
||||
...opts.catalogs[catalogName],
|
||||
[dep.alias]: '^' + validVersion,
|
||||
},
|
||||
}
|
||||
// Set prevSpecifier to the original catalog specifier so the resolver
|
||||
// preserves the original pinning style (i.e. pinned stays pinned).
|
||||
dep.prevSpecifier = specifier
|
||||
} else {
|
||||
// If no catalog is used, we directly update the specifier.
|
||||
dep.bareSpecifier = '^' + validVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.wantedLockfile?.importers) {
|
||||
forgetResolutionsOfPrevWantedDeps(ctx.wantedLockfile.importers[project.id], wantedDependencies, _isWantedDepBareSpecifierSame)
|
||||
|
||||
@@ -13,12 +13,20 @@ import { filterDependenciesByType } from '@pnpm/manifest-utils'
|
||||
import { findWorkspacePackages } from '@pnpm/workspace.find-packages'
|
||||
import type { LockfileObject } from '@pnpm/lockfile.types'
|
||||
import { createStoreController, type CreateStoreControllerOptions } from '@pnpm/store-connection-manager'
|
||||
import type { IncludedDependencies, Project, ProjectsGraph, ProjectRootDir } from '@pnpm/types'
|
||||
import type {
|
||||
IncludedDependencies,
|
||||
Project,
|
||||
ProjectsGraph,
|
||||
ProjectRootDir,
|
||||
PackageVulnerabilityAudit,
|
||||
VulnerabilitySeverity,
|
||||
} from '@pnpm/types'
|
||||
import {
|
||||
IgnoredBuildsError,
|
||||
install,
|
||||
mutateModulesInSingleProject,
|
||||
type MutateModulesOptions,
|
||||
type UpdateMatchingFunction,
|
||||
type WorkspacePackages,
|
||||
} from '@pnpm/core'
|
||||
import { globalInfo, logger } from '@pnpm/logger'
|
||||
@@ -26,6 +34,7 @@ import { sequenceGraph } from '@pnpm/sort-packages'
|
||||
import { updateWorkspaceManifest } from '@pnpm/workspace.manifest-writer'
|
||||
import { createPkgGraph } from '@pnpm/workspace.pkgs-graph'
|
||||
import { updateWorkspaceState, type WorkspaceStateSettings } from '@pnpm/workspace.state'
|
||||
import type { PreferredVersions, VersionSelectors } from '@pnpm/resolver-base'
|
||||
import { getPinnedVersion } from './getPinnedVersion.js'
|
||||
import { getSaveType } from './getSaveType.js'
|
||||
import {
|
||||
@@ -127,7 +136,7 @@ export type InstallDepsOptions = Pick<Config,
|
||||
lockfileCheck?: (prev: LockfileObject, next: LockfileObject) => void
|
||||
update?: boolean
|
||||
updateToLatest?: boolean
|
||||
updateMatching?: (pkgName: string) => boolean
|
||||
updateMatching?: UpdateMatchingFunction
|
||||
updatePackageManifest?: boolean
|
||||
useBetaCli?: boolean
|
||||
recursive?: boolean
|
||||
@@ -137,6 +146,7 @@ export type InstallDepsOptions = Pick<Config,
|
||||
fetchFullMetadata?: boolean
|
||||
pruneLockfileImporters?: boolean
|
||||
pnpmfile: string[]
|
||||
packageVulnerabilityAudit?: PackageVulnerabilityAudit
|
||||
} & Partial<Pick<Config, 'pnpmHomeDir' | 'strictDepBuilds'>>
|
||||
|
||||
export async function installDeps (
|
||||
@@ -224,6 +234,7 @@ when running add/update with the --workspace option')
|
||||
},
|
||||
forceHoistPattern,
|
||||
forcePublicHoistPattern,
|
||||
preferredVersions: opts.packageVulnerabilityAudit ? preferNonvulnerablePackageVersions(opts.packageVulnerabilityAudit) : undefined,
|
||||
allProjectsGraph,
|
||||
selectedProjectsGraph,
|
||||
storeControllerAndDir: store,
|
||||
@@ -272,11 +283,12 @@ when running add/update with the --workspace option')
|
||||
storeController: store.ctrl,
|
||||
storeDir: store.dir,
|
||||
workspacePackages,
|
||||
preferredVersions: opts.packageVulnerabilityAudit ? preferNonvulnerablePackageVersions(opts.packageVulnerabilityAudit) : undefined,
|
||||
}
|
||||
|
||||
let updateMatch: UpdateDepsMatcher | null
|
||||
let updatePackageManifest = opts.updatePackageManifest
|
||||
let updateMatching: ((pkgName: string) => boolean) | undefined
|
||||
let updateMatching: UpdateMatchingFunction | undefined
|
||||
if (opts.update) {
|
||||
if (params.length === 0) {
|
||||
const ignoreDeps = opts.updateConfig?.ignoreDependencies
|
||||
@@ -288,6 +300,11 @@ when running add/update with the --workspace option')
|
||||
} else {
|
||||
updateMatch = null
|
||||
}
|
||||
if (opts.packageVulnerabilityAudit != null) {
|
||||
updateMatch = null
|
||||
const { packageVulnerabilityAudit } = opts
|
||||
updateMatching = (pkgName: string, version?: string) => version != null && packageVulnerabilityAudit.isVulnerable(pkgName, version)
|
||||
}
|
||||
if (updateMatch != null) {
|
||||
params = matchDependencies(updateMatch, manifest, includeDirect)
|
||||
if (params.length === 0) {
|
||||
@@ -445,3 +462,55 @@ async function recursiveInstallThenUpdateWorkspaceState (
|
||||
}
|
||||
return recursiveResult
|
||||
}
|
||||
|
||||
function severityStringToNumber (severity: VulnerabilitySeverity): number {
|
||||
switch (severity) {
|
||||
case 'low': return 0
|
||||
case 'moderate': return 1
|
||||
case 'high': return 2
|
||||
case 'critical': return 3
|
||||
default: return -1
|
||||
}
|
||||
}
|
||||
|
||||
function getVulnerabilityPenalty (severity: VulnerabilitySeverity): number {
|
||||
switch (severity) {
|
||||
case 'low': return -1100 // 100 more than DIRECT_DEP_SELECTOR_WEIGHT from @pnpm/resolver-base
|
||||
case 'moderate': return -2000
|
||||
case 'high': return -3000
|
||||
case 'critical': return -4000
|
||||
// Treat unrecognized severity as the lowest severity
|
||||
default: return -1100
|
||||
}
|
||||
}
|
||||
|
||||
function preferNonvulnerablePackageVersions (packageVulnerabilityAudit: PackageVulnerabilityAudit): PreferredVersions {
|
||||
const preferredVersions: PreferredVersions = {}
|
||||
for (const [packageName, vulnerabilities] of packageVulnerabilityAudit.getVulnerabilities()) {
|
||||
const vulnerableRanges = new Map<string, VulnerabilitySeverity>()
|
||||
for (const vuln of vulnerabilities) {
|
||||
const existingSeverity = vulnerableRanges.get(vuln.versionRange)
|
||||
if (existingSeverity == null) {
|
||||
vulnerableRanges.set(vuln.versionRange, vuln.severity)
|
||||
continue
|
||||
}
|
||||
// Choose the highest severity for the same version range
|
||||
if (severityStringToNumber(vuln.severity) > severityStringToNumber(existingSeverity)) {
|
||||
vulnerableRanges.set(vuln.versionRange, vuln.severity)
|
||||
}
|
||||
}
|
||||
const preferredVersionSelectors: VersionSelectors = {}
|
||||
for (const [vulnRange, severity] of vulnerableRanges) {
|
||||
if (vulnRange === '__proto__' || vulnRange === 'constructor' || vulnRange === 'prototype') {
|
||||
// Prevent prototype pollution
|
||||
continue
|
||||
}
|
||||
preferredVersionSelectors[vulnRange] = {
|
||||
selectorType: 'range',
|
||||
weight: getVulnerabilityPenalty(severity),
|
||||
}
|
||||
}
|
||||
preferredVersions[packageName] = preferredVersionSelectors
|
||||
}
|
||||
return preferredVersions
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import { globalInfo } from '@pnpm/logger'
|
||||
import { createMatcher } from '@pnpm/matcher'
|
||||
import { outdatedDepsOfProjects } from '@pnpm/outdated'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import type { IncludedDependencies, ProjectRootDir } from '@pnpm/types'
|
||||
import type { IncludedDependencies, ProjectRootDir, PackageVulnerabilityAudit } from '@pnpm/types'
|
||||
import type { UpdateMatchingFunction } from '@pnpm/core'
|
||||
import enquirer from 'enquirer'
|
||||
import chalk from 'chalk'
|
||||
import { pick, pluck, unnest } from 'ramda'
|
||||
@@ -163,6 +164,7 @@ dependencies is not found inside the workspace',
|
||||
export type UpdateCommandOptions = InstallCommandOptions & {
|
||||
interactive?: boolean
|
||||
latest?: boolean
|
||||
packageVulnerabilityAudit?: PackageVulnerabilityAudit
|
||||
}
|
||||
|
||||
export async function handler (
|
||||
@@ -295,6 +297,15 @@ async function update (
|
||||
optionalDependencies: opts.rawConfig.optional !== false,
|
||||
}
|
||||
const depth = opts.depth ?? Infinity
|
||||
let updateMatching: UpdateMatchingFunction | undefined
|
||||
if (opts.packageVulnerabilityAudit != null) {
|
||||
const { packageVulnerabilityAudit } = opts
|
||||
updateMatching = (pkgName: string, version?: string) => version != null && packageVulnerabilityAudit.isVulnerable(pkgName, version)
|
||||
} else if (
|
||||
(dependencies.length > 0) && dependencies.every(dep => !dep.substring(1).includes('@')) && depth > 0 && !opts.latest
|
||||
) {
|
||||
updateMatching = createMatcher(dependencies)
|
||||
}
|
||||
return installDeps({
|
||||
...opts,
|
||||
allowNew: false,
|
||||
@@ -304,9 +315,7 @@ async function update (
|
||||
include,
|
||||
update: true,
|
||||
updateToLatest: opts.latest,
|
||||
updateMatching: (dependencies.length > 0) && dependencies.every(dep => !dep.substring(1).includes('@')) && depth > 0 && !opts.latest
|
||||
? createMatcher(dependencies)
|
||||
: undefined,
|
||||
updateMatching,
|
||||
updatePackageManifest: opts.save !== false,
|
||||
resolutionMode: opts.save === false ? 'highest' : opts.resolutionMode,
|
||||
}, dependencies)
|
||||
|
||||
@@ -268,7 +268,7 @@ type ParentPkg = Pick<PkgAddress, 'nodeId' | 'installable' | 'rootDir' | 'option
|
||||
|
||||
export type ParentPkgAliases = Record<string, PkgAddress | true>
|
||||
|
||||
export type UpdateMatchingFunction = (pkgName: string) => boolean
|
||||
export type UpdateMatchingFunction = (pkgName: string, version?: string) => boolean
|
||||
|
||||
interface ResolvedDependenciesOptions {
|
||||
currentDepth: number
|
||||
@@ -836,7 +836,7 @@ async function resolveDependenciesOfDependency (
|
||||
(options.updateMatching == null) ||
|
||||
(
|
||||
extendedWantedDep.infoFromLockfile?.name != null &&
|
||||
options.updateMatching(extendedWantedDep.infoFromLockfile.name)
|
||||
options.updateMatching(extendedWantedDep.infoFromLockfile.name, extendedWantedDep.infoFromLockfile.version)
|
||||
)
|
||||
)
|
||||
const update = updateRequested ||
|
||||
|
||||
@@ -94,7 +94,7 @@ export interface Importer<WantedDepExtraProps> {
|
||||
|
||||
export interface ImporterToResolveGeneric<WantedDepExtraProps> extends Importer<WantedDepExtraProps> {
|
||||
updatePackageManifest: boolean
|
||||
updateMatching?: (pkgName: string) => boolean
|
||||
updateMatching?: (pkgName: string, version?: string) => boolean
|
||||
updateToLatest?: boolean
|
||||
hasRemovedDependencies?: boolean
|
||||
preferredVersions?: PreferredVersions
|
||||
|
||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -4047,9 +4047,21 @@ importers:
|
||||
'@pnpm/lockfile.fs':
|
||||
specifier: workspace:*
|
||||
version: link:../fs
|
||||
'@pnpm/lockfile.types':
|
||||
specifier: workspace:*
|
||||
version: link:../types
|
||||
'@pnpm/lockfile.utils':
|
||||
specifier: workspace:*
|
||||
version: link:../utils
|
||||
'@pnpm/lockfile.walker':
|
||||
specifier: workspace:*
|
||||
version: link:../walker
|
||||
'@pnpm/network.auth-header':
|
||||
specifier: workspace:*
|
||||
version: link:../../network/auth-header
|
||||
'@pnpm/plugin-commands-installation':
|
||||
specifier: workspace:*
|
||||
version: link:../../pkg-manager/plugin-commands-installation
|
||||
'@pnpm/read-project-manifest':
|
||||
specifier: workspace:*
|
||||
version: link:../../pkg-manifest/read-project-manifest
|
||||
@@ -4071,19 +4083,28 @@ importers:
|
||||
render-help:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0
|
||||
semver:
|
||||
specifier: 'catalog:'
|
||||
version: 7.7.4
|
||||
devDependencies:
|
||||
'@pnpm/filter-workspace-packages':
|
||||
specifier: workspace:*
|
||||
version: link:../../workspace/filter-workspace-packages
|
||||
'@pnpm/plugin-commands-audit':
|
||||
specifier: workspace:*
|
||||
version: 'link:'
|
||||
'@pnpm/plugin-commands-installation':
|
||||
specifier: workspace:*
|
||||
version: link:../../pkg-manager/plugin-commands-installation
|
||||
'@pnpm/registry-mock':
|
||||
specifier: 'catalog:'
|
||||
version: 5.2.2(verdaccio@6.2.7(encoding@0.1.13)(typanion@3.14.0))
|
||||
'@pnpm/test-fixtures':
|
||||
specifier: workspace:*
|
||||
version: link:../../__utils__/test-fixtures
|
||||
'@types/ramda':
|
||||
specifier: 'catalog:'
|
||||
version: 0.29.12
|
||||
'@types/semver':
|
||||
specifier: 'catalog:'
|
||||
version: 7.7.1
|
||||
'@types/zkochan__table':
|
||||
specifier: 'catalog:'
|
||||
version: '@types/table@6.0.0'
|
||||
|
||||
@@ -184,7 +184,7 @@ export async function main (inputArgv: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
if (
|
||||
(cmd === 'install' || cmd === 'import' || cmd === 'dedupe' || cmd === 'patch-commit' || cmd === 'patch' || cmd === 'patch-remove' || cmd === 'approve-builds') &&
|
||||
(cmd === 'install' || cmd === 'import' || cmd === 'dedupe' || cmd === 'patch-commit' || cmd === 'patch' || cmd === 'patch-remove' || cmd === 'approve-builds' || cmd === 'audit') &&
|
||||
typeof workspaceDir === 'string'
|
||||
) {
|
||||
cliOptions['recursive'] = true
|
||||
|
||||
@@ -188,6 +188,15 @@ function prioritizePreferredVersions (
|
||||
): string[][] {
|
||||
const preferredVerSelectorsArr = Object.entries(preferredVerSelectors ?? {})
|
||||
const versionsPrioritizer = new PreferredVersionsPrioritizer()
|
||||
|
||||
// First, add all versions that satisfy versionRange with default weight 0
|
||||
for (const version of Object.keys(meta.versions)) {
|
||||
if (semverSatisfiesLoose(version, versionRange)) {
|
||||
versionsPrioritizer.add(version, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Then apply weights from preferred selectors
|
||||
for (const [preferredSelector, preferredSelectorType] of preferredVerSelectorsArr) {
|
||||
const { selectorType, weight } = typeof preferredSelectorType === 'string'
|
||||
? { selectorType: preferredSelectorType, weight: 1 }
|
||||
|
||||
@@ -691,6 +691,32 @@ test('prefer the version that has bigger weight in preferred selectors', async (
|
||||
expect(resolveResult!.id).toBe('is-positive@3.0.0')
|
||||
})
|
||||
|
||||
test('versions without selector weights should have higher priority than negatively weighted versions', async () => {
|
||||
nock(registries.default)
|
||||
.get('/is-positive')
|
||||
.reply(200, isPositiveMeta)
|
||||
|
||||
const { resolveFromNpm } = createResolveFromNpm({
|
||||
storeDir: temporaryDirectory(),
|
||||
cacheDir: temporaryDirectory(),
|
||||
registries,
|
||||
})
|
||||
const resolveResult = await resolveFromNpm({
|
||||
alias: 'is-positive',
|
||||
bareSpecifier: '^3.0.0',
|
||||
}, {
|
||||
preferredVersions: {
|
||||
'is-positive': {
|
||||
// Penalize 3.0.0, but don't mention 3.1.0
|
||||
'3.0.0': { selectorType: 'version', weight: -1 },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 3.1.0 should be selected because it has default weight 0, higher than 3.0.0's -1
|
||||
expect(resolveResult!.id).toBe('is-positive@3.1.0')
|
||||
})
|
||||
|
||||
test('offline resolution fails when package meta not found in the store', async () => {
|
||||
const cacheDir = temporaryDirectory()
|
||||
const { resolveFromNpm } = createResolveFromNpm({
|
||||
|
||||
Reference in New Issue
Block a user