diff --git a/.changeset/bright-dingos-stare.md b/.changeset/bright-dingos-stare.md new file mode 100644 index 0000000000..6614be442d --- /dev/null +++ b/.changeset/bright-dingos-stare.md @@ -0,0 +1,6 @@ +--- +"@pnpm/audit": minor +"@pnpm/plugin-commands-audit": minor +--- + +Add authentication to audit command diff --git a/packages/audit/package.json b/packages/audit/package.json index d07c08a0d6..bf5ab39d47 100644 --- a/packages/audit/package.json +++ b/packages/audit/package.json @@ -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:*", diff --git a/packages/audit/src/index.ts b/packages/audit/src/index.ts index 339e020f7b..694ea2f793 100644 --- a/packages/audit/src/index.ts +++ b/packages/audit/src/index.ts @@ -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 } + +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 +} diff --git a/packages/audit/test/index.ts b/packages/audit/test/index.ts index 0098d33e31..24cea6767e 100644 --- a/packages/audit/test/index.ts +++ b/packages/audit/test/index.ts @@ -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, + }, + }) + }) }) diff --git a/packages/audit/tsconfig.json b/packages/audit/tsconfig.json index ce6e5aad50..652e71cdf5 100644 --- a/packages/audit/tsconfig.json +++ b/packages/audit/tsconfig.json @@ -18,6 +18,9 @@ { "path": "../fetch" }, + { + "path": "../fetching-types" + }, { "path": "../lockfile-file" }, diff --git a/packages/plugin-commands-audit/package.json b/packages/plugin-commands-audit/package.json index d3f2c111e2..601cb61cf7 100644 --- a/packages/plugin-commands-audit/package.json +++ b/packages/plugin-commands-audit/package.json @@ -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": { diff --git a/packages/plugin-commands-audit/src/audit.ts b/packages/plugin-commands-audit/src/audit.ts index bb82a79990..de8418235f 100644 --- a/packages/plugin-commands-audit/src/audit.ts +++ b/packages/plugin-commands-audit/src/audit.ts @@ -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, diff --git a/packages/plugin-commands-audit/test/fix.ts b/packages/plugin-commands-audit/test/fix.ts index f9e53e83dc..d2fdd4119e 100644 --- a/packages/plugin-commands-audit/test/fix.ts +++ b/packages/plugin-commands-audit/test/fix.ts @@ -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, }) diff --git a/packages/plugin-commands-audit/test/index.ts b/packages/plugin-commands-audit/test/index.ts index 8159a4ae45..1b42641008 100644 --- a/packages/plugin-commands-audit/test/index.ts +++ b/packages/plugin-commands-audit/test/index.ts @@ -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) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d7162b589..a1df4bf75e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: