@highflame/policy 2.0.9 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_schemas/guardrails/context.json +435 -0
- package/_schemas/guardrails/schema.cedarschema +225 -0
- package/_schemas/guardrails/templates/defaults/agentic_safety.cedar +94 -0
- package/_schemas/guardrails/templates/defaults/baseline.cedar +24 -0
- package/_schemas/guardrails/templates/defaults/injection.cedar +70 -0
- package/_schemas/guardrails/templates/defaults/pii.cedar +48 -0
- package/_schemas/guardrails/templates/defaults/secrets.cedar +40 -0
- package/_schemas/guardrails/templates/defaults/semantic.cedar +59 -0
- package/_schemas/guardrails/templates/defaults/tool_risk.cedar +58 -0
- package/_schemas/guardrails/templates/defaults/toxicity.cedar +76 -0
- package/_schemas/guardrails/templates/mcp_tool_permissions.cedar +84 -0
- package/_schemas/guardrails/templates/profiles/chat_assistant/privacy.cedar +22 -0
- package/_schemas/guardrails/templates/profiles/chat_assistant/security.cedar +35 -0
- package/_schemas/guardrails/templates/profiles/chat_assistant/trust_safety.cedar +43 -0
- package/_schemas/guardrails/templates/profiles/chat_assistant.cedar +85 -0
- package/_schemas/guardrails/templates/profiles/code_agent/agentic_security.cedar +109 -0
- package/_schemas/guardrails/templates/profiles/code_agent/security.cedar +22 -0
- package/_schemas/guardrails/templates/profiles/code_agent.cedar +125 -0
- package/_schemas/guardrails/templates/profiles/data_pipeline/agentic_security.cedar +38 -0
- package/_schemas/guardrails/templates/profiles/data_pipeline/privacy.cedar +40 -0
- package/_schemas/guardrails/templates/profiles/data_pipeline/security.cedar +49 -0
- package/_schemas/guardrails/templates/profiles/data_pipeline.cedar +111 -0
- package/_schemas/guardrails/templates/templates.json +213 -0
- package/_schemas/overwatch/context.json +54 -54
- package/_schemas/overwatch/schema.cedarschema +77 -68
- package/dist/builder.d.ts +106 -13
- package/dist/builder.js +103 -34
- package/dist/engine.d.ts +20 -2
- package/dist/engine.js +50 -20
- package/dist/entities.gen.d.ts +4 -0
- package/dist/entities.gen.js +4 -0
- package/dist/explain.d.ts +150 -0
- package/dist/explain.js +363 -0
- package/dist/guardrails-context.gen.d.ts +49 -0
- package/dist/guardrails-context.gen.js +50 -0
- package/dist/guardrails-defaults.gen.d.ts +61 -0
- package/dist/guardrails-defaults.gen.js +1278 -0
- package/dist/guardrails-entities.gen.d.ts +11 -0
- package/dist/guardrails-entities.gen.js +37 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/overwatch-defaults.gen.js +122 -2
- package/dist/parser.js +136 -4
- package/dist/schema.gen.d.ts +1 -1
- package/dist/schema.gen.js +6 -0
- package/dist/service-schemas.gen.d.ts +15 -11
- package/dist/service-schemas.gen.js +509 -84
- package/dist/types.d.ts +6 -1
- package/dist/types.js +6 -1
- package/package.json +5 -1
|
@@ -84,8 +84,8 @@ action process_prompt appliesTo {
|
|
|
84
84
|
user_email: String, // User identifier
|
|
85
85
|
|
|
86
86
|
// Workspace
|
|
87
|
-
cwd
|
|
88
|
-
workspace_root
|
|
87
|
+
cwd?: String, // Current working directory
|
|
88
|
+
workspace_root?: String, // Workspace/repository root
|
|
89
89
|
|
|
90
90
|
// Threat Detection
|
|
91
91
|
threat_count: Long, // Total threats detected
|
|
@@ -94,24 +94,27 @@ action process_prompt appliesTo {
|
|
|
94
94
|
yara_threats: Set<String>, // YARA rule names
|
|
95
95
|
max_threat_severity: Long, // Numeric severity (0-4)
|
|
96
96
|
contains_secrets: Bool, // Whether secrets detected
|
|
97
|
-
prompt_text
|
|
98
|
-
response_content
|
|
97
|
+
prompt_text?: String, // Same as content (legacy)
|
|
98
|
+
response_content?: String, // Response content (if available)
|
|
99
99
|
|
|
100
100
|
// Trust/Safety Scores (0-100, from Javelin/Lakera/LlamaGuard classifiers)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
// Required: content safety classifiers always run for prompt processing
|
|
102
|
+
violence_score: Long, // Violence content detection score
|
|
103
|
+
weapons_score: Long, // Weapons content detection score
|
|
104
|
+
hate_speech_score: Long, // Hate speech detection score
|
|
105
|
+
crime_score: Long, // Criminal content detection score
|
|
106
|
+
sexual_score: Long, // Sexual content detection score
|
|
107
|
+
profanity_score: Long, // Profanity detection score
|
|
107
108
|
|
|
108
109
|
// Detector Confidence Scores (0-100, ML classifier confidence)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
// Required: ML classifiers always run for prompt processing
|
|
111
|
+
pii_confidence: Long, // PII detection confidence
|
|
112
|
+
injection_confidence: Long, // Prompt injection confidence
|
|
113
|
+
jailbreak_confidence: Long, // Jailbreak detection confidence
|
|
112
114
|
|
|
113
115
|
// Agent Security (0-100)
|
|
114
|
-
|
|
116
|
+
// Required: agent security scanners always run for prompt processing
|
|
117
|
+
indirect_injection_score: Long, // Indirect prompt injection risk
|
|
115
118
|
},
|
|
116
119
|
};
|
|
117
120
|
|
|
@@ -127,46 +130,50 @@ action call_tool appliesTo {
|
|
|
127
130
|
user_email: String, // User identifier
|
|
128
131
|
|
|
129
132
|
// Tool & MCP
|
|
130
|
-
tool_name
|
|
131
|
-
mcp_server
|
|
132
|
-
mcp_tool
|
|
133
|
+
tool_name?: String, // Normalized tool name ("shell", "read_file", etc.)
|
|
134
|
+
mcp_server?: String, // MCP server name
|
|
135
|
+
mcp_tool?: String, // MCP tool name
|
|
133
136
|
|
|
134
137
|
// File & Path
|
|
135
|
-
path
|
|
138
|
+
path?: String, // File path (if file operation)
|
|
136
139
|
|
|
137
140
|
// Workspace
|
|
138
|
-
cwd
|
|
139
|
-
workspace_root
|
|
140
|
-
|
|
141
|
-
// Threat Detection
|
|
142
|
-
threat_count
|
|
143
|
-
highest_severity
|
|
144
|
-
threat_categories
|
|
145
|
-
yara_threats
|
|
146
|
-
max_threat_severity
|
|
147
|
-
contains_secrets
|
|
148
|
-
response_content
|
|
141
|
+
cwd?: String,
|
|
142
|
+
workspace_root?: String,
|
|
143
|
+
|
|
144
|
+
// Threat Detection (optional: scanning may not have run before tool call)
|
|
145
|
+
threat_count?: Long,
|
|
146
|
+
highest_severity?: String,
|
|
147
|
+
threat_categories?: Set<String>,
|
|
148
|
+
yara_threats?: Set<String>,
|
|
149
|
+
max_threat_severity?: Long,
|
|
150
|
+
contains_secrets?: Bool,
|
|
151
|
+
response_content?: String,
|
|
149
152
|
|
|
150
153
|
// Trust/Safety Scores (0-100, from Javelin/Lakera/LlamaGuard classifiers)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
// Optional: only present when trust/safety classifiers have run
|
|
155
|
+
violence_score?: Long, // Violence content detection score
|
|
156
|
+
weapons_score?: Long, // Weapons content detection score
|
|
157
|
+
hate_speech_score?: Long, // Hate speech detection score
|
|
158
|
+
crime_score?: Long, // Criminal content detection score
|
|
159
|
+
sexual_score?: Long, // Sexual content detection score
|
|
160
|
+
profanity_score?: Long, // Profanity detection score
|
|
157
161
|
|
|
158
162
|
// Detector Confidence Scores (0-100, ML classifier confidence)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
// Optional: only present when ML classifiers have run
|
|
164
|
+
pii_confidence?: Long, // PII detection confidence
|
|
165
|
+
injection_confidence?: Long, // Prompt injection confidence
|
|
166
|
+
jailbreak_confidence?: Long, // Jailbreak detection confidence
|
|
162
167
|
|
|
163
168
|
// Agent Security (0-100)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
169
|
+
// Optional: only present when agent security scanners have run
|
|
170
|
+
tool_poisoning_score?: Long, // Tool description manipulation risk
|
|
171
|
+
rug_pull_score?: Long, // Tool behavior mismatch risk
|
|
172
|
+
indirect_injection_score?: Long, // Indirect prompt injection risk
|
|
167
173
|
|
|
168
174
|
// MCP Trust
|
|
169
|
-
|
|
175
|
+
// Optional: only present when MCP server verification has run
|
|
176
|
+
mcp_server_verified?: Bool, // Whether server is from verified registry
|
|
170
177
|
},
|
|
171
178
|
};
|
|
172
179
|
|
|
@@ -175,23 +182,25 @@ action connect_server appliesTo {
|
|
|
175
182
|
principal: [User, Agent],
|
|
176
183
|
resource: [Server],
|
|
177
184
|
context: {
|
|
178
|
-
content
|
|
185
|
+
content?: String, // No content to scan when connecting
|
|
179
186
|
source: String,
|
|
180
187
|
event: String,
|
|
181
188
|
user_email: String,
|
|
182
|
-
mcp_server
|
|
183
|
-
threat_count
|
|
184
|
-
highest_severity
|
|
185
|
-
threat_categories
|
|
186
|
-
max_threat_severity
|
|
189
|
+
mcp_server?: String,
|
|
190
|
+
threat_count?: Long, // Threat scanning may not run for connections
|
|
191
|
+
highest_severity?: String,
|
|
192
|
+
threat_categories?: Set<String>,
|
|
193
|
+
max_threat_severity?: Long,
|
|
187
194
|
|
|
188
195
|
// Agent Security (0-100)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
// Optional: only present when agent security scanners have run
|
|
197
|
+
tool_poisoning_score?: Long, // Tool description manipulation risk
|
|
198
|
+
rug_pull_score?: Long, // Tool behavior mismatch risk
|
|
199
|
+
indirect_injection_score?: Long, // Indirect prompt injection risk
|
|
192
200
|
|
|
193
201
|
// MCP Trust
|
|
194
|
-
|
|
202
|
+
// Optional: only present when MCP server verification has run
|
|
203
|
+
mcp_server_verified?: Bool, // Whether server is from verified registry
|
|
195
204
|
},
|
|
196
205
|
};
|
|
197
206
|
|
|
@@ -204,14 +213,14 @@ action read_file appliesTo {
|
|
|
204
213
|
source: String,
|
|
205
214
|
event: String,
|
|
206
215
|
user_email: String,
|
|
207
|
-
path
|
|
208
|
-
cwd
|
|
209
|
-
workspace_root
|
|
210
|
-
threat_count
|
|
211
|
-
highest_severity
|
|
212
|
-
threat_categories
|
|
213
|
-
max_threat_severity
|
|
214
|
-
contains_secrets
|
|
216
|
+
path?: String,
|
|
217
|
+
cwd?: String,
|
|
218
|
+
workspace_root?: String,
|
|
219
|
+
threat_count?: Long, // Threat scanning may not have run
|
|
220
|
+
highest_severity?: String,
|
|
221
|
+
threat_categories?: Set<String>,
|
|
222
|
+
max_threat_severity?: Long,
|
|
223
|
+
contains_secrets?: Bool,
|
|
215
224
|
},
|
|
216
225
|
};
|
|
217
226
|
|
|
@@ -224,14 +233,14 @@ action write_file appliesTo {
|
|
|
224
233
|
source: String,
|
|
225
234
|
event: String,
|
|
226
235
|
user_email: String,
|
|
227
|
-
path
|
|
228
|
-
cwd
|
|
229
|
-
workspace_root
|
|
230
|
-
threat_count
|
|
231
|
-
highest_severity
|
|
232
|
-
threat_categories
|
|
233
|
-
max_threat_severity
|
|
234
|
-
contains_secrets
|
|
236
|
+
path?: String,
|
|
237
|
+
cwd?: String,
|
|
238
|
+
workspace_root?: String,
|
|
239
|
+
threat_count?: Long, // Threat scanning may not have run
|
|
240
|
+
highest_severity?: String,
|
|
241
|
+
threat_categories?: Set<String>,
|
|
242
|
+
max_threat_severity?: Long,
|
|
243
|
+
contains_secrets?: Bool,
|
|
235
244
|
},
|
|
236
245
|
};
|
|
237
246
|
|
package/dist/builder.d.ts
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { EntityType, EntityUID } from './entities.gen.js';
|
|
34
34
|
import { ActionType } from './actions.gen.js';
|
|
35
35
|
import { type PolicyAnnotations, type CustomAnnotations, type PolicySeverity } from './annotations.js';
|
|
36
|
+
import type { ServiceContext } from './service-schemas.gen.js';
|
|
36
37
|
/**
|
|
37
38
|
* Policy effect - permit or forbid
|
|
38
39
|
*/
|
|
@@ -52,16 +53,75 @@ export interface PolicyCondition {
|
|
|
52
53
|
/** The value to compare against */
|
|
53
54
|
value: string | number | boolean | string[];
|
|
54
55
|
}
|
|
56
|
+
/** context.field <op> value */
|
|
57
|
+
export interface ConditionComparison {
|
|
58
|
+
kind: 'comparison';
|
|
59
|
+
field: string;
|
|
60
|
+
operator: ConditionOperator;
|
|
61
|
+
value: string | number | boolean | string[];
|
|
62
|
+
}
|
|
63
|
+
/** context.field.contains(value) */
|
|
64
|
+
export interface ConditionContains {
|
|
65
|
+
kind: 'contains';
|
|
66
|
+
field: string;
|
|
67
|
+
value: string | number | boolean;
|
|
68
|
+
}
|
|
69
|
+
/** context.field like "pattern" */
|
|
70
|
+
export interface ConditionLike {
|
|
71
|
+
kind: 'like';
|
|
72
|
+
field: string;
|
|
73
|
+
pattern: string;
|
|
74
|
+
}
|
|
75
|
+
/** context has field (existence check) */
|
|
76
|
+
export interface ConditionHas {
|
|
77
|
+
kind: 'has';
|
|
78
|
+
field: string;
|
|
79
|
+
}
|
|
80
|
+
/** N-ary AND (flattened from binary && chains) */
|
|
81
|
+
export interface ConditionAnd {
|
|
82
|
+
kind: 'and';
|
|
83
|
+
children: ConditionExpression[];
|
|
84
|
+
}
|
|
85
|
+
/** N-ary OR (flattened from binary || chains) */
|
|
86
|
+
export interface ConditionOr {
|
|
87
|
+
kind: 'or';
|
|
88
|
+
children: ConditionExpression[];
|
|
89
|
+
}
|
|
90
|
+
/** Unary NOT */
|
|
91
|
+
export interface ConditionNot {
|
|
92
|
+
kind: 'not';
|
|
93
|
+
child: ConditionExpression;
|
|
94
|
+
}
|
|
95
|
+
/** Fallback for expressions that cannot be decomposed */
|
|
96
|
+
export interface ConditionRaw {
|
|
97
|
+
kind: 'raw';
|
|
98
|
+
text: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Recursive condition expression tree parsed from Cedar JSON AST.
|
|
102
|
+
* Used by Studio UI for visual condition block rendering and by
|
|
103
|
+
* explainDecision() for per-condition evaluation with actual values.
|
|
104
|
+
*/
|
|
105
|
+
export type ConditionExpression = ConditionComparison | ConditionContains | ConditionLike | ConditionHas | ConditionAnd | ConditionOr | ConditionNot | ConditionRaw;
|
|
55
106
|
/**
|
|
56
107
|
* Principal or resource entity constraint.
|
|
57
108
|
* Used to specify type-only constraints (any entity of type) or
|
|
58
109
|
* specific entity constraints (type + id).
|
|
110
|
+
*
|
|
111
|
+
* The `operator` field controls Cedar scope syntax:
|
|
112
|
+
* - `'eq'` (default): `resource == Type::"id"` — exact match
|
|
113
|
+
* - `'in'`: `resource in Type::"id"` — hierarchy match (descendants)
|
|
114
|
+
*
|
|
115
|
+
* Use `'in'` for container resource types (Account, Project, App) to match
|
|
116
|
+
* all descendant entities. Use `'eq'` for leaf types (Session, Tool).
|
|
59
117
|
*/
|
|
60
118
|
export interface PolicyEntity {
|
|
61
119
|
/** Entity type (e.g., "Agent", "Tool", "FilePath", "User") */
|
|
62
120
|
type: string;
|
|
63
121
|
/** Optional specific entity ID. If omitted, matches any entity of this type. */
|
|
64
122
|
id?: string;
|
|
123
|
+
/** Scope operator: 'eq' for exact match (==), 'in' for hierarchy match (in). Default: 'eq' */
|
|
124
|
+
operator?: 'eq' | 'in';
|
|
65
125
|
}
|
|
66
126
|
/** Alias for PolicyEntity when used as principal constraint */
|
|
67
127
|
export type PolicyPrincipal = PolicyEntity;
|
|
@@ -129,6 +189,8 @@ export interface PolicyRule {
|
|
|
129
189
|
conditions: PolicyCondition[];
|
|
130
190
|
/** Raw condition string (for advanced/complex conditions) */
|
|
131
191
|
rawCondition?: string;
|
|
192
|
+
/** Recursive condition expression tree from Cedar JSON AST */
|
|
193
|
+
conditionExpression?: ConditionExpression;
|
|
132
194
|
/** Whether this rule is active - NOT embedded in Cedar (runtime state) */
|
|
133
195
|
enabled: boolean;
|
|
134
196
|
/** Display/evaluation order - NOT embedded in Cedar (runtime state) */
|
|
@@ -163,8 +225,11 @@ export declare class Policy {
|
|
|
163
225
|
/**
|
|
164
226
|
* Convert to Cedar policy text.
|
|
165
227
|
* Uses proper Cedar @annotation syntax.
|
|
228
|
+
*
|
|
229
|
+
* @param optionalFields - Set of context field names that are optional and need `context has` guards.
|
|
230
|
+
* Use `getOptionalFields()` to compute this from service context metadata.
|
|
166
231
|
*/
|
|
167
|
-
toCedar(): string;
|
|
232
|
+
toCedar(optionalFields?: Set<string>): string;
|
|
168
233
|
/**
|
|
169
234
|
* Get JSON representation for storage
|
|
170
235
|
*/
|
|
@@ -178,10 +243,35 @@ export declare class Policy {
|
|
|
178
243
|
*/
|
|
179
244
|
getName(): string | undefined;
|
|
180
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Get the set of optional context fields for the given action(s).
|
|
248
|
+
*
|
|
249
|
+
* A field is considered optional if it has `required: false` in ANY of the
|
|
250
|
+
* targeted actions. This is the safe choice because at evaluation time,
|
|
251
|
+
* the policy could be matched against any of the specified actions.
|
|
252
|
+
*
|
|
253
|
+
* @param serviceContext - The service context metadata (from OVERWATCH_CONTEXT, etc.)
|
|
254
|
+
* @param actions - Single action name or array of action names
|
|
255
|
+
* @returns Set of field names that are optional and need `context has` guards
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* import { OVERWATCH_CONTEXT } from '@highflame/policy/types';
|
|
260
|
+
*
|
|
261
|
+
* const optionalFields = getOptionalFields(OVERWATCH_CONTEXT, 'call_tool');
|
|
262
|
+
* // Set { 'tool_name', 'mcp_server', 'threat_count', ... }
|
|
263
|
+
*
|
|
264
|
+
* const cedar = ruleToCedar(rule, optionalFields);
|
|
265
|
+
* // Conditions on optional fields auto-get `context has` guards
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
export declare function getOptionalFields(serviceContext: ServiceContext, actions: string | string[]): Set<string>;
|
|
181
269
|
/**
|
|
182
270
|
* Convert a PolicyRule to Cedar policy text with proper annotations.
|
|
183
271
|
*
|
|
184
272
|
* @param rule - The PolicyRule to convert
|
|
273
|
+
* @param optionalFields - Set of context field names that are optional and need `context has` guards.
|
|
274
|
+
* Use `getOptionalFields()` to compute this from service context metadata.
|
|
185
275
|
* @returns Cedar policy text string
|
|
186
276
|
*
|
|
187
277
|
* @example
|
|
@@ -197,35 +287,38 @@ export declare class Policy {
|
|
|
197
287
|
* order: 0,
|
|
198
288
|
* };
|
|
199
289
|
*
|
|
290
|
+
* // Without optional fields - no guards injected
|
|
200
291
|
* const cedar = ruleToCedar(rule);
|
|
201
|
-
*
|
|
202
|
-
* //
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* //
|
|
207
|
-
* // action == Action::"call_tool",
|
|
208
|
-
* // resource
|
|
209
|
-
* // )
|
|
210
|
-
* // when { context.threat_count > 0 };
|
|
292
|
+
*
|
|
293
|
+
* // With optional fields - auto-injects `context has` guards
|
|
294
|
+
* import { OVERWATCH_CONTEXT } from '@highflame/policy/types';
|
|
295
|
+
* const optionalFields = getOptionalFields(OVERWATCH_CONTEXT, 'call_tool');
|
|
296
|
+
* const cedarWithGuards = ruleToCedar(rule, optionalFields);
|
|
297
|
+
* // when { context has threat_count && context.threat_count > 0 };
|
|
211
298
|
* ```
|
|
212
299
|
*/
|
|
213
|
-
export declare function ruleToCedar(rule: PolicyRule): string;
|
|
300
|
+
export declare function ruleToCedar(rule: PolicyRule, optionalFields?: Set<string>): string;
|
|
214
301
|
/**
|
|
215
302
|
* Convert multiple PolicyRules to Cedar policy text.
|
|
216
303
|
* Only enabled rules are included, sorted by order.
|
|
217
304
|
*
|
|
218
305
|
* @param rules - Array of PolicyRules to convert
|
|
219
306
|
* @param includeDisabled - If true, include disabled rules as comments (default: false)
|
|
307
|
+
* @param optionalFields - Set of context field names that are optional and need `context has` guards.
|
|
308
|
+
* Use `getOptionalFields()` to compute this from service context metadata.
|
|
220
309
|
* @returns Cedar policy text with all rules separated by blank lines
|
|
221
310
|
*
|
|
222
311
|
* @example
|
|
223
312
|
* ```typescript
|
|
224
313
|
* const rules: PolicyRule[] = [...];
|
|
225
314
|
* const cedarText = rulesToCedar(rules);
|
|
315
|
+
*
|
|
316
|
+
* // With optional fields awareness
|
|
317
|
+
* const optionalFields = getOptionalFields(OVERWATCH_CONTEXT, 'call_tool');
|
|
318
|
+
* const cedarText = rulesToCedar(rules, false, optionalFields);
|
|
226
319
|
* ```
|
|
227
320
|
*/
|
|
228
|
-
export declare function rulesToCedar(rules: PolicyRule[], includeDisabled?: boolean): string;
|
|
321
|
+
export declare function rulesToCedar(rules: PolicyRule[], includeDisabled?: boolean, optionalFields?: Set<string>): string;
|
|
229
322
|
/**
|
|
230
323
|
* Builder for constructing Cedar policies with type safety.
|
|
231
324
|
*/
|
package/dist/builder.js
CHANGED
|
@@ -133,8 +133,11 @@ export class Policy {
|
|
|
133
133
|
/**
|
|
134
134
|
* Convert to Cedar policy text.
|
|
135
135
|
* Uses proper Cedar @annotation syntax.
|
|
136
|
+
*
|
|
137
|
+
* @param optionalFields - Set of context field names that are optional and need `context has` guards.
|
|
138
|
+
* Use `getOptionalFields()` to compute this from service context metadata.
|
|
136
139
|
*/
|
|
137
|
-
toCedar() {
|
|
140
|
+
toCedar(optionalFields) {
|
|
138
141
|
const lines = [];
|
|
139
142
|
// Generate proper Cedar annotations
|
|
140
143
|
if (this.data.id || this.data.name) {
|
|
@@ -145,7 +148,7 @@ export class Policy {
|
|
|
145
148
|
lines.push(...generateAnnotationLines(annotations));
|
|
146
149
|
}
|
|
147
150
|
// Generate policy body
|
|
148
|
-
lines.push(generatePolicyBody(this.data.effect, this.data.principal, this.data.action, this.data.resource, this.data.conditions, this.data.rawCondition));
|
|
151
|
+
lines.push(generatePolicyBody(this.data.effect, this.data.principal, this.data.action, this.data.resource, this.data.conditions, this.data.rawCondition, optionalFields));
|
|
149
152
|
return lines.join('\n');
|
|
150
153
|
}
|
|
151
154
|
/**
|
|
@@ -170,40 +173,99 @@ export class Policy {
|
|
|
170
173
|
// ============================================================================
|
|
171
174
|
// Cedar Generation Functions
|
|
172
175
|
// ============================================================================
|
|
176
|
+
/**
|
|
177
|
+
* Get the set of optional context fields for the given action(s).
|
|
178
|
+
*
|
|
179
|
+
* A field is considered optional if it has `required: false` in ANY of the
|
|
180
|
+
* targeted actions. This is the safe choice because at evaluation time,
|
|
181
|
+
* the policy could be matched against any of the specified actions.
|
|
182
|
+
*
|
|
183
|
+
* @param serviceContext - The service context metadata (from OVERWATCH_CONTEXT, etc.)
|
|
184
|
+
* @param actions - Single action name or array of action names
|
|
185
|
+
* @returns Set of field names that are optional and need `context has` guards
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* import { OVERWATCH_CONTEXT } from '@highflame/policy/types';
|
|
190
|
+
*
|
|
191
|
+
* const optionalFields = getOptionalFields(OVERWATCH_CONTEXT, 'call_tool');
|
|
192
|
+
* // Set { 'tool_name', 'mcp_server', 'threat_count', ... }
|
|
193
|
+
*
|
|
194
|
+
* const cedar = ruleToCedar(rule, optionalFields);
|
|
195
|
+
* // Conditions on optional fields auto-get `context has` guards
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export function getOptionalFields(serviceContext, actions) {
|
|
199
|
+
const actionList = Array.isArray(actions) ? actions : [actions];
|
|
200
|
+
const optionalFields = new Set();
|
|
201
|
+
for (const actionName of actionList) {
|
|
202
|
+
const action = serviceContext.actions.find(a => a.name === actionName);
|
|
203
|
+
if (action) {
|
|
204
|
+
for (const attr of action.context_attributes) {
|
|
205
|
+
if (!attr.required) {
|
|
206
|
+
optionalFields.add(attr.key);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return optionalFields;
|
|
212
|
+
}
|
|
173
213
|
/**
|
|
174
214
|
* Convert a condition to Cedar syntax.
|
|
175
215
|
* Field names are sanitized to prevent injection attacks.
|
|
216
|
+
*
|
|
217
|
+
* When `optionalFields` is provided and the condition's field is in the set,
|
|
218
|
+
* the output is wrapped with a `context has` guard:
|
|
219
|
+
* `context has field && context.field > value`
|
|
176
220
|
*/
|
|
177
|
-
function conditionToCedar(condition) {
|
|
221
|
+
function conditionToCedar(condition, optionalFields) {
|
|
178
222
|
const field = sanitizeIdentifier(condition.field, 'field');
|
|
179
223
|
const { operator, value } = condition;
|
|
180
224
|
const valueStr = valueToString(value);
|
|
225
|
+
let expr;
|
|
181
226
|
switch (operator) {
|
|
182
227
|
case 'eq':
|
|
183
|
-
|
|
228
|
+
expr = `context.${field} == ${valueStr}`;
|
|
229
|
+
break;
|
|
184
230
|
case 'neq':
|
|
185
|
-
|
|
231
|
+
expr = `context.${field} != ${valueStr}`;
|
|
232
|
+
break;
|
|
186
233
|
case 'lt':
|
|
187
|
-
|
|
234
|
+
expr = `context.${field} < ${valueStr}`;
|
|
235
|
+
break;
|
|
188
236
|
case 'lte':
|
|
189
|
-
|
|
237
|
+
expr = `context.${field} <= ${valueStr}`;
|
|
238
|
+
break;
|
|
190
239
|
case 'gt':
|
|
191
|
-
|
|
240
|
+
expr = `context.${field} > ${valueStr}`;
|
|
241
|
+
break;
|
|
192
242
|
case 'gte':
|
|
193
|
-
|
|
243
|
+
expr = `context.${field} >= ${valueStr}`;
|
|
244
|
+
break;
|
|
194
245
|
case 'contains':
|
|
195
|
-
|
|
246
|
+
expr = `context.${field}.contains(${valueStr})`;
|
|
247
|
+
break;
|
|
196
248
|
case 'in':
|
|
197
249
|
if (Array.isArray(value)) {
|
|
198
250
|
const items = value.map(v => `"${escapeCedarString(v)}"`).join(', ');
|
|
199
|
-
|
|
251
|
+
expr = `context.${field} in [${items}]`;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
expr = `context.${field} in ${valueStr}`;
|
|
200
255
|
}
|
|
201
|
-
|
|
256
|
+
break;
|
|
202
257
|
case 'like':
|
|
203
|
-
|
|
258
|
+
expr = `context.${field} like ${valueStr}`;
|
|
259
|
+
break;
|
|
204
260
|
default:
|
|
205
|
-
|
|
261
|
+
expr = `context.${field} == ${valueStr}`;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
// Auto-inject `context has` guard for optional fields
|
|
265
|
+
if (optionalFields?.has(field)) {
|
|
266
|
+
return `context has ${field} && ${expr}`;
|
|
206
267
|
}
|
|
268
|
+
return expr;
|
|
207
269
|
}
|
|
208
270
|
/**
|
|
209
271
|
* Convert a value to Cedar string representation.
|
|
@@ -225,13 +287,14 @@ function valueToString(value) {
|
|
|
225
287
|
* Generate the Cedar policy body (permit/forbid statement).
|
|
226
288
|
* All inputs are sanitized/escaped to prevent injection attacks.
|
|
227
289
|
*/
|
|
228
|
-
function generatePolicyBody(effect, principal, action, resource, conditions, rawCondition) {
|
|
290
|
+
function generatePolicyBody(effect, principal, action, resource, conditions, rawCondition, optionalFields) {
|
|
229
291
|
let policyLine = `${effect} (`;
|
|
230
292
|
// Principal
|
|
231
293
|
if (principal) {
|
|
232
294
|
const entityType = sanitizeIdentifier(principal.type, 'principal_type');
|
|
233
295
|
if (principal.id) {
|
|
234
|
-
|
|
296
|
+
const op = principal.operator === 'in' ? 'in' : '==';
|
|
297
|
+
policyLine += `\n principal ${op} ${entityType}::"${escapeCedarString(principal.id)}"`;
|
|
235
298
|
}
|
|
236
299
|
else {
|
|
237
300
|
policyLine += `\n principal is ${entityType}`;
|
|
@@ -264,7 +327,8 @@ function generatePolicyBody(effect, principal, action, resource, conditions, raw
|
|
|
264
327
|
if (resource) {
|
|
265
328
|
const entityType = sanitizeIdentifier(resource.type, 'resource_type');
|
|
266
329
|
if (resource.id) {
|
|
267
|
-
|
|
330
|
+
const op = resource.operator === 'in' ? 'in' : '==';
|
|
331
|
+
policyLine += `,\n resource ${op} ${entityType}::"${escapeCedarString(resource.id)}"`;
|
|
268
332
|
}
|
|
269
333
|
else {
|
|
270
334
|
policyLine += `,\n resource is ${entityType}`;
|
|
@@ -283,7 +347,7 @@ function generatePolicyBody(effect, principal, action, resource, conditions, raw
|
|
|
283
347
|
}
|
|
284
348
|
else if (conditions.length > 0) {
|
|
285
349
|
// Fallback to structured conditions if rawCondition is rejected
|
|
286
|
-
const conditionStr = conditions.map(c => conditionToCedar(c)).join(' && ');
|
|
350
|
+
const conditionStr = conditions.map(c => conditionToCedar(c, optionalFields)).join(' && ');
|
|
287
351
|
policyLine += `\nwhen { ${conditionStr} };`;
|
|
288
352
|
}
|
|
289
353
|
else {
|
|
@@ -291,7 +355,7 @@ function generatePolicyBody(effect, principal, action, resource, conditions, raw
|
|
|
291
355
|
}
|
|
292
356
|
}
|
|
293
357
|
else if (conditions.length > 0) {
|
|
294
|
-
const conditionStr = conditions.map(c => conditionToCedar(c)).join(' && ');
|
|
358
|
+
const conditionStr = conditions.map(c => conditionToCedar(c, optionalFields)).join(' && ');
|
|
295
359
|
policyLine += `\nwhen { ${conditionStr} };`;
|
|
296
360
|
}
|
|
297
361
|
else {
|
|
@@ -303,6 +367,8 @@ function generatePolicyBody(effect, principal, action, resource, conditions, raw
|
|
|
303
367
|
* Convert a PolicyRule to Cedar policy text with proper annotations.
|
|
304
368
|
*
|
|
305
369
|
* @param rule - The PolicyRule to convert
|
|
370
|
+
* @param optionalFields - Set of context field names that are optional and need `context has` guards.
|
|
371
|
+
* Use `getOptionalFields()` to compute this from service context metadata.
|
|
306
372
|
* @returns Cedar policy text string
|
|
307
373
|
*
|
|
308
374
|
* @example
|
|
@@ -318,25 +384,22 @@ function generatePolicyBody(effect, principal, action, resource, conditions, raw
|
|
|
318
384
|
* order: 0,
|
|
319
385
|
* };
|
|
320
386
|
*
|
|
387
|
+
* // Without optional fields - no guards injected
|
|
321
388
|
* const cedar = ruleToCedar(rule);
|
|
322
|
-
*
|
|
323
|
-
* //
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* //
|
|
328
|
-
* // action == Action::"call_tool",
|
|
329
|
-
* // resource
|
|
330
|
-
* // )
|
|
331
|
-
* // when { context.threat_count > 0 };
|
|
389
|
+
*
|
|
390
|
+
* // With optional fields - auto-injects `context has` guards
|
|
391
|
+
* import { OVERWATCH_CONTEXT } from '@highflame/policy/types';
|
|
392
|
+
* const optionalFields = getOptionalFields(OVERWATCH_CONTEXT, 'call_tool');
|
|
393
|
+
* const cedarWithGuards = ruleToCedar(rule, optionalFields);
|
|
394
|
+
* // when { context has threat_count && context.threat_count > 0 };
|
|
332
395
|
* ```
|
|
333
396
|
*/
|
|
334
|
-
export function ruleToCedar(rule) {
|
|
397
|
+
export function ruleToCedar(rule, optionalFields) {
|
|
335
398
|
const lines = [];
|
|
336
399
|
// Generate Cedar annotations
|
|
337
400
|
lines.push(...generateAnnotationLines(rule.annotations, rule.customAnnotations));
|
|
338
401
|
// Generate policy body
|
|
339
|
-
lines.push(generatePolicyBody(rule.effect, rule.principal, rule.action, rule.resource, rule.conditions, rule.rawCondition));
|
|
402
|
+
lines.push(generatePolicyBody(rule.effect, rule.principal, rule.action, rule.resource, rule.conditions, rule.rawCondition, optionalFields));
|
|
340
403
|
return lines.join('\n');
|
|
341
404
|
}
|
|
342
405
|
/**
|
|
@@ -345,24 +408,30 @@ export function ruleToCedar(rule) {
|
|
|
345
408
|
*
|
|
346
409
|
* @param rules - Array of PolicyRules to convert
|
|
347
410
|
* @param includeDisabled - If true, include disabled rules as comments (default: false)
|
|
411
|
+
* @param optionalFields - Set of context field names that are optional and need `context has` guards.
|
|
412
|
+
* Use `getOptionalFields()` to compute this from service context metadata.
|
|
348
413
|
* @returns Cedar policy text with all rules separated by blank lines
|
|
349
414
|
*
|
|
350
415
|
* @example
|
|
351
416
|
* ```typescript
|
|
352
417
|
* const rules: PolicyRule[] = [...];
|
|
353
418
|
* const cedarText = rulesToCedar(rules);
|
|
419
|
+
*
|
|
420
|
+
* // With optional fields awareness
|
|
421
|
+
* const optionalFields = getOptionalFields(OVERWATCH_CONTEXT, 'call_tool');
|
|
422
|
+
* const cedarText = rulesToCedar(rules, false, optionalFields);
|
|
354
423
|
* ```
|
|
355
424
|
*/
|
|
356
|
-
export function rulesToCedar(rules, includeDisabled = false) {
|
|
425
|
+
export function rulesToCedar(rules, includeDisabled = false, optionalFields) {
|
|
357
426
|
const sortedRules = [...rules].sort((a, b) => a.order - b.order);
|
|
358
427
|
const cedarPolicies = [];
|
|
359
428
|
for (const rule of sortedRules) {
|
|
360
429
|
if (rule.enabled) {
|
|
361
|
-
cedarPolicies.push(ruleToCedar(rule));
|
|
430
|
+
cedarPolicies.push(ruleToCedar(rule, optionalFields));
|
|
362
431
|
}
|
|
363
432
|
else if (includeDisabled) {
|
|
364
433
|
// Include disabled rules as comments
|
|
365
|
-
const cedarLines = ruleToCedar(rule).split('\n');
|
|
434
|
+
const cedarLines = ruleToCedar(rule, optionalFields).split('\n');
|
|
366
435
|
cedarPolicies.push(cedarLines.map(line => `// [DISABLED] ${line}`).join('\n'));
|
|
367
436
|
}
|
|
368
437
|
}
|