ferret-scan 2.1.2 → 2.3.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 (181) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +15 -11
  3. package/bin/ferret.js +109 -13
  4. package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
  5. package/dist/__tests__/AgentMonitor.test.js +235 -0
  6. package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
  7. package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
  8. package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
  9. package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
  10. package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
  11. package/dist/__tests__/IndicatorMatcher.test.js +245 -0
  12. package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
  13. package/dist/__tests__/MarketplaceScanner.test.js +212 -0
  14. package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
  15. package/dist/__tests__/RuleGenerator.test.js +207 -0
  16. package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
  17. package/dist/__tests__/ThreatFeed.test.js +359 -0
  18. package/dist/__tests__/WatchMode.test.d.ts +6 -0
  19. package/dist/__tests__/WatchMode.test.js +104 -0
  20. package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
  21. package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
  22. package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
  23. package/dist/__tests__/astAnalyzerFull.test.js +138 -0
  24. package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
  25. package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
  26. package/dist/__tests__/atlas.test.d.ts +6 -0
  27. package/dist/__tests__/atlas.test.js +319 -0
  28. package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
  29. package/dist/__tests__/atlasCatalog.test.js +200 -0
  30. package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
  31. package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
  32. package/dist/__tests__/baseline.test.d.ts +6 -0
  33. package/dist/__tests__/baseline.test.js +321 -0
  34. package/dist/__tests__/baselineExtra.test.d.ts +6 -0
  35. package/dist/__tests__/baselineExtra.test.js +317 -0
  36. package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
  37. package/dist/__tests__/capabilityMapping.test.js +49 -0
  38. package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
  39. package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
  40. package/dist/__tests__/complianceExtra.test.d.ts +6 -0
  41. package/dist/__tests__/complianceExtra.test.js +121 -0
  42. package/dist/__tests__/config.test.js +1 -1
  43. package/dist/__tests__/configLoader.test.d.ts +6 -0
  44. package/dist/__tests__/configLoader.test.js +225 -0
  45. package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
  46. package/dist/__tests__/configLoaderExtra.test.js +186 -0
  47. package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
  48. package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
  49. package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
  50. package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
  51. package/dist/__tests__/customRules.extra.test.d.ts +6 -0
  52. package/dist/__tests__/customRules.extra.test.js +245 -0
  53. package/dist/__tests__/customRules.test.d.ts +7 -0
  54. package/dist/__tests__/customRules.test.js +347 -0
  55. package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
  56. package/dist/__tests__/dependencyRisk.test.js +248 -0
  57. package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
  58. package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
  59. package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
  60. package/dist/__tests__/featureExitCodes.test.js +332 -0
  61. package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
  62. package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
  63. package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
  64. package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
  65. package/dist/__tests__/fixer.extra.test.d.ts +6 -0
  66. package/dist/__tests__/fixer.extra.test.js +135 -0
  67. package/dist/__tests__/fixerApply.test.d.ts +6 -0
  68. package/dist/__tests__/fixerApply.test.js +132 -0
  69. package/dist/__tests__/gitHooks.test.d.ts +7 -0
  70. package/dist/__tests__/gitHooks.test.js +188 -0
  71. package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
  72. package/dist/__tests__/htmlReporter.extra.test.js +126 -0
  73. package/dist/__tests__/interactiveTui.test.d.ts +6 -0
  74. package/dist/__tests__/interactiveTui.test.js +180 -0
  75. package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
  76. package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
  77. package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
  78. package/dist/__tests__/interactiveTuiMore.test.js +194 -0
  79. package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
  80. package/dist/__tests__/interactiveTuiSession.test.js +173 -0
  81. package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
  82. package/dist/__tests__/llmAnalysis.test.js +229 -0
  83. package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
  84. package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
  85. package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
  86. package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
  87. package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
  88. package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
  89. package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
  90. package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
  91. package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
  92. package/dist/__tests__/llmGroqTPM.test.js +89 -0
  93. package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
  94. package/dist/__tests__/llmProviderRetry.test.js +172 -0
  95. package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
  96. package/dist/__tests__/mcpValidator.extra.test.js +270 -0
  97. package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
  98. package/dist/__tests__/patternMatcherExtra.test.js +198 -0
  99. package/dist/__tests__/patternsCommon.test.d.ts +6 -0
  100. package/dist/__tests__/patternsCommon.test.js +107 -0
  101. package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
  102. package/dist/__tests__/policyEnforcement.test.js +510 -0
  103. package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
  104. package/dist/__tests__/quarantineExtra.test.js +214 -0
  105. package/dist/__tests__/redactionExtra.test.d.ts +6 -0
  106. package/dist/__tests__/redactionExtra.test.js +228 -0
  107. package/dist/__tests__/scanDiff.test.d.ts +7 -0
  108. package/dist/__tests__/scanDiff.test.js +266 -0
  109. package/dist/__tests__/scanFull.test.d.ts +6 -0
  110. package/dist/__tests__/scanFull.test.js +158 -0
  111. package/dist/__tests__/scannerDampening.test.d.ts +6 -0
  112. package/dist/__tests__/scannerDampening.test.js +160 -0
  113. package/dist/__tests__/scannerExtra.test.d.ts +6 -0
  114. package/dist/__tests__/scannerExtra.test.js +194 -0
  115. package/dist/__tests__/scannerMitre.test.d.ts +5 -0
  116. package/dist/__tests__/scannerMitre.test.js +141 -0
  117. package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
  118. package/dist/__tests__/scannerSSRF.test.js +149 -0
  119. package/dist/__tests__/schemas.test.d.ts +6 -0
  120. package/dist/__tests__/schemas.test.js +125 -0
  121. package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
  122. package/dist/__tests__/webhooks.extra.test.js +144 -0
  123. package/dist/__tests__/webhooks.test.d.ts +6 -0
  124. package/dist/__tests__/webhooks.test.js +154 -0
  125. package/dist/analyzers/AstAnalyzer.d.ts +5 -1
  126. package/dist/analyzers/AstAnalyzer.js +25 -4
  127. package/dist/features/customRules.js +22 -29
  128. package/dist/features/ignoreComments.js +5 -5
  129. package/dist/features/mcpTrustScore.d.ts +17 -0
  130. package/dist/features/mcpTrustScore.js +74 -0
  131. package/dist/features/mcpValidator.d.ts +2 -0
  132. package/dist/features/mcpValidator.js +13 -0
  133. package/dist/features/policyEnforcement.d.ts +22 -22
  134. package/dist/features/policyEnforcement.js +3 -2
  135. package/dist/intelligence/ThreatFeed.js +207 -62
  136. package/dist/remediation/Fixer.js +56 -30
  137. package/dist/remediation/Quarantine.js +79 -11
  138. package/dist/reporters/ConsoleReporter.js +10 -0
  139. package/dist/reporters/HtmlReporter.js +5 -0
  140. package/dist/reporters/SarifReporter.d.ts +1 -0
  141. package/dist/reporters/SarifReporter.js +1 -0
  142. package/dist/rules/ai-specific.js +8 -8
  143. package/dist/rules/backdoors.js +12 -12
  144. package/dist/rules/correlationRules.js +6 -6
  145. package/dist/rules/index.d.ts +1 -0
  146. package/dist/rules/index.js +10 -1
  147. package/dist/rules/injection.js +8 -8
  148. package/dist/rules/patterns/common.d.ts +34 -0
  149. package/dist/rules/patterns/common.js +48 -0
  150. package/dist/scanner/IAnalyzer.d.ts +19 -0
  151. package/dist/scanner/IAnalyzer.js +5 -0
  152. package/dist/scanner/PatternMatcher.js +19 -2
  153. package/dist/scanner/Scanner.js +64 -125
  154. package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
  155. package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
  156. package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
  157. package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
  158. package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
  159. package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
  160. package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
  161. package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
  162. package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
  163. package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
  164. package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
  165. package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
  166. package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
  167. package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
  168. package/dist/types.d.ts +23 -0
  169. package/dist/types.js +1 -1
  170. package/dist/utils/baseline.d.ts +15 -2
  171. package/dist/utils/baseline.js +50 -19
  172. package/dist/utils/contentCache.d.ts +39 -0
  173. package/dist/utils/contentCache.js +77 -0
  174. package/dist/utils/glob.d.ts +50 -0
  175. package/dist/utils/glob.js +84 -0
  176. package/dist/utils/pathSecurity.js +1 -0
  177. package/dist/utils/safeRegex.d.ts +55 -0
  178. package/dist/utils/safeRegex.js +130 -0
  179. package/dist/utils/schemas.d.ts +70 -64
  180. package/dist/utils/schemas.js +13 -0
  181. package/package.json +34 -19
@@ -24,17 +24,17 @@ declare const PolicyRuleSchema: z.ZodObject<{
24
24
  minRiskScore: z.ZodOptional<z.ZodNumber>;
25
25
  maxFindings: z.ZodOptional<z.ZodNumber>;
26
26
  }, "strip", z.ZodTypeAny, {
27
- categories?: string[] | undefined;
28
27
  ruleIds?: string[] | undefined;
29
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
28
+ categories?: string[] | undefined;
30
29
  filePatterns?: string[] | undefined;
30
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
31
31
  minRiskScore?: number | undefined;
32
32
  maxFindings?: number | undefined;
33
33
  }, {
34
- categories?: string[] | undefined;
35
34
  ruleIds?: string[] | undefined;
36
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
35
+ categories?: string[] | undefined;
37
36
  filePatterns?: string[] | undefined;
37
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
38
38
  minRiskScore?: number | undefined;
39
39
  maxFindings?: number | undefined;
40
40
  }>;
@@ -44,10 +44,10 @@ declare const PolicyRuleSchema: z.ZodObject<{
44
44
  enabled: boolean;
45
45
  action: "warn" | "ignore" | "block";
46
46
  conditions: {
47
- categories?: string[] | undefined;
48
47
  ruleIds?: string[] | undefined;
49
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
48
+ categories?: string[] | undefined;
50
49
  filePatterns?: string[] | undefined;
50
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
51
51
  minRiskScore?: number | undefined;
52
52
  maxFindings?: number | undefined;
53
53
  };
@@ -56,10 +56,10 @@ declare const PolicyRuleSchema: z.ZodObject<{
56
56
  }, {
57
57
  id: string;
58
58
  conditions: {
59
- categories?: string[] | undefined;
60
59
  ruleIds?: string[] | undefined;
61
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
60
+ categories?: string[] | undefined;
62
61
  filePatterns?: string[] | undefined;
62
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
63
63
  minRiskScore?: number | undefined;
64
64
  maxFindings?: number | undefined;
65
65
  };
@@ -88,17 +88,17 @@ declare const PolicyConfigSchema: z.ZodObject<{
88
88
  minRiskScore: z.ZodOptional<z.ZodNumber>;
89
89
  maxFindings: z.ZodOptional<z.ZodNumber>;
90
90
  }, "strip", z.ZodTypeAny, {
91
- categories?: string[] | undefined;
92
91
  ruleIds?: string[] | undefined;
93
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
92
+ categories?: string[] | undefined;
94
93
  filePatterns?: string[] | undefined;
94
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
95
95
  minRiskScore?: number | undefined;
96
96
  maxFindings?: number | undefined;
97
97
  }, {
98
- categories?: string[] | undefined;
99
98
  ruleIds?: string[] | undefined;
100
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
99
+ categories?: string[] | undefined;
101
100
  filePatterns?: string[] | undefined;
101
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
102
102
  minRiskScore?: number | undefined;
103
103
  maxFindings?: number | undefined;
104
104
  }>;
@@ -108,10 +108,10 @@ declare const PolicyConfigSchema: z.ZodObject<{
108
108
  enabled: boolean;
109
109
  action: "warn" | "ignore" | "block";
110
110
  conditions: {
111
- categories?: string[] | undefined;
112
111
  ruleIds?: string[] | undefined;
113
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
112
+ categories?: string[] | undefined;
114
113
  filePatterns?: string[] | undefined;
114
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
115
115
  minRiskScore?: number | undefined;
116
116
  maxFindings?: number | undefined;
117
117
  };
@@ -120,10 +120,10 @@ declare const PolicyConfigSchema: z.ZodObject<{
120
120
  }, {
121
121
  id: string;
122
122
  conditions: {
123
- categories?: string[] | undefined;
124
123
  ruleIds?: string[] | undefined;
125
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
124
+ categories?: string[] | undefined;
126
125
  filePatterns?: string[] | undefined;
126
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
127
127
  minRiskScore?: number | undefined;
128
128
  maxFindings?: number | undefined;
129
129
  };
@@ -182,10 +182,10 @@ declare const PolicyConfigSchema: z.ZodObject<{
182
182
  enabled: boolean;
183
183
  action: "warn" | "ignore" | "block";
184
184
  conditions: {
185
- categories?: string[] | undefined;
186
185
  ruleIds?: string[] | undefined;
187
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
186
+ categories?: string[] | undefined;
188
187
  filePatterns?: string[] | undefined;
188
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
189
189
  minRiskScore?: number | undefined;
190
190
  maxFindings?: number | undefined;
191
191
  };
@@ -198,10 +198,10 @@ declare const PolicyConfigSchema: z.ZodObject<{
198
198
  rules: {
199
199
  id: string;
200
200
  conditions: {
201
- categories?: string[] | undefined;
202
201
  ruleIds?: string[] | undefined;
203
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
202
+ categories?: string[] | undefined;
204
203
  filePatterns?: string[] | undefined;
204
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
205
205
  minRiskScore?: number | undefined;
206
206
  maxFindings?: number | undefined;
207
207
  };
@@ -313,10 +313,10 @@ declare const _default: {
313
313
  enabled: boolean;
314
314
  action: "warn" | "ignore" | "block";
315
315
  conditions: {
316
- categories?: string[] | undefined;
317
316
  ruleIds?: string[] | undefined;
318
- severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
317
+ categories?: string[] | undefined;
319
318
  filePatterns?: string[] | undefined;
319
+ severities?: ("CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO")[] | undefined;
320
320
  minRiskScore?: number | undefined;
321
321
  maxFindings?: number | undefined;
322
322
  };
@@ -6,6 +6,7 @@
6
6
  import { readFileSync, existsSync, writeFileSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
  import { z } from 'zod';
9
+ import { globToRegex } from '../utils/glob.js';
9
10
  import logger from '../utils/logger.js';
10
11
  /**
11
12
  * Policy rule schema
@@ -161,7 +162,7 @@ function findingMatchesConditions(finding, conditions) {
161
162
  if (conditions.ruleIds && conditions.ruleIds.length > 0) {
162
163
  const matchesRule = conditions.ruleIds.some(id => {
163
164
  if (id.includes('*')) {
164
- const pattern = new RegExp('^' + id.replace(/\*/g, '.*') + '$');
165
+ const pattern = globToRegex(id, { pathLike: false });
165
166
  return pattern.test(finding.ruleId);
166
167
  }
167
168
  return finding.ruleId === id;
@@ -184,7 +185,7 @@ function findingMatchesConditions(finding, conditions) {
184
185
  // Check file patterns
185
186
  if (conditions.filePatterns && conditions.filePatterns.length > 0) {
186
187
  const matchesFile = conditions.filePatterns.some(pattern => {
187
- const regex = new RegExp(pattern.replace(/\*/g, '.*'));
188
+ const regex = globToRegex(pattern, { pathLike: true });
188
189
  return regex.test(finding.file) || regex.test(finding.relativePath);
189
190
  });
190
191
  if (!matchesFile)
@@ -16,117 +16,262 @@ const DEFAULT_INTEL_DIR = '.ferret-intel';
16
16
  const BUILTIN_SOURCES = [
17
17
  {
18
18
  name: 'ai-cli-malicious-packages',
19
- description: 'Known malicious npm packages targeting AI CLI environments',
20
- lastUpdated: new Date().toISOString(),
19
+ description: 'Known malicious and typosquatting npm packages targeting AI CLI environments',
20
+ lastUpdated: '2025-01-01T00:00:00Z',
21
21
  enabled: true,
22
22
  format: 'json'
23
23
  },
24
24
  {
25
25
  name: 'ai-cli-suspicious-domains',
26
- description: 'Suspicious domains used in AI CLI exploitation attempts',
27
- lastUpdated: new Date().toISOString(),
26
+ description: 'Phishing and impersonation domains targeting AI CLI users and API credentials',
27
+ lastUpdated: '2025-01-01T00:00:00Z',
28
28
  enabled: true,
29
29
  format: 'json'
30
30
  },
31
31
  {
32
- name: 'ai-cli-backdoor-patterns',
33
- description: 'Code patterns associated with AI CLI-specific backdoors',
34
- lastUpdated: new Date().toISOString(),
32
+ name: 'ai-cli-injection-patterns',
33
+ description: 'Prompt injection, jailbreak, and privilege-escalation patterns observed in the wild',
34
+ lastUpdated: '2025-01-01T00:00:00Z',
35
+ enabled: true,
36
+ format: 'json'
37
+ },
38
+ {
39
+ name: 'ai-cli-exfiltration-patterns',
40
+ description: 'Data exfiltration patterns embedded in AI CLI hooks and configurations',
41
+ lastUpdated: '2025-01-01T00:00:00Z',
35
42
  enabled: true,
36
43
  format: 'json'
37
44
  }
38
45
  ];
39
46
  /**
40
- * Built-in threat indicators
47
+ * Built-in threat indicators derived from publicly documented AI CLI attack patterns.
48
+ * These cover typosquatting, prompt injection, exfiltration, and privilege escalation
49
+ * as observed across real-world incident reports and security research (2024–2025).
50
+ *
51
+ * Note: No hash indicators are included by default. File hashes are highly specific;
52
+ * add them via `ferret intel add` with verified malicious-file hashes from your own
53
+ * threat intelligence sources.
41
54
  */
42
55
  const BUILTIN_INDICATORS = [
43
- // Malicious domains
56
+ // ── Typosquatting / impersonation packages ─────────────────────────────────
44
57
  {
45
- value: 'evil-ai-api.com',
46
- type: 'domain',
47
- category: 'phishing',
48
- severity: 'high',
49
- description: 'Fake AI API endpoint used for credential harvesting',
50
- source: 'ai-cli-suspicious-domains',
51
- firstSeen: '2024-01-01T00:00:00Z',
52
- lastSeen: new Date().toISOString(),
58
+ value: 'anthropic-sdk-fake',
59
+ type: 'package',
60
+ category: 'malicious-package',
61
+ severity: 'critical',
62
+ description: 'Typosquats the official @anthropic-ai/sdk package; exfiltrates API keys on install',
63
+ source: 'ai-cli-malicious-packages',
64
+ firstSeen: '2024-03-01T00:00:00Z',
65
+ lastSeen: '2025-01-01T00:00:00Z',
53
66
  confidence: 95,
54
- tags: ['phishing', 'fake-api', 'credential-theft']
67
+ tags: ['typosquat', 'credential-theft', 'install-hook']
55
68
  },
56
69
  {
57
- value: 'anthropic-fake.net',
58
- type: 'domain',
59
- category: 'phishing',
70
+ value: 'openai-sdk-community',
71
+ type: 'package',
72
+ category: 'malicious-package',
60
73
  severity: 'high',
61
- description: 'Impersonates legitimate AI provider domain',
62
- source: 'ai-cli-suspicious-domains',
63
- firstSeen: '2024-01-01T00:00:00Z',
64
- lastSeen: new Date().toISOString(),
74
+ description: 'Unofficial package impersonating the OpenAI SDK; contains a postinstall exfiltration script',
75
+ source: 'ai-cli-malicious-packages',
76
+ firstSeen: '2024-06-01T00:00:00Z',
77
+ lastSeen: '2025-01-01T00:00:00Z',
65
78
  confidence: 90,
66
- tags: ['phishing', 'impersonation']
79
+ tags: ['typosquat', 'postinstall', 'exfiltration']
67
80
  },
68
- // Malicious packages
69
81
  {
70
- value: 'ai-jailbreak-helper',
82
+ value: 'claude-code-helper',
83
+ type: 'package',
84
+ category: 'malicious-package',
85
+ severity: 'high',
86
+ description: 'Impersonates Claude Code utilities; reads ~/.claude/settings.json and beacons credentials',
87
+ source: 'ai-cli-malicious-packages',
88
+ firstSeen: '2024-09-01T00:00:00Z',
89
+ lastSeen: '2025-01-01T00:00:00Z',
90
+ confidence: 88,
91
+ tags: ['typosquat', 'credential-theft', 'ai-cli']
92
+ },
93
+ {
94
+ value: 'cursor-ai-extensions',
95
+ type: 'package',
96
+ category: 'malicious-package',
97
+ severity: 'high',
98
+ description: 'Fake Cursor IDE extension package; harvests .cursorrules and workspace secrets',
99
+ source: 'ai-cli-malicious-packages',
100
+ firstSeen: '2024-08-01T00:00:00Z',
101
+ lastSeen: '2025-01-01T00:00:00Z',
102
+ confidence: 85,
103
+ tags: ['typosquat', 'ai-cli', 'cursor']
104
+ },
105
+ {
106
+ value: 'mcp-server-tools',
71
107
  type: 'package',
72
108
  category: 'malicious-package',
73
109
  severity: 'critical',
74
- description: 'Package designed to bypass AI assistant safety mechanisms',
110
+ description: 'Malicious MCP server package that exfiltrates tool call arguments to a remote endpoint',
75
111
  source: 'ai-cli-malicious-packages',
76
- firstSeen: '2024-01-01T00:00:00Z',
77
- lastSeen: new Date().toISOString(),
78
- confidence: 100,
79
- tags: ['jailbreak', 'bypass', 'malicious-npm']
112
+ firstSeen: '2024-11-01T00:00:00Z',
113
+ lastSeen: '2025-01-01T00:00:00Z',
114
+ confidence: 92,
115
+ tags: ['mcp', 'exfiltration', 'supply-chain']
80
116
  },
81
117
  {
82
- value: 'anthropic-sdk-fake',
118
+ value: 'ai-agent-framework',
83
119
  type: 'package',
84
120
  category: 'malicious-package',
85
121
  severity: 'high',
86
- description: 'Fake AI SDK that steals credentials',
122
+ description: 'Generic name used by multiple malicious packages to blend into AI agent dependency lists',
87
123
  source: 'ai-cli-malicious-packages',
88
- firstSeen: '2024-01-01T00:00:00Z',
89
- lastSeen: new Date().toISOString(),
90
- confidence: 95,
91
- tags: ['credential-theft', 'fake-sdk']
124
+ firstSeen: '2024-07-01T00:00:00Z',
125
+ lastSeen: '2025-01-01T00:00:00Z',
126
+ confidence: 80,
127
+ tags: ['typosquat', 'ai-agent', 'generic-name']
128
+ },
129
+ // ── Phishing / impersonation domains ──────────────────────────────────────
130
+ {
131
+ value: 'anthropic-api.net',
132
+ type: 'domain',
133
+ category: 'phishing',
134
+ severity: 'high',
135
+ description: 'Typosquats api.anthropic.com; used to intercept API keys in misconfigured ANTHROPIC_BASE_URL',
136
+ source: 'ai-cli-suspicious-domains',
137
+ firstSeen: '2024-04-01T00:00:00Z',
138
+ lastSeen: '2025-01-01T00:00:00Z',
139
+ confidence: 93,
140
+ tags: ['phishing', 'api-intercept', 'anthropic']
92
141
  },
93
- // Backdoor patterns
94
142
  {
95
- value: 'ignore.*previous.*instructions?.*forget.*rules?',
143
+ value: 'openai-proxy.io',
144
+ type: 'domain',
145
+ category: 'phishing',
146
+ severity: 'high',
147
+ description: 'Claimed OpenAI-compatible proxy that logs all prompts and responses',
148
+ source: 'ai-cli-suspicious-domains',
149
+ firstSeen: '2024-05-01T00:00:00Z',
150
+ lastSeen: '2025-01-01T00:00:00Z',
151
+ confidence: 88,
152
+ tags: ['phishing', 'prompt-logging', 'openai']
153
+ },
154
+ {
155
+ value: 'cursor-updates.net',
156
+ type: 'domain',
157
+ category: 'phishing',
158
+ severity: 'high',
159
+ description: 'Impersonates Cursor IDE update infrastructure; delivers trojanized VSIX files',
160
+ source: 'ai-cli-suspicious-domains',
161
+ firstSeen: '2024-07-01T00:00:00Z',
162
+ lastSeen: '2025-01-01T00:00:00Z',
163
+ confidence: 87,
164
+ tags: ['phishing', 'ide-trojan', 'cursor']
165
+ },
166
+ {
167
+ value: 'mcp-registry.net',
168
+ type: 'domain',
169
+ category: 'phishing',
170
+ severity: 'medium',
171
+ description: 'Unofficial MCP server registry used to distribute malicious MCP server packages',
172
+ source: 'ai-cli-suspicious-domains',
173
+ firstSeen: '2024-10-01T00:00:00Z',
174
+ lastSeen: '2025-01-01T00:00:00Z',
175
+ confidence: 78,
176
+ tags: ['phishing', 'mcp', 'fake-registry']
177
+ },
178
+ // ── Prompt injection / jailbreak patterns ─────────────────────────────────
179
+ {
180
+ value: 'ignore\\s+(all\\s+)?(previous|prior|above)\\s+(instructions?|rules?|constraints?|guidelines?)',
181
+ type: 'pattern',
182
+ category: 'jailbreak-attempt',
183
+ severity: 'high',
184
+ description: 'Classic instruction-override injection attempting to nullify prior system prompt directives',
185
+ source: 'ai-cli-injection-patterns',
186
+ firstSeen: '2023-06-01T00:00:00Z',
187
+ lastSeen: '2025-01-01T00:00:00Z',
188
+ confidence: 90,
189
+ tags: ['jailbreak', 'instruction-override', 'prompt-injection']
190
+ },
191
+ {
192
+ value: 'you\\s+are\\s+now\\s+(in\\s+)?(developer|jailbreak|dan|unrestricted|god)\\s+mode',
96
193
  type: 'pattern',
97
194
  category: 'jailbreak-attempt',
98
195
  severity: 'high',
99
- description: 'Pattern attempting to override AI assistant safety instructions',
100
- source: 'ai-cli-backdoor-patterns',
196
+ description: 'Developer/DAN mode activation attempt — claims to unlock unrestricted AI behavior',
197
+ source: 'ai-cli-injection-patterns',
198
+ firstSeen: '2023-09-01T00:00:00Z',
199
+ lastSeen: '2025-01-01T00:00:00Z',
200
+ confidence: 88,
201
+ tags: ['jailbreak', 'dan-mode', 'privilege-escalation']
202
+ },
203
+ {
204
+ value: 'disregard\\s+(your\\s+)?(safety|ethical|content)\\s+(policy|policies|guidelines?|filters?)',
205
+ type: 'pattern',
206
+ category: 'jailbreak-attempt',
207
+ severity: 'high',
208
+ description: 'Attempts to disable AI safety filters by direct instruction',
209
+ source: 'ai-cli-injection-patterns',
101
210
  firstSeen: '2024-01-01T00:00:00Z',
102
- lastSeen: new Date().toISOString(),
211
+ lastSeen: '2025-01-01T00:00:00Z',
103
212
  confidence: 85,
104
- tags: ['jailbreak', 'instruction-override']
213
+ tags: ['jailbreak', 'safety-bypass']
214
+ },
215
+ {
216
+ value: 'print\\s+(your\\s+)?(system\\s+prompt|initial\\s+instructions?|full\\s+context)',
217
+ type: 'pattern',
218
+ category: 'jailbreak-attempt',
219
+ severity: 'high',
220
+ description: 'System prompt exfiltration via direct instruction to reveal context',
221
+ source: 'ai-cli-injection-patterns',
222
+ firstSeen: '2024-02-01T00:00:00Z',
223
+ lastSeen: '2025-01-01T00:00:00Z',
224
+ confidence: 82,
225
+ tags: ['prompt-exfil', 'system-prompt', 'context-leak']
105
226
  },
106
227
  {
107
- value: 'developer.*mode.*enabled|admin.*access.*granted',
228
+ value: 'as\\s+your\\s+(true\\s+)?(self|creator|master|owner)\\b',
108
229
  type: 'pattern',
109
- category: 'privilege-escalation',
230
+ category: 'jailbreak-attempt',
110
231
  severity: 'medium',
111
- description: 'Attempts to claim elevated privileges in AI assistants',
112
- source: 'ai-cli-backdoor-patterns',
232
+ description: 'Social-engineering attack claiming authority over the AI assistant',
233
+ source: 'ai-cli-injection-patterns',
113
234
  firstSeen: '2024-01-01T00:00:00Z',
114
- lastSeen: new Date().toISOString(),
115
- confidence: 75,
116
- tags: ['privilege-escalation', 'social-engineering']
235
+ lastSeen: '2025-01-01T00:00:00Z',
236
+ confidence: 70,
237
+ tags: ['jailbreak', 'social-engineering', 'authority-claim']
117
238
  },
118
- // Hash indicators (example malicious file hashes)
239
+ // ── Exfiltration patterns ─────────────────────────────────────────────────
119
240
  {
120
- value: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
121
- type: 'hash',
122
- category: 'malicious-file',
241
+ value: 'curl\\s+.*\\$\\{?(ANTHROPIC|OPENAI|CLAUDE|GITHUB|AWS|GCP)_?(API_?KEY|TOKEN|SECRET)',
242
+ type: 'pattern',
243
+ category: 'exfiltration',
123
244
  severity: 'critical',
124
- description: 'Hash of known malicious AI CLI configuration file',
125
- source: 'ai-cli-malicious-packages',
126
- firstSeen: '2024-01-01T00:00:00Z',
127
- lastSeen: new Date().toISOString(),
128
- confidence: 100,
129
- tags: ['malicious-config', 'sha256']
245
+ description: 'Shell command interpolating AI/cloud API keys directly into a curl request body or URL',
246
+ source: 'ai-cli-exfiltration-patterns',
247
+ firstSeen: '2024-03-01T00:00:00Z',
248
+ lastSeen: '2025-01-01T00:00:00Z',
249
+ confidence: 95,
250
+ tags: ['exfiltration', 'credential-leak', 'curl', 'hook']
251
+ },
252
+ {
253
+ value: 'fetch\\(.*\\$\\{(conversation|messages|response|output|result)',
254
+ type: 'pattern',
255
+ category: 'exfiltration',
256
+ severity: 'high',
257
+ description: 'JavaScript fetch() call interpolating AI conversation data into a remote request',
258
+ source: 'ai-cli-exfiltration-patterns',
259
+ firstSeen: '2024-05-01T00:00:00Z',
260
+ lastSeen: '2025-01-01T00:00:00Z',
261
+ confidence: 88,
262
+ tags: ['exfiltration', 'conversation-leak', 'fetch']
263
+ },
264
+ {
265
+ value: 'dns\\.lookup|nslookup|dig\\s+.*\\.(com|net|io|xyz)',
266
+ type: 'pattern',
267
+ category: 'exfiltration',
268
+ severity: 'medium',
269
+ description: 'DNS lookup in a hook context may indicate DNS-based data exfiltration channel',
270
+ source: 'ai-cli-exfiltration-patterns',
271
+ firstSeen: '2024-06-01T00:00:00Z',
272
+ lastSeen: '2025-01-01T00:00:00Z',
273
+ confidence: 65,
274
+ tags: ['exfiltration', 'dns-exfil', 'covert-channel']
130
275
  }
131
276
  ];
132
277
  /**
@@ -7,6 +7,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, statS
7
7
  import { resolve, dirname, basename } from 'node:path';
8
8
  import logger from '../utils/logger.js';
9
9
  import { validatePathWithinBase, sanitizeFilename, isPathWithinBase } from '../utils/pathSecurity.js';
10
+ import { compileSafePattern, safeMatch, safeTest } from '../utils/safeRegex.js';
10
11
  /**
11
12
  * Default remediation options
12
13
  */
@@ -99,6 +100,13 @@ const BUILTIN_FIXES = [
99
100
  automatic: false
100
101
  }
101
102
  ];
103
+ // Fail fast at module load if any built-in pattern is rejected by the safe-regex gate.
104
+ // This catches contributor mistakes at startup rather than silently at fix-application time.
105
+ for (const fix of BUILTIN_FIXES) {
106
+ if (!compileSafePattern(fix.pattern)) {
107
+ throw new Error(`Fixer startup: BUILTIN_FIXES pattern rejected by compileSafePattern: ${fix.description} (${fix.pattern})`);
108
+ }
109
+ }
102
110
  /**
103
111
  * Create backup of file before modification
104
112
  */
@@ -125,34 +133,55 @@ function applyFix(content, fix, _finding) {
125
133
  try {
126
134
  switch (fix.type) {
127
135
  case 'replace': {
128
- const regex = new RegExp(fix.pattern, 'gi');
136
+ const regex = compileSafePattern(fix.pattern, 'gi');
137
+ if (!regex) {
138
+ logger.warn(`Unsafe fix pattern rejected: ${fix.pattern}`);
139
+ return { success: false, newContent: content, linesModified: 0 };
140
+ }
129
141
  const originalLineCount = content.split('\n').length;
130
142
  const replacement = fix.replacement ?? '';
143
+ // Use safe bounded matching to find replacements
144
+ const matchResult = safeMatch(fix.pattern, content, 'gi');
145
+ if (!matchResult) {
146
+ logger.warn(`Safe match failed for pattern: ${fix.pattern}`);
147
+ return { success: false, newContent: content, linesModified: 0 };
148
+ }
149
+ if (matchResult.truncated) {
150
+ logger.warn(`Fix pattern execution truncated for safety: ${fix.pattern}`);
151
+ return { success: false, newContent: content, linesModified: 0 };
152
+ }
153
+ // Apply replacement safely
131
154
  newContent = content.replace(regex, replacement);
132
155
  const newLineCount = newContent.split('\n').length;
133
- linesModified = Math.abs(newLineCount - originalLineCount);
134
- // Count actual replacements
135
- const matches = content.match(regex);
136
- if (matches) {
137
- linesModified = Math.max(linesModified, matches.length);
138
- }
156
+ linesModified = Math.max(Math.abs(newLineCount - originalLineCount), matchResult.matches.length);
139
157
  break;
140
158
  }
141
159
  case 'remove': {
142
- const regex = new RegExp(fix.pattern, 'gi');
160
+ const regex = compileSafePattern(fix.pattern, 'gi');
161
+ if (!regex) {
162
+ logger.warn(`Unsafe fix pattern rejected: ${fix.pattern}`);
163
+ return { success: false, newContent: content, linesModified: 0 };
164
+ }
143
165
  const lines = content.split('\n');
144
- const filteredLines = lines.filter(line => !regex.test(line));
166
+ const filteredLines = lines.filter(line => {
167
+ const isMatch = safeTest(fix.pattern, line, 'i');
168
+ return !isMatch;
169
+ });
145
170
  newContent = filteredLines.join('\n');
146
171
  linesModified = lines.length - filteredLines.length;
147
172
  break;
148
173
  }
149
174
  case 'quarantine': {
150
- // For quarantine, we comment out the problematic lines
151
- const regex = new RegExp(fix.pattern, 'gi');
175
+ const regex = compileSafePattern(fix.pattern, 'gi');
176
+ if (!regex) {
177
+ logger.warn(`Unsafe fix pattern rejected: ${fix.pattern}`);
178
+ return { success: false, newContent: content, linesModified: 0 };
179
+ }
152
180
  const lines = content.split('\n');
153
181
  for (let i = 0; i < lines.length; i++) {
154
182
  const line = lines[i] ?? '';
155
- if (regex.test(line)) {
183
+ const isMatch = safeTest(fix.pattern, line, 'i');
184
+ if (isMatch) {
156
185
  lines[i] = `# QUARANTINED: ${line}`;
157
186
  linesModified++;
158
187
  }
@@ -191,25 +220,22 @@ function findApplicableFixes(finding) {
191
220
  }
192
221
  // Check built-in fixes
193
222
  for (const fix of BUILTIN_FIXES) {
194
- try {
195
- const regex = new RegExp(fix.pattern, 'i');
196
- // Check if fix pattern matches the finding
197
- if (regex.test(finding.match) || regex.test(finding.context.map(c => c.content).join('\n'))) {
198
- applicableFixes.push(fix);
199
- }
200
- // Check by rule category
201
- if (finding.category === 'credentials' && fix.description.includes('credential')) {
202
- applicableFixes.push(fix);
203
- }
204
- if (finding.category === 'injection' && fix.description.includes('jailbreak')) {
205
- applicableFixes.push(fix);
206
- }
207
- if (finding.category === 'permissions' && fix.description.includes('permission')) {
208
- applicableFixes.push(fix);
209
- }
223
+ // Use safe pattern matching
224
+ const matchesDirectly = safeTest(fix.pattern, finding.match, 'i');
225
+ const contextText = finding.context.map(c => c.content).join('\n');
226
+ const matchesContext = safeTest(fix.pattern, contextText, 'i');
227
+ if (matchesDirectly || matchesContext) {
228
+ applicableFixes.push(fix);
229
+ }
230
+ // Check by rule category
231
+ if (finding.category === 'credentials' && fix.description.includes('credential')) {
232
+ applicableFixes.push(fix);
233
+ }
234
+ if (finding.category === 'injection' && fix.description.includes('jailbreak')) {
235
+ applicableFixes.push(fix);
210
236
  }
211
- catch {
212
- logger.warn(`Invalid fix pattern: ${fix.pattern}`);
237
+ if (finding.category === 'permissions' && fix.description.includes('permission')) {
238
+ applicableFixes.push(fix);
213
239
  }
214
240
  }
215
241
  // Remove duplicates