ferret-scan 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/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/bin/ferret.js +822 -0
- package/dist/__tests__/basic.test.d.ts +6 -0
- package/dist/__tests__/basic.test.js +80 -0
- package/dist/analyzers/AstAnalyzer.d.ts +30 -0
- package/dist/analyzers/AstAnalyzer.js +332 -0
- package/dist/analyzers/CorrelationAnalyzer.d.ts +21 -0
- package/dist/analyzers/CorrelationAnalyzer.js +288 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +22 -0
- package/dist/intelligence/IndicatorMatcher.d.ts +50 -0
- package/dist/intelligence/IndicatorMatcher.js +285 -0
- package/dist/intelligence/ThreatFeed.d.ts +99 -0
- package/dist/intelligence/ThreatFeed.js +296 -0
- package/dist/remediation/Fixer.d.ts +71 -0
- package/dist/remediation/Fixer.js +391 -0
- package/dist/remediation/Quarantine.d.ts +102 -0
- package/dist/remediation/Quarantine.js +329 -0
- package/dist/reporters/ConsoleReporter.d.ts +13 -0
- package/dist/reporters/ConsoleReporter.js +185 -0
- package/dist/reporters/HtmlReporter.d.ts +25 -0
- package/dist/reporters/HtmlReporter.js +604 -0
- package/dist/reporters/SarifReporter.d.ts +86 -0
- package/dist/reporters/SarifReporter.js +117 -0
- package/dist/rules/ai-specific.d.ts +8 -0
- package/dist/rules/ai-specific.js +221 -0
- package/dist/rules/backdoors.d.ts +8 -0
- package/dist/rules/backdoors.js +134 -0
- package/dist/rules/correlationRules.d.ts +8 -0
- package/dist/rules/correlationRules.js +227 -0
- package/dist/rules/credentials.d.ts +8 -0
- package/dist/rules/credentials.js +194 -0
- package/dist/rules/exfiltration.d.ts +8 -0
- package/dist/rules/exfiltration.js +139 -0
- package/dist/rules/index.d.ts +51 -0
- package/dist/rules/index.js +97 -0
- package/dist/rules/injection.d.ts +8 -0
- package/dist/rules/injection.js +136 -0
- package/dist/rules/obfuscation.d.ts +8 -0
- package/dist/rules/obfuscation.js +159 -0
- package/dist/rules/permissions.d.ts +8 -0
- package/dist/rules/permissions.js +129 -0
- package/dist/rules/persistence.d.ts +8 -0
- package/dist/rules/persistence.js +117 -0
- package/dist/rules/semanticRules.d.ts +10 -0
- package/dist/rules/semanticRules.js +212 -0
- package/dist/rules/supply-chain.d.ts +8 -0
- package/dist/rules/supply-chain.js +148 -0
- package/dist/scanner/FileDiscovery.d.ts +24 -0
- package/dist/scanner/FileDiscovery.js +282 -0
- package/dist/scanner/PatternMatcher.d.ts +25 -0
- package/dist/scanner/PatternMatcher.js +206 -0
- package/dist/scanner/Scanner.d.ts +14 -0
- package/dist/scanner/Scanner.js +266 -0
- package/dist/scanner/WatchMode.d.ts +29 -0
- package/dist/scanner/WatchMode.js +195 -0
- package/dist/types.d.ts +332 -0
- package/dist/types.js +53 -0
- package/dist/utils/baseline.d.ts +80 -0
- package/dist/utils/baseline.js +276 -0
- package/dist/utils/config.d.ts +21 -0
- package/dist/utils/config.js +247 -0
- package/dist/utils/ignore.d.ts +18 -0
- package/dist/utils/ignore.js +82 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +75 -0
- package/package.json +119 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Reporter - Beautiful HTML reports with interactive filtering
|
|
3
|
+
* Generates standalone HTML files with embedded CSS and JavaScript
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Escape HTML special characters
|
|
7
|
+
*/
|
|
8
|
+
function escapeHtml(text) {
|
|
9
|
+
const div = { innerHTML: '', textContent: text };
|
|
10
|
+
return div.innerHTML || text
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Format timestamp for display
|
|
19
|
+
*/
|
|
20
|
+
function formatTimestamp(date) {
|
|
21
|
+
return date.toLocaleString('en-US', {
|
|
22
|
+
year: 'numeric',
|
|
23
|
+
month: 'short',
|
|
24
|
+
day: 'numeric',
|
|
25
|
+
hour: '2-digit',
|
|
26
|
+
minute: '2-digit',
|
|
27
|
+
second: '2-digit',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get severity color
|
|
32
|
+
*/
|
|
33
|
+
function getSeverityColor(severity) {
|
|
34
|
+
switch (severity) {
|
|
35
|
+
case 'CRITICAL': return '#dc2626'; // red-600
|
|
36
|
+
case 'HIGH': return '#ea580c'; // orange-600
|
|
37
|
+
case 'MEDIUM': return '#ca8a04'; // yellow-600
|
|
38
|
+
case 'LOW': return '#16a34a'; // green-600
|
|
39
|
+
case 'INFO': return '#2563eb'; // blue-600
|
|
40
|
+
default: return '#6b7280'; // gray-500
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get severity icon
|
|
45
|
+
*/
|
|
46
|
+
function getSeverityIcon(severity) {
|
|
47
|
+
switch (severity) {
|
|
48
|
+
case 'CRITICAL': return '🚨';
|
|
49
|
+
case 'HIGH': return '⚠️';
|
|
50
|
+
case 'MEDIUM': return '🟡';
|
|
51
|
+
case 'LOW': return '🟢';
|
|
52
|
+
case 'INFO': return 'ℹ️';
|
|
53
|
+
default: return '❓';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Generate CSS styles
|
|
58
|
+
*/
|
|
59
|
+
function generateCSS(darkMode = false) {
|
|
60
|
+
const theme = darkMode ? {
|
|
61
|
+
bg: '#0f172a',
|
|
62
|
+
bgSecondary: '#1e293b',
|
|
63
|
+
bgTertiary: '#334155',
|
|
64
|
+
text: '#f8fafc',
|
|
65
|
+
textSecondary: '#cbd5e1',
|
|
66
|
+
border: '#475569',
|
|
67
|
+
accent: '#3b82f6',
|
|
68
|
+
} : {
|
|
69
|
+
bg: '#ffffff',
|
|
70
|
+
bgSecondary: '#f8fafc',
|
|
71
|
+
bgTertiary: '#e2e8f0',
|
|
72
|
+
text: '#1e293b',
|
|
73
|
+
textSecondary: '#64748b',
|
|
74
|
+
border: '#e2e8f0',
|
|
75
|
+
accent: '#3b82f6',
|
|
76
|
+
};
|
|
77
|
+
return `
|
|
78
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
79
|
+
|
|
80
|
+
body {
|
|
81
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
82
|
+
background: ${theme.bg};
|
|
83
|
+
color: ${theme.text};
|
|
84
|
+
line-height: 1.6;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.header {
|
|
88
|
+
background: ${theme.bgSecondary};
|
|
89
|
+
padding: 2rem 0;
|
|
90
|
+
border-bottom: 1px solid ${theme.border};
|
|
91
|
+
text-align: center;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.title {
|
|
95
|
+
font-size: 2.5rem;
|
|
96
|
+
font-weight: 700;
|
|
97
|
+
margin-bottom: 0.5rem;
|
|
98
|
+
background: linear-gradient(45deg, #3b82f6, #8b5cf6);
|
|
99
|
+
-webkit-background-clip: text;
|
|
100
|
+
-webkit-text-fill-color: transparent;
|
|
101
|
+
background-clip: text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.subtitle {
|
|
105
|
+
color: ${theme.textSecondary};
|
|
106
|
+
font-size: 1.1rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.container {
|
|
110
|
+
max-width: 1200px;
|
|
111
|
+
margin: 0 auto;
|
|
112
|
+
padding: 0 1rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.summary {
|
|
116
|
+
display: grid;
|
|
117
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
118
|
+
gap: 1.5rem;
|
|
119
|
+
margin: 2rem 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.summary-card {
|
|
123
|
+
background: ${theme.bgSecondary};
|
|
124
|
+
border: 1px solid ${theme.border};
|
|
125
|
+
border-radius: 8px;
|
|
126
|
+
padding: 1.5rem;
|
|
127
|
+
text-align: center;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.summary-number {
|
|
131
|
+
font-size: 2rem;
|
|
132
|
+
font-weight: 700;
|
|
133
|
+
margin-bottom: 0.5rem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.summary-label {
|
|
137
|
+
color: ${theme.textSecondary};
|
|
138
|
+
text-transform: uppercase;
|
|
139
|
+
font-size: 0.875rem;
|
|
140
|
+
letter-spacing: 0.05em;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.filters {
|
|
144
|
+
background: ${theme.bgSecondary};
|
|
145
|
+
border: 1px solid ${theme.border};
|
|
146
|
+
border-radius: 8px;
|
|
147
|
+
padding: 1.5rem;
|
|
148
|
+
margin: 2rem 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.filter-group {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-wrap: wrap;
|
|
154
|
+
gap: 0.5rem;
|
|
155
|
+
margin-bottom: 1rem;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.filter-label {
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
margin-bottom: 0.5rem;
|
|
161
|
+
display: block;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.filter-btn {
|
|
165
|
+
padding: 0.5rem 1rem;
|
|
166
|
+
border: 1px solid ${theme.border};
|
|
167
|
+
background: ${theme.bg};
|
|
168
|
+
color: ${theme.text};
|
|
169
|
+
border-radius: 4px;
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
transition: all 0.2s;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.filter-btn:hover,
|
|
175
|
+
.filter-btn.active {
|
|
176
|
+
background: ${theme.accent};
|
|
177
|
+
color: white;
|
|
178
|
+
border-color: ${theme.accent};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.search-box {
|
|
182
|
+
width: 100%;
|
|
183
|
+
padding: 0.75rem;
|
|
184
|
+
border: 1px solid ${theme.border};
|
|
185
|
+
border-radius: 4px;
|
|
186
|
+
background: ${theme.bg};
|
|
187
|
+
color: ${theme.text};
|
|
188
|
+
font-size: 1rem;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.findings {
|
|
192
|
+
margin: 2rem 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.finding {
|
|
196
|
+
background: ${theme.bgSecondary};
|
|
197
|
+
border: 1px solid ${theme.border};
|
|
198
|
+
border-radius: 8px;
|
|
199
|
+
margin-bottom: 1rem;
|
|
200
|
+
overflow: hidden;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.finding-header {
|
|
204
|
+
padding: 1rem 1.5rem;
|
|
205
|
+
border-bottom: 1px solid ${theme.border};
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
gap: 1rem;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.finding-header:hover {
|
|
213
|
+
background: ${theme.bgTertiary};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.severity-badge {
|
|
217
|
+
padding: 0.25rem 0.75rem;
|
|
218
|
+
border-radius: 9999px;
|
|
219
|
+
font-size: 0.75rem;
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
text-transform: uppercase;
|
|
222
|
+
letter-spacing: 0.05em;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.finding-title {
|
|
226
|
+
flex: 1;
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.finding-file {
|
|
231
|
+
color: ${theme.textSecondary};
|
|
232
|
+
font-size: 0.875rem;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.finding-details {
|
|
236
|
+
padding: 1.5rem;
|
|
237
|
+
border-top: 1px solid ${theme.border};
|
|
238
|
+
display: none;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.finding.expanded .finding-details {
|
|
242
|
+
display: block;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.finding-description {
|
|
246
|
+
margin-bottom: 1rem;
|
|
247
|
+
color: ${theme.textSecondary};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.finding-match {
|
|
251
|
+
background: ${theme.bgTertiary};
|
|
252
|
+
border-radius: 4px;
|
|
253
|
+
padding: 1rem;
|
|
254
|
+
margin: 1rem 0;
|
|
255
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
256
|
+
font-size: 0.875rem;
|
|
257
|
+
overflow-x: auto;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.finding-context {
|
|
261
|
+
background: ${theme.bgTertiary};
|
|
262
|
+
border-radius: 4px;
|
|
263
|
+
padding: 1rem;
|
|
264
|
+
margin: 1rem 0;
|
|
265
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
266
|
+
font-size: 0.875rem;
|
|
267
|
+
overflow-x: auto;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.context-line {
|
|
271
|
+
display: block;
|
|
272
|
+
padding: 0.125rem 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.context-line.match {
|
|
276
|
+
background: #fef3c7;
|
|
277
|
+
color: #92400e;
|
|
278
|
+
padding: 0.125rem 0.5rem;
|
|
279
|
+
border-radius: 2px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.line-number {
|
|
283
|
+
color: ${theme.textSecondary};
|
|
284
|
+
margin-right: 1rem;
|
|
285
|
+
min-width: 3rem;
|
|
286
|
+
display: inline-block;
|
|
287
|
+
text-align: right;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.remediation {
|
|
291
|
+
background: #dbeafe;
|
|
292
|
+
border: 1px solid #93c5fd;
|
|
293
|
+
border-radius: 4px;
|
|
294
|
+
padding: 1rem;
|
|
295
|
+
margin: 1rem 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.risk-score {
|
|
299
|
+
float: right;
|
|
300
|
+
font-weight: 600;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.footer {
|
|
304
|
+
margin-top: 4rem;
|
|
305
|
+
padding: 2rem 0;
|
|
306
|
+
text-align: center;
|
|
307
|
+
color: ${theme.textSecondary};
|
|
308
|
+
border-top: 1px solid ${theme.border};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.hidden { display: none !important; }
|
|
312
|
+
|
|
313
|
+
@media (max-width: 768px) {
|
|
314
|
+
.summary { grid-template-columns: 1fr; }
|
|
315
|
+
.filter-group { flex-direction: column; }
|
|
316
|
+
.finding-header { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
|
|
317
|
+
}
|
|
318
|
+
`;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Generate JavaScript functionality
|
|
322
|
+
*/
|
|
323
|
+
function generateJavaScript() {
|
|
324
|
+
return `
|
|
325
|
+
// Global state
|
|
326
|
+
let findings = [];
|
|
327
|
+
let filteredFindings = [];
|
|
328
|
+
|
|
329
|
+
// Initialize
|
|
330
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
331
|
+
findings = Array.from(document.querySelectorAll('.finding'));
|
|
332
|
+
filteredFindings = [...findings];
|
|
333
|
+
|
|
334
|
+
// Set up event listeners
|
|
335
|
+
setupFilters();
|
|
336
|
+
setupSearch();
|
|
337
|
+
setupFindingToggles();
|
|
338
|
+
|
|
339
|
+
// Initial filter
|
|
340
|
+
updateDisplay();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
function setupFilters() {
|
|
344
|
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
345
|
+
btn.addEventListener('click', function() {
|
|
346
|
+
const filterType = this.dataset.filter;
|
|
347
|
+
const filterValue = this.dataset.value;
|
|
348
|
+
|
|
349
|
+
if (filterType === 'severity') {
|
|
350
|
+
filterBySeverity(filterValue);
|
|
351
|
+
} else if (filterType === 'category') {
|
|
352
|
+
filterByCategory(filterValue);
|
|
353
|
+
} else if (filterType === 'clear') {
|
|
354
|
+
clearFilters();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Update button states
|
|
358
|
+
if (filterType !== 'clear') {
|
|
359
|
+
document.querySelectorAll(\`[data-filter="\${filterType}"]\`).forEach(b =>
|
|
360
|
+
b.classList.remove('active')
|
|
361
|
+
);
|
|
362
|
+
this.classList.add('active');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
updateDisplay();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function setupSearch() {
|
|
371
|
+
const searchBox = document.getElementById('search');
|
|
372
|
+
if (searchBox) {
|
|
373
|
+
searchBox.addEventListener('input', function() {
|
|
374
|
+
filterBySearch(this.value);
|
|
375
|
+
updateDisplay();
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function setupFindingToggles() {
|
|
381
|
+
document.querySelectorAll('.finding-header').forEach(header => {
|
|
382
|
+
header.addEventListener('click', function() {
|
|
383
|
+
const finding = this.closest('.finding');
|
|
384
|
+
finding.classList.toggle('expanded');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function filterBySeverity(severity) {
|
|
390
|
+
if (severity === 'all') {
|
|
391
|
+
filteredFindings = [...findings];
|
|
392
|
+
} else {
|
|
393
|
+
filteredFindings = findings.filter(f =>
|
|
394
|
+
f.dataset.severity === severity
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function filterByCategory(category) {
|
|
400
|
+
if (category === 'all') {
|
|
401
|
+
filteredFindings = [...findings];
|
|
402
|
+
} else {
|
|
403
|
+
filteredFindings = findings.filter(f =>
|
|
404
|
+
f.dataset.category === category
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function filterBySearch(query) {
|
|
410
|
+
if (!query.trim()) {
|
|
411
|
+
filteredFindings = [...findings];
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const lowerQuery = query.toLowerCase();
|
|
416
|
+
filteredFindings = findings.filter(f => {
|
|
417
|
+
const text = f.textContent.toLowerCase();
|
|
418
|
+
return text.includes(lowerQuery);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function clearFilters() {
|
|
423
|
+
filteredFindings = [...findings];
|
|
424
|
+
document.querySelectorAll('.filter-btn.active').forEach(btn =>
|
|
425
|
+
btn.classList.remove('active')
|
|
426
|
+
);
|
|
427
|
+
document.getElementById('search').value = '';
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function updateDisplay() {
|
|
431
|
+
findings.forEach(f => f.classList.add('hidden'));
|
|
432
|
+
filteredFindings.forEach(f => f.classList.remove('hidden'));
|
|
433
|
+
|
|
434
|
+
// Update count
|
|
435
|
+
const countEl = document.getElementById('filtered-count');
|
|
436
|
+
if (countEl) {
|
|
437
|
+
countEl.textContent = \`\${filteredFindings.length} of \${findings.length} findings\`;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
`;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Generate finding HTML
|
|
444
|
+
*/
|
|
445
|
+
function generateFindingHtml(finding, options) {
|
|
446
|
+
const severityColor = getSeverityColor(finding.severity);
|
|
447
|
+
const severityIcon = getSeverityIcon(finding.severity);
|
|
448
|
+
let contextHtml = '';
|
|
449
|
+
if (options.showCode && finding.context.length > 0) {
|
|
450
|
+
const contextLines = finding.context.map(line => `<span class="context-line ${line.isMatch ? 'match' : ''}">
|
|
451
|
+
<span class="line-number">${line.lineNumber}</span>${escapeHtml(line.content)}
|
|
452
|
+
</span>`).join('\n');
|
|
453
|
+
contextHtml = `
|
|
454
|
+
<div class="finding-context">
|
|
455
|
+
<strong>Code Context:</strong>
|
|
456
|
+
<pre>${contextLines}</pre>
|
|
457
|
+
</div>
|
|
458
|
+
`;
|
|
459
|
+
}
|
|
460
|
+
return `
|
|
461
|
+
<div class="finding" data-severity="${finding.severity}" data-category="${finding.category}">
|
|
462
|
+
<div class="finding-header">
|
|
463
|
+
<span class="severity-badge" style="background: ${severityColor}; color: white;">
|
|
464
|
+
${severityIcon} ${finding.severity}
|
|
465
|
+
</span>
|
|
466
|
+
<div class="finding-title">${escapeHtml(finding.ruleName)}</div>
|
|
467
|
+
<div class="finding-file">${escapeHtml(finding.relativePath)}:${finding.line}</div>
|
|
468
|
+
<div class="risk-score">Risk: ${finding.riskScore}/100</div>
|
|
469
|
+
</div>
|
|
470
|
+
<div class="finding-details">
|
|
471
|
+
<div class="finding-description">
|
|
472
|
+
<strong>Rule:</strong> ${escapeHtml(finding.ruleId)} - ${escapeHtml(finding.ruleName)}
|
|
473
|
+
</div>
|
|
474
|
+
<div class="finding-match">
|
|
475
|
+
<strong>Match:</strong> <code>${escapeHtml(finding.match)}</code>
|
|
476
|
+
</div>
|
|
477
|
+
${contextHtml}
|
|
478
|
+
<div class="remediation">
|
|
479
|
+
<strong>🔧 Remediation:</strong> ${escapeHtml(finding.remediation)}
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Generate complete HTML report
|
|
487
|
+
*/
|
|
488
|
+
export function generateHtmlReport(result, options = {}) {
|
|
489
|
+
const opts = {
|
|
490
|
+
title: 'Ferret Security Scan Report',
|
|
491
|
+
includeContext: true,
|
|
492
|
+
darkMode: false,
|
|
493
|
+
showCode: true,
|
|
494
|
+
...options,
|
|
495
|
+
};
|
|
496
|
+
const timestamp = formatTimestamp(result.endTime);
|
|
497
|
+
const duration = (result.duration / 1000).toFixed(2);
|
|
498
|
+
// Generate filter buttons
|
|
499
|
+
const severities = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'];
|
|
500
|
+
const categories = [
|
|
501
|
+
'credentials', 'injection', 'backdoors', 'supply-chain', 'permissions',
|
|
502
|
+
'persistence', 'obfuscation', 'ai-specific', 'exfiltration', 'advanced-hiding', 'behavioral'
|
|
503
|
+
];
|
|
504
|
+
const severityFilters = severities
|
|
505
|
+
.map(s => `<button class="filter-btn" data-filter="severity" data-value="${s}">${s}</button>`)
|
|
506
|
+
.join('');
|
|
507
|
+
const categoryFilters = categories
|
|
508
|
+
.map(c => `<button class="filter-btn" data-filter="category" data-value="${c}">${c.toUpperCase()}</button>`)
|
|
509
|
+
.join('');
|
|
510
|
+
// Generate findings HTML
|
|
511
|
+
const findingsHtml = result.findings
|
|
512
|
+
.map(finding => generateFindingHtml(finding, opts))
|
|
513
|
+
.join('');
|
|
514
|
+
return `<!DOCTYPE html>
|
|
515
|
+
<html lang="en">
|
|
516
|
+
<head>
|
|
517
|
+
<meta charset="UTF-8">
|
|
518
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
519
|
+
<title>${escapeHtml(opts.title)}</title>
|
|
520
|
+
<style>${generateCSS(opts.darkMode)}</style>
|
|
521
|
+
</head>
|
|
522
|
+
<body>
|
|
523
|
+
<div class="header">
|
|
524
|
+
<div class="container">
|
|
525
|
+
<h1 class="title">🦫 Ferret Scan Report</h1>
|
|
526
|
+
<p class="subtitle">Security analysis completed on ${timestamp}</p>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<div class="container">
|
|
531
|
+
<div class="summary">
|
|
532
|
+
<div class="summary-card">
|
|
533
|
+
<div class="summary-number" style="color: ${getSeverityColor('CRITICAL')}">${result.summary.critical}</div>
|
|
534
|
+
<div class="summary-label">Critical</div>
|
|
535
|
+
</div>
|
|
536
|
+
<div class="summary-card">
|
|
537
|
+
<div class="summary-number" style="color: ${getSeverityColor('HIGH')}">${result.summary.high}</div>
|
|
538
|
+
<div class="summary-label">High</div>
|
|
539
|
+
</div>
|
|
540
|
+
<div class="summary-card">
|
|
541
|
+
<div class="summary-number" style="color: ${getSeverityColor('MEDIUM')}">${result.summary.medium}</div>
|
|
542
|
+
<div class="summary-label">Medium</div>
|
|
543
|
+
</div>
|
|
544
|
+
<div class="summary-card">
|
|
545
|
+
<div class="summary-number">${result.analyzedFiles}</div>
|
|
546
|
+
<div class="summary-label">Files Scanned</div>
|
|
547
|
+
</div>
|
|
548
|
+
<div class="summary-card">
|
|
549
|
+
<div class="summary-number">${duration}s</div>
|
|
550
|
+
<div class="summary-label">Scan Time</div>
|
|
551
|
+
</div>
|
|
552
|
+
<div class="summary-card">
|
|
553
|
+
<div class="summary-number" style="color: ${result.overallRiskScore > 75 ? '#dc2626' : result.overallRiskScore > 50 ? '#ea580c' : '#16a34a'}">${result.overallRiskScore}</div>
|
|
554
|
+
<div class="summary-label">Risk Score</div>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
<div class="filters">
|
|
559
|
+
<label class="filter-label">Filter by Severity:</label>
|
|
560
|
+
<div class="filter-group">
|
|
561
|
+
<button class="filter-btn active" data-filter="severity" data-value="all">ALL</button>
|
|
562
|
+
${severityFilters}
|
|
563
|
+
<button class="filter-btn" data-filter="clear" data-value="">CLEAR</button>
|
|
564
|
+
</div>
|
|
565
|
+
|
|
566
|
+
<label class="filter-label">Filter by Category:</label>
|
|
567
|
+
<div class="filter-group">
|
|
568
|
+
<button class="filter-btn active" data-filter="category" data-value="all">ALL</button>
|
|
569
|
+
${categoryFilters}
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<label class="filter-label">Search:</label>
|
|
573
|
+
<input type="text" class="search-box" id="search" placeholder="Search findings by rule name, file, or content...">
|
|
574
|
+
|
|
575
|
+
<div style="margin-top: 1rem; color: #6b7280;">
|
|
576
|
+
<span id="filtered-count">${result.findings.length} of ${result.findings.length} findings</span>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<div class="findings">
|
|
581
|
+
${findingsHtml}
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
<div class="footer">
|
|
586
|
+
<div class="container">
|
|
587
|
+
<p>Generated by <strong>Ferret-Scan v1.0.0</strong> •
|
|
588
|
+
<a href="https://github.com/anthropics/ferret-scan" style="color: #3b82f6;">GitHub</a>
|
|
589
|
+
</p>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<script>${generateJavaScript()}</script>
|
|
594
|
+
</body>
|
|
595
|
+
</html>`;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Format HTML report as string
|
|
599
|
+
*/
|
|
600
|
+
export function formatHtmlReport(result, options = {}) {
|
|
601
|
+
return generateHtmlReport(result, options);
|
|
602
|
+
}
|
|
603
|
+
export default { generateHtmlReport, formatHtmlReport };
|
|
604
|
+
//# sourceMappingURL=HtmlReporter.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SARIF Reporter - Static Analysis Results Interchange Format
|
|
3
|
+
* Generates SARIF 2.1.0 compliant output for IDE and CI integration
|
|
4
|
+
* Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
|
|
5
|
+
*/
|
|
6
|
+
import type { ScanResult } from '../types.js';
|
|
7
|
+
interface SarifResult {
|
|
8
|
+
ruleId: string;
|
|
9
|
+
level: 'error' | 'warning' | 'note' | 'info';
|
|
10
|
+
message: {
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
locations: {
|
|
14
|
+
physicalLocation: {
|
|
15
|
+
artifactLocation: {
|
|
16
|
+
uri: string;
|
|
17
|
+
};
|
|
18
|
+
region: {
|
|
19
|
+
startLine: number;
|
|
20
|
+
startColumn?: number;
|
|
21
|
+
snippet?: {
|
|
22
|
+
text: string;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}[];
|
|
27
|
+
properties?: {
|
|
28
|
+
category: string;
|
|
29
|
+
riskScore: number;
|
|
30
|
+
remediation: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
interface SarifRule {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
shortDescription: {
|
|
37
|
+
text: string;
|
|
38
|
+
};
|
|
39
|
+
fullDescription?: {
|
|
40
|
+
text: string;
|
|
41
|
+
};
|
|
42
|
+
defaultConfiguration: {
|
|
43
|
+
level: 'error' | 'warning' | 'note' | 'info';
|
|
44
|
+
};
|
|
45
|
+
helpUri?: string;
|
|
46
|
+
properties?: {
|
|
47
|
+
category: string;
|
|
48
|
+
tags: string[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface SarifDocument {
|
|
52
|
+
version: '2.1.0';
|
|
53
|
+
$schema: string;
|
|
54
|
+
runs: {
|
|
55
|
+
tool: {
|
|
56
|
+
driver: {
|
|
57
|
+
name: string;
|
|
58
|
+
version: string;
|
|
59
|
+
informationUri: string;
|
|
60
|
+
rules: SarifRule[];
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
results: SarifResult[];
|
|
64
|
+
properties?: {
|
|
65
|
+
ferret: {
|
|
66
|
+
scanDuration: number;
|
|
67
|
+
filesScanned: number;
|
|
68
|
+
riskScore: number;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generate SARIF document from scan results
|
|
75
|
+
*/
|
|
76
|
+
export declare function generateSarifReport(result: ScanResult): SarifDocument;
|
|
77
|
+
/**
|
|
78
|
+
* Format SARIF document as JSON string
|
|
79
|
+
*/
|
|
80
|
+
export declare function formatSarifReport(result: ScanResult): string;
|
|
81
|
+
declare const _default: {
|
|
82
|
+
generateSarifReport: typeof generateSarifReport;
|
|
83
|
+
formatSarifReport: typeof formatSarifReport;
|
|
84
|
+
};
|
|
85
|
+
export default _default;
|
|
86
|
+
//# sourceMappingURL=SarifReporter.d.ts.map
|