mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-26 15:37:26 -04:00
fix(audit): add authentication to pnpm-audit (#5053)
Adds authentication to pnpm-audit for private registries close #5038
This commit is contained in:
6
.changeset/bright-dingos-stare.md
Normal file
6
.changeset/bright-dingos-stare.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/audit": minor
|
||||
"@pnpm/plugin-commands-audit": minor
|
||||
---
|
||||
|
||||
Add authentication to audit command
|
||||
@@ -38,6 +38,7 @@
|
||||
"dependencies": {
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/fetch": "workspace:*",
|
||||
"@pnpm/fetching-types": "workspace:*",
|
||||
"@pnpm/lockfile-types": "workspace:*",
|
||||
"@pnpm/lockfile-utils": "workspace:*",
|
||||
"@pnpm/lockfile-walker": "workspace:*",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PnpmError from '@pnpm/error'
|
||||
import { AgentOptions, fetchWithAgent, RetryTimeoutOptions } from '@pnpm/fetch'
|
||||
import { GetCredentials } from '@pnpm/fetching-types'
|
||||
import { Lockfile } from '@pnpm/lockfile-types'
|
||||
import { DependenciesField } from '@pnpm/types'
|
||||
import lockfileToAuditTree from './lockfileToAuditTree'
|
||||
@@ -9,6 +10,7 @@ export * from './types'
|
||||
|
||||
export default async function audit (
|
||||
lockfile: Lockfile,
|
||||
getCredentials: GetCredentials,
|
||||
opts: {
|
||||
agentOptions?: AgentOptions
|
||||
include?: { [dependenciesField in DependenciesField]: boolean }
|
||||
@@ -20,10 +22,15 @@ export default async function audit (
|
||||
const auditTree = lockfileToAuditTree(lockfile, { include: opts.include })
|
||||
const registry = opts.registry.endsWith('/') ? opts.registry : `${opts.registry}/`
|
||||
const auditUrl = `${registry}-/npm/v1/security/audits`
|
||||
const credentials = getCredentials(registry)
|
||||
|
||||
const res = await fetchWithAgent(auditUrl, {
|
||||
agentOptions: opts.agentOptions ?? {},
|
||||
body: JSON.stringify(auditTree),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...getAuthHeaders(credentials),
|
||||
},
|
||||
method: 'post',
|
||||
retry: opts.retry,
|
||||
timeout: opts.timeout,
|
||||
@@ -33,3 +40,16 @@ export default async function audit (
|
||||
}
|
||||
return res.json() as Promise<AuditReport>
|
||||
}
|
||||
|
||||
function getAuthHeaders (
|
||||
credentials: {
|
||||
authHeaderValue: string | undefined
|
||||
alwaysAuth: boolean | undefined
|
||||
}
|
||||
) {
|
||||
const headers: { authorization?: string } = {}
|
||||
if (credentials.alwaysAuth && credentials.authHeaderValue) {
|
||||
headers['authorization'] = credentials.authHeaderValue // eslint-disable-line
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
@@ -73,7 +73,10 @@ describe('audit', () => {
|
||||
|
||||
test('an error is thrown if the audit endpoint responds with a non-OK code', async () => {
|
||||
const registry = 'http://registry.registry/'
|
||||
nock(registry)
|
||||
const getCredentials = () => ({ authHeaderValue: undefined, alwaysAuth: undefined })
|
||||
nock(registry, {
|
||||
badheaders: ['authorization'],
|
||||
})
|
||||
.post('/-/npm/v1/security/audits')
|
||||
.reply(500, { message: 'Something bad happened' })
|
||||
|
||||
@@ -82,7 +85,9 @@ describe('audit', () => {
|
||||
await audit({
|
||||
importers: {},
|
||||
lockfileVersion: 5,
|
||||
}, {
|
||||
},
|
||||
getCredentials,
|
||||
{
|
||||
registry,
|
||||
retry: {
|
||||
retries: 0,
|
||||
@@ -96,4 +101,27 @@ describe('audit', () => {
|
||||
expect(err.code).toEqual('ERR_PNPM_AUDIT_BAD_RESPONSE')
|
||||
expect(err.message).toEqual('The audit endpoint (at http://registry.registry/-/npm/v1/security/audits) responded with 500: {"message":"Something bad happened"}')
|
||||
})
|
||||
|
||||
test('authorization header is sent if alwaysAuth is true', async () => {
|
||||
const registry = 'http://registry.registry/'
|
||||
const getCredentials = () => ({ authHeaderValue: 'Bearer 123', alwaysAuth: true })
|
||||
|
||||
nock(registry, {
|
||||
reqheaders: { authorization: 'Bearer 123' },
|
||||
})
|
||||
.post('/-/npm/v1/security/audits')
|
||||
.reply(200, {})
|
||||
|
||||
await audit({
|
||||
importers: {},
|
||||
lockfileVersion: 5,
|
||||
},
|
||||
getCredentials,
|
||||
{
|
||||
registry,
|
||||
retry: {
|
||||
retries: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
{
|
||||
"path": "../fetch"
|
||||
},
|
||||
{
|
||||
"path": "../fetching-types"
|
||||
},
|
||||
{
|
||||
"path": "../lockfile-file"
|
||||
},
|
||||
|
||||
@@ -52,7 +52,9 @@
|
||||
"@zkochan/table": "^1.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"ramda": "^0.28.0",
|
||||
"render-help": "^1.0.2"
|
||||
"render-help": "^1.0.2",
|
||||
"credentials-by-uri": "^2.1.0",
|
||||
"mem": "^8.1.1"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"exports": {
|
||||
|
||||
@@ -10,6 +10,7 @@ import chalk from 'chalk'
|
||||
import pick from 'ramda/src/pick.js'
|
||||
import renderHelp from 'render-help'
|
||||
import fix from './fix'
|
||||
import getCredentialsByURI from 'credentials-by-uri'
|
||||
|
||||
// eslint-disable
|
||||
const AUDIT_LEVEL_NUMBER = {
|
||||
@@ -123,6 +124,9 @@ export async function handler (
|
||||
| 'production'
|
||||
| 'dev'
|
||||
| 'optional'
|
||||
| 'alwaysAuth'
|
||||
| 'userConfig'
|
||||
| 'rawConfig'
|
||||
>
|
||||
) {
|
||||
const lockfile = await readWantedLockfile(opts.lockfileDir ?? opts.dir, { ignoreIncompatible: true })
|
||||
@@ -135,8 +139,9 @@ export async function handler (
|
||||
optionalDependencies: opts.optional !== false,
|
||||
}
|
||||
let auditReport!: AuditReport
|
||||
const getCredentials = (registry: string) => getCredentialsByURI(opts.rawConfig, registry, opts.userConfig)
|
||||
try {
|
||||
auditReport = await audit(lockfile, {
|
||||
auditReport = await audit(lockfile, getCredentials, {
|
||||
agentOptions: {
|
||||
ca: opts.ca,
|
||||
cert: opts.cert,
|
||||
|
||||
@@ -10,6 +10,9 @@ const f = fixtures(__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')
|
||||
@@ -22,6 +25,8 @@ test('overrides are added for vulnerable dependencies', async () => {
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
fix: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
})
|
||||
|
||||
@@ -44,6 +49,8 @@ test('no overrides are added if no vulnerabilities are found', async () => {
|
||||
auditLevel: 'moderate',
|
||||
dir: tmp,
|
||||
fix: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
})
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import * as responses from './utils/responses'
|
||||
const registries = {
|
||||
default: 'https://registry.npmjs.org/',
|
||||
}
|
||||
const rawConfig = {
|
||||
registry: registries.default,
|
||||
}
|
||||
|
||||
test('audit', async () => {
|
||||
nock(registries.default)
|
||||
@@ -15,6 +18,8 @@ test('audit', async () => {
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
})
|
||||
expect(exitCode).toBe(1)
|
||||
@@ -30,6 +35,8 @@ test('audit --dev', async () => {
|
||||
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
|
||||
dev: true,
|
||||
production: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
})
|
||||
|
||||
@@ -45,6 +52,8 @@ test('audit --audit-level', async () => {
|
||||
const { output, exitCode } = await audit.handler({
|
||||
auditLevel: 'moderate',
|
||||
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
})
|
||||
|
||||
@@ -59,6 +68,8 @@ test('audit: no vulnerabilities', async () => {
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
dir: path.join(__dirname, '../../../fixtures/has-outdated-deps'),
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
})
|
||||
|
||||
@@ -74,6 +85,8 @@ test('audit --json', async () => {
|
||||
const { output, exitCode } = await audit.handler({
|
||||
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
|
||||
json: true,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
})
|
||||
|
||||
@@ -90,6 +103,8 @@ test.skip('audit does not exit with code 1 if the found vulnerabilities are havi
|
||||
const { output, exitCode } = await audit.handler({
|
||||
auditLevel: 'high',
|
||||
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
dev: true,
|
||||
registries,
|
||||
})
|
||||
@@ -109,9 +124,33 @@ test('audit does not exit with code 1 if the registry responds with a non-200 re
|
||||
fetchRetries: 0,
|
||||
ignoreRegistryErrors: true,
|
||||
production: false,
|
||||
userConfig: {},
|
||||
rawConfig,
|
||||
registries,
|
||||
})
|
||||
|
||||
expect(exitCode).toBe(0)
|
||||
expect(stripAnsi(output)).toBe(`The audit endpoint (at ${registries.default}-/npm/v1/security/audits) responded with 500: {"message":"Something bad happened"}`)
|
||||
})
|
||||
|
||||
test('audit sends authToken if alwaysAuth is true', async () => {
|
||||
nock(registries.default, {
|
||||
reqheaders: { authorization: 'Bearer 123' },
|
||||
})
|
||||
.post('/-/npm/v1/security/audits')
|
||||
.reply(200, responses.NO_VULN_RESP)
|
||||
|
||||
const { output, exitCode } = await audit.handler({
|
||||
dir: path.join(__dirname, '../../../fixtures/has-outdated-deps'),
|
||||
userConfig: {},
|
||||
rawConfig: {
|
||||
registry: registries.default,
|
||||
'always-auth': true,
|
||||
[`${registries.default.replace(/^https?:/, '')}:_authToken`]: '123',
|
||||
},
|
||||
registries,
|
||||
})
|
||||
|
||||
expect(stripAnsi(output)).toBe('No known vulnerabilities found\n')
|
||||
expect(exitCode).toBe(0)
|
||||
})
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -157,6 +157,7 @@ importers:
|
||||
'@pnpm/constants': workspace:*
|
||||
'@pnpm/error': workspace:*
|
||||
'@pnpm/fetch': workspace:*
|
||||
'@pnpm/fetching-types': workspace:*
|
||||
'@pnpm/lockfile-file': workspace:*
|
||||
'@pnpm/lockfile-types': workspace:*
|
||||
'@pnpm/lockfile-utils': workspace:*
|
||||
@@ -166,6 +167,7 @@ importers:
|
||||
dependencies:
|
||||
'@pnpm/error': link:../error
|
||||
'@pnpm/fetch': link:../fetch
|
||||
'@pnpm/fetching-types': link:../fetching-types
|
||||
'@pnpm/lockfile-types': link:../lockfile-types
|
||||
'@pnpm/lockfile-utils': link:../lockfile-utils
|
||||
'@pnpm/lockfile-walker': link:../lockfile-walker
|
||||
@@ -2128,7 +2130,9 @@ importers:
|
||||
'@types/zkochan__table': npm:@types/table@6.0.0
|
||||
'@zkochan/table': ^1.0.0
|
||||
chalk: ^4.1.2
|
||||
credentials-by-uri: ^2.1.0
|
||||
load-json-file: ^6.2.0
|
||||
mem: ^8.1.1
|
||||
nock: 13.2.8
|
||||
ramda: ^0.28.0
|
||||
render-help: ^1.0.2
|
||||
@@ -2145,6 +2149,8 @@ importers:
|
||||
'@pnpm/types': link:../types
|
||||
'@zkochan/table': 1.0.0
|
||||
chalk: 4.1.2
|
||||
credentials-by-uri: 2.1.0
|
||||
mem: 8.1.1
|
||||
ramda: 0.28.0
|
||||
render-help: 1.0.2
|
||||
devDependencies:
|
||||
|
||||
Reference in New Issue
Block a user