@highflame/policy 2.1.0 → 2.1.1
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/explain.d.ts +30 -2
- package/dist/explain.js +43 -16
- package/package.json +1 -1
package/dist/explain.d.ts
CHANGED
|
@@ -46,6 +46,18 @@ export interface PolicyExplanation {
|
|
|
46
46
|
/** Raw Cedar condition text if the policy uses rawCondition instead of structured conditions */
|
|
47
47
|
raw_condition?: string;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Identifies the detector that produced a context value.
|
|
51
|
+
* Used for provenance tracking — linking policy conditions back to their source detector.
|
|
52
|
+
*/
|
|
53
|
+
export interface EvidenceSource {
|
|
54
|
+
/** Detector name (e.g., "injection", "secrets", "tool_validator") */
|
|
55
|
+
detector: string;
|
|
56
|
+
/** Detector execution latency in milliseconds */
|
|
57
|
+
latency_ms?: number;
|
|
58
|
+
/** Producer-specific metadata (e.g., "tier", "phase", "priority"). Treated as opaque key-value pairs. */
|
|
59
|
+
labels?: Record<string, string>;
|
|
60
|
+
}
|
|
49
61
|
/**
|
|
50
62
|
* Result of evaluating a single condition against the request context.
|
|
51
63
|
*/
|
|
@@ -60,6 +72,15 @@ export interface ConditionResult {
|
|
|
60
72
|
actual?: unknown;
|
|
61
73
|
/** Whether this condition matched */
|
|
62
74
|
matched: boolean;
|
|
75
|
+
/** Source detector that produced this context value (when provenance is available) */
|
|
76
|
+
source?: EvidenceSource;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Options for explainDecision.
|
|
80
|
+
*/
|
|
81
|
+
export interface ExplainOptions {
|
|
82
|
+
/** Maps context field names to the detector that produced them */
|
|
83
|
+
provenance?: Record<string, EvidenceSource>;
|
|
63
84
|
}
|
|
64
85
|
/** Evaluated comparison: context.field <op> value */
|
|
65
86
|
export interface EvaluatedComparison {
|
|
@@ -69,6 +90,8 @@ export interface EvaluatedComparison {
|
|
|
69
90
|
expected: string | number | boolean | string[];
|
|
70
91
|
actual: unknown;
|
|
71
92
|
matched: boolean;
|
|
93
|
+
/** Source detector that produced this context value (when provenance is available) */
|
|
94
|
+
source?: EvidenceSource;
|
|
72
95
|
}
|
|
73
96
|
/** Evaluated contains: context.field.contains(value) */
|
|
74
97
|
export interface EvaluatedContains {
|
|
@@ -77,6 +100,8 @@ export interface EvaluatedContains {
|
|
|
77
100
|
expected: string | number | boolean;
|
|
78
101
|
actual: unknown;
|
|
79
102
|
matched: boolean;
|
|
103
|
+
/** Source detector that produced this context value (when provenance is available) */
|
|
104
|
+
source?: EvidenceSource;
|
|
80
105
|
}
|
|
81
106
|
/** Evaluated like: context.field like "pattern" */
|
|
82
107
|
export interface EvaluatedLike {
|
|
@@ -85,6 +110,8 @@ export interface EvaluatedLike {
|
|
|
85
110
|
pattern: string;
|
|
86
111
|
actual: unknown;
|
|
87
112
|
matched: boolean;
|
|
113
|
+
/** Source detector that produced this context value (when provenance is available) */
|
|
114
|
+
source?: EvidenceSource;
|
|
88
115
|
}
|
|
89
116
|
/** Evaluated has: context has field */
|
|
90
117
|
export interface EvaluatedHas {
|
|
@@ -142,9 +169,10 @@ export type EvaluatedExpression = EvaluatedComparison | EvaluatedContains | Eval
|
|
|
142
169
|
* }
|
|
143
170
|
* ```
|
|
144
171
|
*/
|
|
145
|
-
export declare function explainDecision(decision: DecisionInput, rules: PolicyRule[], context: Record<string, unknown
|
|
172
|
+
export declare function explainDecision(decision: DecisionInput, rules: PolicyRule[], context: Record<string, unknown>, options?: ExplainOptions): ExplainedDecision;
|
|
146
173
|
/**
|
|
147
174
|
* Recursively evaluate a ConditionExpression tree against a context map.
|
|
148
175
|
* Returns an EvaluatedExpression tree with `matched` booleans and `actual` values.
|
|
176
|
+
* The optional provenance map links context field names to their source detectors.
|
|
149
177
|
*/
|
|
150
|
-
export declare function evaluateExpression(expr: ConditionExpression, context: Record<string, unknown>): EvaluatedExpression;
|
|
178
|
+
export declare function evaluateExpression(expr: ConditionExpression, context: Record<string, unknown>, provenance?: Record<string, EvidenceSource>): EvaluatedExpression;
|
package/dist/explain.js
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* }
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
-
export function explainDecision(decision, rules, context) {
|
|
32
|
+
export function explainDecision(decision, rules, context, options) {
|
|
33
|
+
const provenance = options?.provenance;
|
|
33
34
|
// Build lookup map: annotations.id → PolicyRule
|
|
34
35
|
const ruleMap = new Map();
|
|
35
36
|
for (const rule of rules) {
|
|
@@ -49,7 +50,7 @@ export function explainDecision(decision, rules, context) {
|
|
|
49
50
|
// Recursive condition evaluation
|
|
50
51
|
let evaluatedExpression;
|
|
51
52
|
if (rule.conditionExpression) {
|
|
52
|
-
evaluatedExpression = evaluateExpression(rule.conditionExpression, context);
|
|
53
|
+
evaluatedExpression = evaluateExpression(rule.conditionExpression, context, provenance);
|
|
53
54
|
}
|
|
54
55
|
// Populate condition_results: prefer leaf extraction from expression tree,
|
|
55
56
|
// fall back to flat conditions[] for backward compat with simple policies
|
|
@@ -62,7 +63,11 @@ export function explainDecision(decision, rules, context) {
|
|
|
62
63
|
conditionResults = conditions.map(cond => {
|
|
63
64
|
const actual = context[cond.field];
|
|
64
65
|
const matched = evaluateCondition(cond.operator, actual, cond.value);
|
|
65
|
-
|
|
66
|
+
const result = { field: cond.field, operator: cond.operator, expected: cond.value, actual, matched };
|
|
67
|
+
if (provenance?.[cond.field]) {
|
|
68
|
+
result.source = provenance[cond.field];
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
66
71
|
});
|
|
67
72
|
}
|
|
68
73
|
// Surface rawCondition when there are no structured conditions and no expression tree
|
|
@@ -93,19 +98,20 @@ export function explainDecision(decision, rules, context) {
|
|
|
93
98
|
/**
|
|
94
99
|
* Recursively evaluate a ConditionExpression tree against a context map.
|
|
95
100
|
* Returns an EvaluatedExpression tree with `matched` booleans and `actual` values.
|
|
101
|
+
* The optional provenance map links context field names to their source detectors.
|
|
96
102
|
*/
|
|
97
|
-
export function evaluateExpression(expr, context) {
|
|
103
|
+
export function evaluateExpression(expr, context, provenance) {
|
|
98
104
|
switch (expr.kind) {
|
|
99
105
|
case 'and': {
|
|
100
|
-
const children = expr.children.map(c => evaluateExpression(c, context));
|
|
106
|
+
const children = expr.children.map(c => evaluateExpression(c, context, provenance));
|
|
101
107
|
return { kind: 'and', children, matched: children.every(c => c.matched) };
|
|
102
108
|
}
|
|
103
109
|
case 'or': {
|
|
104
|
-
const children = expr.children.map(c => evaluateExpression(c, context));
|
|
110
|
+
const children = expr.children.map(c => evaluateExpression(c, context, provenance));
|
|
105
111
|
return { kind: 'or', children, matched: children.some(c => c.matched) };
|
|
106
112
|
}
|
|
107
113
|
case 'not': {
|
|
108
|
-
const child = evaluateExpression(expr.child, context);
|
|
114
|
+
const child = evaluateExpression(expr.child, context, provenance);
|
|
109
115
|
return { kind: 'not', child, matched: !child.matched };
|
|
110
116
|
}
|
|
111
117
|
case 'has': {
|
|
@@ -115,17 +121,26 @@ export function evaluateExpression(expr, context) {
|
|
|
115
121
|
case 'comparison': {
|
|
116
122
|
const actual = context[expr.field];
|
|
117
123
|
const matched = evaluateCondition(expr.operator, actual, expr.value);
|
|
118
|
-
|
|
124
|
+
const result = { kind: 'comparison', field: expr.field, operator: expr.operator, expected: expr.value, actual, matched };
|
|
125
|
+
if (provenance?.[expr.field])
|
|
126
|
+
result.source = provenance[expr.field];
|
|
127
|
+
return result;
|
|
119
128
|
}
|
|
120
129
|
case 'contains': {
|
|
121
130
|
const actual = context[expr.field];
|
|
122
131
|
const matched = evaluateContains(actual, expr.value);
|
|
123
|
-
|
|
132
|
+
const result = { kind: 'contains', field: expr.field, expected: expr.value, actual, matched };
|
|
133
|
+
if (provenance?.[expr.field])
|
|
134
|
+
result.source = provenance[expr.field];
|
|
135
|
+
return result;
|
|
124
136
|
}
|
|
125
137
|
case 'like': {
|
|
126
138
|
const actual = context[expr.field];
|
|
127
139
|
const matched = evaluateLike(actual, expr.pattern);
|
|
128
|
-
|
|
140
|
+
const result = { kind: 'like', field: expr.field, pattern: expr.pattern, actual, matched };
|
|
141
|
+
if (provenance?.[expr.field])
|
|
142
|
+
result.source = provenance[expr.field];
|
|
143
|
+
return result;
|
|
129
144
|
}
|
|
130
145
|
case 'raw':
|
|
131
146
|
return { kind: 'raw', text: expr.text, matched: false };
|
|
@@ -296,12 +311,24 @@ function collectLeafResults(expr) {
|
|
|
296
311
|
case 'has':
|
|
297
312
|
case 'raw':
|
|
298
313
|
return [];
|
|
299
|
-
case 'comparison':
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
314
|
+
case 'comparison': {
|
|
315
|
+
const result = { field: expr.field, operator: expr.operator, expected: expr.expected, actual: expr.actual, matched: expr.matched };
|
|
316
|
+
if (expr.source)
|
|
317
|
+
result.source = expr.source;
|
|
318
|
+
return [result];
|
|
319
|
+
}
|
|
320
|
+
case 'contains': {
|
|
321
|
+
const result = { field: expr.field, operator: 'contains', expected: expr.expected, actual: expr.actual, matched: expr.matched };
|
|
322
|
+
if (expr.source)
|
|
323
|
+
result.source = expr.source;
|
|
324
|
+
return [result];
|
|
325
|
+
}
|
|
326
|
+
case 'like': {
|
|
327
|
+
const result = { field: expr.field, operator: 'like', expected: expr.pattern, actual: expr.actual, matched: expr.matched };
|
|
328
|
+
if (expr.source)
|
|
329
|
+
result.source = expr.source;
|
|
330
|
+
return [result];
|
|
331
|
+
}
|
|
305
332
|
}
|
|
306
333
|
}
|
|
307
334
|
/**
|