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.
- package/CHANGELOG.md +35 -0
- package/README.md +15 -11
- package/bin/ferret.js +109 -13
- package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
- package/dist/__tests__/AgentMonitor.test.js +235 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
- package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
- package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
- package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
- package/dist/__tests__/IndicatorMatcher.test.js +245 -0
- package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
- package/dist/__tests__/MarketplaceScanner.test.js +212 -0
- package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
- package/dist/__tests__/RuleGenerator.test.js +207 -0
- package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
- package/dist/__tests__/ThreatFeed.test.js +359 -0
- package/dist/__tests__/WatchMode.test.d.ts +6 -0
- package/dist/__tests__/WatchMode.test.js +104 -0
- package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
- package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerFull.test.js +138 -0
- package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
- package/dist/__tests__/atlas.test.d.ts +6 -0
- package/dist/__tests__/atlas.test.js +319 -0
- package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalog.test.js +200 -0
- package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
- package/dist/__tests__/baseline.test.d.ts +6 -0
- package/dist/__tests__/baseline.test.js +321 -0
- package/dist/__tests__/baselineExtra.test.d.ts +6 -0
- package/dist/__tests__/baselineExtra.test.js +317 -0
- package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
- package/dist/__tests__/capabilityMapping.test.js +49 -0
- package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
- package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
- package/dist/__tests__/complianceExtra.test.d.ts +6 -0
- package/dist/__tests__/complianceExtra.test.js +121 -0
- package/dist/__tests__/config.test.js +1 -1
- package/dist/__tests__/configLoader.test.d.ts +6 -0
- package/dist/__tests__/configLoader.test.js +225 -0
- package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
- package/dist/__tests__/configLoaderExtra.test.js +186 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
- package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
- package/dist/__tests__/customRules.extra.test.d.ts +6 -0
- package/dist/__tests__/customRules.extra.test.js +245 -0
- package/dist/__tests__/customRules.test.d.ts +7 -0
- package/dist/__tests__/customRules.test.js +347 -0
- package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
- package/dist/__tests__/dependencyRisk.test.js +248 -0
- package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
- package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
- package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
- package/dist/__tests__/featureExitCodes.test.js +332 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
- package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
- package/dist/__tests__/fixer.extra.test.d.ts +6 -0
- package/dist/__tests__/fixer.extra.test.js +135 -0
- package/dist/__tests__/fixerApply.test.d.ts +6 -0
- package/dist/__tests__/fixerApply.test.js +132 -0
- package/dist/__tests__/gitHooks.test.d.ts +7 -0
- package/dist/__tests__/gitHooks.test.js +188 -0
- package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
- package/dist/__tests__/htmlReporter.extra.test.js +126 -0
- package/dist/__tests__/interactiveTui.test.d.ts +6 -0
- package/dist/__tests__/interactiveTui.test.js +180 -0
- package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
- package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiMore.test.js +194 -0
- package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiSession.test.js +173 -0
- package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysis.test.js +229 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
- package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
- package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
- package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
- package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
- package/dist/__tests__/llmGroqTPM.test.js +89 -0
- package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
- package/dist/__tests__/llmProviderRetry.test.js +172 -0
- package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
- package/dist/__tests__/mcpValidator.extra.test.js +270 -0
- package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
- package/dist/__tests__/patternMatcherExtra.test.js +198 -0
- package/dist/__tests__/patternsCommon.test.d.ts +6 -0
- package/dist/__tests__/patternsCommon.test.js +107 -0
- package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
- package/dist/__tests__/policyEnforcement.test.js +510 -0
- package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
- package/dist/__tests__/quarantineExtra.test.js +214 -0
- package/dist/__tests__/redactionExtra.test.d.ts +6 -0
- package/dist/__tests__/redactionExtra.test.js +228 -0
- package/dist/__tests__/scanDiff.test.d.ts +7 -0
- package/dist/__tests__/scanDiff.test.js +266 -0
- package/dist/__tests__/scanFull.test.d.ts +6 -0
- package/dist/__tests__/scanFull.test.js +158 -0
- package/dist/__tests__/scannerDampening.test.d.ts +6 -0
- package/dist/__tests__/scannerDampening.test.js +160 -0
- package/dist/__tests__/scannerExtra.test.d.ts +6 -0
- package/dist/__tests__/scannerExtra.test.js +194 -0
- package/dist/__tests__/scannerMitre.test.d.ts +5 -0
- package/dist/__tests__/scannerMitre.test.js +141 -0
- package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
- package/dist/__tests__/scannerSSRF.test.js +149 -0
- package/dist/__tests__/schemas.test.d.ts +6 -0
- package/dist/__tests__/schemas.test.js +125 -0
- package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
- package/dist/__tests__/webhooks.extra.test.js +144 -0
- package/dist/__tests__/webhooks.test.d.ts +6 -0
- package/dist/__tests__/webhooks.test.js +154 -0
- package/dist/analyzers/AstAnalyzer.d.ts +5 -1
- package/dist/analyzers/AstAnalyzer.js +25 -4
- package/dist/features/customRules.js +22 -29
- package/dist/features/ignoreComments.js +5 -5
- package/dist/features/mcpTrustScore.d.ts +17 -0
- package/dist/features/mcpTrustScore.js +74 -0
- package/dist/features/mcpValidator.d.ts +2 -0
- package/dist/features/mcpValidator.js +13 -0
- package/dist/features/policyEnforcement.d.ts +22 -22
- package/dist/features/policyEnforcement.js +3 -2
- package/dist/intelligence/ThreatFeed.js +207 -62
- package/dist/remediation/Fixer.js +56 -30
- package/dist/remediation/Quarantine.js +79 -11
- package/dist/reporters/ConsoleReporter.js +10 -0
- package/dist/reporters/HtmlReporter.js +5 -0
- package/dist/reporters/SarifReporter.d.ts +1 -0
- package/dist/reporters/SarifReporter.js +1 -0
- package/dist/rules/ai-specific.js +8 -8
- package/dist/rules/backdoors.js +12 -12
- package/dist/rules/correlationRules.js +6 -6
- package/dist/rules/index.d.ts +1 -0
- package/dist/rules/index.js +10 -1
- package/dist/rules/injection.js +8 -8
- package/dist/rules/patterns/common.d.ts +34 -0
- package/dist/rules/patterns/common.js +48 -0
- package/dist/scanner/IAnalyzer.d.ts +19 -0
- package/dist/scanner/IAnalyzer.js +5 -0
- package/dist/scanner/PatternMatcher.js +19 -2
- package/dist/scanner/Scanner.js +64 -125
- package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
- package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
- package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
- package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +1 -1
- package/dist/utils/baseline.d.ts +15 -2
- package/dist/utils/baseline.js +50 -19
- package/dist/utils/contentCache.d.ts +39 -0
- package/dist/utils/contentCache.js +77 -0
- package/dist/utils/glob.d.ts +50 -0
- package/dist/utils/glob.js +84 -0
- package/dist/utils/pathSecurity.js +1 -0
- package/dist/utils/safeRegex.d.ts +55 -0
- package/dist/utils/safeRegex.js +130 -0
- package/dist/utils/schemas.d.ts +70 -64
- package/dist/utils/schemas.js +13 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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:
|
|
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: '
|
|
27
|
-
lastUpdated:
|
|
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-
|
|
33
|
-
description: '
|
|
34
|
-
lastUpdated:
|
|
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
|
-
//
|
|
56
|
+
// ── Typosquatting / impersonation packages ─────────────────────────────────
|
|
44
57
|
{
|
|
45
|
-
value: '
|
|
46
|
-
type: '
|
|
47
|
-
category: '
|
|
48
|
-
severity: '
|
|
49
|
-
description: '
|
|
50
|
-
source: 'ai-cli-
|
|
51
|
-
firstSeen: '2024-
|
|
52
|
-
lastSeen:
|
|
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: ['
|
|
67
|
+
tags: ['typosquat', 'credential-theft', 'install-hook']
|
|
55
68
|
},
|
|
56
69
|
{
|
|
57
|
-
value: '
|
|
58
|
-
type: '
|
|
59
|
-
category: '
|
|
70
|
+
value: 'openai-sdk-community',
|
|
71
|
+
type: 'package',
|
|
72
|
+
category: 'malicious-package',
|
|
60
73
|
severity: 'high',
|
|
61
|
-
description: '
|
|
62
|
-
source: 'ai-cli-
|
|
63
|
-
firstSeen: '2024-
|
|
64
|
-
lastSeen:
|
|
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: ['
|
|
79
|
+
tags: ['typosquat', 'postinstall', 'exfiltration']
|
|
67
80
|
},
|
|
68
|
-
// Malicious packages
|
|
69
81
|
{
|
|
70
|
-
value: '
|
|
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: '
|
|
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-
|
|
77
|
-
lastSeen:
|
|
78
|
-
confidence:
|
|
79
|
-
tags: ['
|
|
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: '
|
|
118
|
+
value: 'ai-agent-framework',
|
|
83
119
|
type: 'package',
|
|
84
120
|
category: 'malicious-package',
|
|
85
121
|
severity: 'high',
|
|
86
|
-
description: '
|
|
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-
|
|
89
|
-
lastSeen:
|
|
90
|
-
confidence:
|
|
91
|
-
tags: ['
|
|
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: '
|
|
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: '
|
|
100
|
-
source: 'ai-cli-
|
|
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:
|
|
211
|
+
lastSeen: '2025-01-01T00:00:00Z',
|
|
103
212
|
confidence: 85,
|
|
104
|
-
tags: ['jailbreak', '
|
|
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: '
|
|
228
|
+
value: 'as\\s+your\\s+(true\\s+)?(self|creator|master|owner)\\b',
|
|
108
229
|
type: 'pattern',
|
|
109
|
-
category: '
|
|
230
|
+
category: 'jailbreak-attempt',
|
|
110
231
|
severity: 'medium',
|
|
111
|
-
description: '
|
|
112
|
-
source: 'ai-cli-
|
|
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:
|
|
115
|
-
confidence:
|
|
116
|
-
tags: ['
|
|
235
|
+
lastSeen: '2025-01-01T00:00:00Z',
|
|
236
|
+
confidence: 70,
|
|
237
|
+
tags: ['jailbreak', 'social-engineering', 'authority-claim']
|
|
117
238
|
},
|
|
118
|
-
//
|
|
239
|
+
// ── Exfiltration patterns ─────────────────────────────────────────────────
|
|
119
240
|
{
|
|
120
|
-
value: '
|
|
121
|
-
type: '
|
|
122
|
-
category: '
|
|
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: '
|
|
125
|
-
source: 'ai-cli-
|
|
126
|
-
firstSeen: '2024-
|
|
127
|
-
lastSeen:
|
|
128
|
-
confidence:
|
|
129
|
-
tags: ['
|
|
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 =
|
|
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 =
|
|
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 =>
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
212
|
-
|
|
237
|
+
if (finding.category === 'permissions' && fix.description.includes('permission')) {
|
|
238
|
+
applicableFixes.push(fix);
|
|
213
239
|
}
|
|
214
240
|
}
|
|
215
241
|
// Remove duplicates
|