@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,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
+ }