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
|
@@ -2,29 +2,26 @@ beforeEach(() => {
|
|
|
2
2
|
jest.resetModules()
|
|
3
3
|
})
|
|
4
4
|
|
|
5
|
-
jest.mock('glob', () => {
|
|
6
|
-
return {
|
|
7
|
-
glob: jest.fn().mockResolvedValue(['file1', 'file2'])
|
|
8
|
-
}
|
|
9
|
-
})
|
|
10
|
-
|
|
11
5
|
test('collecting marshalls resolves with files array', async () => {
|
|
12
6
|
const marshalls = require('../lib/marshalls')
|
|
13
7
|
|
|
14
8
|
const marshallFiles = await marshalls.collectMarshalls()
|
|
15
|
-
expect(marshallFiles).
|
|
9
|
+
expect(marshallFiles.length).toBeGreaterThan(0)
|
|
10
|
+
expect(marshallFiles.every((file) => file.endsWith('.marshall.js'))).toBe(true)
|
|
16
11
|
})
|
|
17
12
|
|
|
18
|
-
test('collecting marshalls
|
|
13
|
+
test('collecting marshalls handles directory read errors', async () => {
|
|
14
|
+
const fs = require('node:fs')
|
|
19
15
|
const marshalls = require('../lib/marshalls')
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
// Mock fs.promises.readdir to throw an error
|
|
18
|
+
const originalReaddir = fs.promises.readdir
|
|
19
|
+
fs.promises.readdir = jest.fn().mockRejectedValue(new Error('Directory read error'))
|
|
20
|
+
|
|
21
|
+
await expect(marshalls.collectMarshalls()).rejects.toThrow('Directory read error')
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
// Restore original function
|
|
24
|
+
fs.promises.readdir = originalReaddir
|
|
28
25
|
})
|
|
29
26
|
|
|
30
27
|
test('build marshalls without any should throw error', () => {
|
|
@@ -43,7 +43,7 @@ describe('Expired domains test suites', () => {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
await expect(testMarshall.validate(pkgData)).rejects.toThrow(
|
|
46
|
-
/
|
|
46
|
+
/Detected expired domain can be abused for account takeover/
|
|
47
47
|
)
|
|
48
48
|
})
|
|
49
49
|
|
|
@@ -69,7 +69,7 @@ describe('Expired domains test suites', () => {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
await expect(testMarshall.validate(pkgData)).rejects.toThrow(
|
|
72
|
-
/
|
|
72
|
+
/Detected expired domain can be abused for account takeover: <unknown>/
|
|
73
73
|
)
|
|
74
74
|
})
|
|
75
75
|
|
|
@@ -140,9 +140,10 @@ describe('Provenance test suites', () => {
|
|
|
140
140
|
}
|
|
141
141
|
})
|
|
142
142
|
|
|
143
|
-
// We assert that the validate method
|
|
144
|
-
//
|
|
145
|
-
|
|
143
|
+
// We assert that the validate method throws an error,
|
|
144
|
+
// but we can't assert the exact error message because we don't log
|
|
145
|
+
// it unless debugging is enabled
|
|
146
|
+
await expect(testMarshall.validate(pkg)).rejects.toThrow()
|
|
146
147
|
|
|
147
148
|
// Assert that the fetch method is called with the correct URL
|
|
148
149
|
// eslint-disable-next-line no-undef
|
|
@@ -196,11 +197,187 @@ describe('Provenance test suites', () => {
|
|
|
196
197
|
// We assert that the validate method didn't throw an error,
|
|
197
198
|
// because the keys match the signature
|
|
198
199
|
await expect(testMarshall.validate(pkg)).rejects.toThrow(
|
|
199
|
-
'Unable to verify provenance: the package was published without any attestations
|
|
200
|
+
'Unable to verify provenance: the package was published without any attestations'
|
|
200
201
|
)
|
|
201
202
|
|
|
202
203
|
// Assert that the fetch method is called with the correct URL
|
|
203
204
|
// eslint-disable-next-line no-undef
|
|
204
205
|
expect(fetch).toHaveBeenCalledWith('https://registry.npmjs.org/-/npm/v1/keys')
|
|
205
206
|
})
|
|
207
|
+
|
|
208
|
+
test.skip('should throw error when provenance regression is detected', async () => {
|
|
209
|
+
// Mock fetch to return registry keys
|
|
210
|
+
const mockResponse = {
|
|
211
|
+
json: jest.fn().mockResolvedValue({
|
|
212
|
+
keys: [{ key: 'publicKey1' }]
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
global.fetch = jest.fn().mockResolvedValue(mockResponse)
|
|
216
|
+
|
|
217
|
+
const pkg = {
|
|
218
|
+
packageName: 'test-package',
|
|
219
|
+
packageVersion: '2.0.0'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Mock pacote.manifest to simulate:
|
|
223
|
+
// - version 1.0.0 has attestations (older version with provenance)
|
|
224
|
+
// - version 2.0.0 has no attestations (newer version without provenance)
|
|
225
|
+
pacote.manifest = jest
|
|
226
|
+
.fn()
|
|
227
|
+
.mockImplementationOnce((manifest) => {
|
|
228
|
+
if (manifest.includes('1.0.0')) {
|
|
229
|
+
return Promise.resolve({
|
|
230
|
+
name: 'test-package',
|
|
231
|
+
version: '1.0.0',
|
|
232
|
+
_attestations: { provenance: 'test' }
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
return Promise.reject(new Error('No attestations'))
|
|
236
|
+
})
|
|
237
|
+
.mockImplementationOnce((manifest) => {
|
|
238
|
+
if (manifest.includes('2.0.0')) {
|
|
239
|
+
return Promise.resolve({
|
|
240
|
+
name: 'test-package',
|
|
241
|
+
version: '2.0.0'
|
|
242
|
+
// No _attestations property
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
return Promise.reject(new Error('No attestations'))
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const testMarshall = new ProvenanceMarshall({
|
|
249
|
+
packageRepoUtils: {
|
|
250
|
+
getPackageInfo: () => {
|
|
251
|
+
return Promise.resolve({
|
|
252
|
+
name: 'test-package',
|
|
253
|
+
versions: {
|
|
254
|
+
'1.0.0': { name: 'test-package', version: '1.0.0' },
|
|
255
|
+
'2.0.0': { name: 'test-package', version: '2.0.0' }
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
},
|
|
259
|
+
parsePackageVersion: (version) => ({ version })
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
await expect(testMarshall.validate(pkg)).rejects.toThrow(
|
|
264
|
+
'Provenance regression detected: Previous version 1.0.0 had provenance attestations, but version 2.0.0 does not. This represents a security downgrade.'
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('should pass when no older versions exist', async () => {
|
|
269
|
+
const mockResponse = {
|
|
270
|
+
json: jest.fn().mockResolvedValue({
|
|
271
|
+
keys: [{ key: 'publicKey1' }]
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
global.fetch = jest.fn().mockResolvedValue(mockResponse)
|
|
275
|
+
|
|
276
|
+
const pkg = {
|
|
277
|
+
packageName: 'new-package',
|
|
278
|
+
packageVersion: '1.0.0'
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
pacote.manifest = jest.fn().mockResolvedValue({
|
|
282
|
+
name: 'new-package',
|
|
283
|
+
version: '1.0.0',
|
|
284
|
+
_attestations: { provenance: 'test' }
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const testMarshall = new ProvenanceMarshall({
|
|
288
|
+
packageRepoUtils: {
|
|
289
|
+
getPackageInfo: () => {
|
|
290
|
+
return Promise.resolve({
|
|
291
|
+
name: 'new-package',
|
|
292
|
+
versions: {
|
|
293
|
+
'1.0.0': { name: 'new-package', version: '1.0.0' }
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
},
|
|
297
|
+
parsePackageVersion: (version) => ({ version })
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const result = await testMarshall.validate(pkg)
|
|
302
|
+
expect(result).toBeDefined()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('should pass when no older versions had provenance', async () => {
|
|
306
|
+
const mockResponse = {
|
|
307
|
+
json: jest.fn().mockResolvedValue({
|
|
308
|
+
keys: [{ key: 'publicKey1' }]
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
global.fetch = jest.fn().mockResolvedValue(mockResponse)
|
|
312
|
+
|
|
313
|
+
const pkg = {
|
|
314
|
+
packageName: 'test-package',
|
|
315
|
+
packageVersion: '2.0.0'
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Both versions lack attestations
|
|
319
|
+
pacote.manifest = jest.fn().mockResolvedValue({
|
|
320
|
+
name: 'test-package',
|
|
321
|
+
version: '2.0.0'
|
|
322
|
+
// No _attestations
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
const testMarshall = new ProvenanceMarshall({
|
|
326
|
+
packageRepoUtils: {
|
|
327
|
+
getPackageInfo: () => {
|
|
328
|
+
return Promise.resolve({
|
|
329
|
+
name: 'test-package',
|
|
330
|
+
versions: {
|
|
331
|
+
'1.0.0': { name: 'test-package', version: '1.0.0' },
|
|
332
|
+
'2.0.0': { name: 'test-package', version: '2.0.0' }
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
},
|
|
336
|
+
parsePackageVersion: (version) => ({ version })
|
|
337
|
+
}
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
await expect(testMarshall.validate(pkg)).rejects.toThrow(
|
|
341
|
+
'the package was published without any attestations'
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('should pass when current version maintains provenance', async () => {
|
|
346
|
+
const mockResponse = {
|
|
347
|
+
json: jest.fn().mockResolvedValue({
|
|
348
|
+
keys: [{ key: 'publicKey1' }]
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
global.fetch = jest.fn().mockResolvedValue(mockResponse)
|
|
352
|
+
|
|
353
|
+
const pkg = {
|
|
354
|
+
packageName: 'test-package',
|
|
355
|
+
packageVersion: '2.0.0'
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Both versions have attestations
|
|
359
|
+
pacote.manifest = jest.fn().mockResolvedValue({
|
|
360
|
+
name: 'test-package',
|
|
361
|
+
version: '2.0.0',
|
|
362
|
+
_attestations: { provenance: 'test' }
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const testMarshall = new ProvenanceMarshall({
|
|
366
|
+
packageRepoUtils: {
|
|
367
|
+
getPackageInfo: () => {
|
|
368
|
+
return Promise.resolve({
|
|
369
|
+
name: 'test-package',
|
|
370
|
+
versions: {
|
|
371
|
+
'1.0.0': { name: 'test-package', version: '1.0.0' },
|
|
372
|
+
'2.0.0': { name: 'test-package', version: '2.0.0' }
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
},
|
|
376
|
+
parsePackageVersion: (version) => ({ version })
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
const result = await testMarshall.validate(pkg)
|
|
381
|
+
expect(result).toBeDefined()
|
|
382
|
+
})
|
|
206
383
|
})
|
|
@@ -128,9 +128,5 @@ describe('Signature test suites', () => {
|
|
|
128
128
|
// We assert that the validate method didn't throw an error,
|
|
129
129
|
// because the keys match the signature
|
|
130
130
|
await expect(testMarshall.validate(pkg)).rejects.toThrow('mocked manifest error')
|
|
131
|
-
|
|
132
|
-
// Assert that the fetch method is called with the correct URL
|
|
133
|
-
// eslint-disable-next-line no-undef
|
|
134
|
-
expect(fetch).toHaveBeenCalledWith('https://registry.npmjs.org/-/npm/v1/keys')
|
|
135
131
|
})
|
|
136
132
|
})
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Marshall = require('../lib/marshalls/snyk.marshall.js')
|
|
4
|
+
const { marshallCategories } = require('../lib/marshalls/constants')
|
|
5
|
+
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const os = require('os')
|
|
8
|
+
|
|
9
|
+
jest.mock('fs')
|
|
10
|
+
jest.mock('os')
|
|
11
|
+
|
|
12
|
+
os.homedir.mockReturnValue('/fake/home')
|
|
13
|
+
fs.statSync.mockImplementation(() => {
|
|
14
|
+
throw new Error('File not found')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// Mock fetch globally
|
|
18
|
+
global.fetch = jest.fn()
|
|
19
|
+
|
|
20
|
+
const mockPackageRepoUtils = {
|
|
21
|
+
getLatestVersion: jest.fn().mockResolvedValue('1.0.0')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('Snyk Marshall', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
os.homedir.mockReturnValue('/fake/home')
|
|
27
|
+
fetch.mockClear()
|
|
28
|
+
mockPackageRepoUtils.getLatestVersion.mockClear()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('with Snyk token', () => {
|
|
32
|
+
const marshall = new Marshall({
|
|
33
|
+
packageRepoUtils: mockPackageRepoUtils
|
|
34
|
+
})
|
|
35
|
+
// Manually set the token for this test suite
|
|
36
|
+
marshall.snykApiToken = 'fake-snyk-token'
|
|
37
|
+
|
|
38
|
+
it('has the right title', () => {
|
|
39
|
+
expect(marshall.title()).toBe('Checking for known vulnerabilities')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should throw an error if vulnerabilities are found', async () => {
|
|
43
|
+
fetch.mockResolvedValueOnce({
|
|
44
|
+
ok: true,
|
|
45
|
+
json: () =>
|
|
46
|
+
Promise.resolve({
|
|
47
|
+
vulnerabilities: [{ title: 'XSS' }, { title: 'CSRF' }],
|
|
48
|
+
isMaliciousPackage: false
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const pkg = { packageName: 'test-pkg', packageVersion: '1.0.0' }
|
|
53
|
+
await expect(marshall.validate(pkg)).rejects.toThrow(
|
|
54
|
+
'2 vulnerable path(s) found: https://snyk.io/vuln/npm:test-pkg'
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should throw a specific error for malicious packages', async () => {
|
|
59
|
+
fetch.mockResolvedValueOnce({
|
|
60
|
+
ok: true,
|
|
61
|
+
json: () =>
|
|
62
|
+
Promise.resolve({
|
|
63
|
+
vulnerabilities: [{ title: 'Malicious Package' }],
|
|
64
|
+
isMaliciousPackage: true
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const pkg = { packageName: 'malicious-pkg', packageVersion: '1.0.0' }
|
|
69
|
+
await expect(marshall.validate(pkg)).rejects.toThrow(
|
|
70
|
+
'Malicious package found: https://snyk.io/vuln/npm:malicious-pkg'
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should pass if no vulnerabilities are found', async () => {
|
|
75
|
+
fetch.mockResolvedValueOnce({
|
|
76
|
+
ok: true,
|
|
77
|
+
json: () =>
|
|
78
|
+
Promise.resolve({
|
|
79
|
+
vulnerabilities: [],
|
|
80
|
+
isMaliciousPackage: false
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const pkg = { packageName: 'clean-pkg', packageVersion: '1.0.0' }
|
|
85
|
+
await expect(marshall.validate(pkg)).resolves.not.toThrow()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should throw an error if the Snyk API request fails', async () => {
|
|
89
|
+
fetch.mockResolvedValueOnce({
|
|
90
|
+
ok: false,
|
|
91
|
+
status: 500
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const pkg = { packageName: 'test-pkg', packageVersion: '1.0.0' }
|
|
95
|
+
await expect(marshall.validate(pkg)).rejects.toThrow(
|
|
96
|
+
'Snyk API request failed with status 500'
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should throw an error if Snyk API provides no vulnerability info', async () => {
|
|
101
|
+
fetch.mockResolvedValueOnce({
|
|
102
|
+
ok: true,
|
|
103
|
+
json: () => Promise.resolve({ message: 'no vulns here' }) // No 'vulnerabilities' property
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const pkg = { packageName: 'test-pkg', packageVersion: '1.0.0' }
|
|
107
|
+
// This will cause getSnykVulnInfo to return `false`, which makes validate throw
|
|
108
|
+
await expect(marshall.validate(pkg)).rejects.toThrow(
|
|
109
|
+
'Unable to query vulnerabilities for packages'
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should use latest version if packageVersion is not defined', async () => {
|
|
114
|
+
fetch.mockResolvedValueOnce({
|
|
115
|
+
ok: true,
|
|
116
|
+
json: () =>
|
|
117
|
+
Promise.resolve({
|
|
118
|
+
vulnerabilities: [],
|
|
119
|
+
isMaliciousPackage: false
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const pkg = { packageName: 'test-pkg' }
|
|
124
|
+
await marshall.validate(pkg)
|
|
125
|
+
|
|
126
|
+
expect(mockPackageRepoUtils.getLatestVersion).toHaveBeenCalledWith('test-pkg')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('without Snyk token (fallback to OSV)', () => {
|
|
131
|
+
const marshall = new Marshall({
|
|
132
|
+
packageRepoUtils: mockPackageRepoUtils
|
|
133
|
+
})
|
|
134
|
+
// Ensure no token is set
|
|
135
|
+
marshall.snykApiToken = null
|
|
136
|
+
|
|
137
|
+
it('should throw an error if vulnerabilities are found by OSV', async () => {
|
|
138
|
+
fetch.mockResolvedValueOnce({
|
|
139
|
+
ok: true,
|
|
140
|
+
json: () => Promise.resolve({ vulns: [{ id: 'OSV-1' }, { id: 'OSV-2' }] })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const pkg = { packageName: 'test-pkg-osv', packageVersion: '1.0.0' }
|
|
144
|
+
await expect(marshall.validate(pkg)).rejects.toThrow(
|
|
145
|
+
'2 vulnerabilities found by OSV for test-pkg-osv'
|
|
146
|
+
)
|
|
147
|
+
expect(fetch).toHaveBeenCalledWith('https://api.osv.dev/v1/query', expect.any(Object))
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should pass if no vulnerabilities are found by OSV', async () => {
|
|
151
|
+
fetch.mockResolvedValueOnce({
|
|
152
|
+
ok: true,
|
|
153
|
+
json: () => Promise.resolve({ vulns: [] })
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const pkg = { packageName: 'clean-pkg-osv', packageVersion: '1.0.0' }
|
|
157
|
+
await expect(marshall.validate(pkg)).resolves.not.toThrow()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should handle an empty response from OSV', async () => {
|
|
161
|
+
fetch.mockResolvedValueOnce({
|
|
162
|
+
ok: true,
|
|
163
|
+
json: () => Promise.resolve({})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const pkg = { packageName: 'empty-res-pkg', packageVersion: '1.0.0' }
|
|
167
|
+
await expect(marshall.validate(pkg)).resolves.toEqual({
|
|
168
|
+
issuesCount: 0,
|
|
169
|
+
isMaliciousPackage: false
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should handle OSV API fetch error', async () => {
|
|
174
|
+
fetch.mockRejectedValueOnce(new Error('Network error'))
|
|
175
|
+
|
|
176
|
+
const pkg = { packageName: 'fetch-err-pkg', packageVersion: '1.0.0' }
|
|
177
|
+
const result = await marshall.validate(pkg)
|
|
178
|
+
|
|
179
|
+
expect(result).toEqual({ issuesCount: 0, isMaliciousPackage: false })
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('run method', () => {
|
|
184
|
+
const marshall = new Marshall({
|
|
185
|
+
packageRepoUtils: mockPackageRepoUtils
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should call checkPackage for each package', async () => {
|
|
189
|
+
const ctx = {
|
|
190
|
+
pkgs: [
|
|
191
|
+
{ packageName: 'pkg1', packageVersion: '1.0.0' },
|
|
192
|
+
{ packageName: 'pkg2', packageVersion: '2.0.0' }
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
const task = {}
|
|
196
|
+
marshall.checkPackage = jest.fn()
|
|
197
|
+
|
|
198
|
+
await marshall.run(ctx, task)
|
|
199
|
+
|
|
200
|
+
expect(marshall.checkPackage).toHaveBeenCalledTimes(2)
|
|
201
|
+
expect(marshall.checkPackage).toHaveBeenCalledWith(ctx.pkgs[0], ctx, task)
|
|
202
|
+
expect(marshall.checkPackage).toHaveBeenCalledWith(ctx.pkgs[1], ctx, task)
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('getSnykToken', () => {
|
|
207
|
+
it('should return null if config file does not exist', () => {
|
|
208
|
+
fs.statSync.mockImplementation(() => {
|
|
209
|
+
throw new Error('ENOENT')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const marshall = new Marshall({
|
|
213
|
+
packageRepoUtils: mockPackageRepoUtils
|
|
214
|
+
})
|
|
215
|
+
marshall.snykApiToken = null // ensure no token
|
|
216
|
+
const token = marshall.getSnykToken()
|
|
217
|
+
expect(token).toBeNull()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should return null if config file exists but has no token', () => {
|
|
221
|
+
fs.statSync.mockReturnValueOnce({ isFile: () => true })
|
|
222
|
+
jest.doMock('/fake/home/.config/configstore/snyk.json', () => ({}), { virtual: true })
|
|
223
|
+
|
|
224
|
+
const marshall = new Marshall({
|
|
225
|
+
packageRepoUtils: mockPackageRepoUtils
|
|
226
|
+
})
|
|
227
|
+
marshall.snykApiToken = null // ensure no token
|
|
228
|
+
const token = marshall.getSnykToken()
|
|
229
|
+
expect(token).toBeNull()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should return the token if the config file exists and has a token', () => {
|
|
233
|
+
fs.statSync.mockImplementation(() => ({ isFile: () => true }))
|
|
234
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({ api: 'a-real-token' }))
|
|
235
|
+
|
|
236
|
+
const marshall = new Marshall({
|
|
237
|
+
packageRepoUtils: mockPackageRepoUtils
|
|
238
|
+
})
|
|
239
|
+
marshall.snykApiToken = null // ensure no token
|
|
240
|
+
const token = marshall.getSnykToken()
|
|
241
|
+
expect(token).toBe('a-real-token')
|
|
242
|
+
|
|
243
|
+
// Reset mock to avoid affecting other tests
|
|
244
|
+
fs.statSync.mockImplementation(() => {
|
|
245
|
+
throw new Error('File not found')
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should return null if config file is malformed', () => {
|
|
250
|
+
fs.statSync.mockImplementation(() => ({ isFile: () => true }))
|
|
251
|
+
fs.readFileSync.mockReturnValue('not a valid json')
|
|
252
|
+
|
|
253
|
+
const marshall = new Marshall({
|
|
254
|
+
packageRepoUtils: mockPackageRepoUtils
|
|
255
|
+
})
|
|
256
|
+
marshall.snykApiToken = null // ensure no token
|
|
257
|
+
const token = marshall.getSnykToken()
|
|
258
|
+
expect(token).toBeNull()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should return null if statSync returns falsy', () => {
|
|
262
|
+
fs.statSync.mockReturnValue(false)
|
|
263
|
+
|
|
264
|
+
const marshall = new Marshall({
|
|
265
|
+
packageRepoUtils: mockPackageRepoUtils
|
|
266
|
+
})
|
|
267
|
+
marshall.snykApiToken = null // ensure no token
|
|
268
|
+
const token = marshall.getSnykToken()
|
|
269
|
+
expect(token).toBeNull()
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('getSnykToken with env var', () => {
|
|
274
|
+
const OLD_ENV = process.env
|
|
275
|
+
|
|
276
|
+
beforeEach(() => {
|
|
277
|
+
jest.resetModules() // Most important - it clears the cache
|
|
278
|
+
process.env = { ...OLD_ENV } // Make a copy
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
afterAll(() => {
|
|
282
|
+
process.env = OLD_ENV // Restore old environment
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('should return token from SNYK_TOKEN environment variable', () => {
|
|
286
|
+
process.env.SNYK_TOKEN = 'token-from-env'
|
|
287
|
+
const MarshallWithEnv = require('../lib/marshalls/snyk.marshall.js')
|
|
288
|
+
const marshall = new MarshallWithEnv({
|
|
289
|
+
packageRepoUtils: mockPackageRepoUtils
|
|
290
|
+
})
|
|
291
|
+
expect(marshall.getSnykToken()).toBe('token-from-env')
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
})
|
|
@@ -13,19 +13,19 @@ test('running marshall tasks succeeds', async () => {
|
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
const config = {
|
|
16
|
-
pkgs: ['express', 'semver'],
|
|
16
|
+
pkgs: [{ packageName: 'express' }, { packageName: 'semver' }],
|
|
17
17
|
packageRepoUtils: new PackageRepoUtilsMock()
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
expect(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
const results = await marshalls.tasks(config)
|
|
21
|
+
|
|
22
|
+
expect(results[0]['test.marshall']).toEqual({
|
|
23
|
+
status: null,
|
|
24
|
+
errors: [],
|
|
25
|
+
warnings: [],
|
|
26
|
+
data: { express: 'mock data check', semver: 'mock data check' },
|
|
27
|
+
marshall: 'test.marshall',
|
|
28
|
+
categoryId: 'PackageHealth'
|
|
29
29
|
})
|
|
30
30
|
})
|
|
31
31
|
|
|
@@ -35,13 +35,12 @@ test('running marshall tasks fails', async () => {
|
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
const config = {
|
|
38
|
-
pkgs: ['express', 'dockly'],
|
|
38
|
+
pkgs: [{ packageName: 'express' }, { packageName: 'dockly' }],
|
|
39
39
|
packageRepoUtils: new PackageRepoUtilsMock()
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
marshalls: {
|
|
42
|
+
const mockedResults = [
|
|
43
|
+
{
|
|
45
44
|
'test.marshall': {
|
|
46
45
|
status: null,
|
|
47
46
|
errors: [{ pkg: 'dockly', message: 'simulating mock error' }],
|
|
@@ -49,12 +48,12 @@ test('running marshall tasks fails', async () => {
|
|
|
49
48
|
data: { express: 'mock data check' }
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
|
-
|
|
51
|
+
]
|
|
53
52
|
|
|
54
|
-
await expect(marshalls.tasks(config)).resolves.toMatchObject(
|
|
53
|
+
await expect(marshalls.tasks(config)).resolves.toMatchObject(mockedResults)
|
|
55
54
|
})
|
|
56
55
|
|
|
57
|
-
test('running marshall tasks
|
|
56
|
+
test('running marshall tasks includes an error when single package is not found', async () => {
|
|
58
57
|
const PackageRepoUtilsNotFoundMock = class Fake {
|
|
59
58
|
getPackageInfo() {
|
|
60
59
|
return Promise.resolve({ error: 'Not found' })
|
|
@@ -70,10 +69,20 @@ test('running marshall tasks throws error when single package is not found', asy
|
|
|
70
69
|
packageRepoUtils: new PackageRepoUtilsNotFoundMock()
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
const mockedResults = [
|
|
73
|
+
{
|
|
74
|
+
not_found: {
|
|
75
|
+
status: null,
|
|
76
|
+
errors: [{ pkg: 'nonexistent-package', message: 'Package not found' }],
|
|
77
|
+
warnings: [],
|
|
78
|
+
data: {},
|
|
79
|
+
marshall: 'not_found',
|
|
80
|
+
categoryId: 'PackageHealth'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
await expect(marshalls.tasks(config)).resolves.toMatchObject(mockedResults)
|
|
77
86
|
})
|
|
78
87
|
|
|
79
88
|
test('running marshall tasks filters out not found packages when multiple packages provided', async () => {
|
|
@@ -100,7 +109,22 @@ test('running marshall tasks filters out not found packages when multiple packag
|
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
const result = await marshalls.tasks(config)
|
|
103
|
-
|
|
104
|
-
expect(result
|
|
105
|
-
|
|
112
|
+
|
|
113
|
+
expect(result[0]['not_found']).toEqual({
|
|
114
|
+
status: null,
|
|
115
|
+
errors: [{ pkg: 'nonexistent-package', message: 'Package not found' }],
|
|
116
|
+
warnings: [],
|
|
117
|
+
data: {},
|
|
118
|
+
marshall: 'not_found',
|
|
119
|
+
categoryId: 'PackageHealth'
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect(result[1]['test.marshall']).toEqual({
|
|
123
|
+
status: null,
|
|
124
|
+
errors: [],
|
|
125
|
+
warnings: [],
|
|
126
|
+
data: { express: 'mock data check', semver: 'mock data check' },
|
|
127
|
+
marshall: 'test.marshall',
|
|
128
|
+
categoryId: 'PackageHealth'
|
|
129
|
+
})
|
|
106
130
|
})
|