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
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();
|