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.
- package/README.md +227 -0
- package/commands/scan.md +317 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +39 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +636 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/scan.d.ts +11 -0
- package/dist/cli/scan.d.ts.map +1 -0
- package/dist/cli/scan.js +498 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/report/generator.d.ts +48 -0
- package/dist/report/generator.d.ts.map +1 -0
- package/dist/report/generator.js +368 -0
- package/dist/report/generator.js.map +1 -0
- package/dist/report/index.d.ts +35 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +463 -0
- package/dist/report/index.js.map +1 -0
- package/dist/templates/report.html +796 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
- package/src/cli/index.ts +43 -0
- package/src/cli/init.ts +611 -0
- package/src/cli/scan.ts +483 -0
- package/src/prompts/architecture-reviewer.md +171 -0
- package/src/prompts/consensus-builder.md +247 -0
- package/src/prompts/context-discovery.md +174 -0
- package/src/prompts/cross-validator.md +224 -0
- package/src/prompts/deep-dive-expert.md +224 -0
- package/src/prompts/dependency-auditor.md +190 -0
- package/src/prompts/performance-hunter.md +200 -0
- package/src/prompts/quality-analyzer.md +150 -0
- package/src/prompts/report-generator.md +285 -0
- package/src/prompts/security-scanner.md +180 -0
- package/src/report/generator.ts +382 -0
- package/src/report/index.ts +483 -0
- package/src/templates/report.html +796 -0
- package/src/types.ts +107 -0
- 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, '&')
|
|
78
|
+
.replace(/</g, '<')
|
|
79
|
+
.replace(/>/g, '>')
|
|
80
|
+
.replace(/"/g, '"')
|
|
81
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|