npq 3.19.6 → 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.
@@ -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
 
@@ -6,6 +6,7 @@ const { marshallCategories } = require('./constants')
6
6
  const path = require('path')
7
7
  const { levenshteinDistance } = require('../helpers/levenshteinDistance')
8
8
  const topPackagesRawJSON = require(path.join(__dirname, '../../data/top-packages.json'))
9
+ const topPackages = new Set(topPackagesRawJSON)
9
10
 
10
11
  const MARSHALL_NAME = 'typosquatting'
11
12
 
@@ -29,13 +30,13 @@ class Marshall extends BaseMarshall {
29
30
  return resolve([])
30
31
  }
31
32
 
32
- for (const popularPackageNameInRepository of topPackagesRawJSON) {
33
- // If the package to be installed is itself found within the Top Packages dataset
34
- // then we don't report on it
35
- if (pkg.packageName === popularPackageNameInRepository) {
36
- return resolve([])
37
- }
33
+ // If the package to be installed is itself found within the Top Packages dataset
34
+ // then we don't report on it
35
+ if (topPackages.has(pkg.packageName)) {
36
+ return resolve([])
37
+ }
38
38
 
39
+ for (const popularPackageNameInRepository of topPackagesRawJSON) {
39
40
  editDistance = levenshteinDistance(pkg.packageName, popularPackageNameInRepository)
40
41
 
41
42
  if (editDistance > 0 && editDistance < 3) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npq",
3
- "version": "3.19.6",
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",
@@ -14,13 +14,14 @@
14
14
  "build": "node scripts/build.js",
15
15
  "test:watch": "jest --watch",
16
16
  "format": "prettier --config .prettierrc.js --write \"**/*.js\"",
17
- "semantic-release": "npx semantic-release",
17
+ "semantic-release": "semantic-release",
18
18
  "#postinstall": "node scripts/postinstall.js",
19
19
  "#preuninstall": "node scripts/preuninstall.js",
20
20
  "prepare": "husky || true"
21
21
  },
22
22
  "engines": {
23
- "node": ">=20.13.0"
23
+ "npm": ">=11.10.0",
24
+ "node": ">=24.0.0"
24
25
  },
25
26
  "files": [
26
27
  "bin/",
@@ -42,7 +43,9 @@
42
43
  "access": "public"
43
44
  },
44
45
  "dependencies": {
46
+ "@npmcli/config": "^10.10.0",
45
47
  "npm-package-arg": "^13.0.2",
48
+ "npm-registry-fetch": "^19.1.1",
46
49
  "semver": "^7.7.4",
47
50
  "sigstore": "^4.1.0",
48
51
  "ssri": "^13.0.1"
@@ -57,7 +60,8 @@
57
60
  "jest": "^30.3.0",
58
61
  "lint-staged": "^16.4.0",
59
62
  "lockfile-lint": "^5.0.0",
60
- "prettier": "^3.6.2"
63
+ "prettier": "^3.6.2",
64
+ "semantic-release": "25.0.3"
61
65
  },
62
66
  "lint-staged": {
63
67
  "**/*.js": [
package/scripts/build.js CHANGED
@@ -3,15 +3,46 @@
3
3
  const fs = require('fs/promises')
4
4
  const path = require('path')
5
5
 
6
- const TOP_PACKAGES_ASSET_URL =
7
- 'https://github.com/lirantal/npm-rank/releases/download/latest/list-package-names.json'
8
-
9
6
  const TOP_PACKAGES_FILE_PATH = path.join(__dirname, '../data/top-packages.json')
7
+ const NPM_HIGH_IMPACT_TOP_PACKAGES_URL = 'https://unpkg.com/npm-high-impact@1.13.0/lib/top.js'
8
+ const TOP_PACKAGES_EXPORT_PATTERN = /^export const top = \[\n([\s\S]*)\n\]\n?$/
9
+ const TOP_PACKAGE_NAME_PATTERN = /^[ ]{2}'([^'\\]+)',?$/
10
+
11
+ function parseTopPackages(source) {
12
+ const topPackagesExport = source.match(TOP_PACKAGES_EXPORT_PATTERN)
13
+
14
+ if (!topPackagesExport) {
15
+ throw new Error('Unexpected npm-high-impact top packages export format')
16
+ }
17
+
18
+ const topPackages = topPackagesExport[1].split('\n').map((line) => {
19
+ const packageName = line.match(TOP_PACKAGE_NAME_PATTERN)
20
+
21
+ if (!packageName) {
22
+ throw new Error(`Unexpected npm-high-impact package entry: ${line}`)
23
+ }
24
+
25
+ return packageName[1]
26
+ })
27
+
28
+ if (topPackages.length === 0) {
29
+ throw new Error('npm-high-impact top packages export is empty')
30
+ }
31
+
32
+ return [...new Set(topPackages)]
33
+ }
10
34
 
11
35
  async function downloadTopPackages() {
12
- const response = await fetch(TOP_PACKAGES_ASSET_URL)
13
- const data = await response.json()
14
- return data
36
+ // Keep this pinned to a published npm-high-impact artifact for reproducible builds.
37
+ const response = await fetch(NPM_HIGH_IMPACT_TOP_PACKAGES_URL)
38
+
39
+ if (!response.ok) {
40
+ throw new Error(
41
+ `Failed to download npm-high-impact top packages: ${response.status} ${response.statusText}`
42
+ )
43
+ }
44
+
45
+ return parseTopPackages(await response.text())
15
46
  }
16
47
 
17
48
  async function saveTopPackagesToFile() {