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,214 @@
1
+ /**
2
+ * Additional Quarantine Tests
3
+ */
4
+ import { loadQuarantineDatabase, saveQuarantineDatabase, quarantineFile, restoreQuarantinedFile, deleteQuarantinedFile, listQuarantinedFiles, getQuarantineStats, cleanupQuarantine, checkQuarantineHealth, } from '../remediation/Quarantine.js';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ function makeFinding(overrides = {}) {
9
+ return {
10
+ ruleId: 'INJ-001',
11
+ ruleName: 'Test Rule',
12
+ severity: 'HIGH',
13
+ category: 'injection',
14
+ file: '/project/test.md',
15
+ relativePath: 'test.md',
16
+ line: 1,
17
+ match: 'bad content',
18
+ context: [],
19
+ remediation: 'fix',
20
+ timestamp: new Date(),
21
+ riskScore: 75,
22
+ ...overrides,
23
+ };
24
+ }
25
+ describe('loadQuarantineDatabase', () => {
26
+ let tmpDir;
27
+ beforeEach(() => {
28
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-quarantine-'));
29
+ });
30
+ afterEach(() => {
31
+ fs.rmSync(tmpDir, { recursive: true, force: true });
32
+ });
33
+ it('creates empty database when directory is empty', () => {
34
+ const db = loadQuarantineDatabase(tmpDir);
35
+ expect(db.version).toBe('1.0');
36
+ expect(db.entries).toHaveLength(0);
37
+ });
38
+ it('loads existing valid database', () => {
39
+ const validDb = {
40
+ version: '1.0',
41
+ created: new Date().toISOString(),
42
+ lastUpdated: new Date().toISOString(),
43
+ entries: [],
44
+ stats: { totalQuarantined: 0, totalRestored: 0, byCategory: {}, bySeverity: {} },
45
+ };
46
+ fs.writeFileSync(path.join(tmpDir, 'quarantine.json'), JSON.stringify(validDb));
47
+ const db = loadQuarantineDatabase(tmpDir);
48
+ expect(db.version).toBe('1.0');
49
+ });
50
+ it('returns empty database for invalid JSON', () => {
51
+ fs.writeFileSync(path.join(tmpDir, 'quarantine.json'), 'invalid json {{{');
52
+ const db = loadQuarantineDatabase(tmpDir);
53
+ expect(db.entries).toHaveLength(0);
54
+ });
55
+ it('returns empty database for invalid structure', () => {
56
+ fs.writeFileSync(path.join(tmpDir, 'quarantine.json'), JSON.stringify({ invalid: 'structure' }));
57
+ const db = loadQuarantineDatabase(tmpDir);
58
+ expect(db.entries).toHaveLength(0);
59
+ });
60
+ });
61
+ describe('saveQuarantineDatabase', () => {
62
+ let tmpDir;
63
+ beforeEach(() => {
64
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-quarantine-'));
65
+ });
66
+ afterEach(() => {
67
+ fs.rmSync(tmpDir, { recursive: true, force: true });
68
+ });
69
+ it('saves database to disk', () => {
70
+ const db = {
71
+ version: '1.0',
72
+ created: new Date().toISOString(),
73
+ lastUpdated: new Date().toISOString(),
74
+ entries: [],
75
+ stats: { totalQuarantined: 0, totalRestored: 0, byCategory: {}, bySeverity: {} },
76
+ };
77
+ saveQuarantineDatabase(db, tmpDir);
78
+ expect(fs.existsSync(path.join(tmpDir, 'quarantine.json'))).toBe(true);
79
+ });
80
+ });
81
+ describe('quarantineFile', () => {
82
+ let tmpDir;
83
+ let testFilePath;
84
+ beforeEach(() => {
85
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-quarantine-'));
86
+ testFilePath = path.join(tmpDir, 'test-file.md');
87
+ fs.writeFileSync(testFilePath, '# Test file content');
88
+ });
89
+ afterEach(() => {
90
+ fs.rmSync(tmpDir, { recursive: true, force: true });
91
+ });
92
+ it('returns null for non-existent file', () => {
93
+ const entry = quarantineFile('/nonexistent/file.md', [], 'test reason', {
94
+ quarantineDir: tmpDir,
95
+ });
96
+ expect(entry).toBeNull();
97
+ });
98
+ it('quarantines a file', () => {
99
+ const quarantineDir = path.join(tmpDir, 'quarantine');
100
+ const finding = makeFinding({ file: testFilePath });
101
+ const entry = quarantineFile(testFilePath, [finding], 'security issue', {
102
+ quarantineDir,
103
+ createBackup: false,
104
+ });
105
+ expect(entry).not.toBeNull();
106
+ expect(entry?.originalPath).toBe(testFilePath);
107
+ expect(entry?.restored).toBe(false);
108
+ });
109
+ it('returns null for file exceeding max size', () => {
110
+ const entry = quarantineFile(testFilePath, [], 'test', {
111
+ quarantineDir: path.join(tmpDir, 'q'),
112
+ maxFileSizeMB: 0.000001, // Extremely small limit
113
+ });
114
+ expect(entry).toBeNull();
115
+ });
116
+ });
117
+ describe('listQuarantinedFiles', () => {
118
+ it('returns empty array for empty quarantine', () => {
119
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-q-'));
120
+ const files = listQuarantinedFiles(tmpDir);
121
+ expect(files).toHaveLength(0);
122
+ fs.rmSync(tmpDir, { recursive: true });
123
+ });
124
+ });
125
+ describe('getQuarantineStats', () => {
126
+ it('returns default stats for empty quarantine', () => {
127
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-q-'));
128
+ const stats = getQuarantineStats(tmpDir);
129
+ expect(stats.totalQuarantined).toBe(0);
130
+ expect(stats.totalRestored).toBe(0);
131
+ fs.rmSync(tmpDir, { recursive: true });
132
+ });
133
+ });
134
+ describe('cleanupQuarantine', () => {
135
+ let tmpDir;
136
+ beforeEach(() => {
137
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-cleanup-'));
138
+ });
139
+ afterEach(() => {
140
+ fs.rmSync(tmpDir, { recursive: true, force: true });
141
+ });
142
+ it('cleans up old entries from empty quarantine', () => {
143
+ const result = cleanupQuarantine(30, tmpDir);
144
+ expect(typeof result).toBe('number');
145
+ expect(result).toBe(0);
146
+ });
147
+ it('cleans up restored entries', () => {
148
+ const db = {
149
+ version: '1.0',
150
+ created: new Date().toISOString(),
151
+ lastUpdated: new Date().toISOString(),
152
+ entries: [
153
+ {
154
+ id: 'test-001',
155
+ originalPath: '/project/test.md',
156
+ quarantinePath: path.join(tmpDir, 'files', 'test-001_test.md'),
157
+ reason: 'test',
158
+ findings: [],
159
+ quarantineDate: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString(), // 60 days old
160
+ fileSize: 100,
161
+ fileHash: 'abc123',
162
+ restored: true,
163
+ restoredDate: new Date().toISOString(),
164
+ metadata: {
165
+ riskScore: 75,
166
+ severity: 'HIGH',
167
+ category: 'injection',
168
+ },
169
+ },
170
+ ],
171
+ stats: { totalQuarantined: 1, totalRestored: 1, byCategory: {}, bySeverity: {} },
172
+ };
173
+ fs.writeFileSync(path.join(tmpDir, 'quarantine.json'), JSON.stringify(db));
174
+ const result = cleanupQuarantine(30, tmpDir);
175
+ expect(typeof result).toBe('number');
176
+ expect(result).toBeGreaterThanOrEqual(0); // May or may not clean
177
+ });
178
+ });
179
+ describe('checkQuarantineHealth', () => {
180
+ it('returns health status for empty quarantine', () => {
181
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-health-'));
182
+ const health = checkQuarantineHealth(tmpDir);
183
+ expect(typeof health.healthy).toBe('boolean');
184
+ expect(Array.isArray(health.issues)).toBe(true);
185
+ fs.rmSync(tmpDir, { recursive: true });
186
+ });
187
+ });
188
+ describe('restoreQuarantinedFile', () => {
189
+ let tmpDir;
190
+ beforeEach(() => {
191
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-restore-'));
192
+ });
193
+ afterEach(() => {
194
+ fs.rmSync(tmpDir, { recursive: true, force: true });
195
+ });
196
+ it('returns false for non-existent quarantine entry', () => {
197
+ const result = restoreQuarantinedFile('nonexistent-id', tmpDir);
198
+ expect(result).toBe(false);
199
+ });
200
+ });
201
+ describe('deleteQuarantinedFile', () => {
202
+ let tmpDir;
203
+ beforeEach(() => {
204
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-delete-'));
205
+ });
206
+ afterEach(() => {
207
+ fs.rmSync(tmpDir, { recursive: true, force: true });
208
+ });
209
+ it('returns false for non-existent quarantine entry', () => {
210
+ const result = deleteQuarantinedFile('nonexistent-id', tmpDir);
211
+ expect(result).toBe(false);
212
+ });
213
+ });
214
+ //# sourceMappingURL=quarantineExtra.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Additional Redaction Tests
3
+ * Tests for redactSecretsInString, redactFinding, and redactScanResult
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=redactionExtra.test.d.ts.map
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Additional Redaction Tests
3
+ * Tests for redactSecretsInString, redactFinding, and redactScanResult
4
+ */
5
+ import { redactSecretsInString, redactFinding, redactScanResult, } from '../utils/redaction.js';
6
+ function makeFinding(overrides = {}) {
7
+ return {
8
+ ruleId: 'CRED-001',
9
+ ruleName: 'Credential Rule',
10
+ severity: 'HIGH',
11
+ category: 'credentials',
12
+ file: '/project/test.md',
13
+ relativePath: 'test.md',
14
+ line: 1,
15
+ match: 'token=abc123',
16
+ context: [{ lineNumber: 1, content: 'token=abc123', isMatch: true }],
17
+ remediation: 'remove token',
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: 1,
31
+ analyzedFiles: 1,
32
+ skippedFiles: 0,
33
+ findings,
34
+ findingsBySeverity: {
35
+ CRITICAL: [],
36
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
37
+ MEDIUM: [],
38
+ LOW: [],
39
+ INFO: [],
40
+ },
41
+ findingsByCategory: {},
42
+ overallRiskScore: 0,
43
+ summary: { critical: 0, high: 0, medium: 0, low: 0, info: 0, total: 0 },
44
+ errors: [],
45
+ };
46
+ }
47
+ describe('redactSecretsInString', () => {
48
+ it('redacts GitHub PAT tokens', () => {
49
+ const input = 'my token is ghp_abcdefghijklmnopqrstuvwxyz1234';
50
+ const result = redactSecretsInString(input);
51
+ expect(result).toContain('<REDACTED_GITHUB_TOKEN>');
52
+ expect(result).not.toContain('ghp_abcdefghijklmnopqrstuvwxyz1234');
53
+ });
54
+ it('redacts GitHub repo PAT tokens (ghr_)', () => {
55
+ const input = 'secret: ghr_AbCdEfGhIjKlMnOpQrStUvWxYz012345';
56
+ const result = redactSecretsInString(input);
57
+ expect(result).toContain('<REDACTED_GITHUB_TOKEN>');
58
+ });
59
+ it('redacts OpenAI API keys (sk-)', () => {
60
+ const input = 'api_key = sk-abcdefghijklmnop';
61
+ const result = redactSecretsInString(input);
62
+ expect(result).toContain('<REDACTED');
63
+ expect(result).not.toContain('sk-abcdefghijklmnop');
64
+ });
65
+ it('redacts Groq API keys (gsk_)', () => {
66
+ const input = 'GROQ_KEY=gsk_abcdefghijklmnopqrstu';
67
+ const result = redactSecretsInString(input);
68
+ expect(result).toContain('<REDACTED');
69
+ });
70
+ it('redacts AWS access keys (AKIA)', () => {
71
+ const input = 'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE';
72
+ const result = redactSecretsInString(input);
73
+ expect(result).toContain('<REDACTED_AWS_ACCESS_KEY>');
74
+ expect(result).not.toContain('AKIAIOSFODNN7EXAMPLE');
75
+ });
76
+ it('redacts JWT tokens', () => {
77
+ const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI.dGVzdHNpZ25hdHVyZTEyMzQ1Njc4OTAxMjM';
78
+ const result = redactSecretsInString(jwt);
79
+ expect(result).toContain('<REDACTED_JWT>');
80
+ });
81
+ it('redacts Slack tokens (xoxb-)', () => {
82
+ // Constructed at runtime — avoids static secret scanner false-positives on this test file.
83
+ // Slack bot token format: xoxb-{12digits}-{12digits}-{24alphanumeric}
84
+ const tok = 'xox' + 'b-' + '0'.repeat(12) + '-' + '0'.repeat(12) + '-' + 'x'.repeat(24);
85
+ const input = `SLACK_TOKEN=${tok}`;
86
+ const result = redactSecretsInString(input);
87
+ expect(result).toContain('<REDACTED_SLACK_TOKEN>');
88
+ });
89
+ it('does not redact short values', () => {
90
+ const input = 'token: abc';
91
+ const result = redactSecretsInString(input);
92
+ expect(result).toBe(input); // Short values are not redacted
93
+ });
94
+ it('handles empty string', () => {
95
+ expect(redactSecretsInString('')).toBe('');
96
+ });
97
+ it('handles string with no secrets', () => {
98
+ const input = 'This is a normal string with no secrets';
99
+ expect(redactSecretsInString(input)).toBe(input);
100
+ });
101
+ it('redacts token in different formats (key=value)', () => {
102
+ const input = 'token=supersecretvalue123456';
103
+ const result = redactSecretsInString(input);
104
+ expect(result).toContain('<REDACTED>');
105
+ });
106
+ it('redacts password in YAML format', () => {
107
+ const input = 'password: mysecretpassword123456';
108
+ const result = redactSecretsInString(input);
109
+ expect(result).toContain('<REDACTED>');
110
+ });
111
+ it('redacts authorization bearer token', () => {
112
+ const input = 'authorization: Bearer mysupersecrettoken12345';
113
+ const result = redactSecretsInString(input);
114
+ expect(result).toContain('<REDACTED>');
115
+ });
116
+ it('handles quoted values', () => {
117
+ const input = 'api_key: "mysecretapikey123456"';
118
+ const result = redactSecretsInString(input);
119
+ expect(result).toContain('<REDACTED>');
120
+ });
121
+ it('redacts refresh tokens (rt_)', () => {
122
+ const input = 'rt_abcdefghijklmnopqrstuvwxyz1234567890';
123
+ const result = redactSecretsInString(input);
124
+ expect(result).toContain('<REDACTED_REFRESH_TOKEN>');
125
+ });
126
+ });
127
+ describe('redactFinding', () => {
128
+ it('redacts secrets in match field', () => {
129
+ const finding = makeFinding({
130
+ match: 'api_key = sk-supersecretkey12345',
131
+ });
132
+ const redacted = redactFinding(finding);
133
+ expect(redacted.match).not.toContain('sk-supersecretkey12345');
134
+ expect(redacted.match).toContain('<REDACTED');
135
+ });
136
+ it('redacts secrets in context content', () => {
137
+ const finding = makeFinding({
138
+ context: [
139
+ { lineNumber: 1, content: 'token=ghp_abcdefghijklmnopqrstuvwxyz1234', isMatch: true },
140
+ ],
141
+ });
142
+ const redacted = redactFinding(finding);
143
+ expect(redacted.context[0]?.content).not.toContain('ghp_');
144
+ expect(redacted.context[0]?.content).toContain('<REDACTED');
145
+ });
146
+ it('redacts secrets in metadata', () => {
147
+ const finding = makeFinding({
148
+ metadata: {
149
+ apiKey: 'sk-supersecretapikey1234',
150
+ safeField: 'safe value',
151
+ },
152
+ });
153
+ const redacted = redactFinding(finding);
154
+ expect(JSON.stringify(redacted.metadata)).not.toContain('sk-supersecretapikey1234');
155
+ });
156
+ it('preserves non-secret fields', () => {
157
+ const finding = makeFinding({
158
+ ruleId: 'TEST-001',
159
+ severity: 'HIGH',
160
+ line: 42,
161
+ });
162
+ const redacted = redactFinding(finding);
163
+ expect(redacted.ruleId).toBe('TEST-001');
164
+ expect(redacted.severity).toBe('HIGH');
165
+ expect(redacted.line).toBe(42);
166
+ });
167
+ it('handles finding without metadata (no metadata key)', () => {
168
+ const finding = makeFinding();
169
+ // Remove metadata property entirely
170
+ const { metadata: _m, ...findingNoMetadata } = finding;
171
+ const redacted = redactFinding(findingNoMetadata);
172
+ expect(redacted).toBeDefined();
173
+ });
174
+ });
175
+ describe('redactScanResult', () => {
176
+ it('redacts secrets in all findings', () => {
177
+ const findings = [
178
+ makeFinding({ match: 'api_key=sk-secretkey123456789' }),
179
+ makeFinding({ match: 'normal content', ruleId: 'INJ-001' }),
180
+ ];
181
+ const result = makeScanResult(findings);
182
+ const redacted = redactScanResult(result);
183
+ expect(redacted.findings[0]?.match).not.toContain('sk-secretkey123456789');
184
+ expect(redacted.findings[1]?.match).toBe('normal content');
185
+ });
186
+ it('rebuilds findingsBySeverity after redaction', () => {
187
+ const findings = [
188
+ makeFinding({ severity: 'HIGH', match: 'api_key=sk-secretkey123456789' }),
189
+ makeFinding({ severity: 'CRITICAL', match: 'AKIA1234567890ABCDEF' }),
190
+ ];
191
+ const result = makeScanResult(findings);
192
+ const redacted = redactScanResult(result);
193
+ expect(redacted.findingsBySeverity['HIGH']).toHaveLength(1);
194
+ expect(redacted.findingsBySeverity['CRITICAL']).toHaveLength(1);
195
+ });
196
+ it('redacts secrets in error messages', () => {
197
+ const result = makeScanResult();
198
+ result.errors = [
199
+ {
200
+ message: 'Token ghp_abcdefghijklmnopqrstuvwxyz1234 invalid',
201
+ fatal: false,
202
+ },
203
+ ];
204
+ const redacted = redactScanResult(result);
205
+ expect(redacted.errors[0]?.message).not.toContain('ghp_');
206
+ expect(redacted.errors[0]?.message).toContain('<REDACTED');
207
+ });
208
+ it('redacts secrets in error file paths', () => {
209
+ const result = makeScanResult();
210
+ result.errors = [
211
+ {
212
+ file: '/path/to/api_key=sk-secretkey123456/config',
213
+ message: 'error occurred',
214
+ fatal: false,
215
+ },
216
+ ];
217
+ const redacted = redactScanResult(result);
218
+ expect(redacted.errors[0]?.file).not.toContain('sk-secretkey123456');
219
+ });
220
+ it('preserves scan metadata fields', () => {
221
+ const result = makeScanResult();
222
+ const redacted = redactScanResult(result);
223
+ expect(redacted.success).toBe(true);
224
+ expect(redacted.duration).toBe(100);
225
+ expect(redacted.totalFiles).toBe(1);
226
+ });
227
+ });
228
+ //# sourceMappingURL=redactionExtra.test.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * ScanDiff Tests
3
+ * Tests for compareScanResults, formatComparisonReport, formatComparisonJson,
4
+ * loadScanResult, and saveScanResult.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=scanDiff.test.d.ts.map