github-issue-tower-defence-management 1.148.4 → 1.148.6

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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## [1.148.6](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.148.5...v1.148.6) (2026-08-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **security-scan:** fix silent scan failures and issue deduplication ([#1479](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1479)) ([7cdc714](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/7cdc7149fe1420bfb4581014220e8b70c6043b34))
7
+
8
+ ## [1.148.5](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.148.4...v1.148.5) (2026-08-09)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **security-scan:** report KEV additions only for products present in the workspace ([#1475](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/issues/1475)) ([70b9442](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/commit/70b9442f21c46948319a32f8f296b1e290f450ec))
14
+
1
15
  ## [1.148.4](https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/compare/v1.148.3...v1.148.4) (2026-08-09)
2
16
 
3
17
 
@@ -7,6 +7,8 @@ const isKevVulnerability = (value) => {
7
7
  }
8
8
  const record = { ...value };
9
9
  return (typeof record.cveID === 'string' &&
10
+ typeof record.vendorProject === 'string' &&
11
+ typeof record.product === 'string' &&
10
12
  typeof record.vulnerabilityName === 'string' &&
11
13
  typeof record.dateAdded === 'string');
12
14
  };
@@ -38,10 +40,8 @@ class DailySecurityScanUseCase {
38
40
  this.scanRepositories = async (org, manager, today, config) => {
39
41
  const { stdout: findOutput } = await this.localCommandRunner.runCommand('find', [
40
42
  config.scanBaseDirectory,
41
- '-mindepth',
42
- '4',
43
43
  '-maxdepth',
44
- '4',
44
+ '5',
45
45
  '-name',
46
46
  '.git',
47
47
  '-type',
@@ -51,6 +51,10 @@ class DailySecurityScanUseCase {
51
51
  .split('\n')
52
52
  .filter((line) => line.length > 0)
53
53
  .map((gitDirectory) => gitDirectory.replace(/\/\.git$/, ''));
54
+ if (repositoryDirectories.length === 0) {
55
+ console.error(`No repositories found in scan base directory: ${config.scanBaseDirectory}`);
56
+ return;
57
+ }
54
58
  for (const repositoryDirectory of repositoryDirectories) {
55
59
  const { stdout: remoteUrl, exitCode: remoteExitCode } = await this.localCommandRunner.runCommand('git', [
56
60
  '-C',
@@ -70,16 +74,34 @@ class DailySecurityScanUseCase {
70
74
  }
71
75
  const repositoryOrg = remoteMatch[1];
72
76
  const repositoryName = remoteMatch[2];
73
- const { stdout: scanOutput, exitCode: scanExitCode } = await this.localCommandRunner.runCommand('osv-scanner', [
77
+ const { stdout: scanOutput, stderr: scanStderr, exitCode: scanExitCode, } = await this.localCommandRunner.runCommand('osv-scanner', [
74
78
  'scan',
75
79
  'source',
76
80
  '-r',
77
81
  repositoryDirectory,
78
82
  ]);
83
+ if (scanExitCode === 0) {
84
+ continue;
85
+ }
79
86
  if (scanExitCode !== 1) {
87
+ console.error(`osv-scanner failed with exit code ${scanExitCode} for ${repositoryDirectory}: ${scanStderr}`);
80
88
  continue;
81
89
  }
82
- await this.issueRepository.createNewIssue(repositoryOrg, repositoryName, `Daily security scan findings: ${today}`, `## OSV-Scanner findings\n\n\`\`\`\n${scanOutput}\n\`\`\``, [manager], []);
90
+ const findingsBody = `## OSV-Scanner findings\n\n### ${today}\n\n\`\`\`\n${scanOutput}\n\`\`\``;
91
+ const existingIssues = await this.issueRepository.searchIssue({
92
+ owner: repositoryOrg,
93
+ repositoryName,
94
+ type: 'issue',
95
+ state: 'open',
96
+ title: 'Daily security scan findings',
97
+ });
98
+ const existingIssue = existingIssues.find((issue) => issue.title === 'Daily security scan findings');
99
+ if (existingIssue) {
100
+ await this.issueRepository.createCommentByUrl(existingIssue.url, findingsBody);
101
+ }
102
+ else {
103
+ await this.issueRepository.createNewIssue(repositoryOrg, repositoryName, 'Daily security scan findings', findingsBody, [manager], []);
104
+ }
83
105
  }
84
106
  };
85
107
  this.reportKevAdditions = async (org, manager, lastTargetDate, config) => {
@@ -94,13 +116,46 @@ class DailySecurityScanUseCase {
94
116
  if (!isKevCatalog(parsedKev)) {
95
117
  throw new Error(`Unexpected CISA KEV catalog format from ${KEV_CATALOG_URL}`);
96
118
  }
97
- const newKevEntries = parsedKev.vulnerabilities
98
- .filter((vulnerability) => vulnerability.dateAdded >= yesterdayYmd)
99
- .map((vulnerability) => `- ${vulnerability.dateAdded} ${vulnerability.cveID} ${vulnerability.vulnerabilityName}`);
100
- if (newKevEntries.length === 0) {
119
+ const newKevEntries = parsedKev.vulnerabilities.filter((vulnerability) => vulnerability.dateAdded >= yesterdayYmd);
120
+ const usedKevEntries = [];
121
+ for (const vulnerability of newKevEntries) {
122
+ if (await this.isProductPresentInScannedWorkspace(config.scanBaseDirectory, vulnerability.product)) {
123
+ usedKevEntries.push(vulnerability);
124
+ }
125
+ }
126
+ if (usedKevEntries.length === 0) {
101
127
  return;
102
128
  }
103
- await this.issueRepository.createNewIssue(org, config.kevReportRepo, `CISA KEV new additions since ${yesterdayYmd}`, newKevEntries.join('\n'), [manager], []);
129
+ await this.issueRepository.createNewIssue(org, config.kevReportRepo, `CISA KEV new additions since ${yesterdayYmd}`, usedKevEntries
130
+ .map((vulnerability) => `- ${vulnerability.dateAdded} ${vulnerability.cveID} ${vulnerability.vulnerabilityName}`)
131
+ .join('\n'), [manager], []);
132
+ };
133
+ this.isProductPresentInScannedWorkspace = async (scanBaseDirectory, product) => {
134
+ const { stdout: findOutput } = await this.localCommandRunner.runCommand('find', [scanBaseDirectory, '-maxdepth', '3', '-name', '.git', '-type', 'd']);
135
+ const repositoryDirectories = findOutput
136
+ .split('\n')
137
+ .filter((line) => line.length > 0)
138
+ .map((gitDirectory) => gitDirectory.replace(/\/\.git$/, ''));
139
+ for (const repositoryDirectory of repositoryDirectories) {
140
+ const { stderr, exitCode } = await this.localCommandRunner.runCommand('git', [
141
+ '-C',
142
+ repositoryDirectory,
143
+ 'grep',
144
+ '-I',
145
+ '-i',
146
+ '-q',
147
+ '-F',
148
+ '-e',
149
+ product,
150
+ ]);
151
+ if (exitCode === 0) {
152
+ return true;
153
+ }
154
+ if (exitCode !== 1) {
155
+ console.error(`Failed to search ${repositoryDirectory} for ${product}: ${stderr}`);
156
+ }
157
+ }
158
+ return false;
104
159
  };
105
160
  }
106
161
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DailySecurityScanUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/DailySecurityScanUseCase.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DailySecurityScanUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/DailySecurityScanUseCase.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.148.4",
3
+ "version": "1.148.6",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -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,21 +311,28 @@ describe('DailySecurityScanUseCase', () => {
285
311
  mockHttpRepository,
286
312
  } = buildUseCase();
287
313
 
288
- mockLocalCommandRunner.runCommand.mockResolvedValue({
289
- stdout: '',
290
- stderr: '',
291
- exitCode: 0,
292
- });
314
+ mockLocalCommandRunner.runCommand.mockImplementation(
315
+ async (program, args) => {
316
+ if (program === 'find' && args.includes('-maxdepth')) {
317
+ return { stdout: '/repos/app/.git\n', stderr: '', exitCode: 0 };
318
+ }
319
+ return { stdout: '', stderr: '', exitCode: 0 };
320
+ },
321
+ );
293
322
  mockHttpRepository.get.mockResolvedValue(
294
323
  JSON.stringify({
295
324
  vulnerabilities: [
296
325
  {
297
326
  cveID: 'CVE-2024-0001',
327
+ vendorProject: 'Example',
328
+ product: 'NewProduct',
298
329
  vulnerabilityName: 'New Vulnerability',
299
330
  dateAdded: '2024-01-02',
300
331
  },
301
332
  {
302
333
  cveID: 'CVE-2023-9999',
334
+ vendorProject: 'Example',
335
+ product: 'OldProduct',
303
336
  vulnerabilityName: 'Old Vulnerability',
304
337
  dateAdded: '2023-12-31',
305
338
  },
@@ -347,16 +380,30 @@ describe('DailySecurityScanUseCase', () => {
347
380
  mockHttpRepository,
348
381
  } = buildUseCase();
349
382
 
350
- mockLocalCommandRunner.runCommand.mockResolvedValue({
351
- stdout: '',
352
- stderr: '',
353
- exitCode: 0,
383
+ mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
384
+ if (program === 'find') {
385
+ return {
386
+ stdout: '/repos/example-org/app/.git\n',
387
+ stderr: '',
388
+ exitCode: 0,
389
+ };
390
+ }
391
+ if (program === 'git') {
392
+ return {
393
+ stdout: 'git@github.com:example-org/app.git\n',
394
+ stderr: '',
395
+ exitCode: 0,
396
+ };
397
+ }
398
+ return { stdout: '', stderr: '', exitCode: 0 };
354
399
  });
355
400
  mockHttpRepository.get.mockResolvedValue(
356
401
  JSON.stringify({
357
402
  vulnerabilities: [
358
403
  {
359
404
  cveID: 'CVE-2023-9999',
405
+ vendorProject: 'Example',
406
+ product: 'OldProduct',
360
407
  vulnerabilityName: 'Old Vulnerability',
361
408
  dateAdded: '2023-12-31',
362
409
  },
@@ -381,14 +428,238 @@ describe('DailySecurityScanUseCase', () => {
381
428
  expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
382
429
  });
383
430
 
431
+ it('does not create a KEV report issue when the product is absent from the scanned workspace', async () => {
432
+ const {
433
+ useCase,
434
+ mockLocalCommandRunner,
435
+ mockIssueRepository,
436
+ mockHttpRepository,
437
+ } = buildUseCase();
438
+
439
+ mockLocalCommandRunner.runCommand.mockImplementation(
440
+ async (program, args) => {
441
+ if (program === 'find' && args.includes('-maxdepth')) {
442
+ return {
443
+ stdout: '/repos/app/.git\n/repos/site/.git\n',
444
+ stderr: '',
445
+ exitCode: 0,
446
+ };
447
+ }
448
+ if (program === 'git' && args.includes('grep')) {
449
+ return { stdout: '', stderr: '', exitCode: 1 };
450
+ }
451
+ return { stdout: '', stderr: '', exitCode: 0 };
452
+ },
453
+ );
454
+ mockHttpRepository.get.mockResolvedValue(
455
+ JSON.stringify({
456
+ vulnerabilities: [
457
+ {
458
+ cveID: 'CVE-2024-0001',
459
+ vendorProject: 'Progress',
460
+ product: 'LoadMaster',
461
+ vulnerabilityName: 'Progress LoadMaster Command Injection',
462
+ dateAdded: '2024-01-02',
463
+ },
464
+ ],
465
+ }),
466
+ );
467
+
468
+ await useCase.run({
469
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
470
+ org: 'example-org',
471
+ manager: 'manager-name',
472
+ dailySecurityScan: {
473
+ scanBaseDirectory: '/repos',
474
+ targetHourUtc: 5,
475
+ enableKevNvdReport: true,
476
+ kevReportRepo: 'security-reports',
477
+ },
478
+ });
479
+
480
+ const gitGrepCalls = mockLocalCommandRunner.runCommand.mock.calls.filter(
481
+ (call) => call[0] === 'git' && call[1].includes('grep'),
482
+ );
483
+ expect(gitGrepCalls.map((call) => call[1])).toEqual([
484
+ [
485
+ '-C',
486
+ '/repos/app',
487
+ 'grep',
488
+ '-I',
489
+ '-i',
490
+ '-q',
491
+ '-F',
492
+ '-e',
493
+ 'LoadMaster',
494
+ ],
495
+ [
496
+ '-C',
497
+ '/repos/site',
498
+ 'grep',
499
+ '-I',
500
+ '-i',
501
+ '-q',
502
+ '-F',
503
+ '-e',
504
+ 'LoadMaster',
505
+ ],
506
+ ]);
507
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
508
+ });
509
+
510
+ it('reports only the new KEV entries whose product is present in the scanned workspace', async () => {
511
+ const {
512
+ useCase,
513
+ mockLocalCommandRunner,
514
+ mockIssueRepository,
515
+ mockHttpRepository,
516
+ } = buildUseCase();
517
+
518
+ mockLocalCommandRunner.runCommand.mockImplementation(
519
+ async (program, args) => {
520
+ if (program === 'find' && args.includes('-maxdepth')) {
521
+ return { stdout: '/repos/app/.git\n', stderr: '', exitCode: 0 };
522
+ }
523
+ if (program === 'git' && args.includes('grep')) {
524
+ return {
525
+ stdout: '',
526
+ stderr: '',
527
+ exitCode: args.includes('LoadMaster') ? 1 : 0,
528
+ };
529
+ }
530
+ return { stdout: '', stderr: '', exitCode: 0 };
531
+ },
532
+ );
533
+ mockHttpRepository.get.mockResolvedValue(
534
+ JSON.stringify({
535
+ vulnerabilities: [
536
+ {
537
+ cveID: 'CVE-2024-0001',
538
+ vendorProject: 'Progress',
539
+ product: 'LoadMaster',
540
+ vulnerabilityName: 'Progress LoadMaster Command Injection',
541
+ dateAdded: '2024-01-02',
542
+ },
543
+ {
544
+ cveID: 'CVE-2024-0002',
545
+ vendorProject: 'Example',
546
+ product: 'ExampleLibrary',
547
+ vulnerabilityName: 'Example Library Deserialization',
548
+ dateAdded: '2024-01-02',
549
+ },
550
+ ],
551
+ }),
552
+ );
553
+
554
+ await useCase.run({
555
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
556
+ org: 'example-org',
557
+ manager: 'manager-name',
558
+ dailySecurityScan: {
559
+ scanBaseDirectory: '/repos',
560
+ targetHourUtc: 5,
561
+ enableKevNvdReport: true,
562
+ kevReportRepo: 'security-reports',
563
+ },
564
+ });
565
+
566
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
567
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).toContain(
568
+ 'CVE-2024-0002',
569
+ );
570
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).not.toContain(
571
+ 'CVE-2024-0001',
572
+ );
573
+ });
574
+
575
+ it('logs a failed repository search and keeps searching the remaining repositories', async () => {
576
+ const {
577
+ useCase,
578
+ mockLocalCommandRunner,
579
+ mockIssueRepository,
580
+ mockHttpRepository,
581
+ } = buildUseCase();
582
+ const errorSpy = jest
583
+ .spyOn(console, 'error')
584
+ .mockImplementation(() => undefined);
585
+
586
+ mockLocalCommandRunner.runCommand.mockImplementation(
587
+ async (program, args) => {
588
+ if (program === 'find' && args.includes('-maxdepth')) {
589
+ return {
590
+ stdout: '/repos/broken/.git\n/repos/app/.git\n',
591
+ stderr: '',
592
+ exitCode: 0,
593
+ };
594
+ }
595
+ if (program === 'git' && args.includes('grep')) {
596
+ return args.includes('/repos/broken')
597
+ ? {
598
+ stdout: '',
599
+ stderr: 'fatal: not a git repository',
600
+ exitCode: 128,
601
+ }
602
+ : { stdout: '', stderr: '', exitCode: 0 };
603
+ }
604
+ return { stdout: '', stderr: '', exitCode: 0 };
605
+ },
606
+ );
607
+ mockHttpRepository.get.mockResolvedValue(
608
+ JSON.stringify({
609
+ vulnerabilities: [
610
+ {
611
+ cveID: 'CVE-2024-0001',
612
+ vendorProject: 'Progress',
613
+ product: 'LoadMaster',
614
+ vulnerabilityName: 'Progress LoadMaster Command Injection',
615
+ dateAdded: '2024-01-02',
616
+ },
617
+ ],
618
+ }),
619
+ );
620
+
621
+ await useCase.run({
622
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
623
+ org: 'example-org',
624
+ manager: 'manager-name',
625
+ dailySecurityScan: {
626
+ scanBaseDirectory: '/repos',
627
+ targetHourUtc: 5,
628
+ enableKevNvdReport: true,
629
+ kevReportRepo: 'security-reports',
630
+ },
631
+ });
632
+
633
+ expect(errorSpy).toHaveBeenCalledWith(
634
+ 'Failed to search /repos/broken for LoadMaster: fatal: not a git repository',
635
+ );
636
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
637
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).toContain(
638
+ 'CVE-2024-0001',
639
+ );
640
+ errorSpy.mockRestore();
641
+ });
642
+
384
643
  it('throws when the KEV catalog format is unexpected', async () => {
385
644
  const { useCase, mockLocalCommandRunner, mockHttpRepository } =
386
645
  buildUseCase();
387
646
 
388
- mockLocalCommandRunner.runCommand.mockResolvedValue({
389
- stdout: '',
390
- stderr: '',
391
- exitCode: 0,
647
+ mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
648
+ if (program === 'find') {
649
+ return {
650
+ stdout: '/repos/example-org/app/.git\n',
651
+ stderr: '',
652
+ exitCode: 0,
653
+ };
654
+ }
655
+ if (program === 'git') {
656
+ return {
657
+ stdout: 'git@github.com:example-org/app.git\n',
658
+ stderr: '',
659
+ exitCode: 0,
660
+ };
661
+ }
662
+ return { stdout: '', stderr: '', exitCode: 0 };
392
663
  });
393
664
  mockHttpRepository.get.mockResolvedValue(
394
665
  JSON.stringify({ unexpected: 'structure' }),
@@ -408,5 +679,195 @@ describe('DailySecurityScanUseCase', () => {
408
679
  }),
409
680
  ).rejects.toThrow('Unexpected CISA KEV catalog format');
410
681
  });
682
+
683
+ it('logs an error and returns without throwing when no repositories are found in the scan base directory', async () => {
684
+ const { useCase, mockLocalCommandRunner } = buildUseCase();
685
+ const errorSpy = jest
686
+ .spyOn(console, 'error')
687
+ .mockImplementation(() => undefined);
688
+
689
+ mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
690
+ if (program === 'find') {
691
+ return { stdout: '', stderr: '', exitCode: 0 };
692
+ }
693
+ return { stdout: '', stderr: '', exitCode: 0 };
694
+ });
695
+
696
+ await expect(
697
+ useCase.run({
698
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
699
+ org: 'example-org',
700
+ manager: 'manager-name',
701
+ dailySecurityScan: {
702
+ scanBaseDirectory: '/repos',
703
+ targetHourUtc: 5,
704
+ },
705
+ }),
706
+ ).resolves.toBeUndefined();
707
+
708
+ expect(errorSpy).toHaveBeenCalledWith(
709
+ 'No repositories found in scan base directory: /repos',
710
+ );
711
+
712
+ errorSpy.mockRestore();
713
+ });
714
+
715
+ it('logs an error and continues scanning remaining repositories when osv-scanner exits with an unexpected code', async () => {
716
+ const { useCase, mockLocalCommandRunner, mockIssueRepository } =
717
+ buildUseCase();
718
+ const errorSpy = jest
719
+ .spyOn(console, 'error')
720
+ .mockImplementation(() => undefined);
721
+
722
+ mockIssueRepository.searchIssue.mockResolvedValue([]);
723
+
724
+ mockLocalCommandRunner.runCommand.mockImplementation(
725
+ async (program, args) => {
726
+ if (program === 'find') {
727
+ return {
728
+ stdout:
729
+ '/repos/example-org/broken/.git\n/repos/example-org/app/.git\n',
730
+ stderr: '',
731
+ exitCode: 0,
732
+ };
733
+ }
734
+ if (program === 'git') {
735
+ const dir = args[1];
736
+ const repo = dir.split('/').pop() ?? '';
737
+ return {
738
+ stdout: `git@github.com:example-org/${repo}.git\n`,
739
+ stderr: '',
740
+ exitCode: 0,
741
+ };
742
+ }
743
+ if (program === 'osv-scanner') {
744
+ const dir = args[args.length - 1];
745
+ if (dir.includes('broken')) {
746
+ return {
747
+ stdout: '',
748
+ stderr: 'osv-scanner: command not found',
749
+ exitCode: 127,
750
+ };
751
+ }
752
+ return { stdout: 'vulnerability found', stderr: '', exitCode: 1 };
753
+ }
754
+ return { stdout: '', stderr: '', exitCode: 0 };
755
+ },
756
+ );
757
+
758
+ await useCase.run({
759
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
760
+ org: 'example-org',
761
+ manager: 'manager-name',
762
+ dailySecurityScan: {
763
+ scanBaseDirectory: '/repos',
764
+ targetHourUtc: 5,
765
+ },
766
+ });
767
+
768
+ expect(errorSpy).toHaveBeenCalledWith(
769
+ expect.stringContaining('osv-scanner failed with exit code 127'),
770
+ );
771
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
772
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][1]).toBe('app');
773
+ errorSpy.mockRestore();
774
+ });
775
+
776
+ it('adds a comment to the existing open issue for the repository instead of creating a new one', async () => {
777
+ const { useCase, mockLocalCommandRunner, mockIssueRepository } =
778
+ buildUseCase();
779
+
780
+ mockIssueRepository.searchIssue.mockResolvedValue([
781
+ {
782
+ url: 'https://github.com/example-org/app/issues/42',
783
+ title: 'Daily security scan findings',
784
+ number: '42',
785
+ },
786
+ ]);
787
+
788
+ mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
789
+ if (program === 'find') {
790
+ return {
791
+ stdout: '/repos/example-org/app/.git\n',
792
+ stderr: '',
793
+ exitCode: 0,
794
+ };
795
+ }
796
+ if (program === 'git') {
797
+ return {
798
+ stdout: 'git@github.com:example-org/app.git\n',
799
+ stderr: '',
800
+ exitCode: 0,
801
+ };
802
+ }
803
+ if (program === 'osv-scanner') {
804
+ return { stdout: 'vulnerability found', stderr: '', exitCode: 1 };
805
+ }
806
+ return { stdout: '', stderr: '', exitCode: 0 };
807
+ });
808
+
809
+ await useCase.run({
810
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
811
+ org: 'example-org',
812
+ manager: 'manager-name',
813
+ dailySecurityScan: {
814
+ scanBaseDirectory: '/repos',
815
+ targetHourUtc: 5,
816
+ },
817
+ });
818
+
819
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
820
+ expect(mockIssueRepository.createCommentByUrl.mock.calls).toHaveLength(1);
821
+ expect(mockIssueRepository.createCommentByUrl.mock.calls[0][0]).toBe(
822
+ 'https://github.com/example-org/app/issues/42',
823
+ );
824
+ expect(mockIssueRepository.createCommentByUrl.mock.calls[0][1]).toContain(
825
+ '2024-01-02',
826
+ );
827
+ });
828
+
829
+ it('creates a new issue when no existing open issue exists for the repository', async () => {
830
+ const { useCase, mockLocalCommandRunner, mockIssueRepository } =
831
+ buildUseCase();
832
+
833
+ mockIssueRepository.searchIssue.mockResolvedValue([]);
834
+
835
+ mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
836
+ if (program === 'find') {
837
+ return {
838
+ stdout: '/repos/example-org/app/.git\n',
839
+ stderr: '',
840
+ exitCode: 0,
841
+ };
842
+ }
843
+ if (program === 'git') {
844
+ return {
845
+ stdout: 'git@github.com:example-org/app.git\n',
846
+ stderr: '',
847
+ exitCode: 0,
848
+ };
849
+ }
850
+ if (program === 'osv-scanner') {
851
+ return { stdout: 'vulnerability found', stderr: '', exitCode: 1 };
852
+ }
853
+ return { stdout: '', stderr: '', exitCode: 0 };
854
+ });
855
+
856
+ await useCase.run({
857
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
858
+ org: 'example-org',
859
+ manager: 'manager-name',
860
+ dailySecurityScan: {
861
+ scanBaseDirectory: '/repos',
862
+ targetHourUtc: 5,
863
+ },
864
+ });
865
+
866
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
867
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][2]).toBe(
868
+ 'Daily security scan findings',
869
+ );
870
+ expect(mockIssueRepository.createCommentByUrl.mock.calls).toHaveLength(0);
871
+ });
411
872
  });
412
873
  });
@@ -12,6 +12,8 @@ export type DailySecurityScanConfig = {
12
12
 
13
13
  type KevVulnerability = {
14
14
  cveID: string;
15
+ vendorProject: string;
16
+ product: string;
15
17
  vulnerabilityName: string;
16
18
  dateAdded: string;
17
19
  };
@@ -27,6 +29,8 @@ const isKevVulnerability = (value: unknown): value is KevVulnerability => {
27
29
  const record: Record<string, unknown> = { ...value };
28
30
  return (
29
31
  typeof record.cveID === 'string' &&
32
+ typeof record.vendorProject === 'string' &&
33
+ typeof record.product === 'string' &&
30
34
  typeof record.vulnerabilityName === 'string' &&
31
35
  typeof record.dateAdded === 'string'
32
36
  );
@@ -49,7 +53,10 @@ const KEV_CATALOG_URL =
49
53
  export class DailySecurityScanUseCase {
50
54
  constructor(
51
55
  readonly localCommandRunner: LocalCommandRunner,
52
- readonly issueRepository: Pick<IssueRepository, 'createNewIssue'>,
56
+ readonly issueRepository: Pick<
57
+ IssueRepository,
58
+ 'createNewIssue' | 'searchIssue' | 'createCommentByUrl'
59
+ >,
53
60
  readonly httpRepository: HttpRepository,
54
61
  ) {}
55
62
 
@@ -96,10 +103,8 @@ export class DailySecurityScanUseCase {
96
103
  'find',
97
104
  [
98
105
  config.scanBaseDirectory,
99
- '-mindepth',
100
- '4',
101
106
  '-maxdepth',
102
- '4',
107
+ '5',
103
108
  '-name',
104
109
  '.git',
105
110
  '-type',
@@ -112,6 +117,13 @@ export class DailySecurityScanUseCase {
112
117
  .filter((line) => line.length > 0)
113
118
  .map((gitDirectory) => gitDirectory.replace(/\/\.git$/, ''));
114
119
 
120
+ if (repositoryDirectories.length === 0) {
121
+ console.error(
122
+ `No repositories found in scan base directory: ${config.scanBaseDirectory}`,
123
+ );
124
+ return;
125
+ }
126
+
115
127
  for (const repositoryDirectory of repositoryDirectories) {
116
128
  const { stdout: remoteUrl, exitCode: remoteExitCode } =
117
129
  await this.localCommandRunner.runCommand('git', [
@@ -134,25 +146,52 @@ export class DailySecurityScanUseCase {
134
146
  const repositoryOrg = remoteMatch[1];
135
147
  const repositoryName = remoteMatch[2];
136
148
 
137
- const { stdout: scanOutput, exitCode: scanExitCode } =
138
- await this.localCommandRunner.runCommand('osv-scanner', [
139
- 'scan',
140
- 'source',
141
- '-r',
142
- repositoryDirectory,
143
- ]);
149
+ const {
150
+ stdout: scanOutput,
151
+ stderr: scanStderr,
152
+ exitCode: scanExitCode,
153
+ } = await this.localCommandRunner.runCommand('osv-scanner', [
154
+ 'scan',
155
+ 'source',
156
+ '-r',
157
+ repositoryDirectory,
158
+ ]);
159
+ if (scanExitCode === 0) {
160
+ continue;
161
+ }
144
162
  if (scanExitCode !== 1) {
163
+ console.error(
164
+ `osv-scanner failed with exit code ${scanExitCode} for ${repositoryDirectory}: ${scanStderr}`,
165
+ );
145
166
  continue;
146
167
  }
147
168
 
148
- await this.issueRepository.createNewIssue(
149
- repositoryOrg,
169
+ const findingsBody = `## OSV-Scanner findings\n\n### ${today}\n\n\`\`\`\n${scanOutput}\n\`\`\``;
170
+ const existingIssues = await this.issueRepository.searchIssue({
171
+ owner: repositoryOrg,
150
172
  repositoryName,
151
- `Daily security scan findings: ${today}`,
152
- `## OSV-Scanner findings\n\n\`\`\`\n${scanOutput}\n\`\`\``,
153
- [manager],
154
- [],
173
+ type: 'issue',
174
+ state: 'open',
175
+ title: 'Daily security scan findings',
176
+ });
177
+ const existingIssue = existingIssues.find(
178
+ (issue) => issue.title === 'Daily security scan findings',
155
179
  );
180
+ if (existingIssue) {
181
+ await this.issueRepository.createCommentByUrl(
182
+ existingIssue.url,
183
+ findingsBody,
184
+ );
185
+ } else {
186
+ await this.issueRepository.createNewIssue(
187
+ repositoryOrg,
188
+ repositoryName,
189
+ 'Daily security scan findings',
190
+ findingsBody,
191
+ [manager],
192
+ [],
193
+ );
194
+ }
156
195
  }
157
196
  };
158
197
 
@@ -178,13 +217,21 @@ export class DailySecurityScanUseCase {
178
217
  );
179
218
  }
180
219
 
181
- const newKevEntries = parsedKev.vulnerabilities
182
- .filter((vulnerability) => vulnerability.dateAdded >= yesterdayYmd)
183
- .map(
184
- (vulnerability) =>
185
- `- ${vulnerability.dateAdded} ${vulnerability.cveID} ${vulnerability.vulnerabilityName}`,
186
- );
187
- if (newKevEntries.length === 0) {
220
+ const newKevEntries = parsedKev.vulnerabilities.filter(
221
+ (vulnerability) => vulnerability.dateAdded >= yesterdayYmd,
222
+ );
223
+ const usedKevEntries: KevVulnerability[] = [];
224
+ for (const vulnerability of newKevEntries) {
225
+ if (
226
+ await this.isProductPresentInScannedWorkspace(
227
+ config.scanBaseDirectory,
228
+ vulnerability.product,
229
+ )
230
+ ) {
231
+ usedKevEntries.push(vulnerability);
232
+ }
233
+ }
234
+ if (usedKevEntries.length === 0) {
188
235
  return;
189
236
  }
190
237
 
@@ -192,9 +239,54 @@ export class DailySecurityScanUseCase {
192
239
  org,
193
240
  config.kevReportRepo,
194
241
  `CISA KEV new additions since ${yesterdayYmd}`,
195
- newKevEntries.join('\n'),
242
+ usedKevEntries
243
+ .map(
244
+ (vulnerability) =>
245
+ `- ${vulnerability.dateAdded} ${vulnerability.cveID} ${vulnerability.vulnerabilityName}`,
246
+ )
247
+ .join('\n'),
196
248
  [manager],
197
249
  [],
198
250
  );
199
251
  };
252
+
253
+ private isProductPresentInScannedWorkspace = async (
254
+ scanBaseDirectory: string,
255
+ product: string,
256
+ ): Promise<boolean> => {
257
+ const { stdout: findOutput } = await this.localCommandRunner.runCommand(
258
+ 'find',
259
+ [scanBaseDirectory, '-maxdepth', '3', '-name', '.git', '-type', 'd'],
260
+ );
261
+ const repositoryDirectories = findOutput
262
+ .split('\n')
263
+ .filter((line) => line.length > 0)
264
+ .map((gitDirectory) => gitDirectory.replace(/\/\.git$/, ''));
265
+
266
+ for (const repositoryDirectory of repositoryDirectories) {
267
+ const { stderr, exitCode } = await this.localCommandRunner.runCommand(
268
+ 'git',
269
+ [
270
+ '-C',
271
+ repositoryDirectory,
272
+ 'grep',
273
+ '-I',
274
+ '-i',
275
+ '-q',
276
+ '-F',
277
+ '-e',
278
+ product,
279
+ ],
280
+ );
281
+ if (exitCode === 0) {
282
+ return true;
283
+ }
284
+ if (exitCode !== 1) {
285
+ console.error(
286
+ `Failed to search ${repositoryDirectory} for ${product}: ${stderr}`,
287
+ );
288
+ }
289
+ }
290
+ return false;
291
+ };
200
292
  }
@@ -10,9 +10,9 @@ export type DailySecurityScanConfig = {
10
10
  };
11
11
  export declare class DailySecurityScanUseCase {
12
12
  readonly localCommandRunner: LocalCommandRunner;
13
- readonly issueRepository: Pick<IssueRepository, 'createNewIssue'>;
13
+ readonly issueRepository: Pick<IssueRepository, 'createNewIssue' | 'searchIssue' | 'createCommentByUrl'>;
14
14
  readonly httpRepository: HttpRepository;
15
- constructor(localCommandRunner: LocalCommandRunner, issueRepository: Pick<IssueRepository, 'createNewIssue'>, httpRepository: HttpRepository);
15
+ constructor(localCommandRunner: LocalCommandRunner, issueRepository: Pick<IssueRepository, 'createNewIssue' | 'searchIssue' | 'createCommentByUrl'>, httpRepository: HttpRepository);
16
16
  run: (input: {
17
17
  targetDates: Date[];
18
18
  org: string;
@@ -21,5 +21,6 @@ export declare class DailySecurityScanUseCase {
21
21
  }) => Promise<void>;
22
22
  private scanRepositories;
23
23
  private reportKevAdditions;
24
+ private isProductPresentInScannedWorkspace;
24
25
  }
25
26
  //# sourceMappingURL=DailySecurityScanUseCase.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DailySecurityScanUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/DailySecurityScanUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAsCF,qBAAa,wBAAwB;IAEjC,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB;IAC/C,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,eAAe,EAAE,gBAAgB,CAAC;IACjE,QAAQ,CAAC,cAAc,EAAE,cAAc;gBAF9B,kBAAkB,EAAE,kBAAkB,EACtC,eAAe,EAAE,IAAI,CAAC,eAAe,EAAE,gBAAgB,CAAC,EACxD,cAAc,EAAE,cAAc;IAGzC,GAAG,GAAU,OAAO;QAClB,WAAW,EAAE,IAAI,EAAE,CAAC;QACpB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACxB,iBAAiB,EAAE,uBAAuB,CAAC;KAC5C,KAAG,OAAO,CAAC,IAAI,CAAC,CA0Bf;IAEF,OAAO,CAAC,gBAAgB,CAoEtB;IAEF,OAAO,CAAC,kBAAkB,CAwCxB;CACH"}
1
+ {"version":3,"file":"DailySecurityScanUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/DailySecurityScanUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AA0CF,qBAAa,wBAAwB;IAEjC,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB;IAC/C,QAAQ,CAAC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD;IACD,QAAQ,CAAC,cAAc,EAAE,cAAc;gBAL9B,kBAAkB,EAAE,kBAAkB,EACtC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD,EACQ,cAAc,EAAE,cAAc;IAGzC,GAAG,GAAU,OAAO;QAClB,WAAW,EAAE,IAAI,EAAE,CAAC;QACpB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACxB,iBAAiB,EAAE,uBAAuB,CAAC;KAC5C,KAAG,OAAO,CAAC,IAAI,CAAC,CA0Bf;IAEF,OAAO,CAAC,gBAAgB,CAoGtB;IAEF,OAAO,CAAC,kBAAkB,CAqDxB;IAEF,OAAO,CAAC,kCAAkC,CAsCxC;CACH"}