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,321 @@
1
+ /**
2
+ * Baseline Tests
3
+ * Tests for baseline management: create, add, remove, filter, validate, stats.
4
+ */
5
+ import { computeBaselineIntegrity, verifyBaselineIntegrity, createBaseline, addToBaseline, removeFromBaseline, filterAgainstBaseline, validateBaseline, getDefaultBaselinePath, getBaselineStats, } from '../utils/baseline.js';
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+ function makeFinding(overrides = {}) {
10
+ return {
11
+ ruleId: 'INJ-001',
12
+ ruleName: 'Test Rule',
13
+ severity: 'HIGH',
14
+ category: 'injection',
15
+ file: '/project/test.md',
16
+ relativePath: 'test.md',
17
+ line: 10,
18
+ match: 'ignore previous instructions',
19
+ context: [],
20
+ remediation: 'Fix it.',
21
+ timestamp: new Date(),
22
+ riskScore: 75,
23
+ ...overrides,
24
+ };
25
+ }
26
+ function makeScanResult(findings = []) {
27
+ return {
28
+ success: true,
29
+ startTime: new Date(),
30
+ endTime: new Date(),
31
+ duration: 100,
32
+ scannedPaths: ['/project'],
33
+ totalFiles: 10,
34
+ analyzedFiles: 8,
35
+ skippedFiles: 2,
36
+ findings,
37
+ findingsBySeverity: {
38
+ CRITICAL: findings.filter(f => f.severity === 'CRITICAL'),
39
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
40
+ MEDIUM: findings.filter(f => f.severity === 'MEDIUM'),
41
+ LOW: findings.filter(f => f.severity === 'LOW'),
42
+ INFO: findings.filter(f => f.severity === 'INFO'),
43
+ },
44
+ findingsByCategory: {},
45
+ overallRiskScore: 50,
46
+ summary: {
47
+ critical: 0, high: findings.filter(f => f.severity === 'HIGH').length,
48
+ medium: 0, low: 0, info: 0, total: findings.length,
49
+ },
50
+ errors: [],
51
+ };
52
+ }
53
+ function makeBaseline(overrides = {}) {
54
+ return {
55
+ version: '1.0',
56
+ createdDate: new Date().toISOString(),
57
+ lastUpdated: new Date().toISOString(),
58
+ findings: [],
59
+ ...overrides,
60
+ };
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // computeBaselineIntegrity
64
+ // ---------------------------------------------------------------------------
65
+ describe('computeBaselineIntegrity', () => {
66
+ it('returns a sha256 integrity object', () => {
67
+ const baseline = makeBaseline();
68
+ const integrity = computeBaselineIntegrity(baseline);
69
+ expect(integrity.algorithm).toBe('sha256');
70
+ expect(typeof integrity.hash).toBe('string');
71
+ expect(integrity.hash.length).toBe(64);
72
+ });
73
+ it('produces consistent hashes for the same content', () => {
74
+ const baseline = makeBaseline({ description: 'test' });
75
+ const a = computeBaselineIntegrity(baseline);
76
+ const b = computeBaselineIntegrity(baseline);
77
+ expect(a.hash).toBe(b.hash);
78
+ });
79
+ it('produces different hashes when content differs', () => {
80
+ const a = computeBaselineIntegrity(makeBaseline({ description: 'version-a' }));
81
+ const b = computeBaselineIntegrity(makeBaseline({ description: 'version-b' }));
82
+ expect(a.hash).not.toBe(b.hash);
83
+ });
84
+ });
85
+ // ---------------------------------------------------------------------------
86
+ // verifyBaselineIntegrity
87
+ // ---------------------------------------------------------------------------
88
+ describe('verifyBaselineIntegrity', () => {
89
+ it('returns true when no integrity field is present', () => {
90
+ const baseline = makeBaseline();
91
+ expect(verifyBaselineIntegrity(baseline)).toBe(true);
92
+ });
93
+ it('returns true when integrity matches', () => {
94
+ const base = makeBaseline();
95
+ const integrity = computeBaselineIntegrity(base);
96
+ const baseline = { ...base, integrity };
97
+ expect(verifyBaselineIntegrity(baseline)).toBe(true);
98
+ });
99
+ it('returns false when integrity hash is tampered', () => {
100
+ const base = makeBaseline();
101
+ const baseline = {
102
+ ...base,
103
+ integrity: { algorithm: 'sha256', hash: 'aabbccddeeff0011223344556677889900112233445566778899aabbccddeeff00' },
104
+ };
105
+ expect(verifyBaselineIntegrity(baseline)).toBe(false);
106
+ });
107
+ });
108
+ // ---------------------------------------------------------------------------
109
+ // createBaseline
110
+ // ---------------------------------------------------------------------------
111
+ describe('createBaseline', () => {
112
+ it('creates a baseline with findings from scan result', () => {
113
+ const findings = [makeFinding(), makeFinding({ ruleId: 'CRED-001', line: 20 })];
114
+ const result = makeScanResult(findings);
115
+ const baseline = createBaseline(result);
116
+ expect(baseline.version).toBe('1.0');
117
+ expect(baseline.findings).toHaveLength(2);
118
+ });
119
+ it('each finding has a hash', () => {
120
+ const result = makeScanResult([makeFinding()]);
121
+ const baseline = createBaseline(result);
122
+ expect(baseline.findings[0].hash).toBeTruthy();
123
+ expect(baseline.findings[0].hash.length).toBe(64);
124
+ });
125
+ it('uses provided description', () => {
126
+ const result = makeScanResult();
127
+ const baseline = createBaseline(result, 'My custom baseline');
128
+ expect(baseline.description).toBe('My custom baseline');
129
+ });
130
+ it('generates default description when not provided', () => {
131
+ const result = makeScanResult();
132
+ const baseline = createBaseline(result);
133
+ expect(baseline.description).toContain('/project');
134
+ });
135
+ it('creates empty baseline from empty scan result', () => {
136
+ const result = makeScanResult([]);
137
+ const baseline = createBaseline(result);
138
+ expect(baseline.findings).toHaveLength(0);
139
+ });
140
+ });
141
+ // ---------------------------------------------------------------------------
142
+ // addToBaseline
143
+ // ---------------------------------------------------------------------------
144
+ describe('addToBaseline', () => {
145
+ it('adds new findings to baseline', () => {
146
+ const baseline = makeBaseline();
147
+ const findings = [makeFinding()];
148
+ const updated = addToBaseline(baseline, findings);
149
+ expect(updated.findings).toHaveLength(1);
150
+ });
151
+ it('does not add duplicate findings', () => {
152
+ const finding = makeFinding();
153
+ const baseline = createBaseline(makeScanResult([finding]));
154
+ const updated = addToBaseline(baseline, [finding]);
155
+ expect(updated.findings).toHaveLength(1); // no duplicate added
156
+ });
157
+ it('includes reason when provided', () => {
158
+ const baseline = makeBaseline();
159
+ const updated = addToBaseline(baseline, [makeFinding()], 'Accepted by team');
160
+ expect(updated.findings[0].reason).toBe('Accepted by team');
161
+ });
162
+ it('omits reason when not provided', () => {
163
+ const baseline = makeBaseline();
164
+ const updated = addToBaseline(baseline, [makeFinding()]);
165
+ expect(updated.findings[0].reason).toBeUndefined();
166
+ });
167
+ it('does not mutate original baseline', () => {
168
+ const baseline = makeBaseline();
169
+ addToBaseline(baseline, [makeFinding()]);
170
+ expect(baseline.findings).toHaveLength(0);
171
+ });
172
+ });
173
+ // ---------------------------------------------------------------------------
174
+ // removeFromBaseline
175
+ // ---------------------------------------------------------------------------
176
+ describe('removeFromBaseline', () => {
177
+ it('removes findings by hash', () => {
178
+ const finding = makeFinding();
179
+ const result = makeScanResult([finding]);
180
+ const baseline = createBaseline(result);
181
+ const hashToRemove = baseline.findings[0].hash;
182
+ const updated = removeFromBaseline(baseline, [hashToRemove]);
183
+ expect(updated.findings).toHaveLength(0);
184
+ });
185
+ it('keeps unmatched findings', () => {
186
+ const f1 = makeFinding({ line: 10 });
187
+ const f2 = makeFinding({ line: 20 });
188
+ const baseline = createBaseline(makeScanResult([f1, f2]));
189
+ const hashToRemove = baseline.findings[0].hash;
190
+ const updated = removeFromBaseline(baseline, [hashToRemove]);
191
+ expect(updated.findings).toHaveLength(1);
192
+ });
193
+ it('does not mutate original baseline', () => {
194
+ const baseline = createBaseline(makeScanResult([makeFinding()]));
195
+ const hash = baseline.findings[0].hash;
196
+ removeFromBaseline(baseline, [hash]);
197
+ expect(baseline.findings).toHaveLength(1);
198
+ });
199
+ });
200
+ // ---------------------------------------------------------------------------
201
+ // filterAgainstBaseline
202
+ // ---------------------------------------------------------------------------
203
+ describe('filterAgainstBaseline', () => {
204
+ it('returns original result when baseline is null', () => {
205
+ const result = makeScanResult([makeFinding()]);
206
+ const filtered = filterAgainstBaseline(result, null);
207
+ expect(filtered).toBe(result);
208
+ });
209
+ it('returns original result when baseline has no findings', () => {
210
+ const result = makeScanResult([makeFinding()]);
211
+ const baseline = makeBaseline({ findings: [] });
212
+ const filtered = filterAgainstBaseline(result, baseline);
213
+ expect(filtered).toBe(result);
214
+ });
215
+ it('filters out findings that are in baseline', () => {
216
+ const finding = makeFinding();
217
+ const result = makeScanResult([finding]);
218
+ const baseline = createBaseline(result);
219
+ const filtered = filterAgainstBaseline(result, baseline);
220
+ expect(filtered.findings).toHaveLength(0);
221
+ expect(filtered.summary.total).toBe(0);
222
+ });
223
+ it('keeps findings not in baseline', () => {
224
+ const known = makeFinding({ line: 10 });
225
+ const newFinding = makeFinding({ line: 99, match: 'different match' });
226
+ const baseline = createBaseline(makeScanResult([known]));
227
+ const result = makeScanResult([known, newFinding]);
228
+ const filtered = filterAgainstBaseline(result, baseline);
229
+ expect(filtered.findings).toHaveLength(1);
230
+ expect(filtered.findings[0].line).toBe(99);
231
+ });
232
+ it('updates summary counts correctly after filtering', () => {
233
+ const critFinding = makeFinding({ severity: 'CRITICAL', line: 1 });
234
+ const highFinding = makeFinding({ severity: 'HIGH', line: 2 });
235
+ const baseline = createBaseline(makeScanResult([critFinding]));
236
+ const result = makeScanResult([critFinding, highFinding]);
237
+ const filtered = filterAgainstBaseline(result, baseline);
238
+ expect(filtered.summary.critical).toBe(0);
239
+ expect(filtered.summary.high).toBe(1);
240
+ });
241
+ });
242
+ // ---------------------------------------------------------------------------
243
+ // validateBaseline
244
+ // ---------------------------------------------------------------------------
245
+ describe('validateBaseline', () => {
246
+ it('returns all valid when all baseline findings still present in scan', () => {
247
+ const finding = makeFinding();
248
+ const result = makeScanResult([finding]);
249
+ const baseline = createBaseline(result);
250
+ const { valid, invalid } = validateBaseline(baseline, result);
251
+ expect(valid).toHaveLength(1);
252
+ expect(invalid).toHaveLength(0);
253
+ });
254
+ it('returns invalid when baseline findings are no longer in scan', () => {
255
+ const finding = makeFinding();
256
+ const baseline = createBaseline(makeScanResult([finding]));
257
+ const emptyResult = makeScanResult([]);
258
+ const { valid, invalid } = validateBaseline(baseline, emptyResult);
259
+ expect(valid).toHaveLength(0);
260
+ expect(invalid).toHaveLength(1);
261
+ });
262
+ it('correctly separates valid and invalid findings', () => {
263
+ const f1 = makeFinding({ line: 10 });
264
+ const f2 = makeFinding({ line: 20 });
265
+ const baseline = createBaseline(makeScanResult([f1, f2]));
266
+ // Only f1 still present
267
+ const { valid, invalid } = validateBaseline(baseline, makeScanResult([f1]));
268
+ expect(valid).toHaveLength(1);
269
+ expect(invalid).toHaveLength(1);
270
+ });
271
+ });
272
+ // ---------------------------------------------------------------------------
273
+ // getDefaultBaselinePath
274
+ // ---------------------------------------------------------------------------
275
+ describe('getDefaultBaselinePath', () => {
276
+ it('returns a path ending in .ferret-baseline.json', () => {
277
+ const path = getDefaultBaselinePath(['/some/project/dir']);
278
+ expect(path).toMatch(/\.ferret-baseline\.json$/);
279
+ });
280
+ it('uses process.cwd() when no paths provided', () => {
281
+ const path = getDefaultBaselinePath([]);
282
+ expect(path).toMatch(/\.ferret-baseline\.json$/);
283
+ });
284
+ });
285
+ // ---------------------------------------------------------------------------
286
+ // getBaselineStats
287
+ // ---------------------------------------------------------------------------
288
+ describe('getBaselineStats', () => {
289
+ it('returns zero stats for empty baseline', () => {
290
+ const baseline = makeBaseline();
291
+ const stats = getBaselineStats(baseline);
292
+ expect(stats.totalFindings).toBe(0);
293
+ expect(stats.byRule).toEqual({});
294
+ expect(stats.bySeverity).toEqual({});
295
+ });
296
+ it('counts findings by rule', () => {
297
+ const findings = [
298
+ { ruleId: 'INJ-001', file: 'a.md', line: 1, match: 'x', hash: 'h1', acceptedDate: '2024-01-01T00:00:00Z', severity: 'HIGH' },
299
+ { ruleId: 'INJ-001', file: 'b.md', line: 2, match: 'y', hash: 'h2', acceptedDate: '2024-01-02T00:00:00Z', severity: 'HIGH' },
300
+ { ruleId: 'CRED-001', file: 'c.md', line: 3, match: 'z', hash: 'h3', acceptedDate: '2024-01-03T00:00:00Z', severity: 'CRITICAL' },
301
+ ];
302
+ const baseline = makeBaseline({ findings });
303
+ const stats = getBaselineStats(baseline);
304
+ expect(stats.totalFindings).toBe(3);
305
+ expect(stats.byRule['INJ-001']).toBe(2);
306
+ expect(stats.byRule['CRED-001']).toBe(1);
307
+ expect(stats.bySeverity['HIGH']).toBe(2);
308
+ expect(stats.bySeverity['CRITICAL']).toBe(1);
309
+ });
310
+ it('tracks oldest and newest finding dates', () => {
311
+ const findings = [
312
+ { ruleId: 'X-001', file: 'a.md', line: 1, match: 'x', hash: 'h1', acceptedDate: '2023-01-01T00:00:00Z' },
313
+ { ruleId: 'X-001', file: 'b.md', line: 2, match: 'y', hash: 'h2', acceptedDate: '2025-12-01T00:00:00Z' },
314
+ ];
315
+ const baseline = makeBaseline({ findings });
316
+ const stats = getBaselineStats(baseline);
317
+ expect(stats.oldestFinding).toBe('2023-01-01T00:00:00Z');
318
+ expect(stats.newestFinding).toBe('2025-12-01T00:00:00Z');
319
+ });
320
+ });
321
+ //# sourceMappingURL=baseline.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Additional Baseline Tests
3
+ * Tests for baseline utility functions
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=baselineExtra.test.d.ts.map
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Additional Baseline Tests
3
+ * Tests for baseline utility functions
4
+ */
5
+ import { computeBaselineIntegrity, verifyBaselineIntegrity, loadBaseline, saveBaseline, createBaseline, addToBaseline, removeFromBaseline, filterAgainstBaseline, validateBaseline, getDefaultBaselinePath, getBaselineStats, } from '../utils/baseline.js';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ function makeFinding(overrides = {}) {
10
+ return {
11
+ ruleId: 'INJ-001',
12
+ ruleName: 'Test Rule',
13
+ severity: 'HIGH',
14
+ category: 'injection',
15
+ file: '/project/test.md',
16
+ relativePath: 'test.md',
17
+ line: 1,
18
+ match: 'bad content',
19
+ context: [],
20
+ remediation: 'fix it',
21
+ timestamp: new Date(),
22
+ riskScore: 75,
23
+ ...overrides,
24
+ };
25
+ }
26
+ function makeScanResult(findings = []) {
27
+ return {
28
+ success: true,
29
+ startTime: new Date(),
30
+ endTime: new Date(),
31
+ duration: 100,
32
+ scannedPaths: ['/project'],
33
+ totalFiles: 5,
34
+ analyzedFiles: 5,
35
+ skippedFiles: 0,
36
+ findings,
37
+ findingsBySeverity: {
38
+ CRITICAL: [],
39
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
40
+ MEDIUM: [],
41
+ LOW: [],
42
+ INFO: [],
43
+ },
44
+ findingsByCategory: {},
45
+ overallRiskScore: 0,
46
+ summary: {
47
+ critical: 0,
48
+ high: findings.filter(f => f.severity === 'HIGH').length,
49
+ medium: 0,
50
+ low: 0,
51
+ info: 0,
52
+ total: findings.length,
53
+ },
54
+ errors: [],
55
+ };
56
+ }
57
+ function makeBaseline(overrides = {}) {
58
+ return {
59
+ version: '1.0',
60
+ createdDate: new Date().toISOString(),
61
+ lastUpdated: new Date().toISOString(),
62
+ findings: [],
63
+ ...overrides,
64
+ };
65
+ }
66
+ describe('computeBaselineIntegrity', () => {
67
+ it('returns an integrity object with sha256 algorithm', () => {
68
+ const baseline = makeBaseline();
69
+ const integrity = computeBaselineIntegrity(baseline);
70
+ expect(integrity.algorithm).toBe('sha256');
71
+ expect(typeof integrity.hash).toBe('string');
72
+ expect(integrity.hash.length).toBeGreaterThan(0);
73
+ });
74
+ it('produces consistent hashes', () => {
75
+ const baseline = makeBaseline({ description: 'Test' });
76
+ const i1 = computeBaselineIntegrity(baseline);
77
+ const i2 = computeBaselineIntegrity(baseline);
78
+ expect(i1.hash).toBe(i2.hash);
79
+ });
80
+ it('produces different hashes for different content', () => {
81
+ const b1 = makeBaseline({ description: 'Test 1' });
82
+ const b2 = makeBaseline({ description: 'Test 2' });
83
+ expect(computeBaselineIntegrity(b1).hash).not.toBe(computeBaselineIntegrity(b2).hash);
84
+ });
85
+ });
86
+ describe('verifyBaselineIntegrity', () => {
87
+ it('returns true when no integrity field present', () => {
88
+ const baseline = makeBaseline();
89
+ expect(verifyBaselineIntegrity(baseline)).toBe(true);
90
+ });
91
+ it('returns true when integrity matches', () => {
92
+ const baseline = makeBaseline();
93
+ const integrity = computeBaselineIntegrity(baseline);
94
+ const withIntegrity = { ...baseline, integrity };
95
+ expect(verifyBaselineIntegrity(withIntegrity)).toBe(true);
96
+ });
97
+ it('returns false when integrity does not match', () => {
98
+ const baseline = makeBaseline();
99
+ const withFakeIntegrity = {
100
+ ...baseline,
101
+ integrity: { algorithm: 'sha256', hash: 'fakehash123' },
102
+ };
103
+ expect(verifyBaselineIntegrity(withFakeIntegrity)).toBe(false);
104
+ });
105
+ });
106
+ describe('loadBaseline', () => {
107
+ let tmpDir;
108
+ beforeEach(() => {
109
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-baseline-'));
110
+ });
111
+ afterEach(() => {
112
+ fs.rmSync(tmpDir, { recursive: true, force: true });
113
+ });
114
+ it('returns null for non-existent file', async () => {
115
+ const result = await loadBaseline('/nonexistent/baseline.json');
116
+ expect(result).toBeNull();
117
+ });
118
+ it('loads a valid baseline', async () => {
119
+ const filePath = path.join(tmpDir, 'baseline.json');
120
+ const baseline = makeBaseline();
121
+ fs.writeFileSync(filePath, JSON.stringify(baseline));
122
+ const result = await loadBaseline(filePath);
123
+ expect(result).not.toBeNull();
124
+ expect(result?.version).toBe('1.0');
125
+ });
126
+ it('returns null for invalid JSON', async () => {
127
+ const filePath = path.join(tmpDir, 'baseline.json');
128
+ fs.writeFileSync(filePath, 'invalid json {{{');
129
+ const result = await loadBaseline(filePath);
130
+ expect(result).toBeNull();
131
+ });
132
+ it('returns null for baseline with missing required fields', async () => {
133
+ const filePath = path.join(tmpDir, 'baseline.json');
134
+ fs.writeFileSync(filePath, JSON.stringify({ version: '1.0' })); // missing findings
135
+ const result = await loadBaseline(filePath);
136
+ expect(result).toBeNull();
137
+ });
138
+ });
139
+ describe('saveBaseline', () => {
140
+ let tmpDir;
141
+ beforeEach(() => {
142
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-baseline-'));
143
+ });
144
+ afterEach(() => {
145
+ fs.rmSync(tmpDir, { recursive: true, force: true });
146
+ });
147
+ it('saves baseline to file with integrity', async () => {
148
+ const filePath = path.join(tmpDir, 'baseline.json');
149
+ const baseline = makeBaseline();
150
+ await saveBaseline(baseline, filePath);
151
+ expect(fs.existsSync(filePath)).toBe(true);
152
+ const saved = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
153
+ expect(saved.version).toBe('1.0');
154
+ expect(saved.integrity).toBeDefined();
155
+ });
156
+ it('creates nested directories', async () => {
157
+ const filePath = path.join(tmpDir, 'nested', 'dir', 'baseline.json');
158
+ const baseline = makeBaseline();
159
+ await saveBaseline(baseline, filePath);
160
+ expect(fs.existsSync(filePath)).toBe(true);
161
+ });
162
+ });
163
+ describe('createBaseline', () => {
164
+ it('creates a baseline from scan results', () => {
165
+ const findings = [makeFinding(), makeFinding({ ruleId: 'CRED-001', line: 5 })];
166
+ const result = makeScanResult(findings);
167
+ const baseline = createBaseline(result);
168
+ expect(baseline.version).toBe('1.0');
169
+ expect(baseline.findings).toHaveLength(2);
170
+ expect(baseline.findings[0]?.hash).toBeDefined();
171
+ });
172
+ it('includes description when provided', () => {
173
+ const baseline = createBaseline(makeScanResult(), 'My custom baseline');
174
+ expect(baseline.description).toBe('My custom baseline');
175
+ });
176
+ it('generates default description when not provided', () => {
177
+ const baseline = createBaseline(makeScanResult());
178
+ expect(baseline.description).toBeDefined();
179
+ expect(baseline.description).toContain('/project');
180
+ });
181
+ });
182
+ describe('addToBaseline', () => {
183
+ it('adds new findings to baseline', () => {
184
+ const baseline = makeBaseline();
185
+ const findings = [makeFinding()];
186
+ const updated = addToBaseline(baseline, findings);
187
+ expect(updated.findings).toHaveLength(1);
188
+ });
189
+ it('does not add duplicate findings', () => {
190
+ const finding = makeFinding();
191
+ // First add via createBaseline which generates the same hash
192
+ const baseline = createBaseline(makeScanResult([finding]));
193
+ const updated = addToBaseline(baseline, [finding]);
194
+ // Should still be 1 (not duplicated)
195
+ expect(updated.findings).toHaveLength(1);
196
+ });
197
+ it('includes reason when provided', () => {
198
+ const baseline = makeBaseline();
199
+ const updated = addToBaseline(baseline, [makeFinding()], 'Accepted as known issue');
200
+ expect(updated.findings[0]?.reason).toBe('Accepted as known issue');
201
+ });
202
+ });
203
+ describe('removeFromBaseline', () => {
204
+ it('removes findings by hash', () => {
205
+ const finding = {
206
+ ruleId: 'INJ-001',
207
+ file: 'test.md',
208
+ line: 1,
209
+ match: 'bad',
210
+ hash: 'abc123',
211
+ acceptedDate: new Date().toISOString(),
212
+ };
213
+ const baseline = makeBaseline({ findings: [finding] });
214
+ const updated = removeFromBaseline(baseline, ['abc123']);
215
+ expect(updated.findings).toHaveLength(0);
216
+ });
217
+ it('keeps non-matching findings', () => {
218
+ const finding1 = {
219
+ ruleId: 'INJ-001', file: 'test.md', line: 1, match: 'bad',
220
+ hash: 'abc123', acceptedDate: new Date().toISOString(),
221
+ };
222
+ const finding2 = {
223
+ ruleId: 'CRED-001', file: 'test.md', line: 2, match: 'secret',
224
+ hash: 'def456', acceptedDate: new Date().toISOString(),
225
+ };
226
+ const baseline = makeBaseline({ findings: [finding1, finding2] });
227
+ const updated = removeFromBaseline(baseline, ['abc123']);
228
+ expect(updated.findings).toHaveLength(1);
229
+ expect(updated.findings[0]?.hash).toBe('def456');
230
+ });
231
+ });
232
+ describe('filterAgainstBaseline', () => {
233
+ it('returns same result when baseline is null', () => {
234
+ const findings = [makeFinding()];
235
+ const result = makeScanResult(findings);
236
+ const filtered = filterAgainstBaseline(result, null);
237
+ expect(filtered.findings).toHaveLength(1);
238
+ });
239
+ it('returns same result when baseline is empty', () => {
240
+ const findings = [makeFinding()];
241
+ const result = makeScanResult(findings);
242
+ const filtered = filterAgainstBaseline(result, makeBaseline());
243
+ expect(filtered.findings).toHaveLength(1);
244
+ });
245
+ it('filters out baseline findings', () => {
246
+ const finding = makeFinding();
247
+ // Create a baseline that contains this finding
248
+ const baselineResult = makeScanResult([finding]);
249
+ const baseline = createBaseline(baselineResult);
250
+ const result = makeScanResult([finding]);
251
+ const filtered = filterAgainstBaseline(result, baseline);
252
+ expect(filtered.findings).toHaveLength(0);
253
+ });
254
+ it('keeps new findings not in baseline', () => {
255
+ const knownFinding = makeFinding({ ruleId: 'INJ-001', line: 1 });
256
+ const newFinding = makeFinding({ ruleId: 'CRED-001', line: 5 });
257
+ const baseline = createBaseline(makeScanResult([knownFinding]));
258
+ const result = makeScanResult([knownFinding, newFinding]);
259
+ const filtered = filterAgainstBaseline(result, baseline);
260
+ expect(filtered.findings).toHaveLength(1);
261
+ expect(filtered.findings[0]?.ruleId).toBe('CRED-001');
262
+ });
263
+ });
264
+ describe('getDefaultBaselinePath', () => {
265
+ it('returns a string path', () => {
266
+ const p = getDefaultBaselinePath(['/project']);
267
+ expect(typeof p).toBe('string');
268
+ expect(p.length).toBeGreaterThan(0);
269
+ });
270
+ it('handles empty paths', () => {
271
+ const p = getDefaultBaselinePath([]);
272
+ expect(typeof p).toBe('string');
273
+ });
274
+ });
275
+ describe('getBaselineStats', () => {
276
+ it('returns stats for empty baseline', () => {
277
+ const stats = getBaselineStats(makeBaseline());
278
+ expect(stats.totalFindings).toBe(0);
279
+ });
280
+ it('returns correct stats for baseline with findings', () => {
281
+ const baseline = makeBaseline({
282
+ findings: [
283
+ { ruleId: 'INJ-001', file: 'test.md', line: 1, match: 'x', hash: 'a', acceptedDate: new Date().toISOString(), severity: 'HIGH' },
284
+ { ruleId: 'CRED-001', file: 'test.md', line: 2, match: 'y', hash: 'b', acceptedDate: new Date().toISOString(), severity: 'CRITICAL' },
285
+ ],
286
+ });
287
+ const stats = getBaselineStats(baseline);
288
+ expect(stats.totalFindings).toBe(2);
289
+ });
290
+ });
291
+ describe('validateBaseline', () => {
292
+ it('validates a baseline against scan results', () => {
293
+ const finding = makeFinding();
294
+ const scanResult = makeScanResult([finding]);
295
+ const baseline = createBaseline(scanResult);
296
+ const result = validateBaseline(baseline, scanResult);
297
+ expect(Array.isArray(result.valid)).toBe(true);
298
+ expect(Array.isArray(result.invalid)).toBe(true);
299
+ expect(result.valid.length).toBe(1);
300
+ });
301
+ it('marks findings as invalid when not in current scan', () => {
302
+ const oldFinding = {
303
+ ruleId: 'OLD-001',
304
+ file: 'old.md',
305
+ line: 1,
306
+ match: 'old content',
307
+ hash: 'oldhash123',
308
+ acceptedDate: new Date().toISOString(),
309
+ };
310
+ const baseline = makeBaseline({ findings: [oldFinding] });
311
+ const emptyResult = makeScanResult([]);
312
+ const result = validateBaseline(baseline, emptyResult);
313
+ expect(result.invalid).toHaveLength(1);
314
+ expect(result.valid).toHaveLength(0);
315
+ });
316
+ });
317
+ //# sourceMappingURL=baselineExtra.test.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Capability Mapping Tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=capabilityMapping.test.d.ts.map