@highflame/policy 2.0.6 → 2.0.8

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/src/parser.ts CHANGED
@@ -119,7 +119,14 @@ export function parseCedarToRules(cedarText: string): ParseResult {
119
119
 
120
120
  const policy = jsonResult.json as CedarPolicyJSON;
121
121
  const policyId = policy.annotations?.id || `policy${index}`;
122
- const conversion = cedarJsonToRule(policy, policyId, index, policyText);
122
+
123
+ // Get engine-serialized Cedar text for rawCondition extraction.
124
+ // Using cedar-wasm's policyToText ensures we get the official engine's
125
+ // representation rather than relying on our own text extraction.
126
+ const engineTextResult = cedar.policyToText(jsonResult.json);
127
+ const engineText = engineTextResult.type === "success" ? engineTextResult.text : policyText;
128
+
129
+ const conversion = cedarJsonToRule(policy, policyId, index, engineText);
123
130
 
124
131
  if (conversion.error) {
125
132
  result.errors.push(`Policy ${policyId}: ${conversion.error}`);
@@ -207,7 +214,7 @@ function cedarJsonToRule(
207
214
  };
208
215
 
209
216
  // Map conditions
210
- const { conditions, rawCondition } = mapConditions(policy.conditions);
217
+ const { conditions, rawCondition } = mapConditions(policy.conditions, originalText);
211
218
  rule.conditions = conditions;
212
219
  if (rawCondition) {
213
220
  rule.rawCondition = rawCondition;
@@ -325,6 +332,20 @@ function mapScopeToEntity(scope: CedarScopeConstraint, field: string): PolicyEnt
325
332
  *
326
333
  * Throws ParserError for malformed constraints to prevent silent misinterpretation.
327
334
  */
335
+ /**
336
+ * Format action entity reference, preserving namespace if present.
337
+ *
338
+ * If entity type is just "Action", returns just the id (e.g., "process_prompt").
339
+ * If entity type has a namespace (e.g., "Overwatch::Action"),
340
+ * returns the fully qualified action string (e.g., 'Overwatch::Action::"process_prompt"').
341
+ */
342
+ function formatActionEntity(entity: { type: string; id: string }): string {
343
+ if (entity.type !== "Action") {
344
+ return `${entity.type}::"${entity.id}"`;
345
+ }
346
+ return entity.id;
347
+ }
348
+
328
349
  function mapActionScope(scope: CedarActionConstraint): string | string[] {
329
350
  if (scope.op === "All") {
330
351
  return "*";
@@ -333,19 +354,19 @@ function mapActionScope(scope: CedarActionConstraint): string | string[] {
333
354
  if (scope.op === "==") {
334
355
  if ("entity" in scope) {
335
356
  const entity = normalizeEntityRef(scope.entity);
336
- return entity.id;
357
+ return formatActionEntity(entity);
337
358
  }
338
359
  throw ParserError.actionMissingEntity("==");
339
360
  }
340
361
 
341
362
  if (scope.op === "in") {
342
363
  if ("entities" in scope) {
343
- const actions = scope.entities.map(e => normalizeEntityRef(e).id);
364
+ const actions = scope.entities.map(e => formatActionEntity(normalizeEntityRef(e)));
344
365
  return actions.length === 1 ? actions[0] : actions;
345
366
  }
346
367
  if ("entity" in scope) {
347
368
  const entity = normalizeEntityRef(scope.entity);
348
- return entity.id;
369
+ return formatActionEntity(entity);
349
370
  }
350
371
  throw ParserError.actionMissingEntities();
351
372
  }
@@ -354,14 +375,16 @@ function mapActionScope(scope: CedarActionConstraint): string | string[] {
354
375
  }
355
376
 
356
377
  /**
357
- * Map Cedar conditions to PolicyCondition array
378
+ * Map Cedar conditions to PolicyCondition array.
379
+ * When conditions can't be mapped to structured format, extract the raw Cedar
380
+ * condition text from the engine-serialized policy text (not JSON AST).
358
381
  */
359
- function mapConditions(conditions: CedarCondition[]): {
382
+ function mapConditions(conditions: CedarCondition[], originalText?: string): {
360
383
  conditions: PolicyCondition[];
361
384
  rawCondition?: string;
362
385
  } {
363
386
  const result: PolicyCondition[] = [];
364
- const rawParts: string[] = [];
387
+ let hasUnmapped = false;
365
388
 
366
389
  for (const cond of conditions) {
367
390
  if (cond.kind !== "when") {
@@ -372,18 +395,49 @@ function mapConditions(conditions: CedarCondition[]): {
372
395
  if (parsed.condition) {
373
396
  result.push(parsed.condition);
374
397
  } else if (parsed.raw) {
375
- rawParts.push(parsed.raw);
398
+ hasUnmapped = true;
376
399
  }
377
400
  }
378
401
 
379
- // Store raw conditions as a valid JSON array instead of joining with " && "
380
- // This ensures downstream systems can parse the rawCondition field
402
+ // Extract readable Cedar condition text instead of storing JSON AST
403
+ let rawCondition: string | undefined;
404
+ if (hasUnmapped && originalText) {
405
+ rawCondition = extractWhenClause(originalText);
406
+ }
407
+
381
408
  return {
382
409
  conditions: result,
383
- rawCondition: rawParts.length > 0 ? `[${rawParts.join(",")}]` : undefined,
410
+ rawCondition: rawCondition || undefined,
384
411
  };
385
412
  }
386
413
 
414
+ /**
415
+ * Extract the readable condition text from a Cedar policy's when clause.
416
+ * Given: `forbid (...)\nwhen { context.path like "/etc/*" };`
417
+ * Returns: `context.path like "/etc/*"`
418
+ */
419
+ function extractWhenClause(cedarText: string): string {
420
+ const whenPrefix = "when {";
421
+ const idx = cedarText.indexOf(whenPrefix);
422
+ if (idx < 0) {
423
+ return "";
424
+ }
425
+
426
+ const body = cedarText.substring(idx + whenPrefix.length);
427
+ let depth = 1;
428
+ for (let i = 0; i < body.length; i++) {
429
+ if (body[i] === "{") depth++;
430
+ if (body[i] === "}") {
431
+ depth--;
432
+ if (depth === 0) {
433
+ return body.substring(0, i).trim();
434
+ }
435
+ }
436
+ }
437
+
438
+ return body.trim();
439
+ }
440
+
387
441
  /**
388
442
  * Cedar expression types (subset used for condition mapping)
389
443
  */
@@ -216,6 +216,22 @@ describe('Service-Specific Schemas', () => {
216
216
  max_threat_severity: 1,
217
217
  contains_secrets: false,
218
218
  response_content: '',
219
+ // Trust/Safety scores
220
+ violence_score: 0,
221
+ weapons_score: 0,
222
+ hate_speech_score: 0,
223
+ crime_score: 0,
224
+ sexual_score: 0,
225
+ profanity_score: 0,
226
+ // Detector confidence
227
+ pii_confidence: 0,
228
+ injection_confidence: 0,
229
+ jailbreak_confidence: 0,
230
+ // Agent security
231
+ tool_poisoning_score: 0,
232
+ rug_pull_score: 0,
233
+ indirect_injection_score: 0,
234
+ mcp_server_verified: false,
219
235
  },
220
236
  entities,
221
237
  });
@@ -397,6 +413,22 @@ describe('Service-Specific Schemas', () => {
397
413
  max_threat_severity: 2,
398
414
  contains_secrets: false,
399
415
  response_content: '',
416
+ // Trust/Safety scores
417
+ violence_score: 0,
418
+ weapons_score: 0,
419
+ hate_speech_score: 0,
420
+ crime_score: 0,
421
+ sexual_score: 0,
422
+ profanity_score: 0,
423
+ // Detector confidence
424
+ pii_confidence: 0,
425
+ injection_confidence: 0,
426
+ jailbreak_confidence: 0,
427
+ // Agent security
428
+ tool_poisoning_score: 0,
429
+ rug_pull_score: 0,
430
+ indirect_injection_score: 0,
431
+ mcp_server_verified: false,
400
432
  },
401
433
  entities,
402
434
  });