@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,46 @@
1
+ import { AuthenticatedActor } from '../types';
2
+ import { PolicyService } from './policy';
3
+ import { AuditService } from './audit';
4
+ import { UpstreamConnector } from './upstream';
5
+ import { RateLimiter } from './ratelimit';
6
+ export interface ToolExecutorContext {
7
+ policy: PolicyService;
8
+ audit: AuditService;
9
+ upstream: UpstreamConnector;
10
+ rateLimiter: RateLimiter;
11
+ }
12
+ export interface ExecuteToolInput {
13
+ actor: AuthenticatedActor;
14
+ upstreamId: string;
15
+ tool: string;
16
+ args: Record<string, unknown>;
17
+ sessionId?: string;
18
+ ip: string;
19
+ }
20
+ export type ExecuteToolResult = {
21
+ kind: 'allow';
22
+ requestId: string;
23
+ result: unknown;
24
+ upstreamId: string;
25
+ latencyMs: number;
26
+ reason: string;
27
+ } | {
28
+ kind: 'deny';
29
+ requestId: string;
30
+ reason: string;
31
+ httpStatus: 400 | 403 | 413 | 429;
32
+ retryAfterMs?: number;
33
+ } | {
34
+ kind: 'error';
35
+ requestId: string;
36
+ message: string;
37
+ httpStatus: number;
38
+ upstreamId: string;
39
+ latencyMs: number;
40
+ reason: string;
41
+ };
42
+ export declare class ToolExecutor {
43
+ private readonly ctx;
44
+ constructor(ctx: ToolExecutorContext);
45
+ execute(input: ExecuteToolInput): Promise<ExecuteToolResult>;
46
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ToolExecutor = void 0;
4
+ const uuid_1 = require("uuid");
5
+ const upstream_1 = require("./upstream");
6
+ const sanitizer_1 = require("./sanitizer");
7
+ // ---------------------------------------------------------------------------
8
+ // ToolExecutor — shared 7-step pipeline (validate → rate-limit → policy →
9
+ // size-check → forward → sanitize → audit)
10
+ // ---------------------------------------------------------------------------
11
+ class ToolExecutor {
12
+ ctx;
13
+ constructor(ctx) {
14
+ this.ctx = ctx;
15
+ }
16
+ async execute(input) {
17
+ const requestId = (0, uuid_1.v4)();
18
+ const startMs = Date.now();
19
+ const sessionId = input.sessionId ?? (0, uuid_1.v4)();
20
+ const argsJson = JSON.stringify(input.args);
21
+ const storedArgsJson = this.ctx.audit.storeFullArgs ? argsJson : null;
22
+ // ------------------------------------------------------------------
23
+ // 1. Validate upstream
24
+ // ------------------------------------------------------------------
25
+ const upstreamCfg = this.ctx.upstream.getConfig(input.upstreamId);
26
+ if (!upstreamCfg) {
27
+ return { kind: 'deny', requestId, reason: `Unknown upstreamId '${input.upstreamId}'`, httpStatus: 400 };
28
+ }
29
+ // ------------------------------------------------------------------
30
+ // 2. Rate limiting (token bucket per actor:tool)
31
+ // ------------------------------------------------------------------
32
+ const rlKey = `${input.actor.actor}:${input.tool}`;
33
+ const rlResult = this.ctx.rateLimiter.check(rlKey);
34
+ if (!rlResult.allowed) {
35
+ this.ctx.audit.log({
36
+ requestId,
37
+ timestamp: new Date().toISOString(),
38
+ actor: input.actor.actor,
39
+ orgId: input.actor.orgId,
40
+ upstreamId: input.upstreamId,
41
+ tool: input.tool,
42
+ argsHash: this.ctx.audit.hash(input.args),
43
+ argsJson: storedArgsJson,
44
+ decision: 'deny',
45
+ reason: 'rate limit exceeded',
46
+ latencyMs: Date.now() - startMs,
47
+ outcome: 'denied',
48
+ ip: input.ip,
49
+ sessionId,
50
+ });
51
+ return { kind: 'deny', requestId, reason: 'rate limit exceeded', httpStatus: 429, retryAfterMs: rlResult.retryAfterMs };
52
+ }
53
+ // ------------------------------------------------------------------
54
+ // 3. Policy evaluation (OPA or built-in fallback)
55
+ // ------------------------------------------------------------------
56
+ const policyInput = {
57
+ actor: input.actor.actor,
58
+ orgId: input.actor.orgId,
59
+ roles: input.actor.roles,
60
+ upstreamId: input.upstreamId,
61
+ tool: input.tool,
62
+ args: input.args,
63
+ time: new Date().toISOString(),
64
+ ip: input.ip,
65
+ sessionId,
66
+ };
67
+ const decision = await this.ctx.policy.evaluate(policyInput);
68
+ if (!decision.allow) {
69
+ this.ctx.audit.log({
70
+ requestId,
71
+ timestamp: new Date().toISOString(),
72
+ actor: input.actor.actor,
73
+ orgId: input.actor.orgId,
74
+ upstreamId: input.upstreamId,
75
+ tool: input.tool,
76
+ argsHash: this.ctx.audit.hash(input.args),
77
+ argsJson: storedArgsJson,
78
+ decision: 'deny',
79
+ reason: decision.reason,
80
+ latencyMs: Date.now() - startMs,
81
+ outcome: 'denied',
82
+ ip: input.ip,
83
+ sessionId,
84
+ });
85
+ return { kind: 'deny', requestId, reason: decision.reason, httpStatus: 403 };
86
+ }
87
+ // ------------------------------------------------------------------
88
+ // 4. Args size check
89
+ // ------------------------------------------------------------------
90
+ const maxSize = decision.maxArgsSize ?? 102400;
91
+ if (Buffer.byteLength(argsJson) > maxSize) {
92
+ return { kind: 'deny', requestId, reason: `Args payload exceeds max size (${maxSize} bytes)`, httpStatus: 413 };
93
+ }
94
+ // ------------------------------------------------------------------
95
+ // 5. Forward to upstream + 6. Sanitize + 7. Audit
96
+ // ------------------------------------------------------------------
97
+ try {
98
+ const result = await this.ctx.upstream.call(input.upstreamId, {
99
+ tool: input.tool,
100
+ args: input.args,
101
+ sessionId,
102
+ actor: input.actor.actor,
103
+ });
104
+ const sanitized = (0, sanitizer_1.sanitizeResponse)(result.data, decision);
105
+ const responseStr = JSON.stringify(result.data);
106
+ const responseJson = responseStr.length <= 8192 ? responseStr : responseStr.slice(0, 8192) + '…';
107
+ this.ctx.audit.log({
108
+ requestId,
109
+ timestamp: new Date().toISOString(),
110
+ actor: input.actor.actor,
111
+ orgId: input.actor.orgId,
112
+ upstreamId: input.upstreamId,
113
+ tool: input.tool,
114
+ argsHash: this.ctx.audit.hash(input.args),
115
+ argsJson: storedArgsJson,
116
+ decision: 'allow',
117
+ reason: decision.reason,
118
+ latencyMs: Date.now() - startMs,
119
+ outcome: 'success',
120
+ responseHash: this.ctx.audit.hash(result.data),
121
+ responseJson,
122
+ ip: input.ip,
123
+ sessionId,
124
+ });
125
+ return {
126
+ kind: 'allow',
127
+ requestId,
128
+ result: sanitized,
129
+ upstreamId: input.upstreamId,
130
+ latencyMs: result.latencyMs,
131
+ reason: decision.reason,
132
+ };
133
+ }
134
+ catch (err) {
135
+ const upstreamErr = err;
136
+ const errMsg = upstreamErr.message ?? 'Upstream error';
137
+ const latencyMs = upstreamErr.latencyMs ?? 0;
138
+ const statusCode = upstreamErr instanceof upstream_1.UpstreamError ? upstreamErr.statusCode : 502;
139
+ this.ctx.audit.log({
140
+ requestId,
141
+ timestamp: new Date().toISOString(),
142
+ actor: input.actor.actor,
143
+ orgId: input.actor.orgId,
144
+ upstreamId: input.upstreamId,
145
+ tool: input.tool,
146
+ argsHash: this.ctx.audit.hash(input.args),
147
+ argsJson: storedArgsJson,
148
+ decision: 'allow',
149
+ reason: decision.reason,
150
+ latencyMs: Date.now() - startMs,
151
+ outcome: 'error',
152
+ ip: input.ip,
153
+ sessionId,
154
+ });
155
+ return {
156
+ kind: 'error',
157
+ requestId,
158
+ message: errMsg,
159
+ httpStatus: statusCode,
160
+ upstreamId: input.upstreamId,
161
+ latencyMs,
162
+ reason: decision.reason,
163
+ };
164
+ }
165
+ }
166
+ }
167
+ exports.ToolExecutor = ToolExecutor;
@@ -0,0 +1,18 @@
1
+ import { AppConfig } from '../config';
2
+ import { UpstreamCallRequest, UpstreamConfig } from '../types';
3
+ export interface UpstreamResult {
4
+ data: unknown;
5
+ latencyMs: number;
6
+ }
7
+ export declare class UpstreamConnector {
8
+ private readonly registry;
9
+ constructor(config: AppConfig);
10
+ getConfig(upstreamId: string): UpstreamConfig | undefined;
11
+ listUpstreams(): UpstreamConfig[];
12
+ call(upstreamId: string, body: UpstreamCallRequest): Promise<UpstreamResult>;
13
+ }
14
+ export declare class UpstreamError extends Error {
15
+ readonly statusCode: number;
16
+ readonly latencyMs: number;
17
+ constructor(message: string, statusCode: number, latencyMs: number);
18
+ }
@@ -0,0 +1,72 @@
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.UpstreamError = exports.UpstreamConnector = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ class UpstreamConnector {
9
+ registry;
10
+ constructor(config) {
11
+ this.registry = new Map(config.upstreams.map((u) => [u.id, u]));
12
+ }
13
+ getConfig(upstreamId) {
14
+ return this.registry.get(upstreamId);
15
+ }
16
+ listUpstreams() {
17
+ return Array.from(this.registry.values());
18
+ }
19
+ async call(upstreamId, body) {
20
+ const upstream = this.registry.get(upstreamId);
21
+ if (!upstream) {
22
+ throw new Error(`Unknown upstream: '${upstreamId}'`);
23
+ }
24
+ const url = `${upstream.baseUrl}/v1/call`;
25
+ const headers = {
26
+ 'Content-Type': 'application/json',
27
+ };
28
+ if (upstream.authHeader) {
29
+ headers['Authorization'] = upstream.authHeader;
30
+ }
31
+ const startMs = Date.now();
32
+ try {
33
+ const response = await axios_1.default.post(url, body, {
34
+ headers,
35
+ timeout: upstream.timeout,
36
+ validateStatus: () => true, // handle non-2xx ourselves
37
+ });
38
+ const latencyMs = Date.now() - startMs;
39
+ if (response.status >= 400) {
40
+ const errData = response.data;
41
+ throw new UpstreamError(errData?.error ??
42
+ `Upstream returned ${response.status}`, response.status, latencyMs);
43
+ }
44
+ return { data: response.data, latencyMs };
45
+ }
46
+ catch (err) {
47
+ if (err instanceof UpstreamError)
48
+ throw err;
49
+ const latencyMs = Date.now() - startMs;
50
+ const axiosErr = err;
51
+ if (axiosErr.code === 'ECONNABORTED') {
52
+ throw new UpstreamError(`Upstream '${upstreamId}' timed out after ${upstream.timeout}ms`, 504, latencyMs);
53
+ }
54
+ if (axiosErr.code === 'ECONNREFUSED') {
55
+ throw new UpstreamError(`Upstream '${upstreamId}' connection refused at ${url}`, 502, latencyMs);
56
+ }
57
+ throw new UpstreamError(`Upstream '${upstreamId}' error: ${axiosErr.message}`, 502, latencyMs);
58
+ }
59
+ }
60
+ }
61
+ exports.UpstreamConnector = UpstreamConnector;
62
+ class UpstreamError extends Error {
63
+ statusCode;
64
+ latencyMs;
65
+ constructor(message, statusCode, latencyMs) {
66
+ super(message);
67
+ this.statusCode = statusCode;
68
+ this.latencyMs = latencyMs;
69
+ this.name = 'UpstreamError';
70
+ }
71
+ }
72
+ exports.UpstreamError = UpstreamError;
@@ -0,0 +1,112 @@
1
+ export interface JWTPayload {
2
+ sub: string;
3
+ actor: string;
4
+ orgId: string;
5
+ roles: string[];
6
+ iat: number;
7
+ exp: number;
8
+ }
9
+ export interface AuthenticatedActor {
10
+ sub: string;
11
+ actor: string;
12
+ orgId: string;
13
+ roles: string[];
14
+ }
15
+ /** POST /v1/call — incoming request body */
16
+ export interface CallRequest {
17
+ upstreamId: string;
18
+ tool: string;
19
+ args: Record<string, unknown>;
20
+ sessionId?: string;
21
+ }
22
+ /** Structured response from the gateway */
23
+ export interface GatewayResponse {
24
+ requestId: string;
25
+ decision: {
26
+ allow: boolean;
27
+ reason: string;
28
+ };
29
+ upstream?: {
30
+ id: string;
31
+ latencyMs: number;
32
+ };
33
+ result?: unknown;
34
+ error?: string;
35
+ }
36
+ /** Input sent to OPA (or fallback policy engine) */
37
+ export interface PolicyInput {
38
+ actor: string;
39
+ orgId: string;
40
+ roles: string[];
41
+ upstreamId: string;
42
+ tool: string;
43
+ args: Record<string, unknown>;
44
+ time: string;
45
+ ip: string;
46
+ sessionId: string;
47
+ }
48
+ /** Decision returned by OPA / fallback */
49
+ export interface PolicyDecision {
50
+ allow: boolean;
51
+ reason: string;
52
+ redactions?: Record<string, boolean>;
53
+ transforms?: Record<string, Transform>;
54
+ maxArgsSize?: number;
55
+ }
56
+ export interface Transform {
57
+ type: 'truncate' | 'mask' | 'remove';
58
+ maxLength?: number;
59
+ mask?: string;
60
+ }
61
+ export interface UpstreamConfig {
62
+ id: string;
63
+ name: string;
64
+ baseUrl: string;
65
+ authHeader?: string;
66
+ timeout: number;
67
+ tools?: string[];
68
+ }
69
+ /** Body forwarded to the upstream MCP server */
70
+ export interface UpstreamCallRequest {
71
+ tool: string;
72
+ args: Record<string, unknown>;
73
+ sessionId: string;
74
+ actor: string;
75
+ }
76
+ /** A single tool-call event reported by an external agent */
77
+ export interface TelemetryEvent {
78
+ tool: string;
79
+ args?: Record<string, unknown>;
80
+ result?: unknown;
81
+ error?: string;
82
+ durationMs?: number;
83
+ timestamp?: string;
84
+ actor?: string;
85
+ sessionId?: string;
86
+ meta?: Record<string, unknown>;
87
+ }
88
+ /** POST /v1/events — batch of events from an agent */
89
+ export interface TelemetryPayload {
90
+ source: string;
91
+ orgId?: string;
92
+ events: TelemetryEvent[];
93
+ }
94
+ export interface AuditRecord {
95
+ id?: number;
96
+ requestId: string;
97
+ timestamp: string;
98
+ actor: string;
99
+ orgId: string;
100
+ upstreamId: string;
101
+ tool: string;
102
+ argsHash: string;
103
+ argsJson?: string | null;
104
+ decision: 'allow' | 'deny';
105
+ reason: string;
106
+ latencyMs: number;
107
+ outcome: 'success' | 'error' | 'denied';
108
+ responseHash?: string | null;
109
+ responseJson?: string | null;
110
+ ip: string;
111
+ sessionId: string;
112
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // Core domain types for the Oculi Security Gateway
3
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@oculisecurity/cli",
3
+ "version": "0.1.0",
4
+ "description": "Security layer for AI coding agents. Visibility, policy enforcement, and audit trails for Claude Code, Cursor, and Windsurf.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "oculi": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist/",
11
+ "public/",
12
+ "README.md",
13
+ "LICENSE.txt"
14
+ ],
15
+ "scripts": {
16
+ "dev": "tsx watch src/index.ts",
17
+ "build": "tsc",
18
+ "postbuild": "chmod +x dist/cli.js",
19
+ "start": "node dist/index.js",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:coverage": "vitest run --coverage",
23
+ "lint": "tsc --noEmit",
24
+ "hook": "tsx src/cli.ts",
25
+ "prepublishOnly": "npm run build",
26
+ "preuninstall": "node dist/cli.js uninstall || true"
27
+ },
28
+ "keywords": [
29
+ "ai-security",
30
+ "claude-code",
31
+ "cursor",
32
+ "windsurf",
33
+ "policy-enforcement",
34
+ "audit",
35
+ "hooks",
36
+ "agent-security",
37
+ "mcp"
38
+ ],
39
+ "author": "Oculi Security LLC <alex@oculisecurity.com> (https://oculisecurity.com)",
40
+ "homepage": "https://oculisecurity.com",
41
+ "license": "SEE LICENSE IN LICENSE.txt",
42
+ "engines": {
43
+ "node": ">=20.0.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@fastify/cors": "^9.0.1",
50
+ "@fastify/static": "^7.0.4",
51
+ "axios": "^1.7.2",
52
+ "fastify": "^4.28.1",
53
+ "jsonwebtoken": "^9.0.2",
54
+ "uuid": "^11.1.1",
55
+ "yaml": "^2.4.5",
56
+ "zod": "^3.25.76"
57
+ },
58
+ "optionalDependencies": {
59
+ "better-sqlite3": "^9.6.0"
60
+ },
61
+ "devDependencies": {
62
+ "@types/better-sqlite3": "^7.6.10",
63
+ "@types/jsonwebtoken": "^9.0.6",
64
+ "@types/node": "^20.14.10",
65
+ "@types/supertest": "^6.0.2",
66
+ "@vitest/coverage-v8": "^1.6.0",
67
+ "supertest": "^7.0.0",
68
+ "tsx": "^4.16.2",
69
+ "typescript": "^5.5.3",
70
+ "vitest": "^1.6.0"
71
+ }
72
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="8" fill="#0a0a0f"/>
3
+ <circle cx="16" cy="16" r="11" fill="rgba(99, 179, 237, 0.1)" stroke="#63b3ed" stroke-width="2"/>
4
+ </svg>