github-issue-tower-defence-management 1.148.5 → 1.148.7
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/CHANGELOG.md +14 -0
- package/bin/domain/usecases/DailySecurityScanUseCase.js +88 -44
- package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/usecases/DailySecurityScanUseCase.test.ts +499 -197
- package/src/domain/usecases/DailySecurityScanUseCase.ts +156 -75
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts +2 -3
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts.map +1 -1
|
@@ -64,6 +64,8 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
64
64
|
const { useCase, mockLocalCommandRunner, mockIssueRepository } =
|
|
65
65
|
buildUseCase();
|
|
66
66
|
|
|
67
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
68
|
+
|
|
67
69
|
mockLocalCommandRunner.runCommand.mockImplementation(
|
|
68
70
|
async (program, args) => {
|
|
69
71
|
if (program === 'find') {
|
|
@@ -107,7 +109,7 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
107
109
|
);
|
|
108
110
|
expect(mockIssueRepository.createNewIssue.mock.calls[0][1]).toBe('app');
|
|
109
111
|
expect(mockIssueRepository.createNewIssue.mock.calls[0][2]).toBe(
|
|
110
|
-
'Daily security scan findings
|
|
112
|
+
'Daily security scan findings',
|
|
111
113
|
);
|
|
112
114
|
expect(mockIssueRepository.createNewIssue.mock.calls[0][4]).toEqual([
|
|
113
115
|
'manager-name',
|
|
@@ -233,10 +235,22 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
233
235
|
const { useCase, mockLocalCommandRunner, mockHttpRepository } =
|
|
234
236
|
buildUseCase();
|
|
235
237
|
|
|
236
|
-
mockLocalCommandRunner.runCommand.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
|
|
239
|
+
if (program === 'find') {
|
|
240
|
+
return {
|
|
241
|
+
stdout: '/repos/example-org/app/.git\n',
|
|
242
|
+
stderr: '',
|
|
243
|
+
exitCode: 0,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (program === 'git') {
|
|
247
|
+
return {
|
|
248
|
+
stdout: 'git@github.com:example-org/app.git\n',
|
|
249
|
+
stderr: '',
|
|
250
|
+
exitCode: 0,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
240
254
|
});
|
|
241
255
|
|
|
242
256
|
await useCase.run({
|
|
@@ -257,10 +271,22 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
257
271
|
const { useCase, mockLocalCommandRunner, mockHttpRepository } =
|
|
258
272
|
buildUseCase();
|
|
259
273
|
|
|
260
|
-
mockLocalCommandRunner.runCommand.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
274
|
+
mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
|
|
275
|
+
if (program === 'find') {
|
|
276
|
+
return {
|
|
277
|
+
stdout: '/repos/example-org/app/.git\n',
|
|
278
|
+
stderr: '',
|
|
279
|
+
exitCode: 0,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (program === 'git') {
|
|
283
|
+
return {
|
|
284
|
+
stdout: 'git@github.com:example-org/app.git\n',
|
|
285
|
+
stderr: '',
|
|
286
|
+
exitCode: 0,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
264
290
|
});
|
|
265
291
|
|
|
266
292
|
await useCase.run({
|
|
@@ -285,14 +311,53 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
285
311
|
mockHttpRepository,
|
|
286
312
|
} = buildUseCase();
|
|
287
313
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
314
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
315
|
+
mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
|
|
316
|
+
if (program === 'find') {
|
|
317
|
+
return {
|
|
318
|
+
stdout: '/repos/example-org/app/.git\n',
|
|
319
|
+
stderr: '',
|
|
320
|
+
exitCode: 0,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (program === 'git') {
|
|
324
|
+
return {
|
|
325
|
+
stdout: 'git@github.com:example-org/app.git\n',
|
|
326
|
+
stderr: '',
|
|
327
|
+
exitCode: 0,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (program === 'osv-scanner') {
|
|
331
|
+
return {
|
|
332
|
+
stdout: JSON.stringify({
|
|
333
|
+
results: [
|
|
334
|
+
{
|
|
335
|
+
source: { path: '/repos/example-org/app', type: 'lockfile' },
|
|
336
|
+
packages: [
|
|
337
|
+
{
|
|
338
|
+
package: {
|
|
339
|
+
name: 'example-library',
|
|
340
|
+
version: '1.2.3',
|
|
341
|
+
ecosystem: 'npm',
|
|
342
|
+
},
|
|
343
|
+
vulnerabilities: [
|
|
344
|
+
{
|
|
345
|
+
id: 'GHSA-1111-2222-3333',
|
|
346
|
+
aliases: ['CVE-2024-0001', 'CVE-2023-9999'],
|
|
347
|
+
summary: 'New Vulnerability',
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
}),
|
|
355
|
+
stderr: '',
|
|
356
|
+
exitCode: 1,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
360
|
+
});
|
|
296
361
|
mockHttpRepository.get.mockResolvedValue(
|
|
297
362
|
JSON.stringify({
|
|
298
363
|
vulnerabilities: [
|
|
@@ -328,22 +393,17 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
328
393
|
|
|
329
394
|
expect(mockHttpRepository.get.mock.calls).toHaveLength(1);
|
|
330
395
|
expect(mockHttpRepository.get.mock.calls[0][0]).toBe(KEV_CATALOG_URL);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
expect(
|
|
336
|
-
|
|
337
|
-
)
|
|
338
|
-
expect(mockIssueRepository.createNewIssue.mock.calls[0][2]).toBe(
|
|
396
|
+
const kevIssueCalls =
|
|
397
|
+
mockIssueRepository.createNewIssue.mock.calls.filter(
|
|
398
|
+
(call) => call[1] === 'security-reports',
|
|
399
|
+
);
|
|
400
|
+
expect(kevIssueCalls).toHaveLength(1);
|
|
401
|
+
expect(kevIssueCalls[0][0]).toBe('example-org');
|
|
402
|
+
expect(kevIssueCalls[0][2]).toBe(
|
|
339
403
|
'CISA KEV new additions since 2024-01-01',
|
|
340
404
|
);
|
|
341
|
-
expect(
|
|
342
|
-
|
|
343
|
-
);
|
|
344
|
-
expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).not.toContain(
|
|
345
|
-
'CVE-2023-9999',
|
|
346
|
-
);
|
|
405
|
+
expect(kevIssueCalls[0][3]).toContain('CVE-2024-0001');
|
|
406
|
+
expect(kevIssueCalls[0][3]).not.toContain('CVE-2023-9999');
|
|
347
407
|
});
|
|
348
408
|
|
|
349
409
|
it('does not create a KEV report issue when there are no new additions', async () => {
|
|
@@ -354,10 +414,22 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
354
414
|
mockHttpRepository,
|
|
355
415
|
} = buildUseCase();
|
|
356
416
|
|
|
357
|
-
mockLocalCommandRunner.runCommand.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
417
|
+
mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
|
|
418
|
+
if (program === 'find') {
|
|
419
|
+
return {
|
|
420
|
+
stdout: '/repos/example-org/app/.git\n',
|
|
421
|
+
stderr: '',
|
|
422
|
+
exitCode: 0,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (program === 'git') {
|
|
426
|
+
return {
|
|
427
|
+
stdout: 'git@github.com:example-org/app.git\n',
|
|
428
|
+
stderr: '',
|
|
429
|
+
exitCode: 0,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
361
433
|
});
|
|
362
434
|
mockHttpRepository.get.mockResolvedValue(
|
|
363
435
|
JSON.stringify({
|
|
@@ -390,42 +462,146 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
390
462
|
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
|
|
391
463
|
});
|
|
392
464
|
|
|
393
|
-
it('
|
|
394
|
-
const {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
465
|
+
it('throws when the KEV catalog format is unexpected', async () => {
|
|
466
|
+
const { useCase, mockLocalCommandRunner, mockHttpRepository } =
|
|
467
|
+
buildUseCase();
|
|
468
|
+
|
|
469
|
+
mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
|
|
470
|
+
if (program === 'find') {
|
|
471
|
+
return {
|
|
472
|
+
stdout: '/repos/example-org/app/.git\n',
|
|
473
|
+
stderr: '',
|
|
474
|
+
exitCode: 0,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (program === 'git') {
|
|
478
|
+
return {
|
|
479
|
+
stdout: 'git@github.com:example-org/app.git\n',
|
|
480
|
+
stderr: '',
|
|
481
|
+
exitCode: 0,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
485
|
+
});
|
|
486
|
+
mockHttpRepository.get.mockResolvedValue(
|
|
487
|
+
JSON.stringify({ unexpected: 'structure' }),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
await expect(
|
|
491
|
+
useCase.run({
|
|
492
|
+
targetDates: [new Date('2024-01-02T05:00:00Z')],
|
|
493
|
+
org: 'example-org',
|
|
494
|
+
manager: 'manager-name',
|
|
495
|
+
dailySecurityScan: {
|
|
496
|
+
scanBaseDirectory: '/repos',
|
|
497
|
+
targetHourUtc: 5,
|
|
498
|
+
enableKevNvdReport: true,
|
|
499
|
+
kevReportRepo: 'security-reports',
|
|
500
|
+
},
|
|
501
|
+
}),
|
|
502
|
+
).rejects.toThrow('Unexpected CISA KEV catalog format');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('logs an error and returns without throwing when no repositories are found in the scan base directory', async () => {
|
|
506
|
+
const { useCase, mockLocalCommandRunner } = buildUseCase();
|
|
507
|
+
const errorSpy = jest
|
|
508
|
+
.spyOn(console, 'error')
|
|
509
|
+
.mockImplementation(() => undefined);
|
|
510
|
+
|
|
511
|
+
mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
|
|
512
|
+
if (program === 'find') {
|
|
513
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
514
|
+
}
|
|
515
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await expect(
|
|
519
|
+
useCase.run({
|
|
520
|
+
targetDates: [new Date('2024-01-02T05:00:00Z')],
|
|
521
|
+
org: 'example-org',
|
|
522
|
+
manager: 'manager-name',
|
|
523
|
+
dailySecurityScan: {
|
|
524
|
+
scanBaseDirectory: '/repos',
|
|
525
|
+
targetHourUtc: 5,
|
|
526
|
+
},
|
|
527
|
+
}),
|
|
528
|
+
).resolves.toBeUndefined();
|
|
529
|
+
|
|
530
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
531
|
+
'No repositories found in scan base directory: /repos',
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
errorSpy.mockRestore();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('logs an error and continues scanning remaining repositories when osv-scanner exits with an unexpected code', async () => {
|
|
538
|
+
const { useCase, mockLocalCommandRunner, mockIssueRepository } =
|
|
539
|
+
buildUseCase();
|
|
540
|
+
const errorSpy = jest
|
|
541
|
+
.spyOn(console, 'error')
|
|
542
|
+
.mockImplementation(() => undefined);
|
|
543
|
+
|
|
544
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
400
545
|
|
|
401
546
|
mockLocalCommandRunner.runCommand.mockImplementation(
|
|
402
547
|
async (program, args) => {
|
|
403
|
-
if (program === 'find'
|
|
548
|
+
if (program === 'find') {
|
|
404
549
|
return {
|
|
405
|
-
stdout:
|
|
550
|
+
stdout:
|
|
551
|
+
'/repos/example-org/broken/.git\n/repos/example-org/app/.git\n',
|
|
406
552
|
stderr: '',
|
|
407
553
|
exitCode: 0,
|
|
408
554
|
};
|
|
409
555
|
}
|
|
410
|
-
if (program === 'git'
|
|
411
|
-
|
|
556
|
+
if (program === 'git') {
|
|
557
|
+
const dir = args[1];
|
|
558
|
+
const repo = dir.split('/').pop() ?? '';
|
|
559
|
+
return {
|
|
560
|
+
stdout: `git@github.com:example-org/${repo}.git\n`,
|
|
561
|
+
stderr: '',
|
|
562
|
+
exitCode: 0,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
if (program === 'osv-scanner') {
|
|
566
|
+
const scannedDirectory = args[args.indexOf('-r') + 1];
|
|
567
|
+
if (scannedDirectory.includes('broken')) {
|
|
568
|
+
return {
|
|
569
|
+
stdout: '',
|
|
570
|
+
stderr: 'osv-scanner: command not found',
|
|
571
|
+
exitCode: 127,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
stdout: JSON.stringify({
|
|
576
|
+
results: [
|
|
577
|
+
{
|
|
578
|
+
source: { path: scannedDirectory, type: 'lockfile' },
|
|
579
|
+
packages: [
|
|
580
|
+
{
|
|
581
|
+
package: {
|
|
582
|
+
name: 'example-library',
|
|
583
|
+
version: '1.2.3',
|
|
584
|
+
ecosystem: 'npm',
|
|
585
|
+
},
|
|
586
|
+
vulnerabilities: [
|
|
587
|
+
{
|
|
588
|
+
id: 'GHSA-1111-2222-3333',
|
|
589
|
+
aliases: [],
|
|
590
|
+
summary: 'Example Library Deserialization',
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
}),
|
|
598
|
+
stderr: '',
|
|
599
|
+
exitCode: 1,
|
|
600
|
+
};
|
|
412
601
|
}
|
|
413
602
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
414
603
|
},
|
|
415
604
|
);
|
|
416
|
-
mockHttpRepository.get.mockResolvedValue(
|
|
417
|
-
JSON.stringify({
|
|
418
|
-
vulnerabilities: [
|
|
419
|
-
{
|
|
420
|
-
cveID: 'CVE-2024-0001',
|
|
421
|
-
vendorProject: 'Progress',
|
|
422
|
-
product: 'LoadMaster',
|
|
423
|
-
vulnerabilityName: 'Progress LoadMaster Command Injection',
|
|
424
|
-
dateAdded: '2024-01-02',
|
|
425
|
-
},
|
|
426
|
-
],
|
|
427
|
-
}),
|
|
428
|
-
);
|
|
429
605
|
|
|
430
606
|
await useCase.run({
|
|
431
607
|
targetDates: [new Date('2024-01-02T05:00:00Z')],
|
|
@@ -434,84 +610,96 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
434
610
|
dailySecurityScan: {
|
|
435
611
|
scanBaseDirectory: '/repos',
|
|
436
612
|
targetHourUtc: 5,
|
|
437
|
-
enableKevNvdReport: true,
|
|
438
|
-
kevReportRepo: 'security-reports',
|
|
439
613
|
},
|
|
440
614
|
});
|
|
441
615
|
|
|
442
|
-
|
|
443
|
-
(
|
|
616
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
617
|
+
expect.stringContaining('osv-scanner failed with exit code 127'),
|
|
444
618
|
);
|
|
445
|
-
expect(
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
'/repos/app',
|
|
449
|
-
'grep',
|
|
450
|
-
'-I',
|
|
451
|
-
'-i',
|
|
452
|
-
'-q',
|
|
453
|
-
'-F',
|
|
454
|
-
'-e',
|
|
455
|
-
'LoadMaster',
|
|
456
|
-
],
|
|
457
|
-
[
|
|
458
|
-
'-C',
|
|
459
|
-
'/repos/site',
|
|
460
|
-
'grep',
|
|
461
|
-
'-I',
|
|
462
|
-
'-i',
|
|
463
|
-
'-q',
|
|
464
|
-
'-F',
|
|
465
|
-
'-e',
|
|
466
|
-
'LoadMaster',
|
|
467
|
-
],
|
|
468
|
-
]);
|
|
469
|
-
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
|
|
619
|
+
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
|
|
620
|
+
expect(mockIssueRepository.createNewIssue.mock.calls[0][1]).toBe('app');
|
|
621
|
+
errorSpy.mockRestore();
|
|
470
622
|
});
|
|
471
623
|
|
|
472
|
-
it('
|
|
473
|
-
const {
|
|
474
|
-
|
|
475
|
-
mockLocalCommandRunner,
|
|
476
|
-
mockIssueRepository,
|
|
477
|
-
mockHttpRepository,
|
|
478
|
-
} = buildUseCase();
|
|
624
|
+
it('adds a comment to the existing open issue for the repository instead of creating a new one', async () => {
|
|
625
|
+
const { useCase, mockLocalCommandRunner, mockIssueRepository } =
|
|
626
|
+
buildUseCase();
|
|
479
627
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (program === 'git' && args.includes('grep')) {
|
|
486
|
-
return {
|
|
487
|
-
stdout: '',
|
|
488
|
-
stderr: '',
|
|
489
|
-
exitCode: args.includes('LoadMaster') ? 1 : 0,
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
return { stdout: '', stderr: '', exitCode: 0 };
|
|
628
|
+
mockIssueRepository.searchIssue.mockResolvedValue([
|
|
629
|
+
{
|
|
630
|
+
url: 'https://github.com/example-org/app/issues/42',
|
|
631
|
+
title: 'Daily security scan findings',
|
|
632
|
+
number: '42',
|
|
493
633
|
},
|
|
634
|
+
]);
|
|
635
|
+
|
|
636
|
+
mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
|
|
637
|
+
if (program === 'find') {
|
|
638
|
+
return {
|
|
639
|
+
stdout: '/repos/example-org/app/.git\n',
|
|
640
|
+
stderr: '',
|
|
641
|
+
exitCode: 0,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (program === 'git') {
|
|
645
|
+
return {
|
|
646
|
+
stdout: 'git@github.com:example-org/app.git\n',
|
|
647
|
+
stderr: '',
|
|
648
|
+
exitCode: 0,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (program === 'osv-scanner') {
|
|
652
|
+
return { stdout: 'vulnerability found', stderr: '', exitCode: 1 };
|
|
653
|
+
}
|
|
654
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
await useCase.run({
|
|
658
|
+
targetDates: [new Date('2024-01-02T05:00:00Z')],
|
|
659
|
+
org: 'example-org',
|
|
660
|
+
manager: 'manager-name',
|
|
661
|
+
dailySecurityScan: {
|
|
662
|
+
scanBaseDirectory: '/repos',
|
|
663
|
+
targetHourUtc: 5,
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
|
|
668
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls).toHaveLength(1);
|
|
669
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls[0][0]).toBe(
|
|
670
|
+
'https://github.com/example-org/app/issues/42',
|
|
494
671
|
);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
vulnerabilities: [
|
|
498
|
-
{
|
|
499
|
-
cveID: 'CVE-2024-0001',
|
|
500
|
-
vendorProject: 'Progress',
|
|
501
|
-
product: 'LoadMaster',
|
|
502
|
-
vulnerabilityName: 'Progress LoadMaster Command Injection',
|
|
503
|
-
dateAdded: '2024-01-02',
|
|
504
|
-
},
|
|
505
|
-
{
|
|
506
|
-
cveID: 'CVE-2024-0002',
|
|
507
|
-
vendorProject: 'Example',
|
|
508
|
-
product: 'ExampleLibrary',
|
|
509
|
-
vulnerabilityName: 'Example Library Deserialization',
|
|
510
|
-
dateAdded: '2024-01-02',
|
|
511
|
-
},
|
|
512
|
-
],
|
|
513
|
-
}),
|
|
672
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls[0][1]).toContain(
|
|
673
|
+
'2024-01-02',
|
|
514
674
|
);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('creates a new issue when no existing open issue exists for the repository', async () => {
|
|
678
|
+
const { useCase, mockLocalCommandRunner, mockIssueRepository } =
|
|
679
|
+
buildUseCase();
|
|
680
|
+
|
|
681
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
682
|
+
|
|
683
|
+
mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
|
|
684
|
+
if (program === 'find') {
|
|
685
|
+
return {
|
|
686
|
+
stdout: '/repos/example-org/app/.git\n',
|
|
687
|
+
stderr: '',
|
|
688
|
+
exitCode: 0,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
if (program === 'git') {
|
|
692
|
+
return {
|
|
693
|
+
stdout: 'git@github.com:example-org/app.git\n',
|
|
694
|
+
stderr: '',
|
|
695
|
+
exitCode: 0,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
if (program === 'osv-scanner') {
|
|
699
|
+
return { stdout: 'vulnerability found', stderr: '', exitCode: 1 };
|
|
700
|
+
}
|
|
701
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
702
|
+
});
|
|
515
703
|
|
|
516
704
|
await useCase.run({
|
|
517
705
|
targetDates: [new Date('2024-01-02T05:00:00Z')],
|
|
@@ -520,67 +708,85 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
520
708
|
dailySecurityScan: {
|
|
521
709
|
scanBaseDirectory: '/repos',
|
|
522
710
|
targetHourUtc: 5,
|
|
523
|
-
enableKevNvdReport: true,
|
|
524
|
-
kevReportRepo: 'security-reports',
|
|
525
711
|
},
|
|
526
712
|
});
|
|
527
713
|
|
|
528
714
|
expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
|
|
529
|
-
expect(mockIssueRepository.createNewIssue.mock.calls[0][
|
|
530
|
-
'
|
|
531
|
-
);
|
|
532
|
-
expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).not.toContain(
|
|
533
|
-
'CVE-2024-0001',
|
|
715
|
+
expect(mockIssueRepository.createNewIssue.mock.calls[0][2]).toBe(
|
|
716
|
+
'Daily security scan findings',
|
|
534
717
|
);
|
|
718
|
+
expect(mockIssueRepository.createCommentByUrl.mock.calls).toHaveLength(0);
|
|
535
719
|
});
|
|
536
720
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
721
|
+
const osvScanOutput = (
|
|
722
|
+
vulnerablePackages: {
|
|
723
|
+
name: string;
|
|
724
|
+
version: string;
|
|
725
|
+
ecosystem: string;
|
|
726
|
+
id: string;
|
|
727
|
+
aliases: string[];
|
|
728
|
+
summary: string;
|
|
729
|
+
}[],
|
|
730
|
+
): string =>
|
|
731
|
+
JSON.stringify({
|
|
732
|
+
results: [
|
|
733
|
+
{
|
|
734
|
+
source: { path: '/repos/example-org/app', type: 'lockfile' },
|
|
735
|
+
packages: vulnerablePackages.map((vulnerablePackage) => ({
|
|
736
|
+
package: {
|
|
737
|
+
name: vulnerablePackage.name,
|
|
738
|
+
version: vulnerablePackage.version,
|
|
739
|
+
ecosystem: vulnerablePackage.ecosystem,
|
|
740
|
+
},
|
|
741
|
+
vulnerabilities: [
|
|
742
|
+
{
|
|
743
|
+
id: vulnerablePackage.id,
|
|
744
|
+
aliases: vulnerablePackage.aliases,
|
|
745
|
+
summary: vulnerablePackage.summary,
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
})),
|
|
749
|
+
},
|
|
750
|
+
],
|
|
751
|
+
});
|
|
547
752
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
753
|
+
const buildScanEnvironment = (scanOutput: string, kevCatalog: unknown) => {
|
|
754
|
+
const built = buildUseCase();
|
|
755
|
+
built.mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
756
|
+
built.mockLocalCommandRunner.runCommand.mockImplementation(
|
|
757
|
+
async (program) => {
|
|
758
|
+
if (program === 'find') {
|
|
551
759
|
return {
|
|
552
|
-
stdout: '/repos/
|
|
760
|
+
stdout: '/repos/example-org/app/.git\n',
|
|
553
761
|
stderr: '',
|
|
554
762
|
exitCode: 0,
|
|
555
763
|
};
|
|
556
764
|
}
|
|
557
|
-
if (program === 'git'
|
|
558
|
-
return
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
765
|
+
if (program === 'git') {
|
|
766
|
+
return {
|
|
767
|
+
stdout: 'git@github.com:example-org/app.git\n',
|
|
768
|
+
stderr: '',
|
|
769
|
+
exitCode: 0,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
if (program === 'osv-scanner') {
|
|
773
|
+
return { stdout: scanOutput, stderr: '', exitCode: 1 };
|
|
565
774
|
}
|
|
566
775
|
return { stdout: '', stderr: '', exitCode: 0 };
|
|
567
776
|
},
|
|
568
777
|
);
|
|
569
|
-
mockHttpRepository.get.mockResolvedValue(
|
|
570
|
-
JSON.stringify(
|
|
571
|
-
vulnerabilities: [
|
|
572
|
-
{
|
|
573
|
-
cveID: 'CVE-2024-0001',
|
|
574
|
-
vendorProject: 'Progress',
|
|
575
|
-
product: 'LoadMaster',
|
|
576
|
-
vulnerabilityName: 'Progress LoadMaster Command Injection',
|
|
577
|
-
dateAdded: '2024-01-02',
|
|
578
|
-
},
|
|
579
|
-
],
|
|
580
|
-
}),
|
|
778
|
+
built.mockHttpRepository.get.mockResolvedValue(
|
|
779
|
+
JSON.stringify(kevCatalog),
|
|
581
780
|
);
|
|
781
|
+
return built;
|
|
782
|
+
};
|
|
582
783
|
|
|
583
|
-
|
|
784
|
+
const kevReportCalls = (
|
|
785
|
+
calls: [string, string, string, string, string[], string[]][],
|
|
786
|
+
) => calls.filter((call) => call[1] === 'security-reports');
|
|
787
|
+
|
|
788
|
+
const runWithKevReporting = async (useCase: DailySecurityScanUseCase) =>
|
|
789
|
+
useCase.run({
|
|
584
790
|
targetDates: [new Date('2024-01-02T05:00:00Z')],
|
|
585
791
|
org: 'example-org',
|
|
586
792
|
manager: 'manager-name',
|
|
@@ -592,42 +798,138 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
592
798
|
},
|
|
593
799
|
});
|
|
594
800
|
|
|
595
|
-
|
|
596
|
-
|
|
801
|
+
it('reports a KEV addition whose CVE the scanner found at an installed version', async () => {
|
|
802
|
+
const { useCase, mockIssueRepository } = buildScanEnvironment(
|
|
803
|
+
osvScanOutput([
|
|
804
|
+
{
|
|
805
|
+
name: 'example-library',
|
|
806
|
+
version: '1.2.3',
|
|
807
|
+
ecosystem: 'npm',
|
|
808
|
+
id: 'GHSA-1111-2222-3333',
|
|
809
|
+
aliases: ['CVE-2024-0001'],
|
|
810
|
+
summary: 'Example Library Deserialization',
|
|
811
|
+
},
|
|
812
|
+
]),
|
|
813
|
+
{
|
|
814
|
+
vulnerabilities: [
|
|
815
|
+
{
|
|
816
|
+
cveID: 'CVE-2024-0001',
|
|
817
|
+
vendorProject: 'Example',
|
|
818
|
+
product: 'ExampleLibrary',
|
|
819
|
+
vulnerabilityName: 'Example Library Deserialization',
|
|
820
|
+
dateAdded: '2024-01-02',
|
|
821
|
+
},
|
|
822
|
+
],
|
|
823
|
+
},
|
|
597
824
|
);
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
825
|
+
|
|
826
|
+
await runWithKevReporting(useCase);
|
|
827
|
+
|
|
828
|
+
const kevCalls = kevReportCalls(
|
|
829
|
+
mockIssueRepository.createNewIssue.mock.calls,
|
|
601
830
|
);
|
|
602
|
-
|
|
831
|
+
expect(kevCalls).toHaveLength(1);
|
|
832
|
+
expect(kevCalls[0][3]).toContain('CVE-2024-0001');
|
|
833
|
+
expect(kevCalls[0][3]).toContain('app');
|
|
834
|
+
expect(kevCalls[0][3]).toContain('example-library');
|
|
835
|
+
expect(kevCalls[0][3]).toContain('1.2.3');
|
|
603
836
|
});
|
|
604
837
|
|
|
605
|
-
it('
|
|
606
|
-
const { useCase,
|
|
607
|
-
|
|
838
|
+
it('does not report a KEV addition that the scanner did not find in any repository', async () => {
|
|
839
|
+
const { useCase, mockIssueRepository } = buildScanEnvironment(
|
|
840
|
+
osvScanOutput([
|
|
841
|
+
{
|
|
842
|
+
name: 'example-library',
|
|
843
|
+
version: '1.2.3',
|
|
844
|
+
ecosystem: 'npm',
|
|
845
|
+
id: 'GHSA-1111-2222-3333',
|
|
846
|
+
aliases: ['CVE-2024-0001'],
|
|
847
|
+
summary: 'Example Library Deserialization',
|
|
848
|
+
},
|
|
849
|
+
]),
|
|
850
|
+
{
|
|
851
|
+
vulnerabilities: [
|
|
852
|
+
{
|
|
853
|
+
cveID: 'CVE-2024-9999',
|
|
854
|
+
vendorProject: 'Progress',
|
|
855
|
+
product: 'LoadMaster',
|
|
856
|
+
vulnerabilityName: 'Progress LoadMaster Command Injection',
|
|
857
|
+
dateAdded: '2024-01-02',
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
},
|
|
861
|
+
);
|
|
608
862
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
863
|
+
await runWithKevReporting(useCase);
|
|
864
|
+
|
|
865
|
+
expect(
|
|
866
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
867
|
+
).toHaveLength(0);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('reports a KEV addition whose CVE the scanner returned as the vulnerability id rather than an alias', async () => {
|
|
871
|
+
const { useCase, mockIssueRepository } = buildScanEnvironment(
|
|
872
|
+
osvScanOutput([
|
|
873
|
+
{
|
|
874
|
+
name: 'example-library',
|
|
875
|
+
version: '1.2.3',
|
|
876
|
+
ecosystem: 'npm',
|
|
877
|
+
id: 'CVE-2024-0001',
|
|
878
|
+
aliases: [],
|
|
879
|
+
summary: 'Example Library Deserialization',
|
|
880
|
+
},
|
|
881
|
+
]),
|
|
882
|
+
{
|
|
883
|
+
vulnerabilities: [
|
|
884
|
+
{
|
|
885
|
+
cveID: 'CVE-2024-0001',
|
|
886
|
+
vendorProject: 'Example',
|
|
887
|
+
product: 'ExampleLibrary',
|
|
888
|
+
vulnerabilityName: 'Example Library Deserialization',
|
|
889
|
+
dateAdded: '2024-01-02',
|
|
890
|
+
},
|
|
891
|
+
],
|
|
892
|
+
},
|
|
616
893
|
);
|
|
617
894
|
|
|
618
|
-
await
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
895
|
+
await runWithKevReporting(useCase);
|
|
896
|
+
|
|
897
|
+
expect(
|
|
898
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
899
|
+
).toHaveLength(1);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it('does not search the repositories for the KEV product name', async () => {
|
|
903
|
+
const { useCase, mockLocalCommandRunner } = buildScanEnvironment(
|
|
904
|
+
osvScanOutput([
|
|
905
|
+
{
|
|
906
|
+
name: 'example-library',
|
|
907
|
+
version: '1.2.3',
|
|
908
|
+
ecosystem: 'npm',
|
|
909
|
+
id: 'GHSA-1111-2222-3333',
|
|
910
|
+
aliases: ['CVE-2024-0001'],
|
|
911
|
+
summary: 'Example Library Deserialization',
|
|
628
912
|
},
|
|
629
|
-
|
|
630
|
-
|
|
913
|
+
]),
|
|
914
|
+
{
|
|
915
|
+
vulnerabilities: [
|
|
916
|
+
{
|
|
917
|
+
cveID: 'CVE-2024-0001',
|
|
918
|
+
vendorProject: 'Example',
|
|
919
|
+
product: 'ExampleLibrary',
|
|
920
|
+
vulnerabilityName: 'Example Library Deserialization',
|
|
921
|
+
dateAdded: '2024-01-02',
|
|
922
|
+
},
|
|
923
|
+
],
|
|
924
|
+
},
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
await runWithKevReporting(useCase);
|
|
928
|
+
|
|
929
|
+
const grepCalls = mockLocalCommandRunner.runCommand.mock.calls.filter(
|
|
930
|
+
(call) => call[0] === 'git' && call[1].includes('grep'),
|
|
931
|
+
);
|
|
932
|
+
expect(grepCalls).toHaveLength(0);
|
|
631
933
|
});
|
|
632
934
|
});
|
|
633
935
|
});
|