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,391 @@
1
+ /**
2
+ * Auto-Remediation Engine - Automated security fix application
3
+ * Provides safe, reversible fixes for common security issues
4
+ */
5
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs';
6
+ import { resolve, dirname, basename } from 'node:path';
7
+ import logger from '../utils/logger.js';
8
+ /**
9
+ * Default remediation options
10
+ */
11
+ const DEFAULT_OPTIONS = {
12
+ createBackups: true,
13
+ backupDir: '.ferret-backups',
14
+ safeOnly: true,
15
+ dryRun: false,
16
+ maxFileSizeMB: 10
17
+ };
18
+ /**
19
+ * Built-in safe fixes for common security issues
20
+ */
21
+ const BUILTIN_FIXES = [
22
+ // Credential exposure fixes
23
+ {
24
+ type: 'replace',
25
+ description: 'Remove hardcoded credentials',
26
+ pattern: '(password|secret|token|key)\\s*[=:]\\s*["\'][^"\']+["\']',
27
+ replacement: '$1="<REDACTED>"',
28
+ safety: 0.9,
29
+ automatic: true
30
+ },
31
+ {
32
+ type: 'replace',
33
+ description: 'Remove API keys from URLs',
34
+ pattern: '(api[_-]?key|token)=([a-zA-Z0-9]+)',
35
+ replacement: '$1=<REDACTED>',
36
+ safety: 0.95,
37
+ automatic: true
38
+ },
39
+ // Dangerous command fixes
40
+ {
41
+ type: 'remove',
42
+ description: 'Remove dangerous shell commands',
43
+ pattern: 'rm\\s+-rf\\s+/',
44
+ replacement: '',
45
+ safety: 1.0,
46
+ automatic: true
47
+ },
48
+ {
49
+ type: 'replace',
50
+ description: 'Replace insecure curl commands',
51
+ pattern: 'curl\\s+(-k|--insecure)',
52
+ replacement: 'curl',
53
+ safety: 0.8,
54
+ automatic: true
55
+ },
56
+ // Permission fixes
57
+ {
58
+ type: 'replace',
59
+ description: 'Replace overly permissive file permissions',
60
+ pattern: 'chmod\\s+777',
61
+ replacement: 'chmod 644',
62
+ safety: 0.7,
63
+ automatic: false
64
+ },
65
+ {
66
+ type: 'replace',
67
+ description: 'Remove sudo without specific commands',
68
+ pattern: 'sudo\\s*$',
69
+ replacement: '# sudo command removed for security',
70
+ safety: 0.6,
71
+ automatic: false
72
+ },
73
+ // Claude-specific fixes
74
+ {
75
+ type: 'remove',
76
+ description: 'Remove jailbreak attempts',
77
+ pattern: 'ignore\\s+(previous\\s+)?instructions?',
78
+ replacement: '',
79
+ safety: 0.9,
80
+ automatic: true
81
+ },
82
+ {
83
+ type: 'remove',
84
+ description: 'Remove capability escalation attempts',
85
+ pattern: '(enable|activate)\\s+(developer|admin|debug)\\s+mode',
86
+ replacement: '',
87
+ safety: 0.85,
88
+ automatic: true
89
+ },
90
+ // Network security fixes
91
+ {
92
+ type: 'replace',
93
+ description: 'Upgrade HTTP URLs to HTTPS',
94
+ pattern: 'http://([^/\\s]+)',
95
+ replacement: 'https://$1',
96
+ safety: 0.7,
97
+ automatic: false
98
+ }
99
+ ];
100
+ /**
101
+ * Create backup of file before modification
102
+ */
103
+ function createBackup(filePath, backupDir) {
104
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
105
+ const fileName = basename(filePath);
106
+ const backupFileName = `${fileName}.backup-${timestamp}`;
107
+ const backupPath = resolve(backupDir, backupFileName);
108
+ // Ensure backup directory exists
109
+ mkdirSync(dirname(backupPath), { recursive: true });
110
+ // Copy file to backup location
111
+ copyFileSync(filePath, backupPath);
112
+ logger.debug(`Created backup: ${backupPath}`);
113
+ return backupPath;
114
+ }
115
+ /**
116
+ * Apply a single fix to file content
117
+ */
118
+ function applyFix(content, fix, _finding) {
119
+ let newContent = content;
120
+ let linesModified = 0;
121
+ try {
122
+ switch (fix.type) {
123
+ case 'replace': {
124
+ const regex = new RegExp(fix.pattern, 'gi');
125
+ const originalLineCount = content.split('\n').length;
126
+ const replacement = fix.replacement ?? '';
127
+ newContent = content.replace(regex, replacement);
128
+ const newLineCount = newContent.split('\n').length;
129
+ linesModified = Math.abs(newLineCount - originalLineCount);
130
+ // Count actual replacements
131
+ const matches = content.match(regex);
132
+ if (matches) {
133
+ linesModified = Math.max(linesModified, matches.length);
134
+ }
135
+ break;
136
+ }
137
+ case 'remove': {
138
+ const regex = new RegExp(fix.pattern, 'gi');
139
+ const lines = content.split('\n');
140
+ const filteredLines = lines.filter(line => !regex.test(line));
141
+ newContent = filteredLines.join('\n');
142
+ linesModified = lines.length - filteredLines.length;
143
+ break;
144
+ }
145
+ case 'quarantine': {
146
+ // For quarantine, we comment out the problematic lines
147
+ const regex = new RegExp(fix.pattern, 'gi');
148
+ const lines = content.split('\n');
149
+ for (let i = 0; i < lines.length; i++) {
150
+ const line = lines[i] ?? '';
151
+ if (regex.test(line)) {
152
+ lines[i] = `# QUARANTINED: ${line}`;
153
+ linesModified++;
154
+ }
155
+ }
156
+ newContent = lines.join('\n');
157
+ break;
158
+ }
159
+ case 'permission-change': {
160
+ // This would need file system operations, not content changes
161
+ logger.warn('Permission changes not implemented for content-based fixes');
162
+ return { success: false, newContent: content, linesModified: 0 };
163
+ }
164
+ }
165
+ return {
166
+ success: newContent !== content,
167
+ newContent,
168
+ linesModified
169
+ };
170
+ }
171
+ catch (error) {
172
+ logger.error(`Error applying fix: ${error instanceof Error ? error.message : String(error)}`);
173
+ return { success: false, newContent: content, linesModified: 0 };
174
+ }
175
+ }
176
+ /**
177
+ * Find applicable fixes for a finding
178
+ */
179
+ function findApplicableFixes(finding) {
180
+ const applicableFixes = [];
181
+ // Check rule-specific fixes first
182
+ if (finding.metadata && 'rule' in finding.metadata) {
183
+ const rule = finding.metadata['rule'];
184
+ if (rule?.remediationFixes) {
185
+ applicableFixes.push(...rule.remediationFixes);
186
+ }
187
+ }
188
+ // Check built-in fixes
189
+ for (const fix of BUILTIN_FIXES) {
190
+ try {
191
+ const regex = new RegExp(fix.pattern, 'i');
192
+ // Check if fix pattern matches the finding
193
+ if (regex.test(finding.match) || regex.test(finding.context.map(c => c.content).join('\n'))) {
194
+ applicableFixes.push(fix);
195
+ }
196
+ // Check by rule category
197
+ if (finding.category === 'credentials' && fix.description.includes('credential')) {
198
+ applicableFixes.push(fix);
199
+ }
200
+ if (finding.category === 'injection' && fix.description.includes('jailbreak')) {
201
+ applicableFixes.push(fix);
202
+ }
203
+ if (finding.category === 'permissions' && fix.description.includes('permission')) {
204
+ applicableFixes.push(fix);
205
+ }
206
+ }
207
+ catch {
208
+ logger.warn(`Invalid fix pattern: ${fix.pattern}`);
209
+ }
210
+ }
211
+ // Remove duplicates
212
+ const uniqueFixes = applicableFixes.filter((fix, index, self) => self.findIndex(f => f.pattern === fix.pattern && f.type === fix.type) === index);
213
+ return uniqueFixes;
214
+ }
215
+ /**
216
+ * Apply automatic remediation to a finding
217
+ */
218
+ export async function applyRemediation(finding, options = {}) {
219
+ const config = { ...DEFAULT_OPTIONS, ...options };
220
+ try {
221
+ // Check file size limits
222
+ const fileSize = (await import('node:fs')).statSync(finding.file).size;
223
+ const fileSizeMB = fileSize / (1024 * 1024);
224
+ if (fileSizeMB > config.maxFileSizeMB) {
225
+ return {
226
+ success: false,
227
+ finding,
228
+ error: `File too large: ${fileSizeMB.toFixed(1)}MB > ${config.maxFileSizeMB}MB`
229
+ };
230
+ }
231
+ // Find applicable fixes
232
+ const applicableFixes = findApplicableFixes(finding);
233
+ if (applicableFixes.length === 0) {
234
+ return {
235
+ success: false,
236
+ finding,
237
+ error: 'No applicable fixes found for this finding'
238
+ };
239
+ }
240
+ // Filter by safety level if safeOnly is enabled
241
+ const safeFixes = config.safeOnly
242
+ ? applicableFixes.filter(fix => fix.safety >= 0.8)
243
+ : applicableFixes;
244
+ if (safeFixes.length === 0) {
245
+ return {
246
+ success: false,
247
+ finding,
248
+ error: 'No safe fixes available for this finding'
249
+ };
250
+ }
251
+ // Select the safest automatic fix
252
+ const bestFix = safeFixes
253
+ .filter(fix => fix.automatic)
254
+ .sort((a, b) => b.safety - a.safety)[0];
255
+ if (!bestFix) {
256
+ return {
257
+ success: false,
258
+ finding,
259
+ error: 'No automatic fixes available'
260
+ };
261
+ }
262
+ // Read current file content
263
+ const content = readFileSync(finding.file, 'utf-8');
264
+ const originalContent = content;
265
+ // Apply the fix
266
+ const fixResult = applyFix(content, bestFix, finding);
267
+ if (!fixResult.success) {
268
+ return {
269
+ success: false,
270
+ finding,
271
+ fixApplied: bestFix,
272
+ error: 'Fix could not be applied to content'
273
+ };
274
+ }
275
+ let backupPath;
276
+ if (!config.dryRun) {
277
+ // Create backup if enabled
278
+ if (config.createBackups) {
279
+ backupPath = createBackup(finding.file, config.backupDir);
280
+ }
281
+ // Write modified content
282
+ writeFileSync(finding.file, fixResult.newContent, 'utf-8');
283
+ logger.info(`Applied fix to ${finding.relativePath}: ${bestFix.description}`);
284
+ }
285
+ else {
286
+ logger.info(`DRY RUN: Would apply fix to ${finding.relativePath}: ${bestFix.description}`);
287
+ }
288
+ return {
289
+ success: true,
290
+ finding,
291
+ fixApplied: bestFix,
292
+ ...(backupPath && { backupPath }),
293
+ changes: {
294
+ linesModified: fixResult.linesModified,
295
+ originalContent,
296
+ newContent: fixResult.newContent
297
+ }
298
+ };
299
+ }
300
+ catch (error) {
301
+ const message = error instanceof Error ? error.message : String(error);
302
+ logger.error(`Error applying remediation to ${finding.relativePath}: ${message}`);
303
+ return {
304
+ success: false,
305
+ finding,
306
+ error: message
307
+ };
308
+ }
309
+ }
310
+ /**
311
+ * Apply remediation to multiple findings
312
+ */
313
+ export async function applyRemediationBatch(findings, options = {}) {
314
+ const results = [];
315
+ logger.info(`Applying remediation to ${findings.length} findings`);
316
+ for (const finding of findings) {
317
+ const result = await applyRemediation(finding, options);
318
+ results.push(result);
319
+ // Add a small delay to avoid overwhelming the file system
320
+ if (findings.length > 10) {
321
+ await new Promise(resolve => setTimeout(resolve, 10));
322
+ }
323
+ }
324
+ const successful = results.filter(r => r.success).length;
325
+ logger.info(`Remediation complete: ${successful}/${findings.length} fixes applied`);
326
+ return results;
327
+ }
328
+ /**
329
+ * Restore file from backup
330
+ */
331
+ export function restoreFromBackup(backupPath, originalPath) {
332
+ try {
333
+ if (!existsSync(backupPath)) {
334
+ logger.error(`Backup file not found: ${backupPath}`);
335
+ return false;
336
+ }
337
+ copyFileSync(backupPath, originalPath);
338
+ logger.info(`Restored ${originalPath} from backup`);
339
+ return true;
340
+ }
341
+ catch (error) {
342
+ logger.error(`Error restoring from backup: ${error instanceof Error ? error.message : String(error)}`);
343
+ return false;
344
+ }
345
+ }
346
+ /**
347
+ * Check if a finding can be automatically remediated
348
+ */
349
+ export function canAutoRemediate(finding) {
350
+ const fixes = findApplicableFixes(finding);
351
+ return fixes.some(fix => fix.automatic && fix.safety >= 0.8);
352
+ }
353
+ /**
354
+ * Get remediation preview without applying changes
355
+ */
356
+ export async function previewRemediation(finding) {
357
+ const fixes = findApplicableFixes(finding);
358
+ const safeFixes = fixes.filter(fix => fix.automatic && fix.safety >= 0.8);
359
+ if (safeFixes.length === 0) {
360
+ return { canFix: false, fixes };
361
+ }
362
+ const bestFix = safeFixes.sort((a, b) => b.safety - a.safety)[0];
363
+ try {
364
+ readFileSync(finding.file, 'utf-8');
365
+ const contextLine = finding.context.find(c => c.isMatch);
366
+ if (contextLine && bestFix) {
367
+ const originalLine = contextLine.content;
368
+ const fixResult = applyFix(originalLine, bestFix, finding);
369
+ return {
370
+ canFix: true,
371
+ fixes: safeFixes,
372
+ preview: {
373
+ originalLine,
374
+ fixedLine: fixResult.newContent
375
+ }
376
+ };
377
+ }
378
+ }
379
+ catch (error) {
380
+ logger.error(`Error creating remediation preview: ${error instanceof Error ? error.message : String(error)}`);
381
+ }
382
+ return { canFix: true, fixes: safeFixes };
383
+ }
384
+ export default {
385
+ applyRemediation,
386
+ applyRemediationBatch,
387
+ restoreFromBackup,
388
+ canAutoRemediate,
389
+ previewRemediation
390
+ };
391
+ //# sourceMappingURL=Fixer.js.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Quarantine System - Safely isolate suspicious files and content
3
+ * Provides reversible quarantine operations with audit trails
4
+ */
5
+ import type { Finding } from '../types.js';
6
+ /**
7
+ * Quarantine entry metadata
8
+ */
9
+ export interface QuarantineEntry {
10
+ id: string;
11
+ originalPath: string;
12
+ quarantinePath: string;
13
+ reason: string;
14
+ findings: Finding[];
15
+ quarantineDate: string;
16
+ fileSize: number;
17
+ fileHash: string;
18
+ restored: boolean;
19
+ restoredDate?: string;
20
+ metadata: {
21
+ originalPermissions?: string;
22
+ riskScore: number;
23
+ severity: string;
24
+ category: string;
25
+ };
26
+ }
27
+ /**
28
+ * Quarantine database
29
+ */
30
+ export interface QuarantineDatabase {
31
+ version: string;
32
+ created: string;
33
+ lastUpdated: string;
34
+ entries: QuarantineEntry[];
35
+ stats: {
36
+ totalQuarantined: number;
37
+ totalRestored: number;
38
+ byCategory: Record<string, number>;
39
+ bySeverity: Record<string, number>;
40
+ };
41
+ }
42
+ /**
43
+ * Quarantine options
44
+ */
45
+ export interface QuarantineOptions {
46
+ quarantineDir: string;
47
+ createBackup: boolean;
48
+ removeOriginal: boolean;
49
+ compressFiles: boolean;
50
+ maxFileSizeMB: number;
51
+ }
52
+ /**
53
+ * Load quarantine database
54
+ */
55
+ export declare function loadQuarantineDatabase(quarantineDir: string): QuarantineDatabase;
56
+ /**
57
+ * Save quarantine database
58
+ */
59
+ export declare function saveQuarantineDatabase(db: QuarantineDatabase, quarantineDir: string): void;
60
+ /**
61
+ * Quarantine a file based on findings
62
+ */
63
+ export declare function quarantineFile(filePath: string, findings: Finding[], reason: string, options?: Partial<QuarantineOptions>): QuarantineEntry | null;
64
+ /**
65
+ * Restore a quarantined file
66
+ */
67
+ export declare function restoreQuarantinedFile(entryId: string, quarantineDir?: string): boolean;
68
+ /**
69
+ * Delete a quarantined file permanently
70
+ */
71
+ export declare function deleteQuarantinedFile(entryId: string, quarantineDir?: string): boolean;
72
+ /**
73
+ * List quarantined files
74
+ */
75
+ export declare function listQuarantinedFiles(quarantineDir?: string): QuarantineEntry[];
76
+ /**
77
+ * Get quarantine statistics
78
+ */
79
+ export declare function getQuarantineStats(quarantineDir?: string): QuarantineDatabase['stats'];
80
+ /**
81
+ * Clean up old quarantine entries
82
+ */
83
+ export declare function cleanupQuarantine(maxAgeDays?: number, quarantineDir?: string): number;
84
+ /**
85
+ * Check quarantine health
86
+ */
87
+ export declare function checkQuarantineHealth(quarantineDir?: string): {
88
+ healthy: boolean;
89
+ issues: string[];
90
+ stats: QuarantineDatabase['stats'];
91
+ };
92
+ declare const _default: {
93
+ quarantineFile: typeof quarantineFile;
94
+ restoreQuarantinedFile: typeof restoreQuarantinedFile;
95
+ deleteQuarantinedFile: typeof deleteQuarantinedFile;
96
+ listQuarantinedFiles: typeof listQuarantinedFiles;
97
+ getQuarantineStats: typeof getQuarantineStats;
98
+ cleanupQuarantine: typeof cleanupQuarantine;
99
+ checkQuarantineHealth: typeof checkQuarantineHealth;
100
+ };
101
+ export default _default;
102
+ //# sourceMappingURL=Quarantine.d.ts.map