ferret-scan 2.2.0 → 2.4.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 +17 -0
- package/README.md +15 -11
- package/bin/ferret.js +104 -8
- 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/features/customRules.js +22 -29
- 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/intelligence/ThreatFeed.js +207 -62
- package/dist/remediation/Quarantine.js +24 -6
- 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/scanner/IAnalyzer.d.ts +19 -0
- package/dist/scanner/IAnalyzer.js +5 -0
- 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 +17 -0
- package/dist/types.js +1 -1
- package/dist/utils/safeRegex.d.ts +12 -51
- package/dist/utils/safeRegex.js +45 -62
- package/dist/utils/schemas.d.ts +64 -64
- package/package.json +25 -19
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Additional Config Loader Tests
|
|
3
|
+
* Tests for loadConfig with LLM and mitreAtlas config file settings
|
|
4
|
+
*/
|
|
5
|
+
import { loadConfig } from '../utils/config.js';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
describe('loadConfig - config file with LLM settings', () => {
|
|
10
|
+
let tmpDir;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-config-extra-'));
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
it('loads LLM settings from config file', () => {
|
|
18
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
19
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
20
|
+
features: {
|
|
21
|
+
llmAnalysis: true,
|
|
22
|
+
},
|
|
23
|
+
llm: {
|
|
24
|
+
provider: 'openai-compatible',
|
|
25
|
+
baseUrl: 'https://api.openai.com/v1/chat/completions',
|
|
26
|
+
model: 'gpt-4o',
|
|
27
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
28
|
+
timeoutMs: 30000,
|
|
29
|
+
jsonMode: true,
|
|
30
|
+
maxInputChars: 8000,
|
|
31
|
+
maxOutputTokens: 1000,
|
|
32
|
+
temperature: 0,
|
|
33
|
+
systemPromptAddendum: 'Extra instructions',
|
|
34
|
+
includeMitreAtlasTechniques: true,
|
|
35
|
+
maxMitreAtlasTechniques: 50,
|
|
36
|
+
cacheDir: '.ferret-cache/llm',
|
|
37
|
+
cacheTtlHours: 168,
|
|
38
|
+
maxRetries: 2,
|
|
39
|
+
retryBackoffMs: 500,
|
|
40
|
+
retryMaxBackoffMs: 5000,
|
|
41
|
+
minRequestIntervalMs: 250,
|
|
42
|
+
onlyIfFindings: true,
|
|
43
|
+
maxFindingsPerFile: 5,
|
|
44
|
+
maxFiles: 10,
|
|
45
|
+
minConfidence: 0.7,
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
const config = loadConfig({ config: configPath });
|
|
49
|
+
expect(config.llm.provider).toBe('openai-compatible');
|
|
50
|
+
expect(config.llm.model).toBe('gpt-4o');
|
|
51
|
+
expect(config.llmAnalysis).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
it('loads mitreAtlasCatalog settings from config file', () => {
|
|
54
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
55
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
56
|
+
mitreAtlasCatalog: {
|
|
57
|
+
enabled: true,
|
|
58
|
+
autoUpdate: true,
|
|
59
|
+
sourceUrl: 'https://example.com/stix-atlas.json',
|
|
60
|
+
cachePath: '.ferret-cache/atlas.json',
|
|
61
|
+
cacheTtlHours: 72,
|
|
62
|
+
timeoutMs: 10000,
|
|
63
|
+
forceRefresh: false,
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
const config = loadConfig({ config: configPath });
|
|
67
|
+
expect(config.mitreAtlasCatalog.enabled).toBe(true);
|
|
68
|
+
expect(config.mitreAtlasCatalog.autoUpdate).toBe(true);
|
|
69
|
+
expect(config.mitreAtlasCatalog.cacheTtlHours).toBe(72);
|
|
70
|
+
});
|
|
71
|
+
it('loads all feature flags from config file', () => {
|
|
72
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
73
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
74
|
+
features: {
|
|
75
|
+
entropyAnalysis: true,
|
|
76
|
+
mcpValidation: true,
|
|
77
|
+
dependencyAnalysis: true,
|
|
78
|
+
dependencyAudit: true,
|
|
79
|
+
capabilityMapping: true,
|
|
80
|
+
ignoreComments: true,
|
|
81
|
+
mitreAtlas: true,
|
|
82
|
+
llmAnalysis: true,
|
|
83
|
+
},
|
|
84
|
+
}));
|
|
85
|
+
const config = loadConfig({ config: configPath });
|
|
86
|
+
expect(config.entropyAnalysis).toBe(true);
|
|
87
|
+
expect(config.mcpValidation).toBe(true);
|
|
88
|
+
expect(config.dependencyAnalysis).toBe(true);
|
|
89
|
+
expect(config.dependencyAudit).toBe(true);
|
|
90
|
+
expect(config.capabilityMapping).toBe(true);
|
|
91
|
+
expect(config.ignoreComments).toBe(true);
|
|
92
|
+
expect(config.mitreAtlas).toBe(true);
|
|
93
|
+
expect(config.llmAnalysis).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it('loads threat intelligence from config file', () => {
|
|
96
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
97
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
98
|
+
threatIntelligence: {
|
|
99
|
+
enabled: true,
|
|
100
|
+
feeds: ['https://example.com/feed'],
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
const config = loadConfig({ config: configPath });
|
|
104
|
+
expect(config.threatIntel).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
it('loads LLM includeMitreAtlasTechniques explicitly', () => {
|
|
107
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
108
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
109
|
+
llm: {
|
|
110
|
+
includeMitreAtlasCatalog: false, // triggers llmIncludeAtlasExplicit
|
|
111
|
+
includeMitreAtlasTechniques: true,
|
|
112
|
+
},
|
|
113
|
+
}));
|
|
114
|
+
const config = loadConfig({ config: configPath });
|
|
115
|
+
expect(config.llm.includeMitreAtlasTechniques).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
it('applies CLI llmMaxInputChars option', () => {
|
|
118
|
+
const config = loadConfig({ llmMaxInputChars: 5000 });
|
|
119
|
+
expect(config.llm.maxInputChars).toBe(5000);
|
|
120
|
+
});
|
|
121
|
+
it('applies CLI llmTimeoutMs option', () => {
|
|
122
|
+
const config = loadConfig({ llmTimeoutMs: 15000 });
|
|
123
|
+
expect(config.llm.timeoutMs).toBe(15000);
|
|
124
|
+
});
|
|
125
|
+
it('applies CLI dependencyAudit option', () => {
|
|
126
|
+
const config = loadConfig({ dependencyAudit: true });
|
|
127
|
+
expect(config.dependencyAudit).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
it('handles config file with customRules as array', () => {
|
|
130
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
131
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
132
|
+
customRules: ['./my-rules.json', './more-rules.yaml'],
|
|
133
|
+
}));
|
|
134
|
+
const config = loadConfig({ config: configPath });
|
|
135
|
+
expect(config.customRules).toBeDefined();
|
|
136
|
+
});
|
|
137
|
+
it('handles config file with customRules as string', () => {
|
|
138
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
139
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
140
|
+
customRules: './my-rules.json',
|
|
141
|
+
}));
|
|
142
|
+
const config = loadConfig({ config: configPath });
|
|
143
|
+
expect(config.customRules.length).toBeGreaterThan(0);
|
|
144
|
+
});
|
|
145
|
+
it('handles config file with failOn', () => {
|
|
146
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
147
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
148
|
+
failOn: 'HIGH',
|
|
149
|
+
}));
|
|
150
|
+
const config = loadConfig({ config: configPath });
|
|
151
|
+
expect(config.failOn).toBe('HIGH');
|
|
152
|
+
});
|
|
153
|
+
it('handles config file with configOnly', () => {
|
|
154
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
155
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
156
|
+
configOnly: true,
|
|
157
|
+
}));
|
|
158
|
+
const config = loadConfig({ config: configPath });
|
|
159
|
+
expect(config.configOnly).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
it('handles config file with marketplaceMode', () => {
|
|
162
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
163
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
164
|
+
marketplaceMode: 'all',
|
|
165
|
+
}));
|
|
166
|
+
const config = loadConfig({ config: configPath });
|
|
167
|
+
expect(config.marketplaceMode).toBe('all');
|
|
168
|
+
});
|
|
169
|
+
it('handles config file with docDampening', () => {
|
|
170
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
171
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
172
|
+
docDampening: false,
|
|
173
|
+
}));
|
|
174
|
+
const config = loadConfig({ config: configPath });
|
|
175
|
+
expect(config.docDampening).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
it('handles config file with redact', () => {
|
|
178
|
+
const configPath = path.join(tmpDir, '.ferretrc.json');
|
|
179
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
180
|
+
redact: true,
|
|
181
|
+
}));
|
|
182
|
+
const config = loadConfig({ config: configPath });
|
|
183
|
+
expect(config.redact).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
//# sourceMappingURL=configLoaderExtra.test.js.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Additional CorrelationAnalyzer Tests
|
|
3
|
+
*/
|
|
4
|
+
import { analyzeCorrelations, shouldAnalyzeCorrelations } from '../analyzers/CorrelationAnalyzer.js';
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as os from 'node:os';
|
|
8
|
+
function makeFile(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
path: '/project/test.md',
|
|
11
|
+
relativePath: 'test.md',
|
|
12
|
+
type: 'md',
|
|
13
|
+
component: 'agent',
|
|
14
|
+
size: 100,
|
|
15
|
+
modified: new Date(),
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function makeRule(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
id: 'TEST-001',
|
|
22
|
+
name: 'Test Rule',
|
|
23
|
+
category: 'injection',
|
|
24
|
+
severity: 'HIGH',
|
|
25
|
+
description: 'Test',
|
|
26
|
+
patterns: [/dangerous/gi],
|
|
27
|
+
fileTypes: ['md'],
|
|
28
|
+
components: ['agent', 'skill', 'hook', 'plugin', 'mcp', 'settings', 'ai-config-md', 'rules-file'],
|
|
29
|
+
remediation: 'Fix it',
|
|
30
|
+
references: [],
|
|
31
|
+
enabled: true,
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
describe('shouldAnalyzeCorrelations', () => {
|
|
36
|
+
it('returns false when correlationAnalysis is disabled', () => {
|
|
37
|
+
const files = [makeFile(), makeFile()];
|
|
38
|
+
expect(shouldAnalyzeCorrelations(files, { correlationAnalysis: false })).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it('returns false with fewer than 2 files', () => {
|
|
41
|
+
expect(shouldAnalyzeCorrelations([makeFile()], { correlationAnalysis: true })).toBe(false);
|
|
42
|
+
expect(shouldAnalyzeCorrelations([], { correlationAnalysis: true })).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('returns true with 2+ files and correlationAnalysis enabled', () => {
|
|
45
|
+
const files = [makeFile(), makeFile(), makeFile()];
|
|
46
|
+
expect(shouldAnalyzeCorrelations(files, { correlationAnalysis: true })).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('analyzeCorrelations', () => {
|
|
50
|
+
let tmpDir;
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-corr-'));
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
it('returns empty array for no files', () => {
|
|
58
|
+
const result = analyzeCorrelations([], [makeRule()]);
|
|
59
|
+
expect(result).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
it('returns empty array for single file', () => {
|
|
62
|
+
const file = makeFile();
|
|
63
|
+
const result = analyzeCorrelations([file], [makeRule()]);
|
|
64
|
+
expect(result).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
it('returns empty array for rules without correlationRules', () => {
|
|
67
|
+
const files = [makeFile(), makeFile()];
|
|
68
|
+
const rules = [makeRule()]; // No correlationRules
|
|
69
|
+
const result = analyzeCorrelations(files, rules);
|
|
70
|
+
expect(result).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
it('analyzes files when correlationRules present', () => {
|
|
73
|
+
// Create two temp files
|
|
74
|
+
const file1Path = path.join(tmpDir, 'agent1.md');
|
|
75
|
+
const file2Path = path.join(tmpDir, 'agent2.md');
|
|
76
|
+
fs.writeFileSync(file1Path, 'secret content');
|
|
77
|
+
fs.writeFileSync(file2Path, 'exfiltration content');
|
|
78
|
+
const file1 = makeFile({ path: file1Path, relativePath: 'agent1.md' });
|
|
79
|
+
const file2 = makeFile({ path: file2Path, relativePath: 'agent2.md' });
|
|
80
|
+
const ruleWithCorrelation = makeRule({
|
|
81
|
+
correlationRules: [{
|
|
82
|
+
id: 'CORR-001',
|
|
83
|
+
description: 'Detects correlated patterns',
|
|
84
|
+
filePatterns: ['*.md'],
|
|
85
|
+
contentPatterns: ['secret', 'exfiltration'],
|
|
86
|
+
maxDistance: 2,
|
|
87
|
+
}],
|
|
88
|
+
});
|
|
89
|
+
const result = analyzeCorrelations([file1, file2], [ruleWithCorrelation]);
|
|
90
|
+
expect(Array.isArray(result)).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it('returns empty array for no rules', () => {
|
|
93
|
+
const files = [makeFile(), makeFile()];
|
|
94
|
+
const result = analyzeCorrelations(files, []);
|
|
95
|
+
expect(result).toHaveLength(0);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=correlationAnalyzerExtra.test.js.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive CorrelationAnalyzer Tests
|
|
3
|
+
* Tests that trigger the cross-file correlation logic with actual files
|
|
4
|
+
*/
|
|
5
|
+
import { analyzeCorrelations } from '../analyzers/CorrelationAnalyzer.js';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
function makeFileWithContent(filePath, content) {
|
|
10
|
+
return {
|
|
11
|
+
path: filePath,
|
|
12
|
+
relativePath: path.basename(filePath),
|
|
13
|
+
type: 'md',
|
|
14
|
+
component: 'agent',
|
|
15
|
+
size: Buffer.byteLength(content),
|
|
16
|
+
modified: new Date(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function makeCorrelationRule(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
id: 'CORR-001',
|
|
22
|
+
name: 'Test Correlation Rule',
|
|
23
|
+
category: 'injection',
|
|
24
|
+
severity: 'HIGH',
|
|
25
|
+
description: 'Detects correlated patterns across files',
|
|
26
|
+
patterns: [],
|
|
27
|
+
fileTypes: ['md'],
|
|
28
|
+
components: ['agent', 'skill', 'hook', 'plugin', 'mcp', 'settings', 'ai-config-md', 'rules-file'],
|
|
29
|
+
remediation: 'Review the correlated patterns',
|
|
30
|
+
references: [],
|
|
31
|
+
enabled: true,
|
|
32
|
+
correlationRules: [
|
|
33
|
+
{
|
|
34
|
+
id: 'CORR-RULE-001',
|
|
35
|
+
description: 'Exfiltration combined with credential access',
|
|
36
|
+
filePatterns: ['*.md'],
|
|
37
|
+
contentPatterns: ['secret', 'exfiltrate'],
|
|
38
|
+
maxDistance: 3,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
describe('analyzeCorrelations - with real files', () => {
|
|
45
|
+
let tmpDir;
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-corr-full-'));
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
it('detects cross-file correlation with matching patterns', () => {
|
|
53
|
+
const file1Path = path.join(tmpDir, 'agent1.md');
|
|
54
|
+
const file2Path = path.join(tmpDir, 'agent2.md');
|
|
55
|
+
fs.writeFileSync(file1Path, '# Agent 1\nAccess secret files here');
|
|
56
|
+
fs.writeFileSync(file2Path, '# Agent 2\nExfiltrate data to external server');
|
|
57
|
+
const file1 = makeFileWithContent(file1Path, '# Agent 1\nAccess secret files here');
|
|
58
|
+
const file2 = makeFileWithContent(file2Path, '# Agent 2\nExfiltrate data to external server');
|
|
59
|
+
const rule = makeCorrelationRule();
|
|
60
|
+
const results = analyzeCorrelations([file1, file2], [rule]);
|
|
61
|
+
expect(Array.isArray(results)).toBe(true);
|
|
62
|
+
// May or may not find correlation depending on pattern matching
|
|
63
|
+
});
|
|
64
|
+
it('returns no results when files are too far apart (maxDistance)', () => {
|
|
65
|
+
// Create deeply nested files
|
|
66
|
+
const deepDir1 = path.join(tmpDir, 'a', 'b', 'c', 'd');
|
|
67
|
+
const deepDir2 = path.join(tmpDir, 'x', 'y', 'z', 'w');
|
|
68
|
+
fs.mkdirSync(deepDir1, { recursive: true });
|
|
69
|
+
fs.mkdirSync(deepDir2, { recursive: true });
|
|
70
|
+
const file1Path = path.join(deepDir1, 'agent1.md');
|
|
71
|
+
const file2Path = path.join(deepDir2, 'agent2.md');
|
|
72
|
+
fs.writeFileSync(file1Path, '# Agent\nAccess secret files');
|
|
73
|
+
fs.writeFileSync(file2Path, '# Agent\nExfiltrate data');
|
|
74
|
+
const file1 = makeFileWithContent(file1Path, '# Agent\nAccess secret files');
|
|
75
|
+
const file2 = makeFileWithContent(file2Path, '# Agent\nExfiltrate data');
|
|
76
|
+
const rule = makeCorrelationRule({
|
|
77
|
+
correlationRules: [{
|
|
78
|
+
id: 'CORR-RULE-001',
|
|
79
|
+
description: 'Test',
|
|
80
|
+
filePatterns: ['*.md'],
|
|
81
|
+
contentPatterns: ['secret', 'exfiltrate'],
|
|
82
|
+
maxDistance: 1, // Very small max distance
|
|
83
|
+
}],
|
|
84
|
+
});
|
|
85
|
+
const results = analyzeCorrelations([file1, file2], [rule]);
|
|
86
|
+
expect(Array.isArray(results)).toBe(true);
|
|
87
|
+
// With small maxDistance, files in deeply different paths may not correlate
|
|
88
|
+
});
|
|
89
|
+
it('handles file read errors gracefully', () => {
|
|
90
|
+
// Provide a file object that points to a non-existent file
|
|
91
|
+
const file1 = {
|
|
92
|
+
path: path.join(tmpDir, 'nonexistent1.md'),
|
|
93
|
+
relativePath: 'nonexistent1.md',
|
|
94
|
+
type: 'md',
|
|
95
|
+
component: 'agent',
|
|
96
|
+
size: 100,
|
|
97
|
+
modified: new Date(),
|
|
98
|
+
};
|
|
99
|
+
const file2Path = path.join(tmpDir, 'agent2.md');
|
|
100
|
+
fs.writeFileSync(file2Path, '# Agent\nAccess secret files');
|
|
101
|
+
const file2 = makeFileWithContent(file2Path, '# Agent\nAccess secret files');
|
|
102
|
+
const rule = makeCorrelationRule();
|
|
103
|
+
const results = analyzeCorrelations([file1, file2], [rule]);
|
|
104
|
+
expect(Array.isArray(results)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
it('handles multiple correlation rules', () => {
|
|
107
|
+
const file1Path = path.join(tmpDir, 'agent1.md');
|
|
108
|
+
const file2Path = path.join(tmpDir, 'agent2.md');
|
|
109
|
+
fs.writeFileSync(file1Path, '# Agent\nSecret credentials here');
|
|
110
|
+
fs.writeFileSync(file2Path, '# Agent\nExfiltrate data externally');
|
|
111
|
+
const file1 = makeFileWithContent(file1Path, '# Agent\nSecret credentials here');
|
|
112
|
+
const file2 = makeFileWithContent(file2Path, '# Agent\nExfiltrate data externally');
|
|
113
|
+
const rule = makeCorrelationRule({
|
|
114
|
+
correlationRules: [
|
|
115
|
+
{
|
|
116
|
+
id: 'CORR-001',
|
|
117
|
+
description: 'Exfiltration + credentials',
|
|
118
|
+
filePatterns: ['*.md'],
|
|
119
|
+
contentPatterns: ['secret', 'exfiltrate'],
|
|
120
|
+
maxDistance: 2,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'CORR-002',
|
|
124
|
+
description: 'Credentials combination',
|
|
125
|
+
filePatterns: ['*.md'],
|
|
126
|
+
contentPatterns: ['credentials', 'externally'],
|
|
127
|
+
maxDistance: 2,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
const results = analyzeCorrelations([file1, file2], [rule]);
|
|
132
|
+
expect(Array.isArray(results)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
it('handles 3+ files in correlation analysis', () => {
|
|
135
|
+
const files = Array.from({ length: 5 }, (_, i) => {
|
|
136
|
+
const filePath = path.join(tmpDir, `agent${i}.md`);
|
|
137
|
+
const content = `# Agent ${i}\nSecret key access here\nExfiltrate to server`;
|
|
138
|
+
fs.writeFileSync(filePath, content);
|
|
139
|
+
return makeFileWithContent(filePath, content);
|
|
140
|
+
});
|
|
141
|
+
const rule = makeCorrelationRule({
|
|
142
|
+
correlationRules: [{
|
|
143
|
+
id: 'CORR-001',
|
|
144
|
+
description: 'Multi-file correlation',
|
|
145
|
+
filePatterns: ['*.md'],
|
|
146
|
+
contentPatterns: ['Secret', 'Exfiltrate'],
|
|
147
|
+
maxDistance: 5,
|
|
148
|
+
}],
|
|
149
|
+
});
|
|
150
|
+
const results = analyzeCorrelations(files, [rule]);
|
|
151
|
+
expect(Array.isArray(results)).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
//# sourceMappingURL=correlationAnalyzerFull.test.js.map
|