Skip to content

Commit 0def984

Browse files
committed
Add a working test
1 parent 054b5fe commit 0def984

File tree

3 files changed

+185
-174
lines changed

3 files changed

+185
-174
lines changed

lib/commands/audit.js

Lines changed: 71 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ const fetch = require('npm-registry-fetch')
66
const localeCompare = require('@isaacs/string-locale-compare')('en')
77
const npa = require('npm-package-arg')
88
const pacote = require('pacote')
9-
// const pickManifest = require('npm-pick-manifest')
109

1110
const ArboristWorkspaceCmd = require('../arborist-cmd.js')
1211
const auditError = require('../utils/audit-error.js')
1312
const {
1413
registry: { default: defaultRegistry },
1514
} = require('../utils/config/definitions.js')
16-
// const log = require('../utils/log-shim.js')
15+
const log = require('../utils/log-shim.js')
16+
const pulseTillDone = require('../utils/pulse-till-done.js')
1717
const reifyFinish = require('../utils/reify-finish.js')
1818

1919
const validateSignature = async ({ message, signature, publicKey }) => {
@@ -45,8 +45,10 @@ class VerifySignatures {
4545
const nodes = this.tree.inventory.values()
4646
this.getEdges(nodes, 'edgesOut')
4747
const edges = Array.from(this.edges)
48+
if (edges.length === 0) {
49+
throw new Error('No dependencies found')
50+
}
4851

49-
// QUESTION: Do we need to get the registry host from the resolved url to handle proxies?
5052
// Prefetch and cache public keys from used registries
5153
const registries = this.findAllRegistryUrls(edges, this.npm.flatOptions)
5254
for (const registry of registries) {
@@ -75,11 +77,13 @@ class VerifySignatures {
7577
this.appendOutput(this.makeJSON({ invalid, missing }))
7678
} else {
7779
const timing = `audited ${edges.length} packages in ${Math.floor(Number(elapsed) / 1e9)}s`
78-
const verifiedPrefix = verified ? 'verified signatures, ' : ''
80+
const verifiedPrefix = verified ? 'verified registry signatures, ' : ''
7981
this.appendOutput(`${verifiedPrefix}${timing}\n`)
8082

8183
if (this.verified && !verified) {
82-
this.appendOutput(`${this.verified} ${chalk.bold('verified')} packages\n`)
84+
this.appendOutput(
85+
`${this.verified} packages have ${chalk.bold('verified')} registry signatures\n`
86+
)
8387
}
8488

8589
if (missing.length) {
@@ -191,16 +195,6 @@ class VerifySignatures {
191195
}
192196
}
193197

194-
// TODO: Remove this once we can get time from pacote.manifest
195-
async getPackument (spec) {
196-
const packument = await pacote.packument(spec, {
197-
...this.npm.flatOptions,
198-
fullMetadata: this.npm.config.get('long'),
199-
preferOffline: true,
200-
})
201-
return packument
202-
}
203-
204198
async getKeys ({ registry }) {
205199
return await fetch.json('/-/npm/v1/keys', {
206200
...this.npm.flatOptions,
@@ -260,95 +254,75 @@ class VerifySignatures {
260254
return null
261255
}
262256

263-
try {
264-
const name = alias ? edge.spec.replace('npm', edge.name) : edge.name
265-
// QUESTION: Is name@version the right way to get the manifest?
266-
const manifest = await pacote.manifest(`${name}@${version}`, this.npm.flatOptions)
267-
const registry = fetch.pickRegistry(spec, this.npm.flatOptions)
268-
269-
const { _integrity: integrity, _signatures } = manifest
270-
const message = `${name}@${version}:${integrity}`
271-
const signatures = _signatures || []
272-
273-
// TODO: Get version created time from manifest
274-
//
275-
// const packument = await this.getPackument(spec)
276-
// const versionCreated = packument.time && packument.time[version]
277-
const keys = this.keys.get(registry) || []
278-
const validKeys = keys.filter((publicKey) => {
279-
if (!publicKey.expires) {
280-
return true
281-
}
282-
// return Date.parse(versionCreated) < Date.parse(publicKey.expires)
283-
return Date.parse(publicKey.expires) > Date.now()
257+
const name = alias ? edge.spec.replace('npm', edge.name) : edge.name
258+
const manifest = await pacote.manifest(`${name}@${version}`, this.npm.flatOptions)
259+
const registry = fetch.pickRegistry(spec, this.npm.flatOptions)
260+
261+
const { _integrity: integrity, _signatures } = manifest
262+
const message = `${name}@${version}:${integrity}`
263+
const signatures = _signatures || []
264+
265+
const keys = this.keys.get(registry) || []
266+
const validKeys = keys.filter((publicKey) => {
267+
if (!publicKey.expires) {
268+
return true
269+
}
270+
return Date.parse(publicKey.expires) > Date.now()
271+
})
272+
273+
// Currently we only care about missing signatures on registries that provide a public key
274+
// We could make this configurable in the future with a strict/paranoid mode
275+
if (!signatures.length && validKeys.length) {
276+
this.missing.add({
277+
name,
278+
path,
279+
version,
280+
location,
281+
registry,
282+
})
283+
284+
return
285+
}
286+
287+
await Promise.all(signatures.map(async (signature) => {
288+
const publicKey = keys.filter(key => key.keyid === signature.keyid)[0]
289+
const validPublicKey = validKeys.filter(key => key.keyid === signature.keyid)[0]
290+
291+
if (!publicKey && !validPublicKey) {
292+
throw new Error(
293+
`${name} has a signature with keyid: ${signature.keyid} ` +
294+
`but not corresponding public key can be found on ${registry}-/npm/v1/keys`
295+
)
296+
} else if (publicKey && !validPublicKey) {
297+
throw new Error(
298+
`${name} has a signature with keyid: ${signature.keyid} ` +
299+
`but the corresponding public key on ${registry}-/npm/v1/keys has expired ` +
300+
`(${publicKey.expires})`
301+
)
302+
}
303+
304+
const valid = await validateSignature({
305+
message,
306+
signature: signature.sig,
307+
publicKey: validPublicKey.pemKey,
284308
})
285309

286-
// Currently we only care about missing signatures on registries that provide a public key
287-
// We could make this configurable in the future with a strict/paranoid mode
288-
if (!signatures.length && validKeys.length) {
289-
this.missing.add({
310+
if (!valid) {
311+
this.invalid.add({
290312
name,
291313
path,
314+
type,
292315
version,
293316
location,
294317
registry,
295-
})
296-
297-
return
298-
}
299-
300-
await Promise.all(signatures.map(async (signature) => {
301-
const publicKey = keys.filter(key => key.keyid === signature.keyid)[0]
302-
const validPublicKey = validKeys.filter(key => key.keyid === signature.keyid)[0]
303-
304-
if (!publicKey && !validPublicKey) {
305-
throw new Error(
306-
`${name} has a signature with keyid: ${signature.keyid} ` +
307-
`but not corresponding public key can be found on ${registry}-/npm/v1/keys`
308-
)
309-
} else if (publicKey && !validPublicKey) {
310-
throw new Error(
311-
`${name} has a signature with keyid: ${signature.keyid} ` +
312-
`but the corresponding public key on ${registry}-/npm/v1/keys has expired ` +
313-
`(${publicKey.expires})`
314-
)
315-
}
316-
317-
const valid = await validateSignature({
318-
message,
318+
integrity,
319319
signature: signature.sig,
320-
publicKey: validPublicKey.pemKey,
320+
keyid: signature.keyid,
321321
})
322-
323-
if (!valid) {
324-
this.invalid.add({
325-
name,
326-
path,
327-
type,
328-
version,
329-
location,
330-
registry,
331-
integrity,
332-
signature: signature.sig,
333-
keyid: signature.keyid,
334-
})
335-
} else {
336-
this.verified++
337-
}
338-
}))
339-
} catch (err) {
340-
// QUESTION: Is this the right way to handle these errors?
341-
//
342-
// silently catch and ignore ETARGET, E403 &
343-
// E404 errors, deps are just skipped
344-
if (!(
345-
err.code === 'ETARGET' ||
346-
err.code === 'E403' ||
347-
err.code === 'E404')
348-
) {
349-
throw err
322+
} else {
323+
this.verified++
350324
}
351-
}
325+
}))
352326
}
353327

354328
humanOutput (list) {
@@ -471,6 +445,7 @@ class Audit extends ArboristWorkspaceCmd {
471445
}
472446

473447
async auditSignatures () {
448+
log.newItem('loading intalled packages')
474449
const reporter = this.npm.config.get('json') ? 'json' : 'detail'
475450
const opts = {
476451
...this.npm.flatOptions,
@@ -494,8 +469,9 @@ class Audit extends ArboristWorkspaceCmd {
494469
arb.excludeWorkspacesDependencySet(tree)
495470
}
496471

472+
log.newItem('verifying registry signatures')
497473
const verify = new VerifySignatures(tree, filterSet, this.npm, { ...opts })
498-
await verify.run()
474+
await pulseTillDone.withPromise(verify.run())
499475
const result = verify.report()
500476
process.exitCode = process.exitCode || result.exitCode
501477
this.npm.output(result.report)

tap-snapshots/test/lib/commands/audit.js.test.cjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ added 1 package, and audited 2 packages in xxx
4141
found 0 vulnerabilities
4242
`
4343

44+
exports[`test/lib/commands/audit.js TAP audit signatures signature verification with valid signatures > must match snapshot 1`] = `
45+
verified registry signatures, audited 1 packages in xxx
46+
47+
`
48+
4449
exports[`test/lib/commands/audit.js TAP fallback audit > must match snapshot 1`] = `
4550
# npm audit report
4651
@@ -124,8 +129,3 @@ node_modules/test-dep-a
124129
To address all issues, run:
125130
npm audit fix
126131
`
127-
128-
exports[`test/lib/commands/audit.js TAP signature verification with valid signatures > must match snapshot 1`] = `
129-
verified signatures, audited 1 packages in xxx
130-
131-
`

0 commit comments

Comments
 (0)