@highflame/policy 2.0.7 → 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.
- package/_schemas/overwatch/context.json +163 -1
- package/_schemas/overwatch/schema.cedarschema +45 -0
- package/dist/actions.gen.d.ts +0 -1
- package/dist/actions.gen.js +0 -1
- package/dist/annotations.d.ts +0 -1
- package/dist/annotations.js +0 -1
- package/dist/builder.d.ts +0 -1
- package/dist/builder.js +0 -1
- package/dist/context.gen.d.ts +0 -1
- package/dist/context.gen.js +0 -1
- package/dist/engine.d.ts +0 -1
- package/dist/engine.js +0 -1
- package/dist/entities.gen.d.ts +0 -1
- package/dist/entities.gen.js +0 -1
- package/dist/entity-metadata-types.gen.d.ts +0 -1
- package/dist/entity-metadata-types.gen.js +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/errors.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/overwatch-context.gen.d.ts +13 -1
- package/dist/overwatch-context.gen.js +13 -1
- package/dist/overwatch-defaults.gen.d.ts +1 -2
- package/dist/overwatch-defaults.gen.js +346 -2
- package/dist/overwatch-entities.gen.d.ts +0 -1
- package/dist/overwatch-entities.gen.js +0 -1
- package/dist/palisade-context.gen.d.ts +0 -1
- package/dist/palisade-context.gen.js +0 -1
- package/dist/palisade-entities.gen.d.ts +0 -1
- package/dist/palisade-entities.gen.js +0 -1
- package/dist/parser.d.ts +0 -1
- package/dist/parser.js +0 -1
- package/dist/schema.gen.d.ts +0 -1
- package/dist/schema.gen.js +0 -1
- package/dist/schemas.d.ts +0 -1
- package/dist/schemas.js +0 -1
- package/dist/service-schemas.gen.d.ts +0 -1
- package/dist/service-schemas.gen.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/package.json +1 -2
- package/dist/actions.gen.d.ts.map +0 -1
- package/dist/actions.gen.js.map +0 -1
- package/dist/annotations.d.ts.map +0 -1
- package/dist/annotations.js.map +0 -1
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js.map +0 -1
- package/dist/context.gen.d.ts.map +0 -1
- package/dist/context.gen.js.map +0 -1
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js.map +0 -1
- package/dist/engine.test.d.ts +0 -8
- package/dist/engine.test.d.ts.map +0 -1
- package/dist/engine.test.js +0 -190
- package/dist/engine.test.js.map +0 -1
- package/dist/entities.gen.d.ts.map +0 -1
- package/dist/entities.gen.js.map +0 -1
- package/dist/entity-metadata-types.gen.d.ts.map +0 -1
- package/dist/entity-metadata-types.gen.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/overwatch-context.gen.d.ts.map +0 -1
- package/dist/overwatch-context.gen.js.map +0 -1
- package/dist/overwatch-defaults.gen.d.ts.map +0 -1
- package/dist/overwatch-defaults.gen.js.map +0 -1
- package/dist/overwatch-defaults.test.d.ts +0 -8
- package/dist/overwatch-defaults.test.d.ts.map +0 -1
- package/dist/overwatch-defaults.test.js +0 -145
- package/dist/overwatch-defaults.test.js.map +0 -1
- package/dist/overwatch-entities.gen.d.ts.map +0 -1
- package/dist/overwatch-entities.gen.js.map +0 -1
- package/dist/overwatch-rebac.test.d.ts +0 -25
- package/dist/overwatch-rebac.test.d.ts.map +0 -1
- package/dist/overwatch-rebac.test.js +0 -301
- package/dist/overwatch-rebac.test.js.map +0 -1
- package/dist/palisade-context.gen.d.ts.map +0 -1
- package/dist/palisade-context.gen.js.map +0 -1
- package/dist/palisade-entities.gen.d.ts.map +0 -1
- package/dist/palisade-entities.gen.js.map +0 -1
- package/dist/parser.d.ts.map +0 -1
- package/dist/parser.js.map +0 -1
- package/dist/parser.test.d.ts +0 -8
- package/dist/parser.test.d.ts.map +0 -1
- package/dist/parser.test.js +0 -212
- package/dist/parser.test.js.map +0 -1
- package/dist/schema.gen.d.ts.map +0 -1
- package/dist/schema.gen.js.map +0 -1
- package/dist/schemas.d.ts.map +0 -1
- package/dist/schemas.js.map +0 -1
- package/dist/schemas.test.d.ts +0 -8
- package/dist/schemas.test.d.ts.map +0 -1
- package/dist/schemas.test.js +0 -375
- package/dist/schemas.test.js.map +0 -1
- package/dist/service-schemas.gen.d.ts.map +0 -1
- package/dist/service-schemas.gen.js.map +0 -1
- package/dist/studio-ui.test.d.ts +0 -8
- package/dist/studio-ui.test.d.ts.map +0 -1
- package/dist/studio-ui.test.js +0 -687
- package/dist/studio-ui.test.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/src/actions.gen.ts +0 -57
- package/src/annotations.ts +0 -243
- package/src/builder.ts +0 -799
- package/src/context.gen.ts +0 -10
- package/src/engine.test.ts +0 -370
- package/src/engine.ts +0 -497
- package/src/entities.gen.ts +0 -65
- package/src/entity-metadata-types.gen.ts +0 -19
- package/src/errors.ts +0 -195
- package/src/index.ts +0 -62
- package/src/overwatch-context.gen.ts +0 -32
- package/src/overwatch-defaults.gen.ts +0 -907
- package/src/overwatch-defaults.test.ts +0 -176
- package/src/overwatch-entities.gen.ts +0 -41
- package/src/overwatch-rebac.test.ts +0 -346
- package/src/palisade-context.gen.ts +0 -28
- package/src/palisade-entities.gen.ts +0 -49
- package/src/parser.test.ts +0 -251
- package/src/parser.ts +0 -579
- package/src/schema.gen.ts +0 -134
- package/src/schemas.test.ts +0 -445
- package/src/schemas.ts +0 -91
- package/src/service-schemas.gen.ts +0 -608
- package/src/studio-ui.test.ts +0 -813
- package/src/types.ts +0 -66
package/src/builder.ts
DELETED
|
@@ -1,799 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PolicyBuilder - Type-safe Cedar policy construction for Highflame.
|
|
3
|
-
*
|
|
4
|
-
* This builder ensures that policies created from the UI are always valid
|
|
5
|
-
* by construction. It uses the generated types from the Cedar schema to
|
|
6
|
-
* provide compile-time safety and autocomplete support.
|
|
7
|
-
*
|
|
8
|
-
* Example usage:
|
|
9
|
-
* ```typescript
|
|
10
|
-
* const policy = PolicyBuilder.permit()
|
|
11
|
-
* .principal(EntityType.User, "user-123")
|
|
12
|
-
* .action(ActionType.ReadFile)
|
|
13
|
-
* .resource(EntityType.FilePath, "/data/reports")
|
|
14
|
-
* .when("context.environment == \"production\"")
|
|
15
|
-
* .build();
|
|
16
|
-
*
|
|
17
|
-
* // Get Cedar policy text (with proper @annotations)
|
|
18
|
-
* const cedarText = policy.toCedar();
|
|
19
|
-
*
|
|
20
|
-
* // Get JSON representation (for storage/editing)
|
|
21
|
-
* const policyJson = policy.toJSON();
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* Cedar Annotations:
|
|
25
|
-
* Policies include proper Cedar annotations that are embedded in the policy text:
|
|
26
|
-
* ```cedar
|
|
27
|
-
* @id("rule-001")
|
|
28
|
-
* @name("Block critical threats")
|
|
29
|
-
* @severity("high")
|
|
30
|
-
* permit(...) when {...};
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import { EntityType, EntityUID } from './entities.gen.js';
|
|
35
|
-
import { ActionType } from './actions.gen.js';
|
|
36
|
-
import {
|
|
37
|
-
type PolicyAnnotations,
|
|
38
|
-
type CustomAnnotations,
|
|
39
|
-
type PolicySeverity,
|
|
40
|
-
generateAnnotationLines,
|
|
41
|
-
generateRuleId,
|
|
42
|
-
} from './annotations.js';
|
|
43
|
-
|
|
44
|
-
// ============================================================================
|
|
45
|
-
// Security: Input Validation and Escaping
|
|
46
|
-
// ============================================================================
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Valid identifier pattern for Cedar (alphanumeric, underscore, with optional namespace).
|
|
50
|
-
*/
|
|
51
|
-
const VALID_IDENTIFIER_REGEX = /^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Pattern that matches potentially dangerous content in raw conditions.
|
|
55
|
-
* Note: '}' is NOT blocked because raw conditions may contain valid Cedar
|
|
56
|
-
* record literals or nested expressions. The ';' blocks when-clause escapes.
|
|
57
|
-
*/
|
|
58
|
-
const DANGEROUS_PATTERN_REGEX = /;|\/\/|\/\*|\*\/|permit\s*\(|forbid\s*\(/;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Escape a string value for use in Cedar string literals.
|
|
62
|
-
* This prevents injection attacks by escaping backslashes and double quotes.
|
|
63
|
-
*/
|
|
64
|
-
function escapeCedarString(value: string): string {
|
|
65
|
-
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Check if a string is a valid Cedar identifier.
|
|
70
|
-
*/
|
|
71
|
-
function isValidIdentifier(s: string): boolean {
|
|
72
|
-
return VALID_IDENTIFIER_REGEX.test(s);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Sanitize an identifier, replacing invalid characters with underscores.
|
|
77
|
-
*/
|
|
78
|
-
function sanitizeIdentifier(s: string, context: string): string {
|
|
79
|
-
if (isValidIdentifier(s)) {
|
|
80
|
-
return s;
|
|
81
|
-
}
|
|
82
|
-
// Replace invalid characters with underscores
|
|
83
|
-
const sanitized = s.replace(/[^A-Za-z0-9_:]/g, '_');
|
|
84
|
-
if (sanitized === '' || !isValidIdentifier(sanitized)) {
|
|
85
|
-
return `invalid_${context}`;
|
|
86
|
-
}
|
|
87
|
-
return sanitized;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Validate a raw condition string for potentially dangerous patterns.
|
|
92
|
-
* Returns true if the condition is safe to use.
|
|
93
|
-
*/
|
|
94
|
-
function isValidRawCondition(condition: string): boolean {
|
|
95
|
-
return !DANGEROUS_PATTERN_REGEX.test(condition);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Format an action string for Cedar policy text.
|
|
100
|
-
* Detects if action is already namespaced (contains 'Action::"') and preserves it,
|
|
101
|
-
* otherwise wraps with Action::"...".
|
|
102
|
-
* Escapes the action name to prevent injection attacks.
|
|
103
|
-
*/
|
|
104
|
-
function formatAction(action: string): string {
|
|
105
|
-
if (action.includes('Action::"')) {
|
|
106
|
-
// Already namespaced - extract and escape the action name
|
|
107
|
-
const parts = action.split('Action::"');
|
|
108
|
-
if (parts.length === 2) {
|
|
109
|
-
const actionName = parts[1].replace(/"$/, '');
|
|
110
|
-
return `${parts[0]}Action::"${escapeCedarString(actionName)}"`;
|
|
111
|
-
}
|
|
112
|
-
return action;
|
|
113
|
-
}
|
|
114
|
-
// Non-namespaced, wrap with Action::"..."
|
|
115
|
-
return `Action::"${escapeCedarString(action)}"`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Policy effect - permit or forbid
|
|
120
|
-
*/
|
|
121
|
-
export type PolicyEffect = 'permit' | 'forbid';
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Condition operator types
|
|
125
|
-
*/
|
|
126
|
-
export type ConditionOperator =
|
|
127
|
-
| 'eq' // ==
|
|
128
|
-
| 'neq' // !=
|
|
129
|
-
| 'lt' // <
|
|
130
|
-
| 'lte' // <=
|
|
131
|
-
| 'gt' // >
|
|
132
|
-
| 'gte' // >=
|
|
133
|
-
| 'contains' // .contains()
|
|
134
|
-
| 'in' // in
|
|
135
|
-
| 'like'; // like
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* A single condition in a policy
|
|
139
|
-
*/
|
|
140
|
-
export interface PolicyCondition {
|
|
141
|
-
/** The context key or attribute path */
|
|
142
|
-
field: string;
|
|
143
|
-
/** The comparison operator */
|
|
144
|
-
operator: ConditionOperator;
|
|
145
|
-
/** The value to compare against */
|
|
146
|
-
value: string | number | boolean | string[];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Principal or resource entity constraint.
|
|
151
|
-
* Used to specify type-only constraints (any entity of type) or
|
|
152
|
-
* specific entity constraints (type + id).
|
|
153
|
-
*/
|
|
154
|
-
export interface PolicyEntity {
|
|
155
|
-
/** Entity type (e.g., "Agent", "Tool", "FilePath", "User") */
|
|
156
|
-
type: string;
|
|
157
|
-
/** Optional specific entity ID. If omitted, matches any entity of this type. */
|
|
158
|
-
id?: string;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Alias for PolicyEntity when used as principal constraint */
|
|
162
|
-
export type PolicyPrincipal = PolicyEntity;
|
|
163
|
-
|
|
164
|
-
/** Alias for PolicyEntity when used as resource constraint */
|
|
165
|
-
export type PolicyResource = PolicyEntity;
|
|
166
|
-
|
|
167
|
-
// Re-export PolicySeverity from annotations for backwards compatibility
|
|
168
|
-
export type { PolicySeverity } from './annotations.js';
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* JSON representation of a policy for storage and editing.
|
|
172
|
-
* This is the base interface used by PolicyBuilder (legacy format).
|
|
173
|
-
*
|
|
174
|
-
* @deprecated Use PolicyRule with annotations for new code.
|
|
175
|
-
*/
|
|
176
|
-
export interface PolicyJSON {
|
|
177
|
-
/** Unique identifier for this policy */
|
|
178
|
-
id?: string;
|
|
179
|
-
/** Human-readable name/description */
|
|
180
|
-
name?: string;
|
|
181
|
-
/** Policy effect */
|
|
182
|
-
effect: PolicyEffect;
|
|
183
|
-
/** Principal constraint */
|
|
184
|
-
principal: PolicyEntity | null;
|
|
185
|
-
/** Action constraint - single action or array of actions */
|
|
186
|
-
action: string | string[];
|
|
187
|
-
/** Resource constraint */
|
|
188
|
-
resource: PolicyEntity | null;
|
|
189
|
-
/** Conditions (when clause) */
|
|
190
|
-
conditions: PolicyCondition[];
|
|
191
|
-
/** Raw condition string (for advanced users) */
|
|
192
|
-
rawCondition?: string;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* A policy rule with full Cedar annotation support.
|
|
197
|
-
*
|
|
198
|
-
* This is the canonical type used across all Highflame services:
|
|
199
|
-
* - highflame-studio (UI)
|
|
200
|
-
* - highflame-authz (Go backend)
|
|
201
|
-
* - Any Python services
|
|
202
|
-
*
|
|
203
|
-
* Each PolicyRule maps 1:1 to a Cedar policy statement with proper annotations.
|
|
204
|
-
*
|
|
205
|
-
* Annotations are embedded in Cedar text:
|
|
206
|
-
* ```cedar
|
|
207
|
-
* @id("rule-001")
|
|
208
|
-
* @name("Block critical threats")
|
|
209
|
-
* @severity("high")
|
|
210
|
-
* @tags("security,baseline")
|
|
211
|
-
* @compliance("SOC2")
|
|
212
|
-
* forbid(...) when {...};
|
|
213
|
-
* ```
|
|
214
|
-
*/
|
|
215
|
-
export interface PolicyRule {
|
|
216
|
-
/** Predefined annotations (embedded in Cedar text) */
|
|
217
|
-
annotations: PolicyAnnotations;
|
|
218
|
-
/** Custom user-defined annotations (embedded in Cedar text) */
|
|
219
|
-
customAnnotations?: CustomAnnotations;
|
|
220
|
-
|
|
221
|
-
/** Policy effect - permit or forbid */
|
|
222
|
-
effect: PolicyEffect;
|
|
223
|
-
/** Principal constraint */
|
|
224
|
-
principal: PolicyEntity | null;
|
|
225
|
-
/** Action constraint - single action or array of actions */
|
|
226
|
-
action: string | string[];
|
|
227
|
-
/** Resource constraint */
|
|
228
|
-
resource: PolicyEntity | null;
|
|
229
|
-
/** Structured conditions (when clause) */
|
|
230
|
-
conditions: PolicyCondition[];
|
|
231
|
-
/** Raw condition string (for advanced/complex conditions) */
|
|
232
|
-
rawCondition?: string;
|
|
233
|
-
|
|
234
|
-
/** Whether this rule is active - NOT embedded in Cedar (runtime state) */
|
|
235
|
-
enabled: boolean;
|
|
236
|
-
/** Display/evaluation order - NOT embedded in Cedar (runtime state) */
|
|
237
|
-
order: number;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Legacy PolicyRule format for backwards compatibility.
|
|
242
|
-
* Used when parsing policies that don't have the new annotations structure.
|
|
243
|
-
*
|
|
244
|
-
* @deprecated Use PolicyRule with annotations for new code.
|
|
245
|
-
*/
|
|
246
|
-
export interface LegacyPolicyRule extends PolicyJSON {
|
|
247
|
-
enabled: boolean;
|
|
248
|
-
order: number;
|
|
249
|
-
description?: string;
|
|
250
|
-
severity?: PolicySeverity;
|
|
251
|
-
tags?: string[];
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Convert a legacy PolicyRule to the new annotations-based format.
|
|
256
|
-
*/
|
|
257
|
-
export function convertLegacyRule(legacy: LegacyPolicyRule, index: number = 0): PolicyRule {
|
|
258
|
-
return {
|
|
259
|
-
annotations: {
|
|
260
|
-
id: legacy.id || generateRuleId(),
|
|
261
|
-
name: legacy.name || legacy.id || `Rule ${index + 1}`,
|
|
262
|
-
description: legacy.description,
|
|
263
|
-
severity: legacy.severity,
|
|
264
|
-
tags: legacy.tags,
|
|
265
|
-
},
|
|
266
|
-
effect: legacy.effect,
|
|
267
|
-
principal: legacy.principal,
|
|
268
|
-
action: legacy.action,
|
|
269
|
-
resource: legacy.resource,
|
|
270
|
-
conditions: legacy.conditions,
|
|
271
|
-
rawCondition: legacy.rawCondition,
|
|
272
|
-
enabled: legacy.enabled,
|
|
273
|
-
order: legacy.order,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* A built policy that can be converted to Cedar text or JSON.
|
|
279
|
-
* This class is used by PolicyBuilder for the legacy API.
|
|
280
|
-
*
|
|
281
|
-
* For new code, use ruleToCedar() and rulesToCedar() functions with PolicyRule.
|
|
282
|
-
*/
|
|
283
|
-
export class Policy {
|
|
284
|
-
constructor(private readonly data: PolicyJSON) {}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Convert to Cedar policy text.
|
|
288
|
-
* Uses proper Cedar @annotation syntax.
|
|
289
|
-
*/
|
|
290
|
-
toCedar(): string {
|
|
291
|
-
const lines: string[] = [];
|
|
292
|
-
|
|
293
|
-
// Generate proper Cedar annotations
|
|
294
|
-
if (this.data.id || this.data.name) {
|
|
295
|
-
const annotations: PolicyAnnotations = {
|
|
296
|
-
id: this.data.id || generateRuleId(),
|
|
297
|
-
name: this.data.name || this.data.id || 'Unnamed Policy',
|
|
298
|
-
};
|
|
299
|
-
lines.push(...generateAnnotationLines(annotations));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Generate policy body
|
|
303
|
-
lines.push(generatePolicyBody(
|
|
304
|
-
this.data.effect,
|
|
305
|
-
this.data.principal,
|
|
306
|
-
this.data.action,
|
|
307
|
-
this.data.resource,
|
|
308
|
-
this.data.conditions,
|
|
309
|
-
this.data.rawCondition
|
|
310
|
-
));
|
|
311
|
-
|
|
312
|
-
return lines.join('\n');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Get JSON representation for storage
|
|
317
|
-
*/
|
|
318
|
-
toJSON(): PolicyJSON {
|
|
319
|
-
return { ...this.data };
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Get the policy ID
|
|
324
|
-
*/
|
|
325
|
-
getId(): string | undefined {
|
|
326
|
-
return this.data.id;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Get the policy name
|
|
331
|
-
*/
|
|
332
|
-
getName(): string | undefined {
|
|
333
|
-
return this.data.name;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ============================================================================
|
|
338
|
-
// Cedar Generation Functions
|
|
339
|
-
// ============================================================================
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Convert a condition to Cedar syntax.
|
|
343
|
-
* Field names are sanitized to prevent injection attacks.
|
|
344
|
-
*/
|
|
345
|
-
function conditionToCedar(condition: PolicyCondition): string {
|
|
346
|
-
const field = sanitizeIdentifier(condition.field, 'field');
|
|
347
|
-
const { operator, value } = condition;
|
|
348
|
-
const valueStr = valueToString(value);
|
|
349
|
-
|
|
350
|
-
switch (operator) {
|
|
351
|
-
case 'eq':
|
|
352
|
-
return `context.${field} == ${valueStr}`;
|
|
353
|
-
case 'neq':
|
|
354
|
-
return `context.${field} != ${valueStr}`;
|
|
355
|
-
case 'lt':
|
|
356
|
-
return `context.${field} < ${valueStr}`;
|
|
357
|
-
case 'lte':
|
|
358
|
-
return `context.${field} <= ${valueStr}`;
|
|
359
|
-
case 'gt':
|
|
360
|
-
return `context.${field} > ${valueStr}`;
|
|
361
|
-
case 'gte':
|
|
362
|
-
return `context.${field} >= ${valueStr}`;
|
|
363
|
-
case 'contains':
|
|
364
|
-
return `context.${field}.contains(${valueStr})`;
|
|
365
|
-
case 'in':
|
|
366
|
-
if (Array.isArray(value)) {
|
|
367
|
-
const items = value.map(v => `"${escapeCedarString(v)}"`).join(', ');
|
|
368
|
-
return `context.${field} in [${items}]`;
|
|
369
|
-
}
|
|
370
|
-
return `context.${field} in ${valueStr}`;
|
|
371
|
-
case 'like':
|
|
372
|
-
return `context.${field} like ${valueStr}`;
|
|
373
|
-
default:
|
|
374
|
-
return `context.${field} == ${valueStr}`;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Convert a value to Cedar string representation.
|
|
380
|
-
* String values are escaped to prevent injection attacks.
|
|
381
|
-
*/
|
|
382
|
-
function valueToString(value: string | number | boolean | string[]): string {
|
|
383
|
-
if (typeof value === 'string') {
|
|
384
|
-
return `"${escapeCedarString(value)}"`;
|
|
385
|
-
}
|
|
386
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
387
|
-
return String(value);
|
|
388
|
-
}
|
|
389
|
-
if (Array.isArray(value)) {
|
|
390
|
-
return `[${value.map(v => `"${escapeCedarString(v)}"`).join(', ')}]`;
|
|
391
|
-
}
|
|
392
|
-
return String(value);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Generate the Cedar policy body (permit/forbid statement).
|
|
397
|
-
* All inputs are sanitized/escaped to prevent injection attacks.
|
|
398
|
-
*/
|
|
399
|
-
function generatePolicyBody(
|
|
400
|
-
effect: PolicyEffect,
|
|
401
|
-
principal: PolicyEntity | null,
|
|
402
|
-
action: string | string[],
|
|
403
|
-
resource: PolicyEntity | null,
|
|
404
|
-
conditions: PolicyCondition[],
|
|
405
|
-
rawCondition?: string
|
|
406
|
-
): string {
|
|
407
|
-
let policyLine = `${effect} (`;
|
|
408
|
-
|
|
409
|
-
// Principal
|
|
410
|
-
if (principal) {
|
|
411
|
-
const entityType = sanitizeIdentifier(principal.type, 'principal_type');
|
|
412
|
-
if (principal.id) {
|
|
413
|
-
policyLine += `\n principal == ${entityType}::"${escapeCedarString(principal.id)}"`;
|
|
414
|
-
} else {
|
|
415
|
-
policyLine += `\n principal is ${entityType}`;
|
|
416
|
-
}
|
|
417
|
-
} else {
|
|
418
|
-
policyLine += `\n principal`;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Action
|
|
422
|
-
if (Array.isArray(action)) {
|
|
423
|
-
const filtered = action.filter(a => a !== '*');
|
|
424
|
-
if (filtered.length === 0) {
|
|
425
|
-
policyLine += `,\n action`;
|
|
426
|
-
} else if (filtered.length === 1) {
|
|
427
|
-
policyLine += `,\n action == ${formatAction(filtered[0])}`;
|
|
428
|
-
} else {
|
|
429
|
-
const actions = filtered.map(a => formatAction(a)).join(', ');
|
|
430
|
-
policyLine += `,\n action in [${actions}]`;
|
|
431
|
-
}
|
|
432
|
-
} else if (!action || action === '*') {
|
|
433
|
-
policyLine += `,\n action`;
|
|
434
|
-
} else {
|
|
435
|
-
policyLine += `,\n action == ${formatAction(action)}`;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Resource
|
|
439
|
-
if (resource) {
|
|
440
|
-
const entityType = sanitizeIdentifier(resource.type, 'resource_type');
|
|
441
|
-
if (resource.id) {
|
|
442
|
-
policyLine += `,\n resource == ${entityType}::"${escapeCedarString(resource.id)}"`;
|
|
443
|
-
} else {
|
|
444
|
-
policyLine += `,\n resource is ${entityType}`;
|
|
445
|
-
}
|
|
446
|
-
} else {
|
|
447
|
-
policyLine += `,\n resource`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
policyLine += '\n)';
|
|
451
|
-
|
|
452
|
-
// When clause
|
|
453
|
-
// SECURITY: rawCondition is validated to prevent injection attacks.
|
|
454
|
-
// If validation fails, fall back to structured conditions.
|
|
455
|
-
if (rawCondition) {
|
|
456
|
-
if (isValidRawCondition(rawCondition)) {
|
|
457
|
-
policyLine += `\nwhen { ${rawCondition} };`;
|
|
458
|
-
} else if (conditions.length > 0) {
|
|
459
|
-
// Fallback to structured conditions if rawCondition is rejected
|
|
460
|
-
const conditionStr = conditions.map(c => conditionToCedar(c)).join(' && ');
|
|
461
|
-
policyLine += `\nwhen { ${conditionStr} };`;
|
|
462
|
-
} else {
|
|
463
|
-
policyLine += ';';
|
|
464
|
-
}
|
|
465
|
-
} else if (conditions.length > 0) {
|
|
466
|
-
const conditionStr = conditions.map(c => conditionToCedar(c)).join(' && ');
|
|
467
|
-
policyLine += `\nwhen { ${conditionStr} };`;
|
|
468
|
-
} else {
|
|
469
|
-
policyLine += ';';
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return policyLine;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Convert a PolicyRule to Cedar policy text with proper annotations.
|
|
477
|
-
*
|
|
478
|
-
* @param rule - The PolicyRule to convert
|
|
479
|
-
* @returns Cedar policy text string
|
|
480
|
-
*
|
|
481
|
-
* @example
|
|
482
|
-
* ```typescript
|
|
483
|
-
* const rule: PolicyRule = {
|
|
484
|
-
* annotations: { id: 'rule-001', name: 'Block threats', severity: 'high' },
|
|
485
|
-
* effect: 'forbid',
|
|
486
|
-
* principal: null,
|
|
487
|
-
* action: 'call_tool',
|
|
488
|
-
* resource: null,
|
|
489
|
-
* conditions: [{ field: 'threat_count', operator: 'gt', value: 0 }],
|
|
490
|
-
* enabled: true,
|
|
491
|
-
* order: 0,
|
|
492
|
-
* };
|
|
493
|
-
*
|
|
494
|
-
* const cedar = ruleToCedar(rule);
|
|
495
|
-
* // Output:
|
|
496
|
-
* // @id("rule-001")
|
|
497
|
-
* // @name("Block threats")
|
|
498
|
-
* // @severity("high")
|
|
499
|
-
* // forbid (
|
|
500
|
-
* // principal,
|
|
501
|
-
* // action == Action::"call_tool",
|
|
502
|
-
* // resource
|
|
503
|
-
* // )
|
|
504
|
-
* // when { context.threat_count > 0 };
|
|
505
|
-
* ```
|
|
506
|
-
*/
|
|
507
|
-
export function ruleToCedar(rule: PolicyRule): string {
|
|
508
|
-
const lines: string[] = [];
|
|
509
|
-
|
|
510
|
-
// Generate Cedar annotations
|
|
511
|
-
lines.push(...generateAnnotationLines(rule.annotations, rule.customAnnotations));
|
|
512
|
-
|
|
513
|
-
// Generate policy body
|
|
514
|
-
lines.push(generatePolicyBody(
|
|
515
|
-
rule.effect,
|
|
516
|
-
rule.principal,
|
|
517
|
-
rule.action,
|
|
518
|
-
rule.resource,
|
|
519
|
-
rule.conditions,
|
|
520
|
-
rule.rawCondition
|
|
521
|
-
));
|
|
522
|
-
|
|
523
|
-
return lines.join('\n');
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Convert multiple PolicyRules to Cedar policy text.
|
|
528
|
-
* Only enabled rules are included, sorted by order.
|
|
529
|
-
*
|
|
530
|
-
* @param rules - Array of PolicyRules to convert
|
|
531
|
-
* @param includeDisabled - If true, include disabled rules as comments (default: false)
|
|
532
|
-
* @returns Cedar policy text with all rules separated by blank lines
|
|
533
|
-
*
|
|
534
|
-
* @example
|
|
535
|
-
* ```typescript
|
|
536
|
-
* const rules: PolicyRule[] = [...];
|
|
537
|
-
* const cedarText = rulesToCedar(rules);
|
|
538
|
-
* ```
|
|
539
|
-
*/
|
|
540
|
-
export function rulesToCedar(rules: PolicyRule[], includeDisabled: boolean = false): string {
|
|
541
|
-
const sortedRules = [...rules].sort((a, b) => a.order - b.order);
|
|
542
|
-
|
|
543
|
-
const cedarPolicies: string[] = [];
|
|
544
|
-
for (const rule of sortedRules) {
|
|
545
|
-
if (rule.enabled) {
|
|
546
|
-
cedarPolicies.push(ruleToCedar(rule));
|
|
547
|
-
} else if (includeDisabled) {
|
|
548
|
-
// Include disabled rules as comments
|
|
549
|
-
const cedarLines = ruleToCedar(rule).split('\n');
|
|
550
|
-
cedarPolicies.push(cedarLines.map(line => `// [DISABLED] ${line}`).join('\n'));
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return cedarPolicies.join('\n\n');
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Builder for constructing Cedar policies with type safety.
|
|
559
|
-
*/
|
|
560
|
-
export class PolicyBuilder {
|
|
561
|
-
private data: PolicyJSON = {
|
|
562
|
-
effect: 'permit',
|
|
563
|
-
principal: null,
|
|
564
|
-
action: '',
|
|
565
|
-
resource: null,
|
|
566
|
-
conditions: [],
|
|
567
|
-
};
|
|
568
|
-
|
|
569
|
-
private constructor(effect: PolicyEffect) {
|
|
570
|
-
this.data.effect = effect;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Start building a permit policy
|
|
575
|
-
*/
|
|
576
|
-
static permit(): PolicyBuilder {
|
|
577
|
-
return new PolicyBuilder('permit');
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Start building a forbid policy
|
|
582
|
-
*/
|
|
583
|
-
static forbid(): PolicyBuilder {
|
|
584
|
-
return new PolicyBuilder('forbid');
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/**
|
|
588
|
-
* Create a builder from existing JSON (for editing)
|
|
589
|
-
*/
|
|
590
|
-
static fromJSON(json: PolicyJSON): PolicyBuilder {
|
|
591
|
-
const builder = new PolicyBuilder(json.effect);
|
|
592
|
-
builder.data = { ...json };
|
|
593
|
-
return builder;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* Set policy ID
|
|
598
|
-
*/
|
|
599
|
-
id(id: string): PolicyBuilder {
|
|
600
|
-
this.data.id = id;
|
|
601
|
-
return this;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* Set policy name/description
|
|
606
|
-
*/
|
|
607
|
-
name(name: string): PolicyBuilder {
|
|
608
|
-
this.data.name = name;
|
|
609
|
-
return this;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Set principal constraint by type only (any entity of this type)
|
|
614
|
-
*/
|
|
615
|
-
principalType(type: EntityType | string): PolicyBuilder {
|
|
616
|
-
this.data.principal = { type };
|
|
617
|
-
return this;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Set principal constraint by type and ID (specific entity)
|
|
622
|
-
*/
|
|
623
|
-
principal(type: EntityType | string, id: string): PolicyBuilder {
|
|
624
|
-
this.data.principal = { type, id };
|
|
625
|
-
return this;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Set principal from EntityUID
|
|
630
|
-
*/
|
|
631
|
-
principalEntity(entity: EntityUID): PolicyBuilder {
|
|
632
|
-
this.data.principal = { type: entity.type, id: entity.id };
|
|
633
|
-
return this;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Set single action constraint
|
|
638
|
-
*/
|
|
639
|
-
action(action: ActionType | string): PolicyBuilder {
|
|
640
|
-
this.data.action = action;
|
|
641
|
-
return this;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Set multiple action constraints (action in [list])
|
|
646
|
-
*/
|
|
647
|
-
actions(actions: (ActionType | string)[]): PolicyBuilder {
|
|
648
|
-
this.data.action = actions;
|
|
649
|
-
return this;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Set resource constraint by type only (any entity of this type)
|
|
654
|
-
*/
|
|
655
|
-
resourceType(type: EntityType | string): PolicyBuilder {
|
|
656
|
-
this.data.resource = { type };
|
|
657
|
-
return this;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Set resource constraint by type and ID (specific entity)
|
|
662
|
-
*/
|
|
663
|
-
resource(type: EntityType | string, id: string): PolicyBuilder {
|
|
664
|
-
this.data.resource = { type, id };
|
|
665
|
-
return this;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* Set resource from EntityUID
|
|
670
|
-
*/
|
|
671
|
-
resourceEntity(entity: EntityUID): PolicyBuilder {
|
|
672
|
-
this.data.resource = { type: entity.type, id: entity.id };
|
|
673
|
-
return this;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Add a structured condition
|
|
678
|
-
*/
|
|
679
|
-
when(field: string, operator: ConditionOperator, value: string | number | boolean | string[]): PolicyBuilder {
|
|
680
|
-
this.data.conditions.push({ field, operator, value });
|
|
681
|
-
return this;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Add a raw condition string (for advanced users)
|
|
686
|
-
*/
|
|
687
|
-
whenRaw(condition: string): PolicyBuilder {
|
|
688
|
-
this.data.rawCondition = condition;
|
|
689
|
-
return this;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Clear all conditions
|
|
694
|
-
*/
|
|
695
|
-
clearConditions(): PolicyBuilder {
|
|
696
|
-
this.data.conditions = [];
|
|
697
|
-
this.data.rawCondition = undefined;
|
|
698
|
-
return this;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Build the policy
|
|
703
|
-
*/
|
|
704
|
-
build(): Policy {
|
|
705
|
-
// Validate required fields
|
|
706
|
-
if (!this.data.action || (Array.isArray(this.data.action) && this.data.action.length === 0)) {
|
|
707
|
-
throw new Error('Policy must have at least one action');
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
return new Policy({ ...this.data });
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
/**
|
|
714
|
-
* Get current state as JSON (for preview/debugging)
|
|
715
|
-
*/
|
|
716
|
-
toJSON(): PolicyJSON {
|
|
717
|
-
return { ...this.data };
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Parse Cedar policy text back to PolicyJSON (best effort)
|
|
723
|
-
* Note: This is a simplified parser for policies created by PolicyBuilder.
|
|
724
|
-
* Complex hand-written policies may not parse correctly.
|
|
725
|
-
*/
|
|
726
|
-
export function parseCedarPolicy(cedarText: string): PolicyJSON | null {
|
|
727
|
-
try {
|
|
728
|
-
const result: PolicyJSON = {
|
|
729
|
-
effect: 'permit',
|
|
730
|
-
principal: null,
|
|
731
|
-
action: '',
|
|
732
|
-
resource: null,
|
|
733
|
-
conditions: [],
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
// Extract name from annotation
|
|
737
|
-
const nameMatch = cedarText.match(/\/\/ @name: (.+)/);
|
|
738
|
-
if (nameMatch) {
|
|
739
|
-
result.name = nameMatch[1].trim();
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// Extract id from annotation
|
|
743
|
-
const idMatch = cedarText.match(/\/\/ @id: (.+)/);
|
|
744
|
-
if (idMatch) {
|
|
745
|
-
result.id = idMatch[1].trim();
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Extract effect
|
|
749
|
-
if (cedarText.includes('forbid')) {
|
|
750
|
-
result.effect = 'forbid';
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// Extract principal
|
|
754
|
-
const principalMatch = cedarText.match(/principal\s*==\s*(\w+)::"([^"]+)"/);
|
|
755
|
-
if (principalMatch) {
|
|
756
|
-
result.principal = { type: principalMatch[1], id: principalMatch[2] };
|
|
757
|
-
} else {
|
|
758
|
-
const principalTypeMatch = cedarText.match(/principal\s+is\s+(\w+)/);
|
|
759
|
-
if (principalTypeMatch) {
|
|
760
|
-
result.principal = { type: principalTypeMatch[1] };
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// Extract action(s)
|
|
765
|
-
const actionMatch = cedarText.match(/action\s*==\s*Action::"([^"]+)"/);
|
|
766
|
-
if (actionMatch) {
|
|
767
|
-
result.action = actionMatch[1];
|
|
768
|
-
} else {
|
|
769
|
-
const actionsMatch = cedarText.match(/action\s+in\s+\[([^\]]+)\]/);
|
|
770
|
-
if (actionsMatch) {
|
|
771
|
-
const actions = actionsMatch[1].match(/Action::"([^"]+)"/g);
|
|
772
|
-
if (actions) {
|
|
773
|
-
result.action = actions.map(a => a.replace(/Action::"([^"]+)"/, '$1'));
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Extract resource
|
|
779
|
-
const resourceMatch = cedarText.match(/resource\s*==\s*(\w+)::"([^"]+)"/);
|
|
780
|
-
if (resourceMatch) {
|
|
781
|
-
result.resource = { type: resourceMatch[1], id: resourceMatch[2] };
|
|
782
|
-
} else {
|
|
783
|
-
const resourceTypeMatch = cedarText.match(/resource\s+is\s+(\w+)/);
|
|
784
|
-
if (resourceTypeMatch) {
|
|
785
|
-
result.resource = { type: resourceTypeMatch[1] };
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Extract when clause (as raw condition)
|
|
790
|
-
const whenMatch = cedarText.match(/when\s*\{([^}]+)\}/);
|
|
791
|
-
if (whenMatch) {
|
|
792
|
-
result.rawCondition = whenMatch[1].trim();
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
return result;
|
|
796
|
-
} catch {
|
|
797
|
-
return null;
|
|
798
|
-
}
|
|
799
|
-
}
|