coverme-security-scanner 3.0.0 → 3.2.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.
@@ -1,684 +0,0 @@
1
- /**
2
- * CoverMe Scanner v3 - PDF Generator
3
- * Generates professional security assessment reports
4
- */
5
-
6
- import PDFDocument from 'pdfkit';
7
- import { createWriteStream } from 'fs';
8
- import type { ScanResult, Finding, DreadScore, ThreatModelEntry } from './types.js';
9
-
10
- export class PDFGenerator {
11
- private doc: PDFKit.PDFDocument;
12
- private yPosition = 0;
13
- private pageHeight = 720;
14
- private pageWidth = 595.28; // A4
15
- private margin = 50;
16
-
17
- constructor() {
18
- this.doc = new PDFDocument({
19
- size: 'A4',
20
- margins: { top: 50, bottom: 50, left: 50, right: 50 },
21
- });
22
- }
23
-
24
- async generate(result: ScanResult, outputPath: string): Promise<void> {
25
- const stream = createWriteStream(outputPath);
26
- this.doc.pipe(stream);
27
-
28
- // Cover Page
29
- this.renderCoverPage(result);
30
-
31
- // Table of Contents
32
- this.addPage();
33
- this.renderTableOfContents(result);
34
-
35
- // Executive Summary
36
- this.addPage();
37
- this.renderExecutiveSummary(result);
38
-
39
- // Architecture Overview
40
- if (result.architectureOverview) {
41
- this.addPage();
42
- this.renderArchitectureOverview(result);
43
- }
44
-
45
- // Security Findings - CRITICAL
46
- const criticalFindings = result.findings.filter((f) => f.severity === 'critical');
47
- if (criticalFindings.length > 0) {
48
- this.addPage();
49
- this.renderFindingsSection('CRITICAL', criticalFindings);
50
- }
51
-
52
- // Security Findings - HIGH
53
- const highFindings = result.findings.filter((f) => f.severity === 'high');
54
- if (highFindings.length > 0) {
55
- this.addPage();
56
- this.renderFindingsSection('HIGH', highFindings);
57
- }
58
-
59
- // Security & Quality Findings - MEDIUM
60
- const mediumFindings = result.findings.filter((f) => f.severity === 'medium');
61
- if (mediumFindings.length > 0) {
62
- this.addPage();
63
- this.renderFindingsSection('MEDIUM', mediumFindings);
64
- }
65
-
66
- // Low & Informational
67
- const lowFindings = result.findings.filter((f) => f.severity === 'low' || f.severity === 'info');
68
- if (lowFindings.length > 0) {
69
- this.addPage();
70
- this.renderLowSeveritySection(lowFindings);
71
- }
72
-
73
- // Threat Model Register
74
- if (result.threatModel && result.threatModel.length > 0) {
75
- this.addPage();
76
- this.renderThreatModel(result);
77
- }
78
-
79
- // Quality Review
80
- if (result.qualityReview) {
81
- this.addPage();
82
- this.renderQualityReview(result);
83
- }
84
-
85
- // Previously Resolved
86
- if (result.previouslyResolved && result.previouslyResolved.length > 0) {
87
- this.addPage();
88
- this.renderPreviouslyResolved(result);
89
- }
90
-
91
- // Positive Observations
92
- if (result.positiveObservations && result.positiveObservations.length > 0) {
93
- this.addPage();
94
- this.renderPositiveObservations(result);
95
- }
96
-
97
- // Summary Page
98
- this.addPage();
99
- this.renderSummaryPage(result);
100
-
101
- this.doc.end();
102
-
103
- return new Promise((resolve) => {
104
- stream.on('finish', resolve);
105
- });
106
- }
107
-
108
- private renderCoverPage(result: ScanResult): void {
109
- // Dark header background
110
- this.doc.rect(0, 0, this.pageWidth, 200).fill('#1e293b');
111
-
112
- // Title
113
- this.doc
114
- .fillColor('#ffffff')
115
- .fontSize(32)
116
- .font('Helvetica-Bold')
117
- .text('Combined Assessment Report', this.margin, 80, {
118
- align: 'center',
119
- width: this.pageWidth - this.margin * 2,
120
- });
121
-
122
- // Project name
123
- this.doc
124
- .fillColor('#e2e8f0')
125
- .fontSize(20)
126
- .font('Helvetica')
127
- .text(result.projectName, this.margin, 130, {
128
- align: 'center',
129
- width: this.pageWidth - this.margin * 2,
130
- });
131
-
132
- // Subtitle
133
- const subtitle = `Comprehensive Codebase Review - ${result.filesScanned} files, ~${result.linesOfCode?.toLocaleString() || 'N/A'} lines - ${result.summary.total} Findings`;
134
- this.doc
135
- .fillColor('#94a3b8')
136
- .fontSize(11)
137
- .text(subtitle, this.margin, 160, {
138
- align: 'center',
139
- width: this.pageWidth - this.margin * 2,
140
- });
141
-
142
- // Date badge
143
- this.doc.roundedRect(this.pageWidth / 2 - 80, 220, 160, 36, 18).fill('#3b82f6');
144
- this.doc
145
- .fillColor('#ffffff')
146
- .fontSize(13)
147
- .font('Helvetica-Bold')
148
- .text(
149
- new Date(result.scanDate).toLocaleDateString('en-US', {
150
- year: 'numeric',
151
- month: 'long',
152
- day: 'numeric',
153
- }),
154
- this.pageWidth / 2 - 80,
155
- 230,
156
- { width: 160, align: 'center' }
157
- );
158
-
159
- // Summary table
160
- this.renderCoverTable(result, 290);
161
-
162
- // Severity boxes
163
- this.renderSeverityBoxes(result, 480);
164
-
165
- // Footer
166
- this.doc
167
- .fontSize(8)
168
- .fillColor('#9ca3af')
169
- .text('CONFIDENTIAL', this.margin, this.pageHeight + 30, {
170
- align: 'center',
171
- width: this.pageWidth - this.margin * 2,
172
- });
173
- }
174
-
175
- private renderCoverTable(result: ScanResult, startY: number): void {
176
- const rows = [
177
- ['Project', result.projectName],
178
- ['Review Type', 'Security - Threat Model - Code Quality - Codebase Review'],
179
- ['Branch', result.branch || 'main'],
180
- ['Scope', `${result.filesScanned} files, ~${result.linesOfCode?.toLocaleString() || 'N/A'} lines`],
181
- ['Components', result.projectOverview?.keyComponents?.slice(0, 3).join(', ') || 'Full platform'],
182
- ['Author', 'Claude Code - Claude Opus 4.5'],
183
- ];
184
-
185
- let y = startY;
186
- rows.forEach(([label, value]) => {
187
- // Background stripe
188
- this.doc.rect(this.margin, y - 5, this.pageWidth - this.margin * 2, 24).fill('#f8fafc');
189
-
190
- this.doc.fontSize(10).font('Helvetica-Bold').fillColor('#64748b').text(label, this.margin + 10, y);
191
- this.doc.fontSize(10).font('Helvetica').fillColor('#1e293b').text(String(value), 160, y, {
192
- width: this.pageWidth - 160 - this.margin,
193
- });
194
- y += 28;
195
- });
196
- }
197
-
198
- private renderSeverityBoxes(result: ScanResult, startY: number): void {
199
- const boxWidth = 80;
200
- const boxHeight = 90;
201
- const gap = 15;
202
- const totalWidth = boxWidth * 5 + gap * 4;
203
- const startX = (this.pageWidth - totalWidth) / 2;
204
-
205
- const severities = [
206
- { label: 'CRITICAL', count: result.summary.critical, color: '#dc2626' },
207
- { label: 'HIGH', count: result.summary.high, color: '#9333ea' },
208
- { label: 'MEDIUM', count: result.summary.medium, color: '#ea580c' },
209
- { label: 'LOW', count: result.summary.low, color: '#6b7280' },
210
- { label: 'TOTAL', count: result.summary.total, color: '#1e293b' },
211
- ];
212
-
213
- severities.forEach((sev, i) => {
214
- const x = startX + i * (boxWidth + gap);
215
-
216
- // Box background
217
- this.doc.roundedRect(x, startY, boxWidth, boxHeight, 4).fill(sev.color + '12');
218
-
219
- // Count
220
- this.doc
221
- .fontSize(36)
222
- .font('Helvetica-Bold')
223
- .fillColor(sev.color)
224
- .text(sev.count.toString(), x, startY + 18, { width: boxWidth, align: 'center' });
225
-
226
- // Label
227
- this.doc
228
- .fontSize(9)
229
- .font('Helvetica')
230
- .fillColor('#6b7280')
231
- .text(sev.label, x, startY + 65, { width: boxWidth, align: 'center' });
232
- });
233
- }
234
-
235
- private renderTableOfContents(result: ScanResult): void {
236
- this.renderSectionHeader('Table of Contents');
237
-
238
- const sections = [
239
- 'Executive Summary',
240
- result.architectureOverview && 'Architecture Overview & Trust Boundaries',
241
- result.findings.filter((f) => f.severity === 'critical').length > 0 && 'Security Findings (CRITICAL)',
242
- result.findings.filter((f) => f.severity === 'high').length > 0 && 'Security Findings (HIGH)',
243
- result.findings.filter((f) => f.severity === 'medium').length > 0 && 'Security & Code Quality Findings (MEDIUM)',
244
- result.findings.filter((f) => f.severity === 'low' || f.severity === 'info').length > 0 &&
245
- 'Low-Severity & Informational Findings',
246
- result.threatModel?.length && 'Threat Model Register (STRIDE + DREAD)',
247
- result.qualityReview && 'Quality Review - Delete / Merge / Simplify',
248
- result.previouslyResolved?.length && 'Previously Resolved Issues',
249
- result.positiveObservations?.length && 'Positive Observations',
250
- 'Summary',
251
- ].filter(Boolean);
252
-
253
- this.yPosition = 120;
254
- sections.forEach((section) => {
255
- this.doc.fillColor('#1e293b').fontSize(12).font('Helvetica').text(String(section), this.margin, this.yPosition);
256
- this.yPosition += 28;
257
- });
258
- }
259
-
260
- private renderExecutiveSummary(result: ScanResult): void {
261
- this.renderSectionHeader('Executive Summary');
262
- this.yPosition = 100;
263
-
264
- // Overview paragraph
265
- if (result.executiveSummary?.overview) {
266
- this.doc
267
- .fillColor('#1e293b')
268
- .fontSize(11)
269
- .font('Helvetica')
270
- .text(result.executiveSummary.overview, this.margin, this.yPosition, {
271
- width: this.pageWidth - this.margin * 2,
272
- align: 'justify',
273
- lineGap: 4,
274
- });
275
- this.yPosition += this.doc.heightOfString(result.executiveSummary.overview, {
276
- width: this.pageWidth - this.margin * 2,
277
- }) + 30;
278
- }
279
-
280
- // No critical banner
281
- if (result.summary.critical === 0) {
282
- this.doc
283
- .roundedRect(this.margin, this.yPosition, this.pageWidth - this.margin * 2, 30, 4)
284
- .fill('#22c55e15');
285
- this.doc
286
- .fontSize(12)
287
- .font('Helvetica-Bold')
288
- .fillColor('#22c55e')
289
- .text('No critical vulnerabilities remain.', this.margin + 15, this.yPosition + 9);
290
- this.yPosition += 50;
291
- }
292
-
293
- // Top Risks
294
- if (result.executiveSummary?.topRisks?.length) {
295
- this.doc.fontSize(14).font('Helvetica-Bold').fillColor('#1e293b').text('Top Risks', this.margin, this.yPosition);
296
- this.yPosition += 25;
297
-
298
- result.executiveSummary.topRisks.slice(0, 5).forEach((risk) => {
299
- this.doc
300
- .fontSize(10)
301
- .font('Helvetica')
302
- .fillColor('#374151')
303
- .text(` - ${risk}`, this.margin, this.yPosition, {
304
- width: this.pageWidth - this.margin * 2 - 10,
305
- lineGap: 2,
306
- });
307
- this.yPosition += this.doc.heightOfString(risk, { width: this.pageWidth - this.margin * 2 - 20 }) + 8;
308
- });
309
- }
310
- }
311
-
312
- private renderArchitectureOverview(result: ScanResult): void {
313
- this.renderSectionHeader('Architecture Overview');
314
- this.yPosition = 100;
315
-
316
- if (!result.architectureOverview) return;
317
-
318
- // Components table
319
- result.architectureOverview.components?.forEach((comp) => {
320
- this.doc.roundedRect(this.margin, this.yPosition, this.pageWidth - this.margin * 2, 50, 4).fill('#f8fafc');
321
-
322
- this.doc.fontSize(12).font('Helvetica-Bold').fillColor('#1e293b').text(comp.name, this.margin + 10, this.yPosition + 10);
323
-
324
- this.doc
325
- .fontSize(10)
326
- .font('Helvetica')
327
- .fillColor('#64748b')
328
- .text(comp.description, this.margin + 10, this.yPosition + 28, {
329
- width: this.pageWidth - this.margin * 2 - 20,
330
- });
331
-
332
- this.yPosition += 60;
333
- });
334
-
335
- // Trust Boundaries
336
- if (result.architectureOverview.trustBoundaries?.length) {
337
- this.yPosition += 20;
338
- this.doc.fontSize(12).font('Helvetica-Bold').fillColor('#1e293b').text('Trust Boundaries', this.margin, this.yPosition);
339
- this.yPosition += 20;
340
-
341
- result.architectureOverview.trustBoundaries.forEach((tb) => {
342
- this.doc
343
- .fontSize(10)
344
- .font('Helvetica')
345
- .fillColor('#374151')
346
- .text(`${tb.name}: ${tb.from} -> ${tb.to} (${tb.protocol})`, this.margin + 10, this.yPosition);
347
- this.yPosition += 18;
348
- });
349
- }
350
- }
351
-
352
- private renderFindingsSection(severity: string, findings: Finding[]): void {
353
- const colors: Record<string, string> = {
354
- CRITICAL: '#dc2626',
355
- HIGH: '#9333ea',
356
- MEDIUM: '#ea580c',
357
- };
358
- const color = colors[severity] || '#6b7280';
359
-
360
- this.renderSectionHeader(`Security Findings - ${severity}`, color);
361
- this.yPosition = 100;
362
-
363
- findings.forEach((finding) => {
364
- this.renderFinding(finding, color);
365
- });
366
- }
367
-
368
- private renderFinding(finding: Finding, color: string): void {
369
- // Check page break
370
- if (this.yPosition > this.pageHeight - 200) {
371
- this.addPage();
372
- }
373
-
374
- // ID badge
375
- this.doc.roundedRect(this.margin, this.yPosition, this.pageWidth - this.margin * 2, 35, 4).fill(color + '10');
376
- this.doc.fontSize(11).font('Helvetica-Bold').fillColor(color).text(finding.id, this.margin + 12, this.yPosition + 11);
377
- this.yPosition += 45;
378
-
379
- // Title
380
- this.doc.fontSize(15).font('Helvetica-Bold').fillColor('#1e293b').text(finding.title, this.margin, this.yPosition);
381
- this.yPosition += 25;
382
-
383
- // Severity badge
384
- this.doc.roundedRect(this.margin, this.yPosition, 55, 22, 11).fill(color);
385
- this.doc
386
- .fontSize(10)
387
- .font('Helvetica-Bold')
388
- .fillColor('#ffffff')
389
- .text(finding.severity.toUpperCase(), this.margin, this.yPosition + 5, { width: 55, align: 'center' });
390
- this.yPosition += 35;
391
-
392
- // File reference
393
- if (finding.file) {
394
- this.doc
395
- .fontSize(9)
396
- .font('Helvetica')
397
- .fillColor('#3b82f6')
398
- .text(`${finding.file}:${finding.line}`, this.margin, this.yPosition);
399
- this.yPosition += 18;
400
- }
401
-
402
- // Description
403
- this.doc.fontSize(10).font('Helvetica-Bold').fillColor('#64748b').text('DESCRIPTION', this.margin, this.yPosition);
404
- this.yPosition += 16;
405
-
406
- this.doc
407
- .fontSize(10)
408
- .font('Helvetica')
409
- .fillColor('#1e293b')
410
- .text(finding.description, this.margin, this.yPosition, {
411
- width: this.pageWidth - this.margin * 2,
412
- align: 'justify',
413
- lineGap: 3,
414
- });
415
- this.yPosition += this.doc.heightOfString(finding.description, { width: this.pageWidth - this.margin * 2 }) + 20;
416
-
417
- // Recommendation
418
- this.doc.fontSize(10).font('Helvetica-Bold').fillColor('#64748b').text('RECOMMENDATION', this.margin, this.yPosition);
419
- this.yPosition += 16;
420
-
421
- this.doc
422
- .fontSize(10)
423
- .font('Helvetica')
424
- .fillColor('#1e293b')
425
- .text(finding.recommendation, this.margin, this.yPosition, {
426
- width: this.pageWidth - this.margin * 2,
427
- align: 'justify',
428
- lineGap: 3,
429
- });
430
- this.yPosition += this.doc.heightOfString(finding.recommendation, { width: this.pageWidth - this.margin * 2 }) + 40;
431
- }
432
-
433
- private renderLowSeveritySection(findings: Finding[]): void {
434
- this.renderSectionHeader('Low-Severity & Informational');
435
- this.yPosition = 100;
436
-
437
- findings.forEach((f) => {
438
- this.doc
439
- .fontSize(10)
440
- .font('Helvetica-Bold')
441
- .fillColor('#64748b')
442
- .text(f.id, this.margin, this.yPosition, { continued: true })
443
- .font('Helvetica')
444
- .fillColor('#1e293b')
445
- .text(` ${f.title}`);
446
- this.yPosition += 22;
447
- });
448
- }
449
-
450
- private renderThreatModel(result: ScanResult): void {
451
- this.renderSectionHeader('Threat Model Register (v2.0)');
452
- this.yPosition = 90;
453
-
454
- this.doc
455
- .fontSize(10)
456
- .font('Helvetica')
457
- .fillColor('#64748b')
458
- .text('STRIDE + DREAD-D analysis. Methodology: STRIDE per component.', this.margin, this.yPosition);
459
- this.yPosition += 30;
460
-
461
- // Table header
462
- this.doc.rect(this.margin, this.yPosition, this.pageWidth - this.margin * 2, 25).fill('#1e293b');
463
-
464
- const cols = [
465
- { label: 'ID', width: 70 },
466
- { label: 'Sev', width: 50 },
467
- { label: 'DREAD', width: 50 },
468
- { label: 'Status', width: 60 },
469
- { label: 'Finding', width: 265 },
470
- ];
471
-
472
- let colX = this.margin + 8;
473
- this.doc.fontSize(9).font('Helvetica-Bold').fillColor('#ffffff');
474
- cols.forEach((col) => {
475
- this.doc.text(col.label, colX, this.yPosition + 8, { width: col.width });
476
- colX += col.width;
477
- });
478
- this.yPosition += 30;
479
-
480
- // Rows
481
- result.threatModel?.slice(0, 16).forEach((threat, i) => {
482
- if (this.yPosition > this.pageHeight - 40) {
483
- this.addPage();
484
- this.yPosition = 50;
485
- }
486
-
487
- // Alternate row background
488
- if (i % 2 === 0) {
489
- this.doc.rect(this.margin, this.yPosition - 3, this.pageWidth - this.margin * 2, 20).fill('#f8fafc');
490
- }
491
-
492
- colX = this.margin + 8;
493
- this.doc.fontSize(9).font('Helvetica').fillColor('#1e293b');
494
-
495
- this.doc.text(threat.id, colX, this.yPosition, { width: cols[0].width });
496
- colX += cols[0].width;
497
-
498
- const dreadNum = threat.dreadScore ?? 5;
499
- const sevColor = dreadNum >= 7 ? '#dc2626' : dreadNum >= 5 ? '#9333ea' : '#ea580c';
500
- const sevLabel = dreadNum >= 7 ? 'HIGH' : dreadNum >= 5 ? 'MED' : 'LOW';
501
-
502
- this.doc.fillColor(sevColor).text(sevLabel, colX, this.yPosition, { width: cols[1].width });
503
- colX += cols[1].width;
504
-
505
- this.doc.fillColor('#1e293b').text(dreadNum.toFixed(1), colX, this.yPosition, { width: cols[2].width });
506
- colX += cols[2].width;
507
-
508
- const statusColors: Record<string, string> = {
509
- mitigated: '#22c55e',
510
- partial: '#ea580c',
511
- accepted: '#6b7280',
512
- open: '#dc2626',
513
- };
514
- this.doc.fillColor(statusColors[threat.status] || '#dc2626').text(
515
- threat.status.charAt(0).toUpperCase() + threat.status.slice(1),
516
- colX,
517
- this.yPosition,
518
- { width: cols[3].width }
519
- );
520
- colX += cols[3].width;
521
-
522
- this.doc.fillColor('#1e293b').text(threat.threat, colX, this.yPosition, { width: cols[4].width });
523
-
524
- this.yPosition += 22;
525
- });
526
- }
527
-
528
- private renderQualityReview(result: ScanResult): void {
529
- this.renderSectionHeader('Quality Review - Dead Code & DRY Violations');
530
- this.yPosition = 100;
531
-
532
- if (!result.qualityReview) return;
533
-
534
- this.doc
535
- .fontSize(11)
536
- .font('Helvetica')
537
- .fillColor('#1e293b')
538
- .text(
539
- `${result.qualityReview.totalLinesRemovable.toLocaleString()} lines removable (${result.qualityReview.percentageOfCodebase.toFixed(1)}% of codebase)`,
540
- this.margin,
541
- this.yPosition
542
- );
543
- this.yPosition += 35;
544
-
545
- // MERGE section
546
- if (result.qualityReview.mergeItems?.length) {
547
- this.doc.fontSize(13).font('Helvetica-Bold').fillColor('#ea580c').text('MERGE - Highest ROI', this.margin, this.yPosition);
548
- this.yPosition += 22;
549
-
550
- result.qualityReview.mergeItems.slice(0, 6).forEach((item) => {
551
- this.renderQualityItem(item);
552
- });
553
- this.yPosition += 15;
554
- }
555
-
556
- // DELETE section
557
- if (result.qualityReview.deleteItems?.length) {
558
- this.doc.fontSize(13).font('Helvetica-Bold').fillColor('#dc2626').text('DELETE - Dead Code', this.margin, this.yPosition);
559
- this.yPosition += 22;
560
-
561
- result.qualityReview.deleteItems.slice(0, 6).forEach((item) => {
562
- this.renderQualityItem(item);
563
- });
564
- }
565
- }
566
-
567
- private renderQualityItem(item: { title: string; description: string; roi?: string; lines?: number }): void {
568
- const roiText = item.roi || (item.lines ? `~${item.lines} lines` : '');
569
-
570
- this.doc
571
- .fontSize(10)
572
- .font('Helvetica-Bold')
573
- .fillColor('#1e293b')
574
- .text(item.title, this.margin + 10, this.yPosition, { continued: true })
575
- .font('Helvetica')
576
- .fillColor('#64748b')
577
- .text(roiText ? ` ${roiText}` : '');
578
- this.yPosition += 16;
579
-
580
- this.doc
581
- .fontSize(9)
582
- .font('Helvetica')
583
- .fillColor('#64748b')
584
- .text(item.description, this.margin + 20, this.yPosition, { width: this.pageWidth - this.margin * 2 - 30 });
585
- this.yPosition += 20;
586
- }
587
-
588
- private renderPreviouslyResolved(result: ScanResult): void {
589
- this.renderSectionHeader('Previously Resolved Issues');
590
- this.yPosition = 100;
591
-
592
- result.previouslyResolved?.forEach((issue) => {
593
- this.doc
594
- .roundedRect(this.margin, this.yPosition, this.pageWidth - this.margin * 2, 50, 4)
595
- .fill('#22c55e10');
596
-
597
- this.doc
598
- .fontSize(11)
599
- .font('Helvetica-Bold')
600
- .fillColor('#22c55e')
601
- .text(` ${issue.title} (was ${issue.originalSeverity.toUpperCase()})`, this.margin + 10, this.yPosition + 10);
602
-
603
- this.doc
604
- .fontSize(10)
605
- .font('Helvetica')
606
- .fillColor('#1e293b')
607
- .text(`Resolved: ${issue.resolution}`, this.margin + 10, this.yPosition + 28, {
608
- width: this.pageWidth - this.margin * 2 - 20,
609
- });
610
-
611
- this.yPosition += 60;
612
- });
613
- }
614
-
615
- private renderPositiveObservations(result: ScanResult): void {
616
- this.renderSectionHeader('Positive Observations');
617
- this.yPosition = 100;
618
-
619
- result.positiveObservations?.forEach((obs) => {
620
- this.doc
621
- .roundedRect(this.margin, this.yPosition, this.pageWidth - this.margin * 2, 55, 4)
622
- .fill('#22c55e08');
623
-
624
- this.doc
625
- .fontSize(11)
626
- .font('Helvetica-Bold')
627
- .fillColor('#22c55e')
628
- .text(` ${obs.title}`, this.margin + 10, this.yPosition + 10);
629
-
630
- this.doc
631
- .fontSize(10)
632
- .font('Helvetica')
633
- .fillColor('#374151')
634
- .text(obs.description, this.margin + 10, this.yPosition + 28, {
635
- width: this.pageWidth - this.margin * 2 - 20,
636
- lineGap: 2,
637
- });
638
-
639
- this.yPosition += 65;
640
- });
641
- }
642
-
643
- private renderSummaryPage(result: ScanResult): void {
644
- this.renderSectionHeader('Summary');
645
- this.yPosition = 100;
646
-
647
- // Finding Severity table
648
- this.doc.fontSize(13).font('Helvetica-Bold').fillColor('#1e293b').text('Finding Severity', this.margin, this.yPosition);
649
- this.yPosition += 25;
650
-
651
- const severities = [
652
- { label: 'CRITICAL', count: result.summary.critical, color: '#dc2626' },
653
- { label: 'HIGH', count: result.summary.high, color: '#9333ea' },
654
- { label: 'MEDIUM', count: result.summary.medium, color: '#ea580c' },
655
- { label: 'LOW', count: result.summary.low, color: '#6b7280' },
656
- { label: 'TOTAL', count: result.summary.total, color: '#1e293b' },
657
- ];
658
-
659
- severities.forEach((sev) => {
660
- this.doc.rect(this.margin, this.yPosition, 150, 25).fill('#f8fafc');
661
- this.doc.fontSize(10).font('Helvetica-Bold').fillColor(sev.color).text(sev.label, this.margin + 10, this.yPosition + 7);
662
- this.doc.fontSize(10).font('Helvetica').fillColor('#1e293b').text(sev.count.toString(), this.margin + 120, this.yPosition + 7);
663
- this.yPosition += 28;
664
- });
665
-
666
- // Metadata
667
- this.yPosition += 30;
668
- this.doc.fontSize(11).font('Helvetica').fillColor('#64748b').text(`Scan Date: ${result.scanDate}`, this.margin, this.yPosition);
669
- this.yPosition += 18;
670
- this.doc.text(`Files Scanned: ${result.filesScanned}`, this.margin, this.yPosition);
671
- this.yPosition += 18;
672
- this.doc.text(`Lines of Code: ${result.linesOfCode?.toLocaleString() || 'N/A'}`, this.margin, this.yPosition);
673
- }
674
-
675
- private renderSectionHeader(title: string, underlineColor = '#ea580c'): void {
676
- this.doc.fontSize(24).font('Helvetica-Bold').fillColor('#1e293b').text(title, this.margin, 50);
677
- this.doc.rect(this.margin, 82, 60, 3).fill(underlineColor);
678
- }
679
-
680
- private addPage(): void {
681
- this.doc.addPage();
682
- this.yPosition = 0;
683
- }
684
- }