npq 3.8.0 → 3.9.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/.github/npq.mov +0 -0
- package/.github/npq.mp4 +0 -0
- package/.github/npq.png +0 -0
- package/.github/workflows/main.yml +2 -2
- package/README.md +18 -9
- package/__tests__/__fixtures__/test.marshall.js +5 -6
- package/__tests__/cli.test.js +110 -0
- package/__tests__/cliPrompt.test.js +409 -0
- package/__tests__/marshalls.base.test.js +1 -1
- package/__tests__/marshalls.classMethods.test.js +11 -14
- package/__tests__/marshalls.expiredDomains.test.js +2 -2
- package/__tests__/marshalls.provenance.test.js +181 -4
- package/__tests__/marshalls.repo.test.js +6 -6
- package/__tests__/marshalls.signatures.test.js +0 -4
- package/__tests__/marshalls.snyk.test.js +294 -0
- package/__tests__/marshalls.tasks.test.js +48 -24
- package/__tests__/marshalls.typosquatting.test.js +16 -9
- package/__tests__/marshalls.version-maturity.test.js +6 -6
- package/__tests__/packageRepoUtils.test.js +4 -4
- package/__tests__/reportResults.test.js +772 -0
- package/__tests__/scripts.test.js +2 -2
- package/bin/npq-hero.js +70 -20
- package/bin/npq.js +113 -21
- package/lib/cli.js +127 -24
- package/lib/helpers/cliPrompt.js +97 -0
- package/lib/helpers/cliSpinner.js +104 -0
- package/lib/helpers/cliSupportHandler.js +50 -7
- package/lib/helpers/packageRepoUtils.js +7 -2
- package/lib/helpers/promiseThrottler.js +96 -0
- package/lib/helpers/reportResults.js +304 -0
- package/lib/helpers/sourcePackages.js +36 -0
- package/lib/marshall.js +42 -65
- package/lib/marshalls/age.marshall.js +4 -3
- package/lib/marshalls/author.marshall.js +38 -9
- package/lib/marshalls/baseMarshall.js +9 -17
- package/lib/marshalls/deprecation.marshall.js +43 -0
- package/lib/marshalls/downloads.marshall.js +3 -3
- package/lib/marshalls/expiredDomains.marshall.js +7 -5
- package/lib/marshalls/index.js +110 -80
- package/lib/marshalls/license.marshall.js +3 -3
- package/lib/marshalls/newbin.marshall.js +1 -2
- package/lib/marshalls/provenance.marshall.js +152 -33
- package/lib/marshalls/repo.marshall.js +8 -7
- package/lib/marshalls/scripts.marshall.js +1 -1
- package/lib/marshalls/signatures.marshall.js +26 -2
- package/lib/marshalls/snyk.marshall.js +61 -15
- package/lib/marshalls/typosquatting.marshall.js +8 -5
- package/lib/marshalls/version-maturity.marshall.js +4 -4
- package/lib/marshallsDecomissioned/readme.marshall.js +2 -2
- package/package.json +16 -19
- package/scripts/postinstall.js +10 -11
- package/lib/cliCommons.js +0 -80
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const { styleText } = require('node:util')
|
|
4
|
+
|
|
4
5
|
// eslint-disable-next-line security/detect-child-process
|
|
5
6
|
const childProcess = require('child_process')
|
|
6
7
|
const semver = require('semver')
|
|
@@ -9,17 +10,22 @@ const DEFAULT_PKGMGR = process.env.NPQ_PKG_MGR || 'npm'
|
|
|
9
10
|
|
|
10
11
|
const nodeVersion = process.versions.node
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
if (!semver.satisfies(nodeVersion, '>=
|
|
13
|
+
function isEnvSupport() {
|
|
14
|
+
if (!semver.satisfies(nodeVersion, '>=20.13.0')) {
|
|
14
15
|
return false
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
return true
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
function noSupportError(failFast) {
|
|
22
|
+
if (isInteractiveTerminal()) {
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.error(styleText('red', 'error:'), 'npq suppressed due to old node version')
|
|
25
|
+
} else {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.error('error: npq suppressed due to old node version')
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
if (failFast === true) {
|
|
25
31
|
process.exit(-1)
|
|
@@ -28,7 +34,7 @@ module.exports.noSupportError = function (failFast) {
|
|
|
28
34
|
return true
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
function packageManagerPassthrough() {
|
|
32
38
|
const result = childProcess.spawnSync(DEFAULT_PKGMGR, process.argv.slice(2), {
|
|
33
39
|
stdio: 'inherit',
|
|
34
40
|
shell: true
|
|
@@ -36,3 +42,40 @@ module.exports.packageManagerPassthrough = function () {
|
|
|
36
42
|
|
|
37
43
|
process.exit(result.status)
|
|
38
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if we're running in an interactive terminal environment
|
|
48
|
+
* @returns {boolean} True if interactive, false if in CI/build environment
|
|
49
|
+
*/
|
|
50
|
+
function isInteractiveTerminal() {
|
|
51
|
+
// Check common CI environment variables
|
|
52
|
+
const ciEnvVars = [
|
|
53
|
+
'CI',
|
|
54
|
+
'CONTINUOUS_INTEGRATION',
|
|
55
|
+
'BUILD_NUMBER',
|
|
56
|
+
'RUN_ID',
|
|
57
|
+
'GITHUB_ACTIONS',
|
|
58
|
+
'GITLAB_CI',
|
|
59
|
+
'TRAVIS',
|
|
60
|
+
'CIRCLECI',
|
|
61
|
+
'JENKINS_URL',
|
|
62
|
+
'TEAMCITY_VERSION',
|
|
63
|
+
'TF_BUILD'
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
// If any CI environment variable is set, we're likely in a CI environment
|
|
67
|
+
const isCI = ciEnvVars.some((envVar) => process.env[envVar])
|
|
68
|
+
|
|
69
|
+
// Check if stdout is a TTY (terminal)
|
|
70
|
+
const isTTY = Boolean(process.stdout.isTTY)
|
|
71
|
+
|
|
72
|
+
// We're interactive if we're in a TTY and not in CI
|
|
73
|
+
return isTTY && !isCI
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
isEnvSupport,
|
|
78
|
+
noSupportError,
|
|
79
|
+
packageManagerPassthrough,
|
|
80
|
+
isInteractiveTerminal
|
|
81
|
+
}
|
|
@@ -52,6 +52,11 @@ class PackageRepoUtils {
|
|
|
52
52
|
return semver.coerce(version)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
async isPackageInAllowList(packageName) {
|
|
56
|
+
const allowList = ['npq', 'ai', 'bun', 'deno']
|
|
57
|
+
return allowList.includes(packageName)
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
async getSemVer(packageName, packageVersion) {
|
|
56
61
|
if (semver.valid(packageVersion)) {
|
|
57
62
|
return packageVersion
|
|
@@ -61,7 +66,7 @@ class PackageRepoUtils {
|
|
|
61
66
|
const packageInfo = await this.getPackageInfo(packageName)
|
|
62
67
|
|
|
63
68
|
if (packageInfo['dist-tags'] === undefined) {
|
|
64
|
-
throw new Error(`
|
|
69
|
+
throw new Error(`Could not find dist-tags for package ${packageName}`)
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
if (packageInfo['dist-tags'][packageVersion] !== undefined) {
|
|
@@ -85,7 +90,7 @@ class PackageRepoUtils {
|
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
throw new Error(`
|
|
93
|
+
throw new Error(`Could not find dist-tag ${packageVersion} for package ${packageName}`)
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Example use-case
|
|
2
|
+
//
|
|
3
|
+
// Using the new throttling mechanism - each audit will be throttled globally
|
|
4
|
+
// const throttledAudits = pkgs.map(pkg =>
|
|
5
|
+
// promiseThrottle(() => auditPackage(pkg), 1, 1000) // max 1 concurrent, 1 second delay
|
|
6
|
+
// )
|
|
7
|
+
// const res = await Promise.all(throttledAudits)
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A function that throttles promises to avoid overwhelming remote APIs
|
|
11
|
+
* rate limits. Uses a global throttling mechanism that works across
|
|
12
|
+
* multiple concurrent calls.
|
|
13
|
+
*
|
|
14
|
+
* @param {Function} promiseFunction - Function that returns a promise
|
|
15
|
+
* @param {number} maxConcurrent - Maximum concurrent promises (global setting)
|
|
16
|
+
* @param {number} delay - Minimum delay between requests in milliseconds
|
|
17
|
+
*/
|
|
18
|
+
async function promiseThrottleHelper(promiseFunction, maxConcurrent = 5, delay = 0) {
|
|
19
|
+
const throttler = PromiseThrottler.getInstance()
|
|
20
|
+
|
|
21
|
+
// Configure global throttling settings
|
|
22
|
+
throttler.configure(maxConcurrent, delay)
|
|
23
|
+
|
|
24
|
+
// Use the global throttler
|
|
25
|
+
return throttler.throttle(promiseFunction)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A throttling class that limits concurrent promise execution across all instances
|
|
30
|
+
* to avoid overwhelming remote APIs with rate limits.
|
|
31
|
+
*/
|
|
32
|
+
class PromiseThrottler {
|
|
33
|
+
constructor() {
|
|
34
|
+
if (!PromiseThrottler.instance) {
|
|
35
|
+
this.runningCount = 0
|
|
36
|
+
this.queue = []
|
|
37
|
+
this.maxConcurrent = 5
|
|
38
|
+
this.minDelay = 0
|
|
39
|
+
PromiseThrottler.instance = this
|
|
40
|
+
}
|
|
41
|
+
return PromiseThrottler.instance
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static getInstance() {
|
|
45
|
+
if (!PromiseThrottler.instance) {
|
|
46
|
+
new PromiseThrottler()
|
|
47
|
+
}
|
|
48
|
+
return PromiseThrottler.instance
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
configure(maxConcurrent = 5, minDelay = 0) {
|
|
52
|
+
this.maxConcurrent = maxConcurrent
|
|
53
|
+
this.minDelay = minDelay
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async processQueue() {
|
|
57
|
+
while (this.queue.length > 0 && this.runningCount < this.maxConcurrent) {
|
|
58
|
+
const { promiseFunction, resolve, reject } = this.queue.shift()
|
|
59
|
+
this.runningCount++
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const startTime = Date.now()
|
|
63
|
+
const result = await promiseFunction()
|
|
64
|
+
|
|
65
|
+
// Ensure minimum delay between requests
|
|
66
|
+
if (this.minDelay > 0) {
|
|
67
|
+
const elapsed = Date.now() - startTime
|
|
68
|
+
const remainingDelay = this.minDelay - elapsed
|
|
69
|
+
if (remainingDelay > 0) {
|
|
70
|
+
await new Promise((r) => setTimeout(r, remainingDelay))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
resolve(result)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
reject(error)
|
|
77
|
+
} finally {
|
|
78
|
+
this.runningCount--
|
|
79
|
+
// Process next items in queue
|
|
80
|
+
setImmediate(() => this.processQueue())
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async throttle(promiseFunction) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
this.queue.push({ promiseFunction, resolve, reject })
|
|
88
|
+
this.processQueue()
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
promiseThrottleHelper,
|
|
95
|
+
PromiseThrottler
|
|
96
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
const { styleText } = require('node:util')
|
|
2
|
+
const { marshallCategories } = require('../marshalls/constants')
|
|
3
|
+
const { isInteractiveTerminal } = require('./cliSupportHandler')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get terminal width, with fallback to a reasonable default
|
|
7
|
+
* @returns {number} Terminal width in columns
|
|
8
|
+
*/
|
|
9
|
+
function getTerminalWidth() {
|
|
10
|
+
try {
|
|
11
|
+
if (process.stdout.isTTY && process.stdout.getWindowSize) {
|
|
12
|
+
const [columns] = process.stdout.getWindowSize()
|
|
13
|
+
return columns
|
|
14
|
+
}
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// Fallback if terminal size detection fails
|
|
17
|
+
}
|
|
18
|
+
return 80 // Default fallback width
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strip ANSI escape codes from a string to get its actual display length
|
|
23
|
+
* @param {string} str - String that may contain ANSI codes
|
|
24
|
+
* @returns {number} Actual display length
|
|
25
|
+
*/
|
|
26
|
+
function getDisplayLength(str) {
|
|
27
|
+
// Remove ANSI escape sequences to get actual character count
|
|
28
|
+
return str.replace(/\u001b\[[0-9;]*m/g, '').length
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Wrap text to fit within terminal width while maintaining indentation
|
|
33
|
+
* @param {string} text - Text to wrap
|
|
34
|
+
* @param {number} maxWidth - Maximum width for each line
|
|
35
|
+
* @param {string} indent - Indentation string for continuation lines
|
|
36
|
+
* @returns {string} Wrapped text with proper indentation
|
|
37
|
+
*/
|
|
38
|
+
function wrapTextWithIndent(text, maxWidth, indent) {
|
|
39
|
+
if (getDisplayLength(text) <= maxWidth) {
|
|
40
|
+
return text
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const words = text.split(' ')
|
|
44
|
+
const lines = []
|
|
45
|
+
let currentLine = ''
|
|
46
|
+
|
|
47
|
+
for (const word of words) {
|
|
48
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word
|
|
49
|
+
|
|
50
|
+
if (getDisplayLength(testLine) <= maxWidth) {
|
|
51
|
+
currentLine = testLine
|
|
52
|
+
} else {
|
|
53
|
+
if (currentLine) {
|
|
54
|
+
lines.push(currentLine)
|
|
55
|
+
currentLine = word
|
|
56
|
+
} else {
|
|
57
|
+
// Single word is longer than maxWidth, break it
|
|
58
|
+
lines.push(word)
|
|
59
|
+
currentLine = ''
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (currentLine) {
|
|
65
|
+
lines.push(currentLine)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Join lines with proper indentation
|
|
69
|
+
return lines
|
|
70
|
+
.map((line, index) => {
|
|
71
|
+
return index === 0 ? line : `\n${indent}${line}`
|
|
72
|
+
})
|
|
73
|
+
.join('')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Report scan results to the console with colored formatting
|
|
78
|
+
* @param {Array} marshallResults - Array of package scan results
|
|
79
|
+
* @param {string} results[].pkg - Package name
|
|
80
|
+
* @param {Array<Array>} results[].errors - Array of error arrays, each containing error objects with pkg and message
|
|
81
|
+
* @param {Array<Array>} results[].warnings - Array of warning arrays, each containing warning objects with pkg and message
|
|
82
|
+
*/
|
|
83
|
+
function reportResults(marshallResults, { plain = false } = {}) {
|
|
84
|
+
// Convert results to issues per package
|
|
85
|
+
const results = marshallResultsToIssuesPerPackage(marshallResults)
|
|
86
|
+
|
|
87
|
+
let resultsForPrettyPrint = ''
|
|
88
|
+
let resultsForJSON = results
|
|
89
|
+
let resultsForPlainTextPrint = ''
|
|
90
|
+
|
|
91
|
+
let summaryForPrettyPrint = ''
|
|
92
|
+
let summaryForPlainTextPrint = ''
|
|
93
|
+
// @TODO: add support for JSON output
|
|
94
|
+
// let summaryForJSON =
|
|
95
|
+
|
|
96
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if we should use rich formatting
|
|
101
|
+
const useRichFormatting = isInteractiveTerminal() && !plain
|
|
102
|
+
|
|
103
|
+
// Get terminal width for text wrapping
|
|
104
|
+
const terminalWidth = getTerminalWidth()
|
|
105
|
+
|
|
106
|
+
let countErrors = 0
|
|
107
|
+
let countWarnings = 0
|
|
108
|
+
let countPackages = results.length
|
|
109
|
+
|
|
110
|
+
results.forEach((result, index) => {
|
|
111
|
+
// Add spacing between packages (except for the first one)
|
|
112
|
+
if (index > 0) {
|
|
113
|
+
resultsForPrettyPrint += ''
|
|
114
|
+
resultsForPlainTextPrint += '\n'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Rich formatting version
|
|
118
|
+
const packageNameRich = styleText(['bold'], result.pkg)
|
|
119
|
+
const prefixRich = styleText(['gray'], `${'│'}`)
|
|
120
|
+
const promptRich = styleText(['gray', 'dim'], `${'>'} `)
|
|
121
|
+
const separatorRich = styleText(['dim'], `${'·'}`)
|
|
122
|
+
|
|
123
|
+
resultsForPrettyPrint += `\n ${styleText(['dim'], '┌─')}`
|
|
124
|
+
resultsForPrettyPrint += `\n ${prefixRich} ${promptRich} ${packageNameRich}`
|
|
125
|
+
resultsForPrettyPrint += `\n ${prefixRich}`
|
|
126
|
+
|
|
127
|
+
// Plain text version
|
|
128
|
+
resultsForPlainTextPrint += `\n--- Package: ${result.pkg} ---`
|
|
129
|
+
|
|
130
|
+
// TBD later change to actually calc'ing it instead of hardcoding
|
|
131
|
+
const maxTextLength = 'Supply Chain Security'.length
|
|
132
|
+
|
|
133
|
+
// Calculate the prefix length for indentation (without ANSI codes)
|
|
134
|
+
// Format: " │ ✖ Supply Chain Security · "
|
|
135
|
+
const basePrefix = ` │ ✖ `
|
|
136
|
+
const topicPadding = maxTextLength + 0 // +1 for space after topic
|
|
137
|
+
const separatorLength = 3 // ` · `
|
|
138
|
+
const prefixLength = basePrefix.length + topicPadding + separatorLength
|
|
139
|
+
const maxMessageWidth = terminalWidth - prefixLength - 2 // -2 for safety margin
|
|
140
|
+
|
|
141
|
+
// Create indentation string for wrapped lines - include the │ prefix and align with message start
|
|
142
|
+
const indentString = ` ${styleText(['gray'], '│')} ${' '.repeat(prefixLength - 3)}`
|
|
143
|
+
|
|
144
|
+
let isPackageMalicious = null
|
|
145
|
+
|
|
146
|
+
if (result.errors && result.errors.length > 0) {
|
|
147
|
+
for (const errorArray of result.errors) {
|
|
148
|
+
isPackageMalicious = errorArray.find((error) => {
|
|
149
|
+
return error.message.includes('Malicious package found')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (isPackageMalicious) {
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isPackageMalicious) {
|
|
158
|
+
countErrors = 1
|
|
159
|
+
|
|
160
|
+
// Rich formatting version
|
|
161
|
+
const topicRich = styleText(
|
|
162
|
+
['red', 'bold'],
|
|
163
|
+
isPackageMalicious.categoryFriendlyName.padEnd(maxTextLength, ' ')
|
|
164
|
+
)
|
|
165
|
+
const iconRich = styleText(['red', 'bold'], `✖`)
|
|
166
|
+
const errorLineRich = ` ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${isPackageMalicious.message} `
|
|
167
|
+
resultsForPrettyPrint += `\n${errorLineRich}`
|
|
168
|
+
|
|
169
|
+
// Plain text version
|
|
170
|
+
resultsForPlainTextPrint += `\n ERROR: ${isPackageMalicious.categoryFriendlyName} - ${isPackageMalicious.message}`
|
|
171
|
+
} else {
|
|
172
|
+
for (const errorArray of result.errors) {
|
|
173
|
+
countErrors += errorArray.length
|
|
174
|
+
// Each error is actually an array of error objects
|
|
175
|
+
errorArray.forEach((error) => {
|
|
176
|
+
const messageText = error.message
|
|
177
|
+
|
|
178
|
+
// Rich formatting version
|
|
179
|
+
const topicRich = styleText(
|
|
180
|
+
['red', 'bold'],
|
|
181
|
+
error.categoryFriendlyName.padEnd(maxTextLength, ' ')
|
|
182
|
+
)
|
|
183
|
+
const iconRich = styleText(['red', 'bold'], `✖`)
|
|
184
|
+
const wrappedMessage = wrapTextWithIndent(messageText, maxMessageWidth, indentString)
|
|
185
|
+
const errorLineRich = ` ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${wrappedMessage} `
|
|
186
|
+
resultsForPrettyPrint += `\n${errorLineRich}`
|
|
187
|
+
|
|
188
|
+
// Plain text version
|
|
189
|
+
resultsForPlainTextPrint += `\n ERROR: ${error.categoryFriendlyName} - ${messageText}`
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Display warnings with yellow color and ⚠ symbol
|
|
196
|
+
if (!isPackageMalicious && result.warnings && result.warnings.length > 0) {
|
|
197
|
+
result.warnings.forEach((warningArray) => {
|
|
198
|
+
countWarnings += warningArray.length
|
|
199
|
+
// Each warning is actually an array of warning objects
|
|
200
|
+
warningArray.forEach((warning) => {
|
|
201
|
+
const messageText = warning.message
|
|
202
|
+
|
|
203
|
+
// Rich formatting version
|
|
204
|
+
const topicRich = styleText(
|
|
205
|
+
['yellow'],
|
|
206
|
+
warning.categoryFriendlyName.padEnd(maxTextLength, ' ')
|
|
207
|
+
)
|
|
208
|
+
const iconRich = styleText(['yellow', 'bold'], `⚠`)
|
|
209
|
+
const wrappedMessage = wrapTextWithIndent(messageText, maxMessageWidth, indentString)
|
|
210
|
+
const warningLineRich = ` ${prefixRich} ${iconRich} ${topicRich} ${separatorRich} ${wrappedMessage} `
|
|
211
|
+
resultsForPrettyPrint += `\n${warningLineRich}`
|
|
212
|
+
|
|
213
|
+
// Plain text version
|
|
214
|
+
resultsForPlainTextPrint += `\n WARNING: ${warning.categoryFriendlyName} - ${messageText}`
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Close rich formatting box
|
|
220
|
+
resultsForPrettyPrint += `\n ${styleText(['dim'], '└─')}`
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const maxTextLength = 'Total packages:'.length
|
|
224
|
+
|
|
225
|
+
// Rich formatting summary
|
|
226
|
+
summaryForPrettyPrint += `\n\n${styleText(['bold'], 'Summary:')}\n`
|
|
227
|
+
summaryForPrettyPrint += `\n - ${'Total packages:'.padEnd(maxTextLength, ' ')} ${styleText(
|
|
228
|
+
['bold'],
|
|
229
|
+
String(countPackages)
|
|
230
|
+
)}`
|
|
231
|
+
summaryForPrettyPrint += `\n - ${'Total errors:'.padEnd(maxTextLength, ' ')} ${styleText(
|
|
232
|
+
['red', 'bold'],
|
|
233
|
+
String(countErrors)
|
|
234
|
+
)}`
|
|
235
|
+
summaryForPrettyPrint += `\n - ${'Total warnings:'.padEnd(maxTextLength, ' ')} ${styleText(
|
|
236
|
+
['yellow', 'bold'],
|
|
237
|
+
String(countWarnings)
|
|
238
|
+
)}`
|
|
239
|
+
summaryForPrettyPrint += `\n`
|
|
240
|
+
|
|
241
|
+
// Plain text summary
|
|
242
|
+
summaryForPlainTextPrint += `\n\nSummary:\n`
|
|
243
|
+
summaryForPlainTextPrint += `\n - Total packages: ${countPackages}`
|
|
244
|
+
summaryForPlainTextPrint += `\n - Total errors: ${countErrors}`
|
|
245
|
+
summaryForPlainTextPrint += `\n - Total warnings: ${countWarnings}`
|
|
246
|
+
summaryForPlainTextPrint += `\n`
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
countErrors,
|
|
250
|
+
countWarnings,
|
|
251
|
+
resultsForPrettyPrint,
|
|
252
|
+
resultsForJSON,
|
|
253
|
+
resultsForPlainTextPrint,
|
|
254
|
+
summaryForPrettyPrint,
|
|
255
|
+
summaryForPlainTextPrint,
|
|
256
|
+
useRichFormatting
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function marshallResultsToIssuesPerPackage(results) {
|
|
261
|
+
const issuesPerPackage = []
|
|
262
|
+
for (const pkg in results) {
|
|
263
|
+
const allMarshallResultsList = results[pkg]
|
|
264
|
+
const issues = {
|
|
265
|
+
pkg,
|
|
266
|
+
errors: [],
|
|
267
|
+
warnings: []
|
|
268
|
+
}
|
|
269
|
+
for (const marshall of allMarshallResultsList) {
|
|
270
|
+
for (const [key, value] of Object.entries(marshall)) {
|
|
271
|
+
if (value.errors && value.errors.length > 0) {
|
|
272
|
+
const marshallErrors = value.errors.map((error) => {
|
|
273
|
+
return {
|
|
274
|
+
marshall: value.marshall,
|
|
275
|
+
categoryId: value.categoryId,
|
|
276
|
+
categoryFriendlyName: marshallCategories[value.categoryId].title,
|
|
277
|
+
...error
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
issues.errors.push(marshallErrors)
|
|
281
|
+
}
|
|
282
|
+
if (value.warnings && value.warnings.length > 0) {
|
|
283
|
+
const marshallWarnings = value.warnings.map((warning) => {
|
|
284
|
+
return {
|
|
285
|
+
marshall: value.marshall,
|
|
286
|
+
categoryId: value.categoryId,
|
|
287
|
+
categoryFriendlyName: marshallCategories[value.categoryId].title,
|
|
288
|
+
...warning
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
issues.warnings.push(marshallWarnings)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (issues.errors.length > 0 || issues.warnings.length > 0) {
|
|
297
|
+
issuesPerPackage.push(issues)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return issuesPerPackage
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = { reportResults, marshallResultsToIssuesPerPackage }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const path = require('node:path')
|
|
2
|
+
const fs = require('node:fs/promises')
|
|
3
|
+
|
|
4
|
+
async function getProjectPackages() {
|
|
5
|
+
const currentDirectory = process.cwd()
|
|
6
|
+
|
|
7
|
+
const packageJSONPath = path.join(currentDirectory, 'package.json')
|
|
8
|
+
try {
|
|
9
|
+
let packages = []
|
|
10
|
+
const packageJSON = await fs.readFile(packageJSONPath, 'utf-8')
|
|
11
|
+
const packageData = JSON.parse(packageJSON)
|
|
12
|
+
if (packageData && packageData.dependencies) {
|
|
13
|
+
packages = Object.keys(packageData.dependencies).map((dep) => {
|
|
14
|
+
return `${dep}@${packageData.dependencies[dep]}`
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return packages
|
|
19
|
+
} catch (error) {
|
|
20
|
+
let errorMessage = ''
|
|
21
|
+
if (error.code === 'ENOENT') {
|
|
22
|
+
errorMessage = `No package.json found in ${currentDirectory}`
|
|
23
|
+
} else {
|
|
24
|
+
errorMessage = `Error reading package.json: ${error.message}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
error: true,
|
|
29
|
+
message: errorMessage
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
getProjectPackages
|
|
36
|
+
}
|
package/lib/marshall.js
CHANGED
|
@@ -1,36 +1,65 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
4
|
+
// const util = require('node:util')
|
|
6
5
|
const Marshalls = require('./marshalls')
|
|
7
6
|
const PackageRepoUtils = require('./helpers/packageRepoUtils')
|
|
8
|
-
|
|
9
|
-
const MESSAGE_TYPE = {
|
|
10
|
-
ERROR: 0,
|
|
11
|
-
WARNING: 1
|
|
12
|
-
}
|
|
7
|
+
// const { marshallCategories } = require('./marshalls/constants')
|
|
13
8
|
|
|
14
9
|
class Marshall {
|
|
15
10
|
constructor(options = {}) {
|
|
16
11
|
this.pkgs = options ? options.pkgs : null
|
|
17
12
|
this.packageRepoUtils = new PackageRepoUtils()
|
|
13
|
+
this.progressManager = options.progressManager || null
|
|
14
|
+
this.promiseThrottleHelper = options.promiseThrottleHelper || null
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
process() {
|
|
17
|
+
async process() {
|
|
21
18
|
// nothing to do? move on
|
|
22
19
|
if (!this.pkgs) {
|
|
23
20
|
return Promise.resolve()
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
packageRepoUtils: this.packageRepoUtils
|
|
23
|
+
if (this.progressManager) {
|
|
24
|
+
this.progressManager.update('Analyzing...')
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
const promises = this.pkgs.map((pkg) => {
|
|
28
|
+
if (!this.promiseThrottleHelper) {
|
|
29
|
+
return this.createPackageAuditFunction(pkg, this.packageRepoUtils)
|
|
30
|
+
} else {
|
|
31
|
+
// use the promise throttler to limit concurrency
|
|
32
|
+
return this.promiseThrottleHelper(
|
|
33
|
+
() => {
|
|
34
|
+
return this.createPackageAuditFunction(pkg, this.packageRepoUtils)
|
|
35
|
+
},
|
|
36
|
+
1,
|
|
37
|
+
50
|
|
38
|
+
) // max 1 concurrent, 0.1 second delay
|
|
39
|
+
}
|
|
33
40
|
})
|
|
41
|
+
const res = await Promise.all(promises)
|
|
42
|
+
|
|
43
|
+
// match pkgs array with results of promises
|
|
44
|
+
const promiseResultsPerPackage = this.pkgs.reduce((acc, pkg, index) => {
|
|
45
|
+
acc[pkg] = res[index]
|
|
46
|
+
return acc
|
|
47
|
+
}, {})
|
|
48
|
+
|
|
49
|
+
if (this.progressManager) {
|
|
50
|
+
this.progressManager.stop()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return promiseResultsPerPackage
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async createPackageAuditFunction(pkg, packageRepoUtils) {
|
|
57
|
+
const allPackages = Array.isArray(pkg) ? pkg : [pkg]
|
|
58
|
+
const config = {
|
|
59
|
+
pkgs: this.createPackageVersionMaps(allPackages),
|
|
60
|
+
packageRepoUtils: packageRepoUtils
|
|
61
|
+
}
|
|
62
|
+
return Marshalls.tasks(config, this.progressManager)
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
createPackageVersionMaps(packages) {
|
|
@@ -52,58 +81,6 @@ class Marshall {
|
|
|
52
81
|
|
|
53
82
|
return packageVersionMapping
|
|
54
83
|
}
|
|
55
|
-
|
|
56
|
-
report(marshalls) {
|
|
57
|
-
const messages = this.collectPackageMessages(marshalls)
|
|
58
|
-
|
|
59
|
-
if (!messages || Object.keys(messages).length === 0) {
|
|
60
|
-
return { error: false }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
console.log()
|
|
64
|
-
console.log('Detected possible issues with the following packages:')
|
|
65
|
-
for (const packageName in messages) {
|
|
66
|
-
const packageMessages = messages[packageName]
|
|
67
|
-
console.log(` [${color.red(packageName)}]`)
|
|
68
|
-
|
|
69
|
-
packageMessages.forEach((message) => {
|
|
70
|
-
if (message.type === MESSAGE_TYPE.ERROR) {
|
|
71
|
-
console.log(` - ${color.red(message.text)}`)
|
|
72
|
-
} else {
|
|
73
|
-
console.log(` - ${color.yellow(message.text)}`)
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { error: true, data: messages }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
collectPackageMessages(marshalls) {
|
|
82
|
-
const allPackageMessages = {}
|
|
83
|
-
for (const key in marshalls) {
|
|
84
|
-
this.prepareMessages(allPackageMessages, marshalls[key].errors)
|
|
85
|
-
this.prepareMessages(allPackageMessages, marshalls[key].warnings, true)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return allPackageMessages
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
prepareMessages(allPackageMessages, messages, isWarning) {
|
|
92
|
-
if (Array.isArray(messages) && messages.length > 0) {
|
|
93
|
-
messages.forEach((msg) => {
|
|
94
|
-
this.appendPackageMessage(allPackageMessages, msg.pkg, {
|
|
95
|
-
text: msg.message,
|
|
96
|
-
type: isWarning ? MESSAGE_TYPE.WARNING : MESSAGE_TYPE.ERROR
|
|
97
|
-
})
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
appendPackageMessage(packages, packageName, packageMessage) {
|
|
103
|
-
packages[packageName]
|
|
104
|
-
? packages[packageName].push(packageMessage)
|
|
105
|
-
: (packages[packageName] = [packageMessage])
|
|
106
|
-
}
|
|
107
84
|
}
|
|
108
85
|
|
|
109
86
|
module.exports = Marshall
|