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.
Files changed (69) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +21 -0
  3. package/README.md +416 -0
  4. package/bin/ferret.js +822 -0
  5. package/dist/__tests__/basic.test.d.ts +6 -0
  6. package/dist/__tests__/basic.test.js +80 -0
  7. package/dist/analyzers/AstAnalyzer.d.ts +30 -0
  8. package/dist/analyzers/AstAnalyzer.js +332 -0
  9. package/dist/analyzers/CorrelationAnalyzer.d.ts +21 -0
  10. package/dist/analyzers/CorrelationAnalyzer.js +288 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.js +22 -0
  13. package/dist/intelligence/IndicatorMatcher.d.ts +50 -0
  14. package/dist/intelligence/IndicatorMatcher.js +285 -0
  15. package/dist/intelligence/ThreatFeed.d.ts +99 -0
  16. package/dist/intelligence/ThreatFeed.js +296 -0
  17. package/dist/remediation/Fixer.d.ts +71 -0
  18. package/dist/remediation/Fixer.js +391 -0
  19. package/dist/remediation/Quarantine.d.ts +102 -0
  20. package/dist/remediation/Quarantine.js +329 -0
  21. package/dist/reporters/ConsoleReporter.d.ts +13 -0
  22. package/dist/reporters/ConsoleReporter.js +185 -0
  23. package/dist/reporters/HtmlReporter.d.ts +25 -0
  24. package/dist/reporters/HtmlReporter.js +604 -0
  25. package/dist/reporters/SarifReporter.d.ts +86 -0
  26. package/dist/reporters/SarifReporter.js +117 -0
  27. package/dist/rules/ai-specific.d.ts +8 -0
  28. package/dist/rules/ai-specific.js +221 -0
  29. package/dist/rules/backdoors.d.ts +8 -0
  30. package/dist/rules/backdoors.js +134 -0
  31. package/dist/rules/correlationRules.d.ts +8 -0
  32. package/dist/rules/correlationRules.js +227 -0
  33. package/dist/rules/credentials.d.ts +8 -0
  34. package/dist/rules/credentials.js +194 -0
  35. package/dist/rules/exfiltration.d.ts +8 -0
  36. package/dist/rules/exfiltration.js +139 -0
  37. package/dist/rules/index.d.ts +51 -0
  38. package/dist/rules/index.js +97 -0
  39. package/dist/rules/injection.d.ts +8 -0
  40. package/dist/rules/injection.js +136 -0
  41. package/dist/rules/obfuscation.d.ts +8 -0
  42. package/dist/rules/obfuscation.js +159 -0
  43. package/dist/rules/permissions.d.ts +8 -0
  44. package/dist/rules/permissions.js +129 -0
  45. package/dist/rules/persistence.d.ts +8 -0
  46. package/dist/rules/persistence.js +117 -0
  47. package/dist/rules/semanticRules.d.ts +10 -0
  48. package/dist/rules/semanticRules.js +212 -0
  49. package/dist/rules/supply-chain.d.ts +8 -0
  50. package/dist/rules/supply-chain.js +148 -0
  51. package/dist/scanner/FileDiscovery.d.ts +24 -0
  52. package/dist/scanner/FileDiscovery.js +282 -0
  53. package/dist/scanner/PatternMatcher.d.ts +25 -0
  54. package/dist/scanner/PatternMatcher.js +206 -0
  55. package/dist/scanner/Scanner.d.ts +14 -0
  56. package/dist/scanner/Scanner.js +266 -0
  57. package/dist/scanner/WatchMode.d.ts +29 -0
  58. package/dist/scanner/WatchMode.js +195 -0
  59. package/dist/types.d.ts +332 -0
  60. package/dist/types.js +53 -0
  61. package/dist/utils/baseline.d.ts +80 -0
  62. package/dist/utils/baseline.js +276 -0
  63. package/dist/utils/config.d.ts +21 -0
  64. package/dist/utils/config.js +247 -0
  65. package/dist/utils/ignore.d.ts +18 -0
  66. package/dist/utils/ignore.js +82 -0
  67. package/dist/utils/logger.d.ts +32 -0
  68. package/dist/utils/logger.js +75 -0
  69. 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, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&#39;');
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