@highflame/policy 2.0.2 → 2.0.4
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/dist/annotations.d.ts +127 -0
- package/dist/annotations.d.ts.map +1 -0
- package/dist/annotations.js +175 -0
- package/dist/annotations.js.map +1 -0
- package/dist/builder.d.ts +114 -25
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +295 -113
- package/dist/builder.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +18 -11
- package/dist/parser.js.map +1 -1
- package/dist/parser.test.js +2 -2
- package/dist/parser.test.js.map +1 -1
- package/dist/studio-ui.test.js +436 -0
- package/dist/studio-ui.test.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/annotations.ts +243 -0
- package/src/builder.ts +386 -127
- package/src/index.ts +1 -0
- package/src/parser.test.ts +2 -2
- package/src/parser.ts +20 -12
- package/src/studio-ui.test.ts +499 -0
- package/src/types.ts +3 -0
package/src/studio-ui.test.ts
CHANGED
|
@@ -312,3 +312,502 @@ describe('Studio UI Integration Tests', () => {
|
|
|
312
312
|
expect(scanPackage.resources).toContain('Package');
|
|
313
313
|
});
|
|
314
314
|
});
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Cedar Policy Annotations Integration Tests
|
|
318
|
+
*
|
|
319
|
+
* These tests verify the full round-trip of Cedar policy annotations:
|
|
320
|
+
* PolicyRule → Cedar text → parse back → verify annotations preserved
|
|
321
|
+
*/
|
|
322
|
+
// Import annotation functions for tests
|
|
323
|
+
import {
|
|
324
|
+
ruleToCedar,
|
|
325
|
+
rulesToCedar,
|
|
326
|
+
parseCedarToRules,
|
|
327
|
+
generateRuleId,
|
|
328
|
+
isValidAnnotationKey,
|
|
329
|
+
type PolicyRule,
|
|
330
|
+
type PolicySeverity,
|
|
331
|
+
type PolicyEffect,
|
|
332
|
+
type ConditionOperator,
|
|
333
|
+
} from './index.js';
|
|
334
|
+
|
|
335
|
+
describe('Cedar Annotations Integration Tests', () => {
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Test 1: Basic Annotations Round-Trip
|
|
339
|
+
*
|
|
340
|
+
* Create a PolicyRule with all standard annotations, convert to Cedar,
|
|
341
|
+
* parse back, and verify all annotations are preserved.
|
|
342
|
+
*/
|
|
343
|
+
it('should preserve annotations through Cedar round-trip', () => {
|
|
344
|
+
// Step 1: Create a PolicyRule with full annotations
|
|
345
|
+
const originalRule: PolicyRule = {
|
|
346
|
+
annotations: {
|
|
347
|
+
id: 'rule-001',
|
|
348
|
+
name: 'Block high-threat tool calls',
|
|
349
|
+
description: 'Forbids tool calls when threat count exceeds threshold',
|
|
350
|
+
severity: 'high' as PolicySeverity,
|
|
351
|
+
tags: ['security', 'baseline', 'v2'],
|
|
352
|
+
},
|
|
353
|
+
effect: 'forbid' as PolicyEffect,
|
|
354
|
+
principal: { type: 'Overwatch::User' },
|
|
355
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
356
|
+
resource: { type: 'Overwatch::Tool' },
|
|
357
|
+
conditions: [{ field: 'threat_count', operator: 'gt' as ConditionOperator, value: 5 }],
|
|
358
|
+
enabled: true,
|
|
359
|
+
order: 0,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Step 2: Convert to Cedar text
|
|
363
|
+
const cedarText = ruleToCedar(originalRule);
|
|
364
|
+
|
|
365
|
+
// Verify Cedar text contains annotations in proper syntax
|
|
366
|
+
expect(cedarText).toContain('@id("rule-001")');
|
|
367
|
+
expect(cedarText).toContain('@name("Block high-threat tool calls")');
|
|
368
|
+
expect(cedarText).toContain('@description("Forbids tool calls when threat count exceeds threshold")');
|
|
369
|
+
expect(cedarText).toContain('@severity("high")');
|
|
370
|
+
expect(cedarText).toContain('@tags("security,baseline,v2")');
|
|
371
|
+
expect(cedarText).toContain('forbid');
|
|
372
|
+
expect(cedarText).toContain('context.threat_count > 5');
|
|
373
|
+
|
|
374
|
+
// Step 3: Validate the generated Cedar against Overwatch schema
|
|
375
|
+
const validator = new PolicyValidator(OVERWATCH_SCHEMA);
|
|
376
|
+
const validationResult = validator.validate(cedarText);
|
|
377
|
+
expect(validationResult.valid).toBe(true);
|
|
378
|
+
|
|
379
|
+
// Step 4: Parse back to PolicyRule
|
|
380
|
+
const parseResult = parseCedarToRules(cedarText);
|
|
381
|
+
expect(parseResult.errors).toHaveLength(0);
|
|
382
|
+
expect(parseResult.rules).toHaveLength(1);
|
|
383
|
+
|
|
384
|
+
const parsedRule = parseResult.rules[0];
|
|
385
|
+
|
|
386
|
+
// Step 5: Verify all annotations are preserved
|
|
387
|
+
expect(parsedRule.annotations.id).toBe('rule-001');
|
|
388
|
+
expect(parsedRule.annotations.name).toBe('Block high-threat tool calls');
|
|
389
|
+
expect(parsedRule.annotations.description).toBe(
|
|
390
|
+
'Forbids tool calls when threat count exceeds threshold'
|
|
391
|
+
);
|
|
392
|
+
expect(parsedRule.annotations.severity).toBe('high');
|
|
393
|
+
expect(parsedRule.annotations.tags).toEqual(['security', 'baseline', 'v2']);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Test 2: Custom Annotations Round-Trip
|
|
398
|
+
*
|
|
399
|
+
* Verifies that custom user-defined annotations are preserved.
|
|
400
|
+
*/
|
|
401
|
+
it('should preserve custom annotations through Cedar round-trip', () => {
|
|
402
|
+
const originalRule: PolicyRule = {
|
|
403
|
+
annotations: {
|
|
404
|
+
id: 'compliance-rule',
|
|
405
|
+
name: 'SOC2 Compliance Policy',
|
|
406
|
+
severity: 'critical' as PolicySeverity,
|
|
407
|
+
},
|
|
408
|
+
customAnnotations: {
|
|
409
|
+
compliance: 'SOC2',
|
|
410
|
+
ticket: 'SEC-1234',
|
|
411
|
+
owner: 'security-team',
|
|
412
|
+
review_date: '2024-06-01',
|
|
413
|
+
},
|
|
414
|
+
effect: 'forbid' as PolicyEffect,
|
|
415
|
+
principal: null,
|
|
416
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
417
|
+
resource: null,
|
|
418
|
+
conditions: [],
|
|
419
|
+
rawCondition: 'context.contains_secrets == true',
|
|
420
|
+
enabled: true,
|
|
421
|
+
order: 0,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// Convert to Cedar
|
|
425
|
+
const cedarText = ruleToCedar(originalRule);
|
|
426
|
+
|
|
427
|
+
// Verify custom annotations in Cedar text (alphabetically ordered)
|
|
428
|
+
expect(cedarText).toContain('@compliance("SOC2")');
|
|
429
|
+
expect(cedarText).toContain('@owner("security-team")');
|
|
430
|
+
expect(cedarText).toContain('@review_date("2024-06-01")');
|
|
431
|
+
expect(cedarText).toContain('@ticket("SEC-1234")');
|
|
432
|
+
|
|
433
|
+
// Validate Cedar
|
|
434
|
+
const validator = new PolicyValidator(OVERWATCH_SCHEMA);
|
|
435
|
+
expect(validator.validate(cedarText).valid).toBe(true);
|
|
436
|
+
|
|
437
|
+
// Parse back
|
|
438
|
+
const parseResult = parseCedarToRules(cedarText);
|
|
439
|
+
expect(parseResult.errors).toHaveLength(0);
|
|
440
|
+
const parsedRule = parseResult.rules[0];
|
|
441
|
+
|
|
442
|
+
// Verify custom annotations preserved
|
|
443
|
+
expect(parsedRule.customAnnotations).toBeDefined();
|
|
444
|
+
expect(parsedRule.customAnnotations?.compliance).toBe('SOC2');
|
|
445
|
+
expect(parsedRule.customAnnotations?.ticket).toBe('SEC-1234');
|
|
446
|
+
expect(parsedRule.customAnnotations?.owner).toBe('security-team');
|
|
447
|
+
expect(parsedRule.customAnnotations?.review_date).toBe('2024-06-01');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Test 3: Multiple Rules with rulesToCedar
|
|
452
|
+
*
|
|
453
|
+
* Verifies that multiple rules are correctly converted and ordered.
|
|
454
|
+
*/
|
|
455
|
+
it('should handle multiple rules with correct ordering', () => {
|
|
456
|
+
const rules: PolicyRule[] = [
|
|
457
|
+
{
|
|
458
|
+
annotations: { id: 'rule-c', name: 'Third Rule' },
|
|
459
|
+
effect: 'permit' as PolicyEffect,
|
|
460
|
+
principal: null,
|
|
461
|
+
action: 'Overwatch::Action::"read_file"',
|
|
462
|
+
resource: null,
|
|
463
|
+
conditions: [],
|
|
464
|
+
enabled: true,
|
|
465
|
+
order: 2,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
annotations: { id: 'rule-a', name: 'First Rule', severity: 'critical' as PolicySeverity },
|
|
469
|
+
effect: 'forbid' as PolicyEffect,
|
|
470
|
+
principal: null,
|
|
471
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
472
|
+
resource: null,
|
|
473
|
+
conditions: [],
|
|
474
|
+
rawCondition: 'context.threat_count > 0',
|
|
475
|
+
enabled: true,
|
|
476
|
+
order: 0,
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
annotations: { id: 'rule-b', name: 'Second Rule (Disabled)' },
|
|
480
|
+
effect: 'forbid' as PolicyEffect,
|
|
481
|
+
principal: null,
|
|
482
|
+
action: 'Overwatch::Action::"write_file"',
|
|
483
|
+
resource: null,
|
|
484
|
+
conditions: [],
|
|
485
|
+
enabled: false, // Disabled rule
|
|
486
|
+
order: 1,
|
|
487
|
+
},
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
// Convert to Cedar (enabled only, sorted by order)
|
|
491
|
+
const cedarText = rulesToCedar(rules);
|
|
492
|
+
|
|
493
|
+
// Verify order (rule-a order=0 should come before rule-c order=2)
|
|
494
|
+
const ruleAIndex = cedarText.indexOf('@id("rule-a")');
|
|
495
|
+
const ruleCIndex = cedarText.indexOf('@id("rule-c")');
|
|
496
|
+
expect(ruleAIndex).toBeLessThan(ruleCIndex);
|
|
497
|
+
|
|
498
|
+
// Disabled rule should NOT be included
|
|
499
|
+
expect(cedarText).not.toContain('@id("rule-b")');
|
|
500
|
+
|
|
501
|
+
// Validate the combined policy text
|
|
502
|
+
const validator = new PolicyValidator(OVERWATCH_SCHEMA);
|
|
503
|
+
expect(validator.validate(cedarText).valid).toBe(true);
|
|
504
|
+
|
|
505
|
+
// Parse back and verify
|
|
506
|
+
const parseResult = parseCedarToRules(cedarText);
|
|
507
|
+
expect(parseResult.errors).toHaveLength(0);
|
|
508
|
+
expect(parseResult.rules).toHaveLength(2); // Only enabled rules
|
|
509
|
+
|
|
510
|
+
// Verify parsed rules
|
|
511
|
+
const ruleIds = parseResult.rules.map((r: any) => r.annotations.id);
|
|
512
|
+
expect(ruleIds).toContain('rule-a');
|
|
513
|
+
expect(ruleIds).toContain('rule-c');
|
|
514
|
+
expect(ruleIds).not.toContain('rule-b');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Test 4: Disabled Rules as Comments
|
|
519
|
+
*
|
|
520
|
+
* Verifies that includeDisabled=true adds disabled rules as comments.
|
|
521
|
+
*/
|
|
522
|
+
it('should include disabled rules as comments when requested', () => {
|
|
523
|
+
const rules: PolicyRule[] = [
|
|
524
|
+
{
|
|
525
|
+
annotations: { id: 'active-rule', name: 'Active Rule' },
|
|
526
|
+
effect: 'permit' as PolicyEffect,
|
|
527
|
+
principal: null,
|
|
528
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
529
|
+
resource: null,
|
|
530
|
+
conditions: [],
|
|
531
|
+
enabled: true,
|
|
532
|
+
order: 0,
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
annotations: { id: 'disabled-rule', name: 'Disabled Rule' },
|
|
536
|
+
effect: 'forbid' as PolicyEffect,
|
|
537
|
+
principal: null,
|
|
538
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
539
|
+
resource: null,
|
|
540
|
+
conditions: [],
|
|
541
|
+
enabled: false,
|
|
542
|
+
order: 1,
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
// Convert with includeDisabled=true
|
|
547
|
+
const cedarText = rulesToCedar(rules, true);
|
|
548
|
+
|
|
549
|
+
// Active rule should be normal
|
|
550
|
+
expect(cedarText).toContain('@id("active-rule")');
|
|
551
|
+
expect(cedarText).not.toMatch(/\/\/ \[DISABLED\].*@id\("active-rule"\)/);
|
|
552
|
+
|
|
553
|
+
// Disabled rule should be commented
|
|
554
|
+
expect(cedarText).toContain('// [DISABLED] @id("disabled-rule")');
|
|
555
|
+
expect(cedarText).toContain('// [DISABLED] @name("Disabled Rule")');
|
|
556
|
+
expect(cedarText).toContain('// [DISABLED] forbid');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Test 5: Annotation Value Escaping
|
|
561
|
+
*
|
|
562
|
+
* Verifies that special characters in annotation values are properly escaped.
|
|
563
|
+
*/
|
|
564
|
+
it('should properly escape special characters in annotation values', () => {
|
|
565
|
+
const rule: PolicyRule = {
|
|
566
|
+
annotations: {
|
|
567
|
+
id: 'escape-test',
|
|
568
|
+
name: 'Rule with "quotes" and \\backslashes',
|
|
569
|
+
description: 'Multi-line text works too',
|
|
570
|
+
},
|
|
571
|
+
effect: 'permit' as PolicyEffect,
|
|
572
|
+
principal: null,
|
|
573
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
574
|
+
resource: null,
|
|
575
|
+
conditions: [],
|
|
576
|
+
enabled: true,
|
|
577
|
+
order: 0,
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const cedarText = ruleToCedar(rule);
|
|
581
|
+
|
|
582
|
+
// Quotes and backslashes should be escaped
|
|
583
|
+
expect(cedarText).toContain('Rule with \\"quotes\\" and \\\\backslashes');
|
|
584
|
+
|
|
585
|
+
// Parse back and verify values are unescaped
|
|
586
|
+
const parseResult = parseCedarToRules(cedarText);
|
|
587
|
+
expect(parseResult.errors).toHaveLength(0);
|
|
588
|
+
|
|
589
|
+
const parsedRule = parseResult.rules[0];
|
|
590
|
+
expect(parsedRule.annotations.name).toBe('Rule with "quotes" and \\backslashes');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Test 6: Annotation Key Validation
|
|
595
|
+
*
|
|
596
|
+
* Verifies that only valid annotation keys are accepted.
|
|
597
|
+
*/
|
|
598
|
+
it('should validate annotation keys correctly', () => {
|
|
599
|
+
// Valid custom keys
|
|
600
|
+
expect(isValidAnnotationKey('compliance')).toBe(true);
|
|
601
|
+
expect(isValidAnnotationKey('ticket_number')).toBe(true);
|
|
602
|
+
expect(isValidAnnotationKey('_internal')).toBe(true);
|
|
603
|
+
expect(isValidAnnotationKey('myKey123')).toBe(true);
|
|
604
|
+
|
|
605
|
+
// Invalid - predefined keys (handled separately)
|
|
606
|
+
expect(isValidAnnotationKey('id')).toBe(false);
|
|
607
|
+
expect(isValidAnnotationKey('name')).toBe(false);
|
|
608
|
+
expect(isValidAnnotationKey('severity')).toBe(false);
|
|
609
|
+
expect(isValidAnnotationKey('tags')).toBe(false);
|
|
610
|
+
|
|
611
|
+
// Invalid - bad format
|
|
612
|
+
expect(isValidAnnotationKey('123abc')).toBe(false); // Starts with number
|
|
613
|
+
expect(isValidAnnotationKey('has-dash')).toBe(false); // Contains dash
|
|
614
|
+
expect(isValidAnnotationKey('has.dot')).toBe(false); // Contains dot
|
|
615
|
+
expect(isValidAnnotationKey('')).toBe(false); // Empty
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Test 7: Generated Rule ID
|
|
620
|
+
*
|
|
621
|
+
* Verifies that rule IDs are auto-generated when not provided.
|
|
622
|
+
*/
|
|
623
|
+
it('should auto-generate rule IDs when not provided', () => {
|
|
624
|
+
const id1 = generateRuleId();
|
|
625
|
+
const id2 = generateRuleId();
|
|
626
|
+
|
|
627
|
+
// Should be valid UUIDs
|
|
628
|
+
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;
|
|
629
|
+
expect(id1).toMatch(uuidRegex);
|
|
630
|
+
expect(id2).toMatch(uuidRegex);
|
|
631
|
+
|
|
632
|
+
// Should be unique
|
|
633
|
+
expect(id1).not.toBe(id2);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Test 8: Palisade Schema Compatibility
|
|
638
|
+
*
|
|
639
|
+
* Verifies annotations work correctly with Palisade schema.
|
|
640
|
+
*/
|
|
641
|
+
it('should work correctly with Palisade schema', () => {
|
|
642
|
+
const rule: PolicyRule = {
|
|
643
|
+
annotations: {
|
|
644
|
+
id: 'palisade-rule-001',
|
|
645
|
+
name: 'Block Critical ML Findings',
|
|
646
|
+
description: 'Prevents loading models with critical security findings',
|
|
647
|
+
severity: 'critical' as PolicySeverity,
|
|
648
|
+
tags: ['ml-security', 'compliance'],
|
|
649
|
+
},
|
|
650
|
+
customAnnotations: {
|
|
651
|
+
framework: 'pytorch',
|
|
652
|
+
scan_type: 'static',
|
|
653
|
+
},
|
|
654
|
+
effect: 'forbid' as PolicyEffect,
|
|
655
|
+
principal: { type: 'Palisade::Scanner' },
|
|
656
|
+
action: 'Palisade::Action::"load_model"',
|
|
657
|
+
resource: { type: 'Palisade::Artifact' },
|
|
658
|
+
conditions: [],
|
|
659
|
+
rawCondition: 'context.severity == "CRITICAL"',
|
|
660
|
+
enabled: true,
|
|
661
|
+
order: 0,
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const cedarText = ruleToCedar(rule);
|
|
665
|
+
|
|
666
|
+
// Validate against Palisade schema
|
|
667
|
+
const validator = new PolicyValidator(PALISADE_SCHEMA);
|
|
668
|
+
expect(validator.validate(cedarText).valid).toBe(true);
|
|
669
|
+
|
|
670
|
+
// Parse and verify
|
|
671
|
+
const parseResult = parseCedarToRules(cedarText);
|
|
672
|
+
expect(parseResult.errors).toHaveLength(0);
|
|
673
|
+
|
|
674
|
+
const parsedRule = parseResult.rules[0];
|
|
675
|
+
expect(parsedRule.annotations.id).toBe('palisade-rule-001');
|
|
676
|
+
expect(parsedRule.annotations.severity).toBe('critical');
|
|
677
|
+
expect(parsedRule.customAnnotations?.framework).toBe('pytorch');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Test 9: Full Policy Lifecycle with Annotations
|
|
682
|
+
*
|
|
683
|
+
* Simulates the complete Studio UI workflow:
|
|
684
|
+
* 1. Create rules via UI form
|
|
685
|
+
* 2. Convert to Cedar for storage/evaluation
|
|
686
|
+
* 3. Validate against schema
|
|
687
|
+
* 4. Load into engine
|
|
688
|
+
* 5. Evaluate requests
|
|
689
|
+
* 6. Parse back for editing
|
|
690
|
+
*/
|
|
691
|
+
it('should support full policy lifecycle with annotations', () => {
|
|
692
|
+
// Step 1: Create rules via UI form (simulated)
|
|
693
|
+
const rules: PolicyRule[] = [
|
|
694
|
+
{
|
|
695
|
+
annotations: {
|
|
696
|
+
id: 'allow-safe-tools',
|
|
697
|
+
name: 'Allow Safe Tools',
|
|
698
|
+
description: 'Permits tool calls with no detected threats',
|
|
699
|
+
severity: 'low' as PolicySeverity,
|
|
700
|
+
},
|
|
701
|
+
effect: 'permit' as PolicyEffect,
|
|
702
|
+
principal: { type: 'Overwatch::User' },
|
|
703
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
704
|
+
resource: { type: 'Overwatch::Tool' },
|
|
705
|
+
conditions: [],
|
|
706
|
+
rawCondition: 'context.threat_count == 0',
|
|
707
|
+
enabled: true,
|
|
708
|
+
order: 0,
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
annotations: {
|
|
712
|
+
id: 'block-high-threats',
|
|
713
|
+
name: 'Block High Threats',
|
|
714
|
+
severity: 'critical' as PolicySeverity,
|
|
715
|
+
tags: ['security'],
|
|
716
|
+
},
|
|
717
|
+
effect: 'forbid' as PolicyEffect,
|
|
718
|
+
principal: null,
|
|
719
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
720
|
+
resource: null,
|
|
721
|
+
conditions: [{ field: 'threat_count', operator: 'gt' as ConditionOperator, value: 0 }],
|
|
722
|
+
enabled: true,
|
|
723
|
+
order: 1,
|
|
724
|
+
},
|
|
725
|
+
];
|
|
726
|
+
|
|
727
|
+
// Step 2: Convert to Cedar for storage
|
|
728
|
+
const cedarText = rulesToCedar(rules);
|
|
729
|
+
|
|
730
|
+
// Step 3: Validate
|
|
731
|
+
const validator = new PolicyValidator(OVERWATCH_SCHEMA);
|
|
732
|
+
expect(validator.validate(cedarText).valid).toBe(true);
|
|
733
|
+
|
|
734
|
+
// Step 4: Load into engine
|
|
735
|
+
const engine = new PolicyEngine({ schema: OVERWATCH_SCHEMA });
|
|
736
|
+
engine.loadPolicies(cedarText);
|
|
737
|
+
|
|
738
|
+
// Step 5: Evaluate - safe request (0 threats) should be allowed
|
|
739
|
+
const entities = [
|
|
740
|
+
newEntity('Overwatch::User', 'mcp_client', { user_type: 'external', email: 'test@example.com' }),
|
|
741
|
+
newEntity('Overwatch::Tool', 'shell', { tool_name: 'shell', risk_level: 'low' }),
|
|
742
|
+
];
|
|
743
|
+
|
|
744
|
+
const safeRequest = engine.evaluate({
|
|
745
|
+
principal: newEntityUID('Overwatch::User', 'mcp_client'),
|
|
746
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
747
|
+
resource: newEntityUID('Overwatch::Tool', 'shell'),
|
|
748
|
+
context: {
|
|
749
|
+
content: 'ls',
|
|
750
|
+
source: 'claudecode',
|
|
751
|
+
event: 'PreToolUse',
|
|
752
|
+
user_email: 'test@example.com',
|
|
753
|
+
tool_name: 'shell',
|
|
754
|
+
threat_count: 0,
|
|
755
|
+
highest_severity: 'low',
|
|
756
|
+
threat_categories: [],
|
|
757
|
+
threat_types: [],
|
|
758
|
+
yara_threats: [],
|
|
759
|
+
max_threat_severity: 0,
|
|
760
|
+
contains_secrets: false,
|
|
761
|
+
mcp_server: '',
|
|
762
|
+
mcp_tool: '',
|
|
763
|
+
path: '',
|
|
764
|
+
cwd: '',
|
|
765
|
+
workspace_root: '',
|
|
766
|
+
response_content: '',
|
|
767
|
+
},
|
|
768
|
+
entities,
|
|
769
|
+
});
|
|
770
|
+
expect(safeRequest.effect).toBe('Allow');
|
|
771
|
+
|
|
772
|
+
// Risky request (threats detected) should be denied
|
|
773
|
+
const riskyRequest = engine.evaluate({
|
|
774
|
+
principal: newEntityUID('Overwatch::User', 'mcp_client'),
|
|
775
|
+
action: 'Overwatch::Action::"call_tool"',
|
|
776
|
+
resource: newEntityUID('Overwatch::Tool', 'shell'),
|
|
777
|
+
context: {
|
|
778
|
+
content: 'rm -rf /',
|
|
779
|
+
source: 'claudecode',
|
|
780
|
+
event: 'PreToolUse',
|
|
781
|
+
user_email: 'test@example.com',
|
|
782
|
+
tool_name: 'shell',
|
|
783
|
+
threat_count: 3,
|
|
784
|
+
highest_severity: 'critical',
|
|
785
|
+
threat_categories: ['destructive'],
|
|
786
|
+
threat_types: ['shell_command'],
|
|
787
|
+
yara_threats: [],
|
|
788
|
+
max_threat_severity: 4,
|
|
789
|
+
contains_secrets: false,
|
|
790
|
+
mcp_server: '',
|
|
791
|
+
mcp_tool: '',
|
|
792
|
+
path: '',
|
|
793
|
+
cwd: '',
|
|
794
|
+
workspace_root: '',
|
|
795
|
+
response_content: '',
|
|
796
|
+
},
|
|
797
|
+
entities,
|
|
798
|
+
});
|
|
799
|
+
expect(riskyRequest.effect).toBe('Deny');
|
|
800
|
+
|
|
801
|
+
// Step 6: Parse back for editing
|
|
802
|
+
const parseResult = parseCedarToRules(cedarText);
|
|
803
|
+
expect(parseResult.errors).toHaveLength(0);
|
|
804
|
+
expect(parseResult.rules).toHaveLength(2);
|
|
805
|
+
|
|
806
|
+
// Verify all rules and annotations are preserved for editing
|
|
807
|
+
const allowRule = parseResult.rules.find((r: any) => r.annotations.id === 'allow-safe-tools');
|
|
808
|
+
const blockRule = parseResult.rules.find((r: any) => r.annotations.id === 'block-high-threats');
|
|
809
|
+
expect(allowRule?.annotations.name).toBe('Allow Safe Tools');
|
|
810
|
+
expect(blockRule?.annotations.severity).toBe('critical');
|
|
811
|
+
expect(blockRule?.annotations.tags).toEqual(['security']);
|
|
812
|
+
});
|
|
813
|
+
});
|
package/src/types.ts
CHANGED
|
@@ -17,6 +17,9 @@ export * from './builder.js';
|
|
|
17
17
|
// Error types - works in browser (no WASM dependency)
|
|
18
18
|
export * from './errors.js';
|
|
19
19
|
|
|
20
|
+
// Annotations - works in browser (no WASM dependency)
|
|
21
|
+
export * from './annotations.js';
|
|
22
|
+
|
|
20
23
|
// Service-specific schemas and context (inlined, browser-safe)
|
|
21
24
|
export {
|
|
22
25
|
OVERWATCH_SCHEMA,
|