npq 3.5.4 → 3.6.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
CHANGED
|
@@ -97,6 +97,7 @@ Note: `npq` by default will offload all commands and their arguments to the `npm
|
|
|
97
97
|
| expired domains | Will show a warning if a package has been found with one of its maintainers having an email address that includes an expired domain | Checks a dependency version for a maintainer with an expired domain
|
|
98
98
|
| signatures | Will compare the package's signature as it shows on the registry's pakument with the keys published on the npmjs.com registry
|
|
99
99
|
| provenance | Will verify the package's attestations of provenance metadata for the published package
|
|
100
|
+
| version-maturity | Will show a warning if the specific version being installed was published less than 7 days ago | Helps identify recently published versions that may not have been reviewed by the community yet
|
|
100
101
|
|
|
101
102
|
### Disabling Marshalls
|
|
102
103
|
|
|
@@ -125,6 +126,7 @@ Here are all the available environment variable names for disabling specific mar
|
|
|
125
126
|
| signatures | `MARSHALL_DISABLE_SIGNATURES` | Disable registry signature verification |
|
|
126
127
|
| snyk | `MARSHALL_DISABLE_SNYK` | Disable Snyk vulnerability checks |
|
|
127
128
|
| typosquatting | `MARSHALL_DISABLE_TYPOSQUATTING` | Disable typosquatting detection |
|
|
129
|
+
| version-maturity | `MARSHALL_DISABLE_VERSION_MATURITY` | Disable version maturity checks |
|
|
128
130
|
|
|
129
131
|
### Run checks on package without installing it:
|
|
130
132
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const TyposquattingMarshall = require('../lib/marshalls/typosquatting.marshall')
|
|
2
|
+
|
|
3
|
+
describe('Typosquatting Marshall', () => {
|
|
4
|
+
test('should remove duplicate entries from similar packages', async () => {
|
|
5
|
+
const typosquattingMarshall = new TyposquattingMarshall({
|
|
6
|
+
packageRepoUtils: null
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
// Mock the top packages data to include duplicates
|
|
10
|
+
const originalTopPackages = require('../data/top-packages.json')
|
|
11
|
+
|
|
12
|
+
// Create a test package that would match multiple similar packages
|
|
13
|
+
const pkg = {
|
|
14
|
+
packageName: 'ghtml' // This should match 'html' which appears multiple times in the data
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await typosquattingMarshall.validate(pkg)
|
|
19
|
+
// If no error is thrown, the test should fail
|
|
20
|
+
expect(true).toBe(false)
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// Check that the error message doesn't contain duplicate entries
|
|
23
|
+
const errorMessage = error.message
|
|
24
|
+
expect(errorMessage).toContain('Package name could be a typosquatting attempt for popular package(s):')
|
|
25
|
+
|
|
26
|
+
// Extract the package names from the error message
|
|
27
|
+
const packagesList = errorMessage.split('popular package(s): ')[1]
|
|
28
|
+
const packages = packagesList.split(', ')
|
|
29
|
+
|
|
30
|
+
// Check that there are no duplicates
|
|
31
|
+
const uniquePackages = [...new Set(packages)]
|
|
32
|
+
expect(packages.length).toBe(uniquePackages.length)
|
|
33
|
+
|
|
34
|
+
// Verify that 'html' appears only once even though it exists multiple times in the data
|
|
35
|
+
const htmlCount = packages.filter(pkg => pkg === 'html').length
|
|
36
|
+
expect(htmlCount).toBe(1)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('should not report typosquatting for packages in top packages list', async () => {
|
|
41
|
+
const typosquattingMarshall = new TyposquattingMarshall({
|
|
42
|
+
packageRepoUtils: null
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Test with a package that exists in the top packages list
|
|
46
|
+
const pkg = {
|
|
47
|
+
packageName: 'express' // This should be in the top packages list
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await typosquattingMarshall.validate(pkg)
|
|
51
|
+
expect(result).toEqual([])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('should not report typosquatting for packages with no similar matches', async () => {
|
|
55
|
+
const typosquattingMarshall = new TyposquattingMarshall({
|
|
56
|
+
packageRepoUtils: null
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Test with a package that has no similar matches
|
|
60
|
+
const pkg = {
|
|
61
|
+
packageName: 'verylonganduniquenamethatdoesnotmatchanything'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await typosquattingMarshall.validate(pkg)
|
|
65
|
+
expect(result).toEqual([])
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Marshall = require('../lib/marshalls/version-maturity.marshall')
|
|
4
|
+
|
|
5
|
+
describe('Version Maturity Marshall', () => {
|
|
6
|
+
test('should have the correct title', () => {
|
|
7
|
+
const testMarshall = new Marshall({
|
|
8
|
+
packageRepoUtils: null
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
expect(testMarshall.title()).toEqual('Checking version maturity')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('should throw error for recently published version (same day)', async () => {
|
|
15
|
+
const now = new Date()
|
|
16
|
+
const hoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000) // 2 hours ago
|
|
17
|
+
|
|
18
|
+
const testMarshall = new Marshall({
|
|
19
|
+
packageRepoUtils: {
|
|
20
|
+
getPackageInfo: () => {
|
|
21
|
+
return Promise.resolve({
|
|
22
|
+
time: {
|
|
23
|
+
'1.0.0': hoursAgo.toISOString()
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await expect(
|
|
32
|
+
testMarshall.validate({
|
|
33
|
+
packageName: 'test-package',
|
|
34
|
+
packageVersion: '1.0.0'
|
|
35
|
+
})
|
|
36
|
+
).rejects.toThrow(
|
|
37
|
+
'detected a recently published version (published 2 hours ago) - consider waiting for community review'
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('should throw error for recently published version (within threshold)', async () => {
|
|
42
|
+
const now = new Date()
|
|
43
|
+
const daysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000) // 3 days ago
|
|
44
|
+
|
|
45
|
+
const testMarshall = new Marshall({
|
|
46
|
+
packageRepoUtils: {
|
|
47
|
+
getPackageInfo: () => {
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
time: {
|
|
50
|
+
'1.0.0': daysAgo.toISOString()
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
await expect(
|
|
59
|
+
testMarshall.validate({
|
|
60
|
+
packageName: 'test-package',
|
|
61
|
+
packageVersion: '1.0.0'
|
|
62
|
+
})
|
|
63
|
+
).rejects.toThrow(
|
|
64
|
+
'detected a recently published version (published 3 days ago) - consider waiting for community review'
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('should throw error for version published exactly 1 day ago', async () => {
|
|
69
|
+
const now = new Date()
|
|
70
|
+
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000) // 1 day ago
|
|
71
|
+
|
|
72
|
+
const testMarshall = new Marshall({
|
|
73
|
+
packageRepoUtils: {
|
|
74
|
+
getPackageInfo: () => {
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
time: {
|
|
77
|
+
'1.0.0': oneDayAgo.toISOString()
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await expect(
|
|
86
|
+
testMarshall.validate({
|
|
87
|
+
packageName: 'test-package',
|
|
88
|
+
packageVersion: '1.0.0'
|
|
89
|
+
})
|
|
90
|
+
).rejects.toThrow(
|
|
91
|
+
'detected a recently published version (published 1 day ago) - consider waiting for community review'
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('should pass for version published beyond threshold', async () => {
|
|
96
|
+
const now = new Date()
|
|
97
|
+
const weekAgo = new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000) // 8 days ago
|
|
98
|
+
|
|
99
|
+
const testMarshall = new Marshall({
|
|
100
|
+
packageRepoUtils: {
|
|
101
|
+
getPackageInfo: () => {
|
|
102
|
+
return Promise.resolve({
|
|
103
|
+
time: {
|
|
104
|
+
'1.0.0': weekAgo.toISOString()
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
},
|
|
108
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const result = await testMarshall.validate({
|
|
113
|
+
packageName: 'test-package',
|
|
114
|
+
packageVersion: '1.0.0'
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
packageName: 'test-package',
|
|
119
|
+
packageVersion: '1.0.0'
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('should throw error when package time information is missing', async () => {
|
|
124
|
+
const testMarshall = new Marshall({
|
|
125
|
+
packageRepoUtils: {
|
|
126
|
+
getPackageInfo: () => {
|
|
127
|
+
return Promise.resolve({})
|
|
128
|
+
},
|
|
129
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
await expect(
|
|
134
|
+
testMarshall.validate({
|
|
135
|
+
packageName: 'test-package',
|
|
136
|
+
packageVersion: '1.0.0'
|
|
137
|
+
})
|
|
138
|
+
).rejects.toThrow('could not determine package version information')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('should throw error when version release date is missing', async () => {
|
|
142
|
+
const testMarshall = new Marshall({
|
|
143
|
+
packageRepoUtils: {
|
|
144
|
+
getPackageInfo: () => {
|
|
145
|
+
return Promise.resolve({
|
|
146
|
+
time: {
|
|
147
|
+
'2.0.0': new Date().toISOString()
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
},
|
|
151
|
+
getSemVer: () => Promise.resolve('1.0.0')
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
await expect(
|
|
156
|
+
testMarshall.validate({
|
|
157
|
+
packageName: 'test-package',
|
|
158
|
+
packageVersion: '1.0.0'
|
|
159
|
+
})
|
|
160
|
+
).rejects.toThrow('could not determine release date for version 1.0.0')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('should handle version aliases correctly', async () => {
|
|
164
|
+
const now = new Date()
|
|
165
|
+
const daysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000) // 2 days ago
|
|
166
|
+
|
|
167
|
+
const testMarshall = new Marshall({
|
|
168
|
+
packageRepoUtils: {
|
|
169
|
+
getPackageInfo: () => {
|
|
170
|
+
return Promise.resolve({
|
|
171
|
+
time: {
|
|
172
|
+
'1.2.3': daysAgo.toISOString()
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
},
|
|
176
|
+
getSemVer: (packageName, packageVersion) => {
|
|
177
|
+
if (packageVersion === 'latest') {
|
|
178
|
+
return Promise.resolve('1.2.3')
|
|
179
|
+
}
|
|
180
|
+
return Promise.resolve(packageVersion)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await expect(
|
|
186
|
+
testMarshall.validate({
|
|
187
|
+
packageName: 'test-package',
|
|
188
|
+
packageVersion: 'latest'
|
|
189
|
+
})
|
|
190
|
+
).rejects.toThrow(
|
|
191
|
+
'detected a recently published version (published 2 days ago) - consider waiting for community review'
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
@@ -41,9 +41,11 @@ class Marshall extends BaseMarshall {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
if (similarPackages.length > 0) {
|
|
44
|
+
// Remove duplicates from similarPackages array
|
|
45
|
+
const uniqueSimilarPackages = [...new Set(similarPackages)]
|
|
44
46
|
return reject(
|
|
45
47
|
new Error(
|
|
46
|
-
`Package name could be a typosquatting attempt for popular package(s): ${
|
|
48
|
+
`Package name could be a typosquatting attempt for popular package(s): ${uniqueSimilarPackages.join(
|
|
47
49
|
', '
|
|
48
50
|
)}`
|
|
49
51
|
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const BaseMarshall = require('./baseMarshall')
|
|
4
|
+
const { marshallCategories } = require('./constants')
|
|
5
|
+
|
|
6
|
+
const MARSHALL_NAME = 'version_maturity'
|
|
7
|
+
const VERSION_AGE_THRESHOLD = 7 // specified in days
|
|
8
|
+
|
|
9
|
+
class Marshall extends BaseMarshall {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options)
|
|
12
|
+
this.name = MARSHALL_NAME
|
|
13
|
+
this.categoryId = marshallCategories.PackageHealth.id
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
title() {
|
|
17
|
+
return 'Checking version maturity'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
validate(pkg) {
|
|
21
|
+
let packageData = null
|
|
22
|
+
return this.packageRepoUtils
|
|
23
|
+
.getPackageInfo(pkg.packageName)
|
|
24
|
+
.then((data) => {
|
|
25
|
+
if (data && data.time) {
|
|
26
|
+
packageData = data
|
|
27
|
+
return pkg
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error('could not determine package version information')
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
.then((pkg) => {
|
|
33
|
+
return this.packageRepoUtils.getSemVer(pkg.packageName, pkg.packageVersion)
|
|
34
|
+
})
|
|
35
|
+
.then((versionResolved) => {
|
|
36
|
+
const versionReleaseDate = packageData.time[versionResolved]
|
|
37
|
+
|
|
38
|
+
if (!versionReleaseDate) {
|
|
39
|
+
throw new Error(`could not determine release date for version ${versionResolved}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const versionDateDiff = new Date() - new Date(versionReleaseDate)
|
|
43
|
+
const versionDateDiffInDays = Math.round(versionDateDiff / (1000 * 60 * 60 * 24))
|
|
44
|
+
|
|
45
|
+
if (versionDateDiffInDays < VERSION_AGE_THRESHOLD) {
|
|
46
|
+
let timeAgoText = 'days'
|
|
47
|
+
let timeAgoNumber = versionDateDiffInDays
|
|
48
|
+
|
|
49
|
+
if (versionDateDiffInDays === 0) {
|
|
50
|
+
timeAgoText = 'hours'
|
|
51
|
+
const versionDateDiffInHours = Math.round(versionDateDiff / (1000 * 60 * 60))
|
|
52
|
+
timeAgoNumber = versionDateDiffInHours
|
|
53
|
+
} else if (versionDateDiffInDays === 1) {
|
|
54
|
+
timeAgoText = 'day'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error(
|
|
58
|
+
`detected a recently published version (published ${timeAgoNumber} ${timeAgoText} ago) - consider waiting for community review`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return pkg
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = Marshall
|