Skip to content

Commit

Permalink
fix(audit): add authentication to pnpm-audit (#5053)
Browse files Browse the repository at this point in the history
Adds authentication to pnpm-audit for private registries

close #5038
  • Loading branch information
sled committed Jul 18, 2022
1 parent 7cba204 commit af79b61
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .changeset/bright-dingos-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pnpm/audit": minor
"@pnpm/plugin-commands-audit": minor
---

Add authentication to audit command
1 change: 1 addition & 0 deletions packages/audit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
22 changes: 21 additions & 1 deletion packages/audit/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -9,6 +10,7 @@ export * from './types'

export default async function audit (
lockfile: Lockfile,
getCredentials: GetCredentials,
opts: {
agentOptions?: AgentOptions
include?: { [dependenciesField in DependenciesField]: boolean }
Expand All @@ -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,
Expand All @@ -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
}
32 changes: 30 additions & 2 deletions packages/audit/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })

Expand All @@ -82,7 +85,9 @@ describe('audit', () => {
await audit({
importers: {},
lockfileVersion: 5,
}, {
},
getCredentials,
{
registry,
retry: {
retries: 0,
Expand All @@ -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,
},
})
})
})
3 changes: 3 additions & 0 deletions packages/audit/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
{
"path": "../fetch"
},
{
"path": "../fetching-types"
},
{
"path": "../lockfile-file"
},
Expand Down
4 changes: 3 additions & 1 deletion packages/plugin-commands-audit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
7 changes: 6 additions & 1 deletion packages/plugin-commands-audit/src/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -123,6 +124,9 @@ export async function handler (
| 'production'
| 'dev'
| 'optional'
| 'alwaysAuth'
| 'userConfig'
| 'rawConfig'
>
) {
const lockfile = await readWantedLockfile(opts.lockfileDir ?? opts.dir, { ignoreIncompatible: true })
Expand All @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions packages/plugin-commands-audit/test/fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -22,6 +25,8 @@ test('overrides are added for vulnerable dependencies', async () => {
auditLevel: 'moderate',
dir: tmp,
fix: true,
userConfig: {},
rawConfig,
registries,
})

Expand All @@ -44,6 +49,8 @@ test('no overrides are added if no vulnerabilities are found', async () => {
auditLevel: 'moderate',
dir: tmp,
fix: true,
userConfig: {},
rawConfig,
registries,
})

Expand Down
39 changes: 39 additions & 0 deletions packages/plugin-commands-audit/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -30,6 +35,8 @@ test('audit --dev', async () => {
dir: path.join(__dirname, 'fixtures/has-vulnerabilities'),
dev: true,
production: false,
userConfig: {},
rawConfig,
registries,
})

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

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

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

Expand All @@ -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,
})
Expand All @@ -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 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit af79b61

Please sign in to comment.