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 +14 -0
- package/bin/domain/usecases/DailySecurityScanUseCase.js +65 -10
- package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/usecases/DailySecurityScanUseCase.test.ts +483 -22
- package/src/domain/usecases/DailySecurityScanUseCase.ts +117 -25
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts +3 -2
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts.map +1 -1
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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}`,
|
|
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":";;;
|
|
1
|
+
{"version":3,"file":"DailySecurityScanUseCase.js","sourceRoot":"","sources":["../../../src/domain/usecases/DailySecurityScanUseCase.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -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,21 +311,28 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
285
311
|
mockHttpRepository,
|
|
286
312
|
} = buildUseCase();
|
|
287
313
|
|
|
288
|
-
mockLocalCommandRunner.runCommand.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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<
|
|
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
|
-
'
|
|
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 {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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;
|
|
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"}
|