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,510 @@
1
+ /**
2
+ * Policy Enforcement Tests
3
+ */
4
+ import { evaluatePolicy, formatPolicyResult, loadPolicy, savePolicy, findPolicyFile, initPolicy, policyViolationsToFindings, DEFAULT_POLICY, } from '../features/policyEnforcement.js';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ // ---------------------------------------------------------------------------
9
+ // Helpers
10
+ // ---------------------------------------------------------------------------
11
+ function makeFinding(overrides = {}) {
12
+ return {
13
+ ruleId: 'INJ-001',
14
+ ruleName: 'Injection Rule',
15
+ severity: 'HIGH',
16
+ category: 'injection',
17
+ file: '/project/test.md',
18
+ relativePath: 'test.md',
19
+ line: 1,
20
+ match: 'bad content',
21
+ context: [],
22
+ remediation: 'fix it',
23
+ timestamp: new Date(),
24
+ riskScore: 75,
25
+ ...overrides,
26
+ };
27
+ }
28
+ function makeScanResult(findings = [], overrides = {}) {
29
+ const critical = findings.filter(f => f.severity === 'CRITICAL').length;
30
+ const high = findings.filter(f => f.severity === 'HIGH').length;
31
+ const medium = findings.filter(f => f.severity === 'MEDIUM').length;
32
+ const low = findings.filter(f => f.severity === 'LOW').length;
33
+ return {
34
+ success: true,
35
+ startTime: new Date(),
36
+ endTime: new Date(),
37
+ duration: 100,
38
+ scannedPaths: ['/project'],
39
+ totalFiles: 10,
40
+ analyzedFiles: 10,
41
+ skippedFiles: 0,
42
+ findings,
43
+ findingsBySeverity: {
44
+ CRITICAL: findings.filter(f => f.severity === 'CRITICAL'),
45
+ HIGH: findings.filter(f => f.severity === 'HIGH'),
46
+ MEDIUM: findings.filter(f => f.severity === 'MEDIUM'),
47
+ LOW: findings.filter(f => f.severity === 'LOW'),
48
+ INFO: findings.filter(f => f.severity === 'INFO'),
49
+ },
50
+ findingsByCategory: {},
51
+ overallRiskScore: 0,
52
+ summary: { critical, high, medium, low, info: 0, total: findings.length },
53
+ errors: [],
54
+ ...overrides,
55
+ };
56
+ }
57
+ function makeMinimalPolicy(overrides = {}) {
58
+ return {
59
+ name: 'Test Policy',
60
+ version: '1.0.0',
61
+ rules: [],
62
+ settings: {
63
+ failOnBlock: true,
64
+ exitCodeOnBlock: 1,
65
+ exitCodeOnWarn: 0,
66
+ reportViolations: true,
67
+ },
68
+ ...overrides,
69
+ };
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // evaluatePolicy
73
+ // ---------------------------------------------------------------------------
74
+ describe('evaluatePolicy', () => {
75
+ it('passes with no rules and no findings', () => {
76
+ const result = evaluatePolicy(makeScanResult(), makeMinimalPolicy());
77
+ expect(result.passed).toBe(true);
78
+ expect(result.violations).toHaveLength(0);
79
+ expect(result.blockers).toHaveLength(0);
80
+ expect(result.exitCode).toBe(0);
81
+ });
82
+ it('blocks on CRITICAL findings with block rule', () => {
83
+ const policy = makeMinimalPolicy({
84
+ rules: [
85
+ {
86
+ id: 'no-critical',
87
+ enabled: true,
88
+ action: 'block',
89
+ conditions: { severities: ['CRITICAL'] },
90
+ message: 'No critical allowed',
91
+ },
92
+ ],
93
+ });
94
+ const findings = [makeFinding({ severity: 'CRITICAL', riskScore: 95 })];
95
+ const result = evaluatePolicy(makeScanResult(findings), policy);
96
+ expect(result.passed).toBe(false);
97
+ expect(result.blockers).toHaveLength(1);
98
+ expect(result.exitCode).toBe(1);
99
+ });
100
+ it('warns (does not block) on warn-action rule', () => {
101
+ const policy = makeMinimalPolicy({
102
+ rules: [
103
+ {
104
+ id: 'warn-high',
105
+ enabled: true,
106
+ action: 'warn',
107
+ conditions: { severities: ['HIGH'] },
108
+ message: 'High severity found',
109
+ },
110
+ ],
111
+ });
112
+ const findings = [makeFinding({ severity: 'HIGH' })];
113
+ const result = evaluatePolicy(makeScanResult(findings), policy);
114
+ expect(result.passed).toBe(true);
115
+ expect(result.warnings).toHaveLength(1);
116
+ expect(result.blockers).toHaveLength(0);
117
+ expect(result.exitCode).toBe(0);
118
+ });
119
+ it('ignores disabled rules', () => {
120
+ const policy = makeMinimalPolicy({
121
+ rules: [
122
+ {
123
+ id: 'disabled-rule',
124
+ enabled: false,
125
+ action: 'block',
126
+ conditions: { severities: ['CRITICAL'] },
127
+ },
128
+ ],
129
+ });
130
+ const findings = [makeFinding({ severity: 'CRITICAL' })];
131
+ const result = evaluatePolicy(makeScanResult(findings), policy);
132
+ expect(result.passed).toBe(true);
133
+ expect(result.blockers).toHaveLength(0);
134
+ });
135
+ it('blocks when maxFindings exceeded', () => {
136
+ const policy = makeMinimalPolicy({
137
+ rules: [
138
+ {
139
+ id: 'max-findings',
140
+ enabled: true,
141
+ action: 'block',
142
+ conditions: { maxFindings: 2 },
143
+ message: 'Too many findings',
144
+ },
145
+ ],
146
+ });
147
+ const findings = [
148
+ makeFinding(), makeFinding(), makeFinding(),
149
+ ];
150
+ const result = evaluatePolicy(makeScanResult(findings), policy);
151
+ expect(result.passed).toBe(false);
152
+ expect(result.blockers).toHaveLength(1);
153
+ });
154
+ it('passes when finding count is within maxFindings', () => {
155
+ const policy = makeMinimalPolicy({
156
+ rules: [
157
+ {
158
+ id: 'max-findings',
159
+ enabled: true,
160
+ action: 'block',
161
+ conditions: { maxFindings: 5 },
162
+ },
163
+ ],
164
+ });
165
+ const findings = [makeFinding(), makeFinding()];
166
+ const result = evaluatePolicy(makeScanResult(findings), policy);
167
+ expect(result.passed).toBe(true);
168
+ });
169
+ it('filters by category', () => {
170
+ const policy = makeMinimalPolicy({
171
+ rules: [
172
+ {
173
+ id: 'no-credentials',
174
+ enabled: true,
175
+ action: 'block',
176
+ conditions: { categories: ['credentials'], severities: ['HIGH', 'CRITICAL'] },
177
+ },
178
+ ],
179
+ });
180
+ const injectionFinding = makeFinding({ severity: 'HIGH', category: 'injection' });
181
+ const credFinding = makeFinding({ severity: 'HIGH', category: 'credentials' });
182
+ const result = evaluatePolicy(makeScanResult([injectionFinding, credFinding]), policy);
183
+ expect(result.blockers).toHaveLength(1);
184
+ expect(result.blockers[0]?.findings).toHaveLength(1);
185
+ });
186
+ it('filters by ruleId', () => {
187
+ const policy = makeMinimalPolicy({
188
+ rules: [
189
+ {
190
+ id: 'specific-rule',
191
+ enabled: true,
192
+ action: 'block',
193
+ conditions: { ruleIds: ['INJ-001'] },
194
+ },
195
+ ],
196
+ });
197
+ const matchingFinding = makeFinding({ ruleId: 'INJ-001' });
198
+ const otherFinding = makeFinding({ ruleId: 'CRED-002' });
199
+ const result = evaluatePolicy(makeScanResult([matchingFinding, otherFinding]), policy);
200
+ expect(result.blockers).toHaveLength(1);
201
+ expect(result.blockers[0]?.findings[0]?.ruleId).toBe('INJ-001');
202
+ });
203
+ it('filters by ruleId with wildcard', () => {
204
+ const policy = makeMinimalPolicy({
205
+ rules: [
206
+ {
207
+ id: 'wildcard-rule',
208
+ enabled: true,
209
+ action: 'block',
210
+ conditions: { ruleIds: ['INJ-*'] },
211
+ },
212
+ ],
213
+ });
214
+ const matchingFinding = makeFinding({ ruleId: 'INJ-001' });
215
+ const otherFinding = makeFinding({ ruleId: 'CRED-002' });
216
+ const result = evaluatePolicy(makeScanResult([matchingFinding, otherFinding]), policy);
217
+ expect(result.blockers).toHaveLength(1);
218
+ });
219
+ it('filters by minRiskScore', () => {
220
+ const policy = makeMinimalPolicy({
221
+ rules: [
222
+ {
223
+ id: 'high-risk',
224
+ enabled: true,
225
+ action: 'block',
226
+ conditions: { minRiskScore: 90 },
227
+ },
228
+ ],
229
+ });
230
+ const lowRiskFinding = makeFinding({ riskScore: 50 });
231
+ const highRiskFinding = makeFinding({ riskScore: 95 });
232
+ const result = evaluatePolicy(makeScanResult([lowRiskFinding, highRiskFinding]), policy);
233
+ expect(result.blockers).toHaveLength(1);
234
+ expect(result.blockers[0]?.findings[0]?.riskScore).toBe(95);
235
+ });
236
+ it('enforces settings.maxCritical', () => {
237
+ const policy = makeMinimalPolicy({
238
+ settings: {
239
+ failOnBlock: true,
240
+ exitCodeOnBlock: 1,
241
+ exitCodeOnWarn: 0,
242
+ reportViolations: true,
243
+ maxCritical: 0,
244
+ },
245
+ });
246
+ const findings = [makeFinding({ severity: 'CRITICAL' })];
247
+ const result = evaluatePolicy(makeScanResult(findings), policy);
248
+ expect(result.passed).toBe(false);
249
+ expect(result.blockers.some(b => b.ruleId === 'settings-max-critical')).toBe(true);
250
+ });
251
+ it('enforces settings.maxHigh', () => {
252
+ const policy = makeMinimalPolicy({
253
+ settings: {
254
+ failOnBlock: true,
255
+ exitCodeOnBlock: 1,
256
+ exitCodeOnWarn: 0,
257
+ reportViolations: true,
258
+ maxHigh: 1,
259
+ },
260
+ });
261
+ const findings = [
262
+ makeFinding({ severity: 'HIGH' }),
263
+ makeFinding({ severity: 'HIGH' }),
264
+ ];
265
+ const result = evaluatePolicy(makeScanResult(findings), policy);
266
+ expect(result.passed).toBe(false);
267
+ expect(result.blockers.some(b => b.ruleId === 'settings-max-high')).toBe(true);
268
+ });
269
+ it('enforces settings.maxTotal', () => {
270
+ const policy = makeMinimalPolicy({
271
+ settings: {
272
+ failOnBlock: true,
273
+ exitCodeOnBlock: 1,
274
+ exitCodeOnWarn: 0,
275
+ reportViolations: true,
276
+ maxTotal: 2,
277
+ },
278
+ });
279
+ const findings = [makeFinding(), makeFinding(), makeFinding()];
280
+ const result = evaluatePolicy(makeScanResult(findings), policy);
281
+ expect(result.passed).toBe(false);
282
+ expect(result.blockers.some(b => b.ruleId === 'settings-max-total')).toBe(true);
283
+ });
284
+ it('enforces settings.minOverallScore', () => {
285
+ const policy = makeMinimalPolicy({
286
+ settings: {
287
+ failOnBlock: true,
288
+ exitCodeOnBlock: 1,
289
+ exitCodeOnWarn: 0,
290
+ reportViolations: true,
291
+ minOverallScore: 80,
292
+ },
293
+ });
294
+ // overallRiskScore=50 => invertedScore=50, which is < 80 minimum
295
+ const result = evaluatePolicy(makeScanResult([], { overallRiskScore: 50 }), policy);
296
+ expect(result.passed).toBe(false);
297
+ expect(result.blockers.some(b => b.ruleId === 'settings-min-score')).toBe(true);
298
+ });
299
+ it('uses custom exitCode when failOnBlock is false', () => {
300
+ const policy = makeMinimalPolicy({
301
+ settings: {
302
+ failOnBlock: false,
303
+ exitCodeOnBlock: 2,
304
+ exitCodeOnWarn: 0,
305
+ reportViolations: true,
306
+ },
307
+ rules: [
308
+ {
309
+ id: 'block-rule',
310
+ enabled: true,
311
+ action: 'block',
312
+ conditions: { severities: ['CRITICAL'] },
313
+ },
314
+ ],
315
+ });
316
+ const findings = [makeFinding({ severity: 'CRITICAL' })];
317
+ const result = evaluatePolicy(makeScanResult(findings), policy);
318
+ expect(result.exitCode).toBe(0); // failOnBlock=false means no exit code change
319
+ });
320
+ it('uses file patterns filter', () => {
321
+ const policy = makeMinimalPolicy({
322
+ rules: [
323
+ {
324
+ id: 'file-rule',
325
+ enabled: true,
326
+ action: 'block',
327
+ conditions: { filePatterns: ['*.md'] },
328
+ },
329
+ ],
330
+ });
331
+ const mdFinding = makeFinding({ file: '/project/test.md', relativePath: 'test.md' });
332
+ const tsFinding = makeFinding({ file: '/project/test.ts', relativePath: 'test.ts' });
333
+ const result = evaluatePolicy(makeScanResult([mdFinding, tsFinding]), policy);
334
+ expect(result.blockers).toHaveLength(1);
335
+ expect(result.blockers[0]?.findings).toHaveLength(1);
336
+ });
337
+ it('DEFAULT_POLICY blocks on critical findings', () => {
338
+ const findings = [makeFinding({ severity: 'CRITICAL', category: 'injection' })];
339
+ const result = evaluatePolicy(makeScanResult(findings), DEFAULT_POLICY);
340
+ expect(result.passed).toBe(false);
341
+ expect(result.blockers.some(b => b.ruleId === 'no-critical')).toBe(true);
342
+ });
343
+ });
344
+ // ---------------------------------------------------------------------------
345
+ // formatPolicyResult
346
+ // ---------------------------------------------------------------------------
347
+ describe('formatPolicyResult', () => {
348
+ it('includes PASSED when no violations', () => {
349
+ const result = evaluatePolicy(makeScanResult(), makeMinimalPolicy());
350
+ const formatted = formatPolicyResult(result);
351
+ expect(formatted).toContain('PASSED');
352
+ expect(formatted).toContain('Exit code: 0');
353
+ });
354
+ it('includes FAILED when violations exist', () => {
355
+ const policy = makeMinimalPolicy({
356
+ rules: [
357
+ { id: 'fail', enabled: true, action: 'block', conditions: { severities: ['CRITICAL'] } },
358
+ ],
359
+ });
360
+ const findings = [makeFinding({ severity: 'CRITICAL' })];
361
+ const result = evaluatePolicy(makeScanResult(findings), policy);
362
+ const formatted = formatPolicyResult(result);
363
+ expect(formatted).toContain('FAILED');
364
+ expect(formatted).toContain('BLOCKERS:');
365
+ });
366
+ it('includes warnings section', () => {
367
+ const policy = makeMinimalPolicy({
368
+ rules: [
369
+ { id: 'warn', enabled: true, action: 'warn', conditions: { severities: ['HIGH'] }, message: 'High found' },
370
+ ],
371
+ });
372
+ const findings = [makeFinding({ severity: 'HIGH' })];
373
+ const result = evaluatePolicy(makeScanResult(findings), policy);
374
+ const formatted = formatPolicyResult(result);
375
+ expect(formatted).toContain('WARNINGS:');
376
+ });
377
+ });
378
+ // ---------------------------------------------------------------------------
379
+ // loadPolicy / savePolicy
380
+ // ---------------------------------------------------------------------------
381
+ describe('loadPolicy', () => {
382
+ it('returns null for non-existent file', () => {
383
+ const result = loadPolicy('/nonexistent/policy.json');
384
+ expect(result).toBeNull();
385
+ });
386
+ it('returns null for invalid JSON', () => {
387
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-policy-'));
388
+ const filePath = path.join(tmpDir, 'policy.json');
389
+ fs.writeFileSync(filePath, 'invalid json {{{');
390
+ const result = loadPolicy(filePath);
391
+ expect(result).toBeNull();
392
+ fs.rmSync(tmpDir, { recursive: true });
393
+ });
394
+ it('returns null for valid JSON but invalid schema', () => {
395
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-policy-'));
396
+ const filePath = path.join(tmpDir, 'policy.json');
397
+ fs.writeFileSync(filePath, JSON.stringify({ not: 'a valid policy' }));
398
+ const result = loadPolicy(filePath);
399
+ expect(result).toBeNull();
400
+ fs.rmSync(tmpDir, { recursive: true });
401
+ });
402
+ it('loads a valid policy', () => {
403
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-policy-'));
404
+ const filePath = path.join(tmpDir, 'policy.json');
405
+ const policy = {
406
+ name: 'Test Policy',
407
+ version: '1.0.0',
408
+ rules: [],
409
+ settings: {
410
+ failOnBlock: true,
411
+ exitCodeOnBlock: 1,
412
+ exitCodeOnWarn: 0,
413
+ reportViolations: true,
414
+ },
415
+ };
416
+ fs.writeFileSync(filePath, JSON.stringify(policy));
417
+ const loaded = loadPolicy(filePath);
418
+ expect(loaded).not.toBeNull();
419
+ expect(loaded?.name).toBe('Test Policy');
420
+ fs.rmSync(tmpDir, { recursive: true });
421
+ });
422
+ });
423
+ describe('savePolicy', () => {
424
+ it('saves policy to file and returns true', () => {
425
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-policy-'));
426
+ const filePath = path.join(tmpDir, 'policy.json');
427
+ const policy = makeMinimalPolicy({ name: 'Saved Policy' });
428
+ const success = savePolicy(policy, filePath);
429
+ expect(success).toBe(true);
430
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
431
+ expect(content.name).toBe('Saved Policy');
432
+ fs.rmSync(tmpDir, { recursive: true });
433
+ });
434
+ it('returns false when write fails', () => {
435
+ const result = savePolicy(makeMinimalPolicy(), '/nonexistent-dir/policy.json');
436
+ expect(result).toBe(false);
437
+ });
438
+ });
439
+ // ---------------------------------------------------------------------------
440
+ // findPolicyFile
441
+ // ---------------------------------------------------------------------------
442
+ describe('findPolicyFile', () => {
443
+ it('returns null when no policy file found', () => {
444
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-policy-'));
445
+ expect(findPolicyFile(tmpDir)).toBeNull();
446
+ fs.rmSync(tmpDir, { recursive: true });
447
+ });
448
+ it('finds .ferret-policy.json', () => {
449
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-policy-'));
450
+ const filePath = path.join(tmpDir, '.ferret-policy.json');
451
+ fs.writeFileSync(filePath, '{}');
452
+ expect(findPolicyFile(tmpDir)).toBe(filePath);
453
+ fs.rmSync(tmpDir, { recursive: true });
454
+ });
455
+ });
456
+ // ---------------------------------------------------------------------------
457
+ // initPolicy
458
+ // ---------------------------------------------------------------------------
459
+ describe('initPolicy', () => {
460
+ it('creates default policy file', () => {
461
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-policy-'));
462
+ const filePath = initPolicy(tmpDir);
463
+ expect(fs.existsSync(filePath)).toBe(true);
464
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
465
+ expect(content.name).toBeTruthy();
466
+ fs.rmSync(tmpDir, { recursive: true });
467
+ });
468
+ it('creates strict policy file', () => {
469
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-policy-'));
470
+ initPolicy(tmpDir, 'strict');
471
+ const filePath = path.join(tmpDir, '.ferret-policy.json');
472
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
473
+ expect(content.name).toContain('Strict');
474
+ fs.rmSync(tmpDir, { recursive: true });
475
+ });
476
+ it('creates minimal policy file', () => {
477
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-policy-'));
478
+ initPolicy(tmpDir, 'minimal');
479
+ const filePath = path.join(tmpDir, '.ferret-policy.json');
480
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
481
+ expect(content.name).toContain('Minimal');
482
+ fs.rmSync(tmpDir, { recursive: true });
483
+ });
484
+ });
485
+ // ---------------------------------------------------------------------------
486
+ // policyViolationsToFindings
487
+ // ---------------------------------------------------------------------------
488
+ describe('policyViolationsToFindings', () => {
489
+ it('converts violations to findings', () => {
490
+ const violations = [
491
+ {
492
+ ruleId: 'no-critical',
493
+ ruleName: 'No Critical Findings',
494
+ action: 'block',
495
+ message: 'Critical issue detected',
496
+ findings: [makeFinding()],
497
+ severity: 'CRITICAL',
498
+ },
499
+ ];
500
+ const findings = policyViolationsToFindings(violations, '/project/.ferret-policy.json');
501
+ expect(findings).toHaveLength(1);
502
+ expect(findings[0]?.ruleId).toBe('POLICY-NO-CRITICAL');
503
+ expect(findings[0]?.severity).toBe('CRITICAL');
504
+ });
505
+ it('returns empty array for empty violations', () => {
506
+ const findings = policyViolationsToFindings([], '/project/.ferret-policy.json');
507
+ expect(findings).toHaveLength(0);
508
+ });
509
+ });
510
+ //# sourceMappingURL=policyEnforcement.test.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Additional Quarantine Tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=quarantineExtra.test.d.ts.map