github-issue-tower-defence-management 1.148.5 → 1.148.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,13 +47,88 @@ const isKevCatalog = (value: unknown): value is KevCatalog => {
47
47
  );
48
48
  };
49
49
 
50
+ type ScannedVulnerablePackage = {
51
+ repositoryName: string;
52
+ ecosystem: string;
53
+ packageName: string;
54
+ packageVersion: string;
55
+ vulnerabilityId: string;
56
+ vulnerabilityIdentifiers: string[];
57
+ summary: string;
58
+ };
59
+
60
+ const parseScannerVulnerabilities = (
61
+ repositoryName: string,
62
+ scannerOutput: string,
63
+ ): ScannedVulnerablePackage[] => {
64
+ let parsed: unknown;
65
+ try {
66
+ parsed = JSON.parse(scannerOutput);
67
+ } catch (error) {
68
+ console.error(
69
+ `Unparsable osv-scanner output for ${repositoryName}: ${String(error)}`,
70
+ );
71
+ return [];
72
+ }
73
+ const toRecord = (value: unknown): Record<string, unknown> =>
74
+ typeof value === 'object' && value !== null ? { ...value } : {};
75
+ const readArray = (value: unknown, key: string): unknown[] => {
76
+ const entry = toRecord(value)[key];
77
+ return Array.isArray(entry) ? entry : [];
78
+ };
79
+ const readString = (value: unknown, key: string): string => {
80
+ const entry = toRecord(value)[key];
81
+ return typeof entry === 'string' ? entry : '';
82
+ };
83
+ return readArray(parsed, 'results').flatMap((result) =>
84
+ readArray(result, 'packages').flatMap((scannedPackage) => {
85
+ const packageDetail = toRecord(scannedPackage).package;
86
+ return readArray(scannedPackage, 'vulnerabilities').map(
87
+ (vulnerability) => {
88
+ const vulnerabilityId = readString(vulnerability, 'id');
89
+ const aliases = readArray(vulnerability, 'aliases').filter(
90
+ (alias): alias is string => typeof alias === 'string',
91
+ );
92
+ return {
93
+ repositoryName,
94
+ ecosystem: readString(packageDetail, 'ecosystem'),
95
+ packageName: readString(packageDetail, 'name'),
96
+ packageVersion: readString(packageDetail, 'version'),
97
+ vulnerabilityId,
98
+ vulnerabilityIdentifiers: [vulnerabilityId, ...aliases],
99
+ summary: readString(vulnerability, 'summary'),
100
+ };
101
+ },
102
+ );
103
+ }),
104
+ );
105
+ };
106
+
107
+ const renderScannerFindings = (
108
+ today: string,
109
+ vulnerablePackages: ScannedVulnerablePackage[],
110
+ ): string =>
111
+ [
112
+ '## OSV-Scanner findings',
113
+ '',
114
+ `### ${today}`,
115
+ '',
116
+ ...vulnerablePackages.map(
117
+ (vulnerablePackage) =>
118
+ `- ${vulnerablePackage.ecosystem} ${vulnerablePackage.packageName} ${vulnerablePackage.packageVersion} ${vulnerablePackage.vulnerabilityIdentifiers.join(' ')} ${vulnerablePackage.summary}`,
119
+ ),
120
+ ].join('\n');
121
+
50
122
  const KEV_CATALOG_URL =
51
123
  'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json';
52
124
 
53
125
  export class DailySecurityScanUseCase {
54
126
  constructor(
55
127
  readonly localCommandRunner: LocalCommandRunner,
56
- readonly issueRepository: Pick<IssueRepository, 'createNewIssue'>,
128
+ readonly issueRepository: Pick<
129
+ IssueRepository,
130
+ 'createNewIssue' | 'searchIssue' | 'createCommentByUrl'
131
+ >,
57
132
  readonly httpRepository: HttpRepository,
58
133
  ) {}
59
134
 
@@ -75,7 +150,7 @@ export class DailySecurityScanUseCase {
75
150
  const lastTargetDate = input.targetDates[input.targetDates.length - 1];
76
151
  const today = lastTargetDate.toISOString().slice(0, 10);
77
152
 
78
- await this.scanRepositories(
153
+ const scannedVulnerablePackages = await this.scanRepositories(
79
154
  input.org,
80
155
  input.manager,
81
156
  today,
@@ -87,6 +162,7 @@ export class DailySecurityScanUseCase {
87
162
  input.manager,
88
163
  lastTargetDate,
89
164
  input.dailySecurityScan,
165
+ scannedVulnerablePackages,
90
166
  );
91
167
  };
92
168
 
@@ -95,15 +171,13 @@ export class DailySecurityScanUseCase {
95
171
  manager: Member['name'],
96
172
  today: string,
97
173
  config: DailySecurityScanConfig,
98
- ): Promise<void> => {
174
+ ): Promise<ScannedVulnerablePackage[]> => {
99
175
  const { stdout: findOutput } = await this.localCommandRunner.runCommand(
100
176
  'find',
101
177
  [
102
178
  config.scanBaseDirectory,
103
- '-mindepth',
104
- '4',
105
179
  '-maxdepth',
106
- '4',
180
+ '5',
107
181
  '-name',
108
182
  '.git',
109
183
  '-type',
@@ -116,6 +190,14 @@ export class DailySecurityScanUseCase {
116
190
  .filter((line) => line.length > 0)
117
191
  .map((gitDirectory) => gitDirectory.replace(/\/\.git$/, ''));
118
192
 
193
+ if (repositoryDirectories.length === 0) {
194
+ console.error(
195
+ `No repositories found in scan base directory: ${config.scanBaseDirectory}`,
196
+ );
197
+ return [];
198
+ }
199
+
200
+ const scannedVulnerablePackages: ScannedVulnerablePackage[] = [];
119
201
  for (const repositoryDirectory of repositoryDirectories) {
120
202
  const { stdout: remoteUrl, exitCode: remoteExitCode } =
121
203
  await this.localCommandRunner.runCommand('git', [
@@ -138,26 +220,62 @@ export class DailySecurityScanUseCase {
138
220
  const repositoryOrg = remoteMatch[1];
139
221
  const repositoryName = remoteMatch[2];
140
222
 
141
- const { stdout: scanOutput, exitCode: scanExitCode } =
142
- await this.localCommandRunner.runCommand('osv-scanner', [
143
- 'scan',
144
- 'source',
145
- '-r',
146
- repositoryDirectory,
147
- ]);
223
+ const {
224
+ stdout: scanOutput,
225
+ stderr: scanStderr,
226
+ exitCode: scanExitCode,
227
+ } = await this.localCommandRunner.runCommand('osv-scanner', [
228
+ 'scan',
229
+ 'source',
230
+ '-r',
231
+ repositoryDirectory,
232
+ '--format',
233
+ 'json',
234
+ ]);
235
+ if (scanExitCode === 0) {
236
+ continue;
237
+ }
148
238
  if (scanExitCode !== 1) {
239
+ console.error(
240
+ `osv-scanner failed with exit code ${scanExitCode} for ${repositoryDirectory}: ${scanStderr}`,
241
+ );
149
242
  continue;
150
243
  }
151
244
 
152
- await this.issueRepository.createNewIssue(
153
- repositoryOrg,
245
+ const vulnerablePackages = parseScannerVulnerabilities(
246
+ repositoryName,
247
+ scanOutput,
248
+ );
249
+ scannedVulnerablePackages.push(...vulnerablePackages);
250
+
251
+ const findingsBody = renderScannerFindings(today, vulnerablePackages);
252
+ const existingIssues = await this.issueRepository.searchIssue({
253
+ owner: repositoryOrg,
154
254
  repositoryName,
155
- `Daily security scan findings: ${today}`,
156
- `## OSV-Scanner findings\n\n\`\`\`\n${scanOutput}\n\`\`\``,
157
- [manager],
158
- [],
255
+ type: 'issue',
256
+ state: 'open',
257
+ title: 'Daily security scan findings',
258
+ });
259
+ const existingIssue = existingIssues.find(
260
+ (issue) => issue.title === 'Daily security scan findings',
159
261
  );
262
+ if (existingIssue) {
263
+ await this.issueRepository.createCommentByUrl(
264
+ existingIssue.url,
265
+ findingsBody,
266
+ );
267
+ } else {
268
+ await this.issueRepository.createNewIssue(
269
+ repositoryOrg,
270
+ repositoryName,
271
+ 'Daily security scan findings',
272
+ findingsBody,
273
+ [manager],
274
+ [],
275
+ );
276
+ }
160
277
  }
278
+ return scannedVulnerablePackages;
161
279
  };
162
280
 
163
281
  private reportKevAdditions = async (
@@ -165,6 +283,7 @@ export class DailySecurityScanUseCase {
165
283
  manager: Member['name'],
166
284
  lastTargetDate: Date,
167
285
  config: DailySecurityScanConfig,
286
+ scannedVulnerablePackages: ScannedVulnerablePackage[],
168
287
  ): Promise<void> => {
169
288
  if (!config.enableKevNvdReport || !config.kevReportRepo) {
170
289
  return;
@@ -185,18 +304,15 @@ export class DailySecurityScanUseCase {
185
304
  const newKevEntries = parsedKev.vulnerabilities.filter(
186
305
  (vulnerability) => vulnerability.dateAdded >= yesterdayYmd,
187
306
  );
188
- const usedKevEntries: KevVulnerability[] = [];
189
- for (const vulnerability of newKevEntries) {
190
- if (
191
- await this.isProductPresentInScannedWorkspace(
192
- config.scanBaseDirectory,
193
- vulnerability.product,
194
- )
195
- ) {
196
- usedKevEntries.push(vulnerability);
197
- }
198
- }
199
- if (usedKevEntries.length === 0) {
307
+ const affectingKevEntries = newKevEntries
308
+ .map((vulnerability) => ({
309
+ vulnerability,
310
+ affectedPackages: scannedVulnerablePackages.filter((scannedPackage) =>
311
+ scannedPackage.vulnerabilityIdentifiers.includes(vulnerability.cveID),
312
+ ),
313
+ }))
314
+ .filter((entry) => entry.affectedPackages.length > 0);
315
+ if (affectingKevEntries.length === 0) {
200
316
  return;
201
317
  }
202
318
 
@@ -204,54 +320,19 @@ export class DailySecurityScanUseCase {
204
320
  org,
205
321
  config.kevReportRepo,
206
322
  `CISA KEV new additions since ${yesterdayYmd}`,
207
- usedKevEntries
208
- .map(
209
- (vulnerability) =>
210
- `- ${vulnerability.dateAdded} ${vulnerability.cveID} ${vulnerability.vulnerabilityName}`,
323
+ affectingKevEntries
324
+ .map((entry) =>
325
+ [
326
+ `- ${entry.vulnerability.dateAdded} ${entry.vulnerability.cveID} ${entry.vulnerability.vulnerabilityName}`,
327
+ ...entry.affectedPackages.map(
328
+ (affectedPackage) =>
329
+ ` - ${affectedPackage.repositoryName} ${affectedPackage.ecosystem} ${affectedPackage.packageName} ${affectedPackage.packageVersion}`,
330
+ ),
331
+ ].join('\n'),
211
332
  )
212
333
  .join('\n'),
213
334
  [manager],
214
335
  [],
215
336
  );
216
337
  };
217
-
218
- private isProductPresentInScannedWorkspace = async (
219
- scanBaseDirectory: string,
220
- product: string,
221
- ): Promise<boolean> => {
222
- const { stdout: findOutput } = await this.localCommandRunner.runCommand(
223
- 'find',
224
- [scanBaseDirectory, '-maxdepth', '3', '-name', '.git', '-type', 'd'],
225
- );
226
- const repositoryDirectories = findOutput
227
- .split('\n')
228
- .filter((line) => line.length > 0)
229
- .map((gitDirectory) => gitDirectory.replace(/\/\.git$/, ''));
230
-
231
- for (const repositoryDirectory of repositoryDirectories) {
232
- const { stderr, exitCode } = await this.localCommandRunner.runCommand(
233
- 'git',
234
- [
235
- '-C',
236
- repositoryDirectory,
237
- 'grep',
238
- '-I',
239
- '-i',
240
- '-q',
241
- '-F',
242
- '-e',
243
- product,
244
- ],
245
- );
246
- if (exitCode === 0) {
247
- return true;
248
- }
249
- if (exitCode !== 1) {
250
- console.error(
251
- `Failed to search ${repositoryDirectory} for ${product}: ${stderr}`,
252
- );
253
- }
254
- }
255
- return false;
256
- };
257
338
  }
@@ -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,6 +21,5 @@ export declare class DailySecurityScanUseCase {
21
21
  }) => Promise<void>;
22
22
  private scanRepositories;
23
23
  private reportKevAdditions;
24
- private isProductPresentInScannedWorkspace;
25
24
  }
26
25
  //# 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;AA0CF,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,CAqDxB;IAEF,OAAO,CAAC,kCAAkC,CAsCxC;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;AAkHF,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,CA2Bf;IAEF,OAAO,CAAC,gBAAgB,CA8GtB;IAEF,OAAO,CAAC,kBAAkB,CAwDxB;CACH"}