npq 3.20.0 → 3.21.1
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 +13 -3
- package/bin/npq-hero.js +28 -14
- package/bin/npq.js +25 -8
- package/lib/cli.js +20 -3
- package/lib/helpers/notEvaluated.js +11 -0
- package/lib/helpers/npmRegistry.js +3 -66
- package/lib/helpers/packageRepoUtils.js +11 -21
- package/lib/helpers/registryClient.js +206 -0
- package/lib/helpers/registryConfig.js +79 -0
- package/lib/helpers/registryErrors.js +32 -0
- package/lib/helpers/reportResults.js +46 -3
- package/lib/marshall.js +12 -3
- package/lib/marshalls/baseMarshall.js +27 -7
- package/lib/marshalls/expiredDomains.marshall.js +9 -0
- package/lib/marshalls/index.js +9 -2
- package/lib/marshalls/provenance.marshall.js +72 -105
- package/lib/marshalls/signatures.marshall.js +36 -75
- package/package.json +3 -1
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
11
|
-
this.
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 BaseMarshall = require('./baseMarshall')
|
|
4
4
|
const { marshallCategories } = require('./constants')
|
|
5
|
+
const NotEvaluated = require('../helpers/notEvaluated')
|
|
5
6
|
const { Resolver } = require('dns/promises')
|
|
6
7
|
|
|
7
8
|
const dns = new Resolver()
|
|
@@ -28,6 +29,10 @@ class Marshall extends BaseMarshall {
|
|
|
28
29
|
|
|
29
30
|
const maintainersAccounts = lastVersionData && lastVersionData.maintainers
|
|
30
31
|
|
|
32
|
+
if (!Array.isArray(maintainersAccounts) || maintainersAccounts.length === 0) {
|
|
33
|
+
throw new NotEvaluated('no maintainers information available for the package')
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
const testEmails = []
|
|
32
37
|
for (const maintainerInfo of maintainersAccounts) {
|
|
33
38
|
const maintainerEmail = maintainerInfo.email
|
|
@@ -38,6 +43,10 @@ class Marshall extends BaseMarshall {
|
|
|
38
43
|
return Promise.all(testEmails)
|
|
39
44
|
})
|
|
40
45
|
.catch((error) => {
|
|
46
|
+
if (error instanceof NotEvaluated) {
|
|
47
|
+
throw error
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
const emailHostname = error.hostname ? error.hostname : '<unknown>'
|
|
42
51
|
this.debug('\nDetected error resolving domain for maintainer e-mail: %s', emailHostname)
|
|
43
52
|
throw new Error(
|
package/lib/marshalls/index.js
CHANGED
|
@@ -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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
174
|
-
})
|
|
175
|
-
}
|
|
157
|
+
}
|
|
176
158
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
3
|
+
"version": "3.21.1",
|
|
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"
|