github-issue-tower-defence-management 1.74.1 → 1.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +10 -0
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +46 -17
  4. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  5. package/bin/adapter/repositories/KyHttpRepository.js +16 -0
  6. package/bin/adapter/repositories/KyHttpRepository.js.map +1 -0
  7. package/bin/domain/usecases/DailySecurityScanUseCase.js +108 -0
  8. package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -0
  9. package/bin/domain/usecases/HandleScheduledEventUseCase.js +10 -1
  10. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  11. package/bin/domain/usecases/adapter-interfaces/HttpRepository.js +3 -0
  12. package/bin/domain/usecases/adapter-interfaces/HttpRepository.js.map +1 -0
  13. package/package.json +1 -1
  14. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
  15. package/src/adapter/repositories/KyHttpRepository.ts +8 -0
  16. package/src/domain/usecases/DailySecurityScanUseCase.test.ts +412 -0
  17. package/src/domain/usecases/DailySecurityScanUseCase.ts +200 -0
  18. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +3 -0
  19. package/src/domain/usecases/HandleScheduledEventUseCase.ts +14 -0
  20. package/src/domain/usecases/adapter-interfaces/HttpRepository.ts +3 -0
  21. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  22. package/types/adapter/repositories/KyHttpRepository.d.ts +5 -0
  23. package/types/adapter/repositories/KyHttpRepository.d.ts.map +1 -0
  24. package/types/domain/usecases/DailySecurityScanUseCase.d.ts +25 -0
  25. package/types/domain/usecases/DailySecurityScanUseCase.d.ts.map +1 -0
  26. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +4 -1
  27. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  28. package/types/domain/usecases/adapter-interfaces/HttpRepository.d.ts +4 -0
  29. package/types/domain/usecases/adapter-interfaces/HttpRepository.d.ts.map +1 -0
@@ -0,0 +1,412 @@
1
+ import { DailySecurityScanUseCase } from './DailySecurityScanUseCase';
2
+ import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
3
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
4
+ import { HttpRepository } from './adapter-interfaces/HttpRepository';
5
+ import { mock } from 'jest-mock-extended';
6
+
7
+ const KEV_CATALOG_URL =
8
+ 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json';
9
+
10
+ describe('DailySecurityScanUseCase', () => {
11
+ const buildUseCase = () => {
12
+ const mockLocalCommandRunner = mock<LocalCommandRunner>();
13
+ const mockIssueRepository = mock<IssueRepository>();
14
+ const mockHttpRepository = mock<HttpRepository>();
15
+ const useCase = new DailySecurityScanUseCase(
16
+ mockLocalCommandRunner,
17
+ mockIssueRepository,
18
+ mockHttpRepository,
19
+ );
20
+ return {
21
+ useCase,
22
+ mockLocalCommandRunner,
23
+ mockIssueRepository,
24
+ mockHttpRepository,
25
+ };
26
+ };
27
+
28
+ describe('run', () => {
29
+ it('does nothing when no target date matches the configured hour', async () => {
30
+ const { useCase, mockLocalCommandRunner, mockIssueRepository } =
31
+ buildUseCase();
32
+
33
+ await useCase.run({
34
+ targetDates: [new Date('2024-01-01T03:30:00Z')],
35
+ org: 'example-org',
36
+ manager: 'manager-name',
37
+ dailySecurityScan: {
38
+ scanBaseDirectory: '/repos',
39
+ targetHourUtc: 5,
40
+ },
41
+ });
42
+
43
+ expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
44
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
45
+ });
46
+
47
+ it('does not run when the matching hour is not at minute zero', async () => {
48
+ const { useCase, mockLocalCommandRunner } = buildUseCase();
49
+
50
+ await useCase.run({
51
+ targetDates: [new Date('2024-01-01T05:15:00Z')],
52
+ org: 'example-org',
53
+ manager: 'manager-name',
54
+ dailySecurityScan: {
55
+ scanBaseDirectory: '/repos',
56
+ targetHourUtc: 5,
57
+ },
58
+ });
59
+
60
+ expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
61
+ });
62
+
63
+ it('scans repositories and creates an issue when osv-scanner finds vulnerabilities', async () => {
64
+ const { useCase, mockLocalCommandRunner, mockIssueRepository } =
65
+ buildUseCase();
66
+
67
+ mockLocalCommandRunner.runCommand.mockImplementation(
68
+ async (program, args) => {
69
+ if (program === 'find') {
70
+ return {
71
+ stdout: '/repos/example-org/app/.git\n',
72
+ stderr: '',
73
+ exitCode: 0,
74
+ };
75
+ }
76
+ if (program === 'git') {
77
+ return {
78
+ stdout: 'git@github.com:example-org/app.git\n',
79
+ stderr: '',
80
+ exitCode: 0,
81
+ };
82
+ }
83
+ if (program === 'osv-scanner') {
84
+ return {
85
+ stdout: 'vulnerability detected in ' + args[args.length - 1],
86
+ stderr: '',
87
+ exitCode: 1,
88
+ };
89
+ }
90
+ return { stdout: '', stderr: '', exitCode: 0 };
91
+ },
92
+ );
93
+
94
+ await useCase.run({
95
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
96
+ org: 'example-org',
97
+ manager: 'manager-name',
98
+ dailySecurityScan: {
99
+ scanBaseDirectory: '/repos',
100
+ targetHourUtc: 5,
101
+ },
102
+ });
103
+
104
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
105
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][0]).toBe(
106
+ 'example-org',
107
+ );
108
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][1]).toBe('app');
109
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][2]).toBe(
110
+ 'Daily security scan findings: 2024-01-02',
111
+ );
112
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][4]).toEqual([
113
+ 'manager-name',
114
+ ]);
115
+ });
116
+
117
+ it('does not create an issue when osv-scanner reports no vulnerabilities', async () => {
118
+ const { useCase, mockLocalCommandRunner, mockIssueRepository } =
119
+ buildUseCase();
120
+
121
+ mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
122
+ if (program === 'find') {
123
+ return {
124
+ stdout: '/repos/example-org/app/.git\n',
125
+ stderr: '',
126
+ exitCode: 0,
127
+ };
128
+ }
129
+ if (program === 'git') {
130
+ return {
131
+ stdout: 'git@github.com:example-org/app.git\n',
132
+ stderr: '',
133
+ exitCode: 0,
134
+ };
135
+ }
136
+ return { stdout: 'no vulnerabilities', stderr: '', exitCode: 0 };
137
+ });
138
+
139
+ await useCase.run({
140
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
141
+ org: 'example-org',
142
+ manager: 'manager-name',
143
+ dailySecurityScan: {
144
+ scanBaseDirectory: '/repos',
145
+ targetHourUtc: 5,
146
+ },
147
+ });
148
+
149
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
150
+ });
151
+
152
+ it('skips repositories whose origin remote does not belong to the org', async () => {
153
+ const { useCase, mockLocalCommandRunner, mockIssueRepository } =
154
+ buildUseCase();
155
+
156
+ mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
157
+ if (program === 'find') {
158
+ return {
159
+ stdout: '/repos/other-org/app/.git\n',
160
+ stderr: '',
161
+ exitCode: 0,
162
+ };
163
+ }
164
+ if (program === 'git') {
165
+ return {
166
+ stdout: 'git@github.com:other-org/app.git\n',
167
+ stderr: '',
168
+ exitCode: 0,
169
+ };
170
+ }
171
+ return { stdout: '', stderr: '', exitCode: 1 };
172
+ });
173
+
174
+ await useCase.run({
175
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
176
+ org: 'example-org',
177
+ manager: 'manager-name',
178
+ dailySecurityScan: {
179
+ scanBaseDirectory: '/repos',
180
+ targetHourUtc: 5,
181
+ },
182
+ });
183
+
184
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
185
+ const osvScannerInvoked =
186
+ mockLocalCommandRunner.runCommand.mock.calls.some(
187
+ (call) => call[0] === 'osv-scanner',
188
+ );
189
+ expect(osvScannerInvoked).toBe(false);
190
+ });
191
+
192
+ it('skips repositories whose origin remote cannot be resolved', async () => {
193
+ const { useCase, mockLocalCommandRunner, mockIssueRepository } =
194
+ buildUseCase();
195
+
196
+ mockLocalCommandRunner.runCommand.mockImplementation(async (program) => {
197
+ if (program === 'find') {
198
+ return {
199
+ stdout: '/repos/example-org/app/.git\n',
200
+ stderr: '',
201
+ exitCode: 0,
202
+ };
203
+ }
204
+ if (program === 'git') {
205
+ return {
206
+ stdout: '',
207
+ stderr: 'fatal: No such remote',
208
+ exitCode: 2,
209
+ };
210
+ }
211
+ return { stdout: '', stderr: '', exitCode: 1 };
212
+ });
213
+
214
+ await useCase.run({
215
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
216
+ org: 'example-org',
217
+ manager: 'manager-name',
218
+ dailySecurityScan: {
219
+ scanBaseDirectory: '/repos',
220
+ targetHourUtc: 5,
221
+ },
222
+ });
223
+
224
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
225
+ const osvScannerInvoked =
226
+ mockLocalCommandRunner.runCommand.mock.calls.some(
227
+ (call) => call[0] === 'osv-scanner',
228
+ );
229
+ expect(osvScannerInvoked).toBe(false);
230
+ });
231
+
232
+ it('does not fetch the KEV catalog when KEV reporting is disabled', async () => {
233
+ const { useCase, mockLocalCommandRunner, mockHttpRepository } =
234
+ buildUseCase();
235
+
236
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
237
+ stdout: '',
238
+ stderr: '',
239
+ exitCode: 0,
240
+ });
241
+
242
+ await useCase.run({
243
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
244
+ org: 'example-org',
245
+ manager: 'manager-name',
246
+ dailySecurityScan: {
247
+ scanBaseDirectory: '/repos',
248
+ targetHourUtc: 5,
249
+ enableKevNvdReport: false,
250
+ },
251
+ });
252
+
253
+ expect(mockHttpRepository.get.mock.calls).toHaveLength(0);
254
+ });
255
+
256
+ it('does not fetch the KEV catalog when the report repo is missing', async () => {
257
+ const { useCase, mockLocalCommandRunner, mockHttpRepository } =
258
+ buildUseCase();
259
+
260
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
261
+ stdout: '',
262
+ stderr: '',
263
+ exitCode: 0,
264
+ });
265
+
266
+ await useCase.run({
267
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
268
+ org: 'example-org',
269
+ manager: 'manager-name',
270
+ dailySecurityScan: {
271
+ scanBaseDirectory: '/repos',
272
+ targetHourUtc: 5,
273
+ enableKevNvdReport: true,
274
+ },
275
+ });
276
+
277
+ expect(mockHttpRepository.get.mock.calls).toHaveLength(0);
278
+ });
279
+
280
+ it('creates a KEV report issue for vulnerabilities added since yesterday', async () => {
281
+ const {
282
+ useCase,
283
+ mockLocalCommandRunner,
284
+ mockIssueRepository,
285
+ mockHttpRepository,
286
+ } = buildUseCase();
287
+
288
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
289
+ stdout: '',
290
+ stderr: '',
291
+ exitCode: 0,
292
+ });
293
+ mockHttpRepository.get.mockResolvedValue(
294
+ JSON.stringify({
295
+ vulnerabilities: [
296
+ {
297
+ cveID: 'CVE-2024-0001',
298
+ vulnerabilityName: 'New Vulnerability',
299
+ dateAdded: '2024-01-02',
300
+ },
301
+ {
302
+ cveID: 'CVE-2023-9999',
303
+ vulnerabilityName: 'Old Vulnerability',
304
+ dateAdded: '2023-12-31',
305
+ },
306
+ ],
307
+ }),
308
+ );
309
+
310
+ await useCase.run({
311
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
312
+ org: 'example-org',
313
+ manager: 'manager-name',
314
+ dailySecurityScan: {
315
+ scanBaseDirectory: '/repos',
316
+ targetHourUtc: 5,
317
+ enableKevNvdReport: true,
318
+ kevReportRepo: 'security-reports',
319
+ },
320
+ });
321
+
322
+ expect(mockHttpRepository.get.mock.calls).toHaveLength(1);
323
+ expect(mockHttpRepository.get.mock.calls[0][0]).toBe(KEV_CATALOG_URL);
324
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(1);
325
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][0]).toBe(
326
+ 'example-org',
327
+ );
328
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][1]).toBe(
329
+ 'security-reports',
330
+ );
331
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][2]).toBe(
332
+ 'CISA KEV new additions since 2024-01-01',
333
+ );
334
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).toContain(
335
+ 'CVE-2024-0001',
336
+ );
337
+ expect(mockIssueRepository.createNewIssue.mock.calls[0][3]).not.toContain(
338
+ 'CVE-2023-9999',
339
+ );
340
+ });
341
+
342
+ it('does not create a KEV report issue when there are no new additions', async () => {
343
+ const {
344
+ useCase,
345
+ mockLocalCommandRunner,
346
+ mockIssueRepository,
347
+ mockHttpRepository,
348
+ } = buildUseCase();
349
+
350
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
351
+ stdout: '',
352
+ stderr: '',
353
+ exitCode: 0,
354
+ });
355
+ mockHttpRepository.get.mockResolvedValue(
356
+ JSON.stringify({
357
+ vulnerabilities: [
358
+ {
359
+ cveID: 'CVE-2023-9999',
360
+ vulnerabilityName: 'Old Vulnerability',
361
+ dateAdded: '2023-12-31',
362
+ },
363
+ ],
364
+ }),
365
+ );
366
+
367
+ await useCase.run({
368
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
369
+ org: 'example-org',
370
+ manager: 'manager-name',
371
+ dailySecurityScan: {
372
+ scanBaseDirectory: '/repos',
373
+ targetHourUtc: 5,
374
+ enableKevNvdReport: true,
375
+ kevReportRepo: 'security-reports',
376
+ },
377
+ });
378
+
379
+ expect(mockHttpRepository.get.mock.calls).toHaveLength(1);
380
+ expect(mockHttpRepository.get.mock.calls[0][0]).toBe(KEV_CATALOG_URL);
381
+ expect(mockIssueRepository.createNewIssue.mock.calls).toHaveLength(0);
382
+ });
383
+
384
+ it('throws when the KEV catalog format is unexpected', async () => {
385
+ const { useCase, mockLocalCommandRunner, mockHttpRepository } =
386
+ buildUseCase();
387
+
388
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
389
+ stdout: '',
390
+ stderr: '',
391
+ exitCode: 0,
392
+ });
393
+ mockHttpRepository.get.mockResolvedValue(
394
+ JSON.stringify({ unexpected: 'structure' }),
395
+ );
396
+
397
+ await expect(
398
+ useCase.run({
399
+ targetDates: [new Date('2024-01-02T05:00:00Z')],
400
+ org: 'example-org',
401
+ manager: 'manager-name',
402
+ dailySecurityScan: {
403
+ scanBaseDirectory: '/repos',
404
+ targetHourUtc: 5,
405
+ enableKevNvdReport: true,
406
+ kevReportRepo: 'security-reports',
407
+ },
408
+ }),
409
+ ).rejects.toThrow('Unexpected CISA KEV catalog format');
410
+ });
411
+ });
412
+ });
@@ -0,0 +1,200 @@
1
+ import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
2
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
3
+ import { HttpRepository } from './adapter-interfaces/HttpRepository';
4
+ import { Member } from '../entities/Member';
5
+
6
+ export type DailySecurityScanConfig = {
7
+ scanBaseDirectory: string;
8
+ targetHourUtc: number;
9
+ enableKevNvdReport?: boolean;
10
+ kevReportRepo?: string;
11
+ };
12
+
13
+ type KevVulnerability = {
14
+ cveID: string;
15
+ vulnerabilityName: string;
16
+ dateAdded: string;
17
+ };
18
+
19
+ type KevCatalog = {
20
+ vulnerabilities: KevVulnerability[];
21
+ };
22
+
23
+ const isKevVulnerability = (value: unknown): value is KevVulnerability => {
24
+ if (typeof value !== 'object' || value === null) {
25
+ return false;
26
+ }
27
+ const record: Record<string, unknown> = { ...value };
28
+ return (
29
+ typeof record.cveID === 'string' &&
30
+ typeof record.vulnerabilityName === 'string' &&
31
+ typeof record.dateAdded === 'string'
32
+ );
33
+ };
34
+
35
+ const isKevCatalog = (value: unknown): value is KevCatalog => {
36
+ if (typeof value !== 'object' || value === null) {
37
+ return false;
38
+ }
39
+ const record: Record<string, unknown> = { ...value };
40
+ return (
41
+ Array.isArray(record.vulnerabilities) &&
42
+ record.vulnerabilities.every(isKevVulnerability)
43
+ );
44
+ };
45
+
46
+ const KEV_CATALOG_URL =
47
+ 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json';
48
+
49
+ export class DailySecurityScanUseCase {
50
+ constructor(
51
+ readonly localCommandRunner: LocalCommandRunner,
52
+ readonly issueRepository: Pick<IssueRepository, 'createNewIssue'>,
53
+ readonly httpRepository: HttpRepository,
54
+ ) {}
55
+
56
+ run = async (input: {
57
+ targetDates: Date[];
58
+ org: string;
59
+ manager: Member['name'];
60
+ dailySecurityScan: DailySecurityScanConfig;
61
+ }): Promise<void> => {
62
+ const shouldRun = input.targetDates.some(
63
+ (targetDate) =>
64
+ targetDate.getUTCHours() === input.dailySecurityScan.targetHourUtc &&
65
+ targetDate.getUTCMinutes() === 0,
66
+ );
67
+ if (!shouldRun) {
68
+ return;
69
+ }
70
+
71
+ const lastTargetDate = input.targetDates[input.targetDates.length - 1];
72
+ const today = lastTargetDate.toISOString().slice(0, 10);
73
+
74
+ await this.scanRepositories(
75
+ input.org,
76
+ input.manager,
77
+ today,
78
+ input.dailySecurityScan,
79
+ );
80
+
81
+ await this.reportKevAdditions(
82
+ input.org,
83
+ input.manager,
84
+ lastTargetDate,
85
+ input.dailySecurityScan,
86
+ );
87
+ };
88
+
89
+ private scanRepositories = async (
90
+ org: string,
91
+ manager: Member['name'],
92
+ today: string,
93
+ config: DailySecurityScanConfig,
94
+ ): Promise<void> => {
95
+ const { stdout: findOutput } = await this.localCommandRunner.runCommand(
96
+ 'find',
97
+ [
98
+ config.scanBaseDirectory,
99
+ '-mindepth',
100
+ '4',
101
+ '-maxdepth',
102
+ '4',
103
+ '-name',
104
+ '.git',
105
+ '-type',
106
+ 'd',
107
+ ],
108
+ );
109
+
110
+ const repositoryDirectories = findOutput
111
+ .split('\n')
112
+ .filter((line) => line.length > 0)
113
+ .map((gitDirectory) => gitDirectory.replace(/\/\.git$/, ''));
114
+
115
+ for (const repositoryDirectory of repositoryDirectories) {
116
+ const { stdout: remoteUrl, exitCode: remoteExitCode } =
117
+ await this.localCommandRunner.runCommand('git', [
118
+ '-C',
119
+ repositoryDirectory,
120
+ 'remote',
121
+ 'get-url',
122
+ 'origin',
123
+ ]);
124
+ if (remoteExitCode !== 0) {
125
+ continue;
126
+ }
127
+
128
+ const remoteMatch = remoteUrl
129
+ .trim()
130
+ .match(/github\.com[:/]([^/]+)\/([^/.]+)/);
131
+ if (!remoteMatch || remoteMatch[1] !== org) {
132
+ continue;
133
+ }
134
+ const repositoryOrg = remoteMatch[1];
135
+ const repositoryName = remoteMatch[2];
136
+
137
+ const { stdout: scanOutput, exitCode: scanExitCode } =
138
+ await this.localCommandRunner.runCommand('osv-scanner', [
139
+ 'scan',
140
+ 'source',
141
+ '-r',
142
+ repositoryDirectory,
143
+ ]);
144
+ if (scanExitCode !== 1) {
145
+ continue;
146
+ }
147
+
148
+ await this.issueRepository.createNewIssue(
149
+ repositoryOrg,
150
+ repositoryName,
151
+ `Daily security scan findings: ${today}`,
152
+ `## OSV-Scanner findings\n\n\`\`\`\n${scanOutput}\n\`\`\``,
153
+ [manager],
154
+ [],
155
+ );
156
+ }
157
+ };
158
+
159
+ private reportKevAdditions = async (
160
+ org: string,
161
+ manager: Member['name'],
162
+ lastTargetDate: Date,
163
+ config: DailySecurityScanConfig,
164
+ ): Promise<void> => {
165
+ if (!config.enableKevNvdReport || !config.kevReportRepo) {
166
+ return;
167
+ }
168
+
169
+ const yesterday = new Date(lastTargetDate);
170
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1);
171
+ const yesterdayYmd = yesterday.toISOString().slice(0, 10);
172
+
173
+ const kevJson = await this.httpRepository.get(KEV_CATALOG_URL);
174
+ const parsedKev: unknown = JSON.parse(kevJson);
175
+ if (!isKevCatalog(parsedKev)) {
176
+ throw new Error(
177
+ `Unexpected CISA KEV catalog format from ${KEV_CATALOG_URL}`,
178
+ );
179
+ }
180
+
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) {
188
+ return;
189
+ }
190
+
191
+ await this.issueRepository.createNewIssue(
192
+ org,
193
+ config.kevReportRepo,
194
+ `CISA KEV new additions since ${yesterdayYmd}`,
195
+ newKevEntries.join('\n'),
196
+ [manager],
197
+ [],
198
+ );
199
+ };
200
+ }
@@ -25,6 +25,7 @@ import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUse
25
25
  import { RevertNotReadyAwaitingQualityCheckUseCase } from './RevertNotReadyAwaitingQualityCheckUseCase';
26
26
  import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
27
27
  import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
28
+ import { DailySecurityScanUseCase } from './DailySecurityScanUseCase';
28
29
 
29
30
  describe('HandleScheduledEventUseCase', () => {
30
31
  describe('createTargetDateTimes', () => {
@@ -117,6 +118,7 @@ describe('HandleScheduledEventUseCase', () => {
117
118
  const mockRevertNotReadyAwaitingQualityCheckUseCase =
118
119
  mock<RevertNotReadyAwaitingQualityCheckUseCase>();
119
120
  const mockUpdateRateLimitCacheUseCase = mock<UpdateRateLimitCacheUseCase>();
121
+ const mockDailySecurityScanUseCase = mock<DailySecurityScanUseCase>();
120
122
  const mockDateRepository = mock<DateRepository>();
121
123
  const mockSpreadsheetRepository = mock<SpreadsheetRepository>();
122
124
  const mockProjectRepository = mock<ProjectRepository>();
@@ -142,6 +144,7 @@ describe('HandleScheduledEventUseCase', () => {
142
144
  mockRevertOrphanedPreparationUseCase,
143
145
  mockRevertNotReadyAwaitingQualityCheckUseCase,
144
146
  mockUpdateRateLimitCacheUseCase,
147
+ mockDailySecurityScanUseCase,
145
148
  mockDateRepository,
146
149
  mockSpreadsheetRepository,
147
150
  mockProjectRepository,
@@ -29,6 +29,10 @@ import { RevertNotReadyAwaitingQualityCheckUseCase } from './RevertNotReadyAwait
29
29
  import { resolveLabelsAsLlmAgentName } from './resolveLabelsAsLlmAgentName';
30
30
  import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
31
31
  import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
32
+ import {
33
+ DailySecurityScanConfig,
34
+ DailySecurityScanUseCase,
35
+ } from './DailySecurityScanUseCase';
32
36
 
33
37
  export class ProjectNotFoundError extends Error {
34
38
  constructor(message: string) {
@@ -60,6 +64,7 @@ export class HandleScheduledEventUseCase {
60
64
  readonly revertOrphanedPreparationUseCase: RevertOrphanedPreparationUseCase,
61
65
  readonly revertNotReadyAwaitingQualityCheckUseCase: RevertNotReadyAwaitingQualityCheckUseCase,
62
66
  readonly updateRateLimitCacheUseCase: UpdateRateLimitCacheUseCase | null,
67
+ readonly dailySecurityScanUseCase: DailySecurityScanUseCase | null,
63
68
  readonly dateRepository: DateRepository,
64
69
  readonly spreadsheetRepository: SpreadsheetRepository,
65
70
  readonly projectRepository: ProjectRepository,
@@ -97,6 +102,7 @@ export class HandleScheduledEventUseCase {
97
102
  labelsAsLlmAgentName?: string[] | null;
98
103
  } | null;
99
104
  thresholdForAutoReject?: number;
105
+ dailySecurityScan?: DailySecurityScanConfig | null;
100
106
  }): Promise<{
101
107
  project: Project;
102
108
  issues: Issue[];
@@ -291,6 +297,14 @@ ${JSON.stringify(e)}
291
297
  allowIssueCacheMinutes: input.allowIssueCacheMinutes,
292
298
  labelsAsLlmAgentName,
293
299
  });
300
+ if (this.dailySecurityScanUseCase !== null && input.dailySecurityScan) {
301
+ await this.dailySecurityScanUseCase.run({
302
+ targetDates: targetDateTimes,
303
+ org: input.org,
304
+ manager: input.manager,
305
+ dailySecurityScan: input.dailySecurityScan,
306
+ });
307
+ }
294
308
  if (input.startPreparation) {
295
309
  if (this.updateRateLimitCacheUseCase !== null) {
296
310
  await this.updateRateLimitCacheUseCase.run({
@@ -0,0 +1,3 @@
1
+ export interface HttpRepository {
2
+ get: (url: string) => Promise<string>;
3
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AA4B3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,KAChB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CAySP;CACH"}
1
+ {"version":3,"file":"HandleScheduledEventUseCaseHandler.d.ts","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AA8B3D,qBAAa,kCAAkC;IAC7C,MAAM,GACJ,gBAAgB,MAAM,EACtB,UAAU,OAAO,KAChB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,KAAK,EAAE,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,eAAe,EAAE,IAAI,EAAE,CAAC;KACzB,GAAG,IAAI,CAAC,CAkTP;CACH"}
@@ -0,0 +1,5 @@
1
+ import { HttpRepository } from '../../domain/usecases/adapter-interfaces/HttpRepository';
2
+ export declare class KyHttpRepository implements HttpRepository {
3
+ get: (url: string) => Promise<string>;
4
+ }
5
+ //# sourceMappingURL=KyHttpRepository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KyHttpRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/KyHttpRepository.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,yDAAyD,CAAC;AAEzF,qBAAa,gBAAiB,YAAW,cAAc;IACrD,GAAG,GAAU,KAAK,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC,CAExC;CACH"}