@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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/config/default.json +79 -0
- package/package.json +60 -0
- package/src/Orchestrator.js +482 -0
- package/src/agents/BaseAgent.js +35 -0
- package/src/agents/BlockchainAgent.js +453 -0
- package/src/agents/DeFiSecurityAgent.js +257 -0
- package/src/agents/NetworkAgent.js +341 -0
- package/src/agents/PermissionAgent.js +192 -0
- package/src/agents/PersistenceAgent.js +361 -0
- package/src/agents/ProcessAgent.js +572 -0
- package/src/agents/ResourceAgent.js +217 -0
- package/src/agents/SystemAgent.js +173 -0
- package/src/config/ConfigManager.js +446 -0
- package/src/index.js +629 -0
- package/src/llm/LLMAnalyzer.js +705 -0
- package/src/logging/Logger.js +352 -0
- package/src/report/ReportManager.js +445 -0
- package/src/report/generators/MarkdownGenerator.js +173 -0
- package/src/report/generators/PDFGenerator.js +616 -0
- package/src/report/templates/report.hbs +465 -0
- package/src/report/utils/formatter.js +426 -0
- package/src/report/utils/sanitizer.js +275 -0
- package/src/utils/commander.js +42 -0
- package/src/utils/signature.js +121 -0
|
@@ -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
|
+
}
|