From 469f7885ca47f79bbd3c7171dc56a471a3e422a2 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Mon, 22 Apr 2024 14:28:58 -0700 Subject: [PATCH] fix(cleanup): newlines and whitespace --- lib/base-command.js | 1 + lib/commands/access.js | 1 - lib/commands/adduser.js | 2 +- lib/commands/audit.js | 388 +------------------------------ lib/commands/ci.js | 1 - lib/commands/completion.js | 11 +- lib/commands/config.js | 2 +- lib/commands/dedupe.js | 3 +- lib/commands/dist-tag.js | 1 + lib/commands/docs.js | 2 + lib/commands/doctor.js | 3 +- lib/commands/edit.js | 42 ++-- lib/commands/explain.js | 1 + lib/commands/explore.js | 6 +- lib/commands/find-dupes.js | 3 +- lib/commands/fund.js | 1 + lib/commands/get.js | 1 + lib/commands/help-search.js | 1 + lib/commands/help.js | 3 +- lib/commands/hook.js | 3 +- lib/commands/install-ci-test.js | 6 +- lib/commands/install-test.js | 6 +- lib/commands/install.js | 3 +- lib/commands/link.js | 1 + lib/commands/login.js | 2 +- lib/commands/logout.js | 1 + lib/commands/ls.js | 9 +- lib/commands/org.js | 1 + lib/commands/outdated.js | 2 +- lib/commands/pack.js | 1 + lib/commands/ping.js | 1 + lib/commands/prefix.js | 1 + lib/commands/profile.js | 6 +- lib/commands/prune.js | 5 +- lib/commands/publish.js | 5 +- lib/commands/query.js | 2 - lib/commands/rebuild.js | 3 +- lib/commands/repo.js | 3 +- lib/commands/root.js | 2 + lib/commands/run-script.js | 2 +- lib/commands/sbom.js | 2 - lib/commands/search.js | 2 +- lib/commands/set.js | 1 + lib/commands/shrinkwrap.js | 1 + lib/commands/star.js | 3 +- lib/commands/stars.js | 3 +- lib/commands/team.js | 2 +- lib/commands/token.js | 4 +- lib/commands/uninstall.js | 4 +- lib/commands/unpublish.js | 3 +- lib/commands/unstar.js | 1 + lib/commands/update.js | 5 +- lib/commands/version.js | 1 - lib/commands/view.js | 5 +- lib/commands/whoami.js | 3 +- lib/lifecycle-cmd.js | 1 + lib/npm.js | 1 + lib/package-url-cmd.js | 1 + lib/utils/did-you-mean.js | 1 + lib/utils/verify-signatures.js | 389 ++++++++++++++++++++++++++++++++ 60 files changed, 499 insertions(+), 472 deletions(-) create mode 100644 lib/utils/verify-signatures.js diff --git a/lib/base-command.js b/lib/base-command.js index fb33005cc901b..9c30ef8a4b44e 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -179,4 +179,5 @@ class BaseCommand { this.workspacePaths = [...ws.values()] } } + module.exports = BaseCommand diff --git a/lib/commands/access.js b/lib/commands/access.js index 20565e274398e..1d0161cadc91e 100644 --- a/lib/commands/access.js +++ b/lib/commands/access.js @@ -3,7 +3,6 @@ const npa = require('npm-package-arg') const { output } = require('proc-log') const pkgJson = require('@npmcli/package-json') const localeCompare = require('@isaacs/string-locale-compare')('en') - const otplease = require('../utils/otplease.js') const getIdentity = require('../utils/get-identity.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/adduser.js b/lib/commands/adduser.js index 842819f2bf44b..d896272bf7ed5 100644 --- a/lib/commands/adduser.js +++ b/lib/commands/adduser.js @@ -1,7 +1,6 @@ const { log, output } = require('proc-log') const { redactLog: replaceInfo } = require('@npmcli/redact') const auth = require('../utils/auth.js') - const BaseCommand = require('../base-command.js') class AddUser extends BaseCommand { @@ -47,4 +46,5 @@ class AddUser extends BaseCommand { output.standard(message) } } + module.exports = AddUser diff --git a/lib/commands/audit.js b/lib/commands/audit.js index 7eada9f617e70..aed1be7a82906 100644 --- a/lib/commands/audit.js +++ b/lib/commands/audit.js @@ -1,395 +1,9 @@ const npmAuditReport = require('npm-audit-report') -const fetch = require('npm-registry-fetch') -const localeCompare = require('@isaacs/string-locale-compare')('en') -const npa = require('npm-package-arg') -const pacote = require('pacote') -const pMap = require('p-map') -const tufClient = require('@sigstore/tuf') - const ArboristWorkspaceCmd = require('../arborist-cmd.js') const auditError = require('../utils/audit-error.js') const { log, output } = require('proc-log') const reifyFinish = require('../utils/reify-finish.js') - -const sortAlphabetically = (a, b) => localeCompare(a.name, b.name) - -class VerifySignatures { - constructor (tree, filterSet, npm, opts) { - this.tree = tree - this.filterSet = filterSet - this.npm = npm - this.opts = opts - this.keys = new Map() - this.invalid = [] - this.missing = [] - this.checkedPackages = new Set() - this.auditedWithKeysCount = 0 - this.verifiedSignatureCount = 0 - this.verifiedAttestationCount = 0 - this.exitCode = 0 - } - - async run () { - const start = process.hrtime.bigint() - - // Find all deps in tree - const { edges, registries } = this.getEdgesOut(this.tree.inventory.values(), this.filterSet) - if (edges.size === 0) { - throw new Error('found no installed dependencies to audit') - } - - const tuf = await tufClient.initTUF({ - cachePath: this.opts.tufCache, - retry: this.opts.retry, - timeout: this.opts.timeout, - }) - await Promise.all([...registries].map(registry => this.setKeys({ registry, tuf }))) - - log.verbose('verifying registry signatures') - await pMap(edges, (e) => this.getVerifiedInfo(e), { concurrency: 20, stopOnError: true }) - - // Didn't find any dependencies that could be verified, e.g. only local - // deps, missing version, not on a registry etc. - if (!this.auditedWithKeysCount) { - throw new Error('found no dependencies to audit that were installed from ' + - 'a supported registry') - } - - const invalid = this.invalid.sort(sortAlphabetically) - const missing = this.missing.sort(sortAlphabetically) - - const hasNoInvalidOrMissing = invalid.length === 0 && missing.length === 0 - - if (!hasNoInvalidOrMissing) { - process.exitCode = 1 - } - - if (this.npm.config.get('json')) { - output.standard(JSON.stringify({ - invalid, - missing, - }, null, 2)) - return - } - const end = process.hrtime.bigint() - const elapsed = end - start - - const auditedPlural = this.auditedWithKeysCount > 1 ? 's' : '' - const timing = `audited ${this.auditedWithKeysCount} package${auditedPlural} in ` + - `${Math.floor(Number(elapsed) / 1e9)}s` - output.standard(timing) - output.standard('') - - const verifiedBold = this.npm.chalk.bold('verified') - if (this.verifiedSignatureCount) { - if (this.verifiedSignatureCount === 1) { - /* eslint-disable-next-line max-len */ - output.standard(`${this.verifiedSignatureCount} package has a ${verifiedBold} registry signature`) - } else { - /* eslint-disable-next-line max-len */ - output.standard(`${this.verifiedSignatureCount} packages have ${verifiedBold} registry signatures`) - } - output.standard('') - } - - if (this.verifiedAttestationCount) { - if (this.verifiedAttestationCount === 1) { - /* eslint-disable-next-line max-len */ - output.standard(`${this.verifiedAttestationCount} package has a ${verifiedBold} attestation`) - } else { - /* eslint-disable-next-line max-len */ - output.standard(`${this.verifiedAttestationCount} packages have ${verifiedBold} attestations`) - } - output.standard('') - } - - if (missing.length) { - const missingClr = this.npm.chalk.redBright('missing') - if (missing.length === 1) { - /* eslint-disable-next-line max-len */ - output.standard(`1 package has a ${missingClr} registry signature but the registry is providing signing keys:`) - } else { - /* eslint-disable-next-line max-len */ - output.standard(`${missing.length} packages have ${missingClr} registry signatures but the registry is providing signing keys:`) - } - output.standard('') - missing.map(m => - output.standard(`${this.npm.chalk.red(`${m.name}@${m.version}`)} (${m.registry})`) - ) - } - - if (invalid.length) { - if (missing.length) { - output.standard('') - } - const invalidClr = this.npm.chalk.redBright('invalid') - // We can have either invalid signatures or invalid provenance - const invalidSignatures = this.invalid.filter(i => i.code === 'EINTEGRITYSIGNATURE') - if (invalidSignatures.length) { - if (invalidSignatures.length === 1) { - output.standard(`1 package has an ${invalidClr} registry signature:`) - } else { - /* eslint-disable-next-line max-len */ - output.standard(`${invalidSignatures.length} packages have ${invalidClr} registry signatures:`) - } - output.standard('') - invalidSignatures.map(i => - output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`) - ) - output.standard('') - } - - const invalidAttestations = this.invalid.filter(i => i.code === 'EATTESTATIONVERIFY') - if (invalidAttestations.length) { - if (invalidAttestations.length === 1) { - output.standard(`1 package has an ${invalidClr} attestation:`) - } else { - /* eslint-disable-next-line max-len */ - output.standard(`${invalidAttestations.length} packages have ${invalidClr} attestations:`) - } - output.standard('') - invalidAttestations.map(i => - output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`) - ) - output.standard('') - } - - if (invalid.length === 1) { - /* eslint-disable-next-line max-len */ - output.standard(`Someone might have tampered with this package since it was published on the registry!`) - } else { - /* eslint-disable-next-line max-len */ - output.standard(`Someone might have tampered with these packages since they were published on the registry!`) - } - output.standard('') - } - } - - getEdgesOut (nodes, filterSet) { - const edges = new Set() - const registries = new Set() - for (const node of nodes) { - for (const edge of node.edgesOut.values()) { - const filteredOut = - edge.from - && filterSet - && filterSet.size > 0 - && !filterSet.has(edge.from.target) - - if (!filteredOut) { - const spec = this.getEdgeSpec(edge) - if (spec) { - // Prefetch and cache public keys from used registries - registries.add(this.getSpecRegistry(spec)) - } - edges.add(edge) - } - } - } - return { edges, registries } - } - - async setKeys ({ registry, tuf }) { - const { host, pathname } = new URL(registry) - // Strip any trailing slashes from pathname - const regKey = `${host}${pathname.replace(/\/$/, '')}/keys.json` - let keys = await tuf.getTarget(regKey) - .then((target) => JSON.parse(target)) - .then(({ keys: ks }) => ks.map((key) => ({ - ...key, - keyid: key.keyId, - pemkey: `-----BEGIN PUBLIC KEY-----\n${key.publicKey.rawBytes}\n-----END PUBLIC KEY-----`, - expires: key.publicKey.validFor.end || null, - }))).catch(err => { - if (err.code === 'TUF_FIND_TARGET_ERROR') { - return null - } else { - throw err - } - }) - - // If keys not found in Sigstore TUF repo, fallback to registry keys API - if (!keys) { - keys = await fetch.json('/-/npm/v1/keys', { - ...this.npm.flatOptions, - registry, - }).then(({ keys: ks }) => ks.map((key) => ({ - ...key, - pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`, - }))).catch(err => { - if (err.code === 'E404' || err.code === 'E400') { - return null - } else { - throw err - } - }) - } - - if (keys) { - this.keys.set(registry, keys) - } - } - - getEdgeType (edge) { - return edge.optional ? 'optionalDependencies' - : edge.peer ? 'peerDependencies' - : edge.dev ? 'devDependencies' - : 'dependencies' - } - - getEdgeSpec (edge) { - let name = edge.name - try { - name = npa(edge.spec).subSpec.name - } catch { - // leave it as edge.name - } - try { - return npa(`${name}@${edge.spec}`) - } catch { - // Skip packages with invalid spec - } - } - - buildRegistryConfig (registry) { - const keys = this.keys.get(registry) || [] - const parsedRegistry = new URL(registry) - const regKey = `//${parsedRegistry.host}${parsedRegistry.pathname}` - return { - [`${regKey}:_keys`]: keys, - } - } - - getSpecRegistry (spec) { - return fetch.pickRegistry(spec, this.npm.flatOptions) - } - - getValidPackageInfo (edge) { - const type = this.getEdgeType(edge) - // Skip potentially optional packages that are not on disk, as these could - // be omitted during install - if (edge.error === 'MISSING' && type !== 'dependencies') { - return - } - - const spec = this.getEdgeSpec(edge) - // Skip invalid version requirements - if (!spec) { - return - } - const node = edge.to || edge - const { version } = node.package || {} - - if (node.isWorkspace || // Skip local workspaces packages - !version || // Skip packages that don't have a installed version, e.g. optonal dependencies - !spec.registry) { // Skip if not from registry, e.g. git package - return - } - - for (const omitType of this.npm.config.get('omit')) { - if (node[omitType]) { - return - } - } - - return { - name: spec.name, - version, - type, - location: node.location, - registry: this.getSpecRegistry(spec), - } - } - - async verifySignatures (name, version, registry) { - const { - _integrity: integrity, - _signatures, - _attestations, - _resolved: resolved, - } = await pacote.manifest(`${name}@${version}`, { - verifySignatures: true, - verifyAttestations: true, - ...this.buildRegistryConfig(registry), - ...this.npm.flatOptions, - }) - const signatures = _signatures || [] - const result = { - integrity, - signatures, - attestations: _attestations, - resolved, - } - return result - } - - async getVerifiedInfo (edge) { - const info = this.getValidPackageInfo(edge) - if (!info) { - return - } - const { name, version, location, registry, type } = info - if (this.checkedPackages.has(location)) { - // we already did or are doing this one - return - } - this.checkedPackages.add(location) - - // We only "audit" or verify the signature, or the presence of it, on - // packages whose registry returns signing keys - const keys = this.keys.get(registry) || [] - if (keys.length) { - this.auditedWithKeysCount += 1 - } - - try { - const { integrity, signatures, attestations, resolved } = await this.verifySignatures( - name, version, registry - ) - - // Currently we only care about missing signatures on registries that provide a public key - // We could make this configurable in the future with a strict/paranoid mode - if (signatures.length) { - this.verifiedSignatureCount += 1 - } else if (keys.length) { - this.missing.push({ - integrity, - location, - name, - registry, - resolved, - version, - }) - } - - // Track verified attestations separately to registry signatures, as all - // packages on registries with signing keys are expected to have registry - // signatures, but not all packages have provenance and publish attestations. - if (attestations) { - this.verifiedAttestationCount += 1 - } - } catch (e) { - if (e.code === 'EINTEGRITYSIGNATURE' || e.code === 'EATTESTATIONVERIFY') { - this.invalid.push({ - code: e.code, - message: e.message, - integrity: e.integrity, - keyid: e.keyid, - location, - name, - registry, - resolved: e.resolved, - signature: e.signature, - predicateType: e.predicateType, - type, - version, - }) - } else { - throw e - } - } - } -} +const VerifySignatures = require('../utils/verify-signatures.js') class Audit extends ArboristWorkspaceCmd { static description = 'Run a security audit' diff --git a/lib/commands/ci.js b/lib/commands/ci.js index ff559b6d801d7..a8b4a845126f5 100644 --- a/lib/commands/ci.js +++ b/lib/commands/ci.js @@ -3,7 +3,6 @@ const runScript = require('@npmcli/run-script') const fs = require('fs/promises') const { log, time } = require('proc-log') const validateLockfile = require('../utils/validate-lockfile.js') - const ArboristWorkspaceCmd = require('../arborist-cmd.js') class CI extends ArboristWorkspaceCmd { diff --git a/lib/commands/completion.js b/lib/commands/completion.js index 677d7fa2ec3fe..20ff3ea76e98b 100644 --- a/lib/commands/completion.js +++ b/lib/commands/completion.js @@ -27,23 +27,22 @@ // Matches are wrapped with ' to escape them, if necessary, and then printed // one per line for the shell completion method to consume in IFS=$'\n' mode // as an array. -// const fs = require('fs/promises') const nopt = require('nopt') const { resolve } = require('path') const { output } = require('proc-log') - const Npm = require('../npm.js') const { definitions, shorthands } = require('@npmcli/config/lib/definitions') const { commands, aliases, deref } = require('../utils/cmd-list.js') -const configNames = Object.keys(definitions) -const shorthandNames = Object.keys(shorthands) -const allConfs = configNames.concat(shorthandNames) const { isWindowsShell } = require('../utils/is-windows.js') +const BaseCommand = require('../base-command.js') + const fileExists = (file) => fs.stat(file).then(s => s.isFile()).catch(() => false) -const BaseCommand = require('../base-command.js') +const configNames = Object.keys(definitions) +const shorthandNames = Object.keys(shorthands) +const allConfs = configNames.concat(shorthandNames) class Completion extends BaseCommand { static description = 'Tab Completion for npm' diff --git a/lib/commands/config.js b/lib/commands/config.js index 21faa8838c593..56315208e232f 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -6,6 +6,7 @@ const localeCompare = require('@isaacs/string-locale-compare')('en') const pkgJson = require('@npmcli/package-json') const { defaults, definitions } = require('@npmcli/config/lib/definitions') const { log, output } = require('proc-log') +const BaseCommand = require('../base-command.js') // These are the configs that we can nerf-dart. Not all of them currently even // *have* config definitions so we have to explicitly validate them here @@ -46,7 +47,6 @@ const publicVar = k => { return true } -const BaseCommand = require('../base-command.js') class Config extends BaseCommand { static description = 'Manage the npm configuration files' static name = 'config' diff --git a/lib/commands/dedupe.js b/lib/commands/dedupe.js index 0d0e26621b227..f9785fd66bb49 100644 --- a/lib/commands/dedupe.js +++ b/lib/commands/dedupe.js @@ -1,8 +1,7 @@ -// dedupe duplicated packages, or find them in the tree const reifyFinish = require('../utils/reify-finish.js') - const ArboristWorkspaceCmd = require('../arborist-cmd.js') +// dedupe duplicated packages, or find them in the tree class Dedupe extends ArboristWorkspaceCmd { static description = 'Reduce duplication in the package tree' static name = 'dedupe' diff --git a/lib/commands/dist-tag.js b/lib/commands/dist-tag.js index 741890fc9cec7..7eeeae571bc1c 100644 --- a/lib/commands/dist-tag.js +++ b/lib/commands/dist-tag.js @@ -205,4 +205,5 @@ class DistTag extends BaseCommand { return data } } + module.exports = DistTag diff --git a/lib/commands/docs.js b/lib/commands/docs.js index 5d20215b56a07..2259b49f79617 100644 --- a/lib/commands/docs.js +++ b/lib/commands/docs.js @@ -1,4 +1,5 @@ const PackageUrlCmd = require('../package-url-cmd.js') + class Docs extends PackageUrlCmd { static description = 'Open documentation for a package in a web browser' static name = 'docs' @@ -16,4 +17,5 @@ class Docs extends PackageUrlCmd { return `https://www.npmjs.com/package/${mani.name}` } } + module.exports = Docs diff --git a/lib/commands/doctor.js b/lib/commands/doctor.js index 2c8581a4aba4d..a9f24bd62e0f0 100644 --- a/lib/commands/doctor.js +++ b/lib/commands/doctor.js @@ -8,6 +8,7 @@ const semver = require('semver') const { log, output } = require('proc-log') const ping = require('../utils/ping.js') const { defaults } = require('@npmcli/config/lib/definitions') +const BaseCommand = require('../base-command.js') const maskLabel = mask => { const label = [] @@ -93,7 +94,7 @@ const subcommands = [ // - verify all local packages have bins linked // What is the fix for these? ] -const BaseCommand = require('../base-command.js') + class Doctor extends BaseCommand { static description = 'Check the health of your npm environment' static name = 'doctor' diff --git a/lib/commands/edit.js b/lib/commands/edit.js index eca4fad6fe24c..12c9f759589e7 100644 --- a/lib/commands/edit.js +++ b/lib/commands/edit.js @@ -1,34 +1,31 @@ -// npm edit -// open the package folder in the $EDITOR - const { resolve } = require('path') const { lstat } = require('fs/promises') const cp = require('child_process') const completion = require('../utils/installed-shallow.js') const BaseCommand = require('../base-command.js') -const splitPackageNames = (path) => { - return path.split('/') - // combine scoped parts - .reduce((parts, part) => { - if (parts.length === 0) { - return [part] - } +const splitPackageNames = (path) => path.split('/') +// combine scoped parts + .reduce((parts, part) => { + if (parts.length === 0) { + return [part] + } - const lastPart = parts[parts.length - 1] - // check if previous part is the first part of a scoped package - if (lastPart[0] === '@' && !lastPart.includes('/')) { - parts[parts.length - 1] += '/' + part - } else { - parts.push(part) - } + const lastPart = parts[parts.length - 1] + // check if previous part is the first part of a scoped package + if (lastPart[0] === '@' && !lastPart.includes('/')) { + parts[parts.length - 1] += '/' + part + } else { + parts.push(part) + } - return parts - }, []) - .join('/node_modules/') - .replace(/(\/node_modules)+/, '/node_modules') -} + return parts + }, []) + .join('/node_modules/') + .replace(/(\/node_modules)+/, '/node_modules') +// npm edit +// open the package folder in the $EDITOR class Edit extends BaseCommand { static description = 'Edit an installed package' static name = 'edit' @@ -63,4 +60,5 @@ class Edit extends BaseCommand { }) } } + module.exports = Edit diff --git a/lib/commands/explain.js b/lib/commands/explain.js index 1e09ea0ee61cb..2e7d07df729a8 100644 --- a/lib/commands/explain.js +++ b/lib/commands/explain.js @@ -126,4 +126,5 @@ class Explain extends ArboristWorkspaceCmd { }) } } + module.exports = Explain diff --git a/lib/commands/explore.js b/lib/commands/explore.js index ae29262071555..a4acc821eabf9 100644 --- a/lib/commands/explore.js +++ b/lib/commands/explore.js @@ -1,6 +1,3 @@ -// npm explore [@] -// open a subshell to the package folder. - const pkgJson = require('@npmcli/package-json') const runScript = require('@npmcli/run-script') const { join, relative } = require('path') @@ -8,6 +5,8 @@ const { log, output } = require('proc-log') const completion = require('../utils/installed-shallow.js') const BaseCommand = require('../base-command.js') +// npm explore [@] +// open a subshell to the package folder. class Explore extends BaseCommand { static description = 'Browse an installed package' static name = 'explore' @@ -71,4 +70,5 @@ class Explore extends BaseCommand { }) } } + module.exports = Explore diff --git a/lib/commands/find-dupes.js b/lib/commands/find-dupes.js index 2e06e8b6bd93f..0945089c74b7c 100644 --- a/lib/commands/find-dupes.js +++ b/lib/commands/find-dupes.js @@ -1,6 +1,6 @@ -// dedupe duplicated packages, or find them in the tree const ArboristWorkspaceCmd = require('../arborist-cmd.js') +// dedupe duplicated packages, or find them in the tree class FindDupes extends ArboristWorkspaceCmd { static description = 'Find duplication in the package tree' static name = 'find-dupes' @@ -24,4 +24,5 @@ class FindDupes extends ArboristWorkspaceCmd { return this.npm.exec('dedupe', []) } } + module.exports = FindDupes diff --git a/lib/commands/fund.js b/lib/commands/fund.js index 5aaab2df746a5..8bcd184e70968 100644 --- a/lib/commands/fund.js +++ b/lib/commands/fund.js @@ -222,4 +222,5 @@ class Fund extends ArboristWorkspaceCmd { return [url, message] } } + module.exports = Fund diff --git a/lib/commands/get.js b/lib/commands/get.js index 4bf5d2caf8264..43eca7389ec90 100644 --- a/lib/commands/get.js +++ b/lib/commands/get.js @@ -19,4 +19,5 @@ class Get extends BaseCommand { return this.npm.exec('config', ['get'].concat(args)) } } + module.exports = Get diff --git a/lib/commands/help-search.js b/lib/commands/help-search.js index 8821d932525dc..1bbff2df6cc16 100644 --- a/lib/commands/help-search.js +++ b/lib/commands/help-search.js @@ -191,4 +191,5 @@ class HelpSearch extends BaseCommand { return finalOut.trim() } } + module.exports = HelpSearch diff --git a/lib/commands/help.js b/lib/commands/help.js index 1f51713a8d051..fd84d3f8546ef 100644 --- a/lib/commands/help.js +++ b/lib/commands/help.js @@ -5,9 +5,9 @@ const { glob } = require('glob') const { output } = require('proc-log') const localeCompare = require('@isaacs/string-locale-compare')('en') const { deref } = require('../utils/cmd-list.js') +const BaseCommand = require('../base-command.js') const globify = pattern => pattern.split('\\').join('/') -const BaseCommand = require('../base-command.js') // Strips out the number from foo.7 or foo.7. or foo.7.tgz // We don't currently compress our man pages but if we ever did this would @@ -111,4 +111,5 @@ class Help extends BaseCommand { return 'file:///' + path.resolve(this.npm.npmRoot, `docs/output/${sect}/${f}.html`) } } + module.exports = Help diff --git a/lib/commands/hook.js b/lib/commands/hook.js index 7ec95e079d660..557d04e7a035a 100644 --- a/lib/commands/hook.js +++ b/lib/commands/hook.js @@ -2,8 +2,8 @@ const hookApi = require('libnpmhook') const otplease = require('../utils/otplease.js') const relativeDate = require('tiny-relative-date') const { output } = require('proc-log') - const BaseCommand = require('../base-command.js') + class Hook extends BaseCommand { static description = 'Manage registry hooks' static name = 'hook' @@ -105,4 +105,5 @@ class Hook extends BaseCommand { return `${hook.type === 'owner' ? '~' : ''}${hook.name}` } } + module.exports = Hook diff --git a/lib/commands/install-ci-test.js b/lib/commands/install-ci-test.js index f7a357ba6e124..4b9dd269f8c74 100644 --- a/lib/commands/install-ci-test.js +++ b/lib/commands/install-ci-test.js @@ -1,8 +1,7 @@ -// npm install-ci-test -// Runs `npm ci` and then runs `npm test` - const CI = require('./ci.js') +// npm install-ci-test +// Runs `npm ci` and then runs `npm test` class InstallCITest extends CI { static description = 'Install a project with a clean slate and run tests' static name = 'install-ci-test' @@ -12,4 +11,5 @@ class InstallCITest extends CI { return this.npm.exec('test', []) } } + module.exports = InstallCITest diff --git a/lib/commands/install-test.js b/lib/commands/install-test.js index 11f22e535403c..e21ca7c929c55 100644 --- a/lib/commands/install-test.js +++ b/lib/commands/install-test.js @@ -1,8 +1,7 @@ -// npm install-test -// Runs `npm install` and then runs `npm test` - const Install = require('./install.js') +// npm install-test +// Runs `npm install` and then runs `npm test` class InstallTest extends Install { static description = 'Install package(s) and run tests' static name = 'install-test' @@ -12,4 +11,5 @@ class InstallTest extends Install { return this.npm.exec('test', []) } } + module.exports = InstallTest diff --git a/lib/commands/install.js b/lib/commands/install.js index 6573b4d65555d..24e5f6819b314 100644 --- a/lib/commands/install.js +++ b/lib/commands/install.js @@ -5,8 +5,8 @@ const runScript = require('@npmcli/run-script') const pacote = require('pacote') const checks = require('npm-install-checks') const reifyFinish = require('../utils/reify-finish.js') - const ArboristWorkspaceCmd = require('../arborist-cmd.js') + class Install extends ArboristWorkspaceCmd { static description = 'Install a package' static name = 'install' @@ -172,4 +172,5 @@ class Install extends ArboristWorkspaceCmd { await reifyFinish(this.npm, arb) } } + module.exports = Install diff --git a/lib/commands/link.js b/lib/commands/link.js index 0442e50dc0d83..bde761c4226dc 100644 --- a/lib/commands/link.js +++ b/lib/commands/link.js @@ -185,4 +185,5 @@ class Link extends ArboristWorkspaceCmd { return missing } } + module.exports = Link diff --git a/lib/commands/login.js b/lib/commands/login.js index d38aec51289cc..0801af9726f78 100644 --- a/lib/commands/login.js +++ b/lib/commands/login.js @@ -1,7 +1,6 @@ const { log, output } = require('proc-log') const { redactLog: replaceInfo } = require('@npmcli/redact') const auth = require('../utils/auth.js') - const BaseCommand = require('../base-command.js') class Login extends BaseCommand { @@ -47,4 +46,5 @@ class Login extends BaseCommand { output.standard(message) } } + module.exports = Login diff --git a/lib/commands/logout.js b/lib/commands/logout.js index 338081cccd457..63a8154cd3fdf 100644 --- a/lib/commands/logout.js +++ b/lib/commands/logout.js @@ -46,4 +46,5 @@ class Logout extends BaseCommand { await this.npm.config.save(level) } } + module.exports = Logout diff --git a/lib/commands/ls.js b/lib/commands/ls.js index cf086924092fe..51e99f429816a 100644 --- a/lib/commands/ls.js +++ b/lib/commands/ls.js @@ -1,10 +1,12 @@ const { resolve, relative, sep } = require('path') -const relativePrefix = `.${sep}` - const archy = require('archy') const { breadth } = require('treeverse') const npa = require('npm-package-arg') const { output } = require('proc-log') +const ArboristWorkspaceCmd = require('../arborist-cmd.js') +const localeCompare = require('@isaacs/string-locale-compare')('en') + +const relativePrefix = `.${sep}` const _depth = Symbol('depth') const _dedupe = Symbol('dedupe') @@ -17,8 +19,6 @@ const _parent = Symbol('parent') const _problems = Symbol('problems') const _required = Symbol('required') const _type = Symbol('type') -const ArboristWorkspaceCmd = require('../arborist-cmd.js') -const localeCompare = require('@isaacs/string-locale-compare')('en') class LS extends ArboristWorkspaceCmd { static description = 'List installed packages' @@ -219,6 +219,7 @@ class LS extends ArboristWorkspaceCmd { return tree } } + module.exports = LS const isGitNode = (node) => { diff --git a/lib/commands/org.js b/lib/commands/org.js index f1e5b0e09c62c..f06c537d3f3f7 100644 --- a/lib/commands/org.js +++ b/lib/commands/org.js @@ -149,4 +149,5 @@ class Org extends BaseCommand { } } } + module.exports = Org diff --git a/lib/commands/outdated.js b/lib/commands/outdated.js index 1600315cac642..ceef86be950c5 100644 --- a/lib/commands/outdated.js +++ b/lib/commands/outdated.js @@ -6,7 +6,6 @@ const npa = require('npm-package-arg') const pickManifest = require('npm-pick-manifest') const { output } = require('proc-log') const localeCompare = require('@isaacs/string-locale-compare')('en') - const ArboristWorkspaceCmd = require('../arborist-cmd.js') class Outdated extends ArboristWorkspaceCmd { @@ -369,4 +368,5 @@ class Outdated extends ArboristWorkspaceCmd { return JSON.stringify(out, null, 2) } } + module.exports = Outdated diff --git a/lib/commands/pack.js b/lib/commands/pack.js index 463329235d417..e679377fa7970 100644 --- a/lib/commands/pack.js +++ b/lib/commands/pack.js @@ -83,4 +83,5 @@ class Pack extends BaseCommand { return this.exec([...this.workspacePaths, ...args.filter(a => a !== '.')]) } } + module.exports = Pack diff --git a/lib/commands/ping.js b/lib/commands/ping.js index 3ae4ed80a22cb..2d8f6f91a9111 100644 --- a/lib/commands/ping.js +++ b/lib/commands/ping.js @@ -26,4 +26,5 @@ class Ping extends BaseCommand { } } } + module.exports = Ping diff --git a/lib/commands/prefix.js b/lib/commands/prefix.js index 6c3d6f886f12c..edd5595d09b41 100644 --- a/lib/commands/prefix.js +++ b/lib/commands/prefix.js @@ -11,4 +11,5 @@ class Prefix extends BaseCommand { return output.standard(this.npm.prefix) } } + module.exports = Prefix diff --git a/lib/commands/profile.js b/lib/commands/profile.js index 8bb19e69fd484..a0676e47889af 100644 --- a/lib/commands/profile.js +++ b/lib/commands/profile.js @@ -1,11 +1,11 @@ -const inspect = require('util').inspect +const { inspect } = require('util') const { URL } = require('url') const { log, output } = require('proc-log') const npmProfile = require('npm-profile') const qrcodeTerminal = require('qrcode-terminal') - const otplease = require('../utils/otplease.js') const readUserInfo = require('../utils/read-user-info.js') +const BaseCommand = require('../base-command.js') const qrcode = url => new Promise((resolve) => qrcodeTerminal.generate(url, resolve)) @@ -33,7 +33,6 @@ const writableProfileKeys = [ 'github', ] -const BaseCommand = require('../base-command.js') class Profile extends BaseCommand { static description = 'Change settings on your registry profile' static name = 'profile' @@ -387,4 +386,5 @@ class Profile extends BaseCommand { } } } + module.exports = Profile diff --git a/lib/commands/prune.js b/lib/commands/prune.js index 189fc29cb8bc3..1bcf8a9576316 100644 --- a/lib/commands/prune.js +++ b/lib/commands/prune.js @@ -1,7 +1,7 @@ -// prune extraneous packages const reifyFinish = require('../utils/reify-finish.js') - const ArboristWorkspaceCmd = require('../arborist-cmd.js') + +// prune extraneous packages class Prune extends ArboristWorkspaceCmd { static description = 'Remove extraneous packages' static name = 'prune' @@ -30,4 +30,5 @@ class Prune extends ArboristWorkspaceCmd { await reifyFinish(this.npm, arb) } } + module.exports = Prune diff --git a/lib/commands/publish.js b/lib/commands/publish.js index db4cb7f5dc61e..f174d338b6392 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -7,18 +7,16 @@ const pacote = require('pacote') const npa = require('npm-package-arg') const npmFetch = require('npm-registry-fetch') const { redactLog: replaceInfo } = require('@npmcli/redact') - const otplease = require('../utils/otplease.js') const { getContents, logTar } = require('../utils/tar.js') - // for historical reasons, publishConfig in package.json can contain ANY config // keys that npm supports in .npmrc files and elsewhere. We *may* want to // revisit this at some point, and have a minimal set that's a SemVer-major // change that ought to get a RFC written on it. const { flatten } = require('@npmcli/config/lib/definitions') const pkgJson = require('@npmcli/package-json') - const BaseCommand = require('../base-command.js') + class Publish extends BaseCommand { static description = 'Publish a package' static name = 'publish' @@ -226,4 +224,5 @@ class Publish extends BaseCommand { return manifest } } + module.exports = Publish diff --git a/lib/commands/query.js b/lib/commands/query.js index a25180d6c38d8..1e9dee4ef1fda 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -1,5 +1,3 @@ -'use strict' - const { resolve } = require('node:path') const BaseCommand = require('../base-command.js') const { log, output } = require('proc-log') diff --git a/lib/commands/rebuild.js b/lib/commands/rebuild.js index d9e22de7cf048..3894f0aa290cc 100644 --- a/lib/commands/rebuild.js +++ b/lib/commands/rebuild.js @@ -2,8 +2,8 @@ const { resolve } = require('path') const { output } = require('proc-log') const npa = require('npm-package-arg') const semver = require('semver') - const ArboristWorkspaceCmd = require('../arborist-cmd.js') + class Rebuild extends ArboristWorkspaceCmd { static description = 'Rebuild a package' static name = 'rebuild' @@ -80,4 +80,5 @@ class Rebuild extends ArboristWorkspaceCmd { }) } } + module.exports = Rebuild diff --git a/lib/commands/repo.js b/lib/commands/repo.js index b89b74c0bf1ba..8e2fef24771d9 100644 --- a/lib/commands/repo.js +++ b/lib/commands/repo.js @@ -1,6 +1,6 @@ const { URL } = require('url') - const PackageUrlCmd = require('../package-url-cmd.js') + class Repo extends PackageUrlCmd { static description = 'Open package repository page in the browser' static name = 'repo' @@ -30,6 +30,7 @@ class Repo extends PackageUrlCmd { return url } } + module.exports = Repo const unknownHostedUrl = url => { diff --git a/lib/commands/root.js b/lib/commands/root.js index f1f9579d103fd..f7c330a004169 100644 --- a/lib/commands/root.js +++ b/lib/commands/root.js @@ -1,5 +1,6 @@ const { output } = require('proc-log') const BaseCommand = require('../base-command.js') + class Root extends BaseCommand { static description = 'Display npm root' static name = 'root' @@ -9,4 +10,5 @@ class Root extends BaseCommand { output.standard(this.npm.dir) } } + module.exports = Root diff --git a/lib/commands/run-script.js b/lib/commands/run-script.js index 34c0b00fe95b5..f048996e66354 100644 --- a/lib/commands/run-script.js +++ b/lib/commands/run-script.js @@ -1,7 +1,7 @@ const { log, output } = require('proc-log') const pkgJson = require('@npmcli/package-json') - const BaseCommand = require('../base-command.js') + class RunScript extends BaseCommand { static description = 'Run arbitrary package scripts' static params = [ diff --git a/lib/commands/sbom.js b/lib/commands/sbom.js index 4b20bf5ea1f76..4ee21ec1954fd 100644 --- a/lib/commands/sbom.js +++ b/lib/commands/sbom.js @@ -1,5 +1,3 @@ -'use strict' - const localeCompare = require('@isaacs/string-locale-compare')('en') const BaseCommand = require('../base-command.js') const { log, output } = require('proc-log') diff --git a/lib/commands/search.js b/lib/commands/search.js index 2b338ed4d39b8..cffc4cdacc846 100644 --- a/lib/commands/search.js +++ b/lib/commands/search.js @@ -1,7 +1,6 @@ const Pipeline = require('minipass-pipeline') const libSearch = require('libnpmsearch') const { log, output } = require('proc-log') - const formatSearchStream = require('../utils/format-search-stream.js') const BaseCommand = require('../base-command.js') @@ -68,4 +67,5 @@ class Search extends BaseCommand { log.silly('search', 'search completed') } } + module.exports = Search diff --git a/lib/commands/set.js b/lib/commands/set.js index f315d183845c5..509da1956bf1f 100644 --- a/lib/commands/set.js +++ b/lib/commands/set.js @@ -22,4 +22,5 @@ class Set extends BaseCommand { return this.npm.exec('config', ['set'].concat(args)) } } + module.exports = Set diff --git a/lib/commands/shrinkwrap.js b/lib/commands/shrinkwrap.js index 56d7ffa88e99e..008244e9760f3 100644 --- a/lib/commands/shrinkwrap.js +++ b/lib/commands/shrinkwrap.js @@ -69,4 +69,5 @@ class Shrinkwrap extends BaseCommand { } } } + module.exports = Shrinkwrap diff --git a/lib/commands/star.js b/lib/commands/star.js index 39165d8c3d8dc..fbf81e64bf21d 100644 --- a/lib/commands/star.js +++ b/lib/commands/star.js @@ -2,8 +2,8 @@ const fetch = require('npm-registry-fetch') const npa = require('npm-package-arg') const { log, output } = require('proc-log') const getIdentity = require('../utils/get-identity') - const BaseCommand = require('../base-command.js') + class Star extends BaseCommand { static description = 'Mark your favorite packages' static name = 'star' @@ -68,4 +68,5 @@ class Star extends BaseCommand { } } } + module.exports = Star diff --git a/lib/commands/stars.js b/lib/commands/stars.js index 1d92d97d7760a..628b22fdb8808 100644 --- a/lib/commands/stars.js +++ b/lib/commands/stars.js @@ -1,8 +1,8 @@ const fetch = require('npm-registry-fetch') const { log, output } = require('proc-log') const getIdentity = require('../utils/get-identity.js') - const BaseCommand = require('../base-command.js') + class Stars extends BaseCommand { static description = 'View packages marked as favorites' static name = 'stars' @@ -35,4 +35,5 @@ class Stars extends BaseCommand { } } } + module.exports = Stars diff --git a/lib/commands/team.js b/lib/commands/team.js index 4ffaa5ff86ab9..f7bada28a611e 100644 --- a/lib/commands/team.js +++ b/lib/commands/team.js @@ -1,7 +1,6 @@ const columns = require('cli-columns') const libteam = require('libnpmteam') const { output } = require('proc-log') - const otplease = require('../utils/otplease.js') const BaseCommand = require('../base-command.js') @@ -152,4 +151,5 @@ class Team extends BaseCommand { } } } + module.exports = Team diff --git a/lib/commands/token.js b/lib/commands/token.js index 70ff0a332b18a..4429a20319f7e 100644 --- a/lib/commands/token.js +++ b/lib/commands/token.js @@ -1,10 +1,9 @@ const { log, output } = require('proc-log') const profile = require('npm-profile') - const otplease = require('../utils/otplease.js') const readUserInfo = require('../utils/read-user-info.js') - const BaseCommand = require('../base-command.js') + class Token extends BaseCommand { static description = 'Manage your authentication tokens' static name = 'token' @@ -194,4 +193,5 @@ class Token extends BaseCommand { return list } } + module.exports = Token diff --git a/lib/commands/uninstall.js b/lib/commands/uninstall.js index 51df355d5a899..7496c02deb28f 100644 --- a/lib/commands/uninstall.js +++ b/lib/commands/uninstall.js @@ -1,10 +1,9 @@ const { resolve } = require('path') const pkgJson = require('@npmcli/package-json') - const reifyFinish = require('../utils/reify-finish.js') const completion = require('../utils/installed-shallow.js') - const ArboristWorkspaceCmd = require('../arborist-cmd.js') + class Uninstall extends ArboristWorkspaceCmd { static description = 'Remove a package' static name = 'uninstall' @@ -53,4 +52,5 @@ class Uninstall extends ArboristWorkspaceCmd { await reifyFinish(this.npm, arb) } } + module.exports = Uninstall diff --git a/lib/commands/unpublish.js b/lib/commands/unpublish.js index 3e7e8c8b624a2..e8b7de8395ea9 100644 --- a/lib/commands/unpublish.js +++ b/lib/commands/unpublish.js @@ -7,12 +7,12 @@ const pkgJson = require('@npmcli/package-json') const { flatten } = require('@npmcli/config/lib/definitions') const getIdentity = require('../utils/get-identity.js') const otplease = require('../utils/otplease.js') +const BaseCommand = require('../base-command.js') const LAST_REMAINING_VERSION_ERROR = 'Refusing to delete the last version of the package. ' + 'It will block from republishing a new version for 24 hours.\n' + 'Run with --force to do this.' -const BaseCommand = require('../base-command.js') class Unpublish extends BaseCommand { static description = 'Remove a package from the registry' static name = 'unpublish' @@ -172,4 +172,5 @@ class Unpublish extends BaseCommand { } } } + module.exports = Unpublish diff --git a/lib/commands/unstar.js b/lib/commands/unstar.js index cbcb73636c638..c72966866669a 100644 --- a/lib/commands/unstar.js +++ b/lib/commands/unstar.js @@ -4,4 +4,5 @@ class Unstar extends Star { static description = 'Remove an item from your favorite packages' static name = 'unstar' } + module.exports = Unstar diff --git a/lib/commands/update.js b/lib/commands/update.js index 63711c05e5b97..ddc3e4a47f38a 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -1,10 +1,8 @@ const path = require('path') - const { log } = require('proc-log') - const reifyFinish = require('../utils/reify-finish.js') - const ArboristWorkspaceCmd = require('../arborist-cmd.js') + class Update extends ArboristWorkspaceCmd { static description = 'Update packages' static name = 'update' @@ -66,4 +64,5 @@ class Update extends ArboristWorkspaceCmd { await reifyFinish(this.npm, arb) } } + module.exports = Update diff --git a/lib/commands/version.js b/lib/commands/version.js index 0e1916d7af7fb..4d7b971fa6fbf 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -1,7 +1,6 @@ const { resolve } = require('node:path') const { readFile } = require('node:fs/promises') const { output } = require('proc-log') - const BaseCommand = require('../base-command.js') class Version extends BaseCommand { diff --git a/lib/commands/view.js b/lib/commands/view.js index e8b102737a1e9..9c8630bef154e 100644 --- a/lib/commands/view.js +++ b/lib/commands/view.js @@ -9,11 +9,11 @@ const relativeDate = require('tiny-relative-date') const semver = require('semver') const { inspect } = require('util') const { packument } = require('pacote') +const Queryable = require('../utils/queryable.js') +const BaseCommand = require('../base-command.js') const readJson = async file => jsonParse(await readFile(file, 'utf8')) -const Queryable = require('../utils/queryable.js') -const BaseCommand = require('../base-command.js') class View extends BaseCommand { static description = 'View registry info' static name = 'view' @@ -406,6 +406,7 @@ class View extends BaseCommand { } } } + module.exports = View function cleanBlanks (obj) { diff --git a/lib/commands/whoami.js b/lib/commands/whoami.js index e05993abdd5bf..03d7bb24991cf 100644 --- a/lib/commands/whoami.js +++ b/lib/commands/whoami.js @@ -1,7 +1,7 @@ const { output } = require('proc-log') const getIdentity = require('../utils/get-identity.js') - const BaseCommand = require('../base-command.js') + class Whoami extends BaseCommand { static description = 'Display npm username' static name = 'whoami' @@ -14,4 +14,5 @@ class Whoami extends BaseCommand { ) } } + module.exports = Whoami diff --git a/lib/lifecycle-cmd.js b/lib/lifecycle-cmd.js index 848771a38355e..26e04b12af272 100644 --- a/lib/lifecycle-cmd.js +++ b/lib/lifecycle-cmd.js @@ -16,4 +16,5 @@ class LifecycleCmd extends BaseCommand { return this.npm.exec('run-script', [this.constructor.name, ...args]) } } + module.exports = LifecycleCmd diff --git a/lib/npm.js b/lib/npm.js index 509c2fa2abd36..e76317e905ea3 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -445,4 +445,5 @@ class Npm { this.#display.flushOutput(jsonError) } } + module.exports = Npm diff --git a/lib/package-url-cmd.js b/lib/package-url-cmd.js index c81b933f69254..5ab7cb87a2cd3 100644 --- a/lib/package-url-cmd.js +++ b/lib/package-url-cmd.js @@ -62,4 +62,5 @@ class PackageUrlCommand extends BaseCommand { return (rurl && hostedGitInfo.fromUrl(rurl.replace(/^git\+/, ''))) || null } } + module.exports = PackageUrlCommand diff --git a/lib/utils/did-you-mean.js b/lib/utils/did-you-mean.js index ff3c812b46c3c..499c27c74e1de 100644 --- a/lib/utils/did-you-mean.js +++ b/lib/utils/did-you-mean.js @@ -37,4 +37,5 @@ const didYouMean = async (path, scmd) => { : `\n\nDid you mean one of these?\n${best.slice(0, 3).join('\n')}` return suggestion } + module.exports = didYouMean diff --git a/lib/utils/verify-signatures.js b/lib/utils/verify-signatures.js new file mode 100644 index 0000000000000..f2973316c9b76 --- /dev/null +++ b/lib/utils/verify-signatures.js @@ -0,0 +1,389 @@ +const fetch = require('npm-registry-fetch') +const localeCompare = require('@isaacs/string-locale-compare')('en') +const npa = require('npm-package-arg') +const pacote = require('pacote') +const pMap = require('p-map') +const tufClient = require('@sigstore/tuf') +const { log, output } = require('proc-log') + +const sortAlphabetically = (a, b) => localeCompare(a.name, b.name) + +class VerifySignatures { + constructor (tree, filterSet, npm, opts) { + this.tree = tree + this.filterSet = filterSet + this.npm = npm + this.opts = opts + this.keys = new Map() + this.invalid = [] + this.missing = [] + this.checkedPackages = new Set() + this.auditedWithKeysCount = 0 + this.verifiedSignatureCount = 0 + this.verifiedAttestationCount = 0 + this.exitCode = 0 + } + + async run () { + const start = process.hrtime.bigint() + + // Find all deps in tree + const { edges, registries } = this.getEdgesOut(this.tree.inventory.values(), this.filterSet) + if (edges.size === 0) { + throw new Error('found no installed dependencies to audit') + } + + const tuf = await tufClient.initTUF({ + cachePath: this.opts.tufCache, + retry: this.opts.retry, + timeout: this.opts.timeout, + }) + await Promise.all([...registries].map(registry => this.setKeys({ registry, tuf }))) + + log.verbose('verifying registry signatures') + await pMap(edges, (e) => this.getVerifiedInfo(e), { concurrency: 20, stopOnError: true }) + + // Didn't find any dependencies that could be verified, e.g. only local + // deps, missing version, not on a registry etc. + if (!this.auditedWithKeysCount) { + throw new Error('found no dependencies to audit that were installed from ' + + 'a supported registry') + } + + const invalid = this.invalid.sort(sortAlphabetically) + const missing = this.missing.sort(sortAlphabetically) + + const hasNoInvalidOrMissing = invalid.length === 0 && missing.length === 0 + + if (!hasNoInvalidOrMissing) { + process.exitCode = 1 + } + + if (this.npm.config.get('json')) { + output.standard(JSON.stringify({ + invalid, + missing, + }, null, 2)) + return + } + const end = process.hrtime.bigint() + const elapsed = end - start + + const auditedPlural = this.auditedWithKeysCount > 1 ? 's' : '' + const timing = `audited ${this.auditedWithKeysCount} package${auditedPlural} in ` + + `${Math.floor(Number(elapsed) / 1e9)}s` + output.standard(timing) + output.standard('') + + const verifiedBold = this.npm.chalk.bold('verified') + if (this.verifiedSignatureCount) { + if (this.verifiedSignatureCount === 1) { + /* eslint-disable-next-line max-len */ + output.standard(`${this.verifiedSignatureCount} package has a ${verifiedBold} registry signature`) + } else { + /* eslint-disable-next-line max-len */ + output.standard(`${this.verifiedSignatureCount} packages have ${verifiedBold} registry signatures`) + } + output.standard('') + } + + if (this.verifiedAttestationCount) { + if (this.verifiedAttestationCount === 1) { + /* eslint-disable-next-line max-len */ + output.standard(`${this.verifiedAttestationCount} package has a ${verifiedBold} attestation`) + } else { + /* eslint-disable-next-line max-len */ + output.standard(`${this.verifiedAttestationCount} packages have ${verifiedBold} attestations`) + } + output.standard('') + } + + if (missing.length) { + const missingClr = this.npm.chalk.redBright('missing') + if (missing.length === 1) { + /* eslint-disable-next-line max-len */ + output.standard(`1 package has a ${missingClr} registry signature but the registry is providing signing keys:`) + } else { + /* eslint-disable-next-line max-len */ + output.standard(`${missing.length} packages have ${missingClr} registry signatures but the registry is providing signing keys:`) + } + output.standard('') + missing.map(m => + output.standard(`${this.npm.chalk.red(`${m.name}@${m.version}`)} (${m.registry})`) + ) + } + + if (invalid.length) { + if (missing.length) { + output.standard('') + } + const invalidClr = this.npm.chalk.redBright('invalid') + // We can have either invalid signatures or invalid provenance + const invalidSignatures = this.invalid.filter(i => i.code === 'EINTEGRITYSIGNATURE') + if (invalidSignatures.length) { + if (invalidSignatures.length === 1) { + output.standard(`1 package has an ${invalidClr} registry signature:`) + } else { + /* eslint-disable-next-line max-len */ + output.standard(`${invalidSignatures.length} packages have ${invalidClr} registry signatures:`) + } + output.standard('') + invalidSignatures.map(i => + output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`) + ) + output.standard('') + } + + const invalidAttestations = this.invalid.filter(i => i.code === 'EATTESTATIONVERIFY') + if (invalidAttestations.length) { + if (invalidAttestations.length === 1) { + output.standard(`1 package has an ${invalidClr} attestation:`) + } else { + /* eslint-disable-next-line max-len */ + output.standard(`${invalidAttestations.length} packages have ${invalidClr} attestations:`) + } + output.standard('') + invalidAttestations.map(i => + output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`) + ) + output.standard('') + } + + if (invalid.length === 1) { + /* eslint-disable-next-line max-len */ + output.standard(`Someone might have tampered with this package since it was published on the registry!`) + } else { + /* eslint-disable-next-line max-len */ + output.standard(`Someone might have tampered with these packages since they were published on the registry!`) + } + output.standard('') + } + } + + getEdgesOut (nodes, filterSet) { + const edges = new Set() + const registries = new Set() + for (const node of nodes) { + for (const edge of node.edgesOut.values()) { + const filteredOut = + edge.from + && filterSet + && filterSet.size > 0 + && !filterSet.has(edge.from.target) + + if (!filteredOut) { + const spec = this.getEdgeSpec(edge) + if (spec) { + // Prefetch and cache public keys from used registries + registries.add(this.getSpecRegistry(spec)) + } + edges.add(edge) + } + } + } + return { edges, registries } + } + + async setKeys ({ registry, tuf }) { + const { host, pathname } = new URL(registry) + // Strip any trailing slashes from pathname + const regKey = `${host}${pathname.replace(/\/$/, '')}/keys.json` + let keys = await tuf.getTarget(regKey) + .then((target) => JSON.parse(target)) + .then(({ keys: ks }) => ks.map((key) => ({ + ...key, + keyid: key.keyId, + pemkey: `-----BEGIN PUBLIC KEY-----\n${key.publicKey.rawBytes}\n-----END PUBLIC KEY-----`, + expires: key.publicKey.validFor.end || null, + }))).catch(err => { + if (err.code === 'TUF_FIND_TARGET_ERROR') { + return null + } else { + throw err + } + }) + + // If keys not found in Sigstore TUF repo, fallback to registry keys API + if (!keys) { + keys = await fetch.json('/-/npm/v1/keys', { + ...this.npm.flatOptions, + registry, + }).then(({ keys: ks }) => ks.map((key) => ({ + ...key, + pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`, + }))).catch(err => { + if (err.code === 'E404' || err.code === 'E400') { + return null + } else { + throw err + } + }) + } + + if (keys) { + this.keys.set(registry, keys) + } + } + + getEdgeType (edge) { + return edge.optional ? 'optionalDependencies' + : edge.peer ? 'peerDependencies' + : edge.dev ? 'devDependencies' + : 'dependencies' + } + + getEdgeSpec (edge) { + let name = edge.name + try { + name = npa(edge.spec).subSpec.name + } catch { + // leave it as edge.name + } + try { + return npa(`${name}@${edge.spec}`) + } catch { + // Skip packages with invalid spec + } + } + + buildRegistryConfig (registry) { + const keys = this.keys.get(registry) || [] + const parsedRegistry = new URL(registry) + const regKey = `//${parsedRegistry.host}${parsedRegistry.pathname}` + return { + [`${regKey}:_keys`]: keys, + } + } + + getSpecRegistry (spec) { + return fetch.pickRegistry(spec, this.npm.flatOptions) + } + + getValidPackageInfo (edge) { + const type = this.getEdgeType(edge) + // Skip potentially optional packages that are not on disk, as these could + // be omitted during install + if (edge.error === 'MISSING' && type !== 'dependencies') { + return + } + + const spec = this.getEdgeSpec(edge) + // Skip invalid version requirements + if (!spec) { + return + } + const node = edge.to || edge + const { version } = node.package || {} + + if (node.isWorkspace || // Skip local workspaces packages + !version || // Skip packages that don't have a installed version, e.g. optonal dependencies + !spec.registry) { // Skip if not from registry, e.g. git package + return + } + + for (const omitType of this.npm.config.get('omit')) { + if (node[omitType]) { + return + } + } + + return { + name: spec.name, + version, + type, + location: node.location, + registry: this.getSpecRegistry(spec), + } + } + + async verifySignatures (name, version, registry) { + const { + _integrity: integrity, + _signatures, + _attestations, + _resolved: resolved, + } = await pacote.manifest(`${name}@${version}`, { + verifySignatures: true, + verifyAttestations: true, + ...this.buildRegistryConfig(registry), + ...this.npm.flatOptions, + }) + const signatures = _signatures || [] + const result = { + integrity, + signatures, + attestations: _attestations, + resolved, + } + return result + } + + async getVerifiedInfo (edge) { + const info = this.getValidPackageInfo(edge) + if (!info) { + return + } + const { name, version, location, registry, type } = info + if (this.checkedPackages.has(location)) { + // we already did or are doing this one + return + } + this.checkedPackages.add(location) + + // We only "audit" or verify the signature, or the presence of it, on + // packages whose registry returns signing keys + const keys = this.keys.get(registry) || [] + if (keys.length) { + this.auditedWithKeysCount += 1 + } + + try { + const { integrity, signatures, attestations, resolved } = await this.verifySignatures( + name, version, registry + ) + + // Currently we only care about missing signatures on registries that provide a public key + // We could make this configurable in the future with a strict/paranoid mode + if (signatures.length) { + this.verifiedSignatureCount += 1 + } else if (keys.length) { + this.missing.push({ + integrity, + location, + name, + registry, + resolved, + version, + }) + } + + // Track verified attestations separately to registry signatures, as all + // packages on registries with signing keys are expected to have registry + // signatures, but not all packages have provenance and publish attestations. + if (attestations) { + this.verifiedAttestationCount += 1 + } + } catch (e) { + if (e.code === 'EINTEGRITYSIGNATURE' || e.code === 'EATTESTATIONVERIFY') { + this.invalid.push({ + code: e.code, + message: e.message, + integrity: e.integrity, + keyid: e.keyid, + location, + name, + registry, + resolved: e.resolved, + signature: e.signature, + predicateType: e.predicateType, + type, + version, + }) + } else { + throw e + } + } + } +} + +module.exports = VerifySignatures