coverme-scanner 1.0.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 (46) hide show
  1. package/README.md +227 -0
  2. package/commands/scan.md +317 -0
  3. package/dist/cli/index.d.ts +3 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +39 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/init.d.ts +6 -0
  8. package/dist/cli/init.d.ts.map +1 -0
  9. package/dist/cli/init.js +636 -0
  10. package/dist/cli/init.js.map +1 -0
  11. package/dist/cli/scan.d.ts +11 -0
  12. package/dist/cli/scan.d.ts.map +1 -0
  13. package/dist/cli/scan.js +498 -0
  14. package/dist/cli/scan.js.map +1 -0
  15. package/dist/report/generator.d.ts +48 -0
  16. package/dist/report/generator.d.ts.map +1 -0
  17. package/dist/report/generator.js +368 -0
  18. package/dist/report/generator.js.map +1 -0
  19. package/dist/report/index.d.ts +35 -0
  20. package/dist/report/index.d.ts.map +1 -0
  21. package/dist/report/index.js +463 -0
  22. package/dist/report/index.js.map +1 -0
  23. package/dist/templates/report.html +796 -0
  24. package/dist/types.d.ts +94 -0
  25. package/dist/types.d.ts.map +1 -0
  26. package/dist/types.js +3 -0
  27. package/dist/types.js.map +1 -0
  28. package/package.json +48 -0
  29. package/src/cli/index.ts +43 -0
  30. package/src/cli/init.ts +611 -0
  31. package/src/cli/scan.ts +483 -0
  32. package/src/prompts/architecture-reviewer.md +171 -0
  33. package/src/prompts/consensus-builder.md +247 -0
  34. package/src/prompts/context-discovery.md +174 -0
  35. package/src/prompts/cross-validator.md +224 -0
  36. package/src/prompts/deep-dive-expert.md +224 -0
  37. package/src/prompts/dependency-auditor.md +190 -0
  38. package/src/prompts/performance-hunter.md +200 -0
  39. package/src/prompts/quality-analyzer.md +150 -0
  40. package/src/prompts/report-generator.md +285 -0
  41. package/src/prompts/security-scanner.md +180 -0
  42. package/src/report/generator.ts +382 -0
  43. package/src/report/index.ts +483 -0
  44. package/src/templates/report.html +796 -0
  45. package/src/types.ts +107 -0
  46. package/tsconfig.json +20 -0
@@ -0,0 +1,382 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import puppeteer from 'puppeteer';
4
+ import type { ScanResult, ConsensusFinding, Severity } from '../types.js';
5
+
6
+ interface ReportData {
7
+ projectName: string;
8
+ scanDate: string;
9
+ scoreGrade: string;
10
+ scoreValue: number;
11
+ criticalCount: number;
12
+ highCount: number;
13
+ mediumCount: number;
14
+ lowCount: number;
15
+ infoCount: number;
16
+ executiveSummary: string;
17
+ criticalFindings: ConsensusFinding[];
18
+ highFindings: ConsensusFinding[];
19
+ mediumFindings: ConsensusFinding[];
20
+ lowFindings: ConsensusFinding[];
21
+ falsePositives: Array<{ id: string; title: string; file?: string; rejectionReason: string }>;
22
+ falsePositiveCount: number;
23
+ lowInfoCount: number;
24
+ positiveObservations: string[];
25
+ scanDuration: string;
26
+ agentCount: number;
27
+ }
28
+
29
+ export function calculateScore(result: ScanResult): { grade: string; value: number } {
30
+ // Scoring: Start at 100, subtract based on findings
31
+ // Critical: -15 each
32
+ // High: -8 each
33
+ // Medium: -3 each
34
+ // Low: -1 each
35
+ // Info: 0
36
+
37
+ let score = 100;
38
+ score -= result.summary.critical * 15;
39
+ score -= result.summary.high * 8;
40
+ score -= result.summary.medium * 3;
41
+ score -= result.summary.low * 1;
42
+
43
+ score = Math.max(0, Math.min(100, score));
44
+
45
+ let grade: string;
46
+ if (score >= 90) grade = 'a';
47
+ else if (score >= 80) grade = 'b';
48
+ else if (score >= 70) grade = 'c';
49
+ else if (score >= 60) grade = 'd';
50
+ else grade = 'f';
51
+
52
+ return { grade, value: score };
53
+ }
54
+
55
+ export function generateExecutiveSummary(result: ScanResult): string {
56
+ const total = result.summary.total;
57
+ const critical = result.summary.critical;
58
+ const high = result.summary.high;
59
+
60
+ if (critical > 0) {
61
+ return `This scan identified ${total} security and quality findings, including ${critical} critical issue${critical > 1 ? 's' : ''} that require immediate attention. ${high > 0 ? `Additionally, ${high} high-priority issues were found that should be addressed soon.` : ''} The codebase requires immediate remediation before production deployment.`;
62
+ }
63
+
64
+ if (high > 0) {
65
+ return `This scan identified ${total} findings, with ${high} high-priority issue${high > 1 ? 's' : ''} that should be addressed in the near term. No critical vulnerabilities were detected, but the high-priority findings could pose security risks if left unaddressed.`;
66
+ }
67
+
68
+ if (total > 0) {
69
+ return `This scan identified ${total} findings, primarily medium and low priority items. No critical or high-severity issues were detected. The codebase demonstrates reasonable security practices with room for improvement in the areas noted below.`;
70
+ }
71
+
72
+ return `Excellent! This scan found no significant security or quality issues. The codebase demonstrates strong security practices and code quality. Continue maintaining these standards.`;
73
+ }
74
+
75
+ function escapeHtml(text: string): string {
76
+ return text
77
+ .replace(/&/g, '&amp;')
78
+ .replace(/</g, '&lt;')
79
+ .replace(/>/g, '&gt;')
80
+ .replace(/"/g, '&quot;')
81
+ .replace(/'/g, '&#039;');
82
+ }
83
+
84
+ function renderFindings(findings: ConsensusFinding[], template: string): string {
85
+ if (!findings || findings.length === 0) return '';
86
+
87
+ return findings.map(f => {
88
+ let html = template;
89
+ html = html.replace(/\{\{id\}\}/g, escapeHtml(f.id));
90
+ html = html.replace(/\{\{title\}\}/g, escapeHtml(f.title));
91
+ html = html.replace(/\{\{file\}\}/g, escapeHtml(f.file || 'N/A'));
92
+ html = html.replace(/\{\{line\}\}/g, String(f.line || ''));
93
+ html = html.replace(/\{\{confidence\}\}/g, String(f.confidence || 0));
94
+ html = html.replace(/\{\{description\}\}/g, escapeHtml(f.description));
95
+ html = html.replace(/\{\{category\}\}/g, escapeHtml(f.category));
96
+ html = html.replace(/\{\{cwe\}\}/g, escapeHtml(f.cwe || 'N/A'));
97
+ html = html.replace(/\{\{code\}\}/g, escapeHtml(f.code || ''));
98
+ html = html.replace(/\{\{recommendation\}\}/g, escapeHtml(f.recommendation));
99
+ html = html.replace(/\{\{severity\}\}/g, f.severity);
100
+ // New fields: why, context, checkBefore
101
+ html = html.replace(/\{\{why\}\}/g, escapeHtml((f as any).why || ''));
102
+ html = html.replace(/\{\{context\}\}/g, escapeHtml((f as any).context || ''));
103
+ html = html.replace(/\{\{checkBefore\}\}/g, escapeHtml((f as any).checkBefore || ''));
104
+
105
+ // Handle {{#if why}} blocks
106
+ if ((f as any).why) {
107
+ html = html.replace(/\{\{#if why\}\}/g, '').replace(/\{\{\/if\}\}/g, '');
108
+ } else {
109
+ html = html.replace(/\{\{#if why\}\}[\s\S]*?\{\{\/if\}\}/g, '');
110
+ }
111
+ if ((f as any).context) {
112
+ html = html.replace(/\{\{#if context\}\}/g, '').replace(/\{\{\/if\}\}/g, '');
113
+ } else {
114
+ html = html.replace(/\{\{#if context\}\}[\s\S]*?\{\{\/if\}\}/g, '');
115
+ }
116
+ if ((f as any).checkBefore) {
117
+ html = html.replace(/\{\{#if checkBefore\}\}/g, '').replace(/\{\{\/if\}\}/g, '');
118
+ } else {
119
+ html = html.replace(/\{\{#if checkBefore\}\}[\s\S]*?\{\{\/if\}\}/g, '');
120
+ }
121
+
122
+ return html;
123
+ }).join('\n');
124
+ }
125
+
126
+ export function renderTemplate(templateHtml: string, data: ReportData): string {
127
+ let html = templateHtml;
128
+
129
+ // Simple replacements
130
+ html = html.replace(/\{\{projectName\}\}/g, escapeHtml(data.projectName));
131
+ html = html.replace(/\{\{scanDate\}\}/g, escapeHtml(data.scanDate));
132
+ html = html.replace(/\{\{scoreGrade\}\}/g, data.scoreGrade);
133
+ html = html.replace(/\{\{scoreValue\}\}/g, String(data.scoreValue));
134
+ html = html.replace(/\{\{criticalCount\}\}/g, String(data.criticalCount));
135
+ html = html.replace(/\{\{highCount\}\}/g, String(data.highCount));
136
+ html = html.replace(/\{\{mediumCount\}\}/g, String(data.mediumCount));
137
+ html = html.replace(/\{\{lowCount\}\}/g, String(data.lowCount));
138
+ html = html.replace(/\{\{infoCount\}\}/g, String(data.infoCount));
139
+ html = html.replace(/\{\{lowInfoCount\}\}/g, String(data.lowInfoCount));
140
+ html = html.replace(/\{\{falsePositiveCount\}\}/g, String(data.falsePositiveCount));
141
+ html = html.replace(/\{\{executiveSummary\}\}/g, escapeHtml(data.executiveSummary));
142
+ html = html.replace(/\{\{scanDuration\}\}/g, escapeHtml(data.scanDuration));
143
+ html = html.replace(/\{\{agentCount\}\}/g, String(data.agentCount));
144
+
145
+ // Extract finding template from HTML
146
+ const findingTemplateMatch = html.match(/<div class="finding">[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/);
147
+ const findingTemplate = findingTemplateMatch ? findingTemplateMatch[0] : '';
148
+
149
+ // Render each section with {{#each}} blocks
150
+ // Critical findings
151
+ const criticalSection = html.match(/\{\{#if criticalFindings\}\}[\s\S]*?\{\{\/if\}\}/);
152
+ if (criticalSection) {
153
+ if (data.criticalFindings.length > 0) {
154
+ let section = criticalSection[0]
155
+ .replace('{{#if criticalFindings}}', '')
156
+ .replace('{{/if}}', '');
157
+ const eachMatch = section.match(/\{\{#each criticalFindings\}\}([\s\S]*?)\{\{\/each\}\}/);
158
+ if (eachMatch) {
159
+ const rendered = renderFindings(data.criticalFindings, eachMatch[1]);
160
+ section = section.replace(eachMatch[0], rendered);
161
+ }
162
+ html = html.replace(criticalSection[0], section);
163
+ } else {
164
+ html = html.replace(criticalSection[0], '');
165
+ }
166
+ }
167
+
168
+ // High findings
169
+ const highSection = html.match(/\{\{#if highFindings\}\}[\s\S]*?\{\{\/if\}\}/);
170
+ if (highSection) {
171
+ if (data.highFindings.length > 0) {
172
+ let section = highSection[0]
173
+ .replace('{{#if highFindings}}', '')
174
+ .replace('{{/if}}', '');
175
+ const eachMatch = section.match(/\{\{#each highFindings\}\}([\s\S]*?)\{\{\/each\}\}/);
176
+ if (eachMatch) {
177
+ const rendered = renderFindings(data.highFindings, eachMatch[1]);
178
+ section = section.replace(eachMatch[0], rendered);
179
+ }
180
+ html = html.replace(highSection[0], section);
181
+ } else {
182
+ html = html.replace(highSection[0], '');
183
+ }
184
+ }
185
+
186
+ // Medium findings
187
+ const mediumSection = html.match(/\{\{#if mediumFindings\}\}[\s\S]*?\{\{\/if\}\}/);
188
+ if (mediumSection) {
189
+ if (data.mediumFindings.length > 0) {
190
+ let section = mediumSection[0]
191
+ .replace('{{#if mediumFindings}}', '')
192
+ .replace('{{/if}}', '');
193
+ const eachMatch = section.match(/\{\{#each mediumFindings\}\}([\s\S]*?)\{\{\/each\}\}/);
194
+ if (eachMatch) {
195
+ const rendered = renderFindings(data.mediumFindings, eachMatch[1]);
196
+ section = section.replace(eachMatch[0], rendered);
197
+ }
198
+ html = html.replace(mediumSection[0], section);
199
+ } else {
200
+ html = html.replace(mediumSection[0], '');
201
+ }
202
+ }
203
+
204
+ // Low findings
205
+ const lowSection = html.match(/\{\{#if lowFindings\}\}[\s\S]*?\{\{\/if\}\}/);
206
+ if (lowSection) {
207
+ if (data.lowFindings.length > 0) {
208
+ let section = lowSection[0]
209
+ .replace('{{#if lowFindings}}', '')
210
+ .replace('{{/if}}', '');
211
+ const eachMatch = section.match(/\{\{#each lowFindings\}\}([\s\S]*?)\{\{\/each\}\}/);
212
+ if (eachMatch) {
213
+ const rendered = renderFindings(data.lowFindings, eachMatch[1]);
214
+ section = section.replace(eachMatch[0], rendered);
215
+ }
216
+ html = html.replace(lowSection[0], section);
217
+ } else {
218
+ html = html.replace(lowSection[0], '');
219
+ }
220
+ }
221
+
222
+ // False positives
223
+ const fpSection = html.match(/\{\{#if falsePositives\}\}[\s\S]*?\{\{\/if\}\}/);
224
+ if (fpSection) {
225
+ if (data.falsePositives.length > 0) {
226
+ let section = fpSection[0]
227
+ .replace('{{#if falsePositives}}', '')
228
+ .replace('{{/if}}', '');
229
+ const eachMatch = section.match(/\{\{#each falsePositives\}\}([\s\S]*?)\{\{\/each\}\}/);
230
+ if (eachMatch) {
231
+ const rendered = data.falsePositives.map(fp => {
232
+ let itemHtml = eachMatch[1];
233
+ itemHtml = itemHtml.replace(/\{\{id\}\}/g, escapeHtml(fp.id));
234
+ itemHtml = itemHtml.replace(/\{\{title\}\}/g, escapeHtml(fp.title));
235
+ itemHtml = itemHtml.replace(/\{\{file\}\}/g, escapeHtml(fp.file || ''));
236
+ itemHtml = itemHtml.replace(/\{\{rejectionReason\}\}/g, escapeHtml(fp.rejectionReason));
237
+ return itemHtml;
238
+ }).join('\n');
239
+ section = section.replace(eachMatch[0], rendered);
240
+ }
241
+ html = html.replace(fpSection[0], section);
242
+ } else {
243
+ html = html.replace(fpSection[0], '');
244
+ }
245
+ }
246
+
247
+ // Positive observations
248
+ const posSection = html.match(/\{\{#if positiveObservations\}\}[\s\S]*?\{\{\/if\}\}/);
249
+ if (posSection) {
250
+ if (data.positiveObservations.length > 0) {
251
+ let section = posSection[0]
252
+ .replace('{{#if positiveObservations}}', '')
253
+ .replace('{{/if}}', '');
254
+ const eachMatch = section.match(/\{\{#each positiveObservations\}\}([\s\S]*?)\{\{\/each\}\}/);
255
+ if (eachMatch) {
256
+ const rendered = data.positiveObservations.map(obs => {
257
+ return eachMatch[1].replace(/\{\{this\}\}/g, escapeHtml(obs));
258
+ }).join('\n');
259
+ section = section.replace(eachMatch[0], rendered);
260
+ }
261
+ html = html.replace(posSection[0], section);
262
+ } else {
263
+ html = html.replace(posSection[0], '');
264
+ }
265
+ }
266
+
267
+ return html;
268
+ }
269
+
270
+ export async function generatePdfReport(
271
+ result: ScanResult,
272
+ outputPath: string,
273
+ falsePositives: Array<{ id: string; title: string; file?: string; rejectionReason: string }> = []
274
+ ): Promise<void> {
275
+ const templatePath = path.join(__dirname, '..', 'templates', 'report.html');
276
+ const templateHtml = fs.readFileSync(templatePath, 'utf-8');
277
+
278
+ const { grade, value } = calculateScore(result);
279
+
280
+ const criticalFindings = result.findings.filter(f => f.severity === 'critical');
281
+ const highFindings = result.findings.filter(f => f.severity === 'high');
282
+ const mediumFindings = result.findings.filter(f => f.severity === 'medium');
283
+ const lowFindings = result.findings.filter(f => f.severity === 'low' || f.severity === 'info');
284
+
285
+ const data: ReportData = {
286
+ projectName: result.projectName,
287
+ scanDate: new Date(result.scanDate).toLocaleDateString('en-US', {
288
+ year: 'numeric',
289
+ month: 'long',
290
+ day: 'numeric'
291
+ }),
292
+ scoreGrade: grade,
293
+ scoreValue: value,
294
+ criticalCount: result.summary.critical,
295
+ highCount: result.summary.high,
296
+ mediumCount: result.summary.medium,
297
+ lowCount: result.summary.low,
298
+ infoCount: result.summary.info,
299
+ executiveSummary: generateExecutiveSummary(result),
300
+ criticalFindings,
301
+ highFindings,
302
+ mediumFindings,
303
+ lowFindings,
304
+ falsePositives,
305
+ falsePositiveCount: falsePositives.length,
306
+ lowInfoCount: result.summary.low + result.summary.info,
307
+ positiveObservations: result.positiveObservations,
308
+ scanDuration: `${Math.round(result.scanDuration / 1000)}s`,
309
+ agentCount: result.agentsUsed.length
310
+ };
311
+
312
+ const renderedHtml = renderTemplate(templateHtml, data);
313
+
314
+ // Generate PDF using Puppeteer
315
+ const browser = await puppeteer.launch({
316
+ headless: true,
317
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
318
+ });
319
+
320
+ const page = await browser.newPage();
321
+ await page.setContent(renderedHtml, { waitUntil: 'networkidle0' });
322
+
323
+ await page.pdf({
324
+ path: outputPath,
325
+ format: 'A4',
326
+ margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' },
327
+ printBackground: true,
328
+ preferCSSPageSize: true
329
+ });
330
+
331
+ await browser.close();
332
+
333
+ console.log(`PDF report generated: ${outputPath}`);
334
+ }
335
+
336
+ export async function generateHtmlReport(
337
+ result: ScanResult,
338
+ outputPath: string,
339
+ falsePositives: Array<{ id: string; title: string; file?: string; rejectionReason: string }> = []
340
+ ): Promise<void> {
341
+ const templatePath = path.join(__dirname, '..', 'templates', 'report.html');
342
+ const templateHtml = fs.readFileSync(templatePath, 'utf-8');
343
+
344
+ const { grade, value } = calculateScore(result);
345
+
346
+ const criticalFindings = result.findings.filter(f => f.severity === 'critical');
347
+ const highFindings = result.findings.filter(f => f.severity === 'high');
348
+ const mediumFindings = result.findings.filter(f => f.severity === 'medium');
349
+ const lowFindings = result.findings.filter(f => f.severity === 'low' || f.severity === 'info');
350
+
351
+ const data: ReportData = {
352
+ projectName: result.projectName,
353
+ scanDate: new Date(result.scanDate).toLocaleDateString('en-US', {
354
+ year: 'numeric',
355
+ month: 'long',
356
+ day: 'numeric'
357
+ }),
358
+ scoreGrade: grade,
359
+ scoreValue: value,
360
+ criticalCount: result.summary.critical,
361
+ highCount: result.summary.high,
362
+ mediumCount: result.summary.medium,
363
+ lowCount: result.summary.low,
364
+ infoCount: result.summary.info,
365
+ executiveSummary: generateExecutiveSummary(result),
366
+ criticalFindings,
367
+ highFindings,
368
+ mediumFindings,
369
+ lowFindings,
370
+ falsePositives,
371
+ falsePositiveCount: falsePositives.length,
372
+ lowInfoCount: result.summary.low + result.summary.info,
373
+ positiveObservations: result.positiveObservations,
374
+ scanDuration: `${Math.round(result.scanDuration / 1000)}s`,
375
+ agentCount: result.agentsUsed.length
376
+ };
377
+
378
+ const renderedHtml = renderTemplate(templateHtml, data);
379
+
380
+ fs.writeFileSync(outputPath, renderedHtml);
381
+ console.log(`HTML report generated: ${outputPath}`);
382
+ }