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.
Files changed (159) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +15 -11
  3. package/bin/ferret.js +104 -8
  4. package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
  5. package/dist/__tests__/AgentMonitor.test.js +235 -0
  6. package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
  7. package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
  8. package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
  9. package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
  10. package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
  11. package/dist/__tests__/IndicatorMatcher.test.js +245 -0
  12. package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
  13. package/dist/__tests__/MarketplaceScanner.test.js +212 -0
  14. package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
  15. package/dist/__tests__/RuleGenerator.test.js +207 -0
  16. package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
  17. package/dist/__tests__/ThreatFeed.test.js +359 -0
  18. package/dist/__tests__/WatchMode.test.d.ts +6 -0
  19. package/dist/__tests__/WatchMode.test.js +104 -0
  20. package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
  21. package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
  22. package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
  23. package/dist/__tests__/astAnalyzerFull.test.js +138 -0
  24. package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
  25. package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
  26. package/dist/__tests__/atlas.test.d.ts +6 -0
  27. package/dist/__tests__/atlas.test.js +319 -0
  28. package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
  29. package/dist/__tests__/atlasCatalog.test.js +200 -0
  30. package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
  31. package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
  32. package/dist/__tests__/baseline.test.d.ts +6 -0
  33. package/dist/__tests__/baseline.test.js +321 -0
  34. package/dist/__tests__/baselineExtra.test.d.ts +6 -0
  35. package/dist/__tests__/baselineExtra.test.js +317 -0
  36. package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
  37. package/dist/__tests__/capabilityMapping.test.js +49 -0
  38. package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
  39. package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
  40. package/dist/__tests__/complianceExtra.test.d.ts +6 -0
  41. package/dist/__tests__/complianceExtra.test.js +121 -0
  42. package/dist/__tests__/config.test.js +1 -1
  43. package/dist/__tests__/configLoader.test.d.ts +6 -0
  44. package/dist/__tests__/configLoader.test.js +225 -0
  45. package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
  46. package/dist/__tests__/configLoaderExtra.test.js +186 -0
  47. package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
  48. package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
  49. package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
  50. package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
  51. package/dist/__tests__/customRules.extra.test.d.ts +6 -0
  52. package/dist/__tests__/customRules.extra.test.js +245 -0
  53. package/dist/__tests__/customRules.test.d.ts +7 -0
  54. package/dist/__tests__/customRules.test.js +347 -0
  55. package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
  56. package/dist/__tests__/dependencyRisk.test.js +248 -0
  57. package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
  58. package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
  59. package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
  60. package/dist/__tests__/featureExitCodes.test.js +332 -0
  61. package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
  62. package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
  63. package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
  64. package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
  65. package/dist/__tests__/fixer.extra.test.d.ts +6 -0
  66. package/dist/__tests__/fixer.extra.test.js +135 -0
  67. package/dist/__tests__/fixerApply.test.d.ts +6 -0
  68. package/dist/__tests__/fixerApply.test.js +132 -0
  69. package/dist/__tests__/gitHooks.test.d.ts +7 -0
  70. package/dist/__tests__/gitHooks.test.js +188 -0
  71. package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
  72. package/dist/__tests__/htmlReporter.extra.test.js +126 -0
  73. package/dist/__tests__/interactiveTui.test.d.ts +6 -0
  74. package/dist/__tests__/interactiveTui.test.js +180 -0
  75. package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
  76. package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
  77. package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
  78. package/dist/__tests__/interactiveTuiMore.test.js +194 -0
  79. package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
  80. package/dist/__tests__/interactiveTuiSession.test.js +173 -0
  81. package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
  82. package/dist/__tests__/llmAnalysis.test.js +229 -0
  83. package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
  84. package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
  85. package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
  86. package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
  87. package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
  88. package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
  89. package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
  90. package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
  91. package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
  92. package/dist/__tests__/llmGroqTPM.test.js +89 -0
  93. package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
  94. package/dist/__tests__/llmProviderRetry.test.js +172 -0
  95. package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
  96. package/dist/__tests__/mcpValidator.extra.test.js +270 -0
  97. package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
  98. package/dist/__tests__/patternMatcherExtra.test.js +198 -0
  99. package/dist/__tests__/patternsCommon.test.d.ts +6 -0
  100. package/dist/__tests__/patternsCommon.test.js +107 -0
  101. package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
  102. package/dist/__tests__/policyEnforcement.test.js +510 -0
  103. package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
  104. package/dist/__tests__/quarantineExtra.test.js +214 -0
  105. package/dist/__tests__/redactionExtra.test.d.ts +6 -0
  106. package/dist/__tests__/redactionExtra.test.js +228 -0
  107. package/dist/__tests__/scanDiff.test.d.ts +7 -0
  108. package/dist/__tests__/scanDiff.test.js +266 -0
  109. package/dist/__tests__/scanFull.test.d.ts +6 -0
  110. package/dist/__tests__/scanFull.test.js +158 -0
  111. package/dist/__tests__/scannerDampening.test.d.ts +6 -0
  112. package/dist/__tests__/scannerDampening.test.js +160 -0
  113. package/dist/__tests__/scannerExtra.test.d.ts +6 -0
  114. package/dist/__tests__/scannerExtra.test.js +194 -0
  115. package/dist/__tests__/scannerMitre.test.d.ts +5 -0
  116. package/dist/__tests__/scannerMitre.test.js +141 -0
  117. package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
  118. package/dist/__tests__/scannerSSRF.test.js +149 -0
  119. package/dist/__tests__/schemas.test.d.ts +6 -0
  120. package/dist/__tests__/schemas.test.js +125 -0
  121. package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
  122. package/dist/__tests__/webhooks.extra.test.js +144 -0
  123. package/dist/__tests__/webhooks.test.d.ts +6 -0
  124. package/dist/__tests__/webhooks.test.js +154 -0
  125. package/dist/features/customRules.js +22 -29
  126. package/dist/features/mcpTrustScore.d.ts +17 -0
  127. package/dist/features/mcpTrustScore.js +74 -0
  128. package/dist/features/mcpValidator.d.ts +2 -0
  129. package/dist/features/mcpValidator.js +13 -0
  130. package/dist/features/policyEnforcement.d.ts +22 -22
  131. package/dist/intelligence/ThreatFeed.js +207 -62
  132. package/dist/remediation/Quarantine.js +24 -6
  133. package/dist/reporters/ConsoleReporter.js +10 -0
  134. package/dist/reporters/HtmlReporter.js +5 -0
  135. package/dist/reporters/SarifReporter.d.ts +1 -0
  136. package/dist/reporters/SarifReporter.js +1 -0
  137. package/dist/scanner/IAnalyzer.d.ts +19 -0
  138. package/dist/scanner/IAnalyzer.js +5 -0
  139. package/dist/scanner/Scanner.js +64 -125
  140. package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
  141. package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
  142. package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
  143. package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
  144. package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
  145. package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
  146. package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
  147. package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
  148. package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
  149. package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
  150. package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
  151. package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
  152. package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
  153. package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
  154. package/dist/types.d.ts +17 -0
  155. package/dist/types.js +1 -1
  156. package/dist/utils/safeRegex.d.ts +12 -51
  157. package/dist/utils/safeRegex.js +45 -62
  158. package/dist/utils/schemas.d.ts +64 -64
  159. package/package.json +25 -19
@@ -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,6 @@
1
+ /**
2
+ * WatchMode Tests
3
+ * Tests for the debounce function and createChangeNotifier exported from WatchMode.ts
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=WatchMode.test.d.ts.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,6 @@
1
+ /**
2
+ * Additional AstAnalyzer Tests
3
+ * Tests for shouldAnalyze and getMemoryUsage
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=astAnalyzerExtra.test.d.ts.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
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Full AstAnalyzer Tests
3
+ * Tests for analyzeFile function
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=astAnalyzerFull.test.d.ts.map