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,270 @@
1
+ /**
2
+ * Additional MCP Validator Tests
3
+ */
4
+ import { validateMcpConfigContent, validateMcpConfig, mcpAssessmentsToFindings, findAndValidateMcpConfigs, } from '../features/mcpValidator.js';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ describe('validateMcpConfigContent', () => {
9
+ it('returns valid=true for empty servers', () => {
10
+ const result = validateMcpConfigContent(JSON.stringify({ mcpServers: {} }));
11
+ expect(result.valid).toBe(true);
12
+ expect(result.assessments).toHaveLength(0);
13
+ });
14
+ it('returns valid=false for invalid JSON', () => {
15
+ const result = validateMcpConfigContent('not valid json {{{');
16
+ expect(result.valid).toBe(false);
17
+ expect(result.errors.length).toBeGreaterThan(0);
18
+ });
19
+ it('validates safe server with trusted npx command', () => {
20
+ const config = JSON.stringify({
21
+ mcpServers: {
22
+ 'safe-server': {
23
+ command: 'npx',
24
+ args: ['@modelcontextprotocol/server-filesystem@1.0.0', '/tmp'],
25
+ },
26
+ },
27
+ });
28
+ const result = validateMcpConfigContent(config);
29
+ expect(result.valid).toBe(true);
30
+ expect(result.assessments).toHaveLength(1);
31
+ // No issues for pinned trusted package
32
+ const assessment = result.assessments[0];
33
+ expect(assessment.issues.filter(i => i.type === 'unpinned-npx')).toHaveLength(0);
34
+ });
35
+ it('detects unpinned npx package', () => {
36
+ const config = JSON.stringify({
37
+ mcpServers: {
38
+ 'test-server': {
39
+ command: 'npx',
40
+ args: ['some-mcp-server'],
41
+ },
42
+ },
43
+ });
44
+ const result = validateMcpConfigContent(config);
45
+ expect(result.valid).toBe(true);
46
+ const assessment = result.assessments[0];
47
+ expect(assessment.issues.some(i => i.type === 'unpinned-npx')).toBe(true);
48
+ });
49
+ it('detects insecure HTTP transport', () => {
50
+ const config = JSON.stringify({
51
+ mcpServers: {
52
+ 'remote-server': {
53
+ url: 'http://api.example.com/mcp',
54
+ },
55
+ },
56
+ });
57
+ const result = validateMcpConfigContent(config);
58
+ expect(result.valid).toBe(true);
59
+ const assessment = result.assessments[0];
60
+ expect(assessment.issues.some(i => i.type === 'insecure-transport')).toBe(true);
61
+ });
62
+ it('allows HTTPS transport', () => {
63
+ const config = JSON.stringify({
64
+ mcpServers: {
65
+ 'secure-server': {
66
+ url: 'https://api.example.com/mcp',
67
+ },
68
+ },
69
+ });
70
+ const result = validateMcpConfigContent(config);
71
+ expect(result.valid).toBe(true);
72
+ const assessment = result.assessments[0];
73
+ expect(assessment.issues.filter(i => i.type === 'insecure-transport')).toHaveLength(0);
74
+ });
75
+ it('detects hardcoded secret in env vars', () => {
76
+ const config = JSON.stringify({
77
+ mcpServers: {
78
+ 'api-server': {
79
+ command: 'npx',
80
+ args: ['mcp-server@1.0.0'],
81
+ env: {
82
+ API_TOKEN: 'sk-real-secret-token-123456',
83
+ },
84
+ },
85
+ },
86
+ });
87
+ const result = validateMcpConfigContent(config);
88
+ expect(result.valid).toBe(true);
89
+ const assessment = result.assessments[0];
90
+ expect(assessment.issues.some(i => i.type === 'hardcoded-secret')).toBe(true);
91
+ });
92
+ it('allows env var references', () => {
93
+ const config = JSON.stringify({
94
+ mcpServers: {
95
+ 'api-server': {
96
+ command: 'npx',
97
+ args: ['mcp-server@1.0.0'],
98
+ env: {
99
+ API_TOKEN: '${MY_TOKEN}',
100
+ },
101
+ },
102
+ },
103
+ });
104
+ const result = validateMcpConfigContent(config);
105
+ expect(result.valid).toBe(true);
106
+ const assessment = result.assessments[0];
107
+ expect(assessment.issues.filter(i => i.type === 'hardcoded-secret')).toHaveLength(0);
108
+ });
109
+ it('detects tunnel service', () => {
110
+ const config = JSON.stringify({
111
+ mcpServers: {
112
+ 'tunnel-server': {
113
+ url: 'https://abc123.ngrok.io/mcp',
114
+ },
115
+ },
116
+ });
117
+ const result = validateMcpConfigContent(config);
118
+ expect(result.valid).toBe(true);
119
+ const assessment = result.assessments[0];
120
+ expect(assessment.issues.some(i => i.type === 'tunnel-service')).toBe(true);
121
+ });
122
+ it('handles servers using alternative "servers" key', () => {
123
+ const config = JSON.stringify({
124
+ servers: {
125
+ 'alt-server': {
126
+ command: 'node',
127
+ args: ['./server.js'],
128
+ },
129
+ },
130
+ });
131
+ const result = validateMcpConfigContent(config);
132
+ expect(result.valid).toBe(true);
133
+ expect(result.assessments).toHaveLength(1);
134
+ });
135
+ it('detects shell expansion in command', () => {
136
+ const config = JSON.stringify({
137
+ mcpServers: {
138
+ 'shell-server': {
139
+ command: 'bash',
140
+ args: ['-c', 'echo $(whoami)'],
141
+ },
142
+ },
143
+ });
144
+ const result = validateMcpConfigContent(config);
145
+ expect(result.valid).toBe(true);
146
+ const assessment = result.assessments[0];
147
+ expect(assessment.issues.some(i => i.type === 'shell-expansion')).toBe(true);
148
+ });
149
+ });
150
+ describe('validateMcpConfig', () => {
151
+ let tmpDir;
152
+ beforeEach(() => {
153
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-mcp-test-'));
154
+ });
155
+ afterEach(() => {
156
+ fs.rmSync(tmpDir, { recursive: true, force: true });
157
+ });
158
+ it('returns error for non-existent file', () => {
159
+ const result = validateMcpConfig('/nonexistent/mcp.json');
160
+ expect(result.valid).toBe(false);
161
+ expect(result.errors[0]).toContain('not found');
162
+ });
163
+ it('validates a valid MCP config file', () => {
164
+ const filePath = path.join(tmpDir, '.mcp.json');
165
+ fs.writeFileSync(filePath, JSON.stringify({ mcpServers: {} }));
166
+ const result = validateMcpConfig(filePath);
167
+ expect(result.valid).toBe(true);
168
+ });
169
+ });
170
+ describe('mcpAssessmentsToFindings', () => {
171
+ it('returns empty array for no assessments', () => {
172
+ const findings = mcpAssessmentsToFindings([], '/project/.mcp.json');
173
+ expect(findings).toHaveLength(0);
174
+ });
175
+ it('converts assessments with issues to findings', () => {
176
+ const assessments = [
177
+ {
178
+ serverName: 'test-server',
179
+ command: 'npx',
180
+ args: ['test'],
181
+ url: undefined,
182
+ capabilities: [],
183
+ riskLevel: 'high',
184
+ issues: [
185
+ {
186
+ type: 'unpinned-npx',
187
+ severity: 'MEDIUM',
188
+ description: 'Unpinned npx package',
189
+ remediation: 'Pin package version',
190
+ },
191
+ ],
192
+ },
193
+ ];
194
+ const findings = mcpAssessmentsToFindings(assessments, '/project/.mcp.json');
195
+ expect(findings).toHaveLength(1);
196
+ expect(findings[0]?.severity).toBe('MEDIUM');
197
+ expect(findings[0]?.ruleId).toMatch(/^MCP-/);
198
+ });
199
+ it('categorizes secret issues correctly', () => {
200
+ const assessments = [
201
+ {
202
+ serverName: 'test-server',
203
+ command: 'npx',
204
+ args: [],
205
+ url: undefined,
206
+ capabilities: [],
207
+ riskLevel: 'high',
208
+ issues: [
209
+ {
210
+ type: 'hardcoded-secret',
211
+ severity: 'HIGH',
212
+ description: 'Secret found',
213
+ remediation: 'Use env refs',
214
+ },
215
+ ],
216
+ },
217
+ ];
218
+ const findings = mcpAssessmentsToFindings(assessments, '/project/.mcp.json');
219
+ expect(findings[0]?.category).toBe('credentials');
220
+ });
221
+ it('assigns correct risk scores', () => {
222
+ const assessments = [
223
+ {
224
+ serverName: 'test',
225
+ command: undefined,
226
+ args: [],
227
+ url: undefined,
228
+ capabilities: [],
229
+ riskLevel: 'critical',
230
+ issues: [
231
+ { type: 'x', severity: 'CRITICAL', description: 'x', remediation: 'x' },
232
+ { type: 'y', severity: 'HIGH', description: 'y', remediation: 'y' },
233
+ { type: 'z', severity: 'MEDIUM', description: 'z', remediation: 'z' },
234
+ { type: 'w', severity: 'LOW', description: 'w', remediation: 'w' },
235
+ ],
236
+ },
237
+ ];
238
+ const findings = mcpAssessmentsToFindings(assessments, '/project/.mcp.json');
239
+ expect(findings[0]?.riskScore).toBe(95);
240
+ expect(findings[1]?.riskScore).toBe(80);
241
+ expect(findings[2]?.riskScore).toBe(60);
242
+ expect(findings[3]?.riskScore).toBe(40);
243
+ });
244
+ });
245
+ describe('findAndValidateMcpConfigs', () => {
246
+ let tmpDir;
247
+ beforeEach(() => {
248
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-mcp-find-'));
249
+ });
250
+ afterEach(() => {
251
+ fs.rmSync(tmpDir, { recursive: true, force: true });
252
+ });
253
+ it('returns empty when no MCP configs found', () => {
254
+ const result = findAndValidateMcpConfigs(tmpDir);
255
+ expect(result.configs).toHaveLength(0);
256
+ expect(result.totalIssues).toBe(0);
257
+ });
258
+ it('finds and validates .mcp.json', () => {
259
+ fs.writeFileSync(path.join(tmpDir, '.mcp.json'), JSON.stringify({ mcpServers: {} }));
260
+ const result = findAndValidateMcpConfigs(tmpDir);
261
+ expect(result.configs).toHaveLength(1);
262
+ expect(result.configs[0]?.path).toContain('.mcp.json');
263
+ });
264
+ it('finds and validates mcp.json', () => {
265
+ fs.writeFileSync(path.join(tmpDir, 'mcp.json'), JSON.stringify({ mcpServers: {} }));
266
+ const result = findAndValidateMcpConfigs(tmpDir);
267
+ expect(result.configs).toHaveLength(1);
268
+ });
269
+ });
270
+ //# sourceMappingURL=mcpValidator.extra.test.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Additional PatternMatcher Tests
3
+ * Covers uncovered branches: excludePatterns, excludeContext, requireContext,
4
+ * minMatchLength, createPatternMatcher, multiple matches per line
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=patternMatcherExtra.test.d.ts.map
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Additional PatternMatcher Tests
3
+ * Covers uncovered branches: excludePatterns, excludeContext, requireContext,
4
+ * minMatchLength, createPatternMatcher, multiple matches per line
5
+ */
6
+ import { matchRule, matchRules, createPatternMatcher } from '../scanner/PatternMatcher.js';
7
+ function makeFile(overrides = {}) {
8
+ return {
9
+ path: '/project/.claude/agents/test.md',
10
+ relativePath: 'agents/test.md',
11
+ type: 'md',
12
+ component: 'agent',
13
+ size: 100,
14
+ modified: new Date(),
15
+ ...overrides,
16
+ };
17
+ }
18
+ function makeRule(overrides = {}) {
19
+ return {
20
+ id: 'TEST-001',
21
+ name: 'Test Rule',
22
+ category: 'injection',
23
+ severity: 'HIGH',
24
+ description: 'Test description',
25
+ patterns: [/dangerous/gi],
26
+ fileTypes: ['md'],
27
+ components: ['agent', 'skill', 'hook', 'plugin', 'mcp', 'settings', 'ai-config-md', 'rules-file'],
28
+ remediation: 'Fix it',
29
+ references: [],
30
+ enabled: true,
31
+ ...overrides,
32
+ };
33
+ }
34
+ describe('matchRule', () => {
35
+ const opts = { contextLines: 2 };
36
+ it('returns empty array when rule does not apply to file type', () => {
37
+ const rule = makeRule({ fileTypes: ['json'] });
38
+ const file = makeFile({ type: 'md' });
39
+ const findings = matchRule(rule, file, 'dangerous content', opts);
40
+ expect(findings).toHaveLength(0);
41
+ });
42
+ it('returns empty array when rule does not apply to component', () => {
43
+ const rule = makeRule({ components: ['hook'] });
44
+ const file = makeFile({ component: 'agent' });
45
+ const findings = matchRule(rule, file, 'dangerous content', opts);
46
+ expect(findings).toHaveLength(0);
47
+ });
48
+ it('finds matches in content', () => {
49
+ const rule = makeRule();
50
+ const file = makeFile();
51
+ const findings = matchRule(rule, file, 'this is dangerous', opts);
52
+ expect(findings).toHaveLength(1);
53
+ expect(findings[0]?.ruleId).toBe('TEST-001');
54
+ expect(findings[0]?.line).toBe(1);
55
+ });
56
+ it('handles multiple matches across different lines', () => {
57
+ const rule = makeRule();
58
+ const file = makeFile();
59
+ const content = 'line one dangerous\nline two safe\nline three dangerous';
60
+ const findings = matchRule(rule, file, content, opts);
61
+ expect(findings).toHaveLength(2);
62
+ });
63
+ it('excludes matches below minMatchLength', () => {
64
+ const rule = makeRule({
65
+ patterns: [/bad/gi],
66
+ minMatchLength: 10, // "bad" is only 3 chars
67
+ });
68
+ const file = makeFile();
69
+ const findings = matchRule(rule, file, 'this is bad', opts);
70
+ expect(findings).toHaveLength(0);
71
+ });
72
+ it('keeps matches at or above minMatchLength', () => {
73
+ const rule = makeRule({
74
+ patterns: [/dangerouslylongmatch/gi],
75
+ minMatchLength: 5,
76
+ });
77
+ const file = makeFile();
78
+ const findings = matchRule(rule, file, 'dangerouslylongmatch here', opts);
79
+ expect(findings).toHaveLength(1);
80
+ });
81
+ it('excludes matches matching excludePatterns', () => {
82
+ const rule = makeRule({
83
+ excludePatterns: [/example\./i],
84
+ });
85
+ const file = makeFile();
86
+ // Line contains 'dangerous' but also 'example.' which triggers exclusion
87
+ const findings = matchRule(rule, file, 'dangerous example.com', opts);
88
+ expect(findings).toHaveLength(0);
89
+ });
90
+ it('does not exclude matches when excludePattern does not match', () => {
91
+ const rule = makeRule({
92
+ excludePatterns: [/safe-example/i],
93
+ });
94
+ const file = makeFile();
95
+ const findings = matchRule(rule, file, 'dangerous real attack', opts);
96
+ expect(findings).toHaveLength(1);
97
+ });
98
+ it('excludes matches based on excludeContext', () => {
99
+ const rule = makeRule({
100
+ excludeContext: [/this is documentation/i],
101
+ });
102
+ const file = makeFile();
103
+ const content = 'this is documentation\ndangerous content here\nend of doc';
104
+ const findings = matchRule(rule, file, content, opts);
105
+ expect(findings).toHaveLength(0);
106
+ });
107
+ it('requires context to be present', () => {
108
+ const rule = makeRule({
109
+ requireContext: [/must-be-present/i],
110
+ });
111
+ const file = makeFile();
112
+ const content = 'dangerous content without required context';
113
+ const findings = matchRule(rule, file, content, opts);
114
+ expect(findings).toHaveLength(0);
115
+ });
116
+ it('keeps match when required context is present', () => {
117
+ const rule = makeRule({
118
+ requireContext: [/must-be-present/i],
119
+ });
120
+ const file = makeFile();
121
+ const content = 'must-be-present\ndangerous content here\n';
122
+ const findings = matchRule(rule, file, content, opts);
123
+ expect(findings).toHaveLength(1);
124
+ });
125
+ it('increases risk score for hook components', () => {
126
+ const agentFile = makeFile({ component: 'agent' });
127
+ const hookFile = makeFile({ component: 'hook' });
128
+ const rule = makeRule({
129
+ components: ['agent', 'hook', 'plugin', 'mcp', 'skill', 'settings', 'ai-config-md', 'rules-file'],
130
+ });
131
+ const agentFindings = matchRule(rule, agentFile, 'dangerous content', opts);
132
+ const hookFindings = matchRule(rule, hookFile, 'dangerous content', opts);
133
+ expect(agentFindings).toHaveLength(1);
134
+ expect(hookFindings).toHaveLength(1);
135
+ // Hook components get higher risk scores (1.2x multiplier)
136
+ expect(hookFindings[0]?.riskScore).toBeGreaterThanOrEqual(agentFindings[0].riskScore);
137
+ });
138
+ it('deduplicates multiple matches on the same line', () => {
139
+ const rule = makeRule({
140
+ patterns: [/bad/gi, /dangerous/gi],
141
+ });
142
+ const file = makeFile();
143
+ // "dangerous" matches pattern 2, but let's have two patterns that match same line
144
+ const findings = matchRule(rule, file, 'this is bad dangerous content', opts);
145
+ // Should only create one finding per line, not two
146
+ const line1Findings = findings.filter(f => f.line === 1);
147
+ expect(line1Findings).toHaveLength(1);
148
+ });
149
+ it('returns empty for disabled rule in matchRules', () => {
150
+ const rule = makeRule({ enabled: false });
151
+ const file = makeFile();
152
+ const findings = matchRules([rule], file, 'dangerous content', opts);
153
+ expect(findings).toHaveLength(0);
154
+ });
155
+ });
156
+ describe('matchRules', () => {
157
+ it('returns findings from multiple enabled rules', () => {
158
+ const rule1 = makeRule({ id: 'TEST-001', patterns: [/dangerous/gi] });
159
+ const rule2 = makeRule({ id: 'TEST-002', patterns: [/secret/gi] });
160
+ const file = makeFile();
161
+ const content = 'dangerous secret content';
162
+ const findings = matchRules([rule1, rule2], file, content, { contextLines: 0 });
163
+ expect(findings.length).toBeGreaterThanOrEqual(2);
164
+ });
165
+ it('skips disabled rules', () => {
166
+ const enabledRule = makeRule({ id: 'ENABLED-001', patterns: [/dangerous/gi] });
167
+ const disabledRule = makeRule({ id: 'DISABLED-001', patterns: [/secret/gi], enabled: false });
168
+ const file = makeFile();
169
+ const findings = matchRules([enabledRule, disabledRule], file, 'dangerous secret', { contextLines: 0 });
170
+ expect(findings.every(f => f.ruleId === 'ENABLED-001')).toBe(true);
171
+ });
172
+ });
173
+ describe('createPatternMatcher', () => {
174
+ it('returns an object with matchRule and matchRules methods', () => {
175
+ const matcher = createPatternMatcher({ contextLines: 3 });
176
+ expect(typeof matcher.matchRule).toBe('function');
177
+ expect(typeof matcher.matchRules).toBe('function');
178
+ });
179
+ it('uses the provided options for matching', () => {
180
+ const matcher = createPatternMatcher({ contextLines: 0 });
181
+ const rule = makeRule();
182
+ const file = makeFile();
183
+ const findings = matcher.matchRule(rule, file, 'dangerous content');
184
+ expect(findings).toHaveLength(1);
185
+ expect(findings[0]?.context).toHaveLength(1); // Just the matching line
186
+ });
187
+ it('matchRules method works with multiple rules', () => {
188
+ const matcher = createPatternMatcher({ contextLines: 0 });
189
+ const rules = [
190
+ makeRule({ id: 'R1', patterns: [/dangerous/gi] }),
191
+ makeRule({ id: 'R2', patterns: [/secret/gi] }),
192
+ ];
193
+ const file = makeFile();
194
+ const findings = matcher.matchRules(rules, file, 'dangerous secret');
195
+ expect(findings.length).toBeGreaterThanOrEqual(2);
196
+ });
197
+ });
198
+ //# sourceMappingURL=patternMatcherExtra.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Rules Patterns Common Tests
3
+ * Tests for shared regex building blocks used by security detection rules.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=patternsCommon.test.d.ts.map
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Rules Patterns Common Tests
3
+ * Tests for shared regex building blocks used by security detection rules.
4
+ */
5
+ import { CREDENTIAL_KEYWORDS, HIGH_ENTROPY_SUFFIX, buildHarvestPattern, buildCredentialAssignPattern, } from '../rules/patterns/common.js';
6
+ // ---------------------------------------------------------------------------
7
+ // CREDENTIAL_KEYWORDS
8
+ // ---------------------------------------------------------------------------
9
+ describe('CREDENTIAL_KEYWORDS', () => {
10
+ it('is a non-empty string', () => {
11
+ expect(typeof CREDENTIAL_KEYWORDS).toBe('string');
12
+ expect(CREDENTIAL_KEYWORDS.length).toBeGreaterThan(0);
13
+ });
14
+ it('contains common credential keywords', () => {
15
+ expect(CREDENTIAL_KEYWORDS).toContain('token');
16
+ expect(CREDENTIAL_KEYWORDS).toContain('secret');
17
+ expect(CREDENTIAL_KEYWORDS).toContain('password');
18
+ });
19
+ });
20
+ // ---------------------------------------------------------------------------
21
+ // HIGH_ENTROPY_SUFFIX
22
+ // ---------------------------------------------------------------------------
23
+ describe('HIGH_ENTROPY_SUFFIX', () => {
24
+ it('is a non-empty string', () => {
25
+ expect(typeof HIGH_ENTROPY_SUFFIX).toBe('string');
26
+ expect(HIGH_ENTROPY_SUFFIX.length).toBeGreaterThan(0);
27
+ });
28
+ it('produces a pattern that matches 20+ alphanumeric characters', () => {
29
+ const re = new RegExp(HIGH_ENTROPY_SUFFIX);
30
+ expect(re.test('abcdefghijklmnopqrstu')).toBe(true); // 21 chars
31
+ expect(re.test('short')).toBe(false);
32
+ });
33
+ });
34
+ // ---------------------------------------------------------------------------
35
+ // buildHarvestPattern
36
+ // ---------------------------------------------------------------------------
37
+ describe('buildHarvestPattern', () => {
38
+ it('returns a RegExp', () => {
39
+ const pattern = buildHarvestPattern('send');
40
+ expect(pattern).toBeInstanceOf(RegExp);
41
+ });
42
+ it('is case-insensitive and global', () => {
43
+ const pattern = buildHarvestPattern('send');
44
+ expect(pattern.flags).toContain('g');
45
+ expect(pattern.flags).toContain('i');
46
+ });
47
+ it('matches a simple harvest sentence with credential keyword', () => {
48
+ const pattern = buildHarvestPattern('send');
49
+ expect(pattern.test('send the password now')).toBe(true);
50
+ });
51
+ it('matches with different credential keyword words', () => {
52
+ const pattern = buildHarvestPattern('transmit');
53
+ expect(pattern.test('transmit the secret value')).toBe(true);
54
+ });
55
+ it('does not match when verb is absent', () => {
56
+ const pattern = buildHarvestPattern('upload');
57
+ expect(pattern.test('download the secret value')).toBe(false);
58
+ });
59
+ it('throws on verb containing dangerous metacharacter *', () => {
60
+ expect(() => buildHarvestPattern('send*')).toThrow();
61
+ });
62
+ it('throws on verb containing dangerous metacharacter +', () => {
63
+ expect(() => buildHarvestPattern('send+')).toThrow();
64
+ });
65
+ it('throws on verb containing dangerous metacharacter |', () => {
66
+ expect(() => buildHarvestPattern('send|recv')).toThrow();
67
+ });
68
+ it('throws on verb containing dangerous metacharacter (', () => {
69
+ expect(() => buildHarvestPattern('(send)')).toThrow();
70
+ });
71
+ });
72
+ // ---------------------------------------------------------------------------
73
+ // buildCredentialAssignPattern
74
+ // ---------------------------------------------------------------------------
75
+ describe('buildCredentialAssignPattern', () => {
76
+ it('returns a RegExp', () => {
77
+ const pattern = buildCredentialAssignPattern('api_key');
78
+ expect(pattern).toBeInstanceOf(RegExp);
79
+ });
80
+ it('is case-insensitive and global', () => {
81
+ const pattern = buildCredentialAssignPattern('api_key');
82
+ expect(pattern.flags).toContain('g');
83
+ expect(pattern.flags).toContain('i');
84
+ });
85
+ it('matches assignment with = and quoted high-entropy value', () => {
86
+ const pattern = buildCredentialAssignPattern('api_key');
87
+ expect(pattern.test('api_key = "abcdefghijklmnopqrstuvwxyz"')).toBe(true);
88
+ });
89
+ it('matches assignment with : separator', () => {
90
+ const pattern = buildCredentialAssignPattern('secret');
91
+ expect(pattern.test("secret: 'abcdefghijklmnopqrstuvwxyz'")).toBe(true);
92
+ });
93
+ it('does not match short values (less than 20 chars)', () => {
94
+ const pattern = buildCredentialAssignPattern('api_key');
95
+ expect(pattern.test('api_key = "short"')).toBe(false);
96
+ });
97
+ it('throws on keyword containing dangerous metacharacter *', () => {
98
+ expect(() => buildCredentialAssignPattern('api*key')).toThrow();
99
+ });
100
+ it('throws on keyword containing dangerous metacharacter +', () => {
101
+ expect(() => buildCredentialAssignPattern('api+key')).toThrow();
102
+ });
103
+ it('throws on keyword containing dangerous metacharacter \\', () => {
104
+ expect(() => buildCredentialAssignPattern('api\\key')).toThrow();
105
+ });
106
+ });
107
+ //# sourceMappingURL=patternsCommon.test.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Policy Enforcement Tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=policyEnforcement.test.d.ts.map