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,329 @@
1
+ /**
2
+ * Quarantine System - Safely isolate suspicious files and content
3
+ * Provides reversible quarantine operations with audit trails
4
+ */
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, unlinkSync, statSync } from 'node:fs';
6
+ import { resolve, dirname, basename } from 'node:path';
7
+ import { createHash } from 'node:crypto';
8
+ import logger from '../utils/logger.js';
9
+ /**
10
+ * Default quarantine options
11
+ */
12
+ const DEFAULT_OPTIONS = {
13
+ quarantineDir: '.ferret-quarantine',
14
+ createBackup: true,
15
+ removeOriginal: false, // By default, keep originals for safety
16
+ compressFiles: true,
17
+ maxFileSizeMB: 100
18
+ };
19
+ /**
20
+ * Generate quarantine ID
21
+ */
22
+ function generateQuarantineId() {
23
+ const timestamp = Date.now().toString(36);
24
+ const random = Math.random().toString(36).substring(2, 7);
25
+ return `quar-${timestamp}-${random}`;
26
+ }
27
+ /**
28
+ * Calculate file hash
29
+ */
30
+ function calculateFileHash(filePath) {
31
+ try {
32
+ const content = readFileSync(filePath);
33
+ return createHash('sha256').update(content).digest('hex');
34
+ }
35
+ catch (error) {
36
+ logger.warn(`Could not calculate hash for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
37
+ return 'unknown';
38
+ }
39
+ }
40
+ /**
41
+ * Load quarantine database
42
+ */
43
+ export function loadQuarantineDatabase(quarantineDir) {
44
+ const dbPath = resolve(quarantineDir, 'quarantine.json');
45
+ if (!existsSync(dbPath)) {
46
+ return createEmptyDatabase();
47
+ }
48
+ try {
49
+ const content = readFileSync(dbPath, 'utf-8');
50
+ const db = JSON.parse(content);
51
+ // Validate database structure
52
+ if (!db.version || !db.entries || !Array.isArray(db.entries)) {
53
+ logger.warn('Invalid quarantine database, creating new one');
54
+ return createEmptyDatabase();
55
+ }
56
+ return db;
57
+ }
58
+ catch (error) {
59
+ logger.error(`Failed to load quarantine database: ${error instanceof Error ? error.message : String(error)}`);
60
+ return createEmptyDatabase();
61
+ }
62
+ }
63
+ /**
64
+ * Save quarantine database
65
+ */
66
+ export function saveQuarantineDatabase(db, quarantineDir) {
67
+ try {
68
+ // Ensure directory exists
69
+ mkdirSync(quarantineDir, { recursive: true });
70
+ // Update stats and metadata
71
+ db.lastUpdated = new Date().toISOString();
72
+ db.stats = calculateQuarantineStats(db.entries);
73
+ const dbPath = resolve(quarantineDir, 'quarantine.json');
74
+ const content = JSON.stringify(db, null, 2);
75
+ writeFileSync(dbPath, content, 'utf-8');
76
+ logger.debug(`Saved quarantine database with ${db.entries.length} entries`);
77
+ }
78
+ catch (error) {
79
+ logger.error(`Failed to save quarantine database: ${error instanceof Error ? error.message : String(error)}`);
80
+ throw error;
81
+ }
82
+ }
83
+ /**
84
+ * Create empty quarantine database
85
+ */
86
+ function createEmptyDatabase() {
87
+ return {
88
+ version: '1.0',
89
+ created: new Date().toISOString(),
90
+ lastUpdated: new Date().toISOString(),
91
+ entries: [],
92
+ stats: {
93
+ totalQuarantined: 0,
94
+ totalRestored: 0,
95
+ byCategory: {},
96
+ bySeverity: {}
97
+ }
98
+ };
99
+ }
100
+ /**
101
+ * Calculate quarantine statistics
102
+ */
103
+ function calculateQuarantineStats(entries) {
104
+ const stats = {
105
+ totalQuarantined: entries.length,
106
+ totalRestored: entries.filter(e => e.restored).length,
107
+ byCategory: {},
108
+ bySeverity: {}
109
+ };
110
+ for (const entry of entries) {
111
+ // Count by category
112
+ stats.byCategory[entry.metadata.category] = (stats.byCategory[entry.metadata.category] ?? 0) + 1;
113
+ // Count by severity
114
+ stats.bySeverity[entry.metadata.severity] = (stats.bySeverity[entry.metadata.severity] ?? 0) + 1;
115
+ }
116
+ return stats;
117
+ }
118
+ /**
119
+ * Quarantine a file based on findings
120
+ */
121
+ export function quarantineFile(filePath, findings, reason, options = {}) {
122
+ const config = { ...DEFAULT_OPTIONS, ...options };
123
+ try {
124
+ // Check if file exists
125
+ if (!existsSync(filePath)) {
126
+ logger.error(`File not found for quarantine: ${filePath}`);
127
+ return null;
128
+ }
129
+ // Check file size
130
+ const stats = statSync(filePath);
131
+ const fileSizeMB = stats.size / (1024 * 1024);
132
+ if (fileSizeMB > config.maxFileSizeMB) {
133
+ logger.warn(`File too large for quarantine: ${filePath} (${fileSizeMB.toFixed(1)}MB)`);
134
+ return null;
135
+ }
136
+ // Generate quarantine entry
137
+ const id = generateQuarantineId();
138
+ const fileName = basename(filePath);
139
+ const quarantineFileName = `${id}_${fileName}`;
140
+ const quarantinePath = resolve(config.quarantineDir, 'files', quarantineFileName);
141
+ // Ensure quarantine directory exists
142
+ mkdirSync(dirname(quarantinePath), { recursive: true });
143
+ // Copy file to quarantine
144
+ copyFileSync(filePath, quarantinePath);
145
+ // Calculate metadata
146
+ const fileHash = calculateFileHash(filePath);
147
+ const maxRiskScore = Math.max(...findings.map(f => f.riskScore));
148
+ const severities = findings.map(f => f.severity);
149
+ const severityOrder = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'];
150
+ const highestSeverity = severityOrder.find(s => severities.includes(s)) ?? 'INFO';
151
+ const entry = {
152
+ id,
153
+ originalPath: filePath,
154
+ quarantinePath,
155
+ reason,
156
+ findings,
157
+ quarantineDate: new Date().toISOString(),
158
+ fileSize: stats.size,
159
+ fileHash,
160
+ restored: false,
161
+ metadata: {
162
+ riskScore: maxRiskScore,
163
+ severity: highestSeverity,
164
+ category: findings[0]?.category ?? 'unknown'
165
+ }
166
+ };
167
+ // Update quarantine database
168
+ const db = loadQuarantineDatabase(config.quarantineDir);
169
+ db.entries.push(entry);
170
+ saveQuarantineDatabase(db, config.quarantineDir);
171
+ // Optionally remove original file
172
+ if (config.removeOriginal) {
173
+ try {
174
+ unlinkSync(filePath);
175
+ logger.info(`Quarantined and removed: ${filePath}`);
176
+ }
177
+ catch (error) {
178
+ logger.warn(`Could not remove original file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
179
+ }
180
+ }
181
+ else {
182
+ logger.info(`Quarantined (original preserved): ${filePath}`);
183
+ }
184
+ return entry;
185
+ }
186
+ catch (error) {
187
+ logger.error(`Error quarantining file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
188
+ return null;
189
+ }
190
+ }
191
+ /**
192
+ * Restore a quarantined file
193
+ */
194
+ export function restoreQuarantinedFile(entryId, quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
195
+ try {
196
+ const db = loadQuarantineDatabase(quarantineDir);
197
+ const entry = db.entries.find(e => e.id === entryId);
198
+ if (!entry) {
199
+ logger.error(`Quarantine entry not found: ${entryId}`);
200
+ return false;
201
+ }
202
+ if (entry.restored) {
203
+ logger.warn(`File already restored: ${entryId}`);
204
+ return false;
205
+ }
206
+ if (!existsSync(entry.quarantinePath)) {
207
+ logger.error(`Quarantined file not found: ${entry.quarantinePath}`);
208
+ return false;
209
+ }
210
+ // Ensure original directory exists
211
+ mkdirSync(dirname(entry.originalPath), { recursive: true });
212
+ // Restore file
213
+ copyFileSync(entry.quarantinePath, entry.originalPath);
214
+ // Update entry
215
+ entry.restored = true;
216
+ entry.restoredDate = new Date().toISOString();
217
+ // Save updated database
218
+ saveQuarantineDatabase(db, quarantineDir);
219
+ logger.info(`Restored quarantined file: ${entry.originalPath}`);
220
+ return true;
221
+ }
222
+ catch (error) {
223
+ logger.error(`Error restoring quarantined file: ${error instanceof Error ? error.message : String(error)}`);
224
+ return false;
225
+ }
226
+ }
227
+ /**
228
+ * Delete a quarantined file permanently
229
+ */
230
+ export function deleteQuarantinedFile(entryId, quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
231
+ try {
232
+ const db = loadQuarantineDatabase(quarantineDir);
233
+ const entryIndex = db.entries.findIndex(e => e.id === entryId);
234
+ if (entryIndex === -1) {
235
+ logger.error(`Quarantine entry not found: ${entryId}`);
236
+ return false;
237
+ }
238
+ const entry = db.entries[entryIndex];
239
+ if (!entry) {
240
+ logger.error(`Entry not found at index ${entryIndex}`);
241
+ return false;
242
+ }
243
+ // Delete quarantined file
244
+ if (existsSync(entry.quarantinePath)) {
245
+ unlinkSync(entry.quarantinePath);
246
+ }
247
+ // Remove entry from database
248
+ db.entries.splice(entryIndex, 1);
249
+ saveQuarantineDatabase(db, quarantineDir);
250
+ logger.info(`Permanently deleted quarantined file: ${entryId}`);
251
+ return true;
252
+ }
253
+ catch (error) {
254
+ logger.error(`Error deleting quarantined file: ${error instanceof Error ? error.message : String(error)}`);
255
+ return false;
256
+ }
257
+ }
258
+ /**
259
+ * List quarantined files
260
+ */
261
+ export function listQuarantinedFiles(quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
262
+ const db = loadQuarantineDatabase(quarantineDir);
263
+ return db.entries.sort((a, b) => new Date(b.quarantineDate).getTime() - new Date(a.quarantineDate).getTime());
264
+ }
265
+ /**
266
+ * Get quarantine statistics
267
+ */
268
+ export function getQuarantineStats(quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
269
+ const db = loadQuarantineDatabase(quarantineDir);
270
+ return db.stats;
271
+ }
272
+ /**
273
+ * Clean up old quarantine entries
274
+ */
275
+ export function cleanupQuarantine(maxAgeDays = 30, quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
276
+ try {
277
+ const db = loadQuarantineDatabase(quarantineDir);
278
+ const cutoffDate = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1000);
279
+ const entriesToDelete = db.entries.filter(entry => {
280
+ const entryDate = new Date(entry.quarantineDate);
281
+ return entryDate < cutoffDate && entry.restored;
282
+ });
283
+ let deletedCount = 0;
284
+ for (const entry of entriesToDelete) {
285
+ if (deleteQuarantinedFile(entry.id, quarantineDir)) {
286
+ deletedCount++;
287
+ }
288
+ }
289
+ logger.info(`Cleaned up ${deletedCount} old quarantine entries`);
290
+ return deletedCount;
291
+ }
292
+ catch (error) {
293
+ logger.error(`Error cleaning up quarantine: ${error instanceof Error ? error.message : String(error)}`);
294
+ return 0;
295
+ }
296
+ }
297
+ /**
298
+ * Check quarantine health
299
+ */
300
+ export function checkQuarantineHealth(quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
301
+ const issues = [];
302
+ const db = loadQuarantineDatabase(quarantineDir);
303
+ // Check for missing quarantined files
304
+ for (const entry of db.entries) {
305
+ if (!entry.restored && !existsSync(entry.quarantinePath)) {
306
+ issues.push(`Missing quarantined file: ${entry.id} (${entry.originalPath})`);
307
+ }
308
+ }
309
+ // Check quarantine directory structure
310
+ const quarantineFilesDir = resolve(quarantineDir, 'files');
311
+ if (!existsSync(quarantineFilesDir)) {
312
+ issues.push('Quarantine files directory missing');
313
+ }
314
+ return {
315
+ healthy: issues.length === 0,
316
+ issues,
317
+ stats: db.stats
318
+ };
319
+ }
320
+ export default {
321
+ quarantineFile,
322
+ restoreQuarantinedFile,
323
+ deleteQuarantinedFile,
324
+ listQuarantinedFiles,
325
+ getQuarantineStats,
326
+ cleanupQuarantine,
327
+ checkQuarantineHealth
328
+ };
329
+ //# sourceMappingURL=Quarantine.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ConsoleReporter - Beautiful terminal output for scan results
3
+ */
4
+ import type { ScanResult } from '../types.js';
5
+ /**
6
+ * Generate console report
7
+ */
8
+ export declare function generateConsoleReport(result: ScanResult, options?: {
9
+ verbose?: boolean;
10
+ ci?: boolean;
11
+ }): string;
12
+ export default generateConsoleReport;
13
+ //# sourceMappingURL=ConsoleReporter.d.ts.map
@@ -0,0 +1,185 @@
1
+ /**
2
+ * ConsoleReporter - Beautiful terminal output for scan results
3
+ */
4
+ // ANSI color codes
5
+ const colors = {
6
+ reset: '\x1b[0m',
7
+ bold: '\x1b[1m',
8
+ dim: '\x1b[2m',
9
+ red: '\x1b[31m',
10
+ green: '\x1b[32m',
11
+ yellow: '\x1b[33m',
12
+ blue: '\x1b[34m',
13
+ magenta: '\x1b[35m',
14
+ cyan: '\x1b[36m',
15
+ white: '\x1b[37m',
16
+ bgRed: '\x1b[41m',
17
+ bgYellow: '\x1b[43m',
18
+ bgBlue: '\x1b[44m',
19
+ };
20
+ const FERRET_BANNER = `
21
+ ${colors.cyan} ⡠⢂⠔⠚⠟⠓⠒⠒⢂⠐⢄
22
+ ⣷⣧⣀⠀⢀⣀⣤⣄⠈⢢⢸⡀ ${colors.bold}███████╗███████╗██████╗ ██████╗ ███████╗████████╗
23
+ ${colors.cyan}⢀⣿⣭⣿⣿⣿⣿⣽⣹⣧⠈⣾⢱⡀ ${colors.bold}██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝
24
+ ${colors.cyan}⢸⢿⠋⢸⠂⠈⠹⢿⣿⡿⠀⢸⡷⡇ ${colors.bold}█████╗ █████╗ ██████╔╝██████╔╝█████╗ ██║
25
+ ${colors.cyan}⠈⣆⠉⢇⢁⠶⠈⠀⠉⠀⢀⣾⣇⡇ ${colors.bold}██╔══╝ ██╔══╝ ██╔══██╗██╔══██╗██╔══╝ ██║
26
+ ${colors.cyan} ⢑⣦⣤⣤⣤⣤⣴⣶⣿⡿⢨⠃ ${colors.bold}██║ ███████╗██║ ██║██║ ██║███████╗ ██║
27
+ ${colors.cyan} ⢰⣿⣿⣟⣯⡿⣽⣻⣾⣽⣇⠏ ${colors.bold}╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝${colors.reset}
28
+ ${colors.dim} Security Scanner for AI CLI Configs${colors.reset}
29
+ `;
30
+ const SEVERITY_COLORS = {
31
+ CRITICAL: colors.bgRed + colors.white + colors.bold,
32
+ HIGH: colors.red + colors.bold,
33
+ MEDIUM: colors.yellow,
34
+ LOW: colors.blue,
35
+ INFO: colors.dim,
36
+ };
37
+ const SEVERITY_ICONS = {
38
+ CRITICAL: '!!!',
39
+ HIGH: '!!',
40
+ MEDIUM: '!',
41
+ LOW: '*',
42
+ INFO: '-',
43
+ };
44
+ /**
45
+ * Format a severity badge
46
+ */
47
+ function formatSeverity(severity) {
48
+ const color = SEVERITY_COLORS[severity];
49
+ const icon = SEVERITY_ICONS[severity];
50
+ return `${color}[${icon} ${severity}]${colors.reset}`;
51
+ }
52
+ /**
53
+ * Format a finding for display
54
+ */
55
+ function formatFinding(finding, verbose) {
56
+ const lines = [];
57
+ // Header
58
+ lines.push(`${formatSeverity(finding.severity)} ${colors.bold}${finding.ruleId}${colors.reset} - ${finding.ruleName}`);
59
+ // Location
60
+ lines.push(` ${colors.cyan}File:${colors.reset} ${finding.relativePath}:${finding.line}`);
61
+ // Match
62
+ const matchDisplay = finding.match.length > 80
63
+ ? finding.match.slice(0, 77) + '...'
64
+ : finding.match;
65
+ lines.push(` ${colors.cyan}Match:${colors.reset} ${colors.yellow}${matchDisplay}${colors.reset}`);
66
+ // Context (if verbose)
67
+ if (verbose && finding.context.length > 0) {
68
+ lines.push('');
69
+ lines.push(` ${colors.dim}Context:${colors.reset}`);
70
+ for (const ctx of finding.context) {
71
+ const lineNum = String(ctx.lineNumber).padStart(4, ' ');
72
+ const marker = ctx.isMatch ? `${colors.red}>${colors.reset}` : ' ';
73
+ const lineColor = ctx.isMatch ? colors.yellow : colors.dim;
74
+ lines.push(` ${marker} ${colors.dim}${lineNum}${colors.reset} ${colors.dim}|${colors.reset} ${lineColor}${ctx.content}${colors.reset}`);
75
+ }
76
+ }
77
+ // Remediation
78
+ lines.push(` ${colors.green}Remediation:${colors.reset} ${finding.remediation}`);
79
+ // Risk score
80
+ lines.push(` ${colors.magenta}Risk Score:${colors.reset} ${finding.riskScore}/100`);
81
+ return lines.join('\n');
82
+ }
83
+ /**
84
+ * Format summary statistics
85
+ */
86
+ function formatSummary(summary, result) {
87
+ const lines = [];
88
+ lines.push('');
89
+ lines.push(`${colors.bold}${'━'.repeat(60)}${colors.reset}`);
90
+ lines.push(`${colors.bold}SUMMARY${colors.reset}`);
91
+ lines.push(`${colors.bold}${'━'.repeat(60)}${colors.reset}`);
92
+ const stats = [
93
+ summary.critical > 0 ? `${SEVERITY_COLORS['CRITICAL']}Critical: ${summary.critical}${colors.reset}` : `Critical: ${summary.critical}`,
94
+ summary.high > 0 ? `${SEVERITY_COLORS['HIGH']}High: ${summary.high}${colors.reset}` : `High: ${summary.high}`,
95
+ summary.medium > 0 ? `${SEVERITY_COLORS['MEDIUM']}Medium: ${summary.medium}${colors.reset}` : `Medium: ${summary.medium}`,
96
+ `Low: ${summary.low}`,
97
+ `Info: ${summary.info}`,
98
+ ];
99
+ lines.push(stats.join(' | '));
100
+ lines.push(`Files scanned: ${result.analyzedFiles} | Time: ${result.duration}ms | Risk Score: ${result.overallRiskScore}/100`);
101
+ return lines.join('\n');
102
+ }
103
+ /**
104
+ * Format findings grouped by severity
105
+ */
106
+ function formatGroupedFindings(result, verbose) {
107
+ const lines = [];
108
+ const severities = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'];
109
+ for (const severity of severities) {
110
+ const findings = result.findingsBySeverity[severity];
111
+ if (findings.length === 0)
112
+ continue;
113
+ lines.push('');
114
+ lines.push(`${SEVERITY_COLORS[severity]}${severity} (${findings.length})${colors.reset}`);
115
+ lines.push(`${colors.dim}${'━'.repeat(60)}${colors.reset}`);
116
+ lines.push('');
117
+ for (const finding of findings) {
118
+ lines.push(formatFinding(finding, verbose));
119
+ lines.push('');
120
+ }
121
+ }
122
+ return lines.join('\n');
123
+ }
124
+ /**
125
+ * Format scan errors
126
+ */
127
+ function formatErrors(result) {
128
+ if (result.errors.length === 0)
129
+ return '';
130
+ const lines = [];
131
+ lines.push('');
132
+ lines.push(`${colors.yellow}Errors (${result.errors.length})${colors.reset}`);
133
+ lines.push(`${colors.dim}${'━'.repeat(60)}${colors.reset}`);
134
+ for (const error of result.errors) {
135
+ const file = error.file ? `${error.file}: ` : '';
136
+ lines.push(` ${colors.yellow}!${colors.reset} ${file}${error.message}`);
137
+ }
138
+ return lines.join('\n');
139
+ }
140
+ /**
141
+ * Generate console report
142
+ */
143
+ export function generateConsoleReport(result, options = {}) {
144
+ const { verbose = false, ci = false } = options;
145
+ if (ci) {
146
+ return generateCiReport(result);
147
+ }
148
+ const lines = [];
149
+ // Banner
150
+ lines.push(FERRET_BANNER);
151
+ // Scan info
152
+ lines.push(`${colors.dim}Scanning: ${result.scannedPaths.join(', ')}${colors.reset}`);
153
+ lines.push(`${colors.dim}Found: ${result.analyzedFiles} configuration files${colors.reset}`);
154
+ // Findings
155
+ if (result.summary.total === 0) {
156
+ lines.push('');
157
+ lines.push(`${colors.green}${colors.bold}No security issues found!${colors.reset}`);
158
+ lines.push(`${colors.green}Your AI CLI configurations look clean.${colors.reset}`);
159
+ }
160
+ else {
161
+ lines.push(formatGroupedFindings(result, verbose));
162
+ }
163
+ // Errors
164
+ if (result.errors.length > 0) {
165
+ lines.push(formatErrors(result));
166
+ }
167
+ // Summary
168
+ lines.push(formatSummary(result.summary, result));
169
+ return lines.join('\n');
170
+ }
171
+ /**
172
+ * Generate CI-friendly report (minimal formatting)
173
+ */
174
+ function generateCiReport(result) {
175
+ const lines = [];
176
+ lines.push(`[FERRET] Scanned ${result.analyzedFiles} files in ${result.duration}ms`);
177
+ for (const finding of result.findings) {
178
+ lines.push(`[${finding.severity}] ${finding.ruleId}: ${finding.relativePath}:${finding.line} - ${finding.ruleName}`);
179
+ }
180
+ lines.push(`[SUMMARY] Critical: ${result.summary.critical} | High: ${result.summary.high} | Medium: ${result.summary.medium} | Low: ${result.summary.low} | Info: ${result.summary.info}`);
181
+ lines.push(`[RISK] Overall risk score: ${result.overallRiskScore}/100`);
182
+ return lines.join('\n');
183
+ }
184
+ export default generateConsoleReport;
185
+ //# sourceMappingURL=ConsoleReporter.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * HTML Reporter - Beautiful HTML reports with interactive filtering
3
+ * Generates standalone HTML files with embedded CSS and JavaScript
4
+ */
5
+ import type { ScanResult } from '../types.js';
6
+ interface HtmlReportOptions {
7
+ title?: string;
8
+ includeContext?: boolean;
9
+ darkMode?: boolean;
10
+ showCode?: boolean;
11
+ }
12
+ /**
13
+ * Generate complete HTML report
14
+ */
15
+ export declare function generateHtmlReport(result: ScanResult, options?: HtmlReportOptions): string;
16
+ /**
17
+ * Format HTML report as string
18
+ */
19
+ export declare function formatHtmlReport(result: ScanResult, options?: HtmlReportOptions): string;
20
+ declare const _default: {
21
+ generateHtmlReport: typeof generateHtmlReport;
22
+ formatHtmlReport: typeof formatHtmlReport;
23
+ };
24
+ export default _default;
25
+ //# sourceMappingURL=HtmlReporter.d.ts.map