@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,426 @@
1
+ /**
2
+ * Report Formatter - Data formatting utilities
3
+ */
4
+ export class ReportFormatter {
5
+ constructor() {
6
+ this.agentNameMap = this.initializeAgentNames();
7
+ this.riskLevelMap = this.initializeRiskLevels();
8
+ this.complianceMap = this.initializeComplianceMapping();
9
+ }
10
+
11
+ /**
12
+ * Initialize agent name mappings
13
+ */
14
+ initializeAgentNames() {
15
+ return {
16
+ resource: 'Resource Monitor',
17
+ system: 'System Security',
18
+ persistence: 'Persistence Analysis',
19
+ process: 'Process Analysis',
20
+ network: 'Network Analysis',
21
+ permission: 'Permission Analysis',
22
+ blockchain: 'Blockchain Security',
23
+ defi: 'DeFi Security'
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Initialize risk level mappings
29
+ */
30
+ initializeRiskLevels() {
31
+ return {
32
+ high: {
33
+ label: 'High',
34
+ color: '#dc2626',
35
+ icon: '🔴',
36
+ priority: 1,
37
+ description: 'Immediate attention required'
38
+ },
39
+ medium: {
40
+ label: 'Medium',
41
+ color: '#ca8a04',
42
+ icon: '🟡',
43
+ priority: 2,
44
+ description: 'Should be addressed soon'
45
+ },
46
+ low: {
47
+ label: 'Low',
48
+ color: '#16a34a',
49
+ icon: '🟢',
50
+ priority: 3,
51
+ description: 'Monitor and consider for future improvements'
52
+ },
53
+ unknown: {
54
+ label: 'Unknown',
55
+ color: '#6b7280',
56
+ icon: '⚪',
57
+ priority: 4,
58
+ description: 'Unable to determine risk level'
59
+ }
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Initialize compliance framework mappings
65
+ */
66
+ initializeComplianceMapping() {
67
+ return {
68
+ 'NIST CSF': {
69
+ 'PR.AC': 'Access Control',
70
+ 'PR.PT': 'Protective Technology',
71
+ 'DE.CM': 'Continuous Monitoring',
72
+ 'DE.AE': 'Anomalous Activity'
73
+ },
74
+ 'ISO 27001': {
75
+ 'A.9': 'Access Control',
76
+ 'A.12': 'Operations Security',
77
+ 'A.14': 'System Acquisition',
78
+ 'A.16': 'Incident Management'
79
+ },
80
+ 'SOC 2': {
81
+ 'CC6.1': 'Common Criteria',
82
+ 'CC6.2': 'Security Operations',
83
+ 'CC6.7': 'System Boundaries'
84
+ },
85
+ 'PCI DSS': {
86
+ 'Req1': 'Firewall Configuration',
87
+ 'Req2': 'Default Passwords',
88
+ 'Req10': 'Logging and Monitoring'
89
+ }
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Format agent name for display
95
+ */
96
+ formatAgentName(agentKey) {
97
+ return this.agentNameMap[agentKey] ||
98
+ agentKey.charAt(0).toUpperCase() + agentKey.slice(1);
99
+ }
100
+
101
+ /**
102
+ * Format risk level for display
103
+ */
104
+ formatRiskLevel(risk) {
105
+ return this.riskLevelMap[risk] || this.riskLevelMap.unknown;
106
+ }
107
+
108
+ /**
109
+ * Format finding type for display
110
+ */
111
+ formatFindingType(type) {
112
+ return type
113
+ .split(/[\s_-]+/)
114
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
115
+ .join(' ');
116
+ }
117
+
118
+ /**
119
+ * Format file size in human readable format
120
+ */
121
+ formatFileSize(bytes) {
122
+ if (bytes === 0) return '0 B';
123
+
124
+ const k = 1024;
125
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
126
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
127
+
128
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
129
+ }
130
+
131
+ /**
132
+ * Format duration in human readable format
133
+ */
134
+ formatDuration(seconds) {
135
+ if (seconds < 60) {
136
+ return `${seconds} second${seconds !== 1 ? 's' : ''}`;
137
+ }
138
+
139
+ const minutes = Math.floor(seconds / 60);
140
+ const remainingSeconds = seconds % 60;
141
+
142
+ if (minutes < 60) {
143
+ return `${minutes} minute${minutes !== 1 ? 's' : ''}${remainingSeconds > 0 ? ` ${remainingSeconds}s` : ''}`;
144
+ }
145
+
146
+ const hours = Math.floor(minutes / 60);
147
+ const remainingMinutes = minutes % 60;
148
+
149
+ return `${hours} hour${hours !== 1 ? 's' : ''}${remainingMinutes > 0 ? ` ${remainingMinutes}m` : ''}`;
150
+ }
151
+
152
+ /**
153
+ * Format CPU percentage
154
+ */
155
+ formatCPUPercentage(cpu) {
156
+ return `${parseFloat(cpu).toFixed(1)}%`;
157
+ }
158
+
159
+ /**
160
+ * Format memory in MB/GB
161
+ */
162
+ formatMemory(bytes) {
163
+ const mb = bytes / (1024 * 1024);
164
+ if (mb < 1024) {
165
+ return `${mb.toFixed(1)} MB`;
166
+ }
167
+ const gb = mb / 1024;
168
+ return `${gb.toFixed(2)} GB`;
169
+ }
170
+
171
+ /**
172
+ * Format timestamp
173
+ */
174
+ formatTimestamp(timestamp, format = 'datetime') {
175
+ const date = new Date(timestamp);
176
+
177
+ switch (format) {
178
+ case 'date':
179
+ return date.toLocaleDateString();
180
+ case 'time':
181
+ return date.toLocaleTimeString();
182
+ case 'datetime':
183
+ return date.toLocaleString();
184
+ case 'iso':
185
+ return date.toISOString();
186
+ case 'relative':
187
+ return this.formatRelativeTime(date);
188
+ default:
189
+ return date.toLocaleString();
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Format relative time (e.g., "2 hours ago")
195
+ */
196
+ formatRelativeTime(date) {
197
+ const now = new Date();
198
+ const diffMs = now - date;
199
+ const diffSecs = Math.floor(diffMs / 1000);
200
+ const diffMins = Math.floor(diffSecs / 60);
201
+ const diffHours = Math.floor(diffMins / 60);
202
+ const diffDays = Math.floor(diffHours / 24);
203
+
204
+ if (diffDays > 0) {
205
+ return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
206
+ }
207
+ if (diffHours > 0) {
208
+ return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
209
+ }
210
+ if (diffMins > 0) {
211
+ return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`;
212
+ }
213
+ return `${diffSecs} second${diffSecs !== 1 ? 's' : ''} ago`;
214
+ }
215
+
216
+ /**
217
+ * Format process tree for display
218
+ */
219
+ formatProcessTree(processes) {
220
+ const tree = {};
221
+
222
+ // Build parent-child relationships
223
+ processes.forEach(proc => {
224
+ tree[proc.pid] = { ...proc, children: [] };
225
+ });
226
+
227
+ // Organize into tree structure
228
+ const roots = [];
229
+ processes.forEach(proc => {
230
+ if (proc.ppid && tree[proc.ppid]) {
231
+ tree[proc.ppid].children.push(tree[proc.pid]);
232
+ } else {
233
+ roots.push(tree[proc.pid]);
234
+ }
235
+ });
236
+
237
+ return this.formatTreeNode(roots, 0);
238
+ }
239
+
240
+ /**
241
+ * Format tree node recursively
242
+ */
243
+ formatTreeNode(nodes, level) {
244
+ let output = '';
245
+
246
+ nodes.sort((a, b) => a.pid - b.pid).forEach(node => {
247
+ const indent = ' '.repeat(level);
248
+ const prefix = level > 0 ? '├─ ' : '';
249
+ output += `${indent}${prefix}[${node.pid}] ${node.name || 'Unknown'}\n`;
250
+
251
+ if (node.children && node.children.length > 0) {
252
+ output += this.formatTreeNode(node.children, level + 1);
253
+ }
254
+ });
255
+
256
+ return output;
257
+ }
258
+
259
+ /**
260
+ * Format network connections summary
261
+ */
262
+ formatNetworkSummary(connections) {
263
+ const summary = {
264
+ total: connections.length,
265
+ listening: connections.filter(c => c.state === 'LISTEN').length,
266
+ established: connections.filter(c => c.state === 'ESTABLISHED').length,
267
+ external: connections.filter(c => c.remoteAddress && !c.remoteAddress.startsWith('127.')).length,
268
+ riskyPorts: connections.filter(c => this.isRiskyPort(c.localPort)).length
269
+ };
270
+
271
+ return `Total: ${summary.total}, Listening: ${summary.listening}, Established: ${summary.established}, External: ${summary.external}, Risky Ports: ${summary.riskyPorts}`;
272
+ }
273
+
274
+ /**
275
+ * Check if port is considered risky
276
+ */
277
+ isRiskyPort(port) {
278
+ const riskyPorts = [
279
+ 22, // SSH
280
+ 23, // Telnet
281
+ 135, // RPC
282
+ 139, // NetBIOS
283
+ 445, // SMB
284
+ 1433, // MSSQL
285
+ 3389, // RDP
286
+ 5432, // PostgreSQL
287
+ 6379, // Redis
288
+ 27017, // MongoDB
289
+ 8080, // HTTP Alt
290
+ 8443 // HTTPS Alt
291
+ ];
292
+
293
+ return riskyPorts.includes(parseInt(port));
294
+ }
295
+
296
+ /**
297
+ * Format compliance mapping
298
+ */
299
+ formatComplianceMapping(finding, frameworks = ['NIST CSF', 'ISO 27001']) {
300
+ const mappings = {};
301
+
302
+ frameworks.forEach(framework => {
303
+ const frameworkMapping = this.complianceMap[framework];
304
+ if (frameworkMapping) {
305
+ const relevantControls = Object.keys(frameworkMapping).filter(control =>
306
+ this.isRelevantToControl(finding, control)
307
+ );
308
+ mappings[framework] = relevantControls.map(control => ({
309
+ id: control,
310
+ name: frameworkMapping[control]
311
+ }));
312
+ }
313
+ });
314
+
315
+ return mappings;
316
+ }
317
+
318
+ /**
319
+ * Check if finding is relevant to compliance control
320
+ */
321
+ isRelevantToControl(finding, controlId) {
322
+ // Simplified mapping logic - can be enhanced with more sophisticated rules
323
+ const findingText = (finding.type + ' ' + finding.description).toLowerCase();
324
+
325
+ switch (controlId) {
326
+ case 'PR.AC':
327
+ case 'A.9':
328
+ return findingText.includes('access') || findingText.includes('permission');
329
+ case 'PR.PT':
330
+ case 'A.12':
331
+ return findingText.includes('security') || findingText.includes('protection');
332
+ case 'DE.CM':
333
+ return findingText.includes('monitor') || findingText.includes('log');
334
+ case 'DE.AE':
335
+ return findingText.includes('anomaly') || findingText.includes('suspicious');
336
+ default:
337
+ return false;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Calculate risk score (0-100)
343
+ */
344
+ calculateRiskScore(summary) {
345
+ const weights = { high: 10, medium: 5, low: 1 };
346
+ const score = (summary.highRiskFindings * weights.high) +
347
+ (summary.mediumRiskFindings * weights.medium) +
348
+ (summary.lowRiskFindings * weights.low);
349
+
350
+ return Math.min(100, score);
351
+ }
352
+
353
+ /**
354
+ * Format risk score with color
355
+ */
356
+ formatRiskScore(score) {
357
+ let color, level;
358
+
359
+ if (score >= 70) {
360
+ color = '#dc2626';
361
+ level = 'Critical';
362
+ } else if (score >= 40) {
363
+ color = '#ca8a04';
364
+ level = 'High';
365
+ } else if (score >= 20) {
366
+ color = '#f59e0b';
367
+ level = 'Medium';
368
+ } else {
369
+ color = '#16a34a';
370
+ level = 'Low';
371
+ }
372
+
373
+ return { score, color, level };
374
+ }
375
+
376
+ /**
377
+ * Format chart data for visualization
378
+ */
379
+ formatChartData(data, type = 'pie') {
380
+ switch (type) {
381
+ case 'pie':
382
+ return this.formatPieChartData(data);
383
+ case 'bar':
384
+ return this.formatBarChartData(data);
385
+ case 'line':
386
+ return this.formatLineChartData(data);
387
+ default:
388
+ return data;
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Format data for pie chart
394
+ */
395
+ formatPieChartData(data) {
396
+ const total = Object.values(data).reduce((sum, val) => sum + val, 0);
397
+ return Object.entries(data).map(([key, value]) => ({
398
+ label: this.formatRiskLevel(key).label,
399
+ value,
400
+ percentage: total > 0 ? Math.round((value / total) * 100) : 0,
401
+ color: this.formatRiskLevel(key).color
402
+ }));
403
+ }
404
+
405
+ /**
406
+ * Format data for bar chart
407
+ */
408
+ formatBarChartData(data) {
409
+ return Object.entries(data).map(([key, value]) => ({
410
+ category: key,
411
+ value,
412
+ color: this.formatRiskLevel(key).color
413
+ }));
414
+ }
415
+
416
+ /**
417
+ * Format data for line chart
418
+ */
419
+ formatLineChartData(data) {
420
+ return data.map((point, index) => ({
421
+ x: index,
422
+ y: point.value,
423
+ label: point.label
424
+ }));
425
+ }
426
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Report Sanitizer - Enhanced privacy protection
3
+ */
4
+ export class ReportSanitizer {
5
+ constructor(options = {}) {
6
+ this.options = {
7
+ redactUserPaths: true,
8
+ redactIPs: false,
9
+ redactUsernames: true,
10
+ preserveDomains: true,
11
+ ...options
12
+ };
13
+
14
+ // Initialize sensitive patterns
15
+ this.sensitivePatterns = this.initializePatterns();
16
+ }
17
+
18
+ /**
19
+ * Initialize all sensitive data patterns
20
+ */
21
+ initializePatterns() {
22
+ return {
23
+ // Cryptographic patterns
24
+ privateKey: {
25
+ pattern: /[a-fA-F0-9]{64}/g,
26
+ replacement: '***REDACTED_PRIVATE_KEY***',
27
+ name: 'Private Key'
28
+ },
29
+
30
+ // Ethereum addresses
31
+ ethAddress: {
32
+ pattern: /0x[a-fA-F0-9]{40}/g,
33
+ replacement: '0x***REDACTED_ETH_ADDRESS***',
34
+ name: 'Ethereum Address'
35
+ },
36
+
37
+ // Bitcoin addresses
38
+ btcAddress: {
39
+ pattern: /[13][a-km-zA-HJ-NP-Z1-9]{25,34}/g,
40
+ replacement: '***REDACTED_BTC_ADDRESS***',
41
+ name: 'Bitcoin Address'
42
+ },
43
+
44
+ // API Keys and tokens
45
+ apiKey: {
46
+ pattern: /[a-zA-Z0-9]{32,}={0,2}/g,
47
+ replacement: '***REDACTED_API_KEY***',
48
+ name: 'API Key'
49
+ },
50
+
51
+ // Password/secret patterns
52
+ password: {
53
+ pattern: /(password|secret|key|token)[\s=:]+[a-zA-Z0-9+/]{8,}/gi,
54
+ replacement: '$1***REDACTED***',
55
+ name: 'Password/Secret'
56
+ },
57
+
58
+ // Mnemonic phrases
59
+ mnemonic: {
60
+ pattern: /\b(word|agree|letter|again|animal|already|between|certain|close|common|could|describe|engine|every|example|few|first|follow|group|have|important|inside|just|large|lead|letter|local|matter|might|never|number|open|order|place|point|question|right|second|see|small|sound|still|such|tell|thing|think|three|under|until|voice|water|where|which|world|write|year|yes|your|zone)\b.{0,200}/gi,
61
+ replacement: '***REDACTED_MNEMONIC***',
62
+ name: 'Seed Phrase'
63
+ },
64
+
65
+ // Long hex strings
66
+ hexString: {
67
+ pattern: /[a-fA-F0-9]{16,31}/g,
68
+ replacement: '***REDACTED_HEX***',
69
+ name: 'Hex String'
70
+ },
71
+
72
+ // Email addresses (if usernames should be redacted)
73
+ email: {
74
+ pattern: this.options.redactUsernames ? /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g : null,
75
+ replacement: '***REDACTED_EMAIL***',
76
+ name: 'Email Address'
77
+ }
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Sanitize text content
83
+ */
84
+ sanitizeText(text) {
85
+ if (!text || typeof text !== 'string') {
86
+ return text;
87
+ }
88
+
89
+ let sanitized = text;
90
+
91
+ // Apply all patterns
92
+ for (const [key, config] of Object.entries(this.sensitivePatterns)) {
93
+ if (!config.pattern) continue;
94
+
95
+ const matches = sanitized.match(config.pattern);
96
+ if (matches) {
97
+ sanitized = sanitized.replace(config.pattern, config.replacement);
98
+ }
99
+ }
100
+
101
+ return sanitized;
102
+ }
103
+
104
+ /**
105
+ * Sanitize file paths
106
+ */
107
+ sanitizePath(path) {
108
+ if (!path || typeof path !== 'string') {
109
+ return path;
110
+ }
111
+
112
+ let sanitized = path;
113
+
114
+ // Redact user directories
115
+ if (this.options.redactUserPaths) {
116
+ sanitized = sanitized
117
+ .replace(/\/Users\/[^\/]+/g, '/Users/***REDACTED***')
118
+ .replace(/\/home\/[^\/]+/g, '/home/***REDACTED***')
119
+ .replace(/C:\\Users\\[^\\]+/g, 'C:\\Users\\***REDACTED***')
120
+ .replace(/\/private\/var\/folders\/[^\/]+/g, '/private/var/folders/***REDACTED***');
121
+ }
122
+
123
+ // Redact usernames from paths
124
+ // NOTE: We already redact the username segment for common user-home roots above
125
+ // (e.g., /Users/<name>, /home/<name>, C:\Users\<name>). Avoid broad path redaction here
126
+ // because it can make the report unreadable by replacing most path components.
127
+
128
+ // Sanitize any remaining sensitive data in path components
129
+ const pathComponents = sanitized.split(/[\/\\]/);
130
+ const sanitizedComponents = pathComponents.map(component =>
131
+ this.sanitizePathComponent(component)
132
+ );
133
+
134
+ return sanitizedComponents.join('/');
135
+ }
136
+
137
+ /**
138
+ * Sanitize individual path components
139
+ */
140
+ sanitizePathComponent(component) {
141
+ if (!component) return component;
142
+
143
+ // Apply text sanitization but preserve file extensions
144
+ const lastDot = component.lastIndexOf('.');
145
+ if (lastDot > 0) {
146
+ const name = component.substring(0, lastDot);
147
+ const ext = component.substring(lastDot);
148
+ return this.sanitizeText(name) + ext;
149
+ }
150
+
151
+ return this.sanitizeText(component);
152
+ }
153
+
154
+ /**
155
+ * Sanitize URLs
156
+ */
157
+ sanitizeURL(url) {
158
+ if (!url || typeof url !== 'string') {
159
+ return url;
160
+ }
161
+
162
+ let sanitized = url;
163
+
164
+ // Parse URL components
165
+ try {
166
+ const urlObj = new URL(url);
167
+
168
+ // Sanitize hostname (preserve domains if enabled)
169
+ if (!this.options.preserveDomains) {
170
+ urlObj.hostname = urlObj.hostname.replace(/[a-zA-Z0-9-]+/, '***REDACTED_DOMAIN***');
171
+ }
172
+
173
+ // Sanitize path
174
+ urlObj.pathname = this.sanitizePath(urlObj.pathname);
175
+
176
+ // Remove query parameters and fragments (often contain sensitive data)
177
+ urlObj.search = '';
178
+ urlObj.hash = '';
179
+
180
+ sanitized = urlObj.toString();
181
+ } catch (e) {
182
+ // If URL parsing fails, apply text sanitization
183
+ sanitized = this.sanitizeText(url);
184
+ }
185
+
186
+ return sanitized;
187
+ }
188
+
189
+ /**
190
+ * Sanitize IP addresses
191
+ */
192
+ sanitizeIP(ip) {
193
+ if (!this.options.redactIPs) return ip;
194
+
195
+ if (!ip || typeof ip !== 'string') {
196
+ return ip;
197
+ }
198
+
199
+ // IPv4 pattern
200
+ const ipv4Pattern = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
201
+ return ip.replace(ipv4Pattern, '***.***.***.***');
202
+ }
203
+
204
+ /**
205
+ * Perform comprehensive sanitization check
206
+ */
207
+ performSecurityCheck(data) {
208
+ const sensitivePatterns = [];
209
+ let hasSensitiveData = false;
210
+
211
+ // Convert data to string for pattern matching
212
+ const dataString = JSON.stringify(data);
213
+
214
+ // Check each sensitive pattern
215
+ for (const [key, config] of Object.entries(this.sensitivePatterns)) {
216
+ if (!config.pattern) continue;
217
+
218
+ const matches = dataString.match(config.pattern);
219
+ if (matches && matches.length > 0) {
220
+ hasSensitiveData = true;
221
+ sensitivePatterns.push({
222
+ name: config.name,
223
+ count: matches.length,
224
+ pattern: key
225
+ });
226
+ }
227
+ }
228
+
229
+ return {
230
+ hasSensitiveData,
231
+ sensitivePatterns,
232
+ totalSensitivePatterns: sensitivePatterns.length
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Add custom sensitive pattern
238
+ */
239
+ addCustomPattern(name, pattern, replacement, description) {
240
+ this.sensitivePatterns[name] = {
241
+ pattern,
242
+ replacement: replacement || '***REDACTED***',
243
+ name: description || name
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Remove sensitive pattern
249
+ */
250
+ removePattern(name) {
251
+ delete this.sensitivePatterns[name];
252
+ }
253
+
254
+ /**
255
+ * Get list of active patterns
256
+ */
257
+ getActivePatterns() {
258
+ return Object.keys(this.sensitivePatterns).filter(key =>
259
+ this.sensitivePatterns[key].pattern !== null
260
+ );
261
+ }
262
+
263
+ /**
264
+ * Configure sanitization options
265
+ */
266
+ configure(options) {
267
+ this.options = { ...this.options, ...options };
268
+
269
+ // Update patterns based on new configuration
270
+ if (options.redactUsernames !== undefined) {
271
+ this.sensitivePatterns.email.pattern = options.redactUsernames ?
272
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g : null;
273
+ }
274
+ }
275
+ }