@highflame/policy 2.0.8 → 2.0.9

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 (126) hide show
  1. package/dist/actions.gen.d.ts +0 -1
  2. package/dist/actions.gen.js +0 -1
  3. package/dist/annotations.d.ts +0 -1
  4. package/dist/annotations.js +0 -1
  5. package/dist/builder.d.ts +0 -1
  6. package/dist/builder.js +0 -1
  7. package/dist/context.gen.d.ts +0 -1
  8. package/dist/context.gen.js +0 -1
  9. package/dist/engine.d.ts +0 -1
  10. package/dist/engine.js +0 -1
  11. package/dist/entities.gen.d.ts +0 -1
  12. package/dist/entities.gen.js +0 -1
  13. package/dist/entity-metadata-types.gen.d.ts +0 -1
  14. package/dist/entity-metadata-types.gen.js +0 -1
  15. package/dist/errors.d.ts +0 -1
  16. package/dist/errors.js +0 -1
  17. package/dist/index.d.ts +0 -1
  18. package/dist/index.js +0 -1
  19. package/dist/overwatch-context.gen.d.ts +0 -1
  20. package/dist/overwatch-context.gen.js +0 -1
  21. package/dist/overwatch-defaults.gen.d.ts +0 -1
  22. package/dist/overwatch-defaults.gen.js +0 -1
  23. package/dist/overwatch-entities.gen.d.ts +0 -1
  24. package/dist/overwatch-entities.gen.js +0 -1
  25. package/dist/palisade-context.gen.d.ts +0 -1
  26. package/dist/palisade-context.gen.js +0 -1
  27. package/dist/palisade-entities.gen.d.ts +0 -1
  28. package/dist/palisade-entities.gen.js +0 -1
  29. package/dist/parser.d.ts +0 -1
  30. package/dist/parser.js +0 -1
  31. package/dist/schema.gen.d.ts +0 -1
  32. package/dist/schema.gen.js +0 -1
  33. package/dist/schemas.d.ts +0 -1
  34. package/dist/schemas.js +0 -1
  35. package/dist/service-schemas.gen.d.ts +0 -1
  36. package/dist/service-schemas.gen.js +0 -1
  37. package/dist/types.d.ts +0 -1
  38. package/dist/types.js +0 -1
  39. package/package.json +1 -2
  40. package/dist/actions.gen.d.ts.map +0 -1
  41. package/dist/actions.gen.js.map +0 -1
  42. package/dist/annotations.d.ts.map +0 -1
  43. package/dist/annotations.js.map +0 -1
  44. package/dist/builder.d.ts.map +0 -1
  45. package/dist/builder.js.map +0 -1
  46. package/dist/context.gen.d.ts.map +0 -1
  47. package/dist/context.gen.js.map +0 -1
  48. package/dist/engine.d.ts.map +0 -1
  49. package/dist/engine.js.map +0 -1
  50. package/dist/engine.test.d.ts +0 -8
  51. package/dist/engine.test.d.ts.map +0 -1
  52. package/dist/engine.test.js +0 -190
  53. package/dist/engine.test.js.map +0 -1
  54. package/dist/entities.gen.d.ts.map +0 -1
  55. package/dist/entities.gen.js.map +0 -1
  56. package/dist/entity-metadata-types.gen.d.ts.map +0 -1
  57. package/dist/entity-metadata-types.gen.js.map +0 -1
  58. package/dist/errors.d.ts.map +0 -1
  59. package/dist/errors.js.map +0 -1
  60. package/dist/index.d.ts.map +0 -1
  61. package/dist/index.js.map +0 -1
  62. package/dist/overwatch-context.gen.d.ts.map +0 -1
  63. package/dist/overwatch-context.gen.js.map +0 -1
  64. package/dist/overwatch-defaults.gen.d.ts.map +0 -1
  65. package/dist/overwatch-defaults.gen.js.map +0 -1
  66. package/dist/overwatch-defaults.test.d.ts +0 -8
  67. package/dist/overwatch-defaults.test.d.ts.map +0 -1
  68. package/dist/overwatch-defaults.test.js +0 -145
  69. package/dist/overwatch-defaults.test.js.map +0 -1
  70. package/dist/overwatch-entities.gen.d.ts.map +0 -1
  71. package/dist/overwatch-entities.gen.js.map +0 -1
  72. package/dist/overwatch-rebac.test.d.ts +0 -25
  73. package/dist/overwatch-rebac.test.d.ts.map +0 -1
  74. package/dist/overwatch-rebac.test.js +0 -301
  75. package/dist/overwatch-rebac.test.js.map +0 -1
  76. package/dist/palisade-context.gen.d.ts.map +0 -1
  77. package/dist/palisade-context.gen.js.map +0 -1
  78. package/dist/palisade-entities.gen.d.ts.map +0 -1
  79. package/dist/palisade-entities.gen.js.map +0 -1
  80. package/dist/parser.d.ts.map +0 -1
  81. package/dist/parser.js.map +0 -1
  82. package/dist/parser.test.d.ts +0 -8
  83. package/dist/parser.test.d.ts.map +0 -1
  84. package/dist/parser.test.js +0 -212
  85. package/dist/parser.test.js.map +0 -1
  86. package/dist/schema.gen.d.ts.map +0 -1
  87. package/dist/schema.gen.js.map +0 -1
  88. package/dist/schemas.d.ts.map +0 -1
  89. package/dist/schemas.js.map +0 -1
  90. package/dist/schemas.test.d.ts +0 -8
  91. package/dist/schemas.test.d.ts.map +0 -1
  92. package/dist/schemas.test.js +0 -407
  93. package/dist/schemas.test.js.map +0 -1
  94. package/dist/service-schemas.gen.d.ts.map +0 -1
  95. package/dist/service-schemas.gen.js.map +0 -1
  96. package/dist/studio-ui.test.d.ts +0 -8
  97. package/dist/studio-ui.test.d.ts.map +0 -1
  98. package/dist/studio-ui.test.js +0 -687
  99. package/dist/studio-ui.test.js.map +0 -1
  100. package/dist/types.d.ts.map +0 -1
  101. package/dist/types.js.map +0 -1
  102. package/src/actions.gen.ts +0 -57
  103. package/src/annotations.ts +0 -243
  104. package/src/builder.ts +0 -799
  105. package/src/context.gen.ts +0 -10
  106. package/src/engine.test.ts +0 -370
  107. package/src/engine.ts +0 -497
  108. package/src/entities.gen.ts +0 -65
  109. package/src/entity-metadata-types.gen.ts +0 -19
  110. package/src/errors.ts +0 -195
  111. package/src/index.ts +0 -62
  112. package/src/overwatch-context.gen.ts +0 -45
  113. package/src/overwatch-defaults.gen.ts +0 -1255
  114. package/src/overwatch-defaults.test.ts +0 -176
  115. package/src/overwatch-entities.gen.ts +0 -41
  116. package/src/overwatch-rebac.test.ts +0 -346
  117. package/src/palisade-context.gen.ts +0 -28
  118. package/src/palisade-entities.gen.ts +0 -49
  119. package/src/parser.test.ts +0 -251
  120. package/src/parser.ts +0 -579
  121. package/src/schema.gen.ts +0 -134
  122. package/src/schemas.test.ts +0 -477
  123. package/src/schemas.ts +0 -91
  124. package/src/service-schemas.gen.ts +0 -608
  125. package/src/studio-ui.test.ts +0 -813
  126. package/src/types.ts +0 -66
@@ -1,687 +0,0 @@
1
- /**
2
- * Studio UI Integration Tests
3
- *
4
- * These tests simulate exactly how the Studio UI (Overwatch admin dashboard)
5
- * will use the @highflame/policy npm package.
6
- */
7
- import { describe, it, expect } from 'vitest';
8
- // Browser-safe imports (simulating '@highflame/policy/types')
9
- import { PolicyBuilder, OVERWATCH_SCHEMA, PALISADE_SCHEMA, OVERWATCH_CONTEXT, OverwatchContextKey, PalisadeContextKey,
10
- // Entity metadata for UI dropdowns
11
- OVERWATCH_ENTITIES, OVERWATCH_ACTION_ENTITIES, PALISADE_ENTITIES, PALISADE_ACTION_ENTITIES, } from './types.js';
12
- // Node.js only imports (for API routes)
13
- import { PolicyEngine, PolicyValidator, newEntityUID, newEntity, } from './index.js';
14
- describe('Studio UI Integration Tests', () => {
15
- /**
16
- * Test: PolicyBuilder action formatting
17
- *
18
- * Tests that simple action names are properly formatted to Cedar syntax.
19
- * This is the "normal approach" for non-namespaced schemas.
20
- */
21
- it('should format simple action names to Cedar syntax', () => {
22
- // Using simple action name (normal approach)
23
- const policy = PolicyBuilder.permit()
24
- .principalType('User')
25
- .action('call_tool')
26
- .resourceType('Tool')
27
- .build();
28
- const cedarText = policy.toCedar();
29
- // Simple actions should be wrapped as Action::"..."
30
- expect(cedarText).toContain('action == Action::"call_tool"');
31
- expect(cedarText).not.toContain('Action::"Action::'); // No double-wrapping
32
- });
33
- /**
34
- * Test 1: Schema and Context Loading
35
- *
36
- * Studio UI needs to load schemas for validation and context metadata
37
- * for populating form dropdowns.
38
- */
39
- it('should load schemas and context metadata for form builders', () => {
40
- // Schemas should be strings
41
- expect(typeof OVERWATCH_SCHEMA).toBe('string');
42
- expect(OVERWATCH_SCHEMA).toContain('namespace Overwatch');
43
- expect(typeof PALISADE_SCHEMA).toBe('string');
44
- expect(PALISADE_SCHEMA).toContain('namespace Palisade');
45
- // Context metadata should have action definitions
46
- expect(OVERWATCH_CONTEXT.service).toBe('overwatch');
47
- expect(OVERWATCH_CONTEXT.actions.length).toBeGreaterThan(0);
48
- // Find call_tool action and verify context attributes
49
- const callToolAction = OVERWATCH_CONTEXT.actions.find((a) => a.name === 'call_tool');
50
- expect(callToolAction).toBeDefined();
51
- expect(callToolAction.context_attributes.length).toBeGreaterThan(0);
52
- // Context keys should be available for type-safe form building
53
- expect(OverwatchContextKey.ToolName).toBe('tool_name');
54
- expect(OverwatchContextKey.ThreatCount).toBe('threat_count');
55
- expect(PalisadeContextKey.Severity).toBe('severity');
56
- expect(PalisadeContextKey.Environment).toBe('environment');
57
- });
58
- /**
59
- * Test 2: Full Round-Trip - Create Policy → Validate → Evaluate
60
- *
61
- * This simulates the complete flow:
62
- * 1. User creates a policy using PolicyBuilder in the UI
63
- * 2. API validates the policy against the schema
64
- * 3. Runtime evaluates the policy
65
- */
66
- it('should complete full policy lifecycle for Overwatch', () => {
67
- // Step 1: User creates policy in form UI
68
- const policy = PolicyBuilder.permit()
69
- .principalType('Overwatch::User')
70
- .action('Overwatch::Action::"call_tool"')
71
- .resourceType('Overwatch::Tool')
72
- .whenRaw('context.threat_count < 5')
73
- .build();
74
- const cedarText = policy.toCedar();
75
- expect(cedarText).toContain('permit');
76
- expect(cedarText).toContain('Overwatch::User');
77
- // Step 2: API validates policy against schema
78
- const validator = new PolicyValidator(OVERWATCH_SCHEMA);
79
- const validationResult = validator.validate(cedarText);
80
- expect(validationResult.valid).toBe(true);
81
- // Step 3: Runtime loads and evaluates policy
82
- const engine = new PolicyEngine({ schema: OVERWATCH_SCHEMA });
83
- engine.loadPolicy(cedarText);
84
- const entities = [
85
- newEntity('Overwatch::User', 'mcp_client', { user_type: 'external', email: 'test@example.com' }),
86
- newEntity('Overwatch::Tool', 'shell', { tool_name: 'shell', risk_level: 'high' }),
87
- ];
88
- // Full context as Guardian will provide at runtime
89
- const baseContext = {
90
- content: 'ls -la',
91
- source: 'claudecode',
92
- event: 'PreToolUse',
93
- user_email: 'test@example.com',
94
- tool_name: 'shell',
95
- mcp_server: 'filesystem',
96
- mcp_tool: 'shell',
97
- path: '/workspace',
98
- cwd: '/workspace',
99
- workspace_root: '/workspace',
100
- highest_severity: 'low',
101
- threat_categories: [],
102
- yara_threats: [],
103
- max_threat_severity: 1,
104
- contains_secrets: false,
105
- response_content: '',
106
- };
107
- // Should allow when threat_count < 5
108
- const allowDecision = engine.evaluate({
109
- principal: newEntityUID('Overwatch::User', 'mcp_client'),
110
- action: 'Overwatch::Action::"call_tool"',
111
- resource: newEntityUID('Overwatch::Tool', 'shell'),
112
- context: { ...baseContext, threat_count: 3 },
113
- entities,
114
- });
115
- expect(allowDecision.effect).toBe('Allow');
116
- // Should deny when threat_count >= 5 (no matching permit)
117
- const denyDecision = engine.evaluate({
118
- principal: newEntityUID('Overwatch::User', 'mcp_client'),
119
- action: 'Overwatch::Action::"call_tool"',
120
- resource: newEntityUID('Overwatch::Tool', 'shell'),
121
- context: { ...baseContext, threat_count: 10 },
122
- entities,
123
- });
124
- expect(denyDecision.effect).toBe('Deny');
125
- });
126
- /**
127
- * Test 3: Full Round-Trip for Palisade
128
- *
129
- * Same flow but for Palisade ML security policies.
130
- */
131
- it('should complete full policy lifecycle for Palisade', () => {
132
- // Step 1: Create a forbid policy for CRITICAL findings
133
- const policy = PolicyBuilder.forbid()
134
- .principalType('Palisade::Scanner')
135
- .action('Palisade::Action::"load_model"')
136
- .resourceType('Palisade::Artifact')
137
- .whenRaw('context.severity == "CRITICAL"')
138
- .build();
139
- const cedarText = policy.toCedar();
140
- // Step 2: Validate
141
- const validator = new PolicyValidator(PALISADE_SCHEMA);
142
- expect(validator.validate(cedarText).valid).toBe(true);
143
- // Step 3: Evaluate
144
- const engine = new PolicyEngine({ schema: PALISADE_SCHEMA });
145
- engine.loadPolicy(cedarText);
146
- const entities = [
147
- newEntity('Palisade::Scanner', 'palisade', { scanner_type: 'ml' }),
148
- newEntity('Palisade::Artifact', 'model.pkl', {
149
- artifact_format: 'pickle',
150
- path: '/model.pkl',
151
- signed: false,
152
- signer: 'unsigned',
153
- }),
154
- ];
155
- // Should deny CRITICAL findings
156
- const decision = engine.evaluate({
157
- principal: newEntityUID('Palisade::Scanner', 'palisade'),
158
- action: 'Palisade::Action::"load_model"',
159
- resource: newEntityUID('Palisade::Artifact', 'model.pkl'),
160
- context: { severity: 'CRITICAL' },
161
- entities,
162
- });
163
- expect(decision.effect).toBe('Deny');
164
- });
165
- /**
166
- * Test 4: Entity Metadata for UI Dropdowns - Overwatch
167
- *
168
- * Studio UI needs to know which entity types can be principals,
169
- * which can be resources, and what actions are available.
170
- * This data is extracted from Cedar schema appliesTo blocks.
171
- */
172
- it('should provide correct entity metadata for Overwatch UI dropdowns', () => {
173
- // Verify ServiceEntityMetadata structure
174
- expect(OVERWATCH_ENTITIES.principals).toBeDefined();
175
- expect(OVERWATCH_ENTITIES.resources).toBeDefined();
176
- expect(OVERWATCH_ENTITIES.actions).toBeDefined();
177
- // Overwatch principals should include User and Agent
178
- expect(OVERWATCH_ENTITIES.principals).toContain('Agent');
179
- expect(OVERWATCH_ENTITIES.principals).toContain('User');
180
- expect(OVERWATCH_ENTITIES.principals).toHaveLength(2);
181
- // Overwatch resources should include all resource types
182
- expect(OVERWATCH_ENTITIES.resources).toContain('FilePath');
183
- expect(OVERWATCH_ENTITIES.resources).toContain('LlmPrompt');
184
- expect(OVERWATCH_ENTITIES.resources).toContain('Server');
185
- expect(OVERWATCH_ENTITIES.resources).toContain('Tool');
186
- expect(OVERWATCH_ENTITIES.resources).toHaveLength(4);
187
- // Overwatch actions should match schema
188
- expect(OVERWATCH_ENTITIES.actions).toContain('call_tool');
189
- expect(OVERWATCH_ENTITIES.actions).toContain('connect_server');
190
- expect(OVERWATCH_ENTITIES.actions).toContain('process_prompt');
191
- expect(OVERWATCH_ENTITIES.actions).toContain('read_file');
192
- expect(OVERWATCH_ENTITIES.actions).toContain('write_file');
193
- expect(OVERWATCH_ENTITIES.actions).toHaveLength(5);
194
- });
195
- /**
196
- * Test 5: Per-Action Entity Mapping - Overwatch
197
- *
198
- * Studio UI needs to filter dropdowns based on selected action.
199
- * Each action has specific valid principals and resources.
200
- */
201
- it('should provide per-action entity mapping for Overwatch', () => {
202
- // call_tool action should have correct principals and resources
203
- const callTool = OVERWATCH_ACTION_ENTITIES['call_tool'];
204
- expect(callTool).toBeDefined();
205
- expect(callTool.principals).toContain('User');
206
- expect(callTool.principals).toContain('Agent');
207
- expect(callTool.resources).toContain('Tool');
208
- expect(callTool.resources).toContain('FilePath');
209
- // connect_server action should only apply to Server resource
210
- const connectServer = OVERWATCH_ACTION_ENTITIES['connect_server'];
211
- expect(connectServer).toBeDefined();
212
- expect(connectServer.principals).toContain('User');
213
- expect(connectServer.principals).toContain('Agent');
214
- expect(connectServer.resources).toContain('Server');
215
- expect(connectServer.resources).not.toContain('Tool');
216
- // process_prompt action should only apply to LlmPrompt resource
217
- const processPrompt = OVERWATCH_ACTION_ENTITIES['process_prompt'];
218
- expect(processPrompt).toBeDefined();
219
- expect(processPrompt.resources).toContain('LlmPrompt');
220
- expect(processPrompt.resources).not.toContain('Tool');
221
- // read_file and write_file should apply to FilePath resource
222
- const readFile = OVERWATCH_ACTION_ENTITIES['read_file'];
223
- const writeFile = OVERWATCH_ACTION_ENTITIES['write_file'];
224
- expect(readFile.resources).toContain('FilePath');
225
- expect(writeFile.resources).toContain('FilePath');
226
- });
227
- /**
228
- * Test 6: Entity Metadata for Palisade
229
- *
230
- * Verify Palisade service also has correct entity metadata.
231
- */
232
- it('should provide correct entity metadata for Palisade UI dropdowns', () => {
233
- // Palisade has Scanner as principal
234
- expect(PALISADE_ENTITIES.principals).toContain('Scanner');
235
- // Palisade resources include Artifact and Package
236
- expect(PALISADE_ENTITIES.resources).toContain('Artifact');
237
- expect(PALISADE_ENTITIES.resources).toContain('Package');
238
- // Palisade actions
239
- expect(PALISADE_ENTITIES.actions).toContain('load_model');
240
- expect(PALISADE_ENTITIES.actions).toContain('scan_artifact');
241
- expect(PALISADE_ENTITIES.actions).toContain('quarantine_artifact');
242
- // Per-action mapping - load_model applies to Artifact
243
- const loadModel = PALISADE_ACTION_ENTITIES['load_model'];
244
- expect(loadModel).toBeDefined();
245
- expect(loadModel.principals).toContain('Scanner');
246
- expect(loadModel.resources).toContain('Artifact');
247
- // scan_package applies to Package resource
248
- const scanPackage = PALISADE_ACTION_ENTITIES['scan_package'];
249
- expect(scanPackage).toBeDefined();
250
- expect(scanPackage.resources).toContain('Package');
251
- });
252
- });
253
- /**
254
- * Cedar Policy Annotations Integration Tests
255
- *
256
- * These tests verify the full round-trip of Cedar policy annotations:
257
- * PolicyRule → Cedar text → parse back → verify annotations preserved
258
- */
259
- // Import annotation functions for tests
260
- import { ruleToCedar, rulesToCedar, parseCedarToRules, generateRuleId, isValidAnnotationKey, } from './index.js';
261
- describe('Cedar Annotations Integration Tests', () => {
262
- /**
263
- * Test 1: Basic Annotations Round-Trip
264
- *
265
- * Create a PolicyRule with all standard annotations, convert to Cedar,
266
- * parse back, and verify all annotations are preserved.
267
- */
268
- it('should preserve annotations through Cedar round-trip', () => {
269
- // Step 1: Create a PolicyRule with full annotations
270
- const originalRule = {
271
- annotations: {
272
- id: 'rule-001',
273
- name: 'Block high-threat tool calls',
274
- description: 'Forbids tool calls when threat count exceeds threshold',
275
- severity: 'high',
276
- tags: ['security', 'baseline', 'v2'],
277
- },
278
- effect: 'forbid',
279
- principal: { type: 'Overwatch::User' },
280
- action: 'Overwatch::Action::"call_tool"',
281
- resource: { type: 'Overwatch::Tool' },
282
- conditions: [{ field: 'threat_count', operator: 'gt', value: 5 }],
283
- enabled: true,
284
- order: 0,
285
- };
286
- // Step 2: Convert to Cedar text
287
- const cedarText = ruleToCedar(originalRule);
288
- // Verify Cedar text contains annotations in proper syntax
289
- expect(cedarText).toContain('@id("rule-001")');
290
- expect(cedarText).toContain('@name("Block high-threat tool calls")');
291
- expect(cedarText).toContain('@description("Forbids tool calls when threat count exceeds threshold")');
292
- expect(cedarText).toContain('@severity("high")');
293
- expect(cedarText).toContain('@tags("security,baseline,v2")');
294
- expect(cedarText).toContain('forbid');
295
- expect(cedarText).toContain('context.threat_count > 5');
296
- // Step 3: Validate the generated Cedar against Overwatch schema
297
- const validator = new PolicyValidator(OVERWATCH_SCHEMA);
298
- const validationResult = validator.validate(cedarText);
299
- expect(validationResult.valid).toBe(true);
300
- // Step 4: Parse back to PolicyRule
301
- const parseResult = parseCedarToRules(cedarText);
302
- expect(parseResult.errors).toHaveLength(0);
303
- expect(parseResult.rules).toHaveLength(1);
304
- const parsedRule = parseResult.rules[0];
305
- // Step 5: Verify all annotations are preserved
306
- expect(parsedRule.annotations.id).toBe('rule-001');
307
- expect(parsedRule.annotations.name).toBe('Block high-threat tool calls');
308
- expect(parsedRule.annotations.description).toBe('Forbids tool calls when threat count exceeds threshold');
309
- expect(parsedRule.annotations.severity).toBe('high');
310
- expect(parsedRule.annotations.tags).toEqual(['security', 'baseline', 'v2']);
311
- });
312
- /**
313
- * Test 2: Custom Annotations Round-Trip
314
- *
315
- * Verifies that custom user-defined annotations are preserved.
316
- */
317
- it('should preserve custom annotations through Cedar round-trip', () => {
318
- const originalRule = {
319
- annotations: {
320
- id: 'compliance-rule',
321
- name: 'SOC2 Compliance Policy',
322
- severity: 'critical',
323
- },
324
- customAnnotations: {
325
- compliance: 'SOC2',
326
- ticket: 'SEC-1234',
327
- owner: 'security-team',
328
- review_date: '2024-06-01',
329
- },
330
- effect: 'forbid',
331
- principal: null,
332
- action: 'Overwatch::Action::"call_tool"',
333
- resource: null,
334
- conditions: [],
335
- rawCondition: 'context.contains_secrets == true',
336
- enabled: true,
337
- order: 0,
338
- };
339
- // Convert to Cedar
340
- const cedarText = ruleToCedar(originalRule);
341
- // Verify custom annotations in Cedar text (alphabetically ordered)
342
- expect(cedarText).toContain('@compliance("SOC2")');
343
- expect(cedarText).toContain('@owner("security-team")');
344
- expect(cedarText).toContain('@review_date("2024-06-01")');
345
- expect(cedarText).toContain('@ticket("SEC-1234")');
346
- // Validate Cedar
347
- const validator = new PolicyValidator(OVERWATCH_SCHEMA);
348
- expect(validator.validate(cedarText).valid).toBe(true);
349
- // Parse back
350
- const parseResult = parseCedarToRules(cedarText);
351
- expect(parseResult.errors).toHaveLength(0);
352
- const parsedRule = parseResult.rules[0];
353
- // Verify custom annotations preserved
354
- expect(parsedRule.customAnnotations).toBeDefined();
355
- expect(parsedRule.customAnnotations?.compliance).toBe('SOC2');
356
- expect(parsedRule.customAnnotations?.ticket).toBe('SEC-1234');
357
- expect(parsedRule.customAnnotations?.owner).toBe('security-team');
358
- expect(parsedRule.customAnnotations?.review_date).toBe('2024-06-01');
359
- });
360
- /**
361
- * Test 3: Multiple Rules with rulesToCedar
362
- *
363
- * Verifies that multiple rules are correctly converted and ordered.
364
- */
365
- it('should handle multiple rules with correct ordering', () => {
366
- const rules = [
367
- {
368
- annotations: { id: 'rule-c', name: 'Third Rule' },
369
- effect: 'permit',
370
- principal: null,
371
- action: 'Overwatch::Action::"read_file"',
372
- resource: null,
373
- conditions: [],
374
- enabled: true,
375
- order: 2,
376
- },
377
- {
378
- annotations: { id: 'rule-a', name: 'First Rule', severity: 'critical' },
379
- effect: 'forbid',
380
- principal: null,
381
- action: 'Overwatch::Action::"call_tool"',
382
- resource: null,
383
- conditions: [],
384
- rawCondition: 'context.threat_count > 0',
385
- enabled: true,
386
- order: 0,
387
- },
388
- {
389
- annotations: { id: 'rule-b', name: 'Second Rule (Disabled)' },
390
- effect: 'forbid',
391
- principal: null,
392
- action: 'Overwatch::Action::"write_file"',
393
- resource: null,
394
- conditions: [],
395
- enabled: false, // Disabled rule
396
- order: 1,
397
- },
398
- ];
399
- // Convert to Cedar (enabled only, sorted by order)
400
- const cedarText = rulesToCedar(rules);
401
- // Verify order (rule-a order=0 should come before rule-c order=2)
402
- const ruleAIndex = cedarText.indexOf('@id("rule-a")');
403
- const ruleCIndex = cedarText.indexOf('@id("rule-c")');
404
- expect(ruleAIndex).toBeLessThan(ruleCIndex);
405
- // Disabled rule should NOT be included
406
- expect(cedarText).not.toContain('@id("rule-b")');
407
- // Validate the combined policy text
408
- const validator = new PolicyValidator(OVERWATCH_SCHEMA);
409
- expect(validator.validate(cedarText).valid).toBe(true);
410
- // Parse back and verify
411
- const parseResult = parseCedarToRules(cedarText);
412
- expect(parseResult.errors).toHaveLength(0);
413
- expect(parseResult.rules).toHaveLength(2); // Only enabled rules
414
- // Verify parsed rules
415
- const ruleIds = parseResult.rules.map((r) => r.annotations.id);
416
- expect(ruleIds).toContain('rule-a');
417
- expect(ruleIds).toContain('rule-c');
418
- expect(ruleIds).not.toContain('rule-b');
419
- });
420
- /**
421
- * Test 4: Disabled Rules as Comments
422
- *
423
- * Verifies that includeDisabled=true adds disabled rules as comments.
424
- */
425
- it('should include disabled rules as comments when requested', () => {
426
- const rules = [
427
- {
428
- annotations: { id: 'active-rule', name: 'Active Rule' },
429
- effect: 'permit',
430
- principal: null,
431
- action: 'Overwatch::Action::"call_tool"',
432
- resource: null,
433
- conditions: [],
434
- enabled: true,
435
- order: 0,
436
- },
437
- {
438
- annotations: { id: 'disabled-rule', name: 'Disabled Rule' },
439
- effect: 'forbid',
440
- principal: null,
441
- action: 'Overwatch::Action::"call_tool"',
442
- resource: null,
443
- conditions: [],
444
- enabled: false,
445
- order: 1,
446
- },
447
- ];
448
- // Convert with includeDisabled=true
449
- const cedarText = rulesToCedar(rules, true);
450
- // Active rule should be normal
451
- expect(cedarText).toContain('@id("active-rule")');
452
- expect(cedarText).not.toMatch(/\/\/ \[DISABLED\].*@id\("active-rule"\)/);
453
- // Disabled rule should be commented
454
- expect(cedarText).toContain('// [DISABLED] @id("disabled-rule")');
455
- expect(cedarText).toContain('// [DISABLED] @name("Disabled Rule")');
456
- expect(cedarText).toContain('// [DISABLED] forbid');
457
- });
458
- /**
459
- * Test 5: Annotation Value Escaping
460
- *
461
- * Verifies that special characters in annotation values are properly escaped.
462
- */
463
- it('should properly escape special characters in annotation values', () => {
464
- const rule = {
465
- annotations: {
466
- id: 'escape-test',
467
- name: 'Rule with "quotes" and \\backslashes',
468
- description: 'Multi-line text works too',
469
- },
470
- effect: 'permit',
471
- principal: null,
472
- action: 'Overwatch::Action::"call_tool"',
473
- resource: null,
474
- conditions: [],
475
- enabled: true,
476
- order: 0,
477
- };
478
- const cedarText = ruleToCedar(rule);
479
- // Quotes and backslashes should be escaped
480
- expect(cedarText).toContain('Rule with \\"quotes\\" and \\\\backslashes');
481
- // Parse back and verify values are unescaped
482
- const parseResult = parseCedarToRules(cedarText);
483
- expect(parseResult.errors).toHaveLength(0);
484
- const parsedRule = parseResult.rules[0];
485
- expect(parsedRule.annotations.name).toBe('Rule with "quotes" and \\backslashes');
486
- });
487
- /**
488
- * Test 6: Annotation Key Validation
489
- *
490
- * Verifies that only valid annotation keys are accepted.
491
- */
492
- it('should validate annotation keys correctly', () => {
493
- // Valid custom keys
494
- expect(isValidAnnotationKey('compliance')).toBe(true);
495
- expect(isValidAnnotationKey('ticket_number')).toBe(true);
496
- expect(isValidAnnotationKey('_internal')).toBe(true);
497
- expect(isValidAnnotationKey('myKey123')).toBe(true);
498
- // Invalid - predefined keys (handled separately)
499
- expect(isValidAnnotationKey('id')).toBe(false);
500
- expect(isValidAnnotationKey('name')).toBe(false);
501
- expect(isValidAnnotationKey('severity')).toBe(false);
502
- expect(isValidAnnotationKey('tags')).toBe(false);
503
- // Invalid - bad format
504
- expect(isValidAnnotationKey('123abc')).toBe(false); // Starts with number
505
- expect(isValidAnnotationKey('has-dash')).toBe(false); // Contains dash
506
- expect(isValidAnnotationKey('has.dot')).toBe(false); // Contains dot
507
- expect(isValidAnnotationKey('')).toBe(false); // Empty
508
- });
509
- /**
510
- * Test 7: Generated Rule ID
511
- *
512
- * Verifies that rule IDs are auto-generated when not provided.
513
- */
514
- it('should auto-generate rule IDs when not provided', () => {
515
- const id1 = generateRuleId();
516
- const id2 = generateRuleId();
517
- // Should be valid UUIDs
518
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
519
- expect(id1).toMatch(uuidRegex);
520
- expect(id2).toMatch(uuidRegex);
521
- // Should be unique
522
- expect(id1).not.toBe(id2);
523
- });
524
- /**
525
- * Test 8: Palisade Schema Compatibility
526
- *
527
- * Verifies annotations work correctly with Palisade schema.
528
- */
529
- it('should work correctly with Palisade schema', () => {
530
- const rule = {
531
- annotations: {
532
- id: 'palisade-rule-001',
533
- name: 'Block Critical ML Findings',
534
- description: 'Prevents loading models with critical security findings',
535
- severity: 'critical',
536
- tags: ['ml-security', 'compliance'],
537
- },
538
- customAnnotations: {
539
- framework: 'pytorch',
540
- scan_type: 'static',
541
- },
542
- effect: 'forbid',
543
- principal: { type: 'Palisade::Scanner' },
544
- action: 'Palisade::Action::"load_model"',
545
- resource: { type: 'Palisade::Artifact' },
546
- conditions: [],
547
- rawCondition: 'context.severity == "CRITICAL"',
548
- enabled: true,
549
- order: 0,
550
- };
551
- const cedarText = ruleToCedar(rule);
552
- // Validate against Palisade schema
553
- const validator = new PolicyValidator(PALISADE_SCHEMA);
554
- expect(validator.validate(cedarText).valid).toBe(true);
555
- // Parse and verify
556
- const parseResult = parseCedarToRules(cedarText);
557
- expect(parseResult.errors).toHaveLength(0);
558
- const parsedRule = parseResult.rules[0];
559
- expect(parsedRule.annotations.id).toBe('palisade-rule-001');
560
- expect(parsedRule.annotations.severity).toBe('critical');
561
- expect(parsedRule.customAnnotations?.framework).toBe('pytorch');
562
- });
563
- /**
564
- * Test 9: Full Policy Lifecycle with Annotations
565
- *
566
- * Simulates the complete Studio UI workflow:
567
- * 1. Create rules via UI form
568
- * 2. Convert to Cedar for storage/evaluation
569
- * 3. Validate against schema
570
- * 4. Load into engine
571
- * 5. Evaluate requests
572
- * 6. Parse back for editing
573
- */
574
- it('should support full policy lifecycle with annotations', () => {
575
- // Step 1: Create rules via UI form (simulated)
576
- const rules = [
577
- {
578
- annotations: {
579
- id: 'allow-safe-tools',
580
- name: 'Allow Safe Tools',
581
- description: 'Permits tool calls with no detected threats',
582
- severity: 'low',
583
- },
584
- effect: 'permit',
585
- principal: { type: 'Overwatch::User' },
586
- action: 'Overwatch::Action::"call_tool"',
587
- resource: { type: 'Overwatch::Tool' },
588
- conditions: [],
589
- rawCondition: 'context.threat_count == 0',
590
- enabled: true,
591
- order: 0,
592
- },
593
- {
594
- annotations: {
595
- id: 'block-high-threats',
596
- name: 'Block High Threats',
597
- severity: 'critical',
598
- tags: ['security'],
599
- },
600
- effect: 'forbid',
601
- principal: null,
602
- action: 'Overwatch::Action::"call_tool"',
603
- resource: null,
604
- conditions: [{ field: 'threat_count', operator: 'gt', value: 0 }],
605
- enabled: true,
606
- order: 1,
607
- },
608
- ];
609
- // Step 2: Convert to Cedar for storage
610
- const cedarText = rulesToCedar(rules);
611
- // Step 3: Validate
612
- const validator = new PolicyValidator(OVERWATCH_SCHEMA);
613
- expect(validator.validate(cedarText).valid).toBe(true);
614
- // Step 4: Load into engine
615
- const engine = new PolicyEngine({ schema: OVERWATCH_SCHEMA });
616
- engine.loadPolicy(cedarText);
617
- // Step 5: Evaluate - safe request (0 threats) should be allowed
618
- const entities = [
619
- newEntity('Overwatch::User', 'mcp_client', { user_type: 'external', email: 'test@example.com' }),
620
- newEntity('Overwatch::Tool', 'shell', { tool_name: 'shell', risk_level: 'low' }),
621
- ];
622
- const safeRequest = engine.evaluate({
623
- principal: newEntityUID('Overwatch::User', 'mcp_client'),
624
- action: 'Overwatch::Action::"call_tool"',
625
- resource: newEntityUID('Overwatch::Tool', 'shell'),
626
- context: {
627
- content: 'ls',
628
- source: 'claudecode',
629
- event: 'PreToolUse',
630
- user_email: 'test@example.com',
631
- tool_name: 'shell',
632
- threat_count: 0,
633
- highest_severity: 'low',
634
- threat_categories: [],
635
- yara_threats: [],
636
- max_threat_severity: 0,
637
- contains_secrets: false,
638
- mcp_server: '',
639
- mcp_tool: '',
640
- path: '',
641
- cwd: '',
642
- workspace_root: '',
643
- response_content: '',
644
- },
645
- entities,
646
- });
647
- expect(safeRequest.effect).toBe('Allow');
648
- // Risky request (threats detected) should be denied
649
- const riskyRequest = engine.evaluate({
650
- principal: newEntityUID('Overwatch::User', 'mcp_client'),
651
- action: 'Overwatch::Action::"call_tool"',
652
- resource: newEntityUID('Overwatch::Tool', 'shell'),
653
- context: {
654
- content: 'rm -rf /',
655
- source: 'claudecode',
656
- event: 'PreToolUse',
657
- user_email: 'test@example.com',
658
- tool_name: 'shell',
659
- threat_count: 3,
660
- highest_severity: 'critical',
661
- threat_categories: ['destructive'],
662
- yara_threats: [],
663
- max_threat_severity: 4,
664
- contains_secrets: false,
665
- mcp_server: '',
666
- mcp_tool: '',
667
- path: '',
668
- cwd: '',
669
- workspace_root: '',
670
- response_content: '',
671
- },
672
- entities,
673
- });
674
- expect(riskyRequest.effect).toBe('Deny');
675
- // Step 6: Parse back for editing
676
- const parseResult = parseCedarToRules(cedarText);
677
- expect(parseResult.errors).toHaveLength(0);
678
- expect(parseResult.rules).toHaveLength(2);
679
- // Verify all rules and annotations are preserved for editing
680
- const allowRule = parseResult.rules.find((r) => r.annotations.id === 'allow-safe-tools');
681
- const blockRule = parseResult.rules.find((r) => r.annotations.id === 'block-high-threats');
682
- expect(allowRule?.annotations.name).toBe('Allow Safe Tools');
683
- expect(blockRule?.annotations.severity).toBe('critical');
684
- expect(blockRule?.annotations.tags).toEqual(['security']);
685
- });
686
- });
687
- //# sourceMappingURL=studio-ui.test.js.map