@oculisecurity/cli 0.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.
Files changed (85) hide show
  1. package/LICENSE.txt +201 -0
  2. package/README.md +67 -0
  3. package/dist/cli.d.ts +18 -0
  4. package/dist/cli.js +565 -0
  5. package/dist/commands/init.d.ts +14 -0
  6. package/dist/commands/init.js +135 -0
  7. package/dist/commands/report.d.ts +33 -0
  8. package/dist/commands/report.js +145 -0
  9. package/dist/commands/serve.d.ts +27 -0
  10. package/dist/commands/serve.js +163 -0
  11. package/dist/commands/tail.d.ts +7 -0
  12. package/dist/commands/tail.js +211 -0
  13. package/dist/commands/uninstall.d.ts +13 -0
  14. package/dist/commands/uninstall.js +111 -0
  15. package/dist/config.d.ts +17 -0
  16. package/dist/config.js +90 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +35 -0
  19. package/dist/init.d.ts +9 -0
  20. package/dist/init.js +50 -0
  21. package/dist/install/claude-code.d.ts +13 -0
  22. package/dist/install/claude-code.js +118 -0
  23. package/dist/install/cursor.d.ts +13 -0
  24. package/dist/install/cursor.js +119 -0
  25. package/dist/install/detect.d.ts +5 -0
  26. package/dist/install/detect.js +64 -0
  27. package/dist/middleware/auth.d.ts +15 -0
  28. package/dist/middleware/auth.js +116 -0
  29. package/dist/routes/adapters/claude-code.d.ts +38 -0
  30. package/dist/routes/adapters/claude-code.js +125 -0
  31. package/dist/routes/adapters/cursor.d.ts +21 -0
  32. package/dist/routes/adapters/cursor.js +139 -0
  33. package/dist/routes/adapters/index.d.ts +16 -0
  34. package/dist/routes/adapters/index.js +56 -0
  35. package/dist/routes/adapters/router.d.ts +31 -0
  36. package/dist/routes/adapters/router.js +97 -0
  37. package/dist/routes/adapters/schema.d.ts +141 -0
  38. package/dist/routes/adapters/schema.js +83 -0
  39. package/dist/routes/adapters/windsurf.d.ts +6 -0
  40. package/dist/routes/adapters/windsurf.js +48 -0
  41. package/dist/routes/admin.d.ts +15 -0
  42. package/dist/routes/admin.js +399 -0
  43. package/dist/routes/call.d.ts +13 -0
  44. package/dist/routes/call.js +68 -0
  45. package/dist/routes/events.d.ts +7 -0
  46. package/dist/routes/events.js +125 -0
  47. package/dist/routes/health.d.ts +2 -0
  48. package/dist/routes/health.js +12 -0
  49. package/dist/routes/hooks.d.ts +11 -0
  50. package/dist/routes/hooks.js +166 -0
  51. package/dist/routes/mcp.d.ts +10 -0
  52. package/dist/routes/mcp.js +170 -0
  53. package/dist/routes/openai-tools.d.ts +9 -0
  54. package/dist/routes/openai-tools.js +121 -0
  55. package/dist/server.d.ts +11 -0
  56. package/dist/server.js +118 -0
  57. package/dist/services/audit.d.ts +92 -0
  58. package/dist/services/audit.js +388 -0
  59. package/dist/services/data-dir.d.ts +7 -0
  60. package/dist/services/data-dir.js +61 -0
  61. package/dist/services/local-policy-templates.d.ts +9 -0
  62. package/dist/services/local-policy-templates.js +47 -0
  63. package/dist/services/local-policy.d.ts +39 -0
  64. package/dist/services/local-policy.js +172 -0
  65. package/dist/services/policy-store.d.ts +82 -0
  66. package/dist/services/policy-store.js +331 -0
  67. package/dist/services/policy.d.ts +8 -0
  68. package/dist/services/policy.js +126 -0
  69. package/dist/services/ratelimit.d.ts +26 -0
  70. package/dist/services/ratelimit.js +60 -0
  71. package/dist/services/sanitizer.d.ts +9 -0
  72. package/dist/services/sanitizer.js +73 -0
  73. package/dist/services/sqlite-loader.d.ts +4 -0
  74. package/dist/services/sqlite-loader.js +16 -0
  75. package/dist/services/telemetry-log.d.ts +76 -0
  76. package/dist/services/telemetry-log.js +260 -0
  77. package/dist/services/tool-executor.d.ts +46 -0
  78. package/dist/services/tool-executor.js +167 -0
  79. package/dist/services/upstream.d.ts +18 -0
  80. package/dist/services/upstream.js +72 -0
  81. package/dist/types.d.ts +112 -0
  82. package/dist/types.js +3 -0
  83. package/package.json +72 -0
  84. package/public/favicon.svg +4 -0
  85. package/public/index.html +3893 -0
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.findPolicyFile = findPolicyFile;
40
+ exports.loadPolicyFile = loadPolicyFile;
41
+ exports.ruleMatches = ruleMatches;
42
+ exports.evaluateLocalPolicy = evaluateLocalPolicy;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const os = __importStar(require("os"));
46
+ const yaml_1 = __importDefault(require("yaml"));
47
+ // ---------------------------------------------------------------------------
48
+ // File discovery
49
+ // ---------------------------------------------------------------------------
50
+ /**
51
+ * Search for `.oculi/policy.yaml` starting from `startDir`, walking up to
52
+ * the filesystem root, then falling back to `~/.oculi/policy.yaml`.
53
+ * Returns the first path found, or null.
54
+ */
55
+ function findPolicyFile(startDir) {
56
+ let dir = startDir ?? process.cwd();
57
+ // Walk up from cwd
58
+ while (true) {
59
+ const candidate = path.join(dir, '.oculi', 'policy.yaml');
60
+ if (fs.existsSync(candidate))
61
+ return candidate;
62
+ const parent = path.dirname(dir);
63
+ if (parent === dir)
64
+ break; // reached filesystem root
65
+ dir = parent;
66
+ }
67
+ // Global fallback
68
+ const globalPath = path.join(os.homedir(), '.oculi', 'policy.yaml');
69
+ if (fs.existsSync(globalPath))
70
+ return globalPath;
71
+ return null;
72
+ }
73
+ /**
74
+ * Load and parse a policy file. Returns null if the file doesn't exist.
75
+ * Throws on parse errors so callers can decide how to handle them.
76
+ */
77
+ function loadPolicyFile(filePath) {
78
+ const raw = fs.readFileSync(filePath, 'utf8');
79
+ const parsed = yaml_1.default.parse(raw);
80
+ if (!parsed || !Array.isArray(parsed.rules)) {
81
+ return { rules: [] };
82
+ }
83
+ const rules = parsed.rules.map((r) => {
84
+ const rule = r;
85
+ const match = (rule.match ?? {});
86
+ return {
87
+ id: String(rule.id ?? 'unnamed'),
88
+ match: {
89
+ tool: match.tool != null ? String(match.tool) : undefined,
90
+ command_pattern: match.command_pattern != null ? String(match.command_pattern) : undefined,
91
+ file_pattern: match.file_pattern != null ? String(match.file_pattern) : undefined,
92
+ mcp_server: match.mcp_server != null ? String(match.mcp_server) : undefined,
93
+ },
94
+ action: (['deny', 'warn', 'allow'].includes(String(rule.action))
95
+ ? String(rule.action)
96
+ : 'deny'),
97
+ };
98
+ });
99
+ return { rules };
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // Rule matching
103
+ // ---------------------------------------------------------------------------
104
+ function ruleMatches(rule, event) {
105
+ const { match } = rule;
106
+ // All specified match fields must match (AND logic)
107
+ if (match.tool != null) {
108
+ if (event.tool !== match.tool)
109
+ return false;
110
+ }
111
+ if (match.command_pattern != null) {
112
+ const command = event.action?.command ?? event.shell_command;
113
+ if (!command)
114
+ return false;
115
+ try {
116
+ if (!new RegExp(match.command_pattern).test(command))
117
+ return false;
118
+ }
119
+ catch {
120
+ return false; // invalid regex → no match
121
+ }
122
+ }
123
+ if (match.file_pattern != null) {
124
+ const filePath = event.file_path ?? event.tool_args?.path;
125
+ if (!filePath)
126
+ return false;
127
+ try {
128
+ if (!new RegExp(match.file_pattern).test(filePath))
129
+ return false;
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ }
135
+ if (match.mcp_server != null) {
136
+ if (event.mcp_server !== match.mcp_server)
137
+ return false;
138
+ }
139
+ // At least one match field must be specified (a rule with empty match is invalid)
140
+ const hasAnyMatchField = match.tool != null || match.command_pattern != null ||
141
+ match.file_pattern != null || match.mcp_server != null;
142
+ return hasAnyMatchField;
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ // Evaluation
146
+ // ---------------------------------------------------------------------------
147
+ const ACTION_PRECEDENCE = { deny: 3, warn: 2, allow: 1 };
148
+ /**
149
+ * Evaluate local policy rules against a normalized OculiEvent.
150
+ *
151
+ * - Only evaluates on `pre` phase events. Post/complete → allow.
152
+ * - Collects all matching rules, highest-precedence action wins (deny > warn > allow).
153
+ * - If no rules match → allow.
154
+ */
155
+ function evaluateLocalPolicy(event, rules) {
156
+ // Only enforce on pre-phase events
157
+ if (event.phase !== 'pre') {
158
+ return { action: 'allow', matchedRules: [] };
159
+ }
160
+ const matchedRules = rules.filter((r) => ruleMatches(r, event));
161
+ if (matchedRules.length === 0) {
162
+ return { action: 'allow', matchedRules: [] };
163
+ }
164
+ // Highest precedence wins
165
+ let highestAction = 'allow';
166
+ for (const rule of matchedRules) {
167
+ if (ACTION_PRECEDENCE[rule.action] > ACTION_PRECEDENCE[highestAction]) {
168
+ highestAction = rule.action;
169
+ }
170
+ }
171
+ return { action: highestAction, matchedRules };
172
+ }
@@ -0,0 +1,82 @@
1
+ import { AppConfig } from '../config';
2
+ export interface PolicyRecord {
3
+ id: number;
4
+ name: string;
5
+ description: string;
6
+ version: string;
7
+ rego: string;
8
+ updatedAt: string;
9
+ updatedBy: string;
10
+ createdAt: string;
11
+ isActive: number;
12
+ }
13
+ export interface CreatePolicyInput {
14
+ name: string;
15
+ description?: string;
16
+ version?: string;
17
+ rego: string;
18
+ updatedBy: string;
19
+ }
20
+ export interface UpdatePolicyInput {
21
+ name?: string;
22
+ description?: string;
23
+ version?: string;
24
+ rego?: string;
25
+ updatedBy: string;
26
+ isActive?: number;
27
+ }
28
+ export interface VisualRule {
29
+ id: number;
30
+ rule_id: string;
31
+ tool: string | null;
32
+ command_pattern: string | null;
33
+ file_pattern: string | null;
34
+ mcp_server: string | null;
35
+ action: 'deny' | 'warn' | 'allow';
36
+ description: string;
37
+ enabled: number;
38
+ priority: number;
39
+ createdAt: string;
40
+ updatedAt: string;
41
+ }
42
+ export interface CreateVisualRuleInput {
43
+ rule_id: string;
44
+ tool?: string;
45
+ command_pattern?: string;
46
+ file_pattern?: string;
47
+ mcp_server?: string;
48
+ action: 'deny' | 'warn' | 'allow';
49
+ description?: string;
50
+ enabled?: number;
51
+ priority?: number;
52
+ }
53
+ export interface UpdateVisualRuleInput {
54
+ rule_id?: string;
55
+ tool?: string | null;
56
+ command_pattern?: string | null;
57
+ file_pattern?: string | null;
58
+ mcp_server?: string | null;
59
+ action?: 'deny' | 'warn' | 'allow';
60
+ description?: string;
61
+ enabled?: number;
62
+ priority?: number;
63
+ }
64
+ export declare class PolicyStore {
65
+ private readonly db;
66
+ constructor(config: AppConfig);
67
+ private initialize;
68
+ private seed;
69
+ list(): PolicyRecord[];
70
+ getById(id: number): PolicyRecord | undefined;
71
+ create(input: CreatePolicyInput): PolicyRecord;
72
+ update(id: number, input: UpdatePolicyInput): PolicyRecord | undefined;
73
+ duplicate(id: number): PolicyRecord | undefined;
74
+ delete(id: number): boolean;
75
+ private seedVisualRules;
76
+ listVisualRules(): VisualRule[];
77
+ getVisualRuleById(id: number): VisualRule | undefined;
78
+ createVisualRule(input: CreateVisualRuleInput): VisualRule;
79
+ updateVisualRule(id: number, input: UpdateVisualRuleInput): VisualRule | undefined;
80
+ deleteVisualRule(id: number): boolean;
81
+ close(): void;
82
+ }
@@ -0,0 +1,331 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PolicyStore = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const sqlite_loader_1 = require("./sqlite-loader");
10
+ class PolicyStore {
11
+ db;
12
+ constructor(config) {
13
+ const dir = path_1.default.dirname(config.dbPath);
14
+ if (!fs_1.default.existsSync(dir)) {
15
+ fs_1.default.mkdirSync(dir, { recursive: true });
16
+ }
17
+ const Ctor = (0, sqlite_loader_1.loadSqlite)();
18
+ this.db = new Ctor(config.dbPath);
19
+ this.db.pragma('journal_mode = WAL');
20
+ this.initialize();
21
+ }
22
+ initialize() {
23
+ this.db.exec(`
24
+ CREATE TABLE IF NOT EXISTS policies (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ name TEXT NOT NULL,
27
+ description TEXT NOT NULL DEFAULT '',
28
+ version TEXT NOT NULL DEFAULT '1.0.0',
29
+ rego TEXT NOT NULL,
30
+ updatedAt TEXT NOT NULL,
31
+ updatedBy TEXT NOT NULL,
32
+ createdAt TEXT NOT NULL,
33
+ isActive INTEGER NOT NULL DEFAULT 1
34
+ )
35
+ `);
36
+ this.db.exec(`
37
+ CREATE TABLE IF NOT EXISTS visual_rules (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ rule_id TEXT NOT NULL UNIQUE,
40
+ tool TEXT,
41
+ command_pattern TEXT,
42
+ file_pattern TEXT,
43
+ mcp_server TEXT,
44
+ action TEXT NOT NULL CHECK(action IN ('deny','warn','allow')),
45
+ description TEXT NOT NULL DEFAULT '',
46
+ enabled INTEGER NOT NULL DEFAULT 1,
47
+ priority INTEGER NOT NULL DEFAULT 0,
48
+ createdAt TEXT NOT NULL,
49
+ updatedAt TEXT NOT NULL
50
+ )
51
+ `);
52
+ this.seed();
53
+ this.seedVisualRules();
54
+ }
55
+ seed() {
56
+ const count = this.db.prepare('SELECT COUNT(*) as cnt FROM policies').get();
57
+ if (count.cnt > 0)
58
+ return;
59
+ const insert = this.db.prepare(`
60
+ INSERT INTO policies (name, description, version, rego, updatedAt, updatedBy, createdAt, isActive)
61
+ VALUES (?, ?, ?, ?, ?, ?, ?, 1)
62
+ `);
63
+ const seedPolicies = [
64
+ {
65
+ name: 'Admin Full Access',
66
+ description: 'Grants full access to users with the admin role',
67
+ version: '1.2.3',
68
+ rego: `package mcp.authz
69
+
70
+ default allow = false
71
+
72
+ # Allow admin role full access
73
+ allow {
74
+ input.role == "admin"
75
+ }
76
+
77
+ # Allow developers to read-only tools
78
+ allow {
79
+ input.role == "developer"
80
+ input.tool == "readFile"
81
+ }
82
+
83
+ allow {
84
+ input.role == "developer"
85
+ input.tool == "listDir"
86
+ }`,
87
+ updatedAt: '2026-02-25T10:30:00.000Z',
88
+ updatedBy: 'admin@oculisecurity.com',
89
+ createdAt: '2026-01-15T08:00:00.000Z',
90
+ },
91
+ {
92
+ name: 'Developer Restricted',
93
+ description: 'Standard policy with tool allowlists, dangerous pattern checks, role restrictions, and sensitive field redactions',
94
+ version: '2.1.0',
95
+ rego: `package gateway.authz
96
+
97
+ import future.keywords.if
98
+ import future.keywords.in
99
+
100
+ default allow := false
101
+
102
+ allow if {
103
+ not missing_tool
104
+ tool_allowed
105
+ not dangerous_args
106
+ not role_restricted
107
+ }
108
+
109
+ reason := "allowed" if { allow }
110
+ else := "tool not specified" if { missing_tool }
111
+ else := "tool not in allowlist for this upstream" if {
112
+ not missing_tool
113
+ not tool_allowed
114
+ }
115
+ else := "dangerous argument pattern detected" if {
116
+ not missing_tool
117
+ tool_allowed
118
+ dangerous_args
119
+ }
120
+ else := "role restriction: insufficient permissions for write operations" if {
121
+ not missing_tool
122
+ tool_allowed
123
+ not dangerous_args
124
+ role_restricted
125
+ }
126
+ else := "denied by policy"
127
+
128
+ redactions := {} if { "admin" in input.roles }
129
+ else := {
130
+ "secretKey": true,
131
+ "password": true,
132
+ "token": true,
133
+ "apiKey": true,
134
+ "secret": true,
135
+ "privateKey": true,
136
+ }
137
+
138
+ max_args_size := 102400
139
+
140
+ tool_allowlists := {
141
+ "fs-server": {"readFile", "listDir", "writeFile"},
142
+ "http-server": {"fetchUrl", "postUrl"},
143
+ }
144
+
145
+ missing_tool if { not input.tool }
146
+ missing_tool if { input.tool == "" }
147
+
148
+ tool_allowed if {
149
+ allowed := tool_allowlists[input.upstreamId]
150
+ input.tool in allowed
151
+ }
152
+
153
+ dangerous_patterns := [
154
+ "../", "..\\\\", ";", "&&", "||", "\`", "$(", "\${",
155
+ "%2e%2e", "%252e",
156
+ ]
157
+
158
+ dangerous_args if {
159
+ some key
160
+ val := input.args[key]
161
+ is_string(val)
162
+ some pattern in dangerous_patterns
163
+ contains(val, pattern)
164
+ }
165
+
166
+ write_tools := {"writeFile", "postUrl"}
167
+
168
+ role_restricted if {
169
+ input.tool in write_tools
170
+ "viewer" in input.roles
171
+ not "admin" in input.roles
172
+ not "editor" in input.roles
173
+ }`,
174
+ updatedAt: '2026-02-24T15:45:00.000Z',
175
+ updatedBy: 'security@oculisecurity.com',
176
+ createdAt: '2026-01-10T09:00:00.000Z',
177
+ },
178
+ {
179
+ name: 'Service Account Limited',
180
+ description: 'Restricts service accounts to read-only operations only',
181
+ version: '1.0.5',
182
+ rego: `package gateway.service_account
183
+
184
+ import future.keywords.if
185
+ import future.keywords.in
186
+
187
+ default allow := false
188
+
189
+ read_tools := {"readFile", "listDir", "fetchUrl"}
190
+
191
+ allow if {
192
+ "service" in input.roles
193
+ input.tool in read_tools
194
+ not dangerous_args
195
+ }
196
+
197
+ reason := "allowed" if { allow }
198
+ else := "service accounts restricted to read-only operations"
199
+
200
+ dangerous_patterns := ["../", "..\\\\", ";", "&&", "||", "\`", "$(", "\${"]
201
+
202
+ dangerous_args if {
203
+ some key
204
+ val := input.args[key]
205
+ is_string(val)
206
+ some pattern in dangerous_patterns
207
+ contains(val, pattern)
208
+ }
209
+
210
+ redactions := {
211
+ "secretKey": true,
212
+ "password": true,
213
+ "token": true,
214
+ "apiKey": true,
215
+ }
216
+
217
+ max_args_size := 51200`,
218
+ updatedAt: '2026-02-23T09:20:00.000Z',
219
+ updatedBy: 'devops@oculisecurity.com',
220
+ createdAt: '2026-01-20T14:00:00.000Z',
221
+ },
222
+ ];
223
+ const insertMany = this.db.transaction(() => {
224
+ for (const p of seedPolicies) {
225
+ insert.run(p.name, p.description, p.version, p.rego, p.updatedAt, p.updatedBy, p.createdAt);
226
+ }
227
+ });
228
+ insertMany();
229
+ }
230
+ list() {
231
+ return this.db.prepare('SELECT * FROM policies ORDER BY updatedAt DESC').all();
232
+ }
233
+ getById(id) {
234
+ return this.db.prepare('SELECT * FROM policies WHERE id = ?').get(id);
235
+ }
236
+ create(input) {
237
+ const now = new Date().toISOString();
238
+ const result = this.db.prepare(`
239
+ INSERT INTO policies (name, description, version, rego, updatedAt, updatedBy, createdAt, isActive)
240
+ VALUES (?, ?, ?, ?, ?, ?, ?, 1)
241
+ `).run(input.name, input.description ?? '', input.version ?? '1.0.0', input.rego, now, input.updatedBy, now);
242
+ return this.getById(Number(result.lastInsertRowid));
243
+ }
244
+ update(id, input) {
245
+ const existing = this.getById(id);
246
+ if (!existing)
247
+ return undefined;
248
+ const now = new Date().toISOString();
249
+ this.db.prepare(`
250
+ UPDATE policies
251
+ SET name = ?, description = ?, version = ?, rego = ?, updatedAt = ?, updatedBy = ?, isActive = ?
252
+ WHERE id = ?
253
+ `).run(input.name ?? existing.name, input.description ?? existing.description, input.version ?? existing.version, input.rego ?? existing.rego, now, input.updatedBy, input.isActive ?? existing.isActive, id);
254
+ return this.getById(id);
255
+ }
256
+ duplicate(id) {
257
+ const original = this.getById(id);
258
+ if (!original)
259
+ return undefined;
260
+ return this.create({
261
+ name: original.name + ' (Copy)',
262
+ description: original.description,
263
+ version: '1.0.0',
264
+ rego: original.rego,
265
+ updatedBy: original.updatedBy,
266
+ });
267
+ }
268
+ delete(id) {
269
+ const result = this.db.prepare('DELETE FROM policies WHERE id = ?').run(id);
270
+ return result.changes > 0;
271
+ }
272
+ // ── Visual Rules ──────────────────────────────────────────────────────────
273
+ seedVisualRules() {
274
+ const count = this.db.prepare('SELECT COUNT(*) as cnt FROM visual_rules').get();
275
+ if (count.cnt > 0)
276
+ return;
277
+ const now = new Date().toISOString();
278
+ const insert = this.db.prepare(`
279
+ INSERT INTO visual_rules (rule_id, tool, command_pattern, file_pattern, mcp_server, action, description, enabled, priority, createdAt, updatedAt)
280
+ VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)
281
+ `);
282
+ const seeds = [
283
+ { rule_id: 'no-rm-rf', tool: 'shell', command_pattern: 'rm\\s+-rf', file_pattern: null, mcp_server: null, action: 'deny', description: 'Block recursive force-delete commands', priority: 100 },
284
+ { rule_id: 'no-path-traversal', tool: 'shell', command_pattern: '\\.\\./', file_pattern: null, mcp_server: null, action: 'deny', description: 'Block path traversal in shell commands', priority: 90 },
285
+ { rule_id: 'warn-env-access', tool: 'file_read', command_pattern: null, file_pattern: '\\.env', mcp_server: null, action: 'warn', description: 'Warn when reading .env files', priority: 50 },
286
+ { rule_id: 'warn-env-edit', tool: 'file_edit', command_pattern: null, file_pattern: '\\.env', mcp_server: null, action: 'warn', description: 'Warn when editing .env files', priority: 50 },
287
+ { rule_id: 'warn-mcp', tool: 'mcp_call', command_pattern: null, file_pattern: null, mcp_server: null, action: 'warn', description: 'Warn on all MCP tool calls', priority: 10 },
288
+ ];
289
+ const insertMany = this.db.transaction(() => {
290
+ for (const s of seeds) {
291
+ insert.run(s.rule_id, s.tool, s.command_pattern, s.file_pattern, s.mcp_server, s.action, s.description, s.priority, now, now);
292
+ }
293
+ });
294
+ insertMany();
295
+ }
296
+ listVisualRules() {
297
+ return this.db.prepare('SELECT * FROM visual_rules ORDER BY priority DESC, id ASC').all();
298
+ }
299
+ getVisualRuleById(id) {
300
+ return this.db.prepare('SELECT * FROM visual_rules WHERE id = ?').get(id);
301
+ }
302
+ createVisualRule(input) {
303
+ const now = new Date().toISOString();
304
+ const result = this.db.prepare(`
305
+ INSERT INTO visual_rules (rule_id, tool, command_pattern, file_pattern, mcp_server, action, description, enabled, priority, createdAt, updatedAt)
306
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
307
+ `).run(input.rule_id, input.tool ?? null, input.command_pattern ?? null, input.file_pattern ?? null, input.mcp_server ?? null, input.action, input.description ?? '', input.enabled ?? 1, input.priority ?? 0, now, now);
308
+ return this.getVisualRuleById(Number(result.lastInsertRowid));
309
+ }
310
+ updateVisualRule(id, input) {
311
+ const existing = this.getVisualRuleById(id);
312
+ if (!existing)
313
+ return undefined;
314
+ const now = new Date().toISOString();
315
+ this.db.prepare(`
316
+ UPDATE visual_rules
317
+ SET rule_id = ?, tool = ?, command_pattern = ?, file_pattern = ?, mcp_server = ?,
318
+ action = ?, description = ?, enabled = ?, priority = ?, updatedAt = ?
319
+ WHERE id = ?
320
+ `).run(input.rule_id ?? existing.rule_id, input.tool !== undefined ? input.tool : existing.tool, input.command_pattern !== undefined ? input.command_pattern : existing.command_pattern, input.file_pattern !== undefined ? input.file_pattern : existing.file_pattern, input.mcp_server !== undefined ? input.mcp_server : existing.mcp_server, input.action ?? existing.action, input.description ?? existing.description, input.enabled !== undefined ? input.enabled : existing.enabled, input.priority !== undefined ? input.priority : existing.priority, now, id);
321
+ return this.getVisualRuleById(id);
322
+ }
323
+ deleteVisualRule(id) {
324
+ const result = this.db.prepare('DELETE FROM visual_rules WHERE id = ?').run(id);
325
+ return result.changes > 0;
326
+ }
327
+ close() {
328
+ this.db.close();
329
+ }
330
+ }
331
+ exports.PolicyStore = PolicyStore;
@@ -0,0 +1,8 @@
1
+ import { AppConfig } from '../config';
2
+ import { PolicyDecision, PolicyInput } from '../types';
3
+ export declare class PolicyService {
4
+ private readonly opaUrl;
5
+ private readonly opaEnabled;
6
+ constructor(config: AppConfig);
7
+ evaluate(input: PolicyInput): Promise<PolicyDecision>;
8
+ }