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