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
package/bin/ferret.js ADDED
@@ -0,0 +1,822 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Ferret CLI - Security scanner for AI CLI configurations
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import { readFileSync } from 'node:fs';
9
+ import { resolve, dirname } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { scan, getExitCode } from '../dist/scanner/Scanner.js';
12
+ import { loadConfig, getAIConfigPaths } from '../dist/utils/config.js';
13
+ import { generateConsoleReport } from '../dist/reporters/ConsoleReporter.js';
14
+ import { formatSarifReport } from '../dist/reporters/SarifReporter.js';
15
+ import { formatHtmlReport } from '../dist/reporters/HtmlReporter.js';
16
+ import { startEnhancedWatchMode } from '../dist/scanner/WatchMode.js';
17
+ import {
18
+ loadBaseline,
19
+ saveBaseline,
20
+ createBaseline,
21
+ filterAgainstBaseline,
22
+ getDefaultBaselinePath,
23
+ getBaselineStats
24
+ } from '../dist/utils/baseline.js';
25
+ import { getAllRules, getRuleById, getRuleStats } from '../dist/rules/index.js';
26
+ import {
27
+ loadThreatDatabase,
28
+ saveThreatDatabase,
29
+ addIndicators,
30
+ searchIndicators,
31
+ needsUpdate
32
+ } from '../dist/intelligence/ThreatFeed.js';
33
+ import {
34
+ applyRemediation,
35
+ applyRemediationBatch,
36
+ previewRemediation,
37
+ canAutoRemediate
38
+ } from '../dist/remediation/Fixer.js';
39
+ import {
40
+ quarantineFile,
41
+ listQuarantinedFiles,
42
+ restoreQuarantinedFile,
43
+ getQuarantineStats,
44
+ checkQuarantineHealth
45
+ } from '../dist/remediation/Quarantine.js';
46
+ import { logger } from '../dist/utils/logger.js';
47
+
48
+ const __filename = fileURLToPath(import.meta.url);
49
+ const __dirname = dirname(__filename);
50
+
51
+ // Load package.json for version
52
+ const packageJsonPath = resolve(__dirname, '..', 'package.json');
53
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
54
+
55
+ const program = new Command();
56
+
57
+ program
58
+ .name('ferret')
59
+ .description('Ferret out security threats in your AI CLI configurations')
60
+ .version(packageJson.version);
61
+
62
+ // Main scan command
63
+ program
64
+ .command('scan')
65
+ .description('Scan AI CLI configurations for security issues')
66
+ .argument('[path]', 'Path to scan (defaults to AI CLI config directories)')
67
+ .option('-f, --format <format>', 'Output format: console, json, sarif, html', 'console')
68
+ .option('-s, --severity <levels>', 'Severity levels to report (comma-separated)', 'critical,high,medium,low,info')
69
+ .option('-c, --categories <cats>', 'Categories to scan (comma-separated)')
70
+ .option('--fail-on <severity>', 'Minimum severity to fail on', 'high')
71
+ .option('-o, --output <file>', 'Output file path')
72
+ .option('-w, --watch', 'Watch mode - rescan on file changes')
73
+ .option('--ci', 'CI mode - minimal output, suitable for pipelines')
74
+ .option('-v, --verbose', 'Verbose output with context')
75
+ .option('--ai-detection', 'Enable AI-powered detection (experimental)')
76
+ .option('--threat-intel', 'Enable threat intelligence feeds (experimental)')
77
+ .option('--semantic-analysis', 'Enable AST-based semantic analysis')
78
+ .option('--correlation-analysis', 'Enable cross-file correlation analysis')
79
+ .option('--auto-remediation', 'Enable automated fixing (experimental)')
80
+ .option('--auto-fix', 'Automatically apply safe fixes after scanning')
81
+ .option('--config <file>', 'Path to configuration file')
82
+ .option('--baseline <file>', 'Path to baseline file for filtering known findings')
83
+ .option('--ignore-baseline', 'Ignore baseline file and show all findings')
84
+ .action(async (path, options) => {
85
+ try {
86
+ // Configure logger
87
+ logger.configure({
88
+ verbose: options.verbose,
89
+ ci: options.ci,
90
+ level: options.verbose ? 'debug' : 'info',
91
+ });
92
+
93
+ // Load configuration
94
+ const config = loadConfig({
95
+ path: path,
96
+ format: options.format,
97
+ severity: options.severity,
98
+ categories: options.categories,
99
+ failOn: options.failOn,
100
+ output: options.output,
101
+ watch: options.watch,
102
+ ci: options.ci,
103
+ verbose: options.verbose,
104
+ aiDetection: options.aiDetection,
105
+ threatIntel: options.threatIntel,
106
+ semanticAnalysis: options.semanticAnalysis,
107
+ correlationAnalysis: options.correlationAnalysis,
108
+ autoRemediation: options.autoRemediation,
109
+ config: options.config,
110
+ });
111
+
112
+ // Apply auto-fix if enabled
113
+ const shouldAutoFix = options.autoFix;
114
+
115
+ // If no paths specified and no AI CLI configs found, show helpful message
116
+ if (config.paths.length === 0) {
117
+ console.error('No AI CLI configuration directories found.');
118
+ console.error('');
119
+ console.error('Ferret looks for configurations in:');
120
+ console.error(' Claude Code: ~/.claude/, ./.claude/, CLAUDE.md, .mcp.json');
121
+ console.error(' Cursor: ./.cursor/, .cursorrules');
122
+ console.error(' Windsurf: ./.windsurf/, .windsurfrules');
123
+ console.error(' Continue: ./.continue/');
124
+ console.error(' Aider: ./.aider/, .aider.conf.yml');
125
+ console.error(' Cline: ./.cline/, .clinerules');
126
+ console.error(' Generic: ./.ai/, AI.md, AGENT.md');
127
+ console.error('');
128
+ console.error('You can also specify a path: ferret scan /path/to/config');
129
+ process.exit(1);
130
+ }
131
+
132
+ // Handle watch mode
133
+ if (config.watch) {
134
+ await startEnhancedWatchMode(config);
135
+ return; // Watch mode runs indefinitely
136
+ }
137
+
138
+ // Run scan
139
+ let result = await scan(config);
140
+
141
+ // Apply baseline filtering if enabled
142
+ if (!options.ignoreBaseline) {
143
+ const baselinePath = options.baseline || getDefaultBaselinePath(config.paths);
144
+ const baseline = loadBaseline(baselinePath);
145
+ if (baseline) {
146
+ console.log(`šŸ“‹ Applying baseline from: ${baselinePath}`);
147
+ result = filterAgainstBaseline(result, baseline);
148
+ }
149
+ }
150
+
151
+ // Output results
152
+ if (config.format === 'console') {
153
+ const report = generateConsoleReport(result, {
154
+ verbose: config.verbose,
155
+ ci: config.ci,
156
+ });
157
+ console.log(report);
158
+ } else if (config.format === 'json') {
159
+ const output = JSON.stringify(result, null, 2);
160
+ if (config.outputFile) {
161
+ const { writeFileSync } = await import('node:fs');
162
+ writeFileSync(config.outputFile, output);
163
+ console.log(`JSON report written to: ${config.outputFile}`);
164
+ } else {
165
+ console.log(output);
166
+ }
167
+ } else if (config.format === 'sarif') {
168
+ const output = formatSarifReport(result);
169
+ if (config.outputFile) {
170
+ const { writeFileSync } = await import('node:fs');
171
+ writeFileSync(config.outputFile, output);
172
+ console.log(`SARIF report written to: ${config.outputFile}`);
173
+ } else {
174
+ console.log(output);
175
+ }
176
+ } else if (config.format === 'html') {
177
+ const output = formatHtmlReport(result, {
178
+ title: `Security Scan Report - ${new Date().toLocaleDateString()}`,
179
+ darkMode: false,
180
+ showCode: true,
181
+ });
182
+ if (config.outputFile) {
183
+ const { writeFileSync } = await import('node:fs');
184
+ writeFileSync(config.outputFile, output);
185
+ console.log(`HTML report written to: ${config.outputFile}`);
186
+ } else {
187
+ console.log(output);
188
+ }
189
+ } else {
190
+ console.error(`Format '${config.format}' not yet implemented`);
191
+ process.exit(1);
192
+ }
193
+
194
+ // Apply auto-fix if enabled and findings exist
195
+ if (shouldAutoFix && result.findings.length > 0) {
196
+ const fixableFindings = result.findings.filter(finding => canAutoRemediate(finding));
197
+
198
+ if (fixableFindings.length > 0) {
199
+ console.log(`\nšŸ”§ Auto-fixing ${fixableFindings.length} issues...`);
200
+
201
+ const results = await applyRemediationBatch(fixableFindings, {
202
+ createBackups: true,
203
+ backupDir: '.ferret-backups',
204
+ safeOnly: true,
205
+ dryRun: false
206
+ });
207
+
208
+ const successful = results.filter(r => r.success);
209
+ console.log(`āœ… Applied ${successful.length}/${results.length} fixes automatically`);
210
+
211
+ if (successful.length > 0) {
212
+ console.log('Fixed issues:');
213
+ for (const fix of successful) {
214
+ console.log(` āœ“ ${fix.finding.relativePath}:${fix.finding.line}`);
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ // Exit with appropriate code
221
+ const exitCode = getExitCode(result, config);
222
+ process.exit(exitCode);
223
+ } catch (error) {
224
+ console.error('Error:', error instanceof Error ? error.message : String(error));
225
+ if (options.verbose) {
226
+ console.error(error);
227
+ }
228
+ process.exit(3);
229
+ }
230
+ });
231
+
232
+ // Check command - scan a single file
233
+ program
234
+ .command('check')
235
+ .description('Check a single file for security issues')
236
+ .argument('<file>', 'File to check')
237
+ .option('-v, --verbose', 'Verbose output')
238
+ .action(async (file, options) => {
239
+ try {
240
+ logger.configure({
241
+ verbose: options.verbose,
242
+ level: options.verbose ? 'debug' : 'info',
243
+ });
244
+
245
+ const config = loadConfig({
246
+ path: file,
247
+ verbose: options.verbose,
248
+ });
249
+
250
+ const result = await scan(config);
251
+ const report = generateConsoleReport(result, { verbose: options.verbose });
252
+ console.log(report);
253
+
254
+ process.exit(getExitCode(result, config));
255
+ } catch (error) {
256
+ console.error('Error:', error instanceof Error ? error.message : String(error));
257
+ process.exit(3);
258
+ }
259
+ });
260
+
261
+ // Rules commands
262
+ const rulesCmd = program
263
+ .command('rules')
264
+ .description('Manage security rules');
265
+
266
+ rulesCmd
267
+ .command('list')
268
+ .description('List all available rules')
269
+ .option('-c, --category <category>', 'Filter by category')
270
+ .option('-s, --severity <severity>', 'Filter by severity')
271
+ .action((options) => {
272
+ const rules = getAllRules();
273
+ const filtered = rules.filter(rule => {
274
+ if (options.category && rule.category !== options.category) return false;
275
+ if (options.severity && rule.severity !== options.severity.toUpperCase()) return false;
276
+ return true;
277
+ });
278
+
279
+ console.log(`\nAvailable Rules (${filtered.length}):`);
280
+ console.log('━'.repeat(60));
281
+
282
+ for (const rule of filtered) {
283
+ console.log(`[${rule.severity.padEnd(8)}] ${rule.id.padEnd(12)} ${rule.name}`);
284
+ }
285
+
286
+ console.log('');
287
+ const stats = getRuleStats();
288
+ console.log(`Total: ${stats.total} rules | Enabled: ${stats.enabled}`);
289
+ });
290
+
291
+ rulesCmd
292
+ .command('show')
293
+ .description('Show details for a specific rule')
294
+ .argument('<id>', 'Rule ID (e.g., EXFIL-001)')
295
+ .action((id) => {
296
+ const rule = getRuleById(id.toUpperCase());
297
+
298
+ if (!rule) {
299
+ console.error(`Rule not found: ${id}`);
300
+ process.exit(1);
301
+ }
302
+
303
+ console.log(`\nRule: ${rule.id}`);
304
+ console.log('━'.repeat(60));
305
+ console.log(`Name: ${rule.name}`);
306
+ console.log(`Category: ${rule.category}`);
307
+ console.log(`Severity: ${rule.severity}`);
308
+ console.log(`Enabled: ${rule.enabled}`);
309
+ console.log(`Description: ${rule.description}`);
310
+ console.log(`File Types: ${rule.fileTypes.join(', ')}`);
311
+ console.log(`Components: ${rule.components.join(', ')}`);
312
+ console.log(`Remediation: ${rule.remediation}`);
313
+ if (rule.references.length > 0) {
314
+ console.log(`References:`);
315
+ for (const ref of rule.references) {
316
+ console.log(` - ${ref}`);
317
+ }
318
+ }
319
+ console.log(`Patterns: ${rule.patterns.length}`);
320
+ });
321
+
322
+ rulesCmd
323
+ .command('stats')
324
+ .description('Show rule statistics')
325
+ .action(() => {
326
+ const stats = getRuleStats();
327
+
328
+ console.log('\nRule Statistics');
329
+ console.log('━'.repeat(40));
330
+ console.log(`Total Rules: ${stats.total}`);
331
+ console.log(`Enabled: ${stats.enabled}`);
332
+ console.log('');
333
+ console.log('By Category:');
334
+ for (const [cat, count] of Object.entries(stats.byCategory)) {
335
+ console.log(` ${cat}: ${count}`);
336
+ }
337
+ console.log('');
338
+ console.log('By Severity:');
339
+ for (const [sev, count] of Object.entries(stats.bySeverity)) {
340
+ console.log(` ${sev}: ${count}`);
341
+ }
342
+ });
343
+
344
+ // Baseline commands
345
+ const baselineCmd = program
346
+ .command('baseline')
347
+ .description('Manage baseline of accepted findings');
348
+
349
+ baselineCmd
350
+ .command('create')
351
+ .description('Create baseline from current scan results')
352
+ .argument('[path]', 'Path to scan (defaults to AI CLI config directories)')
353
+ .option('-o, --output <file>', 'Output baseline file path')
354
+ .option('--description <desc>', 'Description for the baseline')
355
+ .action(async (path, options) => {
356
+ try {
357
+ // Load configuration for scanning
358
+ const config = loadConfig({ path });
359
+
360
+ if (config.paths.length === 0) {
361
+ console.error('No AI CLI configuration directories found.');
362
+ process.exit(1);
363
+ }
364
+
365
+ console.log('šŸ” Scanning to create baseline...');
366
+ const result = await scan(config);
367
+
368
+ const baselinePath = options.output || getDefaultBaselinePath(config.paths);
369
+ const baseline = createBaseline(result, options.description);
370
+
371
+ saveBaseline(baseline, baselinePath);
372
+ console.log(`āœ… Created baseline with ${baseline.findings.length} findings`);
373
+ console.log(`šŸ“‹ Baseline saved to: ${baselinePath}`);
374
+
375
+ } catch (error) {
376
+ console.error('Error creating baseline:', error.message);
377
+ process.exit(1);
378
+ }
379
+ });
380
+
381
+ baselineCmd
382
+ .command('show')
383
+ .description('Show baseline information')
384
+ .argument('[file]', 'Baseline file path (defaults to .ferret-baseline.json)')
385
+ .action((file) => {
386
+ try {
387
+ const baselinePath = file || getDefaultBaselinePath([process.cwd()]);
388
+ const baseline = loadBaseline(baselinePath);
389
+
390
+ if (!baseline) {
391
+ console.error(`No baseline found at: ${baselinePath}`);
392
+ process.exit(1);
393
+ }
394
+
395
+ const stats = getBaselineStats(baseline);
396
+
397
+ console.log('šŸ“‹ Baseline Information');
398
+ console.log('━'.repeat(60));
399
+ console.log(`File: ${baselinePath}`);
400
+ console.log(`Description: ${baseline.description || 'No description'}`);
401
+ console.log(`Created: ${new Date(baseline.createdDate).toLocaleString()}`);
402
+ console.log(`Updated: ${new Date(baseline.lastUpdated).toLocaleString()}`);
403
+ console.log(`Total Findings: ${stats.totalFindings}`);
404
+ console.log('');
405
+
406
+ if (Object.keys(stats.byRule).length > 0) {
407
+ console.log('By Rule:');
408
+ for (const [rule, count] of Object.entries(stats.byRule)) {
409
+ console.log(` ${rule}: ${count}`);
410
+ }
411
+ console.log('');
412
+ }
413
+
414
+ if (Object.keys(stats.bySeverity).length > 0) {
415
+ console.log('By Category:');
416
+ for (const [category, count] of Object.entries(stats.bySeverity)) {
417
+ console.log(` ${category}: ${count}`);
418
+ }
419
+ }
420
+
421
+ } catch (error) {
422
+ console.error('Error reading baseline:', error.message);
423
+ process.exit(1);
424
+ }
425
+ });
426
+
427
+ baselineCmd
428
+ .command('remove')
429
+ .description('Remove baseline file')
430
+ .argument('[file]', 'Baseline file path (defaults to .ferret-baseline.json)')
431
+ .option('-y, --yes', 'Skip confirmation prompt')
432
+ .action(async (file, options) => {
433
+ try {
434
+ const baselinePath = file || getDefaultBaselinePath([process.cwd()]);
435
+ const baseline = loadBaseline(baselinePath);
436
+
437
+ if (!baseline) {
438
+ console.error(`No baseline found at: ${baselinePath}`);
439
+ process.exit(1);
440
+ }
441
+
442
+ if (!options.yes) {
443
+ // Simple confirmation (in a real implementation, you'd use a proper prompt library)
444
+ console.log(`This will delete the baseline at: ${baselinePath}`);
445
+ console.log('Use --yes to confirm');
446
+ process.exit(1);
447
+ }
448
+
449
+ const { unlinkSync } = await import('node:fs');
450
+ unlinkSync(baselinePath);
451
+ console.log(`āœ… Baseline removed: ${baselinePath}`);
452
+
453
+ } catch (error) {
454
+ console.error('Error removing baseline:', error.message);
455
+ process.exit(1);
456
+ }
457
+ });
458
+
459
+ // Threat Intelligence commands
460
+ const intelCmd = program
461
+ .command('intel')
462
+ .description('Manage threat intelligence');
463
+
464
+ intelCmd
465
+ .command('status')
466
+ .description('Show threat intelligence database status')
467
+ .option('--intel-dir <dir>', 'Threat intelligence directory', '.ferret-intel')
468
+ .action((options) => {
469
+ try {
470
+ const db = loadThreatDatabase(options.intelDir);
471
+ const updateNeeded = needsUpdate(db, 24);
472
+
473
+ console.log('šŸ›”ļø Threat Intelligence Status');
474
+ console.log('━'.repeat(60));
475
+ console.log(`Database Version: ${db.version}`);
476
+ console.log(`Last Updated: ${new Date(db.lastUpdated).toLocaleString()}`);
477
+ console.log(`Total Indicators: ${db.stats.totalIndicators}`);
478
+ console.log(`Update Needed: ${updateNeeded ? 'āš ļø Yes' : 'āœ… No'}`);
479
+ console.log('');
480
+
481
+ console.log('By Type:');
482
+ for (const [type, count] of Object.entries(db.stats.byType)) {
483
+ if (count > 0) {
484
+ console.log(` ${type}: ${count}`);
485
+ }
486
+ }
487
+ console.log('');
488
+
489
+ console.log('By Category:');
490
+ for (const [category, count] of Object.entries(db.stats.byCategory)) {
491
+ console.log(` ${category}: ${count}`);
492
+ }
493
+ console.log('');
494
+
495
+ console.log('Sources:');
496
+ for (const source of db.sources) {
497
+ console.log(` ${source.enabled ? 'āœ…' : 'āŒ'} ${source.name}: ${source.description}`);
498
+ }
499
+
500
+ } catch (error) {
501
+ console.error('Error loading threat intelligence:', error.message);
502
+ process.exit(1);
503
+ }
504
+ });
505
+
506
+ intelCmd
507
+ .command('search')
508
+ .description('Search threat intelligence indicators')
509
+ .argument('<query>', 'Search term')
510
+ .option('--intel-dir <dir>', 'Threat intelligence directory', '.ferret-intel')
511
+ .option('--limit <num>', 'Maximum results', '20')
512
+ .action((query, options) => {
513
+ try {
514
+ const db = loadThreatDatabase(options.intelDir);
515
+ const results = searchIndicators(db, query);
516
+ const limit = parseInt(options.limit, 10);
517
+
518
+ console.log(`šŸ” Found ${results.length} indicators matching "${query}"`);
519
+ console.log('━'.repeat(60));
520
+
521
+ for (const indicator of results.slice(0, limit)) {
522
+ console.log(`[${indicator.severity.toUpperCase()}] ${indicator.type}: ${indicator.value}`);
523
+ console.log(` ${indicator.description}`);
524
+ console.log(` Tags: ${indicator.tags.join(', ')}`);
525
+ console.log(` Confidence: ${indicator.confidence}%`);
526
+ console.log('');
527
+ }
528
+
529
+ if (results.length > limit) {
530
+ console.log(`... and ${results.length - limit} more results`);
531
+ }
532
+
533
+ } catch (error) {
534
+ console.error('Error searching threat intelligence:', error.message);
535
+ process.exit(1);
536
+ }
537
+ });
538
+
539
+ intelCmd
540
+ .command('add')
541
+ .description('Add threat intelligence indicator')
542
+ .option('--type <type>', 'Indicator type (domain, ip, hash, package, pattern)', 'pattern')
543
+ .option('--value <value>', 'Indicator value', true)
544
+ .option('--category <category>', 'Threat category', 'unknown')
545
+ .option('--severity <severity>', 'Severity level', 'medium')
546
+ .option('--description <desc>', 'Description', '')
547
+ .option('--confidence <num>', 'Confidence level (0-100)', '75')
548
+ .option('--tags <tags>', 'Comma-separated tags', '')
549
+ .option('--intel-dir <dir>', 'Threat intelligence directory', '.ferret-intel')
550
+ .action((options) => {
551
+ try {
552
+ if (!options.value) {
553
+ console.error('Error: --value is required');
554
+ process.exit(1);
555
+ }
556
+
557
+ const db = loadThreatDatabase(options.intelDir);
558
+
559
+ const newIndicator = {
560
+ value: options.value,
561
+ type: options.type,
562
+ category: options.category,
563
+ severity: options.severity,
564
+ description: options.description || `Custom ${options.type} indicator`,
565
+ source: 'user-added',
566
+ confidence: parseInt(options.confidence, 10),
567
+ tags: options.tags ? options.tags.split(',').map(t => t.trim()) : [],
568
+ metadata: { addedBy: 'ferret-cli' }
569
+ };
570
+
571
+ const updatedDb = addIndicators(db, [newIndicator]);
572
+ saveThreatDatabase(updatedDb, options.intelDir);
573
+
574
+ console.log('āœ… Added threat intelligence indicator:');
575
+ console.log(` Type: ${newIndicator.type}`);
576
+ console.log(` Value: ${newIndicator.value}`);
577
+ console.log(` Severity: ${newIndicator.severity}`);
578
+ console.log(` Confidence: ${newIndicator.confidence}%`);
579
+
580
+ } catch (error) {
581
+ console.error('Error adding indicator:', error.message);
582
+ process.exit(1);
583
+ }
584
+ });
585
+
586
+ // Remediation commands
587
+ const fixCmd = program
588
+ .command('fix')
589
+ .description('Auto-remediation and quarantine management');
590
+
591
+ fixCmd
592
+ .command('scan')
593
+ .description('Scan and apply automatic fixes')
594
+ .argument('[path]', 'Path to scan (defaults to AI CLI config directories)')
595
+ .option('--dry-run', 'Preview fixes without applying them')
596
+ .option('--safe-only', 'Only apply safe fixes (safety >= 0.8)', true)
597
+ .option('--backup-dir <dir>', 'Backup directory', '.ferret-backups')
598
+ .option('--auto-quarantine', 'Automatically quarantine high-risk files')
599
+ .option('-v, --verbose', 'Verbose output')
600
+ .action(async (path, options) => {
601
+ try {
602
+ logger.configure({
603
+ verbose: options.verbose,
604
+ level: options.verbose ? 'debug' : 'info',
605
+ });
606
+
607
+ // Run scan first
608
+ const config = loadConfig({
609
+ path: path,
610
+ verbose: options.verbose,
611
+ format: 'json'
612
+ });
613
+
614
+ if (config.paths.length === 0) {
615
+ console.error('No AI CLI configuration directories found.');
616
+ process.exit(1);
617
+ }
618
+
619
+ console.log('šŸ” Scanning for security issues...');
620
+ const result = await scan(config);
621
+
622
+ if (result.findings.length === 0) {
623
+ console.log('āœ… No security issues found');
624
+ return;
625
+ }
626
+
627
+ console.log(`\nFound ${result.findings.length} security issues`);
628
+
629
+ // Filter findings that can be auto-remediated
630
+ const fixableFindings = result.findings.filter(finding => canAutoRemediate(finding));
631
+
632
+ if (fixableFindings.length === 0) {
633
+ console.log('āš ļø No findings can be automatically remediated');
634
+ return;
635
+ }
636
+
637
+ console.log(`šŸ“‹ ${fixableFindings.length} findings can be automatically fixed\n`);
638
+
639
+ if (options.dryRun) {
640
+ console.log('šŸ” DRY RUN - Previewing fixes:\n');
641
+
642
+ for (const finding of fixableFindings) {
643
+ const preview = await previewRemediation(finding);
644
+ console.log(`[${finding.severity}] ${finding.ruleId} - ${finding.relativePath}:${finding.line}`);
645
+ console.log(` Issue: ${finding.match}`);
646
+
647
+ if (preview.preview) {
648
+ console.log(` Before: ${preview.preview.originalLine.trim()}`);
649
+ console.log(` After: ${preview.preview.fixedLine.trim()}`);
650
+ }
651
+
652
+ console.log(` Fix: ${preview.fixes[0]?.description || 'No description'}\n`);
653
+ }
654
+
655
+ console.log(`Use 'ferret fix scan ${path || '.'} --verbose' to apply these fixes`);
656
+ return;
657
+ }
658
+
659
+ // Apply remediation
660
+ console.log('šŸ”§ Applying automatic fixes...');
661
+
662
+ const remediationOptions = {
663
+ createBackups: true,
664
+ backupDir: options.backupDir,
665
+ safeOnly: options.safeOnly,
666
+ dryRun: false
667
+ };
668
+
669
+ const results = await applyRemediationBatch(fixableFindings, remediationOptions);
670
+ const successful = results.filter(r => r.success);
671
+
672
+ console.log(`\nāœ… Applied ${successful.length}/${results.length} fixes successfully`);
673
+
674
+ if (successful.length > 0) {
675
+ console.log('\nFixed issues:');
676
+ for (const result of successful) {
677
+ console.log(` āœ“ ${result.finding.relativePath}:${result.finding.line} - ${result.fixApplied?.description}`);
678
+ }
679
+ }
680
+
681
+ const failed = results.filter(r => !r.success);
682
+ if (failed.length > 0) {
683
+ console.log('\nFailed fixes:');
684
+ for (const result of failed) {
685
+ console.log(` āœ— ${result.finding.relativePath}:${result.finding.line} - ${result.error}`);
686
+ }
687
+ }
688
+
689
+ // Auto-quarantine high-risk files if enabled
690
+ if (options.autoQuarantine) {
691
+ const highRiskFindings = result.findings.filter(f =>
692
+ f.severity === 'CRITICAL' && f.riskScore >= 90
693
+ );
694
+
695
+ if (highRiskFindings.length > 0) {
696
+ console.log(`\nšŸ”’ Auto-quarantining ${highRiskFindings.length} high-risk files...`);
697
+
698
+ const quarantinedFiles = new Set();
699
+ for (const finding of highRiskFindings) {
700
+ if (!quarantinedFiles.has(finding.file)) {
701
+ const entry = quarantineFile(
702
+ finding.file,
703
+ highRiskFindings.filter(f => f.file === finding.file),
704
+ 'Auto-quarantine: High-risk security findings'
705
+ );
706
+
707
+ if (entry) {
708
+ quarantinedFiles.add(finding.file);
709
+ console.log(` šŸ”’ Quarantined: ${finding.relativePath}`);
710
+ }
711
+ }
712
+ }
713
+ }
714
+ }
715
+
716
+ } catch (error) {
717
+ console.error('Error during auto-remediation:', error.message);
718
+ if (options.verbose) {
719
+ console.error(error);
720
+ }
721
+ process.exit(1);
722
+ }
723
+ });
724
+
725
+ fixCmd
726
+ .command('quarantine')
727
+ .description('Manage quarantined files')
728
+ .option('--list', 'List quarantined files')
729
+ .option('--restore <id>', 'Restore quarantined file by ID')
730
+ .option('--stats', 'Show quarantine statistics')
731
+ .option('--health', 'Check quarantine health')
732
+ .option('--quarantine-dir <dir>', 'Quarantine directory', '.ferret-quarantine')
733
+ .action((options) => {
734
+ try {
735
+ if (options.list) {
736
+ const entries = listQuarantinedFiles(options.quarantineDir);
737
+
738
+ if (entries.length === 0) {
739
+ console.log('No quarantined files found');
740
+ return;
741
+ }
742
+
743
+ console.log('šŸ”’ Quarantined Files:');
744
+ console.log('━'.repeat(80));
745
+
746
+ for (const entry of entries) {
747
+ const status = entry.restored ? 'ā™»ļø Restored' : 'šŸ”’ Quarantined';
748
+ console.log(`${status} | ${entry.id}`);
749
+ console.log(` Original: ${entry.originalPath}`);
750
+ console.log(` Date: ${new Date(entry.quarantineDate).toLocaleString()}`);
751
+ console.log(` Reason: ${entry.reason}`);
752
+ console.log(` Risk: ${entry.metadata.severity} (${entry.metadata.riskScore}/100)`);
753
+ console.log(` Findings: ${entry.findings.length}`);
754
+ console.log('');
755
+ }
756
+ } else if (options.restore) {
757
+ const success = restoreQuarantinedFile(options.restore, options.quarantineDir);
758
+
759
+ if (success) {
760
+ console.log(`āœ… Restored quarantined file: ${options.restore}`);
761
+ } else {
762
+ console.error(`āŒ Failed to restore file: ${options.restore}`);
763
+ process.exit(1);
764
+ }
765
+ } else if (options.stats) {
766
+ const stats = getQuarantineStats(options.quarantineDir);
767
+
768
+ console.log('šŸ“Š Quarantine Statistics:');
769
+ console.log('━'.repeat(40));
770
+ console.log(`Total Quarantined: ${stats.totalQuarantined}`);
771
+ console.log(`Total Restored: ${stats.totalRestored}`);
772
+ console.log('');
773
+
774
+ if (Object.keys(stats.byCategory).length > 0) {
775
+ console.log('By Category:');
776
+ for (const [category, count] of Object.entries(stats.byCategory)) {
777
+ console.log(` ${category}: ${count}`);
778
+ }
779
+ console.log('');
780
+ }
781
+
782
+ if (Object.keys(stats.bySeverity).length > 0) {
783
+ console.log('By Severity:');
784
+ for (const [severity, count] of Object.entries(stats.bySeverity)) {
785
+ console.log(` ${severity}: ${count}`);
786
+ }
787
+ }
788
+ } else if (options.health) {
789
+ const health = checkQuarantineHealth(options.quarantineDir);
790
+
791
+ console.log(`šŸ„ Quarantine Health: ${health.healthy ? 'āœ… Healthy' : 'āš ļø Issues Found'}`);
792
+
793
+ if (health.issues.length > 0) {
794
+ console.log('\nIssues:');
795
+ for (const issue of health.issues) {
796
+ console.log(` āš ļø ${issue}`);
797
+ }
798
+ }
799
+
800
+ console.log(`\nTotal Files: ${health.stats.totalQuarantined}`);
801
+ console.log(`Restored: ${health.stats.totalRestored}`);
802
+ } else {
803
+ console.log('Use --list, --restore <id>, --stats, or --health');
804
+ }
805
+
806
+ } catch (error) {
807
+ console.error('Error managing quarantine:', error.message);
808
+ process.exit(1);
809
+ }
810
+ });
811
+
812
+ // Version command
813
+ program
814
+ .command('version')
815
+ .description('Show version information')
816
+ .action(() => {
817
+ console.log(`Ferret v${packageJson.version}`);
818
+ console.log('Security scanner for AI CLI configurations');
819
+ });
820
+
821
+ // Parse and run
822
+ program.parse();