npq 3.8.1 → 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.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__/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 +5 -0
- 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 +3 -2
- package/lib/marshalls/author.marshall.js +36 -7
- package/lib/marshalls/baseMarshall.js +9 -17
- package/lib/marshalls/deprecation.marshall.js +43 -0
- package/lib/marshalls/downloads.marshall.js +1 -1
- package/lib/marshalls/expiredDomains.marshall.js +7 -5
- package/lib/marshalls/index.js +110 -80
- package/lib/marshalls/license.marshall.js +1 -1
- package/lib/marshalls/newbin.marshall.js +1 -2
- package/lib/marshalls/provenance.marshall.js +152 -33
- package/lib/marshalls/repo.marshall.js +6 -5
- 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 +1 -1
- package/package.json +16 -19
- package/scripts/postinstall.js +10 -11
- package/lib/cliCommons.js +0 -80
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const semver = require('semver')
|
|
3
4
|
const BaseMarshall = require('./baseMarshall')
|
|
4
5
|
const pacote = require('pacote')
|
|
5
6
|
const Warning = require('../helpers/warning')
|
|
@@ -21,48 +22,166 @@ class Marshall extends BaseMarshall {
|
|
|
21
22
|
validate(pkg) {
|
|
22
23
|
const validationMetadata = {}
|
|
23
24
|
|
|
24
|
-
return
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
return (
|
|
26
|
+
this.packageRepoUtils
|
|
27
|
+
.getPackageInfo(pkg.packageName)
|
|
28
|
+
.then((packageInfo) => {
|
|
29
|
+
const packageName = packageInfo.name
|
|
30
|
+
const packageVersion =
|
|
31
|
+
pkg.packageVersion === 'latest'
|
|
32
|
+
? packageInfo['dist-tags'] && packageInfo['dist-tags'].latest
|
|
33
|
+
: this.packageRepoUtils.parsePackageVersion(pkg.packageVersion).version
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
validationMetadata.name = packageName
|
|
36
|
+
validationMetadata.version = packageVersion
|
|
37
|
+
validationMetadata.packageInfo = packageInfo
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
if (!validationMetadata.version) {
|
|
40
|
+
throw new Error('Unable to find version or dist-tag for package')
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
// Disabled for now, see notes in function due to slow performance for
|
|
44
|
+
// running this check
|
|
45
|
+
// .then(() => {
|
|
46
|
+
// return this.checkProvenanceRegression(validationMetadata)
|
|
47
|
+
// })
|
|
48
|
+
.then(() => {
|
|
49
|
+
return this.fetchRegistryKeys()
|
|
50
|
+
})
|
|
51
|
+
.then((keys) => {
|
|
52
|
+
// @TODO currently we're hardcoding the official npm registry
|
|
53
|
+
// this should however allow for local proxies and other registries
|
|
54
|
+
return pacote.manifest(`${validationMetadata.name}@${validationMetadata.version}`, {
|
|
55
|
+
verifyAttestations: true,
|
|
56
|
+
registry: 'https://registry.npmjs.org',
|
|
57
|
+
|
|
58
|
+
'//registry.npmjs.org/:_keys': keys
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
.then((metadata) => {
|
|
62
|
+
if (!metadata || !metadata._attestations) {
|
|
63
|
+
throw new Warning('the package was published without any attestations')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const attestations = metadata._attestations
|
|
67
|
+
|
|
68
|
+
return attestations
|
|
69
|
+
})
|
|
70
|
+
.catch((error) => {
|
|
71
|
+
// We can ignore this type of error, false positive
|
|
72
|
+
// See: https://github.com/lirantal/npq/issues/329
|
|
73
|
+
if (
|
|
74
|
+
error.code === 'EATTESTATIONVERIFY' &&
|
|
75
|
+
error.message.includes('malformed checkpoint')
|
|
76
|
+
) {
|
|
77
|
+
return []
|
|
78
|
+
} else {
|
|
79
|
+
if (error.message.includes('package was published without any attestations')) {
|
|
80
|
+
throw new Warning(`Unable to verify provenance: ${error.message}`)
|
|
81
|
+
} else {
|
|
82
|
+
this.debug(
|
|
83
|
+
'\nUnable to verify provenance for package %s@%s: %s',
|
|
84
|
+
validationMetadata.name,
|
|
85
|
+
validationMetadata.version,
|
|
86
|
+
error.message
|
|
87
|
+
)
|
|
88
|
+
throw new Warning(`Unable to verify provenance`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
async checkProvenanceRegression(validationMetadata) {
|
|
97
|
+
const { packageInfo, name, version } = validationMetadata
|
|
98
|
+
|
|
99
|
+
if (!packageInfo || !packageInfo.versions) {
|
|
100
|
+
return // Cannot check regression without version information
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const allVersions = Object.keys(packageInfo.versions)
|
|
104
|
+
const validVersions = allVersions.filter(v => semver.valid(v))
|
|
105
|
+
|
|
106
|
+
if (validVersions.length === 0) {
|
|
107
|
+
return // No valid versions to compare
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Get all versions older than the target version
|
|
111
|
+
const olderVersions = validVersions
|
|
112
|
+
.filter(v => semver.lt(v, version))
|
|
113
|
+
.sort(semver.rcompare) // Sort descending, newest first
|
|
114
|
+
|
|
115
|
+
if (olderVersions.length === 0) {
|
|
116
|
+
return // No older versions to check
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if any older versions had provenance
|
|
120
|
+
let hasProvenanceRegressionDetected = false
|
|
121
|
+
const versionWithProvenance = []
|
|
122
|
+
|
|
123
|
+
// NOTE: Going back all versions and fetching their manifest to check provenance
|
|
124
|
+
// is going to be very slow. We can limit this function to go back just 2-3 versions
|
|
125
|
+
// and then stop there
|
|
126
|
+
for (const olderVersion of olderVersions) {
|
|
127
|
+
try {
|
|
128
|
+
const keys = await this.fetchRegistryKeys()
|
|
129
|
+
const metadata = await pacote.manifest(`${name}@${olderVersion}`, {
|
|
47
130
|
verifyAttestations: true,
|
|
48
131
|
registry: 'https://registry.npmjs.org',
|
|
49
|
-
|
|
50
132
|
'//registry.npmjs.org/:_keys': keys
|
|
51
133
|
})
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
134
|
+
|
|
135
|
+
if (metadata && metadata._attestations) {
|
|
136
|
+
versionWithProvenance.push(olderVersion)
|
|
137
|
+
hasProvenanceRegressionDetected = true
|
|
56
138
|
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// Ignore errors for older versions as they might not have provenance
|
|
141
|
+
// We only care if they successfully had provenance before
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
}
|
|
57
145
|
|
|
58
|
-
|
|
146
|
+
// If older versions had provenance, check if current version lacks it
|
|
147
|
+
if (hasProvenanceRegressionDetected) {
|
|
148
|
+
try {
|
|
149
|
+
const keys = await this.fetchRegistryKeys()
|
|
150
|
+
const currentMetadata = await pacote.manifest(`${name}@${version}`, {
|
|
151
|
+
verifyAttestations: true,
|
|
152
|
+
registry: 'https://registry.npmjs.org',
|
|
153
|
+
'//registry.npmjs.org/:_keys': keys
|
|
154
|
+
})
|
|
59
155
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
156
|
+
if (!currentMetadata || !currentMetadata._attestations) {
|
|
157
|
+
const latestVersionWithProvenance = versionWithProvenance[0] // First is the newest
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Provenance regression detected: Previous version ${latestVersionWithProvenance} had provenance attestations, but version ${version} does not. This represents a security downgrade.`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
// If it's our custom error, re-throw it
|
|
164
|
+
if (error.message.includes('Provenance regression detected')) {
|
|
165
|
+
throw error
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// If it's a verification error, the current version lacks valid provenance
|
|
169
|
+
if (error.code === 'EATTESTATIONVERIFY' || error.message.includes('attestations')) {
|
|
170
|
+
const latestVersionWithProvenance = versionWithProvenance[0]
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Provenance regression detected: Previous version ${latestVersionWithProvenance} had provenance attestations, but version ${version} does not. This represents a security downgrade.`
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// For other errors, treat as if current version lacks provenance
|
|
177
|
+
const latestVersionWithProvenance = versionWithProvenance[0]
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Provenance regression detected: Previous version ${latestVersionWithProvenance} had provenance attestations, but version ${version} does not. This represents a security downgrade.`
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
65
183
|
}
|
|
184
|
+
*/
|
|
66
185
|
|
|
67
186
|
fetchRegistryKeys() {
|
|
68
187
|
const registryHost = 'https://registry.npmjs.org'
|
|
@@ -86,7 +205,7 @@ class Marshall extends BaseMarshall {
|
|
|
86
205
|
return keys
|
|
87
206
|
})
|
|
88
207
|
.catch((error) => {
|
|
89
|
-
throw new
|
|
208
|
+
throw new Warning(`Error fetching registry keys: ${error.message}`)
|
|
90
209
|
})
|
|
91
210
|
}
|
|
92
211
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const BaseMarshall = require('./baseMarshall')
|
|
4
|
+
const Warning = require('../helpers/warning')
|
|
4
5
|
const { marshallCategories } = require('./constants')
|
|
5
6
|
const URL = require('url').URL
|
|
6
7
|
const MARSHALL_NAME = 'repo'
|
|
@@ -9,7 +10,7 @@ class Marshall extends BaseMarshall {
|
|
|
9
10
|
constructor(options) {
|
|
10
11
|
super(options)
|
|
11
12
|
this.name = MARSHALL_NAME
|
|
12
|
-
this.categoryId = marshallCategories.
|
|
13
|
+
this.categoryId = marshallCategories.PackageHealth.id
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
title() {
|
|
@@ -30,21 +31,21 @@ class Marshall extends BaseMarshall {
|
|
|
30
31
|
urlStructure = new URL(lastVersionData.repository.url)
|
|
31
32
|
urlOfGitRepository = new URL(`https://${urlStructure.host}${urlStructure.pathname}`)
|
|
32
33
|
} catch (error) {
|
|
33
|
-
throw new
|
|
34
|
+
throw new Warning('No valid repository is associated with the package')
|
|
34
35
|
}
|
|
35
36
|
return fetch(urlOfGitRepository.href).catch(() => {
|
|
36
|
-
throw new
|
|
37
|
+
throw new Warning(
|
|
37
38
|
`The repository associated with the package (${urlOfGitRepository.href}) does not exist or is unreachable at the moment.`
|
|
38
39
|
)
|
|
39
40
|
})
|
|
40
41
|
} else if (lastVersionData && lastVersionData.homepage) {
|
|
41
42
|
return fetch(lastVersionData.homepage).catch(() => {
|
|
42
|
-
throw new
|
|
43
|
+
throw new Warning(
|
|
43
44
|
`The homepage associated with the package (${lastVersionData.homepage}) does not exist or is unreachable at the moment.`
|
|
44
45
|
)
|
|
45
46
|
})
|
|
46
47
|
} else {
|
|
47
|
-
throw new
|
|
48
|
+
throw new Warning('The package has no associated repository or homepage.')
|
|
48
49
|
}
|
|
49
50
|
})
|
|
50
51
|
}
|
|
@@ -43,7 +43,7 @@ class Marshall extends BaseMarshall {
|
|
|
43
43
|
packageScripts[scriptName].length > 0
|
|
44
44
|
) {
|
|
45
45
|
throw new Error(
|
|
46
|
-
`Detected a possible malicious intent script,
|
|
46
|
+
`Detected a possible malicious intent script, audit required: ${scriptName}: ${packageScripts[scriptName]}`
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
})
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const BaseMarshall = require('./baseMarshall')
|
|
4
|
+
const Warning = require('../helpers/warning')
|
|
5
|
+
const util = require('util')
|
|
4
6
|
const pacote = require('pacote')
|
|
5
7
|
const { marshallCategories } = require('./constants')
|
|
6
8
|
|
|
7
9
|
const MARSHALL_NAME = 'signatures'
|
|
8
10
|
|
|
11
|
+
let registryKeysCache = null
|
|
12
|
+
|
|
9
13
|
class Marshall extends BaseMarshall {
|
|
10
14
|
constructor(options) {
|
|
11
15
|
super(options)
|
|
@@ -20,6 +24,9 @@ class Marshall extends BaseMarshall {
|
|
|
20
24
|
validate(pkg) {
|
|
21
25
|
// @TODO currently we're hardcoding the official npm registry
|
|
22
26
|
// this should however allow for local proxies and other registries
|
|
27
|
+
// @TODO performance improvement: we should cache the registry keys
|
|
28
|
+
// and not fetch them every time because they don't change between
|
|
29
|
+
// requests
|
|
23
30
|
return this.fetchRegistryKeys()
|
|
24
31
|
.then((keys) => {
|
|
25
32
|
return pacote.manifest(`${pkg.packageName}@${pkg.packageVersion}`, {
|
|
@@ -33,11 +40,26 @@ class Marshall extends BaseMarshall {
|
|
|
33
40
|
return metadata
|
|
34
41
|
})
|
|
35
42
|
.catch((error) => {
|
|
36
|
-
|
|
43
|
+
// @TODO error.message that useful for rich debugging when enabled
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
error.message &&
|
|
47
|
+
error.message.includes('but the corresponding public key has expired')
|
|
48
|
+
) {
|
|
49
|
+
// if the error is about an expired key, we throw a warning
|
|
50
|
+
// instead of an error, so that the process can continue
|
|
51
|
+
throw new Warning(`Package is signed with an expired key`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Warning(`Unable to verify package signature on registry: ${error.message}`)
|
|
37
55
|
})
|
|
38
56
|
}
|
|
39
57
|
|
|
40
58
|
fetchRegistryKeys() {
|
|
59
|
+
if (registryKeysCache) {
|
|
60
|
+
return Promise.resolve(registryKeysCache)
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
const registryHost = 'https://registry.npmjs.org'
|
|
42
64
|
const registryKeysEndpoint = '/-/npm/v1/keys'
|
|
43
65
|
|
|
@@ -56,10 +78,12 @@ class Marshall extends BaseMarshall {
|
|
|
56
78
|
}))
|
|
57
79
|
})
|
|
58
80
|
.then((keys) => {
|
|
81
|
+
// save it in the cached singleton object
|
|
82
|
+
registryKeysCache = keys
|
|
59
83
|
return keys
|
|
60
84
|
})
|
|
61
85
|
.catch((error) => {
|
|
62
|
-
throw new
|
|
86
|
+
throw new Warning(`Error fetching registry keys: ${error.message}`)
|
|
63
87
|
})
|
|
64
88
|
}
|
|
65
89
|
}
|
|
@@ -8,7 +8,7 @@ const { marshallCategories } = require('./constants')
|
|
|
8
8
|
|
|
9
9
|
const MARSHALL_NAME = 'snyk'
|
|
10
10
|
const SNYK_API_URL = 'https://snyk.io/api/v1/vuln/npm'
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
const SNYK_PACKAGE_PAGE = 'https://snyk.io/vuln/npm:'
|
|
13
13
|
const SNYK_API_TOKEN = process.env.SNYK_TOKEN
|
|
14
14
|
const SNYK_CONFIG_FILE = '.config/configstore/snyk.json'
|
|
@@ -53,34 +53,70 @@ class Marshall extends BaseMarshall {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
if (data && data.issuesCount && data.issuesCount > 0) {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
if (this.snykApiToken) {
|
|
57
|
+
const packageSecurityInfo = `${SNYK_PACKAGE_PAGE}${encodeURIComponent(pkg.packageName)}`
|
|
58
|
+
if (data.isMaliciousPackage) {
|
|
59
|
+
throw new Error(`Malicious package found: ${packageSecurityInfo}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new Error(`${data.issuesCount} vulnerable path(s) found: ${packageSecurityInfo}`)
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`${data.issuesCount} vulnerabilities found by OSV for ${pkg.packageName}`
|
|
66
|
+
)
|
|
67
|
+
}
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
return data
|
|
61
71
|
})
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
const url =
|
|
74
|
+
getOsvVulnerabilityInfo({ packageName, packageVersion }) {
|
|
75
|
+
const url = 'https://api.osv.dev/v1/query'
|
|
76
|
+
const body = {
|
|
77
|
+
version: packageVersion,
|
|
78
|
+
package: {
|
|
79
|
+
name: packageName,
|
|
80
|
+
ecosystem: 'npm'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
66
83
|
|
|
67
|
-
return fetch(url
|
|
84
|
+
return fetch(url, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json'
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify(body)
|
|
90
|
+
})
|
|
68
91
|
.then((response) => response.json())
|
|
69
92
|
.then((data) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
if (data && data.vulns) {
|
|
94
|
+
const isMaliciousPackage = data.vulns.some(
|
|
95
|
+
(vuln) =>
|
|
96
|
+
vuln.database_specific &&
|
|
97
|
+
vuln.database_specific['malicious-packages-origins'] &&
|
|
98
|
+
vuln.database_specific['malicious-packages-origins'].length > 0
|
|
99
|
+
)
|
|
100
|
+
|
|
73
101
|
return {
|
|
74
|
-
issuesCount: data.
|
|
102
|
+
issuesCount: data.vulns.length,
|
|
103
|
+
isMaliciousPackage
|
|
75
104
|
}
|
|
76
105
|
}
|
|
106
|
+
return {
|
|
107
|
+
issuesCount: 0,
|
|
108
|
+
isMaliciousPackage: false
|
|
109
|
+
}
|
|
77
110
|
})
|
|
78
|
-
.catch(() =>
|
|
111
|
+
.catch(() => ({
|
|
112
|
+
issuesCount: 0,
|
|
113
|
+
isMaliciousPackage: false
|
|
114
|
+
}))
|
|
79
115
|
}
|
|
80
116
|
|
|
81
117
|
getSnykVulnInfo({ packageName, packageVersion } = {}) {
|
|
82
118
|
if (!this.snykApiToken) {
|
|
83
|
-
return this.
|
|
119
|
+
return this.getOsvVulnerabilityInfo({ packageName, packageVersion })
|
|
84
120
|
}
|
|
85
121
|
|
|
86
122
|
const url = `${SNYK_API_URL}/${encodeURIComponent(packageName + '@' + packageVersion)}`
|
|
@@ -90,17 +126,27 @@ class Marshall extends BaseMarshall {
|
|
|
90
126
|
Authorization: `token ${this.snykApiToken}`
|
|
91
127
|
}
|
|
92
128
|
})
|
|
129
|
+
.then((response) => {
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(`Snyk API request failed with status ${response.status}`)
|
|
132
|
+
}
|
|
133
|
+
return response
|
|
134
|
+
})
|
|
93
135
|
.then((response) => response.json())
|
|
94
136
|
.then((data) => {
|
|
95
137
|
if (data && data.vulnerabilities) {
|
|
138
|
+
const isMaliciousPackage = data.vulnerabilities.some(
|
|
139
|
+
(vulnerability) => vulnerability.title === 'Malicious Package'
|
|
140
|
+
)
|
|
141
|
+
|
|
96
142
|
return {
|
|
97
|
-
issuesCount: data.vulnerabilities.length
|
|
143
|
+
issuesCount: data.vulnerabilities.length,
|
|
144
|
+
isMaliciousPackage
|
|
98
145
|
}
|
|
99
146
|
}
|
|
100
147
|
|
|
101
148
|
return false
|
|
102
149
|
})
|
|
103
|
-
.catch(() => false)
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
getSnykToken() {
|
|
@@ -112,7 +158,7 @@ class Marshall extends BaseMarshall {
|
|
|
112
158
|
|
|
113
159
|
try {
|
|
114
160
|
if (fs.statSync(snykConfigPath)) {
|
|
115
|
-
const snykConfig =
|
|
161
|
+
const snykConfig = JSON.parse(fs.readFileSync(snykConfigPath, 'utf8'))
|
|
116
162
|
if (snykConfig && snykConfig.api) {
|
|
117
163
|
return snykConfig.api
|
|
118
164
|
}
|
|
@@ -4,7 +4,7 @@ const BaseMarshall = require('./baseMarshall')
|
|
|
4
4
|
const { marshallCategories } = require('./constants')
|
|
5
5
|
|
|
6
6
|
const path = require('path')
|
|
7
|
-
const
|
|
7
|
+
const { distance } = require('fastest-levenshtein')
|
|
8
8
|
const topPackagesRawJSON = require(path.join(__dirname, '../../data/top-packages.json'))
|
|
9
9
|
|
|
10
10
|
const MARSHALL_NAME = 'typosquatting'
|
|
@@ -25,6 +25,11 @@ class Marshall extends BaseMarshall {
|
|
|
25
25
|
let similarPackages = []
|
|
26
26
|
let packageFoundInTopPackages = false
|
|
27
27
|
return new Promise((resolve, reject) => {
|
|
28
|
+
// If package is within an allow-list
|
|
29
|
+
if (this.packageRepoUtils.isPackageInAllowList(pkg.packageName)) {
|
|
30
|
+
return resolve([])
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
for (const popularPackageNameInRepository of topPackagesRawJSON) {
|
|
29
34
|
// If the package to be installed is itself found within the Top Packages dataset
|
|
30
35
|
// then we don't report on it
|
|
@@ -33,7 +38,7 @@ class Marshall extends BaseMarshall {
|
|
|
33
38
|
return resolve([])
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
levenshteinDistance =
|
|
41
|
+
levenshteinDistance = distance(pkg.packageName, popularPackageNameInRepository)
|
|
37
42
|
|
|
38
43
|
if (levenshteinDistance > 0 && levenshteinDistance < 3) {
|
|
39
44
|
similarPackages.push(popularPackageNameInRepository)
|
|
@@ -45,9 +50,7 @@ class Marshall extends BaseMarshall {
|
|
|
45
50
|
const uniqueSimilarPackages = [...new Set(similarPackages)]
|
|
46
51
|
return reject(
|
|
47
52
|
new Error(
|
|
48
|
-
`
|
|
49
|
-
', '
|
|
50
|
-
)}`
|
|
53
|
+
`Potential typosquatting with popular package(s): ${uniqueSimilarPackages.join(', ')}`
|
|
51
54
|
)
|
|
52
55
|
)
|
|
53
56
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "npq",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"description": "marshall your npm/npm package installs with high quality and class 🎖",
|
|
5
5
|
"bin": {
|
|
6
6
|
"npq": "./bin/npq.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"prepare": "husky install"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": ">=
|
|
27
|
+
"node": ">=20.13.0"
|
|
28
28
|
},
|
|
29
29
|
"author": {
|
|
30
30
|
"name": "Liran Tal",
|
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
"provenance": true,
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"fastest-levenshtein": "^1.0.16",
|
|
44
|
+
"npm-package-arg": "^11.0.1",
|
|
45
|
+
"pacote": "^17.0.4",
|
|
46
|
+
"semver": "^7.7.2"
|
|
47
|
+
},
|
|
42
48
|
"devDependencies": {
|
|
43
49
|
"@babel/core": "^7.23.0",
|
|
44
50
|
"@babel/plugin-transform-runtime": "^7.22.15",
|
|
@@ -66,10 +72,10 @@
|
|
|
66
72
|
"collectCoverage": true,
|
|
67
73
|
"coverageThreshold": {
|
|
68
74
|
"global": {
|
|
69
|
-
"branches":
|
|
70
|
-
"functions":
|
|
71
|
-
"lines":
|
|
72
|
-
"statements":
|
|
75
|
+
"branches": 80,
|
|
76
|
+
"functions": 80,
|
|
77
|
+
"lines": 80,
|
|
78
|
+
"statements": 80
|
|
73
79
|
},
|
|
74
80
|
"scripts/*": {
|
|
75
81
|
"branches": 60,
|
|
@@ -78,6 +84,9 @@
|
|
|
78
84
|
"statements": 80
|
|
79
85
|
}
|
|
80
86
|
},
|
|
87
|
+
"testMatch": [
|
|
88
|
+
"**/__tests__/**/*.test.js"
|
|
89
|
+
],
|
|
81
90
|
"testPathIgnorePatterns": [
|
|
82
91
|
"/__tests__/__fixtures__/*"
|
|
83
92
|
]
|
|
@@ -127,7 +136,7 @@
|
|
|
127
136
|
"node/no-unsupported-features": "off",
|
|
128
137
|
"node/no-unpublished-require": "off",
|
|
129
138
|
"security/detect-non-literal-fs-filename": "warn",
|
|
130
|
-
"security/detect-unsafe-regex": "
|
|
139
|
+
"security/detect-unsafe-regex": "warn",
|
|
131
140
|
"security/detect-buffer-noassert": "error",
|
|
132
141
|
"security/detect-child-process": "warn",
|
|
133
142
|
"security/detect-disable-mustache-escape": "error",
|
|
@@ -169,18 +178,6 @@
|
|
|
169
178
|
}
|
|
170
179
|
}
|
|
171
180
|
},
|
|
172
|
-
"dependencies": {
|
|
173
|
-
"fast-levenshtein": "^3.0.0",
|
|
174
|
-
"glob": "^10.3.10",
|
|
175
|
-
"inquirer": "^8.2.6",
|
|
176
|
-
"kleur": "^4.1.5",
|
|
177
|
-
"listr2": "^7.0.1",
|
|
178
|
-
"npm-package-arg": "^11.0.1",
|
|
179
|
-
"pacote": "^17.0.4",
|
|
180
|
-
"semver": "^7.5.4",
|
|
181
|
-
"validator": "13.11.0",
|
|
182
|
-
"yargs": "^17.7.2"
|
|
183
|
-
},
|
|
184
181
|
"release": {
|
|
185
182
|
"branches": [
|
|
186
183
|
"main"
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
|
-
const
|
|
3
|
-
const inquirer = require('inquirer')
|
|
2
|
+
const { styleText } = require('node:util')
|
|
4
3
|
const semver = require('semver')
|
|
5
4
|
|
|
5
|
+
const cliPrompt = require('../lib/helpers/cliPrompt.js')
|
|
6
6
|
const helpers = require('./scriptHelpers')
|
|
7
7
|
|
|
8
8
|
const runPostInstall = async () => {
|
|
@@ -35,24 +35,23 @@ const runPostInstall = async () => {
|
|
|
35
35
|
console.log(
|
|
36
36
|
'To do that, we can alias npm and yarn to npq, so that e.g. `npm install <package>` will first use npq to verify the package and prompt you if it finds any issues.'
|
|
37
37
|
)
|
|
38
|
-
const answers = await
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
])
|
|
38
|
+
const answers = await cliPrompt.prompt({
|
|
39
|
+
name: 'install',
|
|
40
|
+
message: `Do you want to add ${shellConfig.name} aliases for npm and yarn?`,
|
|
41
|
+
default: true
|
|
42
|
+
})
|
|
43
|
+
|
|
45
44
|
if (answers.install) {
|
|
46
45
|
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
47
46
|
await fs.promises.appendFile(shellConfig.profilePath, shellConfig.aliases)
|
|
48
|
-
console.log(
|
|
47
|
+
console.log(styleText('green', '✔'), 'Reload your shell profile to use npq!')
|
|
49
48
|
}
|
|
50
49
|
} catch (err) {
|
|
51
50
|
if (err.isTtyError) {
|
|
52
51
|
// Could not render inquirer prompt; abort auto-install
|
|
53
52
|
return
|
|
54
53
|
}
|
|
55
|
-
console.error(
|
|
54
|
+
console.error(styleText('red', 'Failed to add aliases: '), err)
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|