fix(audit): add authentication to pnpm-audit (#5053)

Adds authentication to pnpm-audit for private registries

close #5038
This commit is contained in:
Simon Schmid
2022-07-18 16:09:25 +02:00
committed by GitHub
parent 7cba20498a
commit af79b6184c
10 changed files with 122 additions and 5 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/audit": minor
"@pnpm/plugin-commands-audit": minor
---
Add authentication to audit command

View File

@@ -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:*",

View File

@@ -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
}

View File

@@ -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,
},
})
})
})

View File

@@ -18,6 +18,9 @@
{
"path": "../fetch"
},
{
"path": "../fetching-types"
},
{
"path": "../lockfile-file"
},

View 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": {

View File

@@ -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,

View File

@@ -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,
})

View File

@@ -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
View File

@@ -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: