ferret-scan 2.1.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +15 -11
  3. package/bin/ferret.js +109 -13
  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/analyzers/AstAnalyzer.d.ts +5 -1
  126. package/dist/analyzers/AstAnalyzer.js +25 -4
  127. package/dist/features/customRules.js +22 -29
  128. package/dist/features/ignoreComments.js +5 -5
  129. package/dist/features/mcpTrustScore.d.ts +17 -0
  130. package/dist/features/mcpTrustScore.js +74 -0
  131. package/dist/features/mcpValidator.d.ts +2 -0
  132. package/dist/features/mcpValidator.js +13 -0
  133. package/dist/features/policyEnforcement.d.ts +22 -22
  134. package/dist/features/policyEnforcement.js +3 -2
  135. package/dist/intelligence/ThreatFeed.js +207 -62
  136. package/dist/remediation/Fixer.js +56 -30
  137. package/dist/remediation/Quarantine.js +79 -11
  138. package/dist/reporters/ConsoleReporter.js +10 -0
  139. package/dist/reporters/HtmlReporter.js +5 -0
  140. package/dist/reporters/SarifReporter.d.ts +1 -0
  141. package/dist/reporters/SarifReporter.js +1 -0
  142. package/dist/rules/ai-specific.js +8 -8
  143. package/dist/rules/backdoors.js +12 -12
  144. package/dist/rules/correlationRules.js +6 -6
  145. package/dist/rules/index.d.ts +1 -0
  146. package/dist/rules/index.js +10 -1
  147. package/dist/rules/injection.js +8 -8
  148. package/dist/rules/patterns/common.d.ts +34 -0
  149. package/dist/rules/patterns/common.js +48 -0
  150. package/dist/scanner/IAnalyzer.d.ts +19 -0
  151. package/dist/scanner/IAnalyzer.js +5 -0
  152. package/dist/scanner/PatternMatcher.js +19 -2
  153. package/dist/scanner/Scanner.js +64 -125
  154. package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
  155. package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
  156. package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
  157. package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
  158. package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
  159. package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
  160. package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
  161. package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
  162. package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
  163. package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
  164. package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
  165. package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
  166. package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
  167. package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
  168. package/dist/types.d.ts +23 -0
  169. package/dist/types.js +1 -1
  170. package/dist/utils/baseline.d.ts +15 -2
  171. package/dist/utils/baseline.js +50 -19
  172. package/dist/utils/contentCache.d.ts +39 -0
  173. package/dist/utils/contentCache.js +77 -0
  174. package/dist/utils/glob.d.ts +50 -0
  175. package/dist/utils/glob.js +84 -0
  176. package/dist/utils/pathSecurity.js +1 -0
  177. package/dist/utils/safeRegex.d.ts +55 -0
  178. package/dist/utils/safeRegex.js +130 -0
  179. package/dist/utils/schemas.d.ts +70 -64
  180. package/dist/utils/schemas.js +13 -0
  181. package/package.json +34 -19
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Additional Scanner Tests
3
+ * Tests for getExitCode and other scanner utility functions
4
+ */
5
+ // Mock ora to prevent ESM issues in scanner
6
+ jest.mock('ora', () => ({
7
+ __esModule: true,
8
+ default: jest.fn().mockReturnValue({
9
+ start: jest.fn().mockReturnThis(),
10
+ stop: jest.fn().mockReturnThis(),
11
+ succeed: jest.fn().mockReturnThis(),
12
+ fail: jest.fn().mockReturnThis(),
13
+ text: '',
14
+ }),
15
+ }));
16
+ // Mock all the ESM-only analyzers
17
+ jest.mock('../scanner/analyzers/EntropyAnalyzer.js', () => ({
18
+ EntropyAnalyzer: jest.fn().mockImplementation(() => ({
19
+ analyze: jest.fn().mockResolvedValue([]),
20
+ })),
21
+ }));
22
+ jest.mock('../scanner/analyzers/McpAnalyzer.js', () => ({
23
+ McpAnalyzer: jest.fn().mockImplementation(() => ({
24
+ analyze: jest.fn().mockResolvedValue([]),
25
+ })),
26
+ }));
27
+ jest.mock('../scanner/analyzers/DependencyAnalyzer.js', () => ({
28
+ DependencyAnalyzer: jest.fn().mockImplementation(() => ({
29
+ analyze: jest.fn().mockResolvedValue([]),
30
+ })),
31
+ }));
32
+ jest.mock('../scanner/analyzers/CapabilityAnalyzer.js', () => ({
33
+ CapabilityAnalyzer: jest.fn().mockImplementation(() => ({
34
+ analyze: jest.fn().mockResolvedValue([]),
35
+ })),
36
+ }));
37
+ jest.mock('../scanner/analyzers/LlmAnalyzer.js', () => ({
38
+ LlmAnalyzer: jest.fn().mockImplementation(() => ({
39
+ analyze: jest.fn().mockResolvedValue([]),
40
+ })),
41
+ }));
42
+ jest.mock('../scanner/analyzers/SemanticAnalyzer.js', () => ({
43
+ SemanticAnalyzer: jest.fn().mockImplementation(() => ({
44
+ analyze: jest.fn().mockResolvedValue([]),
45
+ })),
46
+ }));
47
+ jest.mock('../scanner/analyzers/ThreatIntelAnalyzer.js', () => ({
48
+ ThreatIntelAnalyzer: jest.fn().mockImplementation(() => ({
49
+ analyze: jest.fn().mockResolvedValue([]),
50
+ })),
51
+ }));
52
+ import { getExitCode } from '../scanner/Scanner.js';
53
+ import { DEFAULT_CONFIG } from '../types.js';
54
+ function makeFinding(severity) {
55
+ return {
56
+ ruleId: 'TEST-001',
57
+ ruleName: 'Test',
58
+ severity,
59
+ category: 'injection',
60
+ file: '/test.md',
61
+ relativePath: 'test.md',
62
+ line: 1,
63
+ match: 'bad',
64
+ context: [],
65
+ remediation: 'fix',
66
+ timestamp: new Date(),
67
+ riskScore: 75,
68
+ };
69
+ }
70
+ function makeScanResult(overrides = {}) {
71
+ return {
72
+ success: true,
73
+ startTime: new Date(),
74
+ endTime: new Date(),
75
+ duration: 100,
76
+ scannedPaths: ['/project'],
77
+ totalFiles: 1,
78
+ analyzedFiles: 1,
79
+ skippedFiles: 0,
80
+ findings: [],
81
+ findingsBySeverity: {
82
+ CRITICAL: [],
83
+ HIGH: [],
84
+ MEDIUM: [],
85
+ LOW: [],
86
+ INFO: [],
87
+ },
88
+ findingsByCategory: {},
89
+ overallRiskScore: 0,
90
+ summary: { critical: 0, high: 0, medium: 0, low: 0, info: 0, total: 0 },
91
+ errors: [],
92
+ ...overrides,
93
+ };
94
+ }
95
+ function makeConfig(failOn = 'HIGH') {
96
+ return { ...DEFAULT_CONFIG, failOn };
97
+ }
98
+ describe('getExitCode', () => {
99
+ it('returns 0 when no findings', () => {
100
+ const result = makeScanResult();
101
+ expect(getExitCode(result, makeConfig('HIGH'))).toBe(0);
102
+ });
103
+ it('returns 3 when success=false', () => {
104
+ const result = makeScanResult({ success: false });
105
+ expect(getExitCode(result, makeConfig())).toBe(3);
106
+ });
107
+ it('returns 2 for CRITICAL findings with failOn=CRITICAL', () => {
108
+ const result = makeScanResult({
109
+ findingsBySeverity: {
110
+ CRITICAL: [makeFinding('CRITICAL')],
111
+ HIGH: [], MEDIUM: [], LOW: [], INFO: [],
112
+ },
113
+ });
114
+ expect(getExitCode(result, makeConfig('CRITICAL'))).toBe(2);
115
+ });
116
+ it('returns 2 for CRITICAL findings with failOn=HIGH', () => {
117
+ const result = makeScanResult({
118
+ findingsBySeverity: {
119
+ CRITICAL: [makeFinding('CRITICAL')],
120
+ HIGH: [], MEDIUM: [], LOW: [], INFO: [],
121
+ },
122
+ });
123
+ expect(getExitCode(result, makeConfig('HIGH'))).toBe(2);
124
+ });
125
+ it('returns 1 for HIGH findings with failOn=HIGH', () => {
126
+ const result = makeScanResult({
127
+ findingsBySeverity: {
128
+ CRITICAL: [],
129
+ HIGH: [makeFinding('HIGH')],
130
+ MEDIUM: [], LOW: [], INFO: [],
131
+ },
132
+ });
133
+ expect(getExitCode(result, makeConfig('HIGH'))).toBe(1);
134
+ });
135
+ it('returns 0 for HIGH findings with failOn=CRITICAL', () => {
136
+ const result = makeScanResult({
137
+ findingsBySeverity: {
138
+ CRITICAL: [],
139
+ HIGH: [makeFinding('HIGH')],
140
+ MEDIUM: [], LOW: [], INFO: [],
141
+ },
142
+ });
143
+ expect(getExitCode(result, makeConfig('CRITICAL'))).toBe(0);
144
+ });
145
+ it('returns 1 for MEDIUM findings with failOn=MEDIUM', () => {
146
+ const result = makeScanResult({
147
+ findingsBySeverity: {
148
+ CRITICAL: [], HIGH: [],
149
+ MEDIUM: [makeFinding('MEDIUM')],
150
+ LOW: [], INFO: [],
151
+ },
152
+ });
153
+ expect(getExitCode(result, makeConfig('MEDIUM'))).toBe(1);
154
+ });
155
+ it('returns 0 for LOW findings with failOn=HIGH', () => {
156
+ const result = makeScanResult({
157
+ findingsBySeverity: {
158
+ CRITICAL: [], HIGH: [], MEDIUM: [],
159
+ LOW: [makeFinding('LOW')],
160
+ INFO: [],
161
+ },
162
+ });
163
+ expect(getExitCode(result, makeConfig('HIGH'))).toBe(0);
164
+ });
165
+ it('returns 1 for LOW findings with failOn=LOW', () => {
166
+ const result = makeScanResult({
167
+ findingsBySeverity: {
168
+ CRITICAL: [], HIGH: [], MEDIUM: [],
169
+ LOW: [makeFinding('LOW')],
170
+ INFO: [],
171
+ },
172
+ });
173
+ expect(getExitCode(result, makeConfig('LOW'))).toBe(1);
174
+ });
175
+ it('returns 0 for INFO findings with failOn=LOW', () => {
176
+ const result = makeScanResult({
177
+ findingsBySeverity: {
178
+ CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [],
179
+ INFO: [makeFinding('INFO')],
180
+ },
181
+ });
182
+ expect(getExitCode(result, makeConfig('LOW'))).toBe(0);
183
+ });
184
+ it('returns 1 for INFO findings with failOn=INFO', () => {
185
+ const result = makeScanResult({
186
+ findingsBySeverity: {
187
+ CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [],
188
+ INFO: [makeFinding('INFO')],
189
+ },
190
+ });
191
+ expect(getExitCode(result, makeConfig('INFO'))).toBe(1);
192
+ });
193
+ });
194
+ //# sourceMappingURL=scannerExtra.test.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Scanner MITRE Atlas and LLM initialization tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=scannerMitre.test.d.ts.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Scanner MITRE Atlas and LLM initialization tests
3
+ */
4
+ jest.mock('ora', () => ({
5
+ __esModule: true,
6
+ default: jest.fn().mockReturnValue({
7
+ start: jest.fn().mockReturnThis(),
8
+ stop: jest.fn().mockReturnThis(),
9
+ succeed: jest.fn().mockReturnThis(),
10
+ fail: jest.fn().mockReturnThis(),
11
+ text: '',
12
+ }),
13
+ }));
14
+ import { scan } from '../scanner/Scanner.js';
15
+ import { DEFAULT_CONFIG } from '../types.js';
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import * as os from 'node:os';
19
+ function makeConfig(overrides = {}) {
20
+ return {
21
+ ...DEFAULT_CONFIG,
22
+ ci: true,
23
+ verbose: false,
24
+ llmAnalysis: false,
25
+ threatIntel: false,
26
+ semanticAnalysis: false,
27
+ correlationAnalysis: false,
28
+ entropyAnalysis: false,
29
+ mcpValidation: false,
30
+ dependencyAnalysis: false,
31
+ capabilityMapping: false,
32
+ mitreAtlas: false,
33
+ mitreAtlasCatalog: {
34
+ ...DEFAULT_CONFIG.mitreAtlasCatalog,
35
+ enabled: false,
36
+ },
37
+ ...overrides,
38
+ };
39
+ }
40
+ describe('scan() with mitreAtlasCatalog', () => {
41
+ let tmpDir;
42
+ beforeEach(() => {
43
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-mitre-'));
44
+ });
45
+ afterEach(() => {
46
+ fs.rmSync(tmpDir, { recursive: true, force: true });
47
+ });
48
+ it('handles mitreAtlasCatalog.enabled=true with failed catalog load', async () => {
49
+ const agentsDir = path.join(tmpDir, '.claude', 'agents');
50
+ fs.mkdirSync(agentsDir, { recursive: true });
51
+ fs.writeFileSync(path.join(agentsDir, 'agent.md'), '# Agent');
52
+ const config = makeConfig({
53
+ paths: [tmpDir],
54
+ mitreAtlasCatalog: {
55
+ enabled: true,
56
+ autoUpdate: false,
57
+ sourceUrl: 'http://localhost:9999/nonexistent', // Will fail
58
+ cachePath: path.join(tmpDir, 'nonexistent-cache.json'),
59
+ cacheTtlHours: 0,
60
+ timeoutMs: 100,
61
+ forceRefresh: false,
62
+ },
63
+ });
64
+ // Should succeed even with catalog load failure
65
+ const result = await scan(config);
66
+ expect(result.success).toBe(true);
67
+ // Should have a non-fatal error about catalog
68
+ expect(result.errors.some(e => e.message.includes('catalog'))).toBe(true);
69
+ });
70
+ it('handles LLM enabled but missing API key', async () => {
71
+ const agentsDir = path.join(tmpDir, '.claude', 'agents');
72
+ fs.mkdirSync(agentsDir, { recursive: true });
73
+ fs.writeFileSync(path.join(agentsDir, 'agent.md'), '# Agent');
74
+ // Make sure env var doesn't exist
75
+ const origKey = process.env['NONEXISTENT_KEY_ABC123'];
76
+ delete process.env['NONEXISTENT_KEY_ABC123'];
77
+ const config = makeConfig({
78
+ paths: [tmpDir],
79
+ llmAnalysis: true,
80
+ llm: {
81
+ ...DEFAULT_CONFIG.llm,
82
+ provider: 'openai-compatible',
83
+ baseUrl: 'https://api.openai.com/v1/chat/completions',
84
+ apiKeyEnv: 'NONEXISTENT_KEY_ABC123',
85
+ },
86
+ });
87
+ const result = await scan(config);
88
+ expect(result.success).toBe(true);
89
+ // Should have a non-fatal error about LLM
90
+ expect(result.errors.some(e => e.message.includes('LLM') || e.message.includes('provider'))).toBe(true);
91
+ if (origKey !== undefined)
92
+ process.env['NONEXISTENT_KEY_ABC123'] = origKey;
93
+ });
94
+ });
95
+ describe('scan() with custom rules', () => {
96
+ let tmpDir;
97
+ beforeEach(() => {
98
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-custom-'));
99
+ });
100
+ afterEach(() => {
101
+ fs.rmSync(tmpDir, { recursive: true, force: true });
102
+ });
103
+ it('loads custom rules from file', async () => {
104
+ const agentsDir = path.join(tmpDir, '.claude', 'agents');
105
+ fs.mkdirSync(agentsDir, { recursive: true });
106
+ fs.writeFileSync(path.join(agentsDir, 'agent.md'), '# Agent');
107
+ const rulesPath = path.join(tmpDir, 'custom-rules.json');
108
+ fs.writeFileSync(rulesPath, JSON.stringify({
109
+ version: '1.0',
110
+ rules: [
111
+ {
112
+ id: 'CUSTOM-001',
113
+ name: 'Test Custom Rule',
114
+ category: 'injection',
115
+ severity: 'HIGH',
116
+ description: 'Test rule',
117
+ patterns: ['test-pattern-xyz'],
118
+ },
119
+ ],
120
+ }));
121
+ const config = makeConfig({
122
+ paths: [tmpDir],
123
+ customRules: [rulesPath],
124
+ });
125
+ const result = await scan(config);
126
+ expect(result.success).toBe(true);
127
+ });
128
+ it('handles invalid custom rules file gracefully', async () => {
129
+ const agentsDir = path.join(tmpDir, '.claude', 'agents');
130
+ fs.mkdirSync(agentsDir, { recursive: true });
131
+ fs.writeFileSync(path.join(agentsDir, 'agent.md'), '# Agent');
132
+ const config = makeConfig({
133
+ paths: [tmpDir],
134
+ customRules: ['/nonexistent/rules.json'],
135
+ });
136
+ const result = await scan(config);
137
+ expect(result.success).toBe(true);
138
+ // Invalid rules file should generate a non-fatal error
139
+ });
140
+ });
141
+ //# sourceMappingURL=scannerMitre.test.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Scanner SSRF protection and remote rules tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=scannerSSRF.test.d.ts.map
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Scanner SSRF protection and remote rules tests
3
+ */
4
+ jest.mock('ora', () => ({
5
+ __esModule: true,
6
+ default: jest.fn().mockReturnValue({
7
+ start: jest.fn().mockReturnThis(),
8
+ stop: jest.fn().mockReturnThis(),
9
+ succeed: jest.fn().mockReturnThis(),
10
+ fail: jest.fn().mockReturnThis(),
11
+ text: '',
12
+ }),
13
+ }));
14
+ import { scan } from '../scanner/Scanner.js';
15
+ import { DEFAULT_CONFIG } from '../types.js';
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import * as os from 'node:os';
19
+ function makeConfig(overrides = {}) {
20
+ return {
21
+ ...DEFAULT_CONFIG,
22
+ ci: true,
23
+ verbose: false,
24
+ llmAnalysis: false,
25
+ threatIntel: false,
26
+ semanticAnalysis: false,
27
+ correlationAnalysis: false,
28
+ entropyAnalysis: false,
29
+ mcpValidation: false,
30
+ dependencyAnalysis: false,
31
+ capabilityMapping: false,
32
+ mitreAtlas: false,
33
+ mitreAtlasCatalog: {
34
+ ...DEFAULT_CONFIG.mitreAtlasCatalog,
35
+ enabled: false,
36
+ },
37
+ ...overrides,
38
+ };
39
+ }
40
+ describe('scan() SSRF protection', () => {
41
+ let tmpDir;
42
+ beforeEach(() => {
43
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-ssrf-'));
44
+ });
45
+ afterEach(() => {
46
+ fs.rmSync(tmpDir, { recursive: true, force: true });
47
+ });
48
+ it('blocks remote custom rules URLs when allowRemoteRules=false', async () => {
49
+ const agentsDir = path.join(tmpDir, '.claude', 'agents');
50
+ fs.mkdirSync(agentsDir, { recursive: true });
51
+ fs.writeFileSync(path.join(agentsDir, 'agent.md'), '# Agent');
52
+ const config = makeConfig({
53
+ paths: [tmpDir],
54
+ customRules: ['https://malicious.example.com/rules.json'],
55
+ allowRemoteRules: false,
56
+ });
57
+ // Should succeed but skip the remote URL
58
+ const result = await scan(config);
59
+ expect(result.success).toBe(true);
60
+ // The remote URL should be blocked - no findings from it
61
+ });
62
+ it('allows remote custom rules URLs when allowRemoteRules=true', async () => {
63
+ const agentsDir = path.join(tmpDir, '.claude', 'agents');
64
+ fs.mkdirSync(agentsDir, { recursive: true });
65
+ fs.writeFileSync(path.join(agentsDir, 'agent.md'), '# Agent');
66
+ // Mock fetch to return valid rules
67
+ globalThis.fetch = jest.fn().mockResolvedValue({
68
+ ok: true,
69
+ text: () => Promise.resolve(JSON.stringify({
70
+ version: '1.0',
71
+ rules: [{
72
+ id: 'CUSTOM-001',
73
+ name: 'Remote Rule',
74
+ category: 'injection',
75
+ severity: 'HIGH',
76
+ description: 'Test',
77
+ patterns: ['nonexistent-pattern-xyz-abc'],
78
+ }],
79
+ })),
80
+ });
81
+ const config = makeConfig({
82
+ paths: [tmpDir],
83
+ customRules: ['https://trusted.example.com/rules.json'],
84
+ allowRemoteRules: true,
85
+ });
86
+ const result = await scan(config);
87
+ expect(result.success).toBe(true);
88
+ });
89
+ it('handles conventional rule discovery from .ferret/ directory', async () => {
90
+ const agentsDir = path.join(tmpDir, '.claude', 'agents');
91
+ fs.mkdirSync(agentsDir, { recursive: true });
92
+ fs.writeFileSync(path.join(agentsDir, 'agent.md'), '# Agent');
93
+ // Create a .ferret/rules.json in the scan path
94
+ const ferretDir = path.join(tmpDir, '.ferret');
95
+ fs.mkdirSync(ferretDir);
96
+ fs.writeFileSync(path.join(ferretDir, 'rules.json'), JSON.stringify({
97
+ version: '1.0',
98
+ rules: [{
99
+ id: 'CUSTOM-001',
100
+ name: 'Convention Rule',
101
+ category: 'injection',
102
+ severity: 'HIGH',
103
+ description: 'Test',
104
+ patterns: ['convention-pattern-xyz'],
105
+ }],
106
+ }));
107
+ const config = makeConfig({ paths: [tmpDir] });
108
+ const result = await scan(config);
109
+ expect(result.success).toBe(true);
110
+ });
111
+ });
112
+ describe('scan() documentation dampening', () => {
113
+ let tmpDir;
114
+ beforeEach(() => {
115
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-docpath-'));
116
+ });
117
+ afterEach(() => {
118
+ fs.rmSync(tmpDir, { recursive: true, force: true });
119
+ });
120
+ it('dampens CRED-001 CRITICAL in documentation paths', async () => {
121
+ // Create a docs directory with a markdown file that might trigger CRED-001
122
+ const docsDir = path.join(tmpDir, 'docs');
123
+ fs.mkdirSync(docsDir);
124
+ // Create a file that would be a "documentation path"
125
+ fs.writeFileSync(path.join(docsDir, 'readme.md'), '# Documentation\nThis is documentation about usage.');
126
+ const agentsDir = path.join(tmpDir, '.claude', 'agents');
127
+ fs.mkdirSync(agentsDir, { recursive: true });
128
+ fs.writeFileSync(path.join(agentsDir, 'agent.md'), '# Agent\nSome config here.');
129
+ const config = makeConfig({
130
+ paths: [tmpDir],
131
+ docDampening: true,
132
+ });
133
+ const result = await scan(config);
134
+ expect(result.success).toBe(true);
135
+ });
136
+ it('scan with references/ directory detects documentation path', async () => {
137
+ const refsDir = path.join(tmpDir, '.claude', 'plugins', 'marketplaces', 'plugin1', 'references');
138
+ fs.mkdirSync(refsDir, { recursive: true });
139
+ fs.writeFileSync(path.join(refsDir, 'api.md'), '# API Reference\nDocumentation here.');
140
+ const config = makeConfig({
141
+ paths: [tmpDir],
142
+ docDampening: true,
143
+ marketplaceMode: 'all',
144
+ });
145
+ const result = await scan(config);
146
+ expect(result.success).toBe(true);
147
+ });
148
+ });
149
+ //# sourceMappingURL=scannerSSRF.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Schemas Tests
3
+ * Tests for safeParseJSON and validateSchema utilities
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=schemas.test.d.ts.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Schemas Tests
3
+ * Tests for safeParseJSON and validateSchema utilities
4
+ */
5
+ import { safeParseJSON, validateSchema, ConfigFileSchema, BaselineSchema, } from '../utils/schemas.js';
6
+ import { z } from 'zod';
7
+ describe('safeParseJSON', () => {
8
+ const SimpleSchema = z.object({
9
+ name: z.string(),
10
+ value: z.number(),
11
+ });
12
+ it('returns success for valid JSON matching schema', () => {
13
+ const result = safeParseJSON('{"name":"test","value":42}', SimpleSchema);
14
+ expect(result.success).toBe(true);
15
+ if (result.success) {
16
+ expect(result.data.name).toBe('test');
17
+ expect(result.data.value).toBe(42);
18
+ }
19
+ });
20
+ it('returns failure for invalid JSON', () => {
21
+ const result = safeParseJSON('{invalid json}', SimpleSchema);
22
+ expect(result.success).toBe(false);
23
+ if (!result.success) {
24
+ expect(result.error).toContain('JSON parse error');
25
+ }
26
+ });
27
+ it('returns failure when schema validation fails', () => {
28
+ const result = safeParseJSON('{"name":123,"value":"not-a-number"}', SimpleSchema);
29
+ expect(result.success).toBe(false);
30
+ if (!result.success) {
31
+ expect(result.error).toContain('Schema validation failed');
32
+ }
33
+ });
34
+ it('returns failure for content exceeding maxLength', () => {
35
+ const bigContent = JSON.stringify({ name: 'x'.repeat(100), value: 1 });
36
+ const result = safeParseJSON(bigContent, SimpleSchema, { maxLength: 10 });
37
+ expect(result.success).toBe(false);
38
+ if (!result.success) {
39
+ expect(result.error).toContain('exceeds maximum length');
40
+ }
41
+ });
42
+ it('uses default 10MB maxLength', () => {
43
+ const content = JSON.stringify({ name: 'test', value: 1 });
44
+ const result = safeParseJSON(content, SimpleSchema);
45
+ expect(result.success).toBe(true);
46
+ });
47
+ it('parses ConfigFileSchema', () => {
48
+ const content = JSON.stringify({
49
+ severity: ['CRITICAL', 'HIGH'],
50
+ ignore: ['node_modules/**'],
51
+ });
52
+ const result = safeParseJSON(content, ConfigFileSchema);
53
+ expect(result.success).toBe(true);
54
+ });
55
+ it('rejects unknown severity values in ConfigFileSchema', () => {
56
+ const content = JSON.stringify({
57
+ severity: ['INVALID_SEVERITY'],
58
+ });
59
+ const result = safeParseJSON(content, ConfigFileSchema);
60
+ expect(result.success).toBe(false);
61
+ });
62
+ it('accepts empty object for ConfigFileSchema (all fields optional)', () => {
63
+ const result = safeParseJSON('{}', ConfigFileSchema);
64
+ expect(result.success).toBe(true);
65
+ });
66
+ it('limits error reporting to 5 issues', () => {
67
+ const StrictSchema = z.object({
68
+ a: z.number(),
69
+ b: z.number(),
70
+ c: z.number(),
71
+ d: z.number(),
72
+ e: z.number(),
73
+ f: z.number(),
74
+ });
75
+ const result = safeParseJSON('{"a":"x","b":"x","c":"x","d":"x","e":"x","f":"x"}', StrictSchema);
76
+ expect(result.success).toBe(false);
77
+ // Error message should not be excessively long
78
+ if (!result.success) {
79
+ expect(result.error.length).toBeLessThan(5000);
80
+ }
81
+ });
82
+ });
83
+ describe('validateSchema', () => {
84
+ const PersonSchema = z.object({
85
+ name: z.string().min(1),
86
+ age: z.number().int().min(0),
87
+ });
88
+ it('returns success for valid data', () => {
89
+ const result = validateSchema({ name: 'Alice', age: 30 }, PersonSchema);
90
+ expect(result.success).toBe(true);
91
+ if (result.success) {
92
+ expect(result.data.name).toBe('Alice');
93
+ }
94
+ });
95
+ it('returns failure for invalid data', () => {
96
+ const result = validateSchema({ name: '', age: -1 }, PersonSchema);
97
+ expect(result.success).toBe(false);
98
+ if (!result.success) {
99
+ expect(result.error).toContain('Schema validation failed');
100
+ }
101
+ });
102
+ it('returns failure for wrong type', () => {
103
+ const result = validateSchema('not-an-object', PersonSchema);
104
+ expect(result.success).toBe(false);
105
+ });
106
+ it('returns failure for null', () => {
107
+ const result = validateSchema(null, PersonSchema);
108
+ expect(result.success).toBe(false);
109
+ });
110
+ it('validates BaselineSchema structure', () => {
111
+ const baseline = {
112
+ version: '1.0.0',
113
+ createdDate: '2024-01-01T00:00:00Z',
114
+ lastUpdated: '2024-01-01T00:00:00Z',
115
+ findings: [],
116
+ };
117
+ const result = validateSchema(baseline, BaselineSchema);
118
+ expect(result.success).toBe(true);
119
+ });
120
+ it('rejects BaselineSchema with missing required fields', () => {
121
+ const result = validateSchema({ version: '1.0.0' }, BaselineSchema);
122
+ expect(result.success).toBe(false);
123
+ });
124
+ });
125
+ //# sourceMappingURL=schemas.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Additional Webhook Tests
3
+ * Covers sendWebhook with slack/discord/teams includeDetails formatting
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=webhooks.extra.test.d.ts.map