@sovr/engine 1.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.
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @sovr/engine — Unified Policy Engine
3
+ *
4
+ * The single decision plane that all SOVR proxies share.
5
+ * Four proxy channels (MCP, HTTP, SQL, Exec) feed into this engine,
6
+ * which evaluates rules and returns allow/deny/escalate decisions.
7
+ *
8
+ * Architecture:
9
+ * AI Agent → [MCP Proxy | HTTP Proxy | SQL Proxy | Exec Proxy]
10
+ * ↓
11
+ * SOVR Policy Engine (this)
12
+ * ↓
13
+ * External World
14
+ */
15
+ /** The channel through which the action was intercepted */
16
+ type Channel = 'mcp' | 'http' | 'sql' | 'exec';
17
+ /** Risk level classification */
18
+ type RiskLevel = 'none' | 'low' | 'medium' | 'high' | 'critical';
19
+ /** Decision verdict */
20
+ type Verdict = 'allow' | 'deny' | 'escalate';
21
+ /** A single policy rule */
22
+ interface PolicyRule {
23
+ /** Unique rule identifier */
24
+ id: string;
25
+ /** Human-readable description */
26
+ description: string;
27
+ /** Which channels this rule applies to (empty = all) */
28
+ channels: Channel[];
29
+ /** Pattern matching for action names (glob-style) */
30
+ action_pattern: string;
31
+ /** Pattern matching for resources (glob-style) */
32
+ resource_pattern: string;
33
+ /** Conditions that must be true for the rule to match */
34
+ conditions?: RuleCondition[];
35
+ /** What to do when the rule matches */
36
+ effect: Verdict;
37
+ /** Risk level assigned when this rule matches */
38
+ risk_level: RiskLevel;
39
+ /** Whether to require human approval */
40
+ require_approval: boolean;
41
+ /** Priority (higher = evaluated first) */
42
+ priority: number;
43
+ /** Whether this rule is enabled */
44
+ enabled: boolean;
45
+ }
46
+ /** A condition within a rule */
47
+ interface RuleCondition {
48
+ /** JSON path into the context object */
49
+ field: string;
50
+ /** Comparison operator */
51
+ operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'matches' | 'in' | 'not_in' | 'exists';
52
+ /** Value to compare against */
53
+ value: unknown;
54
+ }
55
+ /** Input to the policy engine for evaluation */
56
+ interface EvalRequest {
57
+ /** Which proxy channel intercepted this */
58
+ channel: Channel;
59
+ /** The action being attempted */
60
+ action: string;
61
+ /** The resource being acted upon */
62
+ resource: string;
63
+ /** Additional context (varies by channel) */
64
+ context: Record<string, unknown>;
65
+ /** Actor performing the action */
66
+ actor_id?: string;
67
+ /** Trace ID for correlation */
68
+ trace_id?: string;
69
+ }
70
+ /** Channel-specific context for HTTP proxy */
71
+ interface HttpContext extends Record<string, unknown> {
72
+ method: string;
73
+ url: string;
74
+ host: string;
75
+ path: string;
76
+ headers?: Record<string, string>;
77
+ body_preview?: string;
78
+ }
79
+ /** Channel-specific context for MCP proxy */
80
+ interface McpContext extends Record<string, unknown> {
81
+ tool_name: string;
82
+ server_name?: string;
83
+ arguments?: Record<string, unknown>;
84
+ }
85
+ /** Channel-specific context for SQL proxy */
86
+ interface SqlContext extends Record<string, unknown> {
87
+ statement_type: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'DROP' | 'ALTER' | 'TRUNCATE' | 'CREATE' | 'OTHER';
88
+ tables: string[];
89
+ has_where_clause: boolean;
90
+ raw_sql?: string;
91
+ }
92
+ /** Channel-specific context for Exec proxy */
93
+ interface ExecContext extends Record<string, unknown> {
94
+ command: string;
95
+ args: string[];
96
+ cwd?: string;
97
+ env?: Record<string, string>;
98
+ }
99
+ /** Result of policy evaluation */
100
+ interface EvalResult {
101
+ /** Unique decision ID */
102
+ decision_id: string;
103
+ /** The verdict */
104
+ verdict: Verdict;
105
+ /** Whether the action is allowed (verdict === 'allow') */
106
+ allowed: boolean;
107
+ /** Whether human approval is required */
108
+ requires_approval: boolean;
109
+ /** Human-readable reason */
110
+ reason: string;
111
+ /** Computed risk score (0-100) */
112
+ risk_score: number;
113
+ /** Which rules matched */
114
+ matched_rules: string[];
115
+ /** Risk level classification */
116
+ risk_level: RiskLevel;
117
+ /** The channel that was evaluated */
118
+ channel: Channel;
119
+ /** Trace ID */
120
+ trace_id: string;
121
+ /** Timestamp */
122
+ timestamp: number;
123
+ }
124
+ /** Engine configuration */
125
+ interface EngineConfig {
126
+ /** Policy rules */
127
+ rules: PolicyRule[];
128
+ /** Default verdict when no rules match */
129
+ default_verdict?: Verdict;
130
+ /** Whether to log all evaluations */
131
+ audit_log?: boolean;
132
+ /** Callback for audit events */
133
+ on_audit?: (event: AuditEvent) => void | Promise<void>;
134
+ }
135
+ /** Audit event emitted for every evaluation */
136
+ interface AuditEvent {
137
+ decision_id: string;
138
+ channel: Channel;
139
+ action: string;
140
+ resource: string;
141
+ verdict: Verdict;
142
+ risk_score: number;
143
+ matched_rules: string[];
144
+ actor_id?: string;
145
+ trace_id: string;
146
+ timestamp: number;
147
+ context_snapshot: Record<string, unknown>;
148
+ }
149
+ /**
150
+ * Default rules that cover the most dangerous operations across all channels.
151
+ * Users can extend or override these.
152
+ */
153
+ declare const DEFAULT_RULES: PolicyRule[];
154
+ /**
155
+ * The SOVR Policy Engine.
156
+ *
157
+ * Evaluates actions from any proxy channel against a unified rule set.
158
+ * All four proxy types (MCP, HTTP, SQL, Exec) feed into this same engine.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * import { PolicyEngine, DEFAULT_RULES } from '@sovr/engine';
163
+ *
164
+ * const engine = new PolicyEngine({
165
+ * rules: DEFAULT_RULES,
166
+ * audit_log: true,
167
+ * on_audit: (event) => console.log(event),
168
+ * });
169
+ *
170
+ * const result = engine.evaluate({
171
+ * channel: 'http',
172
+ * action: 'POST',
173
+ * resource: 'api.stripe.com/v1/charges',
174
+ * context: { method: 'POST', host: 'api.stripe.com', url: '...', path: '/v1/charges' },
175
+ * });
176
+ *
177
+ * if (!result.allowed) {
178
+ * console.log(`Blocked: ${result.reason}`);
179
+ * }
180
+ * ```
181
+ */
182
+ declare class PolicyEngine {
183
+ private rules;
184
+ private defaultVerdict;
185
+ private auditLog;
186
+ private onAudit?;
187
+ constructor(config: EngineConfig);
188
+ /**
189
+ * Evaluate an action against the policy rules.
190
+ * Returns a decision with verdict, risk score, and matched rules.
191
+ */
192
+ evaluate(request: EvalRequest): EvalResult;
193
+ /**
194
+ * Add a rule at runtime.
195
+ */
196
+ addRule(rule: PolicyRule): void;
197
+ /**
198
+ * Remove a rule by ID.
199
+ */
200
+ removeRule(ruleId: string): boolean;
201
+ /**
202
+ * Enable or disable a rule.
203
+ */
204
+ setRuleEnabled(ruleId: string, enabled: boolean): boolean;
205
+ /**
206
+ * Get all rules (for inspection/export).
207
+ */
208
+ getRules(): readonly PolicyRule[];
209
+ /**
210
+ * Load rules from a YAML/JSON policy file content.
211
+ * Expects { rules: PolicyRule[] } format.
212
+ */
213
+ loadRules(rulesData: {
214
+ rules: PolicyRule[];
215
+ }): void;
216
+ }
217
+
218
+ export { type AuditEvent, type Channel, DEFAULT_RULES, type EngineConfig, type EvalRequest, type EvalResult, type ExecContext, type HttpContext, type McpContext, PolicyEngine, type PolicyRule, type RiskLevel, type RuleCondition, type SqlContext, type Verdict, PolicyEngine as default };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * @sovr/engine — Unified Policy Engine
3
+ *
4
+ * The single decision plane that all SOVR proxies share.
5
+ * Four proxy channels (MCP, HTTP, SQL, Exec) feed into this engine,
6
+ * which evaluates rules and returns allow/deny/escalate decisions.
7
+ *
8
+ * Architecture:
9
+ * AI Agent → [MCP Proxy | HTTP Proxy | SQL Proxy | Exec Proxy]
10
+ * ↓
11
+ * SOVR Policy Engine (this)
12
+ * ↓
13
+ * External World
14
+ */
15
+ /** The channel through which the action was intercepted */
16
+ type Channel = 'mcp' | 'http' | 'sql' | 'exec';
17
+ /** Risk level classification */
18
+ type RiskLevel = 'none' | 'low' | 'medium' | 'high' | 'critical';
19
+ /** Decision verdict */
20
+ type Verdict = 'allow' | 'deny' | 'escalate';
21
+ /** A single policy rule */
22
+ interface PolicyRule {
23
+ /** Unique rule identifier */
24
+ id: string;
25
+ /** Human-readable description */
26
+ description: string;
27
+ /** Which channels this rule applies to (empty = all) */
28
+ channels: Channel[];
29
+ /** Pattern matching for action names (glob-style) */
30
+ action_pattern: string;
31
+ /** Pattern matching for resources (glob-style) */
32
+ resource_pattern: string;
33
+ /** Conditions that must be true for the rule to match */
34
+ conditions?: RuleCondition[];
35
+ /** What to do when the rule matches */
36
+ effect: Verdict;
37
+ /** Risk level assigned when this rule matches */
38
+ risk_level: RiskLevel;
39
+ /** Whether to require human approval */
40
+ require_approval: boolean;
41
+ /** Priority (higher = evaluated first) */
42
+ priority: number;
43
+ /** Whether this rule is enabled */
44
+ enabled: boolean;
45
+ }
46
+ /** A condition within a rule */
47
+ interface RuleCondition {
48
+ /** JSON path into the context object */
49
+ field: string;
50
+ /** Comparison operator */
51
+ operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'matches' | 'in' | 'not_in' | 'exists';
52
+ /** Value to compare against */
53
+ value: unknown;
54
+ }
55
+ /** Input to the policy engine for evaluation */
56
+ interface EvalRequest {
57
+ /** Which proxy channel intercepted this */
58
+ channel: Channel;
59
+ /** The action being attempted */
60
+ action: string;
61
+ /** The resource being acted upon */
62
+ resource: string;
63
+ /** Additional context (varies by channel) */
64
+ context: Record<string, unknown>;
65
+ /** Actor performing the action */
66
+ actor_id?: string;
67
+ /** Trace ID for correlation */
68
+ trace_id?: string;
69
+ }
70
+ /** Channel-specific context for HTTP proxy */
71
+ interface HttpContext extends Record<string, unknown> {
72
+ method: string;
73
+ url: string;
74
+ host: string;
75
+ path: string;
76
+ headers?: Record<string, string>;
77
+ body_preview?: string;
78
+ }
79
+ /** Channel-specific context for MCP proxy */
80
+ interface McpContext extends Record<string, unknown> {
81
+ tool_name: string;
82
+ server_name?: string;
83
+ arguments?: Record<string, unknown>;
84
+ }
85
+ /** Channel-specific context for SQL proxy */
86
+ interface SqlContext extends Record<string, unknown> {
87
+ statement_type: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'DROP' | 'ALTER' | 'TRUNCATE' | 'CREATE' | 'OTHER';
88
+ tables: string[];
89
+ has_where_clause: boolean;
90
+ raw_sql?: string;
91
+ }
92
+ /** Channel-specific context for Exec proxy */
93
+ interface ExecContext extends Record<string, unknown> {
94
+ command: string;
95
+ args: string[];
96
+ cwd?: string;
97
+ env?: Record<string, string>;
98
+ }
99
+ /** Result of policy evaluation */
100
+ interface EvalResult {
101
+ /** Unique decision ID */
102
+ decision_id: string;
103
+ /** The verdict */
104
+ verdict: Verdict;
105
+ /** Whether the action is allowed (verdict === 'allow') */
106
+ allowed: boolean;
107
+ /** Whether human approval is required */
108
+ requires_approval: boolean;
109
+ /** Human-readable reason */
110
+ reason: string;
111
+ /** Computed risk score (0-100) */
112
+ risk_score: number;
113
+ /** Which rules matched */
114
+ matched_rules: string[];
115
+ /** Risk level classification */
116
+ risk_level: RiskLevel;
117
+ /** The channel that was evaluated */
118
+ channel: Channel;
119
+ /** Trace ID */
120
+ trace_id: string;
121
+ /** Timestamp */
122
+ timestamp: number;
123
+ }
124
+ /** Engine configuration */
125
+ interface EngineConfig {
126
+ /** Policy rules */
127
+ rules: PolicyRule[];
128
+ /** Default verdict when no rules match */
129
+ default_verdict?: Verdict;
130
+ /** Whether to log all evaluations */
131
+ audit_log?: boolean;
132
+ /** Callback for audit events */
133
+ on_audit?: (event: AuditEvent) => void | Promise<void>;
134
+ }
135
+ /** Audit event emitted for every evaluation */
136
+ interface AuditEvent {
137
+ decision_id: string;
138
+ channel: Channel;
139
+ action: string;
140
+ resource: string;
141
+ verdict: Verdict;
142
+ risk_score: number;
143
+ matched_rules: string[];
144
+ actor_id?: string;
145
+ trace_id: string;
146
+ timestamp: number;
147
+ context_snapshot: Record<string, unknown>;
148
+ }
149
+ /**
150
+ * Default rules that cover the most dangerous operations across all channels.
151
+ * Users can extend or override these.
152
+ */
153
+ declare const DEFAULT_RULES: PolicyRule[];
154
+ /**
155
+ * The SOVR Policy Engine.
156
+ *
157
+ * Evaluates actions from any proxy channel against a unified rule set.
158
+ * All four proxy types (MCP, HTTP, SQL, Exec) feed into this same engine.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * import { PolicyEngine, DEFAULT_RULES } from '@sovr/engine';
163
+ *
164
+ * const engine = new PolicyEngine({
165
+ * rules: DEFAULT_RULES,
166
+ * audit_log: true,
167
+ * on_audit: (event) => console.log(event),
168
+ * });
169
+ *
170
+ * const result = engine.evaluate({
171
+ * channel: 'http',
172
+ * action: 'POST',
173
+ * resource: 'api.stripe.com/v1/charges',
174
+ * context: { method: 'POST', host: 'api.stripe.com', url: '...', path: '/v1/charges' },
175
+ * });
176
+ *
177
+ * if (!result.allowed) {
178
+ * console.log(`Blocked: ${result.reason}`);
179
+ * }
180
+ * ```
181
+ */
182
+ declare class PolicyEngine {
183
+ private rules;
184
+ private defaultVerdict;
185
+ private auditLog;
186
+ private onAudit?;
187
+ constructor(config: EngineConfig);
188
+ /**
189
+ * Evaluate an action against the policy rules.
190
+ * Returns a decision with verdict, risk score, and matched rules.
191
+ */
192
+ evaluate(request: EvalRequest): EvalResult;
193
+ /**
194
+ * Add a rule at runtime.
195
+ */
196
+ addRule(rule: PolicyRule): void;
197
+ /**
198
+ * Remove a rule by ID.
199
+ */
200
+ removeRule(ruleId: string): boolean;
201
+ /**
202
+ * Enable or disable a rule.
203
+ */
204
+ setRuleEnabled(ruleId: string, enabled: boolean): boolean;
205
+ /**
206
+ * Get all rules (for inspection/export).
207
+ */
208
+ getRules(): readonly PolicyRule[];
209
+ /**
210
+ * Load rules from a YAML/JSON policy file content.
211
+ * Expects { rules: PolicyRule[] } format.
212
+ */
213
+ loadRules(rulesData: {
214
+ rules: PolicyRule[];
215
+ }): void;
216
+ }
217
+
218
+ export { type AuditEvent, type Channel, DEFAULT_RULES, type EngineConfig, type EvalRequest, type EvalResult, type ExecContext, type HttpContext, type McpContext, PolicyEngine, type PolicyRule, type RiskLevel, type RuleCondition, type SqlContext, type Verdict, PolicyEngine as default };
package/dist/index.js ADDED
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_RULES: () => DEFAULT_RULES,
24
+ PolicyEngine: () => PolicyEngine,
25
+ default: () => index_default
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var DEFAULT_RULES = [
29
+ // --- HTTP Proxy: Dangerous outbound calls ---
30
+ {
31
+ id: "http-payment-apis",
32
+ description: "Intercept all payment API calls (Stripe, PayPal, etc.)",
33
+ channels: ["http"],
34
+ action_pattern: "*",
35
+ resource_pattern: "*",
36
+ conditions: [
37
+ { field: "host", operator: "matches", value: "(api\\.stripe\\.com|api\\.paypal\\.com|api\\.square\\.com)" },
38
+ { field: "method", operator: "in", value: ["POST", "PUT", "DELETE"] }
39
+ ],
40
+ effect: "escalate",
41
+ risk_level: "high",
42
+ require_approval: true,
43
+ priority: 100,
44
+ enabled: true
45
+ },
46
+ {
47
+ id: "http-cloud-destructive",
48
+ description: "Block destructive cloud API calls (DELETE on AWS, GCP, Azure)",
49
+ channels: ["http"],
50
+ action_pattern: "*",
51
+ resource_pattern: "*",
52
+ conditions: [
53
+ { field: "host", operator: "matches", value: "(.*\\.amazonaws\\.com|.*\\.googleapis\\.com|.*\\.azure\\.com)" },
54
+ { field: "method", operator: "eq", value: "DELETE" }
55
+ ],
56
+ effect: "escalate",
57
+ risk_level: "critical",
58
+ require_approval: true,
59
+ priority: 110,
60
+ enabled: true
61
+ },
62
+ {
63
+ id: "http-github-push",
64
+ description: "Intercept GitHub write operations",
65
+ channels: ["http"],
66
+ action_pattern: "*",
67
+ resource_pattern: "*",
68
+ conditions: [
69
+ { field: "host", operator: "eq", value: "api.github.com" },
70
+ { field: "method", operator: "in", value: ["POST", "PUT", "PATCH", "DELETE"] }
71
+ ],
72
+ effect: "escalate",
73
+ risk_level: "medium",
74
+ require_approval: false,
75
+ priority: 80,
76
+ enabled: true
77
+ },
78
+ // --- MCP Proxy: Dangerous tool calls ---
79
+ {
80
+ id: "mcp-file-write",
81
+ description: "Intercept file system write operations via MCP",
82
+ channels: ["mcp"],
83
+ action_pattern: "file_write",
84
+ resource_pattern: "*",
85
+ effect: "escalate",
86
+ risk_level: "medium",
87
+ require_approval: false,
88
+ priority: 70,
89
+ enabled: true
90
+ },
91
+ {
92
+ id: "mcp-shell-exec",
93
+ description: "Intercept shell command execution via MCP",
94
+ channels: ["mcp"],
95
+ action_pattern: "shell_exec",
96
+ resource_pattern: "*",
97
+ effect: "escalate",
98
+ risk_level: "high",
99
+ require_approval: true,
100
+ priority: 90,
101
+ enabled: true
102
+ },
103
+ {
104
+ id: "mcp-database-mutation",
105
+ description: "Intercept database mutations via MCP",
106
+ channels: ["mcp"],
107
+ action_pattern: "db_*",
108
+ resource_pattern: "*",
109
+ conditions: [
110
+ { field: "tool_name", operator: "matches", value: "(db_insert|db_update|db_delete|db_execute)" }
111
+ ],
112
+ effect: "escalate",
113
+ risk_level: "high",
114
+ require_approval: true,
115
+ priority: 85,
116
+ enabled: true
117
+ },
118
+ // --- SQL Proxy: Dangerous SQL operations ---
119
+ {
120
+ id: "sql-ddl-block",
121
+ description: "Block DDL operations (DROP, ALTER, TRUNCATE)",
122
+ channels: ["sql"],
123
+ action_pattern: "*",
124
+ resource_pattern: "*",
125
+ conditions: [
126
+ { field: "statement_type", operator: "in", value: ["DROP", "ALTER", "TRUNCATE"] }
127
+ ],
128
+ effect: "deny",
129
+ risk_level: "critical",
130
+ require_approval: true,
131
+ priority: 120,
132
+ enabled: true
133
+ },
134
+ {
135
+ id: "sql-delete-no-where",
136
+ description: "Block DELETE without WHERE clause",
137
+ channels: ["sql"],
138
+ action_pattern: "*",
139
+ resource_pattern: "*",
140
+ conditions: [
141
+ { field: "statement_type", operator: "eq", value: "DELETE" },
142
+ { field: "has_where_clause", operator: "eq", value: false }
143
+ ],
144
+ effect: "deny",
145
+ risk_level: "critical",
146
+ require_approval: true,
147
+ priority: 115,
148
+ enabled: true
149
+ },
150
+ // --- Exec Proxy: Dangerous shell commands ---
151
+ {
152
+ id: "exec-rm-rf",
153
+ description: "Block rm -rf and similar destructive commands",
154
+ channels: ["exec"],
155
+ action_pattern: "*",
156
+ resource_pattern: "*",
157
+ conditions: [
158
+ { field: "command", operator: "matches", value: "(rm|rmdir|del)" },
159
+ { field: "args", operator: "contains", value: "-rf" }
160
+ ],
161
+ effect: "deny",
162
+ risk_level: "critical",
163
+ require_approval: true,
164
+ priority: 130,
165
+ enabled: true
166
+ },
167
+ {
168
+ id: "exec-kubectl-delete",
169
+ description: "Intercept kubectl delete operations",
170
+ channels: ["exec"],
171
+ action_pattern: "*",
172
+ resource_pattern: "*",
173
+ conditions: [
174
+ { field: "command", operator: "eq", value: "kubectl" },
175
+ { field: "args", operator: "contains", value: "delete" }
176
+ ],
177
+ effect: "escalate",
178
+ risk_level: "critical",
179
+ require_approval: true,
180
+ priority: 125,
181
+ enabled: true
182
+ }
183
+ ];
184
+ var decisionCounter = 0;
185
+ function generateDecisionId() {
186
+ const ts = Date.now().toString(36);
187
+ const rand = Math.random().toString(36).substring(2, 8);
188
+ decisionCounter++;
189
+ return `dec_${ts}_${rand}_${decisionCounter}`;
190
+ }
191
+ function generateTraceId() {
192
+ const ts = Date.now().toString(36);
193
+ const rand = Math.random().toString(36).substring(2, 10);
194
+ return `trace_${ts}_${rand}`;
195
+ }
196
+ function globMatch(pattern, value) {
197
+ if (pattern === "*") return true;
198
+ const regex = new RegExp(
199
+ "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
200
+ );
201
+ return regex.test(value);
202
+ }
203
+ function evaluateCondition(condition, context) {
204
+ const fieldValue = getNestedValue(context, condition.field);
205
+ switch (condition.operator) {
206
+ case "eq":
207
+ return fieldValue === condition.value;
208
+ case "neq":
209
+ return fieldValue !== condition.value;
210
+ case "gt":
211
+ return typeof fieldValue === "number" && fieldValue > condition.value;
212
+ case "gte":
213
+ return typeof fieldValue === "number" && fieldValue >= condition.value;
214
+ case "lt":
215
+ return typeof fieldValue === "number" && fieldValue < condition.value;
216
+ case "lte":
217
+ return typeof fieldValue === "number" && fieldValue <= condition.value;
218
+ case "contains":
219
+ if (Array.isArray(fieldValue)) {
220
+ return fieldValue.includes(condition.value);
221
+ }
222
+ if (typeof fieldValue === "string") {
223
+ return fieldValue.includes(condition.value);
224
+ }
225
+ return false;
226
+ case "matches":
227
+ if (typeof fieldValue !== "string") return false;
228
+ try {
229
+ return new RegExp(condition.value).test(fieldValue);
230
+ } catch {
231
+ return false;
232
+ }
233
+ case "in":
234
+ if (!Array.isArray(condition.value)) return false;
235
+ return condition.value.includes(fieldValue);
236
+ case "not_in":
237
+ if (!Array.isArray(condition.value)) return true;
238
+ return !condition.value.includes(fieldValue);
239
+ case "exists":
240
+ return fieldValue !== void 0 && fieldValue !== null;
241
+ default:
242
+ return false;
243
+ }
244
+ }
245
+ function getNestedValue(obj, path) {
246
+ const parts = path.split(".");
247
+ let current = obj;
248
+ for (const part of parts) {
249
+ if (current === null || current === void 0) return void 0;
250
+ current = current[part];
251
+ }
252
+ return current;
253
+ }
254
+ var RISK_SCORES = {
255
+ none: 0,
256
+ low: 20,
257
+ medium: 45,
258
+ high: 70,
259
+ critical: 95
260
+ };
261
+ var PolicyEngine = class {
262
+ rules;
263
+ defaultVerdict;
264
+ auditLog;
265
+ onAudit;
266
+ constructor(config) {
267
+ this.rules = config.rules.map((r) => ({ ...r, conditions: r.conditions ? [...r.conditions] : void 0 })).sort((a, b) => b.priority - a.priority);
268
+ this.defaultVerdict = config.default_verdict ?? "allow";
269
+ this.auditLog = config.audit_log ?? false;
270
+ this.onAudit = config.on_audit;
271
+ }
272
+ /**
273
+ * Evaluate an action against the policy rules.
274
+ * Returns a decision with verdict, risk score, and matched rules.
275
+ */
276
+ evaluate(request) {
277
+ const traceId = request.trace_id || generateTraceId();
278
+ const decisionId = generateDecisionId();
279
+ const matchedRules = [];
280
+ for (const rule of this.rules) {
281
+ if (!rule.enabled) continue;
282
+ if (rule.channels.length > 0 && !rule.channels.includes(request.channel)) {
283
+ continue;
284
+ }
285
+ if (!globMatch(rule.action_pattern, request.action)) {
286
+ continue;
287
+ }
288
+ if (!globMatch(rule.resource_pattern, request.resource)) {
289
+ continue;
290
+ }
291
+ if (rule.conditions && rule.conditions.length > 0) {
292
+ const allMatch = rule.conditions.every(
293
+ (c) => evaluateCondition(c, request.context)
294
+ );
295
+ if (!allMatch) continue;
296
+ }
297
+ matchedRules.push(rule);
298
+ }
299
+ let verdict = this.defaultVerdict;
300
+ let riskLevel = "none";
301
+ let requiresApproval = false;
302
+ let reason = "No matching rules \u2014 default policy applied";
303
+ if (matchedRules.length > 0) {
304
+ const topRule = matchedRules[0];
305
+ verdict = topRule.effect;
306
+ riskLevel = topRule.risk_level;
307
+ requiresApproval = topRule.require_approval;
308
+ reason = `Matched rule: ${topRule.id} \u2014 ${topRule.description}`;
309
+ }
310
+ const riskScore = RISK_SCORES[riskLevel];
311
+ const allowed = verdict === "allow" || verdict === "escalate";
312
+ const result = {
313
+ decision_id: decisionId,
314
+ verdict,
315
+ allowed,
316
+ requires_approval: requiresApproval,
317
+ reason,
318
+ risk_score: riskScore,
319
+ matched_rules: matchedRules.map((r) => r.id),
320
+ risk_level: riskLevel,
321
+ channel: request.channel,
322
+ trace_id: traceId,
323
+ timestamp: Date.now()
324
+ };
325
+ if (this.auditLog && this.onAudit) {
326
+ const event = {
327
+ decision_id: decisionId,
328
+ channel: request.channel,
329
+ action: request.action,
330
+ resource: request.resource,
331
+ verdict,
332
+ risk_score: riskScore,
333
+ matched_rules: matchedRules.map((r) => r.id),
334
+ actor_id: request.actor_id,
335
+ trace_id: traceId,
336
+ timestamp: Date.now(),
337
+ context_snapshot: { ...request.context }
338
+ };
339
+ Promise.resolve(this.onAudit(event)).catch(() => {
340
+ });
341
+ }
342
+ return result;
343
+ }
344
+ /**
345
+ * Add a rule at runtime.
346
+ */
347
+ addRule(rule) {
348
+ this.rules.push(rule);
349
+ this.rules.sort((a, b) => b.priority - a.priority);
350
+ }
351
+ /**
352
+ * Remove a rule by ID.
353
+ */
354
+ removeRule(ruleId) {
355
+ const idx = this.rules.findIndex((r) => r.id === ruleId);
356
+ if (idx === -1) return false;
357
+ this.rules.splice(idx, 1);
358
+ return true;
359
+ }
360
+ /**
361
+ * Enable or disable a rule.
362
+ */
363
+ setRuleEnabled(ruleId, enabled) {
364
+ const rule = this.rules.find((r) => r.id === ruleId);
365
+ if (!rule) return false;
366
+ rule.enabled = enabled;
367
+ return true;
368
+ }
369
+ /**
370
+ * Get all rules (for inspection/export).
371
+ */
372
+ getRules() {
373
+ return this.rules;
374
+ }
375
+ /**
376
+ * Load rules from a YAML/JSON policy file content.
377
+ * Expects { rules: PolicyRule[] } format.
378
+ */
379
+ loadRules(rulesData) {
380
+ this.rules = [...rulesData.rules].sort((a, b) => b.priority - a.priority);
381
+ }
382
+ };
383
+ var index_default = PolicyEngine;
384
+ // Annotate the CommonJS export names for ESM import in node:
385
+ 0 && (module.exports = {
386
+ DEFAULT_RULES,
387
+ PolicyEngine
388
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,362 @@
1
+ // src/index.ts
2
+ var DEFAULT_RULES = [
3
+ // --- HTTP Proxy: Dangerous outbound calls ---
4
+ {
5
+ id: "http-payment-apis",
6
+ description: "Intercept all payment API calls (Stripe, PayPal, etc.)",
7
+ channels: ["http"],
8
+ action_pattern: "*",
9
+ resource_pattern: "*",
10
+ conditions: [
11
+ { field: "host", operator: "matches", value: "(api\\.stripe\\.com|api\\.paypal\\.com|api\\.square\\.com)" },
12
+ { field: "method", operator: "in", value: ["POST", "PUT", "DELETE"] }
13
+ ],
14
+ effect: "escalate",
15
+ risk_level: "high",
16
+ require_approval: true,
17
+ priority: 100,
18
+ enabled: true
19
+ },
20
+ {
21
+ id: "http-cloud-destructive",
22
+ description: "Block destructive cloud API calls (DELETE on AWS, GCP, Azure)",
23
+ channels: ["http"],
24
+ action_pattern: "*",
25
+ resource_pattern: "*",
26
+ conditions: [
27
+ { field: "host", operator: "matches", value: "(.*\\.amazonaws\\.com|.*\\.googleapis\\.com|.*\\.azure\\.com)" },
28
+ { field: "method", operator: "eq", value: "DELETE" }
29
+ ],
30
+ effect: "escalate",
31
+ risk_level: "critical",
32
+ require_approval: true,
33
+ priority: 110,
34
+ enabled: true
35
+ },
36
+ {
37
+ id: "http-github-push",
38
+ description: "Intercept GitHub write operations",
39
+ channels: ["http"],
40
+ action_pattern: "*",
41
+ resource_pattern: "*",
42
+ conditions: [
43
+ { field: "host", operator: "eq", value: "api.github.com" },
44
+ { field: "method", operator: "in", value: ["POST", "PUT", "PATCH", "DELETE"] }
45
+ ],
46
+ effect: "escalate",
47
+ risk_level: "medium",
48
+ require_approval: false,
49
+ priority: 80,
50
+ enabled: true
51
+ },
52
+ // --- MCP Proxy: Dangerous tool calls ---
53
+ {
54
+ id: "mcp-file-write",
55
+ description: "Intercept file system write operations via MCP",
56
+ channels: ["mcp"],
57
+ action_pattern: "file_write",
58
+ resource_pattern: "*",
59
+ effect: "escalate",
60
+ risk_level: "medium",
61
+ require_approval: false,
62
+ priority: 70,
63
+ enabled: true
64
+ },
65
+ {
66
+ id: "mcp-shell-exec",
67
+ description: "Intercept shell command execution via MCP",
68
+ channels: ["mcp"],
69
+ action_pattern: "shell_exec",
70
+ resource_pattern: "*",
71
+ effect: "escalate",
72
+ risk_level: "high",
73
+ require_approval: true,
74
+ priority: 90,
75
+ enabled: true
76
+ },
77
+ {
78
+ id: "mcp-database-mutation",
79
+ description: "Intercept database mutations via MCP",
80
+ channels: ["mcp"],
81
+ action_pattern: "db_*",
82
+ resource_pattern: "*",
83
+ conditions: [
84
+ { field: "tool_name", operator: "matches", value: "(db_insert|db_update|db_delete|db_execute)" }
85
+ ],
86
+ effect: "escalate",
87
+ risk_level: "high",
88
+ require_approval: true,
89
+ priority: 85,
90
+ enabled: true
91
+ },
92
+ // --- SQL Proxy: Dangerous SQL operations ---
93
+ {
94
+ id: "sql-ddl-block",
95
+ description: "Block DDL operations (DROP, ALTER, TRUNCATE)",
96
+ channels: ["sql"],
97
+ action_pattern: "*",
98
+ resource_pattern: "*",
99
+ conditions: [
100
+ { field: "statement_type", operator: "in", value: ["DROP", "ALTER", "TRUNCATE"] }
101
+ ],
102
+ effect: "deny",
103
+ risk_level: "critical",
104
+ require_approval: true,
105
+ priority: 120,
106
+ enabled: true
107
+ },
108
+ {
109
+ id: "sql-delete-no-where",
110
+ description: "Block DELETE without WHERE clause",
111
+ channels: ["sql"],
112
+ action_pattern: "*",
113
+ resource_pattern: "*",
114
+ conditions: [
115
+ { field: "statement_type", operator: "eq", value: "DELETE" },
116
+ { field: "has_where_clause", operator: "eq", value: false }
117
+ ],
118
+ effect: "deny",
119
+ risk_level: "critical",
120
+ require_approval: true,
121
+ priority: 115,
122
+ enabled: true
123
+ },
124
+ // --- Exec Proxy: Dangerous shell commands ---
125
+ {
126
+ id: "exec-rm-rf",
127
+ description: "Block rm -rf and similar destructive commands",
128
+ channels: ["exec"],
129
+ action_pattern: "*",
130
+ resource_pattern: "*",
131
+ conditions: [
132
+ { field: "command", operator: "matches", value: "(rm|rmdir|del)" },
133
+ { field: "args", operator: "contains", value: "-rf" }
134
+ ],
135
+ effect: "deny",
136
+ risk_level: "critical",
137
+ require_approval: true,
138
+ priority: 130,
139
+ enabled: true
140
+ },
141
+ {
142
+ id: "exec-kubectl-delete",
143
+ description: "Intercept kubectl delete operations",
144
+ channels: ["exec"],
145
+ action_pattern: "*",
146
+ resource_pattern: "*",
147
+ conditions: [
148
+ { field: "command", operator: "eq", value: "kubectl" },
149
+ { field: "args", operator: "contains", value: "delete" }
150
+ ],
151
+ effect: "escalate",
152
+ risk_level: "critical",
153
+ require_approval: true,
154
+ priority: 125,
155
+ enabled: true
156
+ }
157
+ ];
158
+ var decisionCounter = 0;
159
+ function generateDecisionId() {
160
+ const ts = Date.now().toString(36);
161
+ const rand = Math.random().toString(36).substring(2, 8);
162
+ decisionCounter++;
163
+ return `dec_${ts}_${rand}_${decisionCounter}`;
164
+ }
165
+ function generateTraceId() {
166
+ const ts = Date.now().toString(36);
167
+ const rand = Math.random().toString(36).substring(2, 10);
168
+ return `trace_${ts}_${rand}`;
169
+ }
170
+ function globMatch(pattern, value) {
171
+ if (pattern === "*") return true;
172
+ const regex = new RegExp(
173
+ "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
174
+ );
175
+ return regex.test(value);
176
+ }
177
+ function evaluateCondition(condition, context) {
178
+ const fieldValue = getNestedValue(context, condition.field);
179
+ switch (condition.operator) {
180
+ case "eq":
181
+ return fieldValue === condition.value;
182
+ case "neq":
183
+ return fieldValue !== condition.value;
184
+ case "gt":
185
+ return typeof fieldValue === "number" && fieldValue > condition.value;
186
+ case "gte":
187
+ return typeof fieldValue === "number" && fieldValue >= condition.value;
188
+ case "lt":
189
+ return typeof fieldValue === "number" && fieldValue < condition.value;
190
+ case "lte":
191
+ return typeof fieldValue === "number" && fieldValue <= condition.value;
192
+ case "contains":
193
+ if (Array.isArray(fieldValue)) {
194
+ return fieldValue.includes(condition.value);
195
+ }
196
+ if (typeof fieldValue === "string") {
197
+ return fieldValue.includes(condition.value);
198
+ }
199
+ return false;
200
+ case "matches":
201
+ if (typeof fieldValue !== "string") return false;
202
+ try {
203
+ return new RegExp(condition.value).test(fieldValue);
204
+ } catch {
205
+ return false;
206
+ }
207
+ case "in":
208
+ if (!Array.isArray(condition.value)) return false;
209
+ return condition.value.includes(fieldValue);
210
+ case "not_in":
211
+ if (!Array.isArray(condition.value)) return true;
212
+ return !condition.value.includes(fieldValue);
213
+ case "exists":
214
+ return fieldValue !== void 0 && fieldValue !== null;
215
+ default:
216
+ return false;
217
+ }
218
+ }
219
+ function getNestedValue(obj, path) {
220
+ const parts = path.split(".");
221
+ let current = obj;
222
+ for (const part of parts) {
223
+ if (current === null || current === void 0) return void 0;
224
+ current = current[part];
225
+ }
226
+ return current;
227
+ }
228
+ var RISK_SCORES = {
229
+ none: 0,
230
+ low: 20,
231
+ medium: 45,
232
+ high: 70,
233
+ critical: 95
234
+ };
235
+ var PolicyEngine = class {
236
+ rules;
237
+ defaultVerdict;
238
+ auditLog;
239
+ onAudit;
240
+ constructor(config) {
241
+ this.rules = config.rules.map((r) => ({ ...r, conditions: r.conditions ? [...r.conditions] : void 0 })).sort((a, b) => b.priority - a.priority);
242
+ this.defaultVerdict = config.default_verdict ?? "allow";
243
+ this.auditLog = config.audit_log ?? false;
244
+ this.onAudit = config.on_audit;
245
+ }
246
+ /**
247
+ * Evaluate an action against the policy rules.
248
+ * Returns a decision with verdict, risk score, and matched rules.
249
+ */
250
+ evaluate(request) {
251
+ const traceId = request.trace_id || generateTraceId();
252
+ const decisionId = generateDecisionId();
253
+ const matchedRules = [];
254
+ for (const rule of this.rules) {
255
+ if (!rule.enabled) continue;
256
+ if (rule.channels.length > 0 && !rule.channels.includes(request.channel)) {
257
+ continue;
258
+ }
259
+ if (!globMatch(rule.action_pattern, request.action)) {
260
+ continue;
261
+ }
262
+ if (!globMatch(rule.resource_pattern, request.resource)) {
263
+ continue;
264
+ }
265
+ if (rule.conditions && rule.conditions.length > 0) {
266
+ const allMatch = rule.conditions.every(
267
+ (c) => evaluateCondition(c, request.context)
268
+ );
269
+ if (!allMatch) continue;
270
+ }
271
+ matchedRules.push(rule);
272
+ }
273
+ let verdict = this.defaultVerdict;
274
+ let riskLevel = "none";
275
+ let requiresApproval = false;
276
+ let reason = "No matching rules \u2014 default policy applied";
277
+ if (matchedRules.length > 0) {
278
+ const topRule = matchedRules[0];
279
+ verdict = topRule.effect;
280
+ riskLevel = topRule.risk_level;
281
+ requiresApproval = topRule.require_approval;
282
+ reason = `Matched rule: ${topRule.id} \u2014 ${topRule.description}`;
283
+ }
284
+ const riskScore = RISK_SCORES[riskLevel];
285
+ const allowed = verdict === "allow" || verdict === "escalate";
286
+ const result = {
287
+ decision_id: decisionId,
288
+ verdict,
289
+ allowed,
290
+ requires_approval: requiresApproval,
291
+ reason,
292
+ risk_score: riskScore,
293
+ matched_rules: matchedRules.map((r) => r.id),
294
+ risk_level: riskLevel,
295
+ channel: request.channel,
296
+ trace_id: traceId,
297
+ timestamp: Date.now()
298
+ };
299
+ if (this.auditLog && this.onAudit) {
300
+ const event = {
301
+ decision_id: decisionId,
302
+ channel: request.channel,
303
+ action: request.action,
304
+ resource: request.resource,
305
+ verdict,
306
+ risk_score: riskScore,
307
+ matched_rules: matchedRules.map((r) => r.id),
308
+ actor_id: request.actor_id,
309
+ trace_id: traceId,
310
+ timestamp: Date.now(),
311
+ context_snapshot: { ...request.context }
312
+ };
313
+ Promise.resolve(this.onAudit(event)).catch(() => {
314
+ });
315
+ }
316
+ return result;
317
+ }
318
+ /**
319
+ * Add a rule at runtime.
320
+ */
321
+ addRule(rule) {
322
+ this.rules.push(rule);
323
+ this.rules.sort((a, b) => b.priority - a.priority);
324
+ }
325
+ /**
326
+ * Remove a rule by ID.
327
+ */
328
+ removeRule(ruleId) {
329
+ const idx = this.rules.findIndex((r) => r.id === ruleId);
330
+ if (idx === -1) return false;
331
+ this.rules.splice(idx, 1);
332
+ return true;
333
+ }
334
+ /**
335
+ * Enable or disable a rule.
336
+ */
337
+ setRuleEnabled(ruleId, enabled) {
338
+ const rule = this.rules.find((r) => r.id === ruleId);
339
+ if (!rule) return false;
340
+ rule.enabled = enabled;
341
+ return true;
342
+ }
343
+ /**
344
+ * Get all rules (for inspection/export).
345
+ */
346
+ getRules() {
347
+ return this.rules;
348
+ }
349
+ /**
350
+ * Load rules from a YAML/JSON policy file content.
351
+ * Expects { rules: PolicyRule[] } format.
352
+ */
353
+ loadRules(rulesData) {
354
+ this.rules = [...rulesData.rules].sort((a, b) => b.priority - a.priority);
355
+ }
356
+ };
357
+ var index_default = PolicyEngine;
358
+ export {
359
+ DEFAULT_RULES,
360
+ PolicyEngine,
361
+ index_default as default
362
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@sovr/engine",
3
+ "version": "1.1.0",
4
+ "description": "Unified Policy Engine for SOVR — the single decision plane for all proxy channels",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
21
+ "test": "vitest run --config vitest.config.ts",
22
+ "lint": "tsc --noEmit",
23
+ "prepublishOnly": "pnpm build"
24
+ },
25
+ "keywords": [
26
+ "sovr",
27
+ "policy-engine",
28
+ "ai-firewall",
29
+ "proxy",
30
+ "rules-engine"
31
+ ],
32
+ "author": "SOVR Inc. <sdk@sovr.inc>",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/xie38388/sovr"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.0.0",
40
+ "tsup": "^8.0.0",
41
+ "typescript": "^5.0.0",
42
+ "vitest": "^1.0.0"
43
+ }
44
+ }