ferret-scan 2.2.0 → 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 +12 -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 +24 -18
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThreatFeed Tests
|
|
3
|
+
* Tests for threat intelligence database operations: load, save, add, remove, query.
|
|
4
|
+
*/
|
|
5
|
+
jest.mock('node:fs');
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import { loadThreatDatabase, saveThreatDatabase, addIndicators, removeIndicators, getIndicatorsByType, getIndicatorsByCategory, getHighConfidenceIndicators, searchIndicators, needsUpdate, } from '../intelligence/ThreatFeed.js';
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const mockFs = fs;
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
function makeIndicator(overrides = {}) {
|
|
14
|
+
return {
|
|
15
|
+
value: 'example.com',
|
|
16
|
+
type: 'domain',
|
|
17
|
+
category: 'phishing',
|
|
18
|
+
severity: 'high',
|
|
19
|
+
description: 'Test domain indicator',
|
|
20
|
+
source: 'test-source',
|
|
21
|
+
firstSeen: '2024-01-01T00:00:00Z',
|
|
22
|
+
lastSeen: '2024-01-01T00:00:00Z',
|
|
23
|
+
confidence: 80,
|
|
24
|
+
tags: ['test'],
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function makeDatabase(overrides = {}) {
|
|
29
|
+
return {
|
|
30
|
+
version: '1.0',
|
|
31
|
+
lastUpdated: new Date(Date.now() - 1000).toISOString(),
|
|
32
|
+
sources: [],
|
|
33
|
+
indicators: [],
|
|
34
|
+
stats: {
|
|
35
|
+
totalIndicators: 0,
|
|
36
|
+
byType: {
|
|
37
|
+
domain: 0, url: 0, ip: 0, hash: 0, email: 0,
|
|
38
|
+
filename: 0, package: 0, pattern: 0, signature: 0,
|
|
39
|
+
},
|
|
40
|
+
byCategory: {},
|
|
41
|
+
bySeverity: {},
|
|
42
|
+
},
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const VALID_DB_JSON = JSON.stringify({
|
|
47
|
+
version: '1.0',
|
|
48
|
+
lastUpdated: new Date().toISOString(),
|
|
49
|
+
sources: [],
|
|
50
|
+
indicators: [
|
|
51
|
+
{
|
|
52
|
+
value: 'evil.com',
|
|
53
|
+
type: 'domain',
|
|
54
|
+
category: 'phishing',
|
|
55
|
+
severity: 'high',
|
|
56
|
+
description: 'Evil domain',
|
|
57
|
+
source: 'test',
|
|
58
|
+
firstSeen: '2024-01-01T00:00:00Z',
|
|
59
|
+
lastSeen: '2024-01-01T00:00:00Z',
|
|
60
|
+
confidence: 90,
|
|
61
|
+
tags: ['phishing'],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
stats: {
|
|
65
|
+
totalIndicators: 1,
|
|
66
|
+
byType: { domain: 1, url: 0, ip: 0, hash: 0, email: 0, filename: 0, package: 0, pattern: 0, signature: 0 },
|
|
67
|
+
byCategory: { phishing: 1 },
|
|
68
|
+
bySeverity: { high: 1 },
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// loadThreatDatabase
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
describe('loadThreatDatabase', () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
jest.clearAllMocks();
|
|
77
|
+
});
|
|
78
|
+
it('returns default database when threat-db.json does not exist', () => {
|
|
79
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
80
|
+
const db = loadThreatDatabase('/nonexistent-dir');
|
|
81
|
+
expect(db).toBeDefined();
|
|
82
|
+
expect(db.version).toBeDefined();
|
|
83
|
+
// Should still have builtin indicators
|
|
84
|
+
expect(db.indicators.length).toBeGreaterThan(0);
|
|
85
|
+
});
|
|
86
|
+
it('loads database from existing file', () => {
|
|
87
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
88
|
+
mockFs.readFileSync.mockReturnValue(VALID_DB_JSON);
|
|
89
|
+
const db = loadThreatDatabase('/intel-dir');
|
|
90
|
+
expect(db.indicators).toHaveLength(1);
|
|
91
|
+
expect(db.indicators[0].value).toBe('evil.com');
|
|
92
|
+
});
|
|
93
|
+
it('falls back to default database when file has invalid JSON', () => {
|
|
94
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
95
|
+
mockFs.readFileSync.mockReturnValue('{ invalid json }');
|
|
96
|
+
const db = loadThreatDatabase('/intel-dir');
|
|
97
|
+
// Should return default (with builtin indicators)
|
|
98
|
+
expect(db).toBeDefined();
|
|
99
|
+
expect(db.indicators.length).toBeGreaterThan(0);
|
|
100
|
+
});
|
|
101
|
+
it('falls back to default database when file fails schema validation', () => {
|
|
102
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
103
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ notADb: true }));
|
|
104
|
+
const db = loadThreatDatabase('/intel-dir');
|
|
105
|
+
expect(db).toBeDefined();
|
|
106
|
+
// Default DB has builtin indicators
|
|
107
|
+
expect(db.indicators.length).toBeGreaterThan(0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// saveThreatDatabase
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
describe('saveThreatDatabase', () => {
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
jest.clearAllMocks();
|
|
116
|
+
mockFs.mkdirSync.mockReturnValue(undefined);
|
|
117
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
118
|
+
});
|
|
119
|
+
it('saves database and updates metadata', () => {
|
|
120
|
+
const db = makeDatabase({
|
|
121
|
+
indicators: [makeIndicator()],
|
|
122
|
+
});
|
|
123
|
+
saveThreatDatabase(db, '/intel-dir');
|
|
124
|
+
expect(mockFs.mkdirSync).toHaveBeenCalled();
|
|
125
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled();
|
|
126
|
+
// Verify the saved content is valid JSON
|
|
127
|
+
const savedArgs = mockFs.writeFileSync.mock.calls[0];
|
|
128
|
+
const saved = JSON.parse(savedArgs[1]);
|
|
129
|
+
expect(saved.indicators).toHaveLength(1);
|
|
130
|
+
expect(saved.stats.totalIndicators).toBe(1);
|
|
131
|
+
});
|
|
132
|
+
it('throws when writeFileSync fails', () => {
|
|
133
|
+
mockFs.mkdirSync.mockReturnValue(undefined);
|
|
134
|
+
mockFs.writeFileSync.mockImplementation(() => { throw new Error('disk full'); });
|
|
135
|
+
const db = makeDatabase();
|
|
136
|
+
expect(() => saveThreatDatabase(db, '/intel-dir')).toThrow('disk full');
|
|
137
|
+
});
|
|
138
|
+
it('updates lastUpdated timestamp on save', () => {
|
|
139
|
+
const oldTime = '2020-01-01T00:00:00Z';
|
|
140
|
+
const db = makeDatabase({ lastUpdated: oldTime });
|
|
141
|
+
saveThreatDatabase(db, '/intel-dir');
|
|
142
|
+
const savedArgs = mockFs.writeFileSync.mock.calls[0];
|
|
143
|
+
const saved = JSON.parse(savedArgs[1]);
|
|
144
|
+
expect(saved.lastUpdated).not.toBe(oldTime);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// addIndicators
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
describe('addIndicators', () => {
|
|
151
|
+
it('adds new indicators to database', () => {
|
|
152
|
+
const db = makeDatabase();
|
|
153
|
+
const result = addIndicators(db, [
|
|
154
|
+
{
|
|
155
|
+
value: 'new-evil.com',
|
|
156
|
+
type: 'domain',
|
|
157
|
+
category: 'phishing',
|
|
158
|
+
severity: 'high',
|
|
159
|
+
description: 'New evil domain',
|
|
160
|
+
source: 'test',
|
|
161
|
+
confidence: 85,
|
|
162
|
+
tags: ['phishing'],
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
165
|
+
expect(result.indicators).toHaveLength(1);
|
|
166
|
+
expect(result.indicators[0].value).toBe('new-evil.com');
|
|
167
|
+
expect(result.indicators[0].firstSeen).toBeDefined();
|
|
168
|
+
expect(result.indicators[0].lastSeen).toBeDefined();
|
|
169
|
+
});
|
|
170
|
+
it('skips duplicate indicators', () => {
|
|
171
|
+
const existing = makeIndicator({ value: 'known.com', type: 'domain' });
|
|
172
|
+
const db = makeDatabase({ indicators: [existing] });
|
|
173
|
+
const result = addIndicators(db, [
|
|
174
|
+
{
|
|
175
|
+
value: 'known.com',
|
|
176
|
+
type: 'domain',
|
|
177
|
+
category: 'phishing',
|
|
178
|
+
severity: 'medium',
|
|
179
|
+
description: 'Duplicate',
|
|
180
|
+
source: 'test2',
|
|
181
|
+
confidence: 70,
|
|
182
|
+
tags: [],
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
expect(result.indicators).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
it('adds multiple indicators at once', () => {
|
|
188
|
+
const db = makeDatabase();
|
|
189
|
+
const result = addIndicators(db, [
|
|
190
|
+
{ value: 'a.com', type: 'domain', category: 'c1', severity: 'high', description: 'd', source: 's', confidence: 80, tags: [] },
|
|
191
|
+
{ value: 'b.com', type: 'domain', category: 'c2', severity: 'low', description: 'd', source: 's', confidence: 60, tags: [] },
|
|
192
|
+
]);
|
|
193
|
+
expect(result.indicators).toHaveLength(2);
|
|
194
|
+
});
|
|
195
|
+
it('updates stats after adding indicators', () => {
|
|
196
|
+
const db = makeDatabase();
|
|
197
|
+
const result = addIndicators(db, [
|
|
198
|
+
{ value: 'pkg-evil', type: 'package', category: 'malicious-package', severity: 'critical', description: 'evil pkg', source: 's', confidence: 100, tags: [] },
|
|
199
|
+
]);
|
|
200
|
+
expect(result.stats.totalIndicators).toBe(1);
|
|
201
|
+
expect(result.stats.byType['package']).toBe(1);
|
|
202
|
+
expect(result.stats.bySeverity['critical']).toBe(1);
|
|
203
|
+
});
|
|
204
|
+
it('does not mutate original database', () => {
|
|
205
|
+
const db = makeDatabase();
|
|
206
|
+
addIndicators(db, [{ value: 'x.com', type: 'domain', category: 'c', severity: 'low', description: 'd', source: 's', confidence: 50, tags: [] }]);
|
|
207
|
+
expect(db.indicators).toHaveLength(0);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// removeIndicators
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
describe('removeIndicators', () => {
|
|
214
|
+
it('removes indicator by type:value key', () => {
|
|
215
|
+
const db = makeDatabase({
|
|
216
|
+
indicators: [
|
|
217
|
+
makeIndicator({ value: 'remove.com', type: 'domain' }),
|
|
218
|
+
makeIndicator({ value: 'keep.com', type: 'domain' }),
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
const result = removeIndicators(db, ['domain:remove.com']);
|
|
222
|
+
expect(result.indicators).toHaveLength(1);
|
|
223
|
+
expect(result.indicators[0].value).toBe('keep.com');
|
|
224
|
+
});
|
|
225
|
+
it('removes indicator by index', () => {
|
|
226
|
+
const db = makeDatabase({
|
|
227
|
+
indicators: [
|
|
228
|
+
makeIndicator({ value: 'first.com' }),
|
|
229
|
+
makeIndicator({ value: 'second.com' }),
|
|
230
|
+
],
|
|
231
|
+
});
|
|
232
|
+
const result = removeIndicators(db, ['0']);
|
|
233
|
+
expect(result.indicators).toHaveLength(1);
|
|
234
|
+
expect(result.indicators[0].value).toBe('second.com');
|
|
235
|
+
});
|
|
236
|
+
it('returns unchanged database when id not found', () => {
|
|
237
|
+
const db = makeDatabase({ indicators: [makeIndicator()] });
|
|
238
|
+
const result = removeIndicators(db, ['nonexistent:key']);
|
|
239
|
+
expect(result.indicators).toHaveLength(1);
|
|
240
|
+
});
|
|
241
|
+
it('updates stats after removal', () => {
|
|
242
|
+
const db = makeDatabase({
|
|
243
|
+
indicators: [makeIndicator({ value: 'x.com', type: 'domain' })],
|
|
244
|
+
});
|
|
245
|
+
const result = removeIndicators(db, ['domain:x.com']);
|
|
246
|
+
expect(result.stats.totalIndicators).toBe(0);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// getIndicatorsByType
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
describe('getIndicatorsByType', () => {
|
|
253
|
+
const db = makeDatabase({
|
|
254
|
+
indicators: [
|
|
255
|
+
makeIndicator({ value: 'a.com', type: 'domain' }),
|
|
256
|
+
makeIndicator({ value: 'b.com', type: 'domain' }),
|
|
257
|
+
makeIndicator({ value: 'pkg-evil', type: 'package' }),
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
it('returns indicators matching the type', () => {
|
|
261
|
+
const domains = getIndicatorsByType(db, 'domain');
|
|
262
|
+
expect(domains).toHaveLength(2);
|
|
263
|
+
});
|
|
264
|
+
it('returns empty array when no indicators match', () => {
|
|
265
|
+
const hashes = getIndicatorsByType(db, 'hash');
|
|
266
|
+
expect(hashes).toHaveLength(0);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// getIndicatorsByCategory
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
describe('getIndicatorsByCategory', () => {
|
|
273
|
+
const db = makeDatabase({
|
|
274
|
+
indicators: [
|
|
275
|
+
makeIndicator({ category: 'phishing' }),
|
|
276
|
+
makeIndicator({ category: 'phishing' }),
|
|
277
|
+
makeIndicator({ category: 'malware' }),
|
|
278
|
+
],
|
|
279
|
+
});
|
|
280
|
+
it('returns indicators in the given category', () => {
|
|
281
|
+
expect(getIndicatorsByCategory(db, 'phishing')).toHaveLength(2);
|
|
282
|
+
});
|
|
283
|
+
it('returns empty array for unknown category', () => {
|
|
284
|
+
expect(getIndicatorsByCategory(db, 'unknown')).toHaveLength(0);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// getHighConfidenceIndicators
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
describe('getHighConfidenceIndicators', () => {
|
|
291
|
+
const db = makeDatabase({
|
|
292
|
+
indicators: [
|
|
293
|
+
makeIndicator({ confidence: 95 }),
|
|
294
|
+
makeIndicator({ confidence: 80 }),
|
|
295
|
+
makeIndicator({ confidence: 60 }),
|
|
296
|
+
makeIndicator({ confidence: 40 }),
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
it('returns indicators above default threshold of 80', () => {
|
|
300
|
+
const result = getHighConfidenceIndicators(db);
|
|
301
|
+
expect(result).toHaveLength(2);
|
|
302
|
+
});
|
|
303
|
+
it('accepts custom confidence threshold', () => {
|
|
304
|
+
const result = getHighConfidenceIndicators(db, 60);
|
|
305
|
+
expect(result).toHaveLength(3);
|
|
306
|
+
});
|
|
307
|
+
it('returns all when threshold is 0', () => {
|
|
308
|
+
const result = getHighConfidenceIndicators(db, 0);
|
|
309
|
+
expect(result).toHaveLength(4);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// searchIndicators
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
describe('searchIndicators', () => {
|
|
316
|
+
const db = makeDatabase({
|
|
317
|
+
indicators: [
|
|
318
|
+
makeIndicator({ value: 'phishing-site.com', description: 'Fake bank', tags: ['phishing', 'finance'] }),
|
|
319
|
+
makeIndicator({ value: 'malware-cdn.net', description: 'Malware CDN', tags: ['malware'] }),
|
|
320
|
+
makeIndicator({ value: 'legit-check.com', description: 'False positive check', tags: ['safe'] }),
|
|
321
|
+
],
|
|
322
|
+
});
|
|
323
|
+
it('finds indicators by value substring', () => {
|
|
324
|
+
expect(searchIndicators(db, 'phishing')).toHaveLength(1);
|
|
325
|
+
});
|
|
326
|
+
it('finds indicators by description', () => {
|
|
327
|
+
expect(searchIndicators(db, 'malware')).toHaveLength(1);
|
|
328
|
+
});
|
|
329
|
+
it('finds indicators by tag', () => {
|
|
330
|
+
expect(searchIndicators(db, 'finance')).toHaveLength(1);
|
|
331
|
+
});
|
|
332
|
+
it('returns empty array when no matches', () => {
|
|
333
|
+
expect(searchIndicators(db, 'zzznomatch')).toHaveLength(0);
|
|
334
|
+
});
|
|
335
|
+
it('search is case-insensitive', () => {
|
|
336
|
+
expect(searchIndicators(db, 'PHISHING')).toHaveLength(1);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// needsUpdate
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
describe('needsUpdate', () => {
|
|
343
|
+
it('returns true when database is older than maxAgeHours', () => {
|
|
344
|
+
const oldDate = new Date(Date.now() - 25 * 60 * 60 * 1000).toISOString(); // 25 hours ago
|
|
345
|
+
const db = makeDatabase({ lastUpdated: oldDate });
|
|
346
|
+
expect(needsUpdate(db, 24)).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
it('returns false when database is fresh', () => {
|
|
349
|
+
const recentDate = new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString(); // 1 hour ago
|
|
350
|
+
const db = makeDatabase({ lastUpdated: recentDate });
|
|
351
|
+
expect(needsUpdate(db, 24)).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
it('uses default maxAge of 24 hours', () => {
|
|
354
|
+
const recentDate = new Date(Date.now() - 23 * 60 * 60 * 1000).toISOString();
|
|
355
|
+
const db = makeDatabase({ lastUpdated: recentDate });
|
|
356
|
+
expect(needsUpdate(db)).toBe(false);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
//# sourceMappingURL=ThreatFeed.test.js.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WatchMode Tests
|
|
3
|
+
* Tests for the debounce function and createChangeNotifier exported from WatchMode.ts
|
|
4
|
+
*/
|
|
5
|
+
// Mock Scanner to avoid ora (ESM-only) import issues
|
|
6
|
+
jest.mock('../scanner/Scanner.js', () => ({
|
|
7
|
+
scan: jest.fn().mockResolvedValue({ findings: [] }),
|
|
8
|
+
}));
|
|
9
|
+
// Mock ConsoleReporter
|
|
10
|
+
jest.mock('../reporters/ConsoleReporter.js', () => ({
|
|
11
|
+
generateConsoleReport: jest.fn().mockReturnValue('mock report'),
|
|
12
|
+
}));
|
|
13
|
+
import { createChangeNotifier } from '../scanner/WatchMode.js';
|
|
14
|
+
import { EventEmitter } from 'events';
|
|
15
|
+
class MockWatcher extends EventEmitter {
|
|
16
|
+
close() { return Promise.resolve(); }
|
|
17
|
+
getWatched() { return {}; }
|
|
18
|
+
}
|
|
19
|
+
let _mockWatcherInstance = null;
|
|
20
|
+
const mockWatch = jest.fn((..._args) => {
|
|
21
|
+
_mockWatcherInstance = new MockWatcher();
|
|
22
|
+
return _mockWatcherInstance;
|
|
23
|
+
});
|
|
24
|
+
// Mock chokidar so we don't need real file watching
|
|
25
|
+
jest.mock('chokidar', () => ({
|
|
26
|
+
__esModule: true,
|
|
27
|
+
default: {
|
|
28
|
+
watch: (...args) => mockWatch(...args),
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
function getWatcherInstance() {
|
|
32
|
+
return _mockWatcherInstance;
|
|
33
|
+
}
|
|
34
|
+
describe('createChangeNotifier', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
jest.clearAllMocks();
|
|
37
|
+
jest.useFakeTimers();
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
jest.useRealTimers();
|
|
41
|
+
});
|
|
42
|
+
it('returns a cleanup function', () => {
|
|
43
|
+
const cleanup = createChangeNotifier(['/tmp'], jest.fn(), { debounceMs: 100 });
|
|
44
|
+
expect(typeof cleanup).toBe('function');
|
|
45
|
+
cleanup();
|
|
46
|
+
});
|
|
47
|
+
it('calls chokidar.watch with the given paths', () => {
|
|
48
|
+
createChangeNotifier(['/project', '/home'], jest.fn(), { debounceMs: 100 });
|
|
49
|
+
expect(mockWatch).toHaveBeenCalledWith(['/project', '/home'], expect.any(Object));
|
|
50
|
+
});
|
|
51
|
+
it('does not invoke callback before debounce period', () => {
|
|
52
|
+
const callback = jest.fn();
|
|
53
|
+
createChangeNotifier(['/tmp'], callback, { debounceMs: 500 });
|
|
54
|
+
const watcher = getWatcherInstance();
|
|
55
|
+
watcher.emit('all', 'change', '/tmp/file.md');
|
|
56
|
+
// No time advance yet
|
|
57
|
+
expect(callback).not.toHaveBeenCalled();
|
|
58
|
+
const cleanup = createChangeNotifier(['/tmp'], callback, { debounceMs: 500 });
|
|
59
|
+
cleanup();
|
|
60
|
+
});
|
|
61
|
+
it('invokes callback after debounce period with changed files', () => {
|
|
62
|
+
const callback = jest.fn();
|
|
63
|
+
createChangeNotifier(['/tmp'], callback, { debounceMs: 500 });
|
|
64
|
+
const watcher = getWatcherInstance();
|
|
65
|
+
watcher.emit('all', 'add', '/tmp/file1.md');
|
|
66
|
+
watcher.emit('all', 'change', '/tmp/file2.md');
|
|
67
|
+
jest.advanceTimersByTime(600);
|
|
68
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(callback).toHaveBeenCalledWith(expect.arrayContaining(['/tmp/file1.md', '/tmp/file2.md']));
|
|
70
|
+
});
|
|
71
|
+
it('ignores non-add/change/unlink events', () => {
|
|
72
|
+
const callback = jest.fn();
|
|
73
|
+
createChangeNotifier(['/tmp'], callback, { debounceMs: 100 });
|
|
74
|
+
const watcher = getWatcherInstance();
|
|
75
|
+
watcher.emit('all', 'ready', '/tmp');
|
|
76
|
+
watcher.emit('all', 'error', '/tmp');
|
|
77
|
+
jest.advanceTimersByTime(200);
|
|
78
|
+
expect(callback).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
it('debounces rapid file changes', () => {
|
|
81
|
+
const callback = jest.fn();
|
|
82
|
+
createChangeNotifier(['/tmp'], callback, { debounceMs: 300 });
|
|
83
|
+
const watcher = getWatcherInstance();
|
|
84
|
+
// Fire multiple change events rapidly
|
|
85
|
+
for (let i = 0; i < 5; i++) {
|
|
86
|
+
watcher.emit('all', 'change', `/tmp/file${i}.md`);
|
|
87
|
+
jest.advanceTimersByTime(50);
|
|
88
|
+
}
|
|
89
|
+
// Not called yet
|
|
90
|
+
expect(callback).not.toHaveBeenCalled();
|
|
91
|
+
// Advance past debounce
|
|
92
|
+
jest.advanceTimersByTime(400);
|
|
93
|
+
// Should have been called once with all files
|
|
94
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
it('cleanup calls watcher.close()', () => {
|
|
97
|
+
const cleanup = createChangeNotifier(['/tmp'], jest.fn(), { debounceMs: 100 });
|
|
98
|
+
const watcher = getWatcherInstance();
|
|
99
|
+
const closeSpy = jest.spyOn(watcher, 'close');
|
|
100
|
+
cleanup();
|
|
101
|
+
expect(closeSpy).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
//# sourceMappingURL=WatchMode.test.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Additional AstAnalyzer Tests
|
|
3
|
+
* Tests for shouldAnalyze and getMemoryUsage
|
|
4
|
+
*/
|
|
5
|
+
import { shouldAnalyze, getMemoryUsage } from '../analyzers/AstAnalyzer.js';
|
|
6
|
+
function makeFile(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
path: '/project/test.md',
|
|
9
|
+
relativePath: 'test.md',
|
|
10
|
+
type: 'md',
|
|
11
|
+
component: 'agent',
|
|
12
|
+
size: 1000,
|
|
13
|
+
modified: new Date(),
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
describe('shouldAnalyze', () => {
|
|
18
|
+
const config = { semanticAnalysis: true, maxFileSize: 1024 * 1024 };
|
|
19
|
+
it('returns false when semanticAnalysis is disabled', () => {
|
|
20
|
+
expect(shouldAnalyze(makeFile(), { ...config, semanticAnalysis: false })).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
it('returns false for files exceeding maxFileSize', () => {
|
|
23
|
+
const bigFile = makeFile({ size: 2 * 1024 * 1024 });
|
|
24
|
+
expect(shouldAnalyze(bigFile, config)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it('returns true for markdown files', () => {
|
|
27
|
+
expect(shouldAnalyze(makeFile({ type: 'md' }), config)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it('returns true for TypeScript files', () => {
|
|
30
|
+
expect(shouldAnalyze(makeFile({ type: 'ts' }), config)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it('returns true for JavaScript files', () => {
|
|
33
|
+
expect(shouldAnalyze(makeFile({ type: 'js' }), config)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('returns true for TSX files', () => {
|
|
36
|
+
expect(shouldAnalyze(makeFile({ type: 'tsx' }), config)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('returns true for JSX files', () => {
|
|
39
|
+
expect(shouldAnalyze(makeFile({ type: 'jsx' }), config)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it('returns false for JSON files', () => {
|
|
42
|
+
expect(shouldAnalyze(makeFile({ type: 'json' }), config)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('returns false for YAML files', () => {
|
|
45
|
+
expect(shouldAnalyze(makeFile({ type: 'yaml' }), config)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('returns false for shell files', () => {
|
|
48
|
+
expect(shouldAnalyze(makeFile({ type: 'sh' }), config)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('getMemoryUsage', () => {
|
|
52
|
+
it('returns an object with used and total properties', () => {
|
|
53
|
+
const usage = getMemoryUsage();
|
|
54
|
+
expect(typeof usage.used).toBe('number');
|
|
55
|
+
expect(typeof usage.total).toBe('number');
|
|
56
|
+
});
|
|
57
|
+
it('returns positive values', () => {
|
|
58
|
+
const usage = getMemoryUsage();
|
|
59
|
+
expect(usage.used).toBeGreaterThan(0);
|
|
60
|
+
expect(usage.total).toBeGreaterThan(0);
|
|
61
|
+
});
|
|
62
|
+
it('used is less than or equal to total', () => {
|
|
63
|
+
const usage = getMemoryUsage();
|
|
64
|
+
expect(usage.used).toBeLessThanOrEqual(usage.total);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
//# sourceMappingURL=astAnalyzerExtra.test.js.map
|