@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,445 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import puppeteer from 'puppeteer';
|
|
4
|
+
import handlebars from 'handlebars';
|
|
5
|
+
import { PDFGenerator } from './generators/PDFGenerator.js';
|
|
6
|
+
import { MarkdownGenerator } from './generators/MarkdownGenerator.js';
|
|
7
|
+
import { ReportSanitizer } from './utils/sanitizer.js';
|
|
8
|
+
import { ReportFormatter } from './utils/formatter.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Enhanced Report Manager - Professional report generation with templates
|
|
12
|
+
*/
|
|
13
|
+
export class ReportManager {
|
|
14
|
+
constructor(results, llmAnalysis = null, options = {}) {
|
|
15
|
+
// Validate inputs
|
|
16
|
+
if (!results) {
|
|
17
|
+
throw new Error('ReportManager requires analysis results');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this.results = results;
|
|
21
|
+
this.llmAnalysis = llmAnalysis;
|
|
22
|
+
this.options = {
|
|
23
|
+
reportsDir: './reports',
|
|
24
|
+
templateDir: './src/report/templates',
|
|
25
|
+
styleDir: './src/report/styles',
|
|
26
|
+
retentionDays: 90,
|
|
27
|
+
defaultTemplate: 'executive',
|
|
28
|
+
...options
|
|
29
|
+
};
|
|
30
|
+
this.timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
|
31
|
+
this.sanitizer = new ReportSanitizer();
|
|
32
|
+
this.formatter = new ReportFormatter();
|
|
33
|
+
this.pdfGenerator = new PDFGenerator(this.options);
|
|
34
|
+
this.markdownGenerator = new MarkdownGenerator(this.options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate all requested report formats
|
|
39
|
+
*/
|
|
40
|
+
async generateReports(formats = ['markdown', 'pdf']) {
|
|
41
|
+
const savedFiles = [];
|
|
42
|
+
const reportData = this.prepareReportData();
|
|
43
|
+
|
|
44
|
+
// Create output directory structure
|
|
45
|
+
await this.ensureReportDirectory();
|
|
46
|
+
|
|
47
|
+
// Force generators to use the dated report directory
|
|
48
|
+
this.markdownGenerator.options.reportsDir = this.reportDir;
|
|
49
|
+
this.pdfGenerator.options.reportsDir = this.reportDir;
|
|
50
|
+
|
|
51
|
+
// Generate reports in parallel
|
|
52
|
+
const generationPromises = [];
|
|
53
|
+
|
|
54
|
+
if (formats.includes('markdown')) {
|
|
55
|
+
generationPromises.push(
|
|
56
|
+
this.markdownGenerator.generate(reportData)
|
|
57
|
+
.then(path => savedFiles.push({ type: 'markdown', path }))
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (formats.includes('pdf')) {
|
|
62
|
+
generationPromises.push(
|
|
63
|
+
this.pdfGenerator.generate(reportData)
|
|
64
|
+
.then(path => savedFiles.push({ type: 'pdf', path }))
|
|
65
|
+
.catch(error => {
|
|
66
|
+
console.warn(`PDF generation failed: ${error.message}`);
|
|
67
|
+
return null;
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await Promise.all(generationPromises);
|
|
73
|
+
|
|
74
|
+
// Save report metadata
|
|
75
|
+
await this.saveReportMetadata(savedFiles, reportData);
|
|
76
|
+
|
|
77
|
+
// Cleanup old reports
|
|
78
|
+
await this.cleanupOldReports();
|
|
79
|
+
|
|
80
|
+
return savedFiles.filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Prepare comprehensive report data
|
|
85
|
+
*/
|
|
86
|
+
prepareReportData() {
|
|
87
|
+
// Validate results structure
|
|
88
|
+
if (!this.results) {
|
|
89
|
+
throw new Error('No analysis results available for report generation');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Ensure summary exists and has required properties
|
|
93
|
+
const summary = this.results.summary || {
|
|
94
|
+
totalFindings: 0,
|
|
95
|
+
highRiskFindings: 0,
|
|
96
|
+
mediumRiskFindings: 0,
|
|
97
|
+
lowRiskFindings: 0,
|
|
98
|
+
agentSummaries: {},
|
|
99
|
+
error: 'Summary data not available'
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
// System Information
|
|
104
|
+
system: {
|
|
105
|
+
hostname: this.results.hostname || 'Unknown',
|
|
106
|
+
osVersion: this.results.osVersion || 'Unknown',
|
|
107
|
+
timestamp: this.results.timestamp || new Date().toISOString(),
|
|
108
|
+
mode: this.results.mode || 'unknown',
|
|
109
|
+
analysisDepth: this.results.analysisDepth || 'comprehensive'
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// Executive Summary
|
|
113
|
+
summary: {
|
|
114
|
+
...summary,
|
|
115
|
+
overallRisk: this.results.overallRisk || 'unknown',
|
|
116
|
+
riskLevel: this.getRiskLevel(this.results.overallRisk || 'unknown'),
|
|
117
|
+
analysisPhases: this.results.analysisPhases || [],
|
|
118
|
+
adaptiveAnalysis: this.results.adaptiveAnalysis || {}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Detailed Findings
|
|
122
|
+
findings: this.processFindings(this.results.agents),
|
|
123
|
+
|
|
124
|
+
// Sensitive data alerts (if any)
|
|
125
|
+
sensitivePatterns: this.llmAnalysis?.securityCheck?.sensitivePatterns || [],
|
|
126
|
+
sensitiveDataAlerts: this.llmAnalysis?.securityCheck?.detectedLocations || [],
|
|
127
|
+
|
|
128
|
+
// LLM Analysis (if available)
|
|
129
|
+
llmAnalysis: this.llmAnalysis,
|
|
130
|
+
|
|
131
|
+
// Charts and Visualizations Data
|
|
132
|
+
visualizations: this.prepareVisualizationData(),
|
|
133
|
+
|
|
134
|
+
// Compliance Information
|
|
135
|
+
compliance: this.prepareComplianceData(),
|
|
136
|
+
|
|
137
|
+
// Recommendations
|
|
138
|
+
recommendations: this.generateRecommendations(),
|
|
139
|
+
|
|
140
|
+
// Report Metadata
|
|
141
|
+
metadata: {
|
|
142
|
+
generatedAt: new Date().toISOString(),
|
|
143
|
+
version: '2.0.0',
|
|
144
|
+
reportId: this.generateReportId()
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Process and sanitize findings for reporting
|
|
151
|
+
*/
|
|
152
|
+
processFindings(agents) {
|
|
153
|
+
const processedFindings = {};
|
|
154
|
+
const perFinding = this.llmAnalysis?.perFinding || {};
|
|
155
|
+
|
|
156
|
+
for (const [agentKey, agentResult] of Object.entries(agents)) {
|
|
157
|
+
if (agentResult.error) {
|
|
158
|
+
processedFindings[agentKey] = {
|
|
159
|
+
name: agentResult.agent || agentKey,
|
|
160
|
+
error: agentResult.error,
|
|
161
|
+
overallRisk: 'unknown',
|
|
162
|
+
findings: []
|
|
163
|
+
};
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const findings = (agentResult.findings || []).map((finding, index) => {
|
|
168
|
+
const findingId = `${agentKey}#${index}`;
|
|
169
|
+
const llm = perFinding[findingId];
|
|
170
|
+
const sanitizedDescription = this.sanitizer.sanitizeText(finding.description);
|
|
171
|
+
|
|
172
|
+
const aiPurpose = llm ? this.sanitizer.sanitizeText(llm.purpose || '') : '';
|
|
173
|
+
const aiRisk = llm ? this.sanitizer.sanitizeText(llm.risk || llm.analysis || '') : '';
|
|
174
|
+
const aiAction = llm ? this.sanitizer.sanitizeText(llm.action || llm.remediation || '') : '';
|
|
175
|
+
|
|
176
|
+
const withLlm = llm ? [
|
|
177
|
+
sanitizedDescription,
|
|
178
|
+
aiPurpose ? `AI Analysis: ${aiPurpose}` : null,
|
|
179
|
+
aiRisk ? `Risk Assessment: ${aiRisk}` : null,
|
|
180
|
+
aiAction ? `AI Recommended Action: ${aiAction}` : null
|
|
181
|
+
].filter(Boolean).join('\n') : sanitizedDescription;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...finding,
|
|
185
|
+
id: findingId,
|
|
186
|
+
description: withLlm,
|
|
187
|
+
command: this.sanitizer.sanitizeText(finding.command),
|
|
188
|
+
path: this.sanitizer.sanitizePath(finding.path),
|
|
189
|
+
program: this.sanitizer.sanitizeText(finding.program),
|
|
190
|
+
risks: finding.risks || [],
|
|
191
|
+
llm
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const derivedRisk = this.deriveOverallRisk(findings, agentResult.overallRisk);
|
|
196
|
+
|
|
197
|
+
processedFindings[agentKey] = {
|
|
198
|
+
name: agentResult.agent,
|
|
199
|
+
overallRisk: derivedRisk,
|
|
200
|
+
riskLevel: this.getRiskLevel(derivedRisk),
|
|
201
|
+
findings,
|
|
202
|
+
summary: {
|
|
203
|
+
total: findings.length,
|
|
204
|
+
high: findings.filter(f => f.risk === 'high').length,
|
|
205
|
+
medium: findings.filter(f => f.risk === 'medium').length,
|
|
206
|
+
low: findings.filter(f => f.risk === 'low').length
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return processedFindings;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Prepare visualization data for charts
|
|
216
|
+
*/
|
|
217
|
+
prepareVisualizationData() {
|
|
218
|
+
const summary = this.results.summary || {
|
|
219
|
+
totalFindings: 0,
|
|
220
|
+
highRiskFindings: 0,
|
|
221
|
+
mediumRiskFindings: 0,
|
|
222
|
+
lowRiskFindings: 0,
|
|
223
|
+
agentSummaries: {}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const total = summary.totalFindings || 1; // Avoid division by zero
|
|
227
|
+
const high = summary.highRiskFindings || 0;
|
|
228
|
+
const medium = summary.mediumRiskFindings || 0;
|
|
229
|
+
const low = summary.lowRiskFindings || 0;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
riskDistribution: {
|
|
233
|
+
high,
|
|
234
|
+
medium,
|
|
235
|
+
low,
|
|
236
|
+
highPercentage: Math.round((high / total) * 100),
|
|
237
|
+
mediumPercentage: Math.round((medium / total) * 100),
|
|
238
|
+
lowPercentage: Math.round((low / total) * 100)
|
|
239
|
+
},
|
|
240
|
+
agentFindings: Object.entries(summary.agentSummaries || {})
|
|
241
|
+
.filter(([_, data]) => !data.error)
|
|
242
|
+
.map(([agent, data]) => ({
|
|
243
|
+
agent: this.formatter.formatAgentName(agent),
|
|
244
|
+
findings: data.findings,
|
|
245
|
+
risk: data.risk
|
|
246
|
+
}))
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Prepare compliance data (deduped, normalized labels)
|
|
252
|
+
*/
|
|
253
|
+
prepareComplianceData() {
|
|
254
|
+
const frameworks = {
|
|
255
|
+
'NIST CSF': new Set(),
|
|
256
|
+
'ISO 27001': new Set(),
|
|
257
|
+
'SOC 2': new Set(),
|
|
258
|
+
'PCI DSS': new Set()
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Friendly labels for common finding types (fallback to raw type)
|
|
262
|
+
const typeLabels = {
|
|
263
|
+
launchdaemon: 'Persistence: LaunchDaemon',
|
|
264
|
+
launchagent: 'Persistence: LaunchAgent',
|
|
265
|
+
login_item: 'Persistence: Login Item',
|
|
266
|
+
crontab: 'Persistence: Crontab',
|
|
267
|
+
suspicious_process: 'Process: Suspicious execution',
|
|
268
|
+
high_cpu_unusual_path: 'Process: High CPU (non-system)',
|
|
269
|
+
high_memory_unusual_path: 'Process: High Memory (non-system)',
|
|
270
|
+
long_running_background: 'Process: Long-running background',
|
|
271
|
+
privacy_permission: 'Permissions: Sensitive access',
|
|
272
|
+
listening: 'Network: Listening service',
|
|
273
|
+
outbound: 'Network: External connection',
|
|
274
|
+
blockchain_network_connection: 'Network: Blockchain/DeFi host',
|
|
275
|
+
login_item: 'Persistence: Login Item'
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
for (const [, result] of Object.entries(this.results.agents || {})) {
|
|
279
|
+
const findings = Array.isArray(result?.findings) ? result.findings : [];
|
|
280
|
+
findings.forEach(finding => {
|
|
281
|
+
const label = typeLabels[finding.type] || finding.type;
|
|
282
|
+
if (finding.risk === 'high') {
|
|
283
|
+
frameworks['NIST CSF'].add(label);
|
|
284
|
+
frameworks['ISO 27001'].add(label);
|
|
285
|
+
frameworks['PCI DSS'].add(label);
|
|
286
|
+
}
|
|
287
|
+
if (finding.risk === 'medium') {
|
|
288
|
+
frameworks['SOC 2'].add(label);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Convert sets to sorted arrays for stable output
|
|
294
|
+
return Object.fromEntries(
|
|
295
|
+
Object.entries(frameworks).map(([k, v]) => [k, Array.from(v).sort()])
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Generate actionable recommendations
|
|
301
|
+
*/
|
|
302
|
+
generateRecommendations() {
|
|
303
|
+
const recommendations = [];
|
|
304
|
+
const summary = this.results.summary || {
|
|
305
|
+
highRiskFindings: 0,
|
|
306
|
+
mediumRiskFindings: 0
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if ((summary.highRiskFindings || 0) > 0) {
|
|
310
|
+
recommendations.push({
|
|
311
|
+
priority: 'high',
|
|
312
|
+
title: 'Address High Risk Findings Immediately',
|
|
313
|
+
description: `There are ${summary.highRiskFindings} high-risk findings that require immediate attention to prevent potential security incidents.`,
|
|
314
|
+
actions: [
|
|
315
|
+
'Review and remediate all high-risk findings',
|
|
316
|
+
'Implement additional security controls',
|
|
317
|
+
'Consider isolating affected systems',
|
|
318
|
+
'Monitor for suspicious activity'
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if ((summary.mediumRiskFindings || 0) > 5) {
|
|
324
|
+
recommendations.push({
|
|
325
|
+
priority: 'medium',
|
|
326
|
+
title: 'Improve Security Posture',
|
|
327
|
+
description: 'Multiple medium-risk findings indicate opportunities to improve overall security posture.',
|
|
328
|
+
actions: [
|
|
329
|
+
'Implement regular security scanning',
|
|
330
|
+
'Update security policies and procedures',
|
|
331
|
+
'Provide security awareness training',
|
|
332
|
+
'Consider security hardening measures'
|
|
333
|
+
]
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (this.results.adaptiveAnalysis?.blockchainAnalysisEnabled) {
|
|
338
|
+
recommendations.push({
|
|
339
|
+
priority: 'medium',
|
|
340
|
+
title: 'Blockchain Security Monitoring',
|
|
341
|
+
description: 'Blockchain and cryptocurrency activities were detected. Implement specialized monitoring.',
|
|
342
|
+
actions: [
|
|
343
|
+
'Implement blockchain security monitoring',
|
|
344
|
+
'Review wallet and DeFi application permissions',
|
|
345
|
+
'Monitor for unusual crypto transactions',
|
|
346
|
+
'Consider dedicated crypto security tools'
|
|
347
|
+
]
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return recommendations;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Derive risk from findings to avoid inconsistent badges
|
|
356
|
+
*/
|
|
357
|
+
deriveOverallRisk(findings, reportedRisk = 'unknown') {
|
|
358
|
+
if (!Array.isArray(findings) || findings.length === 0) return 'low';
|
|
359
|
+
if (findings.some(f => f && f.risk === 'high')) return 'high';
|
|
360
|
+
if (findings.some(f => f && f.risk === 'medium')) return 'medium';
|
|
361
|
+
return reportedRisk === 'unknown' ? 'low' : reportedRisk;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get formatted risk level
|
|
366
|
+
*/
|
|
367
|
+
getRiskLevel(risk) {
|
|
368
|
+
const levels = {
|
|
369
|
+
high: { label: 'High', color: '#dc2626', icon: '🔴' },
|
|
370
|
+
medium: { label: 'Medium', color: '#ca8a04', icon: '🟡' },
|
|
371
|
+
low: { label: 'Low', color: '#16a34a', icon: '🟢' },
|
|
372
|
+
unknown: { label: 'Unknown', color: '#6b7280', icon: '⚪' }
|
|
373
|
+
};
|
|
374
|
+
return levels[risk] || levels.unknown;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Generate unique report ID
|
|
379
|
+
*/
|
|
380
|
+
generateReportId() {
|
|
381
|
+
const timestamp = Date.now();
|
|
382
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
383
|
+
return `RPT-${timestamp}-${random}`.toUpperCase();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Ensure report directory structure exists
|
|
388
|
+
*/
|
|
389
|
+
async ensureReportDirectory() {
|
|
390
|
+
const today = new Date();
|
|
391
|
+
const year = today.getFullYear();
|
|
392
|
+
const month = today.toLocaleString('en-US', { month: 'long' });
|
|
393
|
+
|
|
394
|
+
const reportDir = join(this.options.reportsDir, year.toString(), month);
|
|
395
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
396
|
+
|
|
397
|
+
this.reportDir = reportDir;
|
|
398
|
+
return reportDir;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Save report metadata for cataloging
|
|
403
|
+
*/
|
|
404
|
+
async saveReportMetadata(savedFiles, reportData) {
|
|
405
|
+
const metadata = {
|
|
406
|
+
reportId: reportData.metadata.reportId,
|
|
407
|
+
timestamp: reportData.metadata.generatedAt,
|
|
408
|
+
system: reportData.system,
|
|
409
|
+
summary: reportData.summary,
|
|
410
|
+
files: savedFiles,
|
|
411
|
+
riskScore: this.calculateRiskScore(reportData.summary)
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const metadataPath = join(this.reportDir, `metadata-${this.timestamp}.json`);
|
|
415
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Calculate risk score (0-100)
|
|
420
|
+
*/
|
|
421
|
+
calculateRiskScore(summary) {
|
|
422
|
+
const weights = { high: 10, medium: 5, low: 1 };
|
|
423
|
+
const score = (summary.highRiskFindings * weights.high) +
|
|
424
|
+
(summary.mediumRiskFindings * weights.medium) +
|
|
425
|
+
(summary.lowRiskFindings * weights.low);
|
|
426
|
+
|
|
427
|
+
return Math.min(100, score);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Cleanup old reports based on retention policy
|
|
432
|
+
*/
|
|
433
|
+
async cleanupOldReports() {
|
|
434
|
+
// Implementation for report cleanup based on retentionDays
|
|
435
|
+
// This would scan old report directories and remove files older than retentionDays
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get list of available reports
|
|
440
|
+
*/
|
|
441
|
+
static async listReports(reportsDir = './reports') {
|
|
442
|
+
// Implementation for cataloging existing reports
|
|
443
|
+
// Would scan report directories and return metadata for all reports
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import handlebars from 'handlebars';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Enhanced Markdown Generator with templates
|
|
8
|
+
*/
|
|
9
|
+
export class MarkdownGenerator {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = {
|
|
12
|
+
templateDir: './src/report/templates',
|
|
13
|
+
...options
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate markdown report
|
|
19
|
+
*/
|
|
20
|
+
async generate(reportData) {
|
|
21
|
+
console.log('📄 Generating Markdown report...');
|
|
22
|
+
|
|
23
|
+
// Compile Handlebars template
|
|
24
|
+
const markdown = await this.compileTemplate(reportData);
|
|
25
|
+
|
|
26
|
+
// Save to file
|
|
27
|
+
const filename = `Security-Report-${reportData.metadata.reportId}.md`;
|
|
28
|
+
const filepath = join(this.options.reportsDir || './reports', filename);
|
|
29
|
+
|
|
30
|
+
await fs.writeFile(filepath, markdown, 'utf-8');
|
|
31
|
+
|
|
32
|
+
console.log(`✅ Markdown report saved: ${filepath}`);
|
|
33
|
+
return filepath;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compile Handlebars template with data
|
|
38
|
+
*/
|
|
39
|
+
async compileTemplate(reportData) {
|
|
40
|
+
const templatePath = join(this.options.templateDir, 'report.hbs');
|
|
41
|
+
const templateSource = readFileSync(templatePath, 'utf-8');
|
|
42
|
+
|
|
43
|
+
// Register custom Handlebars helpers
|
|
44
|
+
this.registerHelpers();
|
|
45
|
+
|
|
46
|
+
const template = handlebars.compile(templateSource);
|
|
47
|
+
return template({ ...reportData, format: 'markdown' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register custom Handlebars helpers
|
|
52
|
+
*/
|
|
53
|
+
registerHelpers() {
|
|
54
|
+
// Risk badge helper for markdown
|
|
55
|
+
handlebars.registerHelper('markdownRiskBadge', (risk) => {
|
|
56
|
+
const badges = {
|
|
57
|
+
high: '🔴 **HIGH**',
|
|
58
|
+
medium: '🟡 **MEDIUM**',
|
|
59
|
+
low: '🟢 **LOW**',
|
|
60
|
+
unknown: '⚪ **UNKNOWN**'
|
|
61
|
+
};
|
|
62
|
+
return badges[risk] || badges.unknown;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Format code blocks
|
|
66
|
+
handlebars.registerHelper('codeBlock', (content, language = '') => {
|
|
67
|
+
return `\`\`\`${language}\n${content || ''}\n\`\`\``;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Format list items
|
|
71
|
+
handlebars.registerHelper('listItem', (text, level = 1) => {
|
|
72
|
+
const indent = ' '.repeat(level - 1);
|
|
73
|
+
return `${indent}- ${text}`;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Table header helper
|
|
77
|
+
handlebars.registerHelper('tableHeader', (columns) => {
|
|
78
|
+
const header = `| ${columns.join(' | ')} |`;
|
|
79
|
+
const separator = `| ${columns.map(() => '---').join(' | ')} |`;
|
|
80
|
+
return `${header}\n${separator}`;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Table row helper
|
|
84
|
+
handlebars.registerHelper('tableRow', (cells) => {
|
|
85
|
+
return `| ${cells.join(' | ')} |`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Link helper
|
|
89
|
+
handlebars.registerHelper('link', (text, url) => {
|
|
90
|
+
return `[${text}](${url})`;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Bold helper
|
|
94
|
+
handlebars.registerHelper('bold', (text) => {
|
|
95
|
+
return `**${text}**`;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Italic helper
|
|
99
|
+
handlebars.registerHelper('italic', (text) => {
|
|
100
|
+
return `*${text}*`;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Format agent name
|
|
104
|
+
handlebars.registerHelper('formatAgentName', (agent) => {
|
|
105
|
+
return agent.charAt(0).toUpperCase() + agent.slice(1).replace(/([A-Z])/g, ' $1');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Uppercase helper
|
|
109
|
+
handlebars.registerHelper('uppercase', (str) => {
|
|
110
|
+
if (str === undefined || str === null) return 'UNKNOWN';
|
|
111
|
+
return str.toString().toUpperCase();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Date formatter
|
|
115
|
+
handlebars.registerHelper('formatDate', (date) => {
|
|
116
|
+
return new Date(date).toLocaleDateString();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Time formatter
|
|
120
|
+
handlebars.registerHelper('formatTime', (date) => {
|
|
121
|
+
return new Date(date).toLocaleTimeString();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Conditional helper
|
|
125
|
+
handlebars.registerHelper('ifEquals', function(arg1, arg2, options) {
|
|
126
|
+
return (arg1 == arg2) ? options.fn(this) : options.inverse(this);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Greater than helper
|
|
130
|
+
handlebars.registerHelper('ifGt', function(arg1, arg2, options) {
|
|
131
|
+
return (arg1 > arg2) ? options.fn(this) : options.inverse(this);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Array length helper
|
|
135
|
+
handlebars.registerHelper('length', (array) => {
|
|
136
|
+
return array ? array.length : 0;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// True if any of the provided values are "present"
|
|
140
|
+
handlebars.registerHelper('hasAny', (...args) => {
|
|
141
|
+
// Last arg is Handlebars options hash
|
|
142
|
+
args.pop();
|
|
143
|
+
|
|
144
|
+
return args.some((value) => {
|
|
145
|
+
if (value === undefined || value === null) return false;
|
|
146
|
+
if (typeof value === 'string') return value.trim().length > 0;
|
|
147
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
148
|
+
return true;
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// JSON stringify helper for debugging
|
|
153
|
+
handlebars.registerHelper('json', (obj) => {
|
|
154
|
+
return JSON.stringify(obj, null, 2);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Logical OR helper for template conditions
|
|
158
|
+
handlebars.registerHelper('or', (...args) => {
|
|
159
|
+
args.pop(); // options hash
|
|
160
|
+
return args.some(Boolean);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Calculate risk score helper
|
|
164
|
+
handlebars.registerHelper('calculateRiskScore', (summary) => {
|
|
165
|
+
const weights = { high: 10, medium: 5, low: 1 };
|
|
166
|
+
const score = (summary.highRiskFindings * weights.high) +
|
|
167
|
+
(summary.mediumRiskFindings * weights.medium) +
|
|
168
|
+
(summary.lowRiskFindings * weights.low);
|
|
169
|
+
|
|
170
|
+
return Math.min(100, score);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|