@fulcrum-governance/sdk 0.1.1 → 0.1.3
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.
- package/CHANGELOG.md +42 -1
- package/README.md +299 -6
- package/dist/__tests__/clients.test.d.ts +1 -0
- package/dist/__tests__/clients.test.js +847 -0
- package/dist/clients/agent.d.ts +70 -0
- package/dist/clients/agent.js +127 -0
- package/dist/clients/approval.d.ts +67 -0
- package/dist/clients/approval.js +103 -0
- package/dist/clients/budget.d.ts +221 -0
- package/dist/clients/budget.js +181 -0
- package/dist/clients/checkpoint.d.ts +191 -0
- package/dist/clients/checkpoint.js +195 -0
- package/dist/clients/envelope.d.ts +73 -0
- package/dist/clients/envelope.js +95 -0
- package/dist/clients/eventstore.d.ts +87 -0
- package/dist/clients/eventstore.js +113 -0
- package/dist/clients/index.d.ts +15 -0
- package/dist/clients/index.js +35 -0
- package/dist/clients/metrics.d.ts +126 -0
- package/dist/clients/metrics.js +162 -0
- package/dist/clients/policy.d.ts +83 -0
- package/dist/clients/policy.js +102 -0
- package/dist/clients/tenant.d.ts +66 -0
- package/dist/clients/tenant.js +92 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -3
- package/dist/instrumentation/__tests__/autoGovern.test.d.ts +6 -0
- package/dist/instrumentation/__tests__/autoGovern.test.js +416 -0
- package/dist/instrumentation/__tests__/evaluator.test.d.ts +6 -0
- package/dist/instrumentation/__tests__/evaluator.test.js +712 -0
- package/dist/instrumentation/autoGovern.d.ts +57 -0
- package/dist/instrumentation/autoGovern.js +319 -0
- package/dist/instrumentation/evaluator.d.ts +50 -0
- package/dist/instrumentation/evaluator.js +218 -0
- package/dist/instrumentation/index.d.ts +28 -0
- package/dist/instrumentation/index.js +34 -0
- package/dist/instrumentation/types.d.ts +105 -0
- package/dist/instrumentation/types.js +20 -0
- package/package.json +4 -4
- package/proto/fulcrum/agent/v1/agent_service.proto +170 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Comprehensive unit tests for PolicyEvaluator.
|
|
4
|
+
* Tests cover: policy evaluation logic, condition matching, fail-safe behavior.
|
|
5
|
+
* Target: >90% coverage
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
const evaluator_1 = require("../evaluator");
|
|
9
|
+
describe('PolicyEvaluator', () => {
|
|
10
|
+
describe('initialization', () => {
|
|
11
|
+
it('should initialize with default config', () => {
|
|
12
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
13
|
+
expect(evaluator).toBeDefined();
|
|
14
|
+
expect(evaluator.policyCount).toBe(0);
|
|
15
|
+
});
|
|
16
|
+
it('should initialize with custom config', () => {
|
|
17
|
+
const config = {
|
|
18
|
+
apiKey: 'test-key',
|
|
19
|
+
tenantId: 'test-tenant',
|
|
20
|
+
policySyncInterval: 60,
|
|
21
|
+
};
|
|
22
|
+
const evaluator = new evaluator_1.PolicyEvaluator(config);
|
|
23
|
+
expect(evaluator).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
it('should not be running initially', () => {
|
|
26
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
27
|
+
expect(evaluator.running).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('start and stop', () => {
|
|
31
|
+
it('should start the evaluator', () => {
|
|
32
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
33
|
+
evaluator.start();
|
|
34
|
+
expect(evaluator.running).toBe(true);
|
|
35
|
+
expect(evaluator.syncInterval).not.toBeNull();
|
|
36
|
+
evaluator.stop();
|
|
37
|
+
});
|
|
38
|
+
it('should stop the evaluator', () => {
|
|
39
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
40
|
+
evaluator.start();
|
|
41
|
+
evaluator.stop();
|
|
42
|
+
expect(evaluator.running).toBe(false);
|
|
43
|
+
expect(evaluator.syncInterval).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
it('should be idempotent when starting multiple times', () => {
|
|
46
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
47
|
+
evaluator.start();
|
|
48
|
+
const interval1 = evaluator.syncInterval;
|
|
49
|
+
evaluator.start(); // Should be no-op
|
|
50
|
+
const interval2 = evaluator.syncInterval;
|
|
51
|
+
expect(interval1).toBe(interval2);
|
|
52
|
+
evaluator.stop();
|
|
53
|
+
});
|
|
54
|
+
it('should be idempotent when stopping multiple times', () => {
|
|
55
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
56
|
+
evaluator.start();
|
|
57
|
+
evaluator.stop();
|
|
58
|
+
// Should not throw
|
|
59
|
+
expect(() => evaluator.stop()).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('evaluate - no policies', () => {
|
|
63
|
+
it('should allow when no policies are loaded', () => {
|
|
64
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
65
|
+
const request = {
|
|
66
|
+
toolName: 'test_tool',
|
|
67
|
+
actionType: 'file_read',
|
|
68
|
+
resource: '/test/path',
|
|
69
|
+
toolArgs: {},
|
|
70
|
+
};
|
|
71
|
+
const decision = evaluator.evaluate(request);
|
|
72
|
+
expect(decision.action).toBe('allow');
|
|
73
|
+
expect(decision.reason).toContain('No policies loaded');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('evaluate - policy matching', () => {
|
|
77
|
+
it('should match policy and allow', () => {
|
|
78
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
79
|
+
const testPolicy = {
|
|
80
|
+
policyId: 'policy-1',
|
|
81
|
+
tenantId: 'test-tenant',
|
|
82
|
+
priority: 100,
|
|
83
|
+
status: 'active',
|
|
84
|
+
scope: { applyToAll: true },
|
|
85
|
+
rules: [
|
|
86
|
+
{
|
|
87
|
+
ruleId: 'rule-1',
|
|
88
|
+
name: 'Allow file reads',
|
|
89
|
+
enabled: true,
|
|
90
|
+
priority: 100,
|
|
91
|
+
conditions: [
|
|
92
|
+
{
|
|
93
|
+
field: 'action_type',
|
|
94
|
+
operator: 'equals',
|
|
95
|
+
value: 'file_read',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
actions: [{ actionType: 'allow' }],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
evaluator.updatePolicies([testPolicy]);
|
|
103
|
+
const request = {
|
|
104
|
+
toolName: 'fs.readFile',
|
|
105
|
+
actionType: 'file_read',
|
|
106
|
+
resource: '/test/path',
|
|
107
|
+
toolArgs: {},
|
|
108
|
+
};
|
|
109
|
+
const decision = evaluator.evaluate(request);
|
|
110
|
+
expect(decision.action).toBe('allow');
|
|
111
|
+
expect(decision.matchedPolicyId).toBe('policy-1');
|
|
112
|
+
expect(decision.matchedRuleId).toBe('rule-1');
|
|
113
|
+
});
|
|
114
|
+
it('should match policy and deny', () => {
|
|
115
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
116
|
+
const testPolicy = {
|
|
117
|
+
policyId: 'policy-2',
|
|
118
|
+
tenantId: 'test-tenant',
|
|
119
|
+
priority: 100,
|
|
120
|
+
status: 'active',
|
|
121
|
+
scope: { applyToAll: true },
|
|
122
|
+
rules: [
|
|
123
|
+
{
|
|
124
|
+
ruleId: 'rule-2',
|
|
125
|
+
name: 'Deny sensitive files',
|
|
126
|
+
enabled: true,
|
|
127
|
+
priority: 100,
|
|
128
|
+
conditions: [
|
|
129
|
+
{
|
|
130
|
+
field: 'resource',
|
|
131
|
+
operator: 'contains',
|
|
132
|
+
value: '/secret',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
actions: [{ actionType: 'deny' }],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
evaluator.updatePolicies([testPolicy]);
|
|
140
|
+
const request = {
|
|
141
|
+
toolName: 'fs.readFile',
|
|
142
|
+
actionType: 'file_read',
|
|
143
|
+
resource: '/etc/secret/credentials',
|
|
144
|
+
toolArgs: {},
|
|
145
|
+
};
|
|
146
|
+
const decision = evaluator.evaluate(request);
|
|
147
|
+
expect(decision.action).toBe('deny');
|
|
148
|
+
expect(decision.matchedPolicyId).toBe('policy-2');
|
|
149
|
+
});
|
|
150
|
+
it('should skip disabled rules', () => {
|
|
151
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
152
|
+
const testPolicy = {
|
|
153
|
+
policyId: 'policy-3',
|
|
154
|
+
tenantId: 'test-tenant',
|
|
155
|
+
priority: 100,
|
|
156
|
+
status: 'active',
|
|
157
|
+
scope: { applyToAll: true },
|
|
158
|
+
rules: [
|
|
159
|
+
{
|
|
160
|
+
ruleId: 'rule-disabled',
|
|
161
|
+
name: 'Disabled rule',
|
|
162
|
+
enabled: false, // Disabled
|
|
163
|
+
priority: 100,
|
|
164
|
+
conditions: [],
|
|
165
|
+
actions: [{ actionType: 'deny' }],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
evaluator.updatePolicies([testPolicy]);
|
|
170
|
+
const request = {
|
|
171
|
+
toolName: 'test_tool',
|
|
172
|
+
actionType: 'test_action',
|
|
173
|
+
toolArgs: {},
|
|
174
|
+
};
|
|
175
|
+
const decision = evaluator.evaluate(request);
|
|
176
|
+
// Should allow because rule is disabled
|
|
177
|
+
expect(decision.action).toBe('allow');
|
|
178
|
+
expect(decision.reason).toContain('No rules matched');
|
|
179
|
+
});
|
|
180
|
+
it('should skip inactive policies', () => {
|
|
181
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
182
|
+
const testPolicy = {
|
|
183
|
+
policyId: 'policy-4',
|
|
184
|
+
tenantId: 'test-tenant',
|
|
185
|
+
priority: 100,
|
|
186
|
+
status: 'inactive', // Inactive
|
|
187
|
+
scope: { applyToAll: true },
|
|
188
|
+
rules: [
|
|
189
|
+
{
|
|
190
|
+
ruleId: 'rule-1',
|
|
191
|
+
name: 'Some rule',
|
|
192
|
+
enabled: true,
|
|
193
|
+
priority: 100,
|
|
194
|
+
conditions: [],
|
|
195
|
+
actions: [{ actionType: 'deny' }],
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
evaluator.updatePolicies([testPolicy]);
|
|
200
|
+
const request = {
|
|
201
|
+
toolName: 'test_tool',
|
|
202
|
+
actionType: 'test_action',
|
|
203
|
+
toolArgs: {},
|
|
204
|
+
};
|
|
205
|
+
const decision = evaluator.evaluate(request);
|
|
206
|
+
expect(decision.action).toBe('allow');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe('evaluate - condition operators', () => {
|
|
210
|
+
const createPolicyWithCondition = (operator, value, values) => ({
|
|
211
|
+
policyId: `policy-${operator}`,
|
|
212
|
+
tenantId: 'test-tenant',
|
|
213
|
+
priority: 100,
|
|
214
|
+
status: 'active',
|
|
215
|
+
scope: { applyToAll: true },
|
|
216
|
+
rules: [
|
|
217
|
+
{
|
|
218
|
+
ruleId: `rule-${operator}`,
|
|
219
|
+
name: `Test ${operator}`,
|
|
220
|
+
enabled: true,
|
|
221
|
+
priority: 100,
|
|
222
|
+
conditions: [
|
|
223
|
+
{
|
|
224
|
+
field: 'resource',
|
|
225
|
+
operator,
|
|
226
|
+
value,
|
|
227
|
+
values,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
actions: [{ actionType: 'deny' }],
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
it('should match equals operator', () => {
|
|
235
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
236
|
+
evaluator.updatePolicies([createPolicyWithCondition('equals', 'test_value')]);
|
|
237
|
+
const request = {
|
|
238
|
+
toolName: 'test_tool',
|
|
239
|
+
actionType: 'test_action',
|
|
240
|
+
resource: 'test_value',
|
|
241
|
+
toolArgs: {},
|
|
242
|
+
};
|
|
243
|
+
const decision = evaluator.evaluate(request);
|
|
244
|
+
expect(decision.action).toBe('deny');
|
|
245
|
+
});
|
|
246
|
+
it('should not match equals operator with different value', () => {
|
|
247
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
248
|
+
evaluator.updatePolicies([createPolicyWithCondition('equals', 'test_value')]);
|
|
249
|
+
const request = {
|
|
250
|
+
toolName: 'test_tool',
|
|
251
|
+
actionType: 'test_action',
|
|
252
|
+
resource: 'other_value',
|
|
253
|
+
toolArgs: {},
|
|
254
|
+
};
|
|
255
|
+
const decision = evaluator.evaluate(request);
|
|
256
|
+
expect(decision.action).toBe('allow');
|
|
257
|
+
});
|
|
258
|
+
it('should match not_equals operator', () => {
|
|
259
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
260
|
+
evaluator.updatePolicies([createPolicyWithCondition('not_equals', 'test_value')]);
|
|
261
|
+
const request = {
|
|
262
|
+
toolName: 'test_tool',
|
|
263
|
+
actionType: 'test_action',
|
|
264
|
+
resource: 'other_value',
|
|
265
|
+
toolArgs: {},
|
|
266
|
+
};
|
|
267
|
+
const decision = evaluator.evaluate(request);
|
|
268
|
+
expect(decision.action).toBe('deny');
|
|
269
|
+
});
|
|
270
|
+
it('should match contains operator', () => {
|
|
271
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
272
|
+
evaluator.updatePolicies([createPolicyWithCondition('contains', 'world')]);
|
|
273
|
+
const request = {
|
|
274
|
+
toolName: 'test_tool',
|
|
275
|
+
actionType: 'test_action',
|
|
276
|
+
resource: 'hello world',
|
|
277
|
+
toolArgs: {},
|
|
278
|
+
};
|
|
279
|
+
const decision = evaluator.evaluate(request);
|
|
280
|
+
expect(decision.action).toBe('deny');
|
|
281
|
+
});
|
|
282
|
+
it('should match starts_with operator', () => {
|
|
283
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
284
|
+
evaluator.updatePolicies([createPolicyWithCondition('starts_with', 'hello')]);
|
|
285
|
+
const request = {
|
|
286
|
+
toolName: 'test_tool',
|
|
287
|
+
actionType: 'test_action',
|
|
288
|
+
resource: 'hello world',
|
|
289
|
+
toolArgs: {},
|
|
290
|
+
};
|
|
291
|
+
const decision = evaluator.evaluate(request);
|
|
292
|
+
expect(decision.action).toBe('deny');
|
|
293
|
+
});
|
|
294
|
+
it('should match ends_with operator', () => {
|
|
295
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
296
|
+
evaluator.updatePolicies([createPolicyWithCondition('ends_with', 'world')]);
|
|
297
|
+
const request = {
|
|
298
|
+
toolName: 'test_tool',
|
|
299
|
+
actionType: 'test_action',
|
|
300
|
+
resource: 'hello world',
|
|
301
|
+
toolArgs: {},
|
|
302
|
+
};
|
|
303
|
+
const decision = evaluator.evaluate(request);
|
|
304
|
+
expect(decision.action).toBe('deny');
|
|
305
|
+
});
|
|
306
|
+
it('should match in operator', () => {
|
|
307
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
308
|
+
evaluator.updatePolicies([createPolicyWithCondition('in', undefined, ['value1', 'value2'])]);
|
|
309
|
+
const request = {
|
|
310
|
+
toolName: 'test_tool',
|
|
311
|
+
actionType: 'test_action',
|
|
312
|
+
resource: 'value1',
|
|
313
|
+
toolArgs: {},
|
|
314
|
+
};
|
|
315
|
+
const decision = evaluator.evaluate(request);
|
|
316
|
+
expect(decision.action).toBe('deny');
|
|
317
|
+
});
|
|
318
|
+
it('should match not_in operator', () => {
|
|
319
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
320
|
+
evaluator.updatePolicies([createPolicyWithCondition('not_in', undefined, ['value1', 'value2'])]);
|
|
321
|
+
const request = {
|
|
322
|
+
toolName: 'test_tool',
|
|
323
|
+
actionType: 'test_action',
|
|
324
|
+
resource: 'value3',
|
|
325
|
+
toolArgs: {},
|
|
326
|
+
};
|
|
327
|
+
const decision = evaluator.evaluate(request);
|
|
328
|
+
expect(decision.action).toBe('deny');
|
|
329
|
+
});
|
|
330
|
+
it('should match matches (regex) operator', () => {
|
|
331
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
332
|
+
evaluator.updatePolicies([createPolicyWithCondition('matches', 'test\\d+')]);
|
|
333
|
+
const request = {
|
|
334
|
+
toolName: 'test_tool',
|
|
335
|
+
actionType: 'test_action',
|
|
336
|
+
resource: 'test123',
|
|
337
|
+
toolArgs: {},
|
|
338
|
+
};
|
|
339
|
+
const decision = evaluator.evaluate(request);
|
|
340
|
+
expect(decision.action).toBe('deny');
|
|
341
|
+
});
|
|
342
|
+
it('should handle invalid regex gracefully', () => {
|
|
343
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
344
|
+
evaluator.updatePolicies([createPolicyWithCondition('matches', '[invalid(regex')]);
|
|
345
|
+
const request = {
|
|
346
|
+
toolName: 'test_tool',
|
|
347
|
+
actionType: 'test_action',
|
|
348
|
+
resource: 'test',
|
|
349
|
+
toolArgs: {},
|
|
350
|
+
};
|
|
351
|
+
const decision = evaluator.evaluate(request);
|
|
352
|
+
// Invalid regex should not match
|
|
353
|
+
expect(decision.action).toBe('allow');
|
|
354
|
+
});
|
|
355
|
+
it('should warn on unknown operator', () => {
|
|
356
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
357
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
358
|
+
const policy = {
|
|
359
|
+
policyId: 'policy-unknown',
|
|
360
|
+
tenantId: 'test-tenant',
|
|
361
|
+
priority: 100,
|
|
362
|
+
status: 'active',
|
|
363
|
+
scope: { applyToAll: true },
|
|
364
|
+
rules: [
|
|
365
|
+
{
|
|
366
|
+
ruleId: 'rule-unknown',
|
|
367
|
+
name: 'Unknown operator',
|
|
368
|
+
enabled: true,
|
|
369
|
+
priority: 100,
|
|
370
|
+
conditions: [
|
|
371
|
+
{
|
|
372
|
+
field: 'resource',
|
|
373
|
+
operator: 'unknown_operator',
|
|
374
|
+
value: 'test',
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
actions: [{ actionType: 'deny' }],
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
evaluator.updatePolicies([policy]);
|
|
382
|
+
const request = {
|
|
383
|
+
toolName: 'test_tool',
|
|
384
|
+
actionType: 'test_action',
|
|
385
|
+
resource: 'test',
|
|
386
|
+
toolArgs: {},
|
|
387
|
+
};
|
|
388
|
+
evaluator.evaluate(request);
|
|
389
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown operator'));
|
|
390
|
+
consoleSpy.mockRestore();
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
describe('evaluate - field extraction', () => {
|
|
394
|
+
it('should extract tool_name field', () => {
|
|
395
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
396
|
+
const policy = {
|
|
397
|
+
policyId: 'policy-1',
|
|
398
|
+
tenantId: 'test-tenant',
|
|
399
|
+
priority: 100,
|
|
400
|
+
status: 'active',
|
|
401
|
+
scope: { applyToAll: true },
|
|
402
|
+
rules: [
|
|
403
|
+
{
|
|
404
|
+
ruleId: 'rule-1',
|
|
405
|
+
name: 'Match tool name',
|
|
406
|
+
enabled: true,
|
|
407
|
+
priority: 100,
|
|
408
|
+
conditions: [
|
|
409
|
+
{
|
|
410
|
+
field: 'tool_name',
|
|
411
|
+
operator: 'equals',
|
|
412
|
+
value: 'fs.readFile',
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
actions: [{ actionType: 'deny' }],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
evaluator.updatePolicies([policy]);
|
|
420
|
+
const request = {
|
|
421
|
+
toolName: 'fs.readFile',
|
|
422
|
+
actionType: 'file_read',
|
|
423
|
+
toolArgs: {},
|
|
424
|
+
};
|
|
425
|
+
const decision = evaluator.evaluate(request);
|
|
426
|
+
expect(decision.action).toBe('deny');
|
|
427
|
+
});
|
|
428
|
+
it('should extract action_type field', () => {
|
|
429
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
430
|
+
const policy = {
|
|
431
|
+
policyId: 'policy-1',
|
|
432
|
+
tenantId: 'test-tenant',
|
|
433
|
+
priority: 100,
|
|
434
|
+
status: 'active',
|
|
435
|
+
scope: { applyToAll: true },
|
|
436
|
+
rules: [
|
|
437
|
+
{
|
|
438
|
+
ruleId: 'rule-1',
|
|
439
|
+
name: 'Match action type',
|
|
440
|
+
enabled: true,
|
|
441
|
+
priority: 100,
|
|
442
|
+
conditions: [
|
|
443
|
+
{
|
|
444
|
+
field: 'action_type',
|
|
445
|
+
operator: 'equals',
|
|
446
|
+
value: 'file_write',
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
actions: [{ actionType: 'deny' }],
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
evaluator.updatePolicies([policy]);
|
|
454
|
+
const request = {
|
|
455
|
+
toolName: 'fs.writeFile',
|
|
456
|
+
actionType: 'file_write',
|
|
457
|
+
toolArgs: {},
|
|
458
|
+
};
|
|
459
|
+
const decision = evaluator.evaluate(request);
|
|
460
|
+
expect(decision.action).toBe('deny');
|
|
461
|
+
});
|
|
462
|
+
it('should extract args.* fields', () => {
|
|
463
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
464
|
+
const policy = {
|
|
465
|
+
policyId: 'policy-1',
|
|
466
|
+
tenantId: 'test-tenant',
|
|
467
|
+
priority: 100,
|
|
468
|
+
status: 'active',
|
|
469
|
+
scope: { applyToAll: true },
|
|
470
|
+
rules: [
|
|
471
|
+
{
|
|
472
|
+
ruleId: 'rule-1',
|
|
473
|
+
name: 'Match arg',
|
|
474
|
+
enabled: true,
|
|
475
|
+
priority: 100,
|
|
476
|
+
conditions: [
|
|
477
|
+
{
|
|
478
|
+
field: 'args.path',
|
|
479
|
+
operator: 'equals',
|
|
480
|
+
value: '/sensitive/path',
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
actions: [{ actionType: 'deny' }],
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
};
|
|
487
|
+
evaluator.updatePolicies([policy]);
|
|
488
|
+
const request = {
|
|
489
|
+
toolName: 'fs.readFile',
|
|
490
|
+
actionType: 'file_read',
|
|
491
|
+
toolArgs: { path: '/sensitive/path' },
|
|
492
|
+
};
|
|
493
|
+
const decision = evaluator.evaluate(request);
|
|
494
|
+
expect(decision.action).toBe('deny');
|
|
495
|
+
});
|
|
496
|
+
it('should extract metadata.* fields', () => {
|
|
497
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
498
|
+
const policy = {
|
|
499
|
+
policyId: 'policy-1',
|
|
500
|
+
tenantId: 'test-tenant',
|
|
501
|
+
priority: 100,
|
|
502
|
+
status: 'active',
|
|
503
|
+
scope: { applyToAll: true },
|
|
504
|
+
rules: [
|
|
505
|
+
{
|
|
506
|
+
ruleId: 'rule-1',
|
|
507
|
+
name: 'Match metadata',
|
|
508
|
+
enabled: true,
|
|
509
|
+
priority: 100,
|
|
510
|
+
conditions: [
|
|
511
|
+
{
|
|
512
|
+
field: 'metadata.environment',
|
|
513
|
+
operator: 'equals',
|
|
514
|
+
value: 'production',
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
actions: [{ actionType: 'deny' }],
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
};
|
|
521
|
+
evaluator.updatePolicies([policy]);
|
|
522
|
+
const request = {
|
|
523
|
+
toolName: 'test_tool',
|
|
524
|
+
actionType: 'test_action',
|
|
525
|
+
toolArgs: {},
|
|
526
|
+
metadata: { environment: 'production' },
|
|
527
|
+
};
|
|
528
|
+
const decision = evaluator.evaluate(request);
|
|
529
|
+
expect(decision.action).toBe('deny');
|
|
530
|
+
});
|
|
531
|
+
it('should return undefined for unknown fields', () => {
|
|
532
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
533
|
+
const policy = {
|
|
534
|
+
policyId: 'policy-1',
|
|
535
|
+
tenantId: 'test-tenant',
|
|
536
|
+
priority: 100,
|
|
537
|
+
status: 'active',
|
|
538
|
+
scope: { applyToAll: true },
|
|
539
|
+
rules: [
|
|
540
|
+
{
|
|
541
|
+
ruleId: 'rule-1',
|
|
542
|
+
name: 'Unknown field',
|
|
543
|
+
enabled: true,
|
|
544
|
+
priority: 100,
|
|
545
|
+
conditions: [
|
|
546
|
+
{
|
|
547
|
+
field: 'unknown_field',
|
|
548
|
+
operator: 'equals',
|
|
549
|
+
value: 'test',
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
actions: [{ actionType: 'deny' }],
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
};
|
|
556
|
+
evaluator.updatePolicies([policy]);
|
|
557
|
+
const request = {
|
|
558
|
+
toolName: 'test_tool',
|
|
559
|
+
actionType: 'test_action',
|
|
560
|
+
toolArgs: {},
|
|
561
|
+
};
|
|
562
|
+
const decision = evaluator.evaluate(request);
|
|
563
|
+
// Unknown field should not match
|
|
564
|
+
expect(decision.action).toBe('allow');
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
describe('evaluate - policy scope', () => {
|
|
568
|
+
it('should apply policy when applyToAll is true', () => {
|
|
569
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
570
|
+
const policy = {
|
|
571
|
+
policyId: 'policy-1',
|
|
572
|
+
tenantId: 'test-tenant',
|
|
573
|
+
priority: 100,
|
|
574
|
+
status: 'active',
|
|
575
|
+
scope: { applyToAll: true },
|
|
576
|
+
rules: [
|
|
577
|
+
{
|
|
578
|
+
ruleId: 'rule-1',
|
|
579
|
+
name: 'Deny all',
|
|
580
|
+
enabled: true,
|
|
581
|
+
priority: 100,
|
|
582
|
+
conditions: [],
|
|
583
|
+
actions: [{ actionType: 'deny' }],
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
};
|
|
587
|
+
evaluator.updatePolicies([policy]);
|
|
588
|
+
const request = {
|
|
589
|
+
toolName: 'any_tool',
|
|
590
|
+
actionType: 'any_action',
|
|
591
|
+
toolArgs: {},
|
|
592
|
+
};
|
|
593
|
+
const decision = evaluator.evaluate(request);
|
|
594
|
+
expect(decision.action).toBe('deny');
|
|
595
|
+
});
|
|
596
|
+
it('should filter by tool names', () => {
|
|
597
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
598
|
+
const policy = {
|
|
599
|
+
policyId: 'policy-1',
|
|
600
|
+
tenantId: 'test-tenant',
|
|
601
|
+
priority: 100,
|
|
602
|
+
status: 'active',
|
|
603
|
+
scope: {
|
|
604
|
+
applyToAll: false,
|
|
605
|
+
toolNames: ['fs.readFile', 'fs.writeFile'],
|
|
606
|
+
},
|
|
607
|
+
rules: [
|
|
608
|
+
{
|
|
609
|
+
ruleId: 'rule-1',
|
|
610
|
+
name: 'Deny file operations',
|
|
611
|
+
enabled: true,
|
|
612
|
+
priority: 100,
|
|
613
|
+
conditions: [],
|
|
614
|
+
actions: [{ actionType: 'deny' }],
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
};
|
|
618
|
+
evaluator.updatePolicies([policy]);
|
|
619
|
+
// Should match for allowed tool
|
|
620
|
+
const request1 = {
|
|
621
|
+
toolName: 'fs.readFile',
|
|
622
|
+
actionType: 'file_read',
|
|
623
|
+
toolArgs: {},
|
|
624
|
+
};
|
|
625
|
+
expect(evaluator.evaluate(request1).action).toBe('deny');
|
|
626
|
+
// Should not match for other tool
|
|
627
|
+
const request2 = {
|
|
628
|
+
toolName: 'fetch',
|
|
629
|
+
actionType: 'http_request',
|
|
630
|
+
toolArgs: {},
|
|
631
|
+
};
|
|
632
|
+
expect(evaluator.evaluate(request2).action).toBe('allow');
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
describe('evaluate - fail-safe behavior', () => {
|
|
636
|
+
it('should fail open on evaluation error', () => {
|
|
637
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
638
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
639
|
+
// Mock evaluateInternal to throw an error
|
|
640
|
+
jest.spyOn(evaluator, 'evaluateInternal').mockImplementation(() => {
|
|
641
|
+
throw new Error('Simulated evaluation error');
|
|
642
|
+
});
|
|
643
|
+
const request = {
|
|
644
|
+
toolName: 'test_tool',
|
|
645
|
+
actionType: 'test_action',
|
|
646
|
+
toolArgs: {},
|
|
647
|
+
};
|
|
648
|
+
const decision = evaluator.evaluate(request);
|
|
649
|
+
// Should allow (fail open)
|
|
650
|
+
expect(decision.action).toBe('allow');
|
|
651
|
+
expect(decision.reason).toContain('Evaluation error (fail-open)');
|
|
652
|
+
// Should have logged warning
|
|
653
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Policy evaluation error'));
|
|
654
|
+
consoleSpy.mockRestore();
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
describe('updatePolicies', () => {
|
|
658
|
+
it('should update policy cache', () => {
|
|
659
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
660
|
+
expect(evaluator.policyCount).toBe(0);
|
|
661
|
+
const testPolicies = [
|
|
662
|
+
{
|
|
663
|
+
policyId: 'policy-1',
|
|
664
|
+
tenantId: 'test-tenant',
|
|
665
|
+
priority: 100,
|
|
666
|
+
status: 'active',
|
|
667
|
+
rules: [],
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
policyId: 'policy-2',
|
|
671
|
+
tenantId: 'test-tenant',
|
|
672
|
+
priority: 90,
|
|
673
|
+
status: 'active',
|
|
674
|
+
rules: [],
|
|
675
|
+
},
|
|
676
|
+
];
|
|
677
|
+
evaluator.updatePolicies(testPolicies);
|
|
678
|
+
expect(evaluator.policyCount).toBe(2);
|
|
679
|
+
});
|
|
680
|
+
it('should update last sync time', () => {
|
|
681
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
682
|
+
const before = evaluator.lastSyncTime;
|
|
683
|
+
evaluator.updatePolicies([]);
|
|
684
|
+
const after = evaluator.lastSyncTime;
|
|
685
|
+
expect(after).toBeGreaterThan(before);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
describe('properties', () => {
|
|
689
|
+
it('should return policy count', () => {
|
|
690
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
691
|
+
evaluator.updatePolicies([
|
|
692
|
+
{
|
|
693
|
+
policyId: 'policy-1',
|
|
694
|
+
tenantId: 'test-tenant',
|
|
695
|
+
priority: 100,
|
|
696
|
+
status: 'active',
|
|
697
|
+
rules: [],
|
|
698
|
+
},
|
|
699
|
+
]);
|
|
700
|
+
expect(evaluator.policyCount).toBe(1);
|
|
701
|
+
});
|
|
702
|
+
it('should return last sync time', () => {
|
|
703
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
704
|
+
const time = evaluator.lastSyncTime;
|
|
705
|
+
expect(time).toBeGreaterThanOrEqual(0);
|
|
706
|
+
});
|
|
707
|
+
it('should return sync error', () => {
|
|
708
|
+
const evaluator = new evaluator_1.PolicyEvaluator();
|
|
709
|
+
expect(evaluator.syncErrorMessage).toBeNull();
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
});
|