@highflame/policy 2.0.9 → 2.0.10
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 +54 -54
- package/_schemas/overwatch/schema.cedarschema +77 -68
- package/dist/builder.d.ts +45 -13
- package/dist/builder.js +99 -32
- package/dist/engine.d.ts +20 -2
- package/dist/engine.js +50 -20
- package/dist/overwatch-defaults.gen.js +24 -2
- package/dist/service-schemas.gen.d.ts +5 -11
- package/dist/service-schemas.gen.js +172 -83
- package/package.json +1 -1
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,7 +287,7 @@ 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) {
|
|
@@ -283,7 +345,7 @@ function generatePolicyBody(effect, principal, action, resource, conditions, raw
|
|
|
283
345
|
}
|
|
284
346
|
else if (conditions.length > 0) {
|
|
285
347
|
// Fallback to structured conditions if rawCondition is rejected
|
|
286
|
-
const conditionStr = conditions.map(c => conditionToCedar(c)).join(' && ');
|
|
348
|
+
const conditionStr = conditions.map(c => conditionToCedar(c, optionalFields)).join(' && ');
|
|
287
349
|
policyLine += `\nwhen { ${conditionStr} };`;
|
|
288
350
|
}
|
|
289
351
|
else {
|
|
@@ -291,7 +353,7 @@ function generatePolicyBody(effect, principal, action, resource, conditions, raw
|
|
|
291
353
|
}
|
|
292
354
|
}
|
|
293
355
|
else if (conditions.length > 0) {
|
|
294
|
-
const conditionStr = conditions.map(c => conditionToCedar(c)).join(' && ');
|
|
356
|
+
const conditionStr = conditions.map(c => conditionToCedar(c, optionalFields)).join(' && ');
|
|
295
357
|
policyLine += `\nwhen { ${conditionStr} };`;
|
|
296
358
|
}
|
|
297
359
|
else {
|
|
@@ -303,6 +365,8 @@ function generatePolicyBody(effect, principal, action, resource, conditions, raw
|
|
|
303
365
|
* Convert a PolicyRule to Cedar policy text with proper annotations.
|
|
304
366
|
*
|
|
305
367
|
* @param rule - The PolicyRule to convert
|
|
368
|
+
* @param optionalFields - Set of context field names that are optional and need `context has` guards.
|
|
369
|
+
* Use `getOptionalFields()` to compute this from service context metadata.
|
|
306
370
|
* @returns Cedar policy text string
|
|
307
371
|
*
|
|
308
372
|
* @example
|
|
@@ -318,25 +382,22 @@ function generatePolicyBody(effect, principal, action, resource, conditions, raw
|
|
|
318
382
|
* order: 0,
|
|
319
383
|
* };
|
|
320
384
|
*
|
|
385
|
+
* // Without optional fields - no guards injected
|
|
321
386
|
* const cedar = ruleToCedar(rule);
|
|
322
|
-
*
|
|
323
|
-
* //
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* //
|
|
328
|
-
* // action == Action::"call_tool",
|
|
329
|
-
* // resource
|
|
330
|
-
* // )
|
|
331
|
-
* // when { context.threat_count > 0 };
|
|
387
|
+
*
|
|
388
|
+
* // With optional fields - auto-injects `context has` guards
|
|
389
|
+
* import { OVERWATCH_CONTEXT } from '@highflame/policy/types';
|
|
390
|
+
* const optionalFields = getOptionalFields(OVERWATCH_CONTEXT, 'call_tool');
|
|
391
|
+
* const cedarWithGuards = ruleToCedar(rule, optionalFields);
|
|
392
|
+
* // when { context has threat_count && context.threat_count > 0 };
|
|
332
393
|
* ```
|
|
333
394
|
*/
|
|
334
|
-
export function ruleToCedar(rule) {
|
|
395
|
+
export function ruleToCedar(rule, optionalFields) {
|
|
335
396
|
const lines = [];
|
|
336
397
|
// Generate Cedar annotations
|
|
337
398
|
lines.push(...generateAnnotationLines(rule.annotations, rule.customAnnotations));
|
|
338
399
|
// Generate policy body
|
|
339
|
-
lines.push(generatePolicyBody(rule.effect, rule.principal, rule.action, rule.resource, rule.conditions, rule.rawCondition));
|
|
400
|
+
lines.push(generatePolicyBody(rule.effect, rule.principal, rule.action, rule.resource, rule.conditions, rule.rawCondition, optionalFields));
|
|
340
401
|
return lines.join('\n');
|
|
341
402
|
}
|
|
342
403
|
/**
|
|
@@ -345,24 +406,30 @@ export function ruleToCedar(rule) {
|
|
|
345
406
|
*
|
|
346
407
|
* @param rules - Array of PolicyRules to convert
|
|
347
408
|
* @param includeDisabled - If true, include disabled rules as comments (default: false)
|
|
409
|
+
* @param optionalFields - Set of context field names that are optional and need `context has` guards.
|
|
410
|
+
* Use `getOptionalFields()` to compute this from service context metadata.
|
|
348
411
|
* @returns Cedar policy text with all rules separated by blank lines
|
|
349
412
|
*
|
|
350
413
|
* @example
|
|
351
414
|
* ```typescript
|
|
352
415
|
* const rules: PolicyRule[] = [...];
|
|
353
416
|
* const cedarText = rulesToCedar(rules);
|
|
417
|
+
*
|
|
418
|
+
* // With optional fields awareness
|
|
419
|
+
* const optionalFields = getOptionalFields(OVERWATCH_CONTEXT, 'call_tool');
|
|
420
|
+
* const cedarText = rulesToCedar(rules, false, optionalFields);
|
|
354
421
|
* ```
|
|
355
422
|
*/
|
|
356
|
-
export function rulesToCedar(rules, includeDisabled = false) {
|
|
423
|
+
export function rulesToCedar(rules, includeDisabled = false, optionalFields) {
|
|
357
424
|
const sortedRules = [...rules].sort((a, b) => a.order - b.order);
|
|
358
425
|
const cedarPolicies = [];
|
|
359
426
|
for (const rule of sortedRules) {
|
|
360
427
|
if (rule.enabled) {
|
|
361
|
-
cedarPolicies.push(ruleToCedar(rule));
|
|
428
|
+
cedarPolicies.push(ruleToCedar(rule, optionalFields));
|
|
362
429
|
}
|
|
363
430
|
else if (includeDisabled) {
|
|
364
431
|
// Include disabled rules as comments
|
|
365
|
-
const cedarLines = ruleToCedar(rule).split('\n');
|
|
432
|
+
const cedarLines = ruleToCedar(rule, optionalFields).split('\n');
|
|
366
433
|
cedarPolicies.push(cedarLines.map(line => `// [DISABLED] ${line}`).join('\n'));
|
|
367
434
|
}
|
|
368
435
|
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -38,11 +38,21 @@ export interface EngineOptions {
|
|
|
38
38
|
export declare class InputValidationError extends Error {
|
|
39
39
|
constructor(message: string);
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A policy that contributed to the authorization decision,
|
|
43
|
+
* enriched with its Cedar annotations.
|
|
44
|
+
*/
|
|
45
|
+
export interface DeterminingPolicy {
|
|
46
|
+
/** Policy ID (from @id annotation or positional fallback) */
|
|
47
|
+
id: string;
|
|
48
|
+
/** All annotations from this policy as key-value pairs */
|
|
49
|
+
annotations: Record<string, string>;
|
|
50
|
+
}
|
|
41
51
|
export declare class Decision {
|
|
42
52
|
readonly effect: "Allow" | "Deny";
|
|
43
|
-
readonly determining_policies:
|
|
53
|
+
readonly determining_policies: DeterminingPolicy[];
|
|
44
54
|
readonly reason?: string;
|
|
45
|
-
constructor(effect: "Allow" | "Deny", determining_policies:
|
|
55
|
+
constructor(effect: "Allow" | "Deny", determining_policies: DeterminingPolicy[], reason?: string);
|
|
46
56
|
isAllowed(): boolean;
|
|
47
57
|
isDenied(): boolean;
|
|
48
58
|
}
|
|
@@ -63,6 +73,7 @@ export interface EvaluateRequest {
|
|
|
63
73
|
*/
|
|
64
74
|
export declare class PolicyEngine {
|
|
65
75
|
private policySet;
|
|
76
|
+
private policyAnnotations;
|
|
66
77
|
private schema;
|
|
67
78
|
private options;
|
|
68
79
|
private limits;
|
|
@@ -74,11 +85,13 @@ export declare class PolicyEngine {
|
|
|
74
85
|
/**
|
|
75
86
|
* Load a single Cedar policy text string.
|
|
76
87
|
* Uses @id annotations as policy IDs when available.
|
|
88
|
+
* Stores all annotations per policy for enriching evaluation results.
|
|
77
89
|
*/
|
|
78
90
|
loadPolicy(policy: string): void;
|
|
79
91
|
/**
|
|
80
92
|
* Load multiple Cedar policy texts (concatenated with newlines).
|
|
81
93
|
* Uses @id annotations as policy IDs when available.
|
|
94
|
+
* Stores all annotations per policy for enriching evaluation results.
|
|
82
95
|
*/
|
|
83
96
|
loadPolicies(policies: string[]): void;
|
|
84
97
|
/**
|
|
@@ -89,6 +102,11 @@ export declare class PolicyEngine {
|
|
|
89
102
|
* Load schema from a Cedar schema file.
|
|
90
103
|
*/
|
|
91
104
|
loadSchemaFromFile(path: string): void;
|
|
105
|
+
/**
|
|
106
|
+
* Returns stored annotations for a given policy ID.
|
|
107
|
+
* Returns undefined if the policy ID is not found.
|
|
108
|
+
*/
|
|
109
|
+
getPolicyAnnotations(policyId: string): Record<string, string> | undefined;
|
|
92
110
|
/**
|
|
93
111
|
* Evaluate a policy request and return a decision.
|
|
94
112
|
* @throws InputValidationError if context validation fails
|
package/dist/engine.js
CHANGED
|
@@ -147,39 +147,51 @@ function parseActionString(action) {
|
|
|
147
147
|
}
|
|
148
148
|
return { type: actionType, id: actionId };
|
|
149
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Regex to extract Cedar annotations from policy text.
|
|
152
|
+
* Matches `@key("value")` with proper escaped-quote handling.
|
|
153
|
+
*/
|
|
154
|
+
const ANNOTATION_REGEX = /@(\w+)\("((?:[^"\\]|\\.)*)"\)/g;
|
|
155
|
+
/**
|
|
156
|
+
* Extract all `@key("value")` annotations from a single Cedar policy text string.
|
|
157
|
+
*/
|
|
158
|
+
function extractAnnotationsFromText(policyText) {
|
|
159
|
+
const annotations = {};
|
|
160
|
+
let match;
|
|
161
|
+
const regex = new RegExp(ANNOTATION_REGEX.source, ANNOTATION_REGEX.flags);
|
|
162
|
+
while ((match = regex.exec(policyText)) !== null) {
|
|
163
|
+
// Unescape the annotation value (reverse Cedar escaping)
|
|
164
|
+
annotations[match[1]] = match[2].replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
165
|
+
}
|
|
166
|
+
return annotations;
|
|
167
|
+
}
|
|
150
168
|
/**
|
|
151
169
|
* Extract @id annotations from Cedar policy text and return a
|
|
152
|
-
* Record<PolicyId, Policy> for cedar-wasm
|
|
153
|
-
*
|
|
154
|
-
* instead of positional IDs (policy0, policy1...).
|
|
155
|
-
*
|
|
156
|
-
* Falls back to the raw string when no @id annotations are found.
|
|
170
|
+
* Record<PolicyId, Policy> for cedar-wasm, along with a map of
|
|
171
|
+
* all annotations per policy for enriching evaluation results.
|
|
157
172
|
*/
|
|
158
|
-
function
|
|
173
|
+
function extractPolicies(policyText) {
|
|
174
|
+
const annotationsMap = new Map();
|
|
159
175
|
const parts = cedar.policySetTextToParts(policyText);
|
|
160
176
|
if (parts.type !== "success" || parts.policies.length === 0) {
|
|
161
|
-
return policyText;
|
|
177
|
+
return { policySet: policyText, annotations: annotationsMap };
|
|
162
178
|
}
|
|
163
179
|
const policyMap = {};
|
|
164
|
-
let hasAnnotationIds = false;
|
|
165
180
|
for (let i = 0; i < parts.policies.length; i++) {
|
|
166
181
|
const policy = parts.policies[i];
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
policyMap[`policy${i}`] = policy;
|
|
174
|
-
}
|
|
182
|
+
const policyAnnotations = extractAnnotationsFromText(policy);
|
|
183
|
+
const policyId = policyAnnotations["id"] || `policy${i}`;
|
|
184
|
+
policyMap[policyId] = policy;
|
|
185
|
+
annotationsMap.set(policyId, policyAnnotations);
|
|
175
186
|
}
|
|
176
|
-
return
|
|
187
|
+
return { policySet: policyMap, annotations: annotationsMap };
|
|
177
188
|
}
|
|
178
189
|
/**
|
|
179
190
|
* PolicyEngine wraps cedar-wasm with Highflame schema types.
|
|
180
191
|
*/
|
|
181
192
|
export class PolicyEngine {
|
|
182
193
|
policySet = "";
|
|
194
|
+
policyAnnotations = new Map();
|
|
183
195
|
schema;
|
|
184
196
|
options;
|
|
185
197
|
limits;
|
|
@@ -203,16 +215,22 @@ export class PolicyEngine {
|
|
|
203
215
|
/**
|
|
204
216
|
* Load a single Cedar policy text string.
|
|
205
217
|
* Uses @id annotations as policy IDs when available.
|
|
218
|
+
* Stores all annotations per policy for enriching evaluation results.
|
|
206
219
|
*/
|
|
207
220
|
loadPolicy(policy) {
|
|
208
|
-
|
|
221
|
+
const extracted = extractPolicies(policy);
|
|
222
|
+
this.policySet = extracted.policySet;
|
|
223
|
+
this.policyAnnotations = extracted.annotations;
|
|
209
224
|
}
|
|
210
225
|
/**
|
|
211
226
|
* Load multiple Cedar policy texts (concatenated with newlines).
|
|
212
227
|
* Uses @id annotations as policy IDs when available.
|
|
228
|
+
* Stores all annotations per policy for enriching evaluation results.
|
|
213
229
|
*/
|
|
214
230
|
loadPolicies(policies) {
|
|
215
|
-
|
|
231
|
+
const extracted = extractPolicies(policies.join("\n"));
|
|
232
|
+
this.policySet = extracted.policySet;
|
|
233
|
+
this.policyAnnotations = extracted.annotations;
|
|
216
234
|
}
|
|
217
235
|
/**
|
|
218
236
|
* Load schema from a Cedar schema string.
|
|
@@ -227,6 +245,13 @@ export class PolicyEngine {
|
|
|
227
245
|
const content = fs.readFileSync(path, "utf-8");
|
|
228
246
|
this.loadSchema(content);
|
|
229
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Returns stored annotations for a given policy ID.
|
|
250
|
+
* Returns undefined if the policy ID is not found.
|
|
251
|
+
*/
|
|
252
|
+
getPolicyAnnotations(policyId) {
|
|
253
|
+
return this.policyAnnotations.get(policyId);
|
|
254
|
+
}
|
|
230
255
|
/**
|
|
231
256
|
* Evaluate a policy request and return a decision.
|
|
232
257
|
* @throws InputValidationError if context validation fails
|
|
@@ -276,7 +301,12 @@ export class PolicyEngine {
|
|
|
276
301
|
if (result.type === "failure") {
|
|
277
302
|
return new Decision("Deny", [], result.errors.map(e => e.message).join("; "));
|
|
278
303
|
}
|
|
279
|
-
|
|
304
|
+
// Build enriched DeterminingPolicy objects with annotations
|
|
305
|
+
const determiningPolicies = result.response.diagnostics.reason.map(id => ({
|
|
306
|
+
id,
|
|
307
|
+
annotations: this.policyAnnotations.get(id) || {},
|
|
308
|
+
}));
|
|
309
|
+
return new Decision(result.response.decision === "allow" ? "Allow" : "Deny", determiningPolicies, result.response.diagnostics.errors.length > 0
|
|
280
310
|
? result.response.diagnostics.errors.map(e => e.error.message).join("; ")
|
|
281
311
|
: undefined);
|
|
282
312
|
}
|
|
@@ -59,6 +59,7 @@ const OVERWATCH_SECRETS_DEFAULT_CEDAR = `// ====================================
|
|
|
59
59
|
@description("Block prompts when YARA scanners detect API keys, tokens, or credential patterns")
|
|
60
60
|
@severity("critical")
|
|
61
61
|
@tags("secrets,credentials,prompts,nist-sc-28,nist-ia-5")
|
|
62
|
+
@reject_message("Your prompt was blocked because it contains detected secrets such as API keys, tokens, or credentials. Remove all secrets before resubmitting.")
|
|
62
63
|
forbid (
|
|
63
64
|
principal,
|
|
64
65
|
action == Overwatch::Action::"process_prompt",
|
|
@@ -74,6 +75,7 @@ when {
|
|
|
74
75
|
@description("Prevent file reads and tool execution when secrets or credentials are detected in content")
|
|
75
76
|
@severity("high")
|
|
76
77
|
@tags("secrets,file-access,tools,credentials,nist-sc-28")
|
|
78
|
+
@reject_message("This operation was blocked because secrets or credentials were detected in the content. File reads and tool calls are restricted when credential exposure is identified.")
|
|
77
79
|
forbid (
|
|
78
80
|
principal,
|
|
79
81
|
action in [Overwatch::Action::"read_file", Overwatch::Action::"call_tool"],
|
|
@@ -93,6 +95,7 @@ when {
|
|
|
93
95
|
@description("Block access to .env files that commonly contain secrets, API keys, and database credentials")
|
|
94
96
|
@severity("high")
|
|
95
97
|
@tags("secrets,env-files,config,nist-sc-28,mitre-t1552")
|
|
98
|
+
@reject_message("Access to .env files is blocked because they commonly contain secrets, API keys, and database credentials. Use a secrets manager instead of .env files.")
|
|
96
99
|
forbid (
|
|
97
100
|
principal,
|
|
98
101
|
action in [Overwatch::Action::"read_file", Overwatch::Action::"write_file", Overwatch::Action::"call_tool"],
|
|
@@ -113,6 +116,7 @@ when {
|
|
|
113
116
|
@description("Detect and block AWS access key IDs (AKIA prefix) in AI responses to prevent credential exfiltration")
|
|
114
117
|
@severity("critical")
|
|
115
118
|
@tags("secrets,aws,credentials,response-scan,nist-ia-5,mitre-t1552")
|
|
119
|
+
@reject_message("This response was blocked because an AWS access key ID (AKIA prefix) was detected. AWS credentials must never be exposed in AI responses.")
|
|
116
120
|
forbid (
|
|
117
121
|
principal,
|
|
118
122
|
action,
|
|
@@ -129,6 +133,7 @@ when {
|
|
|
129
133
|
@description("Detect and block AWS secret access keys in AI responses")
|
|
130
134
|
@severity("critical")
|
|
131
135
|
@tags("secrets,aws,credentials,response-scan,nist-ia-5")
|
|
136
|
+
@reject_message("This response was blocked because an AWS secret access key was detected. AWS credentials must never be exposed in AI responses.")
|
|
132
137
|
forbid (
|
|
133
138
|
principal,
|
|
134
139
|
action,
|
|
@@ -146,6 +151,7 @@ when {
|
|
|
146
151
|
@description("Detect and block GitHub personal access tokens (ghp_), fine-grained tokens (github_pat_), and app tokens (ghs_)")
|
|
147
152
|
@severity("critical")
|
|
148
153
|
@tags("secrets,github,tokens,response-scan,mitre-t1552")
|
|
154
|
+
@reject_message("This response was blocked because a GitHub token (personal access token, fine-grained token, or app token) was detected. GitHub tokens must never be exposed in AI responses.")
|
|
149
155
|
forbid (
|
|
150
156
|
principal,
|
|
151
157
|
action,
|
|
@@ -164,6 +170,7 @@ when {
|
|
|
164
170
|
@description("Detect and block SSH, RSA, and OpenSSH private keys in AI responses")
|
|
165
171
|
@severity("critical")
|
|
166
172
|
@tags("secrets,ssh,private-keys,response-scan,nist-sc-28,mitre-t1552")
|
|
173
|
+
@reject_message("This response was blocked because a private key (SSH, RSA, or OpenSSH) was detected. Private keys must never be exposed in AI responses.")
|
|
167
174
|
forbid (
|
|
168
175
|
principal,
|
|
169
176
|
action,
|
|
@@ -187,6 +194,7 @@ when {
|
|
|
187
194
|
@description("Block content flagged by YARA rules for credential exposure, API key leaks, JWT tokens, and bearer tokens")
|
|
188
195
|
@severity("critical")
|
|
189
196
|
@tags("secrets,yara,credentials,jwt,bearer,nist-ia-5")
|
|
197
|
+
@reject_message("This content was blocked because YARA scanning detected credential patterns including secret exposure, credential leaks, API keys, JWT tokens, or bearer tokens.")
|
|
190
198
|
forbid (
|
|
191
199
|
principal,
|
|
192
200
|
action,
|
|
@@ -219,6 +227,7 @@ const OVERWATCH_PII_DEFAULT_CEDAR = `// ========================================
|
|
|
219
227
|
@description("Detect and block content containing credit card number patterns (PCI DSS compliance)")
|
|
220
228
|
@severity("critical")
|
|
221
229
|
@tags("pci,credit-card,payment,compliance,pci-dss-3.4")
|
|
230
|
+
@reject_message("Your prompt was blocked because credit card number patterns were detected. Sharing payment card data violates PCI DSS requirements.")
|
|
222
231
|
forbid (
|
|
223
232
|
principal,
|
|
224
233
|
action == Overwatch::Action::"process_prompt",
|
|
@@ -234,6 +243,7 @@ when {
|
|
|
234
243
|
@description("Detect and block content containing SSN patterns (XXX-XX-XXXX format)")
|
|
235
244
|
@severity("critical")
|
|
236
245
|
@tags("ssn,identity,privacy,compliance")
|
|
246
|
+
@reject_message("Your prompt was blocked because Social Security Number patterns (XXX-XX-XXXX) were detected. SSNs are protected personal identifiers that must not be shared.")
|
|
237
247
|
forbid (
|
|
238
248
|
principal,
|
|
239
249
|
action == Overwatch::Action::"process_prompt",
|
|
@@ -249,6 +259,7 @@ when {
|
|
|
249
259
|
@description("Block content when PII-related threat categories are detected by YARA or Javelin scanners")
|
|
250
260
|
@severity("high")
|
|
251
261
|
@tags("pii,privacy,data-protection,gdpr")
|
|
262
|
+
@reject_message("Your prompt was blocked because personally identifiable information was detected by threat scanners. Remove all PII before resubmitting.")
|
|
252
263
|
forbid (
|
|
253
264
|
principal,
|
|
254
265
|
action == Overwatch::Action::"process_prompt",
|
|
@@ -280,6 +291,7 @@ when {
|
|
|
280
291
|
@description("Prevent tool execution when PII patterns are detected in content")
|
|
281
292
|
@severity("high")
|
|
282
293
|
@tags("pii,tools,data-protection")
|
|
294
|
+
@reject_message("Tool execution was blocked because personally identifiable information was detected in the content. PII must be removed before tool calls are permitted.")
|
|
283
295
|
forbid (
|
|
284
296
|
principal,
|
|
285
297
|
action == Overwatch::Action::"call_tool",
|
|
@@ -308,6 +320,7 @@ const OVERWATCH_SEMANTIC_DEFAULT_CEDAR = `// ===================================
|
|
|
308
320
|
@description("Detect and block prompt injection patterns in user input via YARA scanning (OWASP LLM01)")
|
|
309
321
|
@severity("critical")
|
|
310
322
|
@tags("injection,security,llm,owasp-llm01,baseline")
|
|
323
|
+
@reject_message("Your prompt was blocked because prompt injection patterns were detected by YARA scanning. This is a security measure to prevent manipulation of AI agent behavior.")
|
|
311
324
|
forbid (
|
|
312
325
|
principal,
|
|
313
326
|
action == Overwatch::Action::"process_prompt",
|
|
@@ -339,6 +352,7 @@ when {
|
|
|
339
352
|
@description("Detect and block jailbreak and bypass attempts against AI agents (OWASP LLM02)")
|
|
340
353
|
@severity("critical")
|
|
341
354
|
@tags("jailbreak,bypass,security,owasp-llm02,baseline")
|
|
355
|
+
@reject_message("Your prompt was blocked because jailbreak or bypass patterns were detected by YARA scanning. This is a security measure to prevent circumvention of AI safety controls.")
|
|
342
356
|
forbid (
|
|
343
357
|
principal,
|
|
344
358
|
action == Overwatch::Action::"process_prompt",
|
|
@@ -370,6 +384,7 @@ when {
|
|
|
370
384
|
@description("Block prompts when semantic threat scanners detect high severity issues (severity >= 3)")
|
|
371
385
|
@severity("high")
|
|
372
386
|
@tags("semantic,severity,security")
|
|
387
|
+
@reject_message("Your prompt was blocked because semantic threat scanners detected high severity issues in the content. Review your prompt for manipulative or adversarial patterns.")
|
|
373
388
|
forbid (
|
|
374
389
|
principal,
|
|
375
390
|
action == Overwatch::Action::"process_prompt",
|
|
@@ -387,6 +402,7 @@ when {
|
|
|
387
402
|
@description("Block all content when any scanner detects critical severity threats")
|
|
388
403
|
@severity("critical")
|
|
389
404
|
@tags("critical,baseline,security")
|
|
405
|
+
@reject_message("Your prompt was blocked because security scanners detected a critical-severity threat. This content cannot be processed.")
|
|
390
406
|
forbid (
|
|
391
407
|
principal,
|
|
392
408
|
action == Overwatch::Action::"process_prompt",
|
|
@@ -402,6 +418,7 @@ when {
|
|
|
402
418
|
@description("Prevent tool execution when prompt injection patterns are detected in content")
|
|
403
419
|
@severity("critical")
|
|
404
420
|
@tags("injection,tools,security,owasp-llm01")
|
|
421
|
+
@reject_message("Tool execution was blocked because prompt injection patterns were detected in the content by YARA scanning.")
|
|
405
422
|
forbid (
|
|
406
423
|
principal,
|
|
407
424
|
action == Overwatch::Action::"call_tool",
|
|
@@ -435,6 +452,7 @@ const OVERWATCH_TOOLS_DEFAULT_CEDAR = `// ======================================
|
|
|
435
452
|
@description("Block direct shell, bash, and command execution tools to prevent command injection (MITRE T1059)")
|
|
436
453
|
@severity("critical")
|
|
437
454
|
@tags("shell,command-injection,execution,nist-cm-7,mitre-t1059,baseline")
|
|
455
|
+
@reject_message("Tool execution was blocked because direct shell and command execution tools (shell, bash, terminal, system.exec) are restricted to prevent command injection attacks.")
|
|
438
456
|
forbid (
|
|
439
457
|
principal,
|
|
440
458
|
action == Overwatch::Action::"call_tool",
|
|
@@ -456,6 +474,7 @@ when {
|
|
|
456
474
|
@description("Block file deletion and other destructive tool operations to prevent data loss")
|
|
457
475
|
@severity("high")
|
|
458
476
|
@tags("file,delete,destructive,nist-ac-3")
|
|
477
|
+
@reject_message("Tool execution was blocked because destructive file operations (delete, rmdir, unlink) are restricted to prevent data loss.")
|
|
459
478
|
forbid (
|
|
460
479
|
principal,
|
|
461
480
|
action == Overwatch::Action::"call_tool",
|
|
@@ -478,6 +497,7 @@ when {
|
|
|
478
497
|
@description("Prevent access to system directories, credential files, SSH keys, and cloud config (MITRE T1005, T1552.001)")
|
|
479
498
|
@severity("high")
|
|
480
499
|
@tags("file,path,system,security,nist-ac-6,mitre-t1005")
|
|
500
|
+
@reject_message("Access to this path was blocked because it targets a sensitive system directory or credential file (/etc, /proc, /sys, .ssh, .aws, .gnupg, or private key files).")
|
|
481
501
|
forbid (
|
|
482
502
|
principal,
|
|
483
503
|
action in [Overwatch::Action::"read_file", Overwatch::Action::"write_file", Overwatch::Action::"call_tool"],
|
|
@@ -508,6 +528,7 @@ when {
|
|
|
508
528
|
@description("Prevent tool execution when high or critical severity threats are detected in content")
|
|
509
529
|
@severity("high")
|
|
510
530
|
@tags("tools,threats,severity,security")
|
|
531
|
+
@reject_message("Tool execution was blocked because high or critical severity threats were detected in the content by security scanners.")
|
|
511
532
|
forbid (
|
|
512
533
|
principal,
|
|
513
534
|
action == Overwatch::Action::"call_tool",
|
|
@@ -736,8 +757,9 @@ permit (
|
|
|
736
757
|
resource
|
|
737
758
|
)
|
|
738
759
|
when {
|
|
739
|
-
context
|
|
740
|
-
context.mcp_server == "
|
|
760
|
+
context has mcp_server &&
|
|
761
|
+
(context.mcp_server == "filesystem" ||
|
|
762
|
+
context.mcp_server == "playwright")
|
|
741
763
|
};
|
|
742
764
|
|
|
743
765
|
@id("mcp-allowlist-deny")
|