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,126 @@
1
+ /**
2
+ * Additional HTML Reporter Tests
3
+ */
4
+ import { generateHtmlReport, formatHtmlReport } from '../reporters/HtmlReporter.js';
5
+ function makeFinding(overrides = {}) {
6
+ return {
7
+ ruleId: 'INJ-001',
8
+ ruleName: 'Injection Rule',
9
+ severity: 'HIGH',
10
+ category: 'injection',
11
+ file: '/project/test.md',
12
+ relativePath: 'test.md',
13
+ line: 5,
14
+ match: 'IGNORE <script>alert(1)</script>',
15
+ context: [
16
+ { lineNumber: 4, content: 'previous line', isMatch: false },
17
+ { lineNumber: 5, content: 'IGNORE PREVIOUS', isMatch: true },
18
+ ],
19
+ remediation: 'Remove injection attempt',
20
+ timestamp: new Date(),
21
+ riskScore: 75,
22
+ ...overrides,
23
+ };
24
+ }
25
+ function makeScanResult(findings = [], overrides = {}) {
26
+ return {
27
+ success: true,
28
+ startTime: new Date(),
29
+ endTime: new Date(),
30
+ duration: 1500,
31
+ scannedPaths: ['/project'],
32
+ totalFiles: 10,
33
+ analyzedFiles: 8,
34
+ skippedFiles: 2,
35
+ findings,
36
+ findingsBySeverity: {
37
+ CRITICAL: findings.filter(f => f.severity === 'CRITICAL'),
38
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
39
+ MEDIUM: findings.filter(f => f.severity === 'MEDIUM'),
40
+ LOW: findings.filter(f => f.severity === 'LOW'),
41
+ INFO: findings.filter(f => f.severity === 'INFO'),
42
+ },
43
+ findingsByCategory: {},
44
+ overallRiskScore: 65,
45
+ summary: {
46
+ critical: findings.filter(f => f.severity === 'CRITICAL').length,
47
+ high: findings.filter(f => f.severity === 'HIGH').length,
48
+ medium: findings.filter(f => f.severity === 'MEDIUM').length,
49
+ low: findings.filter(f => f.severity === 'LOW').length,
50
+ info: 0,
51
+ total: findings.length,
52
+ },
53
+ errors: [],
54
+ ...overrides,
55
+ };
56
+ }
57
+ describe('generateHtmlReport', () => {
58
+ it('generates valid HTML with no findings', () => {
59
+ const html = generateHtmlReport(makeScanResult());
60
+ expect(typeof html).toBe('string');
61
+ expect(html).toContain('<!DOCTYPE html>');
62
+ expect(html).toContain('<html');
63
+ expect(html).toContain('</html>');
64
+ });
65
+ it('escapes HTML in finding content', () => {
66
+ const finding = makeFinding({
67
+ match: '<script>alert("xss")</script>',
68
+ });
69
+ const html = generateHtmlReport(makeScanResult([finding]));
70
+ expect(html).not.toContain('<script>alert("xss")</script>');
71
+ expect(html).toContain('&lt;script&gt;');
72
+ });
73
+ it('includes custom title', () => {
74
+ const html = generateHtmlReport(makeScanResult(), { title: 'My Security Report' });
75
+ expect(html).toContain('My Security Report');
76
+ });
77
+ it('supports dark mode option', () => {
78
+ const lightHtml = generateHtmlReport(makeScanResult(), { darkMode: false });
79
+ const darkHtml = generateHtmlReport(makeScanResult(), { darkMode: true });
80
+ expect(lightHtml).not.toBe(darkHtml);
81
+ });
82
+ it('includes findings in report', () => {
83
+ const finding = makeFinding({ severity: 'CRITICAL' });
84
+ const html = generateHtmlReport(makeScanResult([finding]));
85
+ expect(html).toContain('INJ-001');
86
+ expect(html).toContain('CRITICAL');
87
+ });
88
+ it('includes context lines in report', () => {
89
+ const finding = makeFinding({
90
+ context: [
91
+ { lineNumber: 4, content: 'previous line content', isMatch: false },
92
+ { lineNumber: 5, content: 'IGNORE PREVIOUS', isMatch: true },
93
+ ],
94
+ });
95
+ const html = generateHtmlReport(makeScanResult([finding]), { includeContext: true });
96
+ expect(html).toContain('previous line content');
97
+ });
98
+ it('includes scan duration in report', () => {
99
+ const html = generateHtmlReport(makeScanResult());
100
+ // Duration should be formatted
101
+ expect(html).toContain('1.50'); // 1500ms = 1.50s
102
+ });
103
+ it('generates report with multiple severity levels', () => {
104
+ const findings = [
105
+ makeFinding({ severity: 'CRITICAL' }),
106
+ makeFinding({ severity: 'HIGH' }),
107
+ makeFinding({ severity: 'MEDIUM' }),
108
+ makeFinding({ severity: 'LOW' }),
109
+ makeFinding({ severity: 'INFO' }),
110
+ ];
111
+ const html = generateHtmlReport(makeScanResult(findings));
112
+ expect(html).toContain('CRITICAL');
113
+ expect(html).toContain('HIGH');
114
+ expect(html).toContain('MEDIUM');
115
+ expect(html).toContain('LOW');
116
+ expect(html).toContain('INFO');
117
+ });
118
+ });
119
+ describe('formatHtmlReport', () => {
120
+ it('returns same as generateHtmlReport', () => {
121
+ const result = makeScanResult([makeFinding()]);
122
+ const options = { title: 'Test Report' };
123
+ expect(formatHtmlReport(result, options)).toBe(generateHtmlReport(result, options));
124
+ });
125
+ });
126
+ //# sourceMappingURL=htmlReporter.extra.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Interactive TUI Tests
3
+ * Tests for displayFindings and other TUI utility functions
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=interactiveTui.test.d.ts.map
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Interactive TUI Tests
3
+ * Tests for displayFindings and other TUI utility functions
4
+ */
5
+ import interactiveTuiDefault from '../features/interactiveTui.js';
6
+ import { displayFindings } from '../features/interactiveTui.js';
7
+ const { formatFinding, formatSummary } = interactiveTuiDefault;
8
+ function makeFinding(overrides = {}) {
9
+ return {
10
+ ruleId: 'INJ-001',
11
+ ruleName: 'Injection Test Rule',
12
+ severity: 'HIGH',
13
+ category: 'injection',
14
+ file: '/project/test.md',
15
+ relativePath: 'test.md',
16
+ line: 5,
17
+ match: 'IGNORE PREVIOUS',
18
+ context: [
19
+ { lineNumber: 4, content: 'previous line', isMatch: false },
20
+ { lineNumber: 5, content: 'IGNORE PREVIOUS', isMatch: true },
21
+ { lineNumber: 6, content: 'next line', isMatch: false },
22
+ ],
23
+ remediation: 'Remove the injection attempt',
24
+ timestamp: new Date(),
25
+ riskScore: 75,
26
+ ...overrides,
27
+ };
28
+ }
29
+ function makeScanResult(findings = []) {
30
+ return {
31
+ success: true,
32
+ startTime: new Date(),
33
+ endTime: new Date(),
34
+ duration: 1500,
35
+ scannedPaths: ['/project'],
36
+ totalFiles: 10,
37
+ analyzedFiles: 8,
38
+ skippedFiles: 2,
39
+ findings,
40
+ findingsBySeverity: {
41
+ CRITICAL: findings.filter(f => f.severity === 'CRITICAL'),
42
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
43
+ MEDIUM: findings.filter(f => f.severity === 'MEDIUM'),
44
+ LOW: findings.filter(f => f.severity === 'LOW'),
45
+ INFO: findings.filter(f => f.severity === 'INFO'),
46
+ },
47
+ findingsByCategory: {},
48
+ overallRiskScore: 65,
49
+ summary: {
50
+ critical: findings.filter(f => f.severity === 'CRITICAL').length,
51
+ high: findings.filter(f => f.severity === 'HIGH').length,
52
+ medium: findings.filter(f => f.severity === 'MEDIUM').length,
53
+ low: findings.filter(f => f.severity === 'LOW').length,
54
+ info: 0,
55
+ total: findings.length,
56
+ },
57
+ errors: [],
58
+ };
59
+ }
60
+ describe('formatFinding', () => {
61
+ it('formats a finding as a string', () => {
62
+ const finding = makeFinding();
63
+ const result = formatFinding(finding, 0, 1);
64
+ expect(typeof result).toBe('string');
65
+ expect(result.length).toBeGreaterThan(0);
66
+ expect(result).toContain('INJ-001');
67
+ expect(result).toContain('HIGH');
68
+ expect(result).toContain('injection');
69
+ expect(result).toContain('test.md');
70
+ expect(result).toContain('IGNORE PREVIOUS');
71
+ });
72
+ it('includes context when present', () => {
73
+ const finding = makeFinding();
74
+ const result = formatFinding(finding, 0, 1);
75
+ expect(result).toContain('Context:');
76
+ expect(result).toContain('IGNORE PREVIOUS');
77
+ });
78
+ it('includes remediation', () => {
79
+ const finding = makeFinding();
80
+ const result = formatFinding(finding, 0, 1);
81
+ expect(result).toContain('Remove the injection attempt');
82
+ });
83
+ it('handles finding without context', () => {
84
+ const finding = makeFinding({ context: [] });
85
+ const result = formatFinding(finding, 0, 5);
86
+ expect(result).not.toContain('Context:');
87
+ });
88
+ it('handles finding without remediation', () => {
89
+ const finding = makeFinding({ remediation: '' });
90
+ const result = formatFinding(finding, 0, 1);
91
+ expect(typeof result).toBe('string');
92
+ });
93
+ it('shows correct index and total', () => {
94
+ const finding = makeFinding();
95
+ const result = formatFinding(finding, 2, 10);
96
+ expect(result).toContain('3/10');
97
+ });
98
+ it('formats CRITICAL severity', () => {
99
+ const finding = makeFinding({ severity: 'CRITICAL' });
100
+ const result = formatFinding(finding, 0, 1);
101
+ expect(result).toContain('CRITICAL');
102
+ });
103
+ it('formats MEDIUM severity', () => {
104
+ const finding = makeFinding({ severity: 'MEDIUM' });
105
+ const result = formatFinding(finding, 0, 1);
106
+ expect(result).toContain('MEDIUM');
107
+ });
108
+ it('formats LOW severity', () => {
109
+ const finding = makeFinding({ severity: 'LOW' });
110
+ const result = formatFinding(finding, 0, 1);
111
+ expect(result).toContain('LOW');
112
+ });
113
+ it('formats INFO severity', () => {
114
+ const finding = makeFinding({ severity: 'INFO' });
115
+ const result = formatFinding(finding, 0, 1);
116
+ expect(result).toContain('INFO');
117
+ });
118
+ });
119
+ describe('formatSummary', () => {
120
+ it('formats a scan summary as a string', () => {
121
+ const result = makeScanResult([makeFinding()]);
122
+ const summary = formatSummary(result);
123
+ expect(typeof summary).toBe('string');
124
+ expect(summary).toContain('Scan Summary');
125
+ expect(summary).toContain('Files Scanned');
126
+ expect(summary).toContain('Risk Score');
127
+ });
128
+ it('includes findings count', () => {
129
+ const findings = [
130
+ makeFinding({ severity: 'CRITICAL' }),
131
+ makeFinding({ severity: 'HIGH' }),
132
+ makeFinding({ severity: 'MEDIUM' }),
133
+ ];
134
+ const result = makeScanResult(findings);
135
+ const summary = formatSummary(result);
136
+ expect(summary).toContain('CRITICAL');
137
+ expect(summary).toContain('HIGH');
138
+ expect(summary).toContain('MEDIUM');
139
+ });
140
+ it('handles empty scan result', () => {
141
+ const result = makeScanResult([]);
142
+ const summary = formatSummary(result);
143
+ expect(summary).toContain('Scan Summary');
144
+ expect(typeof summary).toBe('string');
145
+ });
146
+ });
147
+ describe('displayFindings', () => {
148
+ let consoleSpy;
149
+ beforeEach(() => {
150
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
151
+ });
152
+ afterEach(() => {
153
+ consoleSpy.mockRestore();
154
+ });
155
+ it('displays a list of findings', () => {
156
+ const findings = [makeFinding(), makeFinding({ ruleId: 'CRED-001', line: 10 })];
157
+ displayFindings(findings);
158
+ expect(consoleSpy).toHaveBeenCalled();
159
+ });
160
+ it('respects maxDisplay limit', () => {
161
+ const findings = Array.from({ length: 20 }, (_, i) => makeFinding({ line: i + 1 }));
162
+ displayFindings(findings, { maxDisplay: 5 });
163
+ // Should show "and X more findings" message
164
+ const calls = consoleSpy.mock.calls.flat().join('');
165
+ expect(calls).toContain('more findings');
166
+ });
167
+ it('handles empty findings array', () => {
168
+ displayFindings([]);
169
+ expect(consoleSpy).toHaveBeenCalled();
170
+ const calls = consoleSpy.mock.calls.flat().join('');
171
+ expect(calls).toContain('0 total');
172
+ });
173
+ it('does not show "more" message when findings fit in maxDisplay', () => {
174
+ const findings = [makeFinding()];
175
+ displayFindings(findings, { maxDisplay: 10 });
176
+ const calls = consoleSpy.mock.calls.flat().join('');
177
+ expect(calls).not.toContain('more findings');
178
+ });
179
+ });
180
+ //# sourceMappingURL=interactiveTui.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Interactive TUI Command Handler Tests
3
+ * Tests for all commands in startInteractiveSession
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=interactiveTuiCommands.test.d.ts.map
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Interactive TUI Command Handler Tests
3
+ * Tests for all commands in startInteractiveSession
4
+ */
5
+ import { EventEmitter } from 'events';
6
+ // Mock readline interface
7
+ const mockRlInstance = new EventEmitter();
8
+ jest.mock('node:readline', () => ({
9
+ createInterface: jest.fn().mockReturnValue(mockRlInstance),
10
+ }));
11
+ import { startInteractiveSession } from '../features/interactiveTui.js';
12
+ function makeFinding(overrides = {}) {
13
+ return {
14
+ ruleId: 'INJ-001',
15
+ ruleName: 'Injection Rule',
16
+ severity: 'HIGH',
17
+ category: 'injection',
18
+ file: '/project/test.md',
19
+ relativePath: 'test.md',
20
+ line: 5,
21
+ match: 'IGNORE PREVIOUS',
22
+ context: [],
23
+ remediation: 'Remove',
24
+ timestamp: new Date(),
25
+ riskScore: 75,
26
+ ...overrides,
27
+ };
28
+ }
29
+ function makeScanResult(findings = []) {
30
+ return {
31
+ success: true,
32
+ startTime: new Date(),
33
+ endTime: new Date(),
34
+ duration: 1000,
35
+ scannedPaths: ['/project'],
36
+ totalFiles: 5,
37
+ analyzedFiles: 4,
38
+ skippedFiles: 1,
39
+ findings,
40
+ findingsBySeverity: {
41
+ CRITICAL: findings.filter(f => f.severity === 'CRITICAL'),
42
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
43
+ MEDIUM: findings.filter(f => f.severity === 'MEDIUM'),
44
+ LOW: findings.filter(f => f.severity === 'LOW'),
45
+ INFO: [],
46
+ },
47
+ findingsByCategory: {},
48
+ overallRiskScore: 50,
49
+ summary: {
50
+ critical: 0,
51
+ high: findings.filter(f => f.severity === 'HIGH').length,
52
+ medium: 0, low: 0, info: 0,
53
+ total: findings.length,
54
+ },
55
+ errors: [],
56
+ };
57
+ }
58
+ async function runCommandSequence(scanResult, commands) {
59
+ const outputs = [];
60
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation((msg) => {
61
+ outputs.push(msg ?? '');
62
+ });
63
+ let callCount = 0;
64
+ mockRlInstance.question = jest.fn((_prompt, cb) => {
65
+ const cmd = commands[callCount++] ?? 'quit';
66
+ cb(cmd);
67
+ });
68
+ mockRlInstance.close = jest.fn(() => {
69
+ mockRlInstance.emit('close');
70
+ });
71
+ try {
72
+ await startInteractiveSession(scanResult);
73
+ }
74
+ finally {
75
+ consoleSpy.mockRestore();
76
+ }
77
+ return outputs;
78
+ }
79
+ describe('startInteractiveSession - command handlers', () => {
80
+ beforeEach(() => {
81
+ jest.clearAllMocks();
82
+ });
83
+ it('handles summary command with scan result', async () => {
84
+ const result = makeScanResult([makeFinding()]);
85
+ const outputs = await runCommandSequence(result, ['summary', 'quit']);
86
+ const allOutput = outputs.join('');
87
+ expect(allOutput).toContain('Scan Summary');
88
+ });
89
+ it('handles summary command without scan result', async () => {
90
+ const outputs = await runCommandSequence(null, ['summary', 'quit']);
91
+ const allOutput = outputs.join('');
92
+ expect(allOutput).toContain('No scan results');
93
+ });
94
+ it('handles list command with findings', async () => {
95
+ const findings = [
96
+ makeFinding({ severity: 'CRITICAL', ruleId: 'INJ-001' }),
97
+ makeFinding({ severity: 'HIGH', ruleId: 'CRED-001' }),
98
+ ];
99
+ const result = makeScanResult(findings);
100
+ const outputs = await runCommandSequence(result, ['list', 'quit']);
101
+ const allOutput = outputs.join('');
102
+ expect(allOutput).toContain('Findings');
103
+ });
104
+ it('handles list command without scan result', async () => {
105
+ const outputs = await runCommandSequence(null, ['list', 'quit']);
106
+ const allOutput = outputs.join('');
107
+ expect(allOutput).toContain('No scan results');
108
+ });
109
+ it('handles show command for specific index', async () => {
110
+ const findings = [makeFinding({ ruleId: 'INJ-001' }), makeFinding({ ruleId: 'CRED-001' })];
111
+ const result = makeScanResult(findings);
112
+ const outputs = await runCommandSequence(result, ['show 1', 'quit']);
113
+ const allOutput = outputs.join('');
114
+ expect(allOutput.length).toBeGreaterThan(0);
115
+ });
116
+ it('handles show with invalid index', async () => {
117
+ const findings = [makeFinding()];
118
+ const result = makeScanResult(findings);
119
+ const outputs = await runCommandSequence(result, ['show 999', 'quit']);
120
+ const allOutput = outputs.join('');
121
+ expect(allOutput).toContain('Invalid index');
122
+ });
123
+ it('handles show with no findings', async () => {
124
+ const result = makeScanResult([]);
125
+ const outputs = await runCommandSequence(result, ['show', 'quit']);
126
+ const allOutput = outputs.join('');
127
+ expect(allOutput).toContain('No findings');
128
+ });
129
+ it('handles next command', async () => {
130
+ const findings = [makeFinding({ ruleId: 'INJ-001' }), makeFinding({ ruleId: 'CRED-001' })];
131
+ const result = makeScanResult(findings);
132
+ const outputs = await runCommandSequence(result, ['next', 'quit']);
133
+ const allOutput = outputs.join('');
134
+ expect(allOutput.length).toBeGreaterThan(0);
135
+ });
136
+ it('handles prev command', async () => {
137
+ const findings = [makeFinding()];
138
+ const result = makeScanResult(findings);
139
+ const outputs = await runCommandSequence(result, ['prev', 'quit']);
140
+ expect(outputs.length).toBeGreaterThan(0);
141
+ });
142
+ it('handles filter command by severity', async () => {
143
+ const findings = [
144
+ makeFinding({ severity: 'HIGH' }),
145
+ makeFinding({ severity: 'CRITICAL' }),
146
+ ];
147
+ const result = makeScanResult(findings);
148
+ const outputs = await runCommandSequence(result, ['filter severity HIGH', 'quit']);
149
+ expect(outputs.length).toBeGreaterThan(0);
150
+ });
151
+ it('handles sort command', async () => {
152
+ const findings = [makeFinding(), makeFinding({ ruleId: 'CRED-001', severity: 'CRITICAL' })];
153
+ const result = makeScanResult(findings);
154
+ const outputs = await runCommandSequence(result, ['sort file', 'quit']);
155
+ expect(outputs.length).toBeGreaterThan(0);
156
+ });
157
+ it('handles quit command via alias q', async () => {
158
+ const result = makeScanResult();
159
+ const outputs = await runCommandSequence(result, ['q']);
160
+ expect(outputs.length).toBeGreaterThan(0);
161
+ });
162
+ it('handles exit command', async () => {
163
+ const result = makeScanResult();
164
+ const outputs = await runCommandSequence(result, ['exit']);
165
+ expect(outputs.length).toBeGreaterThan(0);
166
+ });
167
+ it('handles help with alias h', async () => {
168
+ const result = makeScanResult();
169
+ const outputs = await runCommandSequence(result, ['h', 'quit']);
170
+ const allOutput = outputs.join('');
171
+ expect(allOutput).toContain('Commands');
172
+ });
173
+ it('handles h alias', async () => {
174
+ const result = makeScanResult();
175
+ const outputs = await runCommandSequence(result, ['?', 'quit']);
176
+ const allOutput = outputs.join('');
177
+ expect(allOutput).toContain('Commands');
178
+ });
179
+ it('handles list with limit argument', async () => {
180
+ const findings = Array.from({ length: 30 }, (_, i) => makeFinding({ line: i + 1 }));
181
+ const result = makeScanResult(findings);
182
+ const outputs = await runCommandSequence(result, ['list 5', 'quit']);
183
+ const allOutput = outputs.join('');
184
+ expect(allOutput).toContain('more');
185
+ });
186
+ });
187
+ //# sourceMappingURL=interactiveTuiCommands.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * More Interactive TUI Command Tests
3
+ * Tests for files, export, clear, filter variations, and sort variations
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=interactiveTuiMore.test.d.ts.map