ferret-scan 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/CHANGELOG.md +12 -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 +24 -18
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Capability Mapping Tests
3
+ */
4
+ import { detectAgentType } from '../features/capabilityMapping.js';
5
+ describe('detectAgentType', () => {
6
+ it('detects claude-code from .claude path', () => {
7
+ expect(detectAgentType('/home/user/.claude/agents/test.md')).toBe('claude-code');
8
+ });
9
+ it('detects claude-code from CLAUDE.md', () => {
10
+ expect(detectAgentType('/project/CLAUDE.md')).toBe('claude-code');
11
+ });
12
+ it('detects claude-code from claude.json', () => {
13
+ expect(detectAgentType('/project/claude.json')).toBe('claude-code');
14
+ });
15
+ it('detects cursor from .cursorrules', () => {
16
+ expect(detectAgentType('/project/.cursorrules')).toBe('cursor');
17
+ });
18
+ it('detects cursor from .cursor directory', () => {
19
+ expect(detectAgentType('/project/.cursor/settings.json')).toBe('cursor');
20
+ });
21
+ it('detects windsurf from .windsurfrules', () => {
22
+ expect(detectAgentType('/project/.windsurfrules')).toBe('windsurf');
23
+ });
24
+ it('detects continue from .continuerc', () => {
25
+ expect(detectAgentType('/project/.continuerc')).toBe('continue');
26
+ });
27
+ it('detects continue from .continuerc path', () => {
28
+ expect(detectAgentType('/project/.continuerc')).toBe('continue');
29
+ });
30
+ it('detects aider from .aider.conf.yml', () => {
31
+ expect(detectAgentType('/project/.aider.conf.yml')).toBe('aider');
32
+ });
33
+ it('detects cline from .clinerules', () => {
34
+ expect(detectAgentType('/project/.clinerules')).toBe('cline');
35
+ });
36
+ it('detects mcp from .mcp.json', () => {
37
+ expect(detectAgentType('/project/.mcp.json')).toBe('mcp');
38
+ });
39
+ it('detects mcp from mcp.json', () => {
40
+ expect(detectAgentType('/project/mcp.json')).toBe('mcp');
41
+ });
42
+ it('returns null for unknown file', () => {
43
+ expect(detectAgentType('/project/some-random-file.txt')).toBeNull();
44
+ });
45
+ it('returns null for generic config file', () => {
46
+ expect(detectAgentType('/project/config.yaml')).toBeNull();
47
+ });
48
+ });
49
+ //# sourceMappingURL=capabilityMapping.test.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Additional Capability Mapping Tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=capabilityMappingExtra.test.d.ts.map
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Additional Capability Mapping Tests
3
+ */
4
+ import { analyzeCapabilities, analyzeCapabilitiesContent, capabilityProfileToFindings, } from '../features/capabilityMapping.js';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ describe('analyzeCapabilities', () => {
9
+ let tmpDir;
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-cap-'));
12
+ });
13
+ afterEach(() => {
14
+ fs.rmSync(tmpDir, { recursive: true, force: true });
15
+ });
16
+ it('returns null for non-existent file', () => {
17
+ const result = analyzeCapabilities('/nonexistent/config.json');
18
+ expect(result).toBeNull();
19
+ });
20
+ it('returns null for unknown agent type', () => {
21
+ const filePath = path.join(tmpDir, 'random-file.txt');
22
+ fs.writeFileSync(filePath, 'some content');
23
+ const result = analyzeCapabilities(filePath);
24
+ expect(result).toBeNull();
25
+ });
26
+ it('analyzes a claude settings.json', () => {
27
+ const claudeDir = path.join(tmpDir, '.claude');
28
+ fs.mkdirSync(claudeDir);
29
+ const filePath = path.join(claudeDir, 'settings.json');
30
+ fs.writeFileSync(filePath, JSON.stringify({
31
+ allowedTools: ['Bash', 'Read', 'Write'],
32
+ }));
33
+ const result = analyzeCapabilities(filePath);
34
+ expect(result).not.toBeNull();
35
+ expect(result?.agentType).toContain('Claude');
36
+ });
37
+ it('analyzes a .mcp.json file', () => {
38
+ const filePath = path.join(tmpDir, '.mcp.json');
39
+ fs.writeFileSync(filePath, JSON.stringify({
40
+ mcpServers: {
41
+ 'my-server': {
42
+ command: 'npx',
43
+ args: ['my-server@1.0.0'],
44
+ },
45
+ },
46
+ }));
47
+ const result = analyzeCapabilities(filePath);
48
+ expect(result).not.toBeNull();
49
+ });
50
+ });
51
+ describe('analyzeCapabilitiesContent', () => {
52
+ it('returns null for unknown agent type', () => {
53
+ const result = analyzeCapabilitiesContent('/project/random.txt', '{}');
54
+ expect(result).toBeNull();
55
+ });
56
+ it('analyzes claude settings content', () => {
57
+ const content = JSON.stringify({
58
+ allowedTools: ['Bash', 'Read', 'Write'],
59
+ });
60
+ const result = analyzeCapabilitiesContent('/project/.claude/settings.json', content);
61
+ expect(result).not.toBeNull();
62
+ expect(result?.agentType).toContain('Claude');
63
+ });
64
+ it('parses JSON config with mcpServers', () => {
65
+ const content = JSON.stringify({
66
+ mcpServers: {
67
+ 'fs-server': {
68
+ command: 'npx',
69
+ args: ['@modelcontextprotocol/server-filesystem@1.0.0'],
70
+ },
71
+ 'url-server': {
72
+ url: 'https://api.example.com/mcp',
73
+ },
74
+ },
75
+ });
76
+ const result = analyzeCapabilitiesContent('/project/.claude/settings.json', content);
77
+ expect(result).not.toBeNull();
78
+ expect(result?.capabilities.some(c => c.type === 'code_execution')).toBe(true);
79
+ expect(result?.capabilities.some(c => c.type === 'network_access')).toBe(true);
80
+ });
81
+ it('parses YAML-like config for aider', () => {
82
+ const content = `
83
+ auto-commits: true
84
+ edit-format: diff
85
+ lint-cmd: npm run lint
86
+ `;
87
+ const result = analyzeCapabilitiesContent('/project/.aider.conf.yml', content);
88
+ expect(result).not.toBeNull();
89
+ expect(result?.capabilities.length).toBeGreaterThan(0);
90
+ });
91
+ it('handles invalid JSON gracefully (falls back to YAML parsing)', () => {
92
+ const content = `terminalAccess: true\nfileAccess: false\n`;
93
+ const result = analyzeCapabilitiesContent('/project/cursor.json', content);
94
+ // May return null or a result depending on parsing
95
+ expect(result === null || typeof result === 'object').toBe(true);
96
+ });
97
+ it('computes correct overall risk for critical capabilities', () => {
98
+ const content = JSON.stringify({
99
+ allowedTools: ['Bash', 'task'],
100
+ });
101
+ const result = analyzeCapabilitiesContent('/project/.claude/settings.json', content);
102
+ expect(result).not.toBeNull();
103
+ if (result) {
104
+ const hasCritical = result.capabilities.some(c => c.riskLevel === 'critical' && c.permission === 'allowed');
105
+ if (hasCritical) {
106
+ expect(result.overallRisk).toBe('critical');
107
+ }
108
+ }
109
+ });
110
+ it('generates recommendations for network access', () => {
111
+ const content = JSON.stringify({
112
+ allowedTools: ['webfetch'],
113
+ });
114
+ const result = analyzeCapabilitiesContent('/project/.claude/settings.json', content);
115
+ if (result && result.capabilities.some(c => c.type === 'network_access' && c.permission === 'allowed')) {
116
+ expect(result.recommendations.some(r => r.toLowerCase().includes('network'))).toBe(true);
117
+ }
118
+ });
119
+ });
120
+ describe('capabilityProfileToFindings', () => {
121
+ it('returns empty array for profile with no allowed high-risk capabilities', () => {
122
+ const profile = {
123
+ agentType: 'Claude Code',
124
+ configFile: '/project/.claude/settings.json',
125
+ capabilities: [
126
+ {
127
+ type: 'file_read',
128
+ permission: 'allowed',
129
+ riskLevel: 'low',
130
+ description: 'Can read files',
131
+ source: 'read',
132
+ },
133
+ ],
134
+ overallRisk: 'low',
135
+ recommendations: [],
136
+ };
137
+ const findings = capabilityProfileToFindings(profile);
138
+ expect(findings).toHaveLength(0);
139
+ });
140
+ it('returns findings for critical capabilities', () => {
141
+ const profile = {
142
+ agentType: 'Claude Code',
143
+ configFile: '/project/.claude/settings.json',
144
+ capabilities: [
145
+ {
146
+ type: 'shell_access',
147
+ permission: 'allowed',
148
+ riskLevel: 'critical',
149
+ description: 'Can execute shell commands',
150
+ source: 'bash',
151
+ },
152
+ ],
153
+ overallRisk: 'critical',
154
+ recommendations: ['Review shell access'],
155
+ };
156
+ const findings = capabilityProfileToFindings(profile);
157
+ expect(findings.length).toBeGreaterThan(0);
158
+ expect(findings[0]?.severity).toBe('HIGH'); // critical risk -> HIGH severity
159
+ });
160
+ it('returns findings for high-risk capabilities', () => {
161
+ const profile = {
162
+ agentType: 'Claude Code',
163
+ configFile: '/project/.claude/settings.json',
164
+ capabilities: [
165
+ {
166
+ type: 'network_access',
167
+ permission: 'allowed',
168
+ riskLevel: 'high',
169
+ description: 'Can make network requests',
170
+ source: 'webfetch',
171
+ },
172
+ ],
173
+ overallRisk: 'high',
174
+ recommendations: [],
175
+ };
176
+ const findings = capabilityProfileToFindings(profile);
177
+ expect(findings.length).toBeGreaterThan(0);
178
+ expect(findings[0]?.severity).toBe('MEDIUM'); // high risk -> MEDIUM severity
179
+ });
180
+ it('skips denied capabilities', () => {
181
+ const profile = {
182
+ agentType: 'Claude Code',
183
+ configFile: '/project/.claude/settings.json',
184
+ capabilities: [
185
+ {
186
+ type: 'shell_access',
187
+ permission: 'denied',
188
+ riskLevel: 'critical',
189
+ description: 'Shell access is denied',
190
+ source: 'bash',
191
+ },
192
+ ],
193
+ overallRisk: 'low',
194
+ recommendations: [],
195
+ };
196
+ const findings = capabilityProfileToFindings(profile);
197
+ expect(findings).toHaveLength(0);
198
+ });
199
+ });
200
+ //# sourceMappingURL=capabilityMappingExtra.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Additional Compliance Mapper Tests
3
+ * Tests for getControlRecommendations with credentials/exfiltration findings
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=complianceExtra.test.d.ts.map
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Additional Compliance Mapper Tests
3
+ * Tests for getControlRecommendations with credentials/exfiltration findings
4
+ */
5
+ import { ComplianceMapper } from '../compliance/ComplianceMapper.js';
6
+ function makeFinding(overrides = {}) {
7
+ return {
8
+ ruleId: 'TEST-001',
9
+ ruleName: 'Test',
10
+ severity: 'HIGH',
11
+ category: 'injection',
12
+ file: '/project/test.md',
13
+ relativePath: 'test.md',
14
+ line: 1,
15
+ match: 'test',
16
+ context: [],
17
+ remediation: 'fix',
18
+ timestamp: new Date(),
19
+ riskScore: 75,
20
+ ...overrides,
21
+ };
22
+ }
23
+ function makeScanResult(findings = []) {
24
+ return {
25
+ success: true,
26
+ startTime: new Date(),
27
+ endTime: new Date(),
28
+ duration: 100,
29
+ scannedPaths: ['/project'],
30
+ totalFiles: 5,
31
+ analyzedFiles: 5,
32
+ skippedFiles: 0,
33
+ findings,
34
+ findingsBySeverity: {
35
+ CRITICAL: findings.filter(f => f.severity === 'CRITICAL'),
36
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
37
+ MEDIUM: findings.filter(f => f.severity === 'MEDIUM'),
38
+ LOW: findings.filter(f => f.severity === 'LOW'),
39
+ INFO: [],
40
+ },
41
+ findingsByCategory: {},
42
+ overallRiskScore: 0,
43
+ summary: {
44
+ critical: findings.filter(f => f.severity === 'CRITICAL').length,
45
+ high: findings.filter(f => f.severity === 'HIGH').length,
46
+ medium: 0, low: 0, info: 0,
47
+ total: findings.length,
48
+ },
49
+ errors: [],
50
+ };
51
+ }
52
+ describe('ComplianceMapper - credential recommendations', () => {
53
+ let mapper;
54
+ beforeEach(() => {
55
+ mapper = new ComplianceMapper();
56
+ });
57
+ it('generates credentials recommendations in SOC2 when credentials findings exist', async () => {
58
+ const findings = [
59
+ makeFinding({ category: 'credentials', severity: 'CRITICAL' }),
60
+ ];
61
+ const result = makeScanResult(findings);
62
+ const assessment = await mapper.assessSOC2(result);
63
+ expect(assessment.framework).toBe('SOC2');
64
+ // Check if any control assessments have recommendations about credentials
65
+ const allRecs = assessment.controlAssessments.flatMap(a => a.recommendations);
66
+ expect(allRecs.length).toBeGreaterThanOrEqual(0); // Just check it runs
67
+ });
68
+ it('generates exfiltration recommendations in SOC2 when exfiltration findings exist', async () => {
69
+ const findings = [
70
+ makeFinding({ category: 'exfiltration', severity: 'CRITICAL' }),
71
+ ];
72
+ const result = makeScanResult(findings);
73
+ const assessment = await mapper.assessSOC2(result);
74
+ const allRecs = assessment.controlAssessments.flatMap(a => a.recommendations);
75
+ expect(allRecs.length).toBeGreaterThanOrEqual(0);
76
+ });
77
+ it('generates non-compliant recommendations when SOC2 fails', async () => {
78
+ const findings = [
79
+ makeFinding({ category: 'credentials', severity: 'CRITICAL' }),
80
+ makeFinding({ category: 'injection', severity: 'HIGH' }),
81
+ ];
82
+ const result = makeScanResult(findings);
83
+ const assessment = await mapper.assessSOC2(result);
84
+ expect(assessment.nonCompliantControls.length).toBeGreaterThanOrEqual(0);
85
+ });
86
+ it('assesses ISO27001 with credentials findings', async () => {
87
+ const findings = [
88
+ makeFinding({ category: 'credentials', severity: 'HIGH' }),
89
+ ];
90
+ const result = makeScanResult(findings);
91
+ const assessment = await mapper.assessISO27001(result);
92
+ expect(assessment.framework).toBe('ISO27001');
93
+ expect(assessment.overallScore).toBeGreaterThanOrEqual(0);
94
+ expect(assessment.overallScore).toBeLessThanOrEqual(100);
95
+ });
96
+ it('assesses GDPR with credentials findings', async () => {
97
+ const findings = [
98
+ makeFinding({ category: 'credentials', severity: 'HIGH' }),
99
+ makeFinding({ category: 'exfiltration', severity: 'CRITICAL' }),
100
+ ];
101
+ const result = makeScanResult(findings);
102
+ const assessment = await mapper.assessGDPR(result);
103
+ expect(assessment.framework).toBe('GDPR');
104
+ expect(typeof assessment.overallScore).toBe('number');
105
+ });
106
+ it('generates no non-compliant when no findings', async () => {
107
+ const result = makeScanResult([]);
108
+ const assessment = await mapper.assessSOC2(result);
109
+ expect(assessment.nonCompliantControls.length).toBe(0);
110
+ expect(assessment.overallScore).toBe(100);
111
+ });
112
+ it('assessments have required fields', async () => {
113
+ const result = makeScanResult();
114
+ const assessment = await mapper.assessSOC2(result);
115
+ expect(assessment.framework).toBeDefined();
116
+ expect(assessment.assessmentDate).toBeInstanceOf(Date);
117
+ expect(Array.isArray(assessment.controlAssessments)).toBe(true);
118
+ expect(Array.isArray(assessment.recommendations)).toBe(true);
119
+ });
120
+ });
121
+ //# sourceMappingURL=complianceExtra.test.js.map
@@ -95,7 +95,7 @@ describe('loadConfig defaults', () => {
95
95
  expect(config.watch).toBe(false);
96
96
  expect(config.configOnly).toBe(false);
97
97
  expect(config.docDampening).toBe(true);
98
- expect(config.redact).toBe(false);
98
+ expect(config.redact).toBe(true);
99
99
  expect(config.ignoreComments).toBe(true);
100
100
  expect(config.mitreAtlas).toBe(true);
101
101
  });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Config Loader Tests
3
+ * Tests for loadConfig and getAIConfigPaths in utils/config.ts
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=configLoader.test.d.ts.map
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Config Loader Tests
3
+ * Tests for loadConfig and getAIConfigPaths in utils/config.ts
4
+ */
5
+ import { loadConfig, getAIConfigPaths, getClaudeConfigPaths } from '../utils/config.js';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ function makeCliOptions(overrides = {}) {
10
+ return {
11
+ ...overrides,
12
+ };
13
+ }
14
+ describe('loadConfig', () => {
15
+ it('returns default config when no options provided', () => {
16
+ const config = loadConfig(makeCliOptions());
17
+ expect(config).toBeDefined();
18
+ expect(config.severities).toBeDefined();
19
+ expect(Array.isArray(config.severities)).toBe(true);
20
+ expect(config.categories).toBeDefined();
21
+ });
22
+ it('applies path from CLI options', () => {
23
+ const config = loadConfig(makeCliOptions({ path: '/my/project' }));
24
+ expect(config.paths).toContain(path.resolve('/my/project'));
25
+ });
26
+ it('applies severity filter', () => {
27
+ const config = loadConfig(makeCliOptions({ severity: 'CRITICAL,HIGH' }));
28
+ expect(config.severities).toContain('CRITICAL');
29
+ expect(config.severities).toContain('HIGH');
30
+ expect(config.severities).not.toContain('LOW');
31
+ });
32
+ it('ignores invalid severity values', () => {
33
+ const config = loadConfig(makeCliOptions({ severity: 'INVALID,CRITICAL' }));
34
+ expect(config.severities).toContain('CRITICAL');
35
+ expect(config.severities).not.toContain('INVALID');
36
+ });
37
+ it('applies categories filter', () => {
38
+ const config = loadConfig(makeCliOptions({ categories: 'injection,credentials' }));
39
+ expect(config.categories).toContain('injection');
40
+ expect(config.categories).toContain('credentials');
41
+ });
42
+ it('ignores invalid category values', () => {
43
+ const config = loadConfig(makeCliOptions({ categories: 'invalid-cat,injection' }));
44
+ expect(config.categories).toContain('injection');
45
+ expect(config.categories).not.toContain('invalid-cat');
46
+ });
47
+ it('applies failOn from CLI options', () => {
48
+ const config = loadConfig(makeCliOptions({ failOn: 'critical' }));
49
+ expect(config.failOn).toBe('CRITICAL');
50
+ });
51
+ it('applies format from CLI options', () => {
52
+ const config = loadConfig(makeCliOptions({ format: 'json' }));
53
+ expect(config.format).toBe('json');
54
+ });
55
+ it('applies output file from CLI options', () => {
56
+ const config = loadConfig(makeCliOptions({ output: '/tmp/report.json' }));
57
+ expect(config.outputFile).toBe('/tmp/report.json');
58
+ });
59
+ it('applies watch mode', () => {
60
+ const config = loadConfig(makeCliOptions({ watch: true }));
61
+ expect(config.watch).toBe(true);
62
+ });
63
+ it('applies ci mode', () => {
64
+ const config = loadConfig(makeCliOptions({ ci: true }));
65
+ expect(config.ci).toBe(true);
66
+ });
67
+ it('applies verbose mode', () => {
68
+ const config = loadConfig(makeCliOptions({ verbose: true }));
69
+ expect(config.verbose).toBe(true);
70
+ });
71
+ it('applies configOnly', () => {
72
+ const config = loadConfig(makeCliOptions({ configOnly: true }));
73
+ expect(config.configOnly).toBe(true);
74
+ });
75
+ it('applies marketplace mode (valid)', () => {
76
+ const config = loadConfig(makeCliOptions({ marketplace: 'all' }));
77
+ expect(config.marketplaceMode).toBe('all');
78
+ });
79
+ it('ignores invalid marketplace mode', () => {
80
+ // Should warn and not change from default
81
+ const config1 = loadConfig(makeCliOptions());
82
+ const config2 = loadConfig(makeCliOptions({ marketplace: 'invalid-mode' }));
83
+ expect(config2.marketplaceMode).toBe(config1.marketplaceMode);
84
+ });
85
+ it('applies redact option', () => {
86
+ const config = loadConfig(makeCliOptions({ redact: true }));
87
+ expect(config.redact).toBe(true);
88
+ });
89
+ it('applies docDampening option', () => {
90
+ const config = loadConfig(makeCliOptions({ docDampening: false }));
91
+ expect(config.docDampening).toBe(false);
92
+ });
93
+ it('applies threatIntel option', () => {
94
+ const config = loadConfig(makeCliOptions({ threatIntel: true }));
95
+ expect(config.threatIntel).toBe(true);
96
+ });
97
+ it('applies semanticAnalysis option', () => {
98
+ const config = loadConfig(makeCliOptions({ semanticAnalysis: false }));
99
+ expect(config.semanticAnalysis).toBe(false);
100
+ });
101
+ it('applies correlationAnalysis option', () => {
102
+ const config = loadConfig(makeCliOptions({ correlationAnalysis: false }));
103
+ expect(config.correlationAnalysis).toBe(false);
104
+ });
105
+ it('applies entropyAnalysis option', () => {
106
+ const config = loadConfig(makeCliOptions({ entropyAnalysis: true }));
107
+ expect(config.entropyAnalysis).toBe(true);
108
+ });
109
+ it('applies mcpValidation option', () => {
110
+ const config = loadConfig(makeCliOptions({ mcpValidation: false }));
111
+ expect(config.mcpValidation).toBe(false);
112
+ });
113
+ it('applies dependencyAnalysis option', () => {
114
+ const config = loadConfig(makeCliOptions({ dependencyAnalysis: true }));
115
+ expect(config.dependencyAnalysis).toBe(true);
116
+ });
117
+ it('applies capabilityMapping option', () => {
118
+ const config = loadConfig(makeCliOptions({ capabilityMapping: true }));
119
+ expect(config.capabilityMapping).toBe(true);
120
+ });
121
+ it('applies ignoreComments option', () => {
122
+ const config = loadConfig(makeCliOptions({ ignoreComments: false }));
123
+ expect(config.ignoreComments).toBe(false);
124
+ });
125
+ it('applies mitreAtlas option', () => {
126
+ const config = loadConfig(makeCliOptions({ mitreAtlas: true }));
127
+ expect(config.mitreAtlas).toBe(true);
128
+ });
129
+ it('applies llmAnalysis option', () => {
130
+ const config = loadConfig(makeCliOptions({ llmAnalysis: true }));
131
+ expect(config.llmAnalysis).toBe(true);
132
+ });
133
+ it('applies thorough profile', () => {
134
+ const config = loadConfig(makeCliOptions({ thorough: true }));
135
+ expect(config.threatIntel).toBe(true);
136
+ expect(config.semanticAnalysis).toBe(true);
137
+ expect(config.correlationAnalysis).toBe(true);
138
+ expect(config.entropyAnalysis).toBe(true);
139
+ expect(config.mcpValidation).toBe(true);
140
+ expect(config.dependencyAnalysis).toBe(true);
141
+ expect(config.capabilityMapping).toBe(true);
142
+ expect(config.ignoreComments).toBe(true);
143
+ expect(config.mitreAtlas).toBe(true);
144
+ });
145
+ it('applies mitreAtlasCatalog option', () => {
146
+ const config = loadConfig(makeCliOptions({ mitreAtlasCatalog: true }));
147
+ expect(config.mitreAtlasCatalog.enabled).toBe(true);
148
+ });
149
+ it('applies mitreAtlasCatalogForceRefresh and enables catalog', () => {
150
+ const config = loadConfig(makeCliOptions({ mitreAtlasCatalogForceRefresh: true }));
151
+ expect(config.mitreAtlasCatalog.forceRefresh).toBe(true);
152
+ expect(config.mitreAtlasCatalog.enabled).toBe(true);
153
+ });
154
+ it('applies LLM provider options', () => {
155
+ const config = loadConfig(makeCliOptions({
156
+ llmProvider: 'openai-compatible',
157
+ llmModel: 'gpt-4o',
158
+ llmBaseUrl: 'https://api.openai.com/v1/chat/completions',
159
+ llmApiKeyEnv: 'OPENAI_API_KEY',
160
+ llmTimeoutMs: 30000,
161
+ }));
162
+ expect(config.llm.provider).toBe('openai-compatible');
163
+ expect(config.llm.model).toBe('gpt-4o');
164
+ expect(config.llm.baseUrl).toBe('https://api.openai.com/v1/chat/completions');
165
+ expect(config.llm.apiKeyEnv).toBe('OPENAI_API_KEY');
166
+ expect(config.llm.timeoutMs).toBe(30000);
167
+ });
168
+ it('loads config from file when path is specified', () => {
169
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-config-test-'));
170
+ const configPath = path.join(tmpDir, '.ferretrc.json');
171
+ fs.writeFileSync(configPath, JSON.stringify({
172
+ severity: ['CRITICAL', 'HIGH'],
173
+ ignore: ['node_modules/**'],
174
+ }));
175
+ const config = loadConfig(makeCliOptions({ config: configPath }));
176
+ expect(config.severities).toContain('CRITICAL');
177
+ expect(config.ignore).toContain('node_modules/**');
178
+ fs.rmSync(tmpDir, { recursive: true });
179
+ });
180
+ it('handles config file with features section', () => {
181
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-config-test-'));
182
+ const configPath = path.join(tmpDir, '.ferretrc.json');
183
+ fs.writeFileSync(configPath, JSON.stringify({
184
+ features: {
185
+ entropyAnalysis: false,
186
+ mcpValidation: true,
187
+ dependencyAnalysis: false,
188
+ ignoreComments: true,
189
+ },
190
+ }));
191
+ const config = loadConfig(makeCliOptions({ config: configPath }));
192
+ expect(config.entropyAnalysis).toBe(false);
193
+ expect(config.mcpValidation).toBe(true);
194
+ expect(config.dependencyAnalysis).toBe(false);
195
+ expect(config.ignoreComments).toBe(true);
196
+ fs.rmSync(tmpDir, { recursive: true });
197
+ });
198
+ it('handles invalid config file gracefully', () => {
199
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-config-test-'));
200
+ const configPath = path.join(tmpDir, '.ferretrc.json');
201
+ fs.writeFileSync(configPath, 'invalid json {{{');
202
+ // Should not throw, just use defaults
203
+ expect(() => loadConfig(makeCliOptions({ config: configPath }))).not.toThrow();
204
+ fs.rmSync(tmpDir, { recursive: true });
205
+ });
206
+ });
207
+ describe('getAIConfigPaths', () => {
208
+ it('returns an array', () => {
209
+ const paths = getAIConfigPaths();
210
+ expect(Array.isArray(paths)).toBe(true);
211
+ });
212
+ it('returns unique paths', () => {
213
+ const paths = getAIConfigPaths();
214
+ const unique = [...new Set(paths)];
215
+ expect(paths.length).toBe(unique.length);
216
+ });
217
+ });
218
+ describe('getClaudeConfigPaths', () => {
219
+ it('returns same as getAIConfigPaths', () => {
220
+ const ai = getAIConfigPaths();
221
+ const claude = getClaudeConfigPaths();
222
+ expect(ai).toEqual(claude);
223
+ });
224
+ });
225
+ //# sourceMappingURL=configLoader.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Additional Config Loader Tests
3
+ * Tests for loadConfig with LLM and mitreAtlas config file settings
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=configLoaderExtra.test.d.ts.map