npq 3.20.0 → 3.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,6 +78,16 @@ brew install npq
78
78
  npq install express
79
79
  ```
80
80
 
81
+ ### Custom registries
82
+
83
+ NPQ reads standard npm registry configuration, including default and scoped registries, authentication, proxies, and TLS settings. You can also select a registry on the command line; the option is shared with the package manager used for installation:
84
+
85
+ ```sh
86
+ npq install @company/tool --registry=https://artifactory.example.com/artifactory/api/npm/npm-virtual/
87
+ ```
88
+
89
+ See [Custom registries](docs/feature/custom-registry.md) for `.npmrc` examples, configuration precedence, private CA setup, skipped capability behavior, and NPQ's no-fallback privacy guarantee.
90
+
81
91
  ### Embed in your day to day
82
92
 
83
93
  Since `npq` is a pre-step to ensure that the npm package you're installing is safe, you can safely embed it in your day-to-day `npm` usage so there's no need to remember to run `npq` explicitly.
@@ -130,15 +140,15 @@ Note: `npq` by default will offload all commands and their arguments to the `npm
130
140
  | --- | --- | ---
131
141
  | age | Will show a warning for a package if its age on npm is less than 22 days | Checks a package creation date, not a specific version
132
142
  | author | Validates the resolved version’s publisher (`_npmUser`), flags a **new** maintainer on the package (first publish by that email within 21 days), **dormant maintainer** gaps (warning if over ~6 months since their last publish on that package, error if over ~9 months), and very **recent** publishes | See [docs/feature/author-marshall.md](docs/feature/author-marshall.md)
133
- | downloads | Will show a warning for a package if its download count in the last month is less than 20
143
+ | downloads | Will show a warning for a package if its download count in the last month is less than 20 | Reports `not evaluated` for custom-registry packages instead of querying the public npm downloads service
134
144
  | readme | Will show a warning if a package has no README or it has been detected as a security placeholder package by npm staff
135
145
  | repo | Will show a warning if a package has been found without a valid and working repository URL | Checks the latest version for a repository URL
136
146
  | scripts | Will show a warning if a package has a pre/post install script which could potentially be malicious
137
147
  | snyk | Will show a warning if a package has been found with vulnerabilities in Snyk's database | For Snyk to work you need to either have the `snyk` npm package installed with a valid API token, or make the token available in the `SNYK_TOKEN` environment variable, and npq will use it
138
148
  | license | Will show a warning if a package has been found without a license field | Checks the latest version for a license
139
149
  | expired domains | Will show a warning if a package has been found with one of its maintainers having an email address that includes an expired domain | Checks a dependency version for a maintainer with an expired domain
140
- | signatures | Will compare the package's signature as it shows on the registry's pakument with the keys published on the npmjs.com registry
141
- | provenance | Will verify the package's attestations of provenance metadata for the published package, and **error** on [provenance regression](docs/feature/provenance.md) (an older semver had registry provenance metadata but the version you install does not)
150
+ | signatures | Will compare the package's signature as it shows on the registry's packument with signing keys published by the selected registry | Reports `not evaluated` if the selected registry does not expose signing keys
151
+ | provenance | Will verify the package's attestations of provenance metadata for the published package, and **error** on [provenance regression](docs/feature/provenance.md) (an older semver had registry provenance metadata but the version you install does not) | Reports `not evaluated` if the selected registry does not expose signing keys or attestations
142
152
  | version-maturity | Will show a warning if the specific version being installed was published less than 7 days ago | Helps identify recently published versions that may not have been reviewed by the community yet
143
153
  | newBin | Will show a warning if the package version being installed introduces a new command-line binary (via the `bin` field in `package.json`) that was not present in its previous version. | Helps identify potentially unexpected new executables being added to your `node_modules/.bin/` directory.
144
154
  | typosquatting | Will show a warning if the package name is similar to a popular package name, which could indicate a potential typosquatting attack. | Helps identify packages that may be trying to trick users into installing them by mimicking popular package names.
package/bin/npq-hero.js CHANGED
@@ -12,6 +12,8 @@ const cliPrompt = require('../lib/helpers/cliPrompt.js')
12
12
  const { reportResults } = require('../lib/helpers/reportResults')
13
13
  const { Spinner } = require('../lib/helpers/cliSpinner')
14
14
  const { promiseThrottleHelper } = require('../lib/helpers/promiseThrottler')
15
+ const RegistryConfig = require('../lib/helpers/registryConfig')
16
+ const RegistryClient = require('../lib/helpers/registryClient')
15
17
 
16
18
  const PACKAGE_MANAGER_TOOL = process.env.NPQ_PKG_MGR
17
19
  const DISABLE_AUTO_CONTINUE = process.env.NPQ_DISABLE_AUTO_CONTINUE === 'true'
@@ -27,14 +29,17 @@ if (spinner) {
27
29
  spinner.start()
28
30
  }
29
31
 
30
- const marshall = new Marshall({
31
- pkgs: cliArgs.packages,
32
- progressManager: spinner,
33
- promiseThrottleHelper
34
- })
35
-
36
- marshall
37
- .process()
32
+ RegistryConfig.load({ argv: cliArgs.registryConfigArgs || [] })
33
+ .then((registryConfig) => {
34
+ const registryClient = new RegistryClient(registryConfig)
35
+ const marshall = new Marshall({
36
+ pkgs: cliArgs.packages,
37
+ registryClient,
38
+ progressManager: spinner,
39
+ promiseThrottleHelper
40
+ })
41
+ return marshall.process()
42
+ })
38
43
  .then((marshallResults) => {
39
44
  if (spinner) {
40
45
  spinner.stop()
@@ -46,12 +51,20 @@ marshall
46
51
  Object.hasOwn(results, 'countErrors') &&
47
52
  Object.hasOwn(results, 'countWarnings')
48
53
  ) {
49
- const { countErrors, countWarnings, useRichFormatting } = results
50
- const isErrors = countErrors > 0 || countWarnings > 0
54
+ const {
55
+ countErrors,
56
+ countWarnings,
57
+ countNotEvaluated = 0,
58
+ useRichFormatting
59
+ } = results
60
+ const hasFindings = countErrors > 0 || countWarnings > 0
61
+ const hasReportableResults = hasFindings || countNotEvaluated > 0
51
62
 
52
- if (isErrors) {
63
+ if (hasReportableResults) {
53
64
  console.log()
54
- console.log('Packages with issues found:')
65
+ console.log(
66
+ hasFindings ? 'Packages with issues found:' : 'Package checks not evaluated:'
67
+ )
55
68
 
56
69
  if (useRichFormatting) {
57
70
  console.log(results.resultsForPrettyPrint)
@@ -63,9 +76,10 @@ marshall
63
76
  }
64
77
 
65
78
  return {
66
- anyIssues: isErrors,
79
+ anyIssues: hasFindings,
67
80
  countErrors,
68
- countWarnings
81
+ countWarnings,
82
+ countNotEvaluated
69
83
  }
70
84
  }
71
85
  return undefined
package/bin/npq.js CHANGED
@@ -15,6 +15,8 @@ const cliPrompt = require('../lib/helpers/cliPrompt.js')
15
15
  const { reportResults } = require('../lib/helpers/reportResults')
16
16
  const { Spinner } = require('../lib/helpers/cliSpinner')
17
17
  const { promiseThrottleHelper } = require('../lib/helpers/promiseThrottler')
18
+ const RegistryConfig = require('../lib/helpers/registryConfig')
19
+ const RegistryClient = require('../lib/helpers/registryClient')
18
20
 
19
21
  const debug = util.debuglog('npq')
20
22
 
@@ -27,7 +29,12 @@ if (spinner) {
27
29
  spinner.start()
28
30
  }
29
31
 
30
- Promise.resolve()
32
+ let registryClient
33
+
34
+ RegistryConfig.load({ argv: cliArgs.registryConfigArgs || [] })
35
+ .then((registryConfig) => {
36
+ registryClient = new RegistryClient(registryConfig)
37
+ })
31
38
  .then(() => {
32
39
  if (cliArgs.packages.length === 0) {
33
40
  debug('\nNo packages specified, using project packages from package.json')
@@ -48,6 +55,7 @@ Promise.resolve()
48
55
 
49
56
  const marshall = new Marshall({
50
57
  pkgs: packages,
58
+ registryClient,
51
59
  progressManager: spinner,
52
60
  promiseThrottleHelper
53
61
  })
@@ -65,12 +73,20 @@ Promise.resolve()
65
73
  Object.hasOwn(results, 'countErrors') &&
66
74
  Object.hasOwn(results, 'countWarnings')
67
75
  ) {
68
- const { countErrors, countWarnings, useRichFormatting } = results
69
- const isErrors = countErrors > 0 || countWarnings > 0
70
-
71
- if (isErrors) {
76
+ const {
77
+ countErrors,
78
+ countWarnings,
79
+ countNotEvaluated = 0,
80
+ useRichFormatting
81
+ } = results
82
+ const hasFindings = countErrors > 0 || countWarnings > 0
83
+ const hasReportableResults = hasFindings || countNotEvaluated > 0
84
+
85
+ if (hasReportableResults) {
72
86
  console.log()
73
- console.log('Packages with issues found:')
87
+ console.log(
88
+ hasFindings ? 'Packages with issues found:' : 'Package checks not evaluated:'
89
+ )
74
90
 
75
91
  if (useRichFormatting) {
76
92
  console.log(results.resultsForPrettyPrint)
@@ -82,9 +98,10 @@ Promise.resolve()
82
98
  }
83
99
 
84
100
  return {
85
- anyIssues: isErrors,
101
+ anyIssues: hasFindings,
86
102
  countErrors,
87
- countWarnings
103
+ countWarnings,
104
+ countNotEvaluated
88
105
  }
89
106
  }
90
107
  return undefined
package/lib/cli.js CHANGED
@@ -20,6 +20,12 @@ const INSTALL_SUBCOMMANDS = new Set([
20
20
  ])
21
21
 
22
22
  class CliParser {
23
+ static registryConfigArgs(values = {}) {
24
+ return ['registry', 'userconfig', 'globalconfig'].flatMap((key) =>
25
+ values[key] ? [`--${key}=${values[key]}`] : []
26
+ )
27
+ }
28
+
23
29
  static isInstallSubcommand(token) {
24
30
  return INSTALL_SUBCOMMANDS.has(token)
25
31
  }
@@ -67,6 +73,9 @@ class CliParser {
67
73
  packageManager: { type: 'string' },
68
74
  pkgMgr: { type: 'string' },
69
75
  'disable-auto-continue': { type: 'boolean' },
76
+ registry: { type: 'string' },
77
+ userconfig: { type: 'string' },
78
+ globalconfig: { type: 'string' },
70
79
  help: { type: 'boolean', short: 'h' },
71
80
  version: { type: 'boolean', short: 'v' }
72
81
  }
@@ -95,6 +104,9 @@ Options:
95
104
  --packageManager Package Manager to use (default: npm)
96
105
  --pkgMgr Alias for packageManager
97
106
  --disable-auto-continue Disable auto-continue countdown, always prompt
107
+ --registry npm registry used for audit and install
108
+ --userconfig Path to the user npm configuration file
109
+ --globalconfig Path to the global npm configuration file
98
110
  -h, --help Show help
99
111
  -v, --version Show version
100
112
 
@@ -127,6 +139,7 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
127
139
  plain: values.plain || false,
128
140
  disableAutoContinue:
129
141
  values['disable-auto-continue'] || process.env.NPQ_DISABLE_AUTO_CONTINUE === 'true',
142
+ registryConfigArgs: this.registryConfigArgs(values),
130
143
  installSubcommandExplicit
131
144
  }
132
145
  }
@@ -140,17 +153,21 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
140
153
  type: 'string',
141
154
  short: 'i',
142
155
  default: 'install'
143
- }
156
+ },
157
+ registry: { type: 'string' },
158
+ userconfig: { type: 'string' },
159
+ globalconfig: { type: 'string' }
144
160
  }
145
161
  }
146
162
 
147
- const { positionals } = parseArgs(config)
163
+ const { values, positionals } = parseArgs(config)
148
164
 
149
165
  const earlyExitNoInstall = true
150
166
  const normalizedPackages = this._extractPackagesFromPositionals(positionals, earlyExitNoInstall)
151
167
 
152
168
  return {
153
- packages: normalizedPackages
169
+ packages: normalizedPackages,
170
+ registryConfigArgs: this.registryConfigArgs(values)
154
171
  }
155
172
  }
156
173
  }
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ class NotEvaluated extends Error {
4
+ constructor(message, { capability = null } = {}) {
5
+ super(message)
6
+ this.name = 'NotEvaluated'
7
+ this.capability = capability
8
+ }
9
+ }
10
+
11
+ module.exports = NotEvaluated
@@ -13,57 +13,6 @@ const MISSING_TIME_CUTOFF = '2015-01-01T00:00:00.000Z'
13
13
  * This replaces pacote functionality for npq specific needs
14
14
  */
15
15
  class NpmRegistry {
16
- constructor(opts = {}) {
17
- this.opts = opts
18
- this.registry = opts.registry || 'https://registry.npmjs.org'
19
- }
20
-
21
- /**
22
- * Fetch package manifest from npm registry
23
- * @param {string} packageSpec - Package name and version (e.g., 'express@4.18.2')
24
- * @param {Object} options - Additional options
25
- * @returns {Promise<Object>} Package manifest
26
- */
27
- async getManifest(packageSpec, options = {}) {
28
- const spec = npa(packageSpec)
29
- const escapedName = spec.escapedName
30
- const packumentUrl = `${this.registry}/${escapedName}`
31
-
32
- // Fetch packument (package metadata)
33
- const response = await fetch(packumentUrl, {
34
- headers: {
35
- accept: 'application/json',
36
- 'user-agent': 'npq-npm-registry-client',
37
- ...(options.headers || {})
38
- }
39
- })
40
-
41
- if (!response.ok) {
42
- throw new Error(`Failed to fetch package manifest: ${response.status} ${response.statusText}`)
43
- }
44
-
45
- const packument = await response.json()
46
-
47
- // Pick the specific version
48
- let version = spec.fetchSpec
49
- if (version === 'latest' || !version || version === '*') {
50
- version = packument['dist-tags']?.latest
51
- }
52
-
53
- if (!packument.versions || !packument.versions[version]) {
54
- throw new Error(`Version ${version} not found for package ${spec.name}`)
55
- }
56
-
57
- const manifest = packument.versions[version]
58
-
59
- // Add timing information if available
60
- if (packument.time && packument.time[version]) {
61
- manifest._time = packument.time[version]
62
- }
63
-
64
- return manifest
65
- }
66
-
67
16
  /**
68
17
  * Verify package signatures using npm registry public keys
69
18
  * @param {Object} manifest - Package manifest
@@ -140,27 +89,15 @@ class NpmRegistry {
140
89
  * @param {Array} registryKeys - Public keys from npm registry
141
90
  * @returns {Promise<Object>} Verified manifest with _attestations
142
91
  */
143
- async verifyAttestations(manifest, registryKeys) {
92
+ async verifyAttestations(manifest, registryKeys, attestations) {
144
93
  if (!manifest.dist || !manifest.dist.attestations) {
145
94
  throw new Error('Package has no attestations to verify')
146
95
  }
147
96
 
148
- const attestationsPath = new URL(manifest.dist.attestations.url).pathname
149
- const attestationsUrl = this.registry + attestationsPath
150
-
151
- const response = await fetch(attestationsUrl, {
152
- headers: {
153
- accept: 'application/json',
154
- 'user-agent': 'npq-npm-registry-client'
155
- }
156
- })
157
-
158
- if (!response.ok) {
159
- throw new Error(`Failed to fetch attestations: ${response.status} ${response.statusText}`)
97
+ if (!Array.isArray(attestations)) {
98
+ throw new Error('Package attestations response is invalid')
160
99
  }
161
100
 
162
- const { attestations } = await response.json()
163
-
164
101
  const bundles = attestations.map(({ predicateType, bundle }) => {
165
102
  const statement = JSON.parse(
166
103
  Buffer.from(bundle.dsseEnvelope.payload, 'base64').toString('utf8')
@@ -1,31 +1,23 @@
1
1
  'use strict'
2
2
 
3
3
  const semver = require('semver')
4
- const NPM_REGISTRY = 'http://registry.npmjs.org'
5
- const NPM_REGISTRY_API = 'https://api.npmjs.org'
4
+ const RegistryClient = require('./registryClient')
6
5
 
7
6
  class PackageRepoUtils {
8
- constructor(options = {}) {
9
- this.registryUrl = options.registryUrl ? options.registryUrl : NPM_REGISTRY
10
- this.registryApiUrl = options.registryApiUrl ? options.registryApiUrl : NPM_REGISTRY_API
7
+ constructor({ registryClient = RegistryClient.public() } = {}) {
8
+ this.registryClient = registryClient
11
9
  this.pkgInfoCache = {}
12
10
  }
13
11
 
14
- formatPackageForUrl(pkg) {
15
- return pkg.replace(/\//g, '%2F')
16
- }
17
-
18
12
  getPackageInfo(pkg) {
19
- if (this.pkgInfoCache[pkg]) {
20
- return Promise.resolve(this.pkgInfoCache[pkg])
21
- } else {
22
- return fetch(`${this.registryUrl}/${this.formatPackageForUrl(pkg)}`)
23
- .then((response) => response.json())
24
- .then((data) => {
25
- this.pkgInfoCache[pkg] = data
26
- return data
27
- })
13
+ const cacheKey = `${this.registryClient.registryFor(pkg)}|${pkg}`
14
+ if (this.pkgInfoCache[cacheKey]) {
15
+ return Promise.resolve(this.pkgInfoCache[cacheKey])
28
16
  }
17
+ return this.registryClient.getPackageInfo(pkg).then((data) => {
18
+ this.pkgInfoCache[cacheKey] = data
19
+ return data
20
+ })
29
21
  }
30
22
 
31
23
  getLatestVersion(pkg) {
@@ -35,9 +27,7 @@ class PackageRepoUtils {
35
27
  }
36
28
 
37
29
  getDownloadInfo(pkg) {
38
- return fetch(`${this.registryApiUrl}/downloads/point/last-month/${pkg}`)
39
- .then((response) => response.json())
40
- .then(({ downloads }) => downloads)
30
+ return this.registryClient.getDownloadInfo(pkg)
41
31
  }
42
32
 
43
33
  getReadmeInfo(pkg) {
@@ -0,0 +1,206 @@
1
+ 'use strict'
2
+
3
+ const npa = require('npm-package-arg')
4
+ const npmFetch = require('npm-registry-fetch')
5
+ const NotEvaluated = require('./notEvaluated')
6
+ const RegistryConfig = require('./registryConfig')
7
+ const { RegistryError } = require('./registryErrors')
8
+
9
+ const PUBLIC_REGISTRY = 'https://registry.npmjs.org/'
10
+ const OPTIONAL_STATUS = new Set([404, 405, 501])
11
+ const CAPABILITY = Object.freeze({
12
+ SIGNING_KEYS: 'signing-keys',
13
+ ATTESTATIONS: 'attestations',
14
+ DOWNLOAD_COUNTS: 'download-counts'
15
+ })
16
+ const CAPABILITY_MESSAGE = Object.freeze({
17
+ [CAPABILITY.SIGNING_KEYS]: 'configured registry does not expose signing keys',
18
+ [CAPABILITY.ATTESTATIONS]: 'configured registry does not expose attestations',
19
+ [CAPABILITY.DOWNLOAD_COUNTS]:
20
+ 'download counts are available only for the public npm registry'
21
+ })
22
+
23
+ class RegistryClient {
24
+ constructor(registryConfig, { fetcher = npmFetch } = {}) {
25
+ this.registryConfig = registryConfig
26
+ this.fetcher = fetcher
27
+ this.keyCache = new Map()
28
+ this.capabilityCache = new Map()
29
+ }
30
+
31
+ static public() {
32
+ return new RegistryClient(RegistryConfig.defaults())
33
+ }
34
+
35
+ registryFor(packageSpec) {
36
+ return this.registryConfig.registryFor(packageSpec)
37
+ }
38
+
39
+ requestOptions(packageSpec) {
40
+ return {
41
+ ...this.registryConfig.requestOptions,
42
+ spec: packageSpec,
43
+ headers: {
44
+ accept: 'application/json',
45
+ 'user-agent': 'npq-npm-registry-client'
46
+ }
47
+ }
48
+ }
49
+
50
+ async requestJson(
51
+ requestPath,
52
+ packageSpec,
53
+ { capability = null, notFoundAsData = false } = {}
54
+ ) {
55
+ const registry = this.registryFor(packageSpec)
56
+ try {
57
+ return await this.fetcher.json(requestPath, this.requestOptions(packageSpec))
58
+ } catch (error) {
59
+ const statusCode = error.statusCode || error.status || null
60
+ if (notFoundAsData && statusCode === 404) {
61
+ return { error: 'Not found' }
62
+ }
63
+ if (capability && OPTIONAL_STATUS.has(statusCode)) {
64
+ this.capabilityCache.set(`${registry}|${capability}`, false)
65
+ throw new NotEvaluated(CAPABILITY_MESSAGE[capability], { capability })
66
+ }
67
+ const code =
68
+ statusCode === 401 || statusCode === 403
69
+ ? 'EREGISTRYAUTH'
70
+ : statusCode
71
+ ? 'EREGISTRYHTTP'
72
+ : 'EREGISTRYNETWORK'
73
+ throw new RegistryError(
74
+ statusCode === 401 || statusCode === 403
75
+ ? 'Registry authentication or authorization failed'
76
+ : statusCode
77
+ ? `Registry request failed with HTTP ${statusCode}`
78
+ : 'Registry network request failed',
79
+ { registry, code, statusCode, cause: error }
80
+ )
81
+ }
82
+ }
83
+
84
+ async getPackageInfo(packageName) {
85
+ const spec = npa(packageName)
86
+ const data = await this.requestJson(spec.escapedName, packageName, {
87
+ notFoundAsData: true
88
+ })
89
+ if (data && data.error === 'Not found') {
90
+ return data
91
+ }
92
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
93
+ throw new RegistryError('Registry package metadata is malformed', {
94
+ registry: this.registryFor(packageName),
95
+ code: 'EREGISTRYPROTOCOL'
96
+ })
97
+ }
98
+ return data
99
+ }
100
+
101
+ async getManifest(packageSpec, packument = null) {
102
+ const spec = npa(packageSpec)
103
+ const data = packument || (await this.getPackageInfo(spec.name))
104
+ let version = spec.fetchSpec
105
+ if (!version || version === '*' || version === 'latest') {
106
+ version = data['dist-tags'] && data['dist-tags'].latest
107
+ }
108
+ if (!data.versions || !data.versions[version]) {
109
+ throw new Error(`Version ${version} not found for package ${spec.name}`)
110
+ }
111
+ return {
112
+ ...data.versions[version],
113
+ ...(data.time && data.time[version] ? { _time: data.time[version] } : {})
114
+ }
115
+ }
116
+
117
+ unavailable(capability) {
118
+ throw new NotEvaluated(CAPABILITY_MESSAGE[capability], { capability })
119
+ }
120
+
121
+ async getRegistryKeys(packageSpec) {
122
+ const registry = this.registryFor(packageSpec)
123
+ const cacheKey = `${registry}|${CAPABILITY.SIGNING_KEYS}`
124
+ if (this.keyCache.has(cacheKey)) {
125
+ return this.keyCache.get(cacheKey)
126
+ }
127
+ if (this.capabilityCache.get(cacheKey) === false) {
128
+ return this.unavailable(CAPABILITY.SIGNING_KEYS)
129
+ }
130
+ const response = await this.requestJson('-/npm/v1/keys', packageSpec, {
131
+ capability: CAPABILITY.SIGNING_KEYS
132
+ })
133
+ if (!response || !Array.isArray(response.keys)) {
134
+ throw new RegistryError('Registry signing-key response is malformed', {
135
+ registry,
136
+ code: 'EREGISTRYPROTOCOL'
137
+ })
138
+ }
139
+ if (response.keys.length === 0) {
140
+ this.capabilityCache.set(cacheKey, false)
141
+ return this.unavailable(CAPABILITY.SIGNING_KEYS)
142
+ }
143
+ const keys = response.keys.map((key) => ({
144
+ ...key,
145
+ pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`
146
+ }))
147
+ this.keyCache.set(cacheKey, keys)
148
+ return keys
149
+ }
150
+
151
+ async getAttestations(packageSpec, manifest) {
152
+ const registry = this.registryFor(packageSpec)
153
+ const cacheKey = `${registry}|${CAPABILITY.ATTESTATIONS}`
154
+ if (this.capabilityCache.get(cacheKey) === false) {
155
+ return this.unavailable(CAPABILITY.ATTESTATIONS)
156
+ }
157
+ let requestPath
158
+ try {
159
+ const advertisedPath = new URL(manifest.dist.attestations.url).pathname
160
+ const registryPath = new URL(registry).pathname
161
+ requestPath = advertisedPath.startsWith(registryPath)
162
+ ? advertisedPath.slice(registryPath.length)
163
+ : advertisedPath.replace(/^\/+/, '')
164
+ } catch (error) {
165
+ throw new RegistryError('Package attestation URL is malformed', {
166
+ registry,
167
+ code: 'EREGISTRYPROTOCOL',
168
+ cause: error
169
+ })
170
+ }
171
+ const response = await this.requestJson(requestPath, packageSpec, {
172
+ capability: CAPABILITY.ATTESTATIONS
173
+ })
174
+ if (!response || !Array.isArray(response.attestations)) {
175
+ throw new RegistryError('Registry attestation response is malformed', {
176
+ registry,
177
+ code: 'EREGISTRYPROTOCOL'
178
+ })
179
+ }
180
+ if (response.attestations.length === 0) {
181
+ this.capabilityCache.set(cacheKey, false)
182
+ return this.unavailable(CAPABILITY.ATTESTATIONS)
183
+ }
184
+ return response.attestations
185
+ }
186
+
187
+ async getDownloadInfo(packageName) {
188
+ if (this.registryFor(packageName) !== PUBLIC_REGISTRY) {
189
+ return this.unavailable(CAPABILITY.DOWNLOAD_COUNTS)
190
+ }
191
+ const escapedName = npa(packageName).escapedName
192
+ const response = await this.requestJson(
193
+ `https://api.npmjs.org/downloads/point/last-month/${escapedName}`,
194
+ packageName
195
+ )
196
+ if (!response || typeof response.downloads !== 'number') {
197
+ throw new RegistryError('Registry download response is malformed', {
198
+ registry: PUBLIC_REGISTRY,
199
+ code: 'EREGISTRYPROTOCOL'
200
+ })
201
+ }
202
+ return response.downloads
203
+ }
204
+ }
205
+
206
+ module.exports = RegistryClient
@@ -0,0 +1,79 @@
1
+ 'use strict'
2
+
3
+ const path = require('node:path')
4
+ const Config = require('@npmcli/config')
5
+ const npmFetch = require('npm-registry-fetch')
6
+ const {
7
+ definitions,
8
+ flatten,
9
+ nerfDarts,
10
+ shorthands
11
+ } = require('@npmcli/config/lib/definitions')
12
+ const { RegistryError, sanitizeRegistryUrl } = require('./registryErrors')
13
+
14
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org/'
15
+
16
+ function normalizeRegistry(value) {
17
+ const url = new URL(value || DEFAULT_REGISTRY)
18
+ if (!url.pathname.endsWith('/')) {
19
+ url.pathname += '/'
20
+ }
21
+ return url.toString()
22
+ }
23
+
24
+ class RegistryConfig {
25
+ constructor(requestOptions) {
26
+ this.requestOptions = Object.freeze({
27
+ ...requestOptions,
28
+ registry: normalizeRegistry(requestOptions.registry)
29
+ })
30
+ }
31
+
32
+ static defaults() {
33
+ return new RegistryConfig({ registry: DEFAULT_REGISTRY })
34
+ }
35
+
36
+ static async load({ argv = [], env = process.env, cwd = process.cwd() } = {}) {
37
+ try {
38
+ const registryArg = argv.find((arg) => arg.startsWith('--registry='))
39
+ if (registryArg) {
40
+ normalizeRegistry(registryArg.slice('--registry='.length))
41
+ }
42
+
43
+ const config = new Config({
44
+ npmPath: path.resolve(__dirname, '..'),
45
+ definitions,
46
+ shorthands,
47
+ flatten,
48
+ nerfDarts,
49
+ argv: ['node', 'npq', ...argv],
50
+ env,
51
+ cwd,
52
+ warn: false
53
+ })
54
+ await config.load()
55
+ if (!config.validate()) {
56
+ throw new Error('Invalid npm configuration')
57
+ }
58
+ return new RegistryConfig(config.flat)
59
+ } catch (error) {
60
+ const code =
61
+ error.code === 'ERR_INVALID_AUTH' ? 'EREGISTRYCONFIGAUTH' : 'EREGISTRYCONFIG'
62
+ const message =
63
+ error.code === 'ERR_INVALID_AUTH'
64
+ ? 'Invalid npm registry authentication configuration'
65
+ : `Unable to load npm registry configuration: ${error.code || error.name}`
66
+ throw new RegistryError(message, { code, cause: error })
67
+ }
68
+ }
69
+
70
+ registryFor(packageSpec) {
71
+ return normalizeRegistry(npmFetch.pickRegistry(packageSpec, this.requestOptions))
72
+ }
73
+
74
+ describeRegistry(packageSpec) {
75
+ return sanitizeRegistryUrl(this.registryFor(packageSpec))
76
+ }
77
+ }
78
+
79
+ module.exports = RegistryConfig
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ function sanitizeRegistryUrl(value) {
4
+ try {
5
+ const url = new URL(value)
6
+ url.username = ''
7
+ url.password = ''
8
+ url.search = ''
9
+ url.hash = ''
10
+ return url.toString()
11
+ } catch {
12
+ return 'configured registry'
13
+ }
14
+ }
15
+
16
+ class RegistryError extends Error {
17
+ constructor(
18
+ message,
19
+ { registry = null, code = 'EREGISTRY', statusCode = null, cause = null } = {}
20
+ ) {
21
+ const sanitizedRegistry = registry ? sanitizeRegistryUrl(registry) : null
22
+ super(sanitizedRegistry ? `${message} (${sanitizedRegistry})` : message, {
23
+ cause: cause || undefined
24
+ })
25
+ this.name = 'RegistryError'
26
+ this.code = code
27
+ this.statusCode = statusCode
28
+ this.registry = sanitizedRegistry
29
+ }
30
+ }
31
+
32
+ module.exports = { RegistryError, sanitizeRegistryUrl }
@@ -108,6 +108,7 @@ function reportResults(marshallResults, { plain = false } = {}) {
108
108
 
109
109
  let countErrors = 0
110
110
  let countWarnings = 0
111
+ let countNotEvaluated = 0
111
112
  let countPackages = results.length
112
113
 
113
114
  results.forEach((result, index) => {
@@ -219,11 +220,33 @@ function reportResults(marshallResults, { plain = false } = {}) {
219
220
  })
220
221
  }
221
222
 
223
+ if (result.notEvaluated && result.notEvaluated.length > 0) {
224
+ result.notEvaluated.forEach((entryArray) => {
225
+ countNotEvaluated += entryArray.length
226
+ entryArray.forEach((entry) => {
227
+ const topicRich = styleText(
228
+ ['cyan'],
229
+ entry.categoryFriendlyName.padEnd(maxTextLength, ' ')
230
+ )
231
+ const iconRich = styleText(['cyan'], '○')
232
+ const wrappedMessage = wrapTextWithIndent(
233
+ entry.message,
234
+ maxMessageWidth,
235
+ indentString
236
+ )
237
+ resultsForPrettyPrint +=
238
+ `\n ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${wrappedMessage} `
239
+ resultsForPlainTextPrint +=
240
+ `\n NOT EVALUATED: ${entry.categoryFriendlyName} - ${entry.message}`
241
+ })
242
+ })
243
+ }
244
+
222
245
  // Close rich formatting box
223
246
  resultsForPrettyPrint += `\n ${styleText(['dim'], '└─')}`
224
247
  })
225
248
 
226
- const maxTextLength = 'Total packages:'.length
249
+ const maxTextLength = 'Total not evaluated:'.length
227
250
 
228
251
  // Rich formatting summary
229
252
  summaryForPrettyPrint += `\n\n${styleText(['bold'], 'Summary:')}\n`
@@ -239,6 +262,10 @@ function reportResults(marshallResults, { plain = false } = {}) {
239
262
  ['yellow', 'bold'],
240
263
  String(countWarnings)
241
264
  )}`
265
+ summaryForPrettyPrint += `\n - ${'Total not evaluated:'.padEnd(maxTextLength, ' ')} ${styleText(
266
+ ['cyan'],
267
+ String(countNotEvaluated)
268
+ )}`
242
269
  summaryForPrettyPrint += `\n`
243
270
 
244
271
  // Plain text summary
@@ -246,11 +273,13 @@ function reportResults(marshallResults, { plain = false } = {}) {
246
273
  summaryForPlainTextPrint += `\n - Total packages: ${countPackages}`
247
274
  summaryForPlainTextPrint += `\n - Total errors: ${countErrors}`
248
275
  summaryForPlainTextPrint += `\n - Total warnings: ${countWarnings}`
276
+ summaryForPlainTextPrint += `\n - Total not evaluated: ${countNotEvaluated}`
249
277
  summaryForPlainTextPrint += `\n`
250
278
 
251
279
  return {
252
280
  countErrors,
253
281
  countWarnings,
282
+ countNotEvaluated,
254
283
  resultsForPrettyPrint,
255
284
  resultsForJSON,
256
285
  resultsForPlainTextPrint,
@@ -267,7 +296,8 @@ function marshallResultsToIssuesPerPackage(results) {
267
296
  const issues = {
268
297
  pkg,
269
298
  errors: [],
270
- warnings: []
299
+ warnings: [],
300
+ notEvaluated: []
271
301
  }
272
302
  for (const marshall of allMarshallResultsList) {
273
303
  for (const value of Object.values(marshall)) {
@@ -294,9 +324,22 @@ function marshallResultsToIssuesPerPackage(results) {
294
324
 
295
325
  issues.warnings.push(marshallWarnings)
296
326
  }
327
+ if (value.notEvaluated && value.notEvaluated.length > 0) {
328
+ const skippedChecks = value.notEvaluated.map((entry) => ({
329
+ marshall: value.marshall,
330
+ categoryId: value.categoryId,
331
+ categoryFriendlyName: marshallCategories[value.categoryId].title,
332
+ ...entry
333
+ }))
334
+ issues.notEvaluated.push(skippedChecks)
335
+ }
297
336
  }
298
337
  }
299
- if (issues.errors.length > 0 || issues.warnings.length > 0) {
338
+ if (
339
+ issues.errors.length > 0 ||
340
+ issues.warnings.length > 0 ||
341
+ issues.notEvaluated.length > 0
342
+ ) {
300
343
  issuesPerPackage.push(issues)
301
344
  }
302
345
  }
package/lib/marshall.js CHANGED
@@ -3,12 +3,20 @@
3
3
  // const util = require('node:util')
4
4
  const Marshalls = require('./marshalls')
5
5
  const PackageRepoUtils = require('./helpers/packageRepoUtils')
6
+ const RegistryClient = require('./helpers/registryClient')
6
7
  // const { marshallCategories } = require('./marshalls/constants')
7
8
 
8
9
  class Marshall {
9
10
  constructor(options = {}) {
10
- this.pkgs = options ? options.pkgs : null
11
- this.packageRepoUtils = new PackageRepoUtils()
11
+ options = options || {}
12
+ this.pkgs = options.pkgs || null
13
+ this.registryClient =
14
+ options.registryClient ||
15
+ options.packageRepoUtils?.registryClient ||
16
+ RegistryClient.public()
17
+ this.packageRepoUtils =
18
+ options.packageRepoUtils ||
19
+ new PackageRepoUtils({ registryClient: this.registryClient })
12
20
  this.progressManager = options.progressManager || null
13
21
  this.promiseThrottleHelper = options.promiseThrottleHelper || null
14
22
  }
@@ -56,7 +64,8 @@ class Marshall {
56
64
  const allPackages = Array.isArray(pkg) ? pkg : [pkg]
57
65
  const config = {
58
66
  pkgs: this.createPackageVersionMaps(allPackages),
59
- packageRepoUtils: packageRepoUtils
67
+ packageRepoUtils: packageRepoUtils,
68
+ registryClient: this.registryClient
60
69
  }
61
70
  return Marshalls.tasks(config, this.progressManager)
62
71
  }
@@ -2,6 +2,8 @@
2
2
 
3
3
  const semver = require('semver')
4
4
  const util = require('node:util')
5
+ const NotEvaluated = require('../helpers/notEvaluated')
6
+ const { RegistryError } = require('../helpers/registryErrors')
5
7
  const Warning = require('../helpers/warning')
6
8
  const { marshallCategories } = require('./constants')
7
9
 
@@ -9,6 +11,7 @@ class BaseMarshall {
9
11
  constructor(options) {
10
12
  this.debug = util.debuglog('npq')
11
13
  this.packageRepoUtils = options.packageRepoUtils
14
+ this.registryClient = options.registryClient
12
15
  this.categoryId = marshallCategories.PackageHealth.id
13
16
  }
14
17
 
@@ -19,6 +22,7 @@ class BaseMarshall {
19
22
  status: null,
20
23
  errors: [],
21
24
  warnings: [],
25
+ notEvaluated: [],
22
26
  data: {},
23
27
  marshall: this.name,
24
28
  categoryId: this.categoryId
@@ -42,13 +46,22 @@ class BaseMarshall {
42
46
  return data
43
47
  })
44
48
  .catch((err) => {
45
- this.setMessage(
46
- {
47
- pkg: pkg.packageString,
48
- message: err.message
49
- },
50
- Boolean(err instanceof Warning)
51
- )
49
+ if (err instanceof RegistryError) {
50
+ throw err
51
+ }
52
+
53
+ const message = {
54
+ pkg: pkg.packageString,
55
+ message: err.message
56
+ }
57
+
58
+ if (err instanceof NotEvaluated) {
59
+ this.setNotEvaluated(message)
60
+ return undefined
61
+ }
62
+
63
+ this.setMessage(message, Boolean(err instanceof Warning))
64
+ return undefined
52
65
  })
53
66
  }
54
67
 
@@ -69,6 +82,13 @@ class BaseMarshall {
69
82
  })
70
83
  }
71
84
 
85
+ setNotEvaluated(msg) {
86
+ this.ctx.marshalls[this.name].notEvaluated.push({
87
+ pkg: msg.pkg,
88
+ message: msg.message
89
+ })
90
+ }
91
+
72
92
  /**
73
93
  * Resolves a package version string to an actual semver version
74
94
  * Handles dist-tags (latest, beta, etc.) and direct semver versions
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs')
4
4
  const path = require('node:path')
5
+ const { RegistryError } = require('../helpers/registryErrors')
5
6
 
6
7
  class Marshalls {
7
8
  static async collectMarshalls() {
@@ -20,7 +21,8 @@ class Marshalls {
20
21
  }
21
22
 
22
23
  const config = {
23
- packageRepoUtils: options.packageRepoUtils
24
+ packageRepoUtils: options.packageRepoUtils,
25
+ registryClient: options.registryClient
24
26
  }
25
27
 
26
28
  const marshallTasks = marshalls.reduce((prev, curr) => {
@@ -96,6 +98,7 @@ class Marshalls {
96
98
  status: null,
97
99
  errors: [{ pkg: options.pkgs[index].packageName, message: 'Package not found' }],
98
100
  warnings: [],
101
+ notEvaluated: [],
99
102
  data: {},
100
103
  categoryId: 'PackageHealth'
101
104
  }
@@ -119,7 +122,8 @@ class Marshalls {
119
122
  const packagesToProcess = options.pkgs
120
123
  if (packagesToProcess.length > 0) {
121
124
  const marshallTasks = await Marshalls.buildMarshallTasks(marshalls, {
122
- packageRepoUtils: options.packageRepoUtils
125
+ packageRepoUtils: options.packageRepoUtils,
126
+ registryClient: options.registryClient
123
127
  })
124
128
 
125
129
  for (const marshall of marshallTasks) {
@@ -130,6 +134,9 @@ class Marshalls {
130
134
  })
131
135
  marshallResults.push(res.marshalls)
132
136
  } catch (error) {
137
+ if (error instanceof RegistryError) {
138
+ throw error
139
+ }
133
140
  console.error(`Error running task ${marshall.title}:`, error)
134
141
  }
135
142
  }
@@ -4,6 +4,8 @@ const semver = require('semver')
4
4
  const BaseMarshall = require('./baseMarshall')
5
5
  const NpmRegistry = require('../helpers/npmRegistry')
6
6
  const Warning = require('../helpers/warning')
7
+ const NotEvaluated = require('../helpers/notEvaluated')
8
+ const { RegistryError } = require('../helpers/registryErrors')
7
9
  const { marshallCategories } = require('./constants')
8
10
 
9
11
  const MARSHALL_NAME = 'provenance'
@@ -88,115 +90,80 @@ class Marshall extends BaseMarshall {
88
90
  }
89
91
  }
90
92
 
91
- validate(pkg) {
93
+ async validate(pkg) {
92
94
  const validationMetadata = {}
93
- const npmRegistry = new NpmRegistry({
94
- registry: 'https://registry.npmjs.org'
95
- })
96
-
97
- return this.packageRepoUtils
98
- .getPackageInfo(pkg.packageName)
99
- .then(async (packageInfo) => {
100
- const packageName = packageInfo.name
101
-
102
- // Use the resolvePackageVersion method to handle version ranges properly
103
- const packageVersion = await this.resolvePackageVersion(
104
- pkg.packageName,
105
- pkg.packageVersion,
106
- packageInfo
107
- )
95
+ const verifier = new NpmRegistry()
96
+
97
+ try {
98
+ const packageInfo = await this.packageRepoUtils.getPackageInfo(pkg.packageName)
99
+ validationMetadata.name = packageInfo.name
100
+ validationMetadata.version = await this.resolvePackageVersion(
101
+ pkg.packageName,
102
+ pkg.packageVersion,
103
+ packageInfo
104
+ )
105
+ validationMetadata.packageInfo = packageInfo
108
106
 
109
- validationMetadata.name = packageName
110
- validationMetadata.version = packageVersion
111
- validationMetadata.packageInfo = packageInfo
112
-
113
- if (!validationMetadata.version) {
114
- throw new Error('Unable to find version or dist-tag for package')
115
- }
116
-
117
- return validationMetadata
118
- })
119
- .then((validationMetadata) => {
120
- return this.fetchRegistryKeys().then((keys) => {
121
- return npmRegistry
122
- .getManifest(`${validationMetadata.name}@${validationMetadata.version}`)
123
- .then((manifest) => npmRegistry.verifyAttestations(manifest, keys))
124
- })
125
- })
126
- .then((metadata) => {
127
- if (!metadata || !metadata._attestations) {
128
- this.throwIfProvenanceRegression(validationMetadata)
129
- throw new Warning('the package was published without any attestations')
130
- }
131
-
132
- const attestations = metadata._attestations
133
- return attestations
134
- })
135
- .catch((error) => {
136
- // We can ignore this type of error, false positive
137
- // See: https://github.com/lirantal/npq/issues/329
138
- if (error.code === 'EATTESTATIONVERIFY' && error.message.includes('malformed checkpoint')) {
139
- return []
140
- }
141
-
142
- if (
143
- error instanceof Error &&
144
- error.message === 'Unable to find version or dist-tag for package'
145
- ) {
146
- throw error
147
- }
148
-
149
- if (
150
- error instanceof Warning &&
151
- (error.message.includes('Error fetching registry keys') ||
152
- error.message === 'the package was published without any attestations')
153
- ) {
154
- throw error
155
- }
156
-
157
- if (shouldConsiderProvenanceRegression(error)) {
158
- this.throwIfProvenanceRegression(validationMetadata)
159
- }
160
-
161
- if (error.message.includes('Package has no attestations to verify')) {
162
- throw new Warning(
163
- `Unable to verify provenance: the package was published without any attestations`
164
- )
165
- }
166
-
167
- this.debug(
168
- '\nUnable to verify provenance for package %s@%s: %s',
169
- validationMetadata.name,
170
- validationMetadata.version,
171
- error.message
107
+ if (!validationMetadata.version) {
108
+ throw new Error('Unable to find version or dist-tag for package')
109
+ }
110
+
111
+ const manifest = await this.registryClient.getManifest(
112
+ `${pkg.packageName}@${validationMetadata.version}`,
113
+ packageInfo
114
+ )
115
+ let metadata
116
+ if (!manifest.dist || !manifest.dist.attestations) {
117
+ metadata = await verifier.verifyAttestations(manifest, [], null)
118
+ } else {
119
+ const [keys, attestations] = await Promise.all([
120
+ this.registryClient.getRegistryKeys(pkg.packageName),
121
+ this.registryClient.getAttestations(pkg.packageName, manifest)
122
+ ])
123
+ metadata = await verifier.verifyAttestations(manifest, keys, attestations)
124
+ }
125
+
126
+ if (!metadata || !metadata._attestations) {
127
+ this.throwIfProvenanceRegression(validationMetadata)
128
+ throw new Warning('the package was published without any attestations')
129
+ }
130
+ return metadata._attestations
131
+ } catch (error) {
132
+ if (error instanceof NotEvaluated || error instanceof RegistryError) {
133
+ throw error
134
+ }
135
+ if (error.code === 'EATTESTATIONVERIFY' && error.message.includes('malformed checkpoint')) {
136
+ return []
137
+ }
138
+ if (
139
+ error instanceof Error &&
140
+ error.message === 'Unable to find version or dist-tag for package'
141
+ ) {
142
+ throw error
143
+ }
144
+ if (
145
+ error instanceof Warning &&
146
+ error.message === 'the package was published without any attestations'
147
+ ) {
148
+ throw error
149
+ }
150
+ if (shouldConsiderProvenanceRegression(error)) {
151
+ this.throwIfProvenanceRegression(validationMetadata)
152
+ }
153
+ if (error.message.includes('Package has no attestations to verify')) {
154
+ throw new Warning(
155
+ 'Unable to verify provenance: the package was published without any attestations'
172
156
  )
173
- throw new Warning(`Unable to verify provenance`)
174
- })
175
- }
157
+ }
176
158
 
177
- fetchRegistryKeys() {
178
- const registryHost = 'https://registry.npmjs.org'
179
- const registryKeysEndpoint = '/-/npm/v1/keys'
180
-
181
- const registryKeysUrl = `${registryHost}${registryKeysEndpoint}`
182
- return fetch(registryKeysUrl)
183
- .then((response) => {
184
- return response.json()
185
- })
186
- .then((response) => {
187
- const registryKeys = response.keys
188
-
189
- return registryKeys.map((key) => ({
190
- ...key,
191
- pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`
192
- }))
193
- })
194
- .then((keys) => {
195
- return keys
196
- })
197
- .catch((error) => {
198
- throw new Warning(`Error fetching registry keys: ${error.message}`)
199
- })
159
+ this.debug(
160
+ '\nUnable to verify provenance for package %s@%s: %s',
161
+ validationMetadata.name,
162
+ validationMetadata.version,
163
+ error.message
164
+ )
165
+ throw new Warning('Unable to verify provenance')
166
+ }
200
167
  }
201
168
  }
202
169
 
@@ -3,12 +3,12 @@
3
3
  const BaseMarshall = require('./baseMarshall')
4
4
  const Warning = require('../helpers/warning')
5
5
  const NpmRegistry = require('../helpers/npmRegistry')
6
+ const NotEvaluated = require('../helpers/notEvaluated')
7
+ const { RegistryError } = require('../helpers/registryErrors')
6
8
  const { marshallCategories } = require('./constants')
7
9
 
8
10
  const MARSHALL_NAME = 'signatures'
9
11
 
10
- let registryKeysCache = null
11
-
12
12
  class Marshall extends BaseMarshall {
13
13
  constructor(options) {
14
14
  super(options)
@@ -20,83 +20,44 @@ class Marshall extends BaseMarshall {
20
20
  return 'Verifying registry signatures for package'
21
21
  }
22
22
 
23
- validate(pkg) {
24
- const npmRegistry = new NpmRegistry({
25
- registry: 'https://registry.npmjs.org'
26
- })
27
-
28
- return this.packageRepoUtils
29
- .getPackageInfo(pkg.packageName)
30
- .then(async (packageInfo) => {
31
- // Resolve version range to specific version
32
- const resolvedVersion = await this.resolvePackageVersion(
33
- pkg.packageName,
34
- pkg.packageVersion,
35
- packageInfo
36
- )
23
+ async validate(pkg) {
24
+ const verifier = new NpmRegistry()
37
25
 
38
- if (!resolvedVersion) {
39
- throw new Error(
40
- `Unable to resolve version ${pkg.packageVersion} for package ${pkg.packageName}`
41
- )
42
- }
26
+ try {
27
+ const packageInfo = await this.packageRepoUtils.getPackageInfo(pkg.packageName)
28
+ const resolvedVersion = await this.resolvePackageVersion(
29
+ pkg.packageName,
30
+ pkg.packageVersion,
31
+ packageInfo
32
+ )
43
33
 
44
- return { packageInfo, resolvedVersion }
45
- })
46
- .then(({ resolvedVersion }) => {
47
- return this.fetchRegistryKeys().then((keys) => {
48
- return npmRegistry
49
- .getManifest(`${pkg.packageName}@${resolvedVersion}`)
50
- .then((manifest) => npmRegistry.verifySignatures(manifest, keys))
51
- })
52
- })
53
- .then((metadata) => {
54
- return metadata
55
- })
56
- .catch((error) => {
57
- // Handle specific error cases
58
- if (
59
- error.message &&
60
- error.message.includes('but the corresponding public key has expired')
61
- ) {
62
- // if the error is about an expired key, we throw a warning
63
- // instead of an error, so that the process can continue
64
- throw new Warning(`Package is signed with an expired key`)
65
- }
66
-
67
- throw new Warning(`Unable to verify package signature on registry: ${error.message}`)
68
- })
69
- }
34
+ if (!resolvedVersion) {
35
+ throw new Error(
36
+ `Unable to resolve version ${pkg.packageVersion} for package ${pkg.packageName}`
37
+ )
38
+ }
70
39
 
71
- fetchRegistryKeys() {
72
- if (registryKeysCache) {
73
- return Promise.resolve(registryKeysCache)
40
+ const manifest = await this.registryClient.getManifest(
41
+ `${pkg.packageName}@${resolvedVersion}`,
42
+ packageInfo
43
+ )
44
+ if (!manifest.dist || !manifest.dist.signatures) {
45
+ return await verifier.verifySignatures(manifest, [])
46
+ }
47
+ const keys = await this.registryClient.getRegistryKeys(pkg.packageName)
48
+ return await verifier.verifySignatures(manifest, keys)
49
+ } catch (error) {
50
+ if (error instanceof NotEvaluated || error instanceof RegistryError) {
51
+ throw error
52
+ }
53
+ if (
54
+ error.message &&
55
+ error.message.includes('but the corresponding public key has expired')
56
+ ) {
57
+ throw new Warning('Package is signed with an expired key')
58
+ }
59
+ throw new Warning(`Unable to verify package signature on registry: ${error.message}`)
74
60
  }
75
-
76
- const registryHost = 'https://registry.npmjs.org'
77
- const registryKeysEndpoint = '/-/npm/v1/keys'
78
-
79
- const registryKeysUrl = `${registryHost}${registryKeysEndpoint}`
80
- return fetch(registryKeysUrl)
81
- .then((response) => {
82
- return response.json()
83
- })
84
- .then((response) => {
85
- const registryKeys = response.keys
86
-
87
- return registryKeys.map((key) => ({
88
- ...key,
89
- pemkey: `-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`
90
- }))
91
- })
92
- .then((keys) => {
93
- // save it in the cached singleton object
94
- registryKeysCache = keys
95
- return keys
96
- })
97
- .catch((error) => {
98
- throw new Warning(`Error fetching registry keys: ${error.message}`)
99
- })
100
61
  }
101
62
  }
102
63
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npq",
3
- "version": "3.20.0",
3
+ "version": "3.21.0",
4
4
  "description": "marshall your npm/npm package installs with high quality and class 🎖",
5
5
  "bin": {
6
6
  "npq": "./bin/npq.js",
@@ -43,7 +43,9 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
+ "@npmcli/config": "^10.10.0",
46
47
  "npm-package-arg": "^13.0.2",
48
+ "npm-registry-fetch": "^19.1.1",
47
49
  "semver": "^7.7.4",
48
50
  "sigstore": "^4.1.0",
49
51
  "ssri": "^13.0.1"