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,212 @@
1
+ /**
2
+ * MarketplaceScanner Tests
3
+ */
4
+ import { MarketplaceScanner } from '../marketplace/MarketplaceScanner.js';
5
+ // Mock the scan function to avoid real FS operations
6
+ jest.mock('../scanner/Scanner.js', () => ({
7
+ scan: jest.fn().mockResolvedValue({ findings: [] }),
8
+ }));
9
+ // Mock fs/promises to avoid real temp dir creation
10
+ jest.mock('fs/promises', () => ({
11
+ mkdtemp: jest.fn().mockResolvedValue('/tmp/test-plugin-dir'),
12
+ rm: jest.fn().mockResolvedValue(undefined),
13
+ }));
14
+ function makePlugin(overrides = {}) {
15
+ return {
16
+ id: 'test-plugin-001',
17
+ name: 'Test Plugin',
18
+ author: 'test-author',
19
+ version: '1.0.0',
20
+ source: 'community',
21
+ capabilities: [],
22
+ permissions: [],
23
+ metadata: {
24
+ description: 'A test plugin',
25
+ downloads: 1000,
26
+ rating: 4.0,
27
+ lastUpdated: new Date('2024-01-01'),
28
+ },
29
+ ...overrides,
30
+ };
31
+ }
32
+ describe('MarketplaceScanner', () => {
33
+ let scanner;
34
+ beforeEach(() => {
35
+ scanner = new MarketplaceScanner();
36
+ });
37
+ // -------------------------------------------------------------------------
38
+ // scanMarketplace
39
+ // -------------------------------------------------------------------------
40
+ describe('scanMarketplace', () => {
41
+ it('returns empty array when no plugins found', async () => {
42
+ const config = { source: 'claude-marketplace' };
43
+ const results = await scanner.scanMarketplace(config);
44
+ expect(results).toEqual([]);
45
+ });
46
+ it('resolves without throwing for generic source', async () => {
47
+ await expect(scanner.scanMarketplace({ source: 'community' })).resolves.toBeDefined();
48
+ });
49
+ });
50
+ // -------------------------------------------------------------------------
51
+ // analyzePlugin – permission checks
52
+ // -------------------------------------------------------------------------
53
+ describe('analyzePlugin – permission checks', () => {
54
+ it('returns safe recommendation for plugin with no capabilities', async () => {
55
+ const plugin = makePlugin({ capabilities: [] });
56
+ const result = await scanner.analyzePlugin(plugin);
57
+ expect(result.plugin).toBe(plugin);
58
+ expect(result.recommendation).toBe('safe');
59
+ expect(result.riskScore).toBe(0);
60
+ });
61
+ it('detects shell:execute + network:outbound as CRITICAL', async () => {
62
+ const plugin = makePlugin({
63
+ capabilities: ['shell:execute', 'network:outbound'],
64
+ });
65
+ const result = await scanner.analyzePlugin(plugin);
66
+ expect(result.findings.some(f => f.severity === 'CRITICAL')).toBe(true);
67
+ expect(result.recommendation).toBe('malicious');
68
+ });
69
+ it('detects file:write + network:outbound + startup:autorun as CRITICAL', async () => {
70
+ const plugin = makePlugin({
71
+ capabilities: ['file:write', 'network:outbound', 'startup:autorun'],
72
+ });
73
+ const result = await scanner.analyzePlugin(plugin);
74
+ const criticalFindings = result.findings.filter(f => f.severity === 'CRITICAL');
75
+ expect(criticalFindings.length).toBeGreaterThan(0);
76
+ });
77
+ it('detects clipboard:read + network:outbound as HIGH', async () => {
78
+ const plugin = makePlugin({
79
+ capabilities: ['clipboard:read', 'network:outbound'],
80
+ });
81
+ const result = await scanner.analyzePlugin(plugin);
82
+ expect(result.findings.some(f => f.severity === 'HIGH')).toBe(true);
83
+ });
84
+ it('detects file:read + network:outbound as MEDIUM', async () => {
85
+ const plugin = makePlugin({
86
+ capabilities: ['file:read', 'network:outbound'],
87
+ });
88
+ const result = await scanner.analyzePlugin(plugin);
89
+ expect(result.findings.some(f => f.severity === 'MEDIUM')).toBe(true);
90
+ });
91
+ it('detects excessive permissions (>8 capabilities)', async () => {
92
+ const plugin = makePlugin({
93
+ capabilities: [
94
+ 'cap1', 'cap2', 'cap3', 'cap4', 'cap5',
95
+ 'cap6', 'cap7', 'cap8', 'cap9',
96
+ ],
97
+ });
98
+ const result = await scanner.analyzePlugin(plugin);
99
+ const excessFindings = result.findings.filter(f => f.ruleId === 'MARKETPLACE-002');
100
+ expect(excessFindings.length).toBe(1);
101
+ expect(excessFindings[0]?.severity).toBe('MEDIUM');
102
+ });
103
+ it('does not flag exactly 8 capabilities', async () => {
104
+ const plugin = makePlugin({
105
+ capabilities: ['cap1', 'cap2', 'cap3', 'cap4', 'cap5', 'cap6', 'cap7', 'cap8'],
106
+ });
107
+ const result = await scanner.analyzePlugin(plugin);
108
+ const excessFindings = result.findings.filter(f => f.ruleId === 'MARKETPLACE-002');
109
+ expect(excessFindings.length).toBe(0);
110
+ });
111
+ });
112
+ // -------------------------------------------------------------------------
113
+ // risk score calculation
114
+ // -------------------------------------------------------------------------
115
+ describe('risk score calculation', () => {
116
+ it('reduces risk for popular highly-rated plugins (>10k downloads, >4.5 rating)', async () => {
117
+ const dangerousPlugin = makePlugin({
118
+ capabilities: ['file:read', 'network:outbound'],
119
+ metadata: {
120
+ description: 'Popular plugin',
121
+ downloads: 50000,
122
+ rating: 4.8,
123
+ lastUpdated: new Date(),
124
+ },
125
+ });
126
+ const lowRatedPlugin = makePlugin({
127
+ capabilities: ['file:read', 'network:outbound'],
128
+ metadata: {
129
+ description: 'Low-rated plugin',
130
+ downloads: 100,
131
+ rating: 2.0,
132
+ lastUpdated: new Date(),
133
+ },
134
+ });
135
+ const popularResult = await scanner.analyzePlugin(dangerousPlugin);
136
+ const lowRatedResult = await scanner.analyzePlugin(lowRatedPlugin);
137
+ expect(popularResult.riskScore).toBeLessThanOrEqual(lowRatedResult.riskScore);
138
+ });
139
+ it('reduces risk for claude-marketplace source', async () => {
140
+ const marketplacePlugin = makePlugin({
141
+ capabilities: ['file:read', 'network:outbound'],
142
+ source: 'claude-marketplace',
143
+ });
144
+ const communityPlugin = makePlugin({
145
+ capabilities: ['file:read', 'network:outbound'],
146
+ source: 'community',
147
+ });
148
+ const marketplaceResult = await scanner.analyzePlugin(marketplacePlugin);
149
+ const communityResult = await scanner.analyzePlugin(communityPlugin);
150
+ expect(marketplaceResult.riskScore).toBeLessThanOrEqual(communityResult.riskScore);
151
+ });
152
+ it('caps risk score at 100', async () => {
153
+ const plugin = makePlugin({
154
+ capabilities: [
155
+ 'shell:execute', 'network:outbound',
156
+ 'file:write', 'startup:autorun',
157
+ 'clipboard:read',
158
+ ],
159
+ });
160
+ const result = await scanner.analyzePlugin(plugin);
161
+ expect(result.riskScore).toBeLessThanOrEqual(100);
162
+ });
163
+ });
164
+ // -------------------------------------------------------------------------
165
+ // recommendation thresholds
166
+ // -------------------------------------------------------------------------
167
+ describe('recommendations', () => {
168
+ it('returns safe for risk 0', async () => {
169
+ const plugin = makePlugin({ capabilities: [] });
170
+ const result = await scanner.analyzePlugin(plugin);
171
+ expect(result.recommendation).toBe('safe');
172
+ });
173
+ it('returns malicious when CRITICAL finding present', async () => {
174
+ const plugin = makePlugin({
175
+ capabilities: ['shell:execute', 'network:outbound'],
176
+ });
177
+ const result = await scanner.analyzePlugin(plugin);
178
+ expect(result.recommendation).toBe('malicious');
179
+ });
180
+ });
181
+ // -------------------------------------------------------------------------
182
+ // analyzePlugin with sourceCode triggers code analysis path
183
+ // -------------------------------------------------------------------------
184
+ describe('analyzePlugin with sourceCode', () => {
185
+ it('includes code findings when sourceCode is provided', async () => {
186
+ const plugin = makePlugin({
187
+ sourceCode: '// some plugin code\nconst x = 1;',
188
+ capabilities: [],
189
+ });
190
+ // scan is mocked to return no findings
191
+ const result = await scanner.analyzePlugin(plugin);
192
+ expect(result).toBeDefined();
193
+ expect(result.findings).toBeDefined();
194
+ });
195
+ });
196
+ // -------------------------------------------------------------------------
197
+ // error handling in scanMarketplace
198
+ // -------------------------------------------------------------------------
199
+ describe('error handling', () => {
200
+ it('handles analysis errors gracefully', async () => {
201
+ // Patch analyzePlugin to throw
202
+ const errorScanner = new MarketplaceScanner();
203
+ errorScanner.fetchPluginList = async () => [makePlugin()];
204
+ errorScanner.analyzePlugin = jest.fn().mockRejectedValue(new Error('analysis failed'));
205
+ const results = await errorScanner.scanMarketplace({ source: 'community' });
206
+ expect(results).toHaveLength(1);
207
+ expect(results[0]?.analysisSkipped).toContain('analysis failed');
208
+ expect(results[0]?.recommendation).toBe('review');
209
+ });
210
+ });
211
+ });
212
+ //# sourceMappingURL=MarketplaceScanner.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * RuleGenerator Tests
3
+ * Tests for AIRuleGenerator in src/ai-rules/RuleGenerator.ts
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=RuleGenerator.test.d.ts.map
@@ -0,0 +1,207 @@
1
+ /**
2
+ * RuleGenerator Tests
3
+ * Tests for AIRuleGenerator in src/ai-rules/RuleGenerator.ts
4
+ */
5
+ import { AIRuleGenerator } from '../ai-rules/RuleGenerator.js';
6
+ // Mock the LLM provider creation
7
+ jest.mock('../features/llmAnalysis.js', () => ({
8
+ createLlmProvider: jest.fn().mockReturnValue({
9
+ name: 'mock-provider',
10
+ analyze: jest.fn(),
11
+ }),
12
+ }));
13
+ import { createLlmProvider } from '../features/llmAnalysis.js';
14
+ function makeThreatReport(overrides = {}) {
15
+ return {
16
+ id: 'THREAT-001',
17
+ title: 'Prompt Injection Attack',
18
+ category: 'injection',
19
+ description: 'Attacker crafts prompts to override instructions',
20
+ attackVectors: ['user input', 'system prompt manipulation'],
21
+ iocs: ['IGNORE PREVIOUS', 'jailbreak', 'DAN mode'],
22
+ ...overrides,
23
+ };
24
+ }
25
+ describe('AIRuleGenerator', () => {
26
+ let generator;
27
+ let mockAnalyze;
28
+ beforeEach(() => {
29
+ jest.clearAllMocks();
30
+ // Get the mocked analyze function
31
+ const mockProvider = {
32
+ name: 'mock-provider',
33
+ analyze: jest.fn(),
34
+ };
35
+ mockAnalyze = mockProvider.analyze;
36
+ createLlmProvider.mockReturnValue(mockProvider);
37
+ generator = new AIRuleGenerator('test-api-key', 'gpt-4o-mini');
38
+ });
39
+ // -------------------------------------------------------------------------
40
+ // Constructor
41
+ // -------------------------------------------------------------------------
42
+ describe('constructor', () => {
43
+ it('creates an instance successfully when provider is returned', () => {
44
+ expect(generator).toBeInstanceOf(AIRuleGenerator);
45
+ });
46
+ it('throws when createLlmProvider returns null', () => {
47
+ createLlmProvider.mockReturnValueOnce(null);
48
+ expect(() => new AIRuleGenerator('key')).toThrow('Failed to create LLM provider');
49
+ });
50
+ it('uses default model when not specified', () => {
51
+ expect(() => new AIRuleGenerator('key')).not.toThrow();
52
+ });
53
+ it('accepts custom model parameter', () => {
54
+ expect(() => new AIRuleGenerator('key', 'gpt-3.5-turbo')).not.toThrow();
55
+ });
56
+ });
57
+ // -------------------------------------------------------------------------
58
+ // generateFromThreatIntel
59
+ // -------------------------------------------------------------------------
60
+ describe('generateFromThreatIntel', () => {
61
+ it('returns empty array for empty reports', async () => {
62
+ const rules = await generator.generateFromThreatIntel([]);
63
+ expect(rules).toEqual([]);
64
+ });
65
+ it('generates rules from a threat report', async () => {
66
+ const ruleResponse = JSON.stringify({
67
+ rules: [
68
+ {
69
+ id: 'INJ-001',
70
+ name: 'Prompt Injection Detection',
71
+ category: 'injection',
72
+ severity: 'HIGH',
73
+ description: 'Detects prompt injection patterns',
74
+ patterns: ['IGNORE PREVIOUS', 'jailbreak'],
75
+ fileTypes: ['md', 'json'],
76
+ components: ['agent', 'skill'],
77
+ remediation: 'Validate all user input',
78
+ confidence: 0.85,
79
+ },
80
+ ],
81
+ });
82
+ mockAnalyze.mockResolvedValue(ruleResponse);
83
+ const rules = await generator.generateFromThreatIntel([makeThreatReport()]);
84
+ expect(rules).toHaveLength(1);
85
+ expect(rules[0]?.id).toBe('INJ-001');
86
+ expect(rules[0]?.name).toBe('Prompt Injection Detection');
87
+ expect(rules[0]?.generatedFrom).toBe('THREAT-001');
88
+ expect(rules[0]?.validated).toBe(false);
89
+ expect(rules[0]?.enabled).toBe(false);
90
+ expect(rules[0]?.confidence).toBe(0.85);
91
+ });
92
+ it('converts patterns strings to RegExp objects', async () => {
93
+ const ruleResponse = JSON.stringify({
94
+ rules: [
95
+ {
96
+ id: 'INJ-002',
97
+ name: 'Test Rule',
98
+ category: 'injection',
99
+ severity: 'MEDIUM',
100
+ description: 'Test',
101
+ patterns: ['test.*pattern', '\\bmalicious\\b'],
102
+ fileTypes: ['md'],
103
+ components: ['agent'],
104
+ remediation: 'Fix it',
105
+ confidence: 0.7,
106
+ },
107
+ ],
108
+ });
109
+ mockAnalyze.mockResolvedValue(ruleResponse);
110
+ const rules = await generator.generateFromThreatIntel([makeThreatReport()]);
111
+ expect(rules[0]?.patterns[0]).toBeInstanceOf(RegExp);
112
+ expect(rules[0]?.patterns[1]).toBeInstanceOf(RegExp);
113
+ });
114
+ it('uses default confidence of 0.7 when not provided', async () => {
115
+ const ruleResponse = JSON.stringify({
116
+ rules: [
117
+ {
118
+ id: 'INJ-003',
119
+ name: 'No Confidence Rule',
120
+ category: 'injection',
121
+ severity: 'LOW',
122
+ description: 'Test',
123
+ patterns: ['test'],
124
+ fileTypes: ['md'],
125
+ components: [],
126
+ remediation: 'Fix',
127
+ // confidence omitted
128
+ },
129
+ ],
130
+ });
131
+ mockAnalyze.mockResolvedValue(ruleResponse);
132
+ const rules = await generator.generateFromThreatIntel([makeThreatReport()]);
133
+ expect(rules[0]?.confidence).toBe(0.7);
134
+ });
135
+ it('processes multiple threat reports', async () => {
136
+ const ruleResponse1 = JSON.stringify({
137
+ rules: [{ id: 'R-001', name: 'Rule 1', category: 'injection', severity: 'HIGH',
138
+ description: 'D', patterns: ['p1'], fileTypes: ['md'], components: [], remediation: 'fix', confidence: 0.8 }],
139
+ });
140
+ const ruleResponse2 = JSON.stringify({
141
+ rules: [{ id: 'R-002', name: 'Rule 2', category: 'credentials', severity: 'CRITICAL',
142
+ description: 'D', patterns: ['p2'], fileTypes: ['json'], components: [], remediation: 'fix', confidence: 0.9 }],
143
+ });
144
+ mockAnalyze
145
+ .mockResolvedValueOnce(ruleResponse1)
146
+ .mockResolvedValueOnce(ruleResponse2);
147
+ const reports = [
148
+ makeThreatReport({ id: 'T-001' }),
149
+ makeThreatReport({ id: 'T-002', category: 'credentials' }),
150
+ ];
151
+ const rules = await generator.generateFromThreatIntel(reports);
152
+ expect(rules).toHaveLength(2);
153
+ expect(rules[0]?.generatedFrom).toBe('T-001');
154
+ expect(rules[1]?.generatedFrom).toBe('T-002');
155
+ });
156
+ it('skips reports that fail to generate and continues with others', async () => {
157
+ const successResponse = JSON.stringify({
158
+ rules: [
159
+ {
160
+ id: 'R-001', name: 'Good Rule', category: 'injection', severity: 'HIGH',
161
+ description: 'D', patterns: ['p1'], fileTypes: ['md'], components: [], remediation: 'fix', confidence: 0.8,
162
+ },
163
+ ],
164
+ });
165
+ mockAnalyze
166
+ .mockRejectedValueOnce(new Error('LLM error'))
167
+ .mockResolvedValueOnce(successResponse);
168
+ const reports = [
169
+ makeThreatReport({ id: 'FAIL-001' }),
170
+ makeThreatReport({ id: 'SUCCESS-001' }),
171
+ ];
172
+ const rules = await generator.generateFromThreatIntel(reports);
173
+ // Only the second report should produce rules
174
+ expect(rules).toHaveLength(1);
175
+ expect(rules[0]?.generatedFrom).toBe('SUCCESS-001');
176
+ });
177
+ it('generates rules with references as empty array', async () => {
178
+ const ruleResponse = JSON.stringify({
179
+ rules: [
180
+ {
181
+ id: 'R-001', name: 'Rule', category: 'injection', severity: 'HIGH',
182
+ description: 'D', patterns: ['p'], fileTypes: ['md'], components: [], remediation: 'fix', confidence: 0.8,
183
+ },
184
+ ],
185
+ });
186
+ mockAnalyze.mockResolvedValue(ruleResponse);
187
+ const rules = await generator.generateFromThreatIntel([makeThreatReport()]);
188
+ expect(rules[0]?.references).toEqual([]);
189
+ });
190
+ it('handles multiple rules returned from a single report', async () => {
191
+ const ruleResponse = JSON.stringify({
192
+ rules: [
193
+ { id: 'R-001', name: 'Rule 1', category: 'injection', severity: 'HIGH',
194
+ description: 'D', patterns: ['p1'], fileTypes: ['md'], components: [], remediation: 'fix', confidence: 0.8 },
195
+ { id: 'R-002', name: 'Rule 2', category: 'injection', severity: 'MEDIUM',
196
+ description: 'D', patterns: ['p2'], fileTypes: ['json'], components: [], remediation: 'fix', confidence: 0.75 },
197
+ { id: 'R-003', name: 'Rule 3', category: 'injection', severity: 'LOW',
198
+ description: 'D', patterns: ['p3'], fileTypes: ['yaml'], components: [], remediation: 'fix', confidence: 0.6 },
199
+ ],
200
+ });
201
+ mockAnalyze.mockResolvedValue(ruleResponse);
202
+ const rules = await generator.generateFromThreatIntel([makeThreatReport()]);
203
+ expect(rules).toHaveLength(3);
204
+ });
205
+ });
206
+ });
207
+ //# sourceMappingURL=RuleGenerator.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * ThreatFeed Tests
3
+ * Tests for threat intelligence database operations: load, save, add, remove, query.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=ThreatFeed.test.d.ts.map