@jishankai/solid-cli 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.
@@ -0,0 +1,616 @@
1
+ import puppeteer from 'puppeteer';
2
+ import { promises as fs } from 'fs';
3
+ import { join } from 'path';
4
+ import { readFileSync } from 'fs';
5
+ import handlebars from 'handlebars';
6
+
7
+ /**
8
+ * Enhanced PDF Generator using Puppeteer
9
+ */
10
+ export class PDFGenerator {
11
+ constructor(options = {}) {
12
+ this.options = {
13
+ styleDir: './src/report/styles',
14
+ templateDir: './src/report/templates',
15
+ ...options
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Generate professional PDF report
21
+ */
22
+ async generate(reportData) {
23
+ console.log('📑 Generating PDF report...');
24
+
25
+ // Compile HTML template
26
+ const html = await this.compileTemplate(reportData);
27
+
28
+ // Generate PDF with Puppeteer
29
+ const pdfBuffer = await this.createPDF(html);
30
+
31
+ // Save to file
32
+ const filename = `Security-Report-${reportData.metadata.reportId}.pdf`;
33
+ const filepath = join(this.options.reportsDir || './reports', filename);
34
+
35
+ await fs.writeFile(filepath, pdfBuffer);
36
+
37
+ console.log(`✅ PDF report saved: ${filepath}`);
38
+ return filepath;
39
+ }
40
+
41
+ /**
42
+ * Compile Handlebars template with data
43
+ */
44
+ async compileTemplate(reportData) {
45
+ const templatePath = join(this.options.templateDir, 'report.hbs');
46
+ const templateSource = readFileSync(templatePath, 'utf-8');
47
+
48
+ // Register custom Handlebars helpers
49
+ this.registerHelpers();
50
+
51
+ const template = handlebars.compile(templateSource);
52
+ const html = template({ ...reportData, format: 'pdf' });
53
+
54
+ // Add custom CSS styling
55
+ const css = this.getReportCSS();
56
+ return this.wrapHTML(html, css);
57
+ }
58
+
59
+ /**
60
+ * Create PDF using Puppeteer
61
+ */
62
+ async createPDF(html) {
63
+ const browser = await puppeteer.launch({
64
+ headless: true,
65
+ args: [
66
+ '--no-sandbox',
67
+ '--disable-setuid-sandbox',
68
+ '--disable-dev-shm-usage',
69
+ '--disable-accelerated-2d-canvas',
70
+ '--no-first-run',
71
+ '--no-zygote',
72
+ '--single-process',
73
+ '--disable-gpu'
74
+ ]
75
+ });
76
+
77
+ try {
78
+ const page = await browser.newPage();
79
+
80
+ // Set content and wait for loading
81
+ await page.setContent(html, { waitUntil: 'networkidle0' });
82
+
83
+ // Generate PDF with professional settings
84
+ const pdfBuffer = await page.pdf({
85
+ format: 'A4',
86
+ margin: {
87
+ top: '2cm',
88
+ right: '2cm',
89
+ bottom: '2cm',
90
+ left: '2cm'
91
+ },
92
+ printBackground: true,
93
+ displayHeaderFooter: true,
94
+ headerTemplate: this.getHeaderTemplate(),
95
+ footerTemplate: this.getFooterTemplate(),
96
+ preferCSSPageSize: true
97
+ });
98
+
99
+ return pdfBuffer;
100
+ } finally {
101
+ await browser.close();
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Get professional CSS styling (grayscale only)
107
+ */
108
+ getReportCSS() {
109
+ return `
110
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
111
+
112
+ * {
113
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
114
+ box-sizing: border-box;
115
+ }
116
+
117
+ body {
118
+ margin: 0;
119
+ padding: 0;
120
+ background: #ffffff;
121
+ color: #111111;
122
+ line-height: 1.6;
123
+ font-size: 12px;
124
+ display: flex;
125
+ justify-content: center;
126
+ }
127
+
128
+ .report-container {
129
+ width: 100%;
130
+ max-width: 820px;
131
+ margin: 0 auto;
132
+ padding: 1.5rem 1rem 2rem;
133
+ }
134
+
135
+ .header {
136
+ background: #000000;
137
+ color: #ffffff;
138
+ padding: 2rem;
139
+ border-radius: 8px;
140
+ margin-bottom: 2rem;
141
+ text-align: center;
142
+ }
143
+
144
+ .header h1 {
145
+ margin: 0;
146
+ font-size: 24px;
147
+ font-weight: 700;
148
+ }
149
+
150
+ .header .subtitle {
151
+ margin: 0.5rem 0 0 0;
152
+ font-size: 14px;
153
+ opacity: 0.9;
154
+ }
155
+
156
+ .risk-badge {
157
+ display: inline-block;
158
+ padding: 0.25rem 0.75rem;
159
+ border-radius: 20px;
160
+ font-size: 11px;
161
+ font-weight: 600;
162
+ text-transform: uppercase;
163
+ margin-left: 0.5rem;
164
+ }
165
+
166
+ .risk-high { background: #111111; color: #ffffff; }
167
+ .risk-medium { background: #555555; color: #ffffff; }
168
+ .risk-low { background: #bbbbbb; color: #000000; }
169
+ .risk-unknown { background: #e5e5e5; color: #000000; }
170
+
171
+ code {
172
+ font-family: 'SFMono-Regular', Menlo, Consolas, 'Liberation Mono', monospace;
173
+ background: #f3f4f6;
174
+ padding: 0.15rem 0.35rem;
175
+ border-radius: 4px;
176
+ word-break: break-all;
177
+ }
178
+
179
+ pre {
180
+ font-family: 'SFMono-Regular', Menlo, Consolas, 'Liberation Mono', monospace;
181
+ white-space: pre-wrap;
182
+ word-break: break-word;
183
+ }
184
+
185
+ .agent-section {
186
+ margin-bottom: 2rem;
187
+ }
188
+
189
+ .finding-summary {
190
+ margin: 0.5rem 0 1rem 0;
191
+ color: #374151;
192
+ }
193
+
194
+ .finding-error {
195
+ color: #b91c1c;
196
+ font-weight: 600;
197
+ margin: 0.75rem 0;
198
+ }
199
+
200
+ .risk-label {
201
+ font-size: 11px;
202
+ color: #555555;
203
+ }
204
+
205
+ .llm-analysis-meta {
206
+ background: #f0f9ff;
207
+ border: 1px solid #0ea5e9;
208
+ border-radius: 8px;
209
+ padding: 1.25rem;
210
+ margin-bottom: 1rem;
211
+ }
212
+
213
+ .llm-analysis-meta div {
214
+ margin: 0.15rem 0;
215
+ }
216
+
217
+ .llm-analysis-body {
218
+ background: #ffffff;
219
+ border: 1px solid #e2e8f0;
220
+ border-radius: 6px;
221
+ padding: 1rem;
222
+ white-space: pre-wrap;
223
+ word-break: break-word;
224
+ line-height: 1.5;
225
+ }
226
+
227
+ .llm-analysis-body p {
228
+ margin: 0.35rem 0;
229
+ }
230
+
231
+ .markdown-body ul { margin: 0.35rem 0 0.35rem 1.1rem; }
232
+ .markdown-body ol { margin: 0.35rem 0 0.35rem 1.2rem; }
233
+ .markdown-body li { margin: 0.1rem 0; }
234
+ .markdown-body h1,
235
+ .markdown-body h2,
236
+ .markdown-body h3,
237
+ .markdown-body h4 {
238
+ margin: 0.4rem 0 0.25rem;
239
+ line-height: 1.25;
240
+ }
241
+ .markdown-body code { background: #f3f4f6; padding: 0.15rem 0.35rem; border-radius: 4px; }
242
+
243
+ .llm-analysis-usage {
244
+ margin-top: 1rem;
245
+ font-size: 10px;
246
+ color: #64748b;
247
+ line-height: 1.5;
248
+ }
249
+
250
+ .executive-summary {
251
+ background: #f5f5f5;
252
+ border: 1px solid #d4d4d4;
253
+ border-radius: 8px;
254
+ padding: 1.5rem;
255
+ margin-bottom: 2rem;
256
+ }
257
+
258
+ .executive-summary h2 {
259
+ margin: 0 0 1rem 0;
260
+ color: #111111;
261
+ font-size: 18px;
262
+ border-bottom: 2px solid #000000;
263
+ padding-bottom: 0.5rem;
264
+ }
265
+
266
+ .metrics-grid {
267
+ display: grid;
268
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
269
+ gap: 1rem;
270
+ margin: 1rem 0;
271
+ }
272
+
273
+ .metric-card {
274
+ background: #ffffff;
275
+ border: 1px solid #d4d4d4;
276
+ border-radius: 6px;
277
+ padding: 1rem;
278
+ text-align: center;
279
+ }
280
+
281
+ .metric-value {
282
+ font-size: 24px;
283
+ font-weight: 700;
284
+ color: #111111;
285
+ }
286
+
287
+ .metric-label {
288
+ font-size: 10px;
289
+ color: #555555;
290
+ text-transform: uppercase;
291
+ letter-spacing: 0.05em;
292
+ }
293
+
294
+ .section {
295
+ margin-bottom: 2rem;
296
+ page-break-inside: avoid;
297
+ }
298
+
299
+ .section h2 {
300
+ color: #111111;
301
+ font-size: 16px;
302
+ border-left: 4px solid #000000;
303
+ padding-left: 0.75rem;
304
+ margin-bottom: 1rem;
305
+ }
306
+
307
+ .finding {
308
+ background: #ffffff;
309
+ border: 1px solid #d4d4d4;
310
+ border-radius: 6px;
311
+ padding: 1rem;
312
+ margin-bottom: 1rem;
313
+ page-break-inside: avoid;
314
+ }
315
+
316
+ .finding-header {
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: space-between;
320
+ margin-bottom: 0.5rem;
321
+ }
322
+
323
+ .finding-title {
324
+ font-weight: 600;
325
+ color: #111111;
326
+ }
327
+
328
+ .finding-risk {
329
+ padding: 0.25rem 0.5rem;
330
+ border-radius: 4px;
331
+ font-size: 10px;
332
+ font-weight: 600;
333
+ text-transform: uppercase;
334
+ }
335
+
336
+ .finding-description {
337
+ color: #333333;
338
+ margin-bottom: 0.75rem;
339
+ white-space: pre-line;
340
+ word-break: break-word;
341
+ }
342
+
343
+ .finding-details {
344
+ background: #f2f2f2;
345
+ border-radius: 4px;
346
+ padding: 0.5rem 0.75rem;
347
+ font-size: 10px;
348
+ white-space: pre-line;
349
+ word-break: break-word;
350
+ line-height: 1.4;
351
+ }
352
+
353
+ .detail-row {
354
+ margin: 0.1rem 0;
355
+ }
356
+
357
+ .detail-label {
358
+ font-weight: 600;
359
+ color: #333333;
360
+ }
361
+
362
+ .recommendations {
363
+ background: #f5f5f5;
364
+ border: 1px solid #d4d4d4;
365
+ border-radius: 8px;
366
+ padding: 1.5rem;
367
+ margin-top: 2rem;
368
+ }
369
+
370
+ .recommendations h3 {
371
+ color: #111111;
372
+ margin: 0 0 1rem 0;
373
+ }
374
+
375
+ .recommendation {
376
+ background: #ffffff;
377
+ border-left: 4px solid #000000;
378
+ padding: 1rem;
379
+ margin-bottom: 1rem;
380
+ }
381
+
382
+ .recommendation h4 {
383
+ margin: 0 0 0.5rem 0;
384
+ color: #111111;
385
+ }
386
+
387
+ .recommendation-actions {
388
+ list-style: none;
389
+ padding: 0;
390
+ margin: 0.5rem 0 0 0;
391
+ }
392
+
393
+ .recommendation-actions li {
394
+ padding: 0.25rem 0;
395
+ color: #333333;
396
+ font-size: 11px;
397
+ }
398
+
399
+ .recommendation-actions li:before {
400
+ content: "→ ";
401
+ font-weight: bold;
402
+ color: #111111;
403
+ }
404
+
405
+ .chart-container {
406
+ margin: 1rem 0;
407
+ text-align: center;
408
+ }
409
+
410
+ .risk-distribution {
411
+ display: flex;
412
+ justify-content: space-around;
413
+ align-items: center;
414
+ height: 120px;
415
+ background: #f5f5f5;
416
+ border-radius: 8px;
417
+ margin: 1rem 0;
418
+ }
419
+
420
+ .risk-segment {
421
+ text-align: center;
422
+ flex: 1;
423
+ position: relative;
424
+ }
425
+
426
+ .risk-segment:not(:last-child):after {
427
+ content: '';
428
+ position: absolute;
429
+ right: 0;
430
+ top: 20%;
431
+ height: 60%;
432
+ width: 1px;
433
+ background: #d4d4d4;
434
+ }
435
+
436
+ .risk-count {
437
+ font-size: 20px;
438
+ font-weight: 700;
439
+ color: #111111;
440
+ }
441
+
442
+ .risk-percentage {
443
+ font-size: 10px;
444
+ color: #555555;
445
+ }
446
+
447
+ .footer {
448
+ margin-top: 2rem;
449
+ padding-top: 1rem;
450
+ border-top: 1px solid #d4d4d4;
451
+ font-size: 10px;
452
+ color: #555555;
453
+ text-align: center;
454
+ }
455
+
456
+ @media print {
457
+ .section {
458
+ page-break-inside: avoid;
459
+ }
460
+
461
+ .finding {
462
+ page-break-inside: avoid;
463
+ }
464
+
465
+ .recommendations {
466
+ page-break-inside: avoid;
467
+ }
468
+ }
469
+ `;
470
+ }
471
+
472
+ /**
473
+ * Wrap HTML with proper structure
474
+ */
475
+ wrapHTML(content, css) {
476
+ return `
477
+ <!DOCTYPE html>
478
+ <html lang="en">
479
+ <head>
480
+ <meta charset="UTF-8">
481
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
482
+ <title>Security Analysis Report</title>
483
+ <style>
484
+ ${css}
485
+ </style>
486
+ </head>
487
+ <body>
488
+ <div class="report-container">
489
+ ${content}
490
+ </div>
491
+ </body>
492
+ </html>
493
+ `;
494
+ }
495
+
496
+ /**
497
+ * Get PDF header template
498
+ */
499
+ getHeaderTemplate() {
500
+ return `
501
+ <div style="font-size: 10px; color: #555555; text-align: center; width: 100%;">
502
+ <span>Security Analysis Report</span>
503
+ </div>
504
+ `;
505
+ }
506
+
507
+ /**
508
+ * Get PDF footer template
509
+ */
510
+ getFooterTemplate() {
511
+ return `
512
+ <div style="font-size: 8px; color: #555555; text-align: center; width: 100%;">
513
+ <span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
514
+ <span style="margin-left: 2rem;">Generated: ${new Date().toLocaleDateString()}</span>
515
+ </div>
516
+ `;
517
+ }
518
+
519
+ /**
520
+ * Register custom Handlebars helpers
521
+ */
522
+ registerHelpers() {
523
+ // Format date helper
524
+ handlebars.registerHelper('formatDate', (date) => {
525
+ return new Date(date).toLocaleDateString();
526
+ });
527
+
528
+ // Format time helper
529
+ handlebars.registerHelper('formatTime', (date) => {
530
+ return new Date(date).toLocaleTimeString();
531
+ });
532
+
533
+ // Uppercase helper
534
+ handlebars.registerHelper('uppercase', (str) => {
535
+ if (str === undefined || str === null) return 'UNKNOWN';
536
+ return str.toString().toUpperCase();
537
+ });
538
+
539
+ // Risk color helper (grayscale)
540
+ handlebars.registerHelper('riskColor', (risk) => {
541
+ const colors = {
542
+ high: '#111111',
543
+ medium: '#555555',
544
+ low: '#999999',
545
+ unknown: '#bbbbbb'
546
+ };
547
+ return colors[risk] || colors.unknown;
548
+ });
549
+
550
+ // Risk badge helper (monochrome)
551
+ handlebars.registerHelper('riskBadge', (risk) => {
552
+ const badges = {
553
+ high: '● HIGH',
554
+ medium: '● MEDIUM',
555
+ low: '● LOW',
556
+ unknown: '○ UNKNOWN'
557
+ };
558
+ return badges[risk] || badges.unknown;
559
+ });
560
+
561
+ // Agent name formatter
562
+ handlebars.registerHelper('formatAgentName', (agent) => {
563
+ return agent.charAt(0).toUpperCase() + agent.slice(1).replace(/([A-Z])/g, ' $1');
564
+ });
565
+
566
+ // Conditional helper
567
+ handlebars.registerHelper('ifEquals', function(arg1, arg2, options) {
568
+ return (arg1 == arg2) ? options.fn(this) : options.inverse(this);
569
+ });
570
+
571
+ // Greater than helper
572
+ handlebars.registerHelper('ifGt', function(arg1, arg2, options) {
573
+ return (arg1 > arg2) ? options.fn(this) : options.inverse(this);
574
+ });
575
+
576
+ // Array length helper
577
+ handlebars.registerHelper('length', (array) => {
578
+ return array ? array.length : 0;
579
+ });
580
+
581
+ // True if any of the provided values are "present"
582
+ handlebars.registerHelper('hasAny', (...args) => {
583
+ // Last arg is Handlebars options hash
584
+ args.pop();
585
+
586
+ return args.some((value) => {
587
+ if (value === undefined || value === null) return false;
588
+ if (typeof value === 'string') return value.trim().length > 0;
589
+ if (Array.isArray(value)) return value.length > 0;
590
+ return true;
591
+ });
592
+ });
593
+
594
+ // Logical OR helper for template conditions
595
+ handlebars.registerHelper('or', (...args) => {
596
+ args.pop(); // options hash
597
+ return args.some(Boolean);
598
+ });
599
+
600
+ // JSON stringify helper
601
+ handlebars.registerHelper('json', (obj) => {
602
+ return JSON.stringify(obj, null, 2);
603
+ });
604
+
605
+ // Calculate risk score helper
606
+ handlebars.registerHelper('calculateRiskScore', (summary) => {
607
+ const safeSummary = summary || { highRiskFindings: 0, mediumRiskFindings: 0, lowRiskFindings: 0 };
608
+ const weights = { high: 10, medium: 5, low: 1 };
609
+ const score = (safeSummary.highRiskFindings * weights.high) +
610
+ (safeSummary.mediumRiskFindings * weights.medium) +
611
+ (safeSummary.lowRiskFindings * weights.low);
612
+
613
+ return Math.min(100, score);
614
+ });
615
+ }
616
+ }