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.
@@ -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: 2024-01-02',
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.mockResolvedValue({
237
- stdout: '',
238
- stderr: '',
239
- exitCode: 0,
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.mockResolvedValue({
261
- stdout: '',
262
- stderr: '',
263
- exitCode: 0,
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
- mockLocalCommandRunner.runCommand.mockImplementation(
289
- async (program, args) => {
290
- if (program === 'find' && args.includes('-maxdepth')) {
291
- return { stdout: '/repos/app/.git\n', stderr: '', exitCode: 0 };
292
- }
293
- return { stdout: '', stderr: '', exitCode: 0 };
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
- expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
332
- expect(mockIssueRepository.createNewIssue.mock.calls[0][0]).toBe(
333
- 'example-org',
334
- );
335
- expect(mockIssueRepository.createNewIssue.mock.calls[0][1]).toBe(
336
- 'security-reports',
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(mockIssueRepository.createNewIssue.mock.calls[0][3]).toContain(
342
- 'CVE-2024-0001',
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.mockResolvedValue({
358
- stdout: '',
359
- stderr: '',
360
- exitCode: 0,
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('does not create a KEV report issue when the product is absent from the scanned workspace', async () => {
394
- const {
395
- useCase,
396
- mockLocalCommandRunner,
397
- mockIssueRepository,
398
- mockHttpRepository,
399
- } = buildUseCase();
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' && args.includes('-maxdepth')) {
548
+ if (program === 'find') {
404
549
  return {
405
- stdout: '/repos/app/.git\n/repos/site/.git\n',
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' && args.includes('grep')) {
411
- return { stdout: '', stderr: '', exitCode: 1 };
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
- const gitGrepCalls = mockLocalCommandRunner.runCommand.mock.calls.filter(
443
- (call) => call[0] === 'git' && call[1].includes('grep'),
616
+ expect(errorSpy).toHaveBeenCalledWith(
617
+ expect.stringContaining('osv-scanner failed with exit code 127'),
444
618
  );
445
- expect(gitGrepCalls.map((call) => call[1])).toEqual([
446
- [
447
- '-C',
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('reports only the new KEV entries whose product is present in the scanned workspace', async () => {
473
- const {
474
- useCase,
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
- mockLocalCommandRunner.runCommand.mockImplementation(
481
- async (program, args) => {
482
- if (program === 'find' && args.includes('-maxdepth')) {
483
- return { stdout: '/repos/app/.git\n', stderr: '', exitCode: 0 };
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
- mockHttpRepository.get.mockResolvedValue(
496
- JSON.stringify({
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][3]).toContain(
530
- 'CVE-2024-0002',
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
- it('logs a failed repository search and keeps searching the remaining repositories', async () => {
538
- const {
539
- useCase,
540
- mockLocalCommandRunner,
541
- mockIssueRepository,
542
- mockHttpRepository,
543
- } = buildUseCase();
544
- const errorSpy = jest
545
- .spyOn(console, 'error')
546
- .mockImplementation(() => undefined);
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
- mockLocalCommandRunner.runCommand.mockImplementation(
549
- async (program, args) => {
550
- if (program === 'find' && args.includes('-maxdepth')) {
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/broken/.git\n/repos/app/.git\n',
760
+ stdout: '/repos/example-org/app/.git\n',
553
761
  stderr: '',
554
762
  exitCode: 0,
555
763
  };
556
764
  }
557
- if (program === 'git' && args.includes('grep')) {
558
- return args.includes('/repos/broken')
559
- ? {
560
- stdout: '',
561
- stderr: 'fatal: not a git repository',
562
- exitCode: 128,
563
- }
564
- : { stdout: '', stderr: '', exitCode: 0 };
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
- await useCase.run({
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
- expect(errorSpy).toHaveBeenCalledWith(
596
- 'Failed to search /repos/broken for LoadMaster: fatal: not a git repository',
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
- expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
599
- expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).toContain(
600
- 'CVE-2024-0001',
825
+
826
+ await runWithKevReporting(useCase);
827
+
828
+ const kevCalls = kevReportCalls(
829
+ mockIssueRepository.createNewIssue.mock.calls,
601
830
  );
602
- errorSpy.mockRestore();
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('throws when the KEV catalog format is unexpected', async () => {
606
- const { useCase, mockLocalCommandRunner, mockHttpRepository } =
607
- buildUseCase();
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
- mockLocalCommandRunner.runCommand.mockResolvedValue({
610
- stdout: '',
611
- stderr: '',
612
- exitCode: 0,
613
- });
614
- mockHttpRepository.get.mockResolvedValue(
615
- JSON.stringify({ unexpected: 'structure' }),
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 expect(
619
- useCase.run({
620
- targetDates: [new Date('2024-01-02T05:00:00Z')],
621
- org: 'example-org',
622
- manager: 'manager-name',
623
- dailySecurityScan: {
624
- scanBaseDirectory: '/repos',
625
- targetHourUtc: 5,
626
- enableKevNvdReport: true,
627
- kevReportRepo: 'security-reports',
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
- ).rejects.toThrow('Unexpected CISA KEV catalog format');
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
  });