@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.
- package/LICENSE.txt +201 -0
- package/README.md +67 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +565 -0
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.js +135 -0
- package/dist/commands/report.d.ts +33 -0
- package/dist/commands/report.js +145 -0
- package/dist/commands/serve.d.ts +27 -0
- package/dist/commands/serve.js +163 -0
- package/dist/commands/tail.d.ts +7 -0
- package/dist/commands/tail.js +211 -0
- package/dist/commands/uninstall.d.ts +13 -0
- package/dist/commands/uninstall.js +111 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +90 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +35 -0
- package/dist/init.d.ts +9 -0
- package/dist/init.js +50 -0
- package/dist/install/claude-code.d.ts +13 -0
- package/dist/install/claude-code.js +118 -0
- package/dist/install/cursor.d.ts +13 -0
- package/dist/install/cursor.js +119 -0
- package/dist/install/detect.d.ts +5 -0
- package/dist/install/detect.js +64 -0
- package/dist/middleware/auth.d.ts +15 -0
- package/dist/middleware/auth.js +116 -0
- package/dist/routes/adapters/claude-code.d.ts +38 -0
- package/dist/routes/adapters/claude-code.js +125 -0
- package/dist/routes/adapters/cursor.d.ts +21 -0
- package/dist/routes/adapters/cursor.js +139 -0
- package/dist/routes/adapters/index.d.ts +16 -0
- package/dist/routes/adapters/index.js +56 -0
- package/dist/routes/adapters/router.d.ts +31 -0
- package/dist/routes/adapters/router.js +97 -0
- package/dist/routes/adapters/schema.d.ts +141 -0
- package/dist/routes/adapters/schema.js +83 -0
- package/dist/routes/adapters/windsurf.d.ts +6 -0
- package/dist/routes/adapters/windsurf.js +48 -0
- package/dist/routes/admin.d.ts +15 -0
- package/dist/routes/admin.js +399 -0
- package/dist/routes/call.d.ts +13 -0
- package/dist/routes/call.js +68 -0
- package/dist/routes/events.d.ts +7 -0
- package/dist/routes/events.js +125 -0
- package/dist/routes/health.d.ts +2 -0
- package/dist/routes/health.js +12 -0
- package/dist/routes/hooks.d.ts +11 -0
- package/dist/routes/hooks.js +166 -0
- package/dist/routes/mcp.d.ts +10 -0
- package/dist/routes/mcp.js +170 -0
- package/dist/routes/openai-tools.d.ts +9 -0
- package/dist/routes/openai-tools.js +121 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +118 -0
- package/dist/services/audit.d.ts +92 -0
- package/dist/services/audit.js +388 -0
- package/dist/services/data-dir.d.ts +7 -0
- package/dist/services/data-dir.js +61 -0
- package/dist/services/local-policy-templates.d.ts +9 -0
- package/dist/services/local-policy-templates.js +47 -0
- package/dist/services/local-policy.d.ts +39 -0
- package/dist/services/local-policy.js +172 -0
- package/dist/services/policy-store.d.ts +82 -0
- package/dist/services/policy-store.js +331 -0
- package/dist/services/policy.d.ts +8 -0
- package/dist/services/policy.js +126 -0
- package/dist/services/ratelimit.d.ts +26 -0
- package/dist/services/ratelimit.js +60 -0
- package/dist/services/sanitizer.d.ts +9 -0
- package/dist/services/sanitizer.js +73 -0
- package/dist/services/sqlite-loader.d.ts +4 -0
- package/dist/services/sqlite-loader.js +16 -0
- package/dist/services/telemetry-log.d.ts +76 -0
- package/dist/services/telemetry-log.js +260 -0
- package/dist/services/tool-executor.d.ts +46 -0
- package/dist/services/tool-executor.js +167 -0
- package/dist/services/upstream.d.ts +18 -0
- package/dist/services/upstream.js +72 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.js +3 -0
- package/package.json +72 -0
- package/public/favicon.svg +4 -0
- 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
|
+
}
|