@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/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
- return `context.${field} == ${valueStr}`;
228
+ expr = `context.${field} == ${valueStr}`;
229
+ break;
184
230
  case 'neq':
185
- return `context.${field} != ${valueStr}`;
231
+ expr = `context.${field} != ${valueStr}`;
232
+ break;
186
233
  case 'lt':
187
- return `context.${field} < ${valueStr}`;
234
+ expr = `context.${field} < ${valueStr}`;
235
+ break;
188
236
  case 'lte':
189
- return `context.${field} <= ${valueStr}`;
237
+ expr = `context.${field} <= ${valueStr}`;
238
+ break;
190
239
  case 'gt':
191
- return `context.${field} > ${valueStr}`;
240
+ expr = `context.${field} > ${valueStr}`;
241
+ break;
192
242
  case 'gte':
193
- return `context.${field} >= ${valueStr}`;
243
+ expr = `context.${field} >= ${valueStr}`;
244
+ break;
194
245
  case 'contains':
195
- return `context.${field}.contains(${valueStr})`;
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
- return `context.${field} in [${items}]`;
251
+ expr = `context.${field} in [${items}]`;
252
+ }
253
+ else {
254
+ expr = `context.${field} in ${valueStr}`;
200
255
  }
201
- return `context.${field} in ${valueStr}`;
256
+ break;
202
257
  case 'like':
203
- return `context.${field} like ${valueStr}`;
258
+ expr = `context.${field} like ${valueStr}`;
259
+ break;
204
260
  default:
205
- return `context.${field} == ${valueStr}`;
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
- * // Output:
323
- * // @id("rule-001")
324
- * // @name("Block threats")
325
- * // @severity("high")
326
- * // forbid (
327
- * // principal,
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: string[];
53
+ readonly determining_policies: DeterminingPolicy[];
44
54
  readonly reason?: string;
45
- constructor(effect: "Allow" | "Deny", determining_policies: string[], reason?: string);
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. This ensures that
153
- * determining_policies in evaluation results use the @id values
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 extractPolicyIds(policyText) {
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 idMatch = policy.match(/@id\("([^"]+)"\)/);
168
- if (idMatch) {
169
- policyMap[idMatch[1]] = policy;
170
- hasAnnotationIds = true;
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 hasAnnotationIds ? policyMap : policyText;
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
- this.policySet = extractPolicyIds(policy);
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
- this.policySet = extractPolicyIds(policies.join("\n"));
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
- return new Decision(result.response.decision === "allow" ? "Allow" : "Deny", result.response.diagnostics.reason, result.response.diagnostics.errors.length > 0
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.mcp_server == "filesystem" ||
740
- context.mcp_server == "playwright"
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")