npq 3.23.2 → 3.24.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 +12 -0
- package/bin/npq-hero.js +2 -9
- package/bin/npq.js +150 -139
- package/lib/cli.js +11 -3
- package/lib/helpers/auditFailure.js +25 -0
- package/lib/helpers/jsonOutput.js +24 -0
- package/lib/helpers/jsonPackageSpec.js +29 -0
- package/lib/helpers/jsonReport.js +76 -0
- package/lib/helpers/registryClient.js +2 -7
- package/lib/helpers/registryConfig.js +2 -8
- package/lib/helpers/reportResults.js +5 -16
- package/lib/helpers/versionAge.js +2 -9
- package/lib/jsonAudit.js +71 -0
- package/lib/jsonCli.js +61 -0
- package/lib/marshall.js +11 -6
- package/lib/marshalls/baseMarshall.js +1 -1
- package/lib/marshalls/index.js +44 -7
- package/lib/marshalls/signatures.marshall.js +1 -4
- package/package.json +3 -1
- package/schema/npq-output-v1.schema.json +86 -0
package/README.md
CHANGED
|
@@ -222,6 +222,18 @@ npq install express --dry-run
|
|
|
222
222
|
npq install express --plain
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
+
### Machine-readable audits
|
|
226
|
+
|
|
227
|
+
Use `--json` for a non-interactive, audit-only result suitable for CI:
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
npq install express --json
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
JSON mode never invokes the package manager. It exits `0` for a clean audit,
|
|
234
|
+
`1` for findings, and `2` when the audit cannot complete. See
|
|
235
|
+
[JSON audit output](docs/feature/json-output.md).
|
|
236
|
+
|
|
225
237
|
### Disable auto-continue countdown
|
|
226
238
|
|
|
227
239
|
By default, when npq detects only warnings (no errors), it automatically proceeds with installation after a 15-second countdown. To disable this behavior and always require explicit confirmation:
|
package/bin/npq-hero.js
CHANGED
|
@@ -51,20 +51,13 @@ RegistryConfig.load({ argv: cliArgs.registryConfigArgs || [] })
|
|
|
51
51
|
Object.hasOwn(results, 'countErrors') &&
|
|
52
52
|
Object.hasOwn(results, 'countWarnings')
|
|
53
53
|
) {
|
|
54
|
-
const {
|
|
55
|
-
countErrors,
|
|
56
|
-
countWarnings,
|
|
57
|
-
countNotEvaluated = 0,
|
|
58
|
-
useRichFormatting
|
|
59
|
-
} = results
|
|
54
|
+
const { countErrors, countWarnings, countNotEvaluated = 0, useRichFormatting } = results
|
|
60
55
|
const hasFindings = countErrors > 0 || countWarnings > 0
|
|
61
56
|
const hasReportableResults = hasFindings || countNotEvaluated > 0
|
|
62
57
|
|
|
63
58
|
if (hasReportableResults) {
|
|
64
59
|
console.log()
|
|
65
|
-
console.log(
|
|
66
|
-
hasFindings ? 'Packages with issues found:' : 'Package checks not evaluated:'
|
|
67
|
-
)
|
|
60
|
+
console.log(hasFindings ? 'Packages with issues found:' : 'Package checks not evaluated:')
|
|
68
61
|
|
|
69
62
|
if (useRichFormatting) {
|
|
70
63
|
console.log(results.resultsForPrettyPrint)
|
package/bin/npq.js
CHANGED
|
@@ -15,171 +15,182 @@ 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 { createJsonOutput } = require('../lib/helpers/jsonOutput')
|
|
19
|
+
const { runJsonCli, writeInvalidJsonInvocation } = require('../lib/jsonCli')
|
|
18
20
|
const RegistryConfig = require('../lib/helpers/registryConfig')
|
|
19
21
|
const RegistryClient = require('../lib/helpers/registryClient')
|
|
20
22
|
|
|
21
23
|
const debug = util.debuglog('npq')
|
|
22
24
|
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const spinner = isInteractive ? new Spinner({ text: 'Initiating...' }) : null
|
|
25
|
+
const jsonRequested = process.argv.slice(2).includes('--json')
|
|
26
|
+
const jsonOutput = jsonRequested ? createJsonOutput() : null
|
|
27
|
+
let cliArgs
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
try {
|
|
30
|
+
cliArgs = CliParser.parseArgsFull()
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (!jsonRequested) throw error
|
|
33
|
+
writeInvalidJsonInvocation(jsonOutput)
|
|
34
|
+
process.exitCode = 2
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
if (cliArgs && cliArgs.json) {
|
|
38
|
+
runJsonCli(cliArgs, { output: jsonOutput })
|
|
39
|
+
} else if (cliArgs) {
|
|
40
|
+
const auditOnly = cliArgs.dryRun || !cliArgs.installSubcommandExplicit
|
|
41
|
+
const isInteractive = cliSupport.isInteractiveTerminal() && !cliArgs.plain
|
|
42
|
+
const spinner = isInteractive ? new Spinner({ text: 'Initiating...' }) : null
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
})
|
|
38
|
-
.then(() => {
|
|
39
|
-
if (cliArgs.packages.length === 0) {
|
|
40
|
-
debug('\nNo packages specified, using project packages from package.json')
|
|
41
|
-
return getProjectPackages()
|
|
42
|
-
}
|
|
44
|
+
if (spinner) {
|
|
45
|
+
spinner.start()
|
|
46
|
+
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
})
|
|
46
|
-
.then((packages) => {
|
|
47
|
-
if (packages.error) {
|
|
48
|
-
console.log()
|
|
49
|
-
CliParser.exit({
|
|
50
|
-
errorCode: packages.error.code || -1,
|
|
51
|
-
message: packages.message,
|
|
52
|
-
spinner
|
|
53
|
-
})
|
|
54
|
-
}
|
|
48
|
+
let registryClient
|
|
55
49
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
promiseThrottleHelper
|
|
50
|
+
Promise.resolve()
|
|
51
|
+
.then(() => RegistryConfig.load({ argv: cliArgs.registryConfigArgs || [] }))
|
|
52
|
+
.then((registryConfig) => {
|
|
53
|
+
registryClient = new RegistryClient(registryConfig)
|
|
61
54
|
})
|
|
55
|
+
.then(() => {
|
|
56
|
+
if (cliArgs.packages.length === 0) {
|
|
57
|
+
debug('\nNo packages specified, using project packages from package.json')
|
|
58
|
+
return getProjectPackages()
|
|
59
|
+
}
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
spinner.stop()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const results = reportResults(marshallResults, { plain: cliArgs.plain })
|
|
71
|
-
if (
|
|
72
|
-
results &&
|
|
73
|
-
Object.hasOwn(results, 'countErrors') &&
|
|
74
|
-
Object.hasOwn(results, 'countWarnings')
|
|
75
|
-
) {
|
|
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) {
|
|
61
|
+
return cliArgs.packages
|
|
62
|
+
})
|
|
63
|
+
.then((packages) => {
|
|
64
|
+
if (packages.error) {
|
|
86
65
|
console.log()
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
console.log(results.resultsForPrettyPrint)
|
|
93
|
-
console.log(results.summaryForPrettyPrint)
|
|
94
|
-
} else {
|
|
95
|
-
console.log(results.resultsForPlainTextPrint)
|
|
96
|
-
console.log(results.summaryForPlainTextPrint)
|
|
97
|
-
}
|
|
66
|
+
CliParser.exit({
|
|
67
|
+
errorCode: packages.error.code || -1,
|
|
68
|
+
message: packages.message,
|
|
69
|
+
spinner
|
|
70
|
+
})
|
|
98
71
|
}
|
|
99
72
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return undefined
|
|
108
|
-
})
|
|
109
|
-
.then((result) => {
|
|
110
|
-
if (auditOnly) {
|
|
111
|
-
const auditFailed = result && (result.countErrors > 0 || result.countWarnings > 0)
|
|
112
|
-
CliParser.exit({
|
|
113
|
-
errorCode: auditFailed ? 1 : 0,
|
|
114
|
-
spinner
|
|
73
|
+
const marshall = new Marshall({
|
|
74
|
+
pkgs: packages,
|
|
75
|
+
registryClient,
|
|
76
|
+
progressManager: spinner,
|
|
77
|
+
promiseThrottleHelper
|
|
115
78
|
})
|
|
79
|
+
|
|
80
|
+
return marshall.process()
|
|
81
|
+
})
|
|
82
|
+
.then((marshallResults) => {
|
|
83
|
+
if (spinner) {
|
|
84
|
+
spinner.stop()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const results = reportResults(marshallResults, { plain: cliArgs.plain })
|
|
88
|
+
if (
|
|
89
|
+
results &&
|
|
90
|
+
Object.hasOwn(results, 'countErrors') &&
|
|
91
|
+
Object.hasOwn(results, 'countWarnings')
|
|
92
|
+
) {
|
|
93
|
+
const { countErrors, countWarnings, countNotEvaluated = 0, useRichFormatting } = results
|
|
94
|
+
const hasFindings = countErrors > 0 || countWarnings > 0
|
|
95
|
+
const hasReportableResults = hasFindings || countNotEvaluated > 0
|
|
96
|
+
|
|
97
|
+
if (hasReportableResults) {
|
|
98
|
+
console.log()
|
|
99
|
+
console.log(hasFindings ? 'Packages with issues found:' : 'Package checks not evaluated:')
|
|
100
|
+
|
|
101
|
+
if (useRichFormatting) {
|
|
102
|
+
console.log(results.resultsForPrettyPrint)
|
|
103
|
+
console.log(results.summaryForPrettyPrint)
|
|
104
|
+
} else {
|
|
105
|
+
console.log(results.resultsForPlainTextPrint)
|
|
106
|
+
console.log(results.summaryForPlainTextPrint)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
anyIssues: hasFindings,
|
|
112
|
+
countErrors,
|
|
113
|
+
countWarnings,
|
|
114
|
+
countNotEvaluated
|
|
115
|
+
}
|
|
116
|
+
}
|
|
116
117
|
return undefined
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
})
|
|
119
|
+
.then((result) => {
|
|
120
|
+
if (auditOnly) {
|
|
121
|
+
const auditFailed = result && (result.countErrors > 0 || result.countWarnings > 0)
|
|
122
|
+
CliParser.exit({
|
|
123
|
+
errorCode: auditFailed ? 1 : 0,
|
|
124
|
+
spinner
|
|
125
|
+
})
|
|
126
|
+
return undefined
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (result && result.countErrors > 0) {
|
|
128
130
|
console.log()
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
return cliPrompt.prompt({
|
|
132
|
+
name: 'install',
|
|
133
|
+
message: 'Continue install ?',
|
|
134
|
+
default: false
|
|
135
|
+
})
|
|
136
|
+
} else {
|
|
137
|
+
if (result && result.countWarnings > 0) {
|
|
138
|
+
console.log()
|
|
139
|
+
// Check if auto-continue is disabled via CLI flag or environment variable
|
|
140
|
+
if (cliArgs.disableAutoContinue) {
|
|
141
|
+
return cliPrompt.prompt({
|
|
142
|
+
name: 'install',
|
|
143
|
+
message: 'Continue install ?',
|
|
144
|
+
default: false
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
return cliPrompt.autoContinue({
|
|
132
148
|
name: 'install',
|
|
133
|
-
message: '
|
|
134
|
-
|
|
149
|
+
message: 'Auto-continue with install in... ',
|
|
150
|
+
timeInSeconds: 15
|
|
135
151
|
})
|
|
136
152
|
}
|
|
137
|
-
return cliPrompt.autoContinue({
|
|
138
|
-
name: 'install',
|
|
139
|
-
message: 'Auto-continue with install in... ',
|
|
140
|
-
timeInSeconds: 15
|
|
141
|
-
})
|
|
142
153
|
}
|
|
143
|
-
}
|
|
144
154
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
return { install: true }
|
|
156
|
+
})
|
|
157
|
+
.then((status) => {
|
|
158
|
+
if (
|
|
159
|
+
status &&
|
|
160
|
+
Object.prototype.hasOwnProperty.call(status, 'install') &&
|
|
161
|
+
status.install === true
|
|
162
|
+
) {
|
|
163
|
+
return pkgMgr.process(cliArgs.packageManager)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
.then((exitCode) => {
|
|
167
|
+
if (typeof exitCode === 'number') {
|
|
168
|
+
process.exitCode = exitCode
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
.catch((error) => {
|
|
172
|
+
// Ensure errorCode is always a number
|
|
173
|
+
let errorCode = -1
|
|
174
|
+
if (typeof error.code === 'number') {
|
|
175
|
+
errorCode = error.code
|
|
176
|
+
} else if (error.code === 'ABORT_ERR') {
|
|
177
|
+
errorCode = 1
|
|
178
|
+
} else if (error.code === 'USER_ABORT') {
|
|
179
|
+
errorCode = error.exitCode || 1
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
CliParser.exit({
|
|
183
|
+
errorCode,
|
|
184
|
+
message: error.message || 'An error occurred',
|
|
185
|
+
spinner
|
|
186
|
+
})
|
|
187
|
+
})
|
|
171
188
|
|
|
189
|
+
// attach event handler for CTRL+C
|
|
190
|
+
process.on('SIGINT', () => {
|
|
172
191
|
CliParser.exit({
|
|
173
|
-
errorCode,
|
|
174
|
-
message: error.message || 'An error occurred',
|
|
192
|
+
errorCode: 0,
|
|
175
193
|
spinner
|
|
176
194
|
})
|
|
177
195
|
})
|
|
178
|
-
|
|
179
|
-
// attach event handler for CTRL+C
|
|
180
|
-
process.on('SIGINT', () => {
|
|
181
|
-
CliParser.exit({
|
|
182
|
-
errorCode: 0,
|
|
183
|
-
spinner
|
|
184
|
-
})
|
|
185
|
-
})
|
|
196
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { parseArgs } = require('node:util')
|
|
4
4
|
const npa = require('npm-package-arg')
|
|
5
|
+
const { parseJsonRegistryPackageSpec } = require('./helpers/jsonPackageSpec')
|
|
5
6
|
const pkg = require('../package.json')
|
|
6
7
|
const { parsePackageManagerArguments } = require('./helpers/packageManagerArgumentParser')
|
|
7
8
|
|
|
@@ -45,7 +46,7 @@ class CliParser {
|
|
|
45
46
|
process.exit(exitCode)
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
static _extractPackagesFromPositionals(positionals, earlyExitNoInstall = false) {
|
|
49
|
+
static _extractPackagesFromPositionals(positionals, earlyExitNoInstall = false, json = false) {
|
|
49
50
|
let packages = []
|
|
50
51
|
if (positionals.length > 0) {
|
|
51
52
|
const command = positionals[0]
|
|
@@ -61,7 +62,7 @@ class CliParser {
|
|
|
61
62
|
|
|
62
63
|
// Parse and normalize packages
|
|
63
64
|
return packages.map((pkg) => {
|
|
64
|
-
const parsedPackage = npa(pkg)
|
|
65
|
+
const parsedPackage = json ? parseJsonRegistryPackageSpec(pkg) : npa(pkg)
|
|
65
66
|
const versionModifier = parsedPackage.fetchSpec === '*' ? 'latest' : parsedPackage.fetchSpec
|
|
66
67
|
return `${parsedPackage.name}@${versionModifier}`
|
|
67
68
|
})
|
|
@@ -71,6 +72,7 @@ class CliParser {
|
|
|
71
72
|
const options = {
|
|
72
73
|
'dry-run': { type: 'boolean' },
|
|
73
74
|
plain: { type: 'boolean' },
|
|
75
|
+
json: { type: 'boolean' },
|
|
74
76
|
packageManager: { type: 'string' },
|
|
75
77
|
pkgMgr: { type: 'string' },
|
|
76
78
|
'disable-auto-continue': { type: 'boolean' },
|
|
@@ -102,6 +104,7 @@ Commands:
|
|
|
102
104
|
Options:
|
|
103
105
|
--dry-run Run checks only, don't install
|
|
104
106
|
--plain Force non-rich text output
|
|
107
|
+
--json Emit JSON and never install
|
|
105
108
|
--packageManager Package Manager to use (default: npm)
|
|
106
109
|
--pkgMgr Alias for packageManager
|
|
107
110
|
--disable-auto-continue Disable auto-continue countdown, always prompt
|
|
@@ -129,7 +132,11 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
|
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
// Process install command and packages
|
|
132
|
-
const normalizedPackages = this._extractPackagesFromPositionals(
|
|
135
|
+
const normalizedPackages = this._extractPackagesFromPositionals(
|
|
136
|
+
positionals,
|
|
137
|
+
false,
|
|
138
|
+
values.json === true
|
|
139
|
+
)
|
|
133
140
|
const installSubcommandExplicit =
|
|
134
141
|
positionals.length > 0 && this.isInstallSubcommand(positionals[0])
|
|
135
142
|
|
|
@@ -138,6 +145,7 @@ curated by Liran Tal at https://github.com/lirantal/npq`)
|
|
|
138
145
|
packageManager: process.env.NPQ_PKG_MGR || values.packageManager || values.pkgMgr || 'npm',
|
|
139
146
|
dryRun: values['dry-run'] || false,
|
|
140
147
|
plain: values.plain || false,
|
|
148
|
+
json: values.json || false,
|
|
141
149
|
disableAutoContinue:
|
|
142
150
|
values['disable-auto-continue'] || process.env.NPQ_DISABLE_AUTO_CONTINUE === 'true',
|
|
143
151
|
registryConfigArgs: this.registryConfigArgs(values),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const AUDIT_FAILURE_CODES = Object.freeze({
|
|
4
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
5
|
+
PROJECT_MANIFEST_ERROR: 'PROJECT_MANIFEST_ERROR',
|
|
6
|
+
PACKAGE_LOOKUP_FAILED: 'PACKAGE_LOOKUP_FAILED',
|
|
7
|
+
AUDIT_CHECK_FAILED: 'AUDIT_CHECK_FAILED',
|
|
8
|
+
INTERRUPTED: 'INTERRUPTED',
|
|
9
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR'
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const knownCodes = new Set(Object.values(AUDIT_FAILURE_CODES))
|
|
13
|
+
|
|
14
|
+
function createAuditFailure(code, message, context = {}) {
|
|
15
|
+
const failure = {
|
|
16
|
+
code: knownCodes.has(code) ? code : AUDIT_FAILURE_CODES.INTERNAL_ERROR,
|
|
17
|
+
message: String(message)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (context.package) failure.package = String(context.package)
|
|
21
|
+
if (context.marshall) failure.marshall = String(context.marshall)
|
|
22
|
+
return failure
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { AUDIT_FAILURE_CODES, createAuditFailure }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
function createJsonOutput(write = (value, callback) => process.stdout.write(value, callback)) {
|
|
4
|
+
let written = false
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
write(report, onComplete) {
|
|
8
|
+
if (written) return false
|
|
9
|
+
written = true
|
|
10
|
+
const serialized = `${JSON.stringify(report)}\n`
|
|
11
|
+
if (typeof onComplete === 'function') {
|
|
12
|
+
write(serialized, onComplete)
|
|
13
|
+
} else {
|
|
14
|
+
write(serialized)
|
|
15
|
+
}
|
|
16
|
+
return true
|
|
17
|
+
},
|
|
18
|
+
hasWritten() {
|
|
19
|
+
return written
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { createJsonOutput }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const npa = require('npm-package-arg')
|
|
4
|
+
|
|
5
|
+
const JSON_REGISTRY_SPEC_TYPES = new Set(['tag', 'version', 'range'])
|
|
6
|
+
|
|
7
|
+
function parseJsonRegistryPackageSpec(packageSpec) {
|
|
8
|
+
try {
|
|
9
|
+
const parsedPackage = npa(packageSpec)
|
|
10
|
+
if (!parsedPackage.name || !JSON_REGISTRY_SPEC_TYPES.has(parsedPackage.type)) {
|
|
11
|
+
throw new Error('Unsupported package spec')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return parsedPackage
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error('Invalid JSON package input')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isJsonRegistryPackageSpec(packageSpec) {
|
|
21
|
+
try {
|
|
22
|
+
parseJsonRegistryPackageSpec(packageSpec)
|
|
23
|
+
return true
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { isJsonRegistryPackageSpec, parseJsonRegistryPackageSpec }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const pkg = require('../../package.json')
|
|
4
|
+
const { marshallCategories } = require('../marshalls/constants')
|
|
5
|
+
const { createAuditFailure } = require('./auditFailure')
|
|
6
|
+
|
|
7
|
+
function normalizeMarshallGroup(group = {}) {
|
|
8
|
+
const findings = []
|
|
9
|
+
|
|
10
|
+
for (const value of Object.values(group)) {
|
|
11
|
+
if (!value || typeof value !== 'object') continue
|
|
12
|
+
const category = marshallCategories[value.categoryId] || {
|
|
13
|
+
id: value.categoryId,
|
|
14
|
+
title: value.categoryId
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const warning of value.warnings || []) {
|
|
18
|
+
findings.push({
|
|
19
|
+
severity: 'warning',
|
|
20
|
+
marshall: value.marshall,
|
|
21
|
+
category: { id: category.id, title: category.title },
|
|
22
|
+
message: warning.message
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
for (const error of value.errors || []) {
|
|
26
|
+
findings.push({
|
|
27
|
+
severity: 'error',
|
|
28
|
+
marshall: value.marshall,
|
|
29
|
+
category: { id: category.id, title: category.title },
|
|
30
|
+
message: error.message
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return findings
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildJsonReport({
|
|
39
|
+
packages = [],
|
|
40
|
+
marshallResults = {},
|
|
41
|
+
failures = [],
|
|
42
|
+
version = pkg.version
|
|
43
|
+
} = {}) {
|
|
44
|
+
const positionalResults = Array.isArray(marshallResults)
|
|
45
|
+
const packageReports = packages.map((requested, index) => {
|
|
46
|
+
const results = positionalResults ? marshallResults[index] : marshallResults[requested]
|
|
47
|
+
return {
|
|
48
|
+
requested,
|
|
49
|
+
findings: (results || []).flatMap(normalizeMarshallGroup)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
const findings = packageReports.flatMap((entry) => entry.findings)
|
|
53
|
+
const errors = findings.filter((item) => item.severity === 'error').length
|
|
54
|
+
const warnings = findings.filter((item) => item.severity === 'warning').length
|
|
55
|
+
const status = failures.length > 0 ? 'failed' : errors + warnings > 0 ? 'findings' : 'clean'
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
schemaVersion: 1,
|
|
59
|
+
tool: { name: 'npq', version },
|
|
60
|
+
status,
|
|
61
|
+
summary: { packagesAudited: packageReports.length, errors, warnings },
|
|
62
|
+
packages: packageReports,
|
|
63
|
+
failures: failures.map((failure) =>
|
|
64
|
+
createAuditFailure(failure.code, failure.message, {
|
|
65
|
+
package: failure.package,
|
|
66
|
+
marshall: failure.marshall
|
|
67
|
+
})
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function exitCodeForJsonReport(report) {
|
|
73
|
+
return report.status === 'failed' ? 2 : report.status === 'findings' ? 1 : 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { buildJsonReport, exitCodeForJsonReport }
|
|
@@ -16,8 +16,7 @@ const CAPABILITY = Object.freeze({
|
|
|
16
16
|
const CAPABILITY_MESSAGE = Object.freeze({
|
|
17
17
|
[CAPABILITY.SIGNING_KEYS]: 'configured registry does not expose signing keys',
|
|
18
18
|
[CAPABILITY.ATTESTATIONS]: 'configured registry does not expose attestations',
|
|
19
|
-
[CAPABILITY.DOWNLOAD_COUNTS]:
|
|
20
|
-
'download counts are available only for the public npm registry'
|
|
19
|
+
[CAPABILITY.DOWNLOAD_COUNTS]: 'download counts are available only for the public npm registry'
|
|
21
20
|
})
|
|
22
21
|
|
|
23
22
|
class RegistryClient {
|
|
@@ -47,11 +46,7 @@ class RegistryClient {
|
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
async requestJson(
|
|
51
|
-
requestPath,
|
|
52
|
-
packageSpec,
|
|
53
|
-
{ capability = null, notFoundAsData = false } = {}
|
|
54
|
-
) {
|
|
49
|
+
async requestJson(requestPath, packageSpec, { capability = null, notFoundAsData = false } = {}) {
|
|
55
50
|
const registry = this.registryFor(packageSpec)
|
|
56
51
|
try {
|
|
57
52
|
return await this.fetcher.json(requestPath, this.requestOptions(packageSpec))
|
|
@@ -3,12 +3,7 @@
|
|
|
3
3
|
const path = require('node:path')
|
|
4
4
|
const Config = require('@npmcli/config')
|
|
5
5
|
const npmFetch = require('npm-registry-fetch')
|
|
6
|
-
const {
|
|
7
|
-
definitions,
|
|
8
|
-
flatten,
|
|
9
|
-
nerfDarts,
|
|
10
|
-
shorthands
|
|
11
|
-
} = require('@npmcli/config/lib/definitions')
|
|
6
|
+
const { definitions, flatten, nerfDarts, shorthands } = require('@npmcli/config/lib/definitions')
|
|
12
7
|
const { RegistryError, sanitizeRegistryUrl } = require('./registryErrors')
|
|
13
8
|
|
|
14
9
|
const DEFAULT_REGISTRY = 'https://registry.npmjs.org/'
|
|
@@ -57,8 +52,7 @@ class RegistryConfig {
|
|
|
57
52
|
}
|
|
58
53
|
return new RegistryConfig(config.flat)
|
|
59
54
|
} catch (error) {
|
|
60
|
-
const code =
|
|
61
|
-
error.code === 'ERR_INVALID_AUTH' ? 'EREGISTRYCONFIGAUTH' : 'EREGISTRYCONFIG'
|
|
55
|
+
const code = error.code === 'ERR_INVALID_AUTH' ? 'EREGISTRYCONFIGAUTH' : 'EREGISTRYCONFIG'
|
|
62
56
|
const message =
|
|
63
57
|
error.code === 'ERR_INVALID_AUTH'
|
|
64
58
|
? 'Invalid npm registry authentication configuration'
|
|
@@ -230,15 +230,9 @@ function reportResults(marshallResults, { plain = false } = {}) {
|
|
|
230
230
|
entry.categoryFriendlyName.padEnd(maxTextLength, ' ')
|
|
231
231
|
)
|
|
232
232
|
const iconRich = styleText(['cyan'], '○')
|
|
233
|
-
const wrappedMessage = wrapTextWithIndent(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
indentString
|
|
237
|
-
)
|
|
238
|
-
resultsForPrettyPrint +=
|
|
239
|
-
`\n ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${wrappedMessage} `
|
|
240
|
-
resultsForPlainTextPrint +=
|
|
241
|
-
`\n NOT EVALUATED: ${entry.categoryFriendlyName} - ${entry.message}`
|
|
233
|
+
const wrappedMessage = wrapTextWithIndent(entry.message, maxMessageWidth, indentString)
|
|
234
|
+
resultsForPrettyPrint += `\n ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${wrappedMessage} `
|
|
235
|
+
resultsForPlainTextPrint += `\n NOT EVALUATED: ${entry.categoryFriendlyName} - ${entry.message}`
|
|
242
236
|
})
|
|
243
237
|
})
|
|
244
238
|
}
|
|
@@ -256,16 +250,11 @@ function reportResults(marshallResults, { plain = false } = {}) {
|
|
|
256
250
|
terminalWidth - getDisplayLength(' │ ℹ Suggested older release · ') - 2
|
|
257
251
|
)
|
|
258
252
|
const detailWidth = Math.max(20, terminalWidth - getDisplayLength(' │ ') - 2)
|
|
259
|
-
const wrappedHeadline = wrapTextWithIndent(
|
|
260
|
-
headline,
|
|
261
|
-
headlineWidth,
|
|
262
|
-
suggestionIndent
|
|
263
|
-
)
|
|
253
|
+
const wrappedHeadline = wrapTextWithIndent(headline, headlineWidth, suggestionIndent)
|
|
264
254
|
const wrappedDetail = wrapTextWithIndent(detail, detailWidth, suggestionIndent)
|
|
265
255
|
|
|
266
256
|
resultsForPrettyPrint += `\n ${prefixRich}`
|
|
267
|
-
resultsForPrettyPrint +=
|
|
268
|
-
`\n ${prefixRich} ${iconRich} ${labelRich} ${separatorRich} ${wrappedHeadline}`
|
|
257
|
+
resultsForPrettyPrint += `\n ${prefixRich} ${iconRich} ${labelRich} ${separatorRich} ${wrappedHeadline}`
|
|
269
258
|
resultsForPrettyPrint += `\n${suggestionIndent}${wrappedDetail}`
|
|
270
259
|
|
|
271
260
|
resultsForPlainTextPrint +=
|
|
@@ -23,12 +23,7 @@ function findAlternativeVersion({
|
|
|
23
23
|
requestedVersion,
|
|
24
24
|
now = Date.now()
|
|
25
25
|
}) {
|
|
26
|
-
if (
|
|
27
|
-
!packageInfo ||
|
|
28
|
-
!packageInfo.versions ||
|
|
29
|
-
!packageInfo.time ||
|
|
30
|
-
!semver.valid(targetVersion)
|
|
31
|
-
) {
|
|
26
|
+
if (!packageInfo || !packageInfo.versions || !packageInfo.time || !semver.valid(targetVersion)) {
|
|
32
27
|
return null
|
|
33
28
|
}
|
|
34
29
|
|
|
@@ -49,9 +44,7 @@ function findAlternativeVersion({
|
|
|
49
44
|
})
|
|
50
45
|
.filter(
|
|
51
46
|
({ publishedAt, ageDays }) =>
|
|
52
|
-
typeof publishedAt === 'string' &&
|
|
53
|
-
ageDays !== null &&
|
|
54
|
-
ageDays > MAX_VERSION_RECENCY_DAYS
|
|
47
|
+
typeof publishedAt === 'string' && ageDays !== null && ageDays > MAX_VERSION_RECENCY_DAYS
|
|
55
48
|
)
|
|
56
49
|
.sort((left, right) => semver.rcompare(left.version, right.version))
|
|
57
50
|
|
package/lib/jsonAudit.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { getProjectPackages } = require('./helpers/sourcePackages')
|
|
4
|
+
const Marshall = require('./marshall')
|
|
5
|
+
const { promiseThrottleHelper } = require('./helpers/promiseThrottler')
|
|
6
|
+
const { buildJsonReport } = require('./helpers/jsonReport')
|
|
7
|
+
const { AUDIT_FAILURE_CODES, createAuditFailure } = require('./helpers/auditFailure')
|
|
8
|
+
const { isJsonRegistryPackageSpec } = require('./helpers/jsonPackageSpec')
|
|
9
|
+
const RegistryConfig = require('./helpers/registryConfig')
|
|
10
|
+
const RegistryClient = require('./helpers/registryClient')
|
|
11
|
+
|
|
12
|
+
async function runJsonAudit(cliArgs, dependencies = {}) {
|
|
13
|
+
const getPackages = dependencies.getProjectPackages || getProjectPackages
|
|
14
|
+
const MarshallClass = dependencies.Marshall || Marshall
|
|
15
|
+
const throttle = dependencies.promiseThrottleHelper || promiseThrottleHelper
|
|
16
|
+
const RegistryConfigClass = dependencies.RegistryConfig || RegistryConfig
|
|
17
|
+
const RegistryClientClass = dependencies.RegistryClient || RegistryClient
|
|
18
|
+
let packages = cliArgs.packages || []
|
|
19
|
+
const failures = []
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (packages.length === 0) {
|
|
23
|
+
const projectPackages = await getPackages()
|
|
24
|
+
if (projectPackages && projectPackages.error) {
|
|
25
|
+
return buildJsonReport({
|
|
26
|
+
failures: [
|
|
27
|
+
createAuditFailure(
|
|
28
|
+
AUDIT_FAILURE_CODES.PROJECT_MANIFEST_ERROR,
|
|
29
|
+
'Unable to read project package.json'
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
packages = projectPackages
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!packages.every(isJsonRegistryPackageSpec)) {
|
|
38
|
+
return buildJsonReport({
|
|
39
|
+
failures: [
|
|
40
|
+
createAuditFailure(
|
|
41
|
+
AUDIT_FAILURE_CODES.INVALID_INPUT,
|
|
42
|
+
'Invalid package or option argument'
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const registryConfig = await RegistryConfigClass.load({
|
|
49
|
+
argv: cliArgs.registryConfigArgs || []
|
|
50
|
+
})
|
|
51
|
+
const registryClient = new RegistryClientClass(registryConfig)
|
|
52
|
+
const marshall = new MarshallClass({
|
|
53
|
+
pkgs: packages,
|
|
54
|
+
progressManager: null,
|
|
55
|
+
promiseThrottleHelper: throttle,
|
|
56
|
+
registryClient,
|
|
57
|
+
preserveRequestOrder: true,
|
|
58
|
+
suppressOutput: true,
|
|
59
|
+
onAuditFailure: (failure) => failures.push(failure)
|
|
60
|
+
})
|
|
61
|
+
const marshallResults = await marshall.process()
|
|
62
|
+
return buildJsonReport({ packages, marshallResults, failures })
|
|
63
|
+
} catch {
|
|
64
|
+
failures.push(
|
|
65
|
+
createAuditFailure(AUDIT_FAILURE_CODES.INTERNAL_ERROR, 'JSON audit could not complete')
|
|
66
|
+
)
|
|
67
|
+
return buildJsonReport({ packages, failures })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { runJsonAudit }
|
package/lib/jsonCli.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { createJsonOutput } = require('./helpers/jsonOutput')
|
|
4
|
+
const { buildJsonReport, exitCodeForJsonReport } = require('./helpers/jsonReport')
|
|
5
|
+
const { AUDIT_FAILURE_CODES, createAuditFailure } = require('./helpers/auditFailure')
|
|
6
|
+
const { runJsonAudit } = require('./jsonAudit')
|
|
7
|
+
|
|
8
|
+
function writeInvalidJsonInvocation(output = createJsonOutput()) {
|
|
9
|
+
const report = buildJsonReport({
|
|
10
|
+
failures: [
|
|
11
|
+
createAuditFailure(AUDIT_FAILURE_CODES.INVALID_INPUT, 'Invalid package or option argument')
|
|
12
|
+
]
|
|
13
|
+
})
|
|
14
|
+
output.write(report)
|
|
15
|
+
return report
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function runJsonCli(cliArgs, dependencies = {}) {
|
|
19
|
+
const output = dependencies.output || createJsonOutput()
|
|
20
|
+
const processTarget = dependencies.processTarget || process
|
|
21
|
+
const audit = dependencies.runJsonAudit || runJsonAudit
|
|
22
|
+
const getExitCode = dependencies.exitCodeForJsonReport || exitCodeForJsonReport
|
|
23
|
+
let interrupted = false
|
|
24
|
+
let interruptedReport
|
|
25
|
+
|
|
26
|
+
const interrupt = () => {
|
|
27
|
+
interrupted = true
|
|
28
|
+
interruptedReport = buildJsonReport({
|
|
29
|
+
packages: cliArgs.packages || [],
|
|
30
|
+
failures: [createAuditFailure(AUDIT_FAILURE_CODES.INTERRUPTED, 'Audit interrupted')]
|
|
31
|
+
})
|
|
32
|
+
output.write(interruptedReport, () => {
|
|
33
|
+
processTarget.exit(2)
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
processTarget.once('SIGINT', interrupt)
|
|
38
|
+
let report
|
|
39
|
+
try {
|
|
40
|
+
report = await audit(cliArgs)
|
|
41
|
+
} catch {
|
|
42
|
+
report = buildJsonReport({
|
|
43
|
+
packages: cliArgs.packages || [],
|
|
44
|
+
failures: [
|
|
45
|
+
createAuditFailure(AUDIT_FAILURE_CODES.INTERNAL_ERROR, 'JSON audit could not complete')
|
|
46
|
+
]
|
|
47
|
+
})
|
|
48
|
+
} finally {
|
|
49
|
+
processTarget.removeListener('SIGINT', interrupt)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (interrupted) {
|
|
53
|
+
return interruptedReport
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
output.write(report)
|
|
57
|
+
processTarget.exitCode = getExitCode(report)
|
|
58
|
+
return report
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { runJsonCli, writeInvalidJsonInvocation }
|
package/lib/marshall.js
CHANGED
|
@@ -11,14 +11,15 @@ class Marshall {
|
|
|
11
11
|
options = options || {}
|
|
12
12
|
this.pkgs = options.pkgs || null
|
|
13
13
|
this.registryClient =
|
|
14
|
-
options.registryClient ||
|
|
15
|
-
options.packageRepoUtils?.registryClient ||
|
|
16
|
-
RegistryClient.public()
|
|
14
|
+
options.registryClient || options.packageRepoUtils?.registryClient || RegistryClient.public()
|
|
17
15
|
this.packageRepoUtils =
|
|
18
|
-
options.packageRepoUtils ||
|
|
19
|
-
new PackageRepoUtils({ registryClient: this.registryClient })
|
|
16
|
+
options.packageRepoUtils || new PackageRepoUtils({ registryClient: this.registryClient })
|
|
20
17
|
this.progressManager = options.progressManager || null
|
|
21
18
|
this.promiseThrottleHelper = options.promiseThrottleHelper || null
|
|
19
|
+
this.preserveRequestOrder = options.preserveRequestOrder === true
|
|
20
|
+
this.suppressOutput = options.suppressOutput === true
|
|
21
|
+
this.onAuditFailure =
|
|
22
|
+
typeof options.onAuditFailure === 'function' ? options.onAuditFailure : null
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async process() {
|
|
@@ -57,6 +58,7 @@ class Marshall {
|
|
|
57
58
|
this.progressManager.stop()
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
if (this.preserveRequestOrder) return res
|
|
60
62
|
return promiseResultsPerPackage
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -67,7 +69,10 @@ class Marshall {
|
|
|
67
69
|
packageRepoUtils: packageRepoUtils,
|
|
68
70
|
registryClient: this.registryClient
|
|
69
71
|
}
|
|
70
|
-
return Marshalls.tasks(config, this.progressManager
|
|
72
|
+
return Marshalls.tasks(config, this.progressManager, {
|
|
73
|
+
onAuditFailure: this.onAuditFailure,
|
|
74
|
+
suppressOutput: this.suppressOutput
|
|
75
|
+
})
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
createPackageVersionMaps(packages) {
|
|
@@ -9,7 +9,7 @@ const { marshallCategories } = require('./constants')
|
|
|
9
9
|
|
|
10
10
|
class BaseMarshall {
|
|
11
11
|
constructor(options) {
|
|
12
|
-
this.debug = util.debuglog('npq')
|
|
12
|
+
this.debug = options.suppressOutput ? () => {} : util.debuglog('npq')
|
|
13
13
|
this.packageRepoUtils = options.packageRepoUtils
|
|
14
14
|
this.registryClient = options.registryClient
|
|
15
15
|
this.categoryId = marshallCategories.PackageHealth.id
|
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 { AUDIT_FAILURE_CODES, createAuditFailure } = require('../helpers/auditFailure')
|
|
5
6
|
const { RegistryError } = require('../helpers/registryErrors')
|
|
6
7
|
|
|
7
8
|
class Marshalls {
|
|
@@ -22,7 +23,8 @@ class Marshalls {
|
|
|
22
23
|
|
|
23
24
|
const config = {
|
|
24
25
|
packageRepoUtils: options.packageRepoUtils,
|
|
25
|
-
registryClient: options.registryClient
|
|
26
|
+
registryClient: options.registryClient,
|
|
27
|
+
suppressOutput: options.suppressOutput === true
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const marshallTasks = marshalls.reduce((prev, curr) => {
|
|
@@ -36,6 +38,7 @@ class Marshalls {
|
|
|
36
38
|
|
|
37
39
|
return prev.concat({
|
|
38
40
|
title: marshall.title(),
|
|
41
|
+
name: marshall.name,
|
|
39
42
|
categoryId: marshall.categoryId,
|
|
40
43
|
execute: async (options) => {
|
|
41
44
|
const ctx = {
|
|
@@ -61,7 +64,11 @@ class Marshalls {
|
|
|
61
64
|
* @param {Object} [progressManager] - Optional progress manager for displaying audit progress updates
|
|
62
65
|
* @returns {Promise<Array<Object>>} Array of marshall results containing audit findings for each package
|
|
63
66
|
*/
|
|
64
|
-
static async tasks(
|
|
67
|
+
static async tasks(
|
|
68
|
+
options,
|
|
69
|
+
progressManager,
|
|
70
|
+
{ onAuditFailure = null, suppressOutput = false } = {}
|
|
71
|
+
) {
|
|
65
72
|
const marshallResults = []
|
|
66
73
|
|
|
67
74
|
// @TODO handle throw error because fetch didn't work due to network issues
|
|
@@ -78,7 +85,23 @@ class Marshalls {
|
|
|
78
85
|
// hostname: 'registry.npmjs.org'
|
|
79
86
|
// }
|
|
80
87
|
// }
|
|
81
|
-
|
|
88
|
+
let packagesDataList
|
|
89
|
+
try {
|
|
90
|
+
packagesDataList = await Marshalls.warmUpPackagesCache(options)
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (!onAuditFailure) throw error
|
|
93
|
+
|
|
94
|
+
for (const pkg of options.pkgs) {
|
|
95
|
+
onAuditFailure(
|
|
96
|
+
createAuditFailure(
|
|
97
|
+
AUDIT_FAILURE_CODES.PACKAGE_LOOKUP_FAILED,
|
|
98
|
+
'Unable to retrieve package metadata',
|
|
99
|
+
{ package: pkg.packageString }
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
return marshallResults
|
|
104
|
+
}
|
|
82
105
|
|
|
83
106
|
// handle error in case we get just one package and it is not found
|
|
84
107
|
// if (packagesDataList && packagesDataList.length === 1) {
|
|
@@ -123,7 +146,8 @@ class Marshalls {
|
|
|
123
146
|
if (packagesToProcess.length > 0) {
|
|
124
147
|
const marshallTasks = await Marshalls.buildMarshallTasks(marshalls, {
|
|
125
148
|
packageRepoUtils: options.packageRepoUtils,
|
|
126
|
-
registryClient: options.registryClient
|
|
149
|
+
registryClient: options.registryClient,
|
|
150
|
+
suppressOutput
|
|
127
151
|
})
|
|
128
152
|
|
|
129
153
|
for (const marshall of marshallTasks) {
|
|
@@ -134,10 +158,23 @@ class Marshalls {
|
|
|
134
158
|
})
|
|
135
159
|
marshallResults.push(res.marshalls)
|
|
136
160
|
} catch (error) {
|
|
137
|
-
if (
|
|
138
|
-
|
|
161
|
+
if (!onAuditFailure) {
|
|
162
|
+
if (error instanceof RegistryError) {
|
|
163
|
+
throw error
|
|
164
|
+
}
|
|
165
|
+
console.error(`Error running task ${marshall.title}:`, error)
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const pkg of options.pkgs) {
|
|
170
|
+
onAuditFailure(
|
|
171
|
+
createAuditFailure(
|
|
172
|
+
AUDIT_FAILURE_CODES.AUDIT_CHECK_FAILED,
|
|
173
|
+
'Audit check could not complete',
|
|
174
|
+
{ package: pkg.packageString, marshall: marshall.name }
|
|
175
|
+
)
|
|
176
|
+
)
|
|
139
177
|
}
|
|
140
|
-
console.error(`Error running task ${marshall.title}:`, error)
|
|
141
178
|
}
|
|
142
179
|
}
|
|
143
180
|
}
|
|
@@ -50,10 +50,7 @@ class Marshall extends BaseMarshall {
|
|
|
50
50
|
if (error instanceof NotEvaluated || error instanceof RegistryError) {
|
|
51
51
|
throw error
|
|
52
52
|
}
|
|
53
|
-
if (
|
|
54
|
-
error.message &&
|
|
55
|
-
error.message.includes('but the corresponding public key has expired')
|
|
56
|
-
) {
|
|
53
|
+
if (error.message && error.message.includes('but the corresponding public key has expired')) {
|
|
57
54
|
throw new Warning('Package is signed with an expired key')
|
|
58
55
|
}
|
|
59
56
|
throw new Warning(`Unable to verify package signature on registry: ${error.message}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "npq",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.24.0",
|
|
4
4
|
"description": "marshall your npm/npm package installs with high quality and class 🎖",
|
|
5
5
|
"bin": {
|
|
6
6
|
"npq": "./bin/npq.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"bin/",
|
|
28
28
|
"data/",
|
|
29
29
|
"lib/",
|
|
30
|
+
"schema/",
|
|
30
31
|
"scripts/"
|
|
31
32
|
],
|
|
32
33
|
"author": {
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@eslint/js": "^9.32.0",
|
|
56
|
+
"ajv": "^8.20.0",
|
|
55
57
|
"eslint": "^9.32.0",
|
|
56
58
|
"eslint-plugin-n": "^17.21.3",
|
|
57
59
|
"eslint-plugin-security": "^3.0.1",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/lirantal/npq/main/schema/npq-output-v1.schema.json",
|
|
4
|
+
"title": "npq JSON audit output v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "tool", "status", "summary", "packages", "failures"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": 1 },
|
|
10
|
+
"tool": { "$ref": "#/$defs/tool" },
|
|
11
|
+
"status": { "enum": ["clean", "findings", "failed"] },
|
|
12
|
+
"summary": { "$ref": "#/$defs/summary" },
|
|
13
|
+
"packages": { "type": "array", "items": { "$ref": "#/$defs/package" } },
|
|
14
|
+
"failures": { "type": "array", "items": { "$ref": "#/$defs/failure" } }
|
|
15
|
+
},
|
|
16
|
+
"$defs": {
|
|
17
|
+
"tool": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": ["name", "version"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"name": { "const": "npq" },
|
|
23
|
+
"version": { "type": "string", "minLength": 1 }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"summary": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": false,
|
|
29
|
+
"required": ["packagesAudited", "errors", "warnings"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"packagesAudited": { "type": "integer", "minimum": 0 },
|
|
32
|
+
"errors": { "type": "integer", "minimum": 0 },
|
|
33
|
+
"warnings": { "type": "integer", "minimum": 0 }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"package": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"required": ["requested", "findings"],
|
|
40
|
+
"properties": {
|
|
41
|
+
"requested": { "type": "string", "minLength": 1 },
|
|
42
|
+
"findings": { "type": "array", "items": { "$ref": "#/$defs/finding" } }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"finding": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"required": ["severity", "marshall", "category", "message"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"severity": { "enum": ["warning", "error"] },
|
|
51
|
+
"marshall": { "type": "string", "minLength": 1 },
|
|
52
|
+
"category": { "$ref": "#/$defs/category" },
|
|
53
|
+
"message": { "type": "string" }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"category": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": false,
|
|
59
|
+
"required": ["id", "title"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"id": { "type": "string", "minLength": 1 },
|
|
62
|
+
"title": { "type": "string", "minLength": 1 }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"failure": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"additionalProperties": false,
|
|
68
|
+
"required": ["code", "message"],
|
|
69
|
+
"properties": {
|
|
70
|
+
"code": {
|
|
71
|
+
"enum": [
|
|
72
|
+
"INVALID_INPUT",
|
|
73
|
+
"PROJECT_MANIFEST_ERROR",
|
|
74
|
+
"PACKAGE_LOOKUP_FAILED",
|
|
75
|
+
"AUDIT_CHECK_FAILED",
|
|
76
|
+
"INTERRUPTED",
|
|
77
|
+
"INTERNAL_ERROR"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"message": { "type": "string", "minLength": 1 },
|
|
81
|
+
"package": { "type": "string", "minLength": 1 },
|
|
82
|
+
"marshall": { "type": "string", "minLength": 1 }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|