@quint-security/proxy 0.3.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,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inspectRequest = inspectRequest;
4
+ exports.inspectResponse = inspectResponse;
5
+ exports.buildDenyResponse = buildDenyResponse;
6
+ const core_1 = require("@quint-security/core");
7
+ /**
8
+ * Try to parse a line as JSON-RPC and determine the policy verdict.
9
+ * Non-parseable lines or non-tools/call methods get "passthrough".
10
+ */
11
+ function inspectRequest(line, serverName, policy) {
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(line);
15
+ }
16
+ catch {
17
+ return {
18
+ message: null,
19
+ verdict: "passthrough",
20
+ toolName: null,
21
+ argumentsJson: null,
22
+ method: "unknown",
23
+ messageId: null,
24
+ };
25
+ }
26
+ if (!(0, core_1.isJsonRpcRequest)(parsed)) {
27
+ return {
28
+ message: parsed,
29
+ verdict: "passthrough",
30
+ toolName: null,
31
+ argumentsJson: null,
32
+ method: "unknown",
33
+ messageId: extractId(parsed),
34
+ };
35
+ }
36
+ const req = parsed;
37
+ const toolInfo = (0, core_1.extractToolInfo)(req);
38
+ const toolName = toolInfo?.name ?? null;
39
+ const argumentsJson = toolInfo ? JSON.stringify(toolInfo.args) : null;
40
+ // Only policy-check tools/call; everything else is passthrough
41
+ let verdict;
42
+ if ((0, core_1.isToolCallRequest)(req)) {
43
+ verdict = (0, core_1.evaluatePolicy)(policy, serverName, toolName);
44
+ }
45
+ else {
46
+ verdict = "passthrough";
47
+ }
48
+ return {
49
+ message: req,
50
+ verdict,
51
+ toolName,
52
+ argumentsJson,
53
+ method: req.method,
54
+ messageId: req.id != null ? String(req.id) : null,
55
+ };
56
+ }
57
+ /**
58
+ * Inspect a response line from the child (just for logging purposes — responses always pass through).
59
+ */
60
+ function inspectResponse(line) {
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(line);
64
+ }
65
+ catch {
66
+ return { method: "unknown", messageId: null, responseJson: null };
67
+ }
68
+ return {
69
+ method: "response",
70
+ messageId: extractId(parsed),
71
+ responseJson: line,
72
+ };
73
+ }
74
+ /**
75
+ * Build a JSON-RPC error response for a denied tool call.
76
+ */
77
+ function buildDenyResponse(requestId) {
78
+ const response = {
79
+ jsonrpc: "2.0",
80
+ id: requestId,
81
+ error: {
82
+ code: -32600,
83
+ message: "Quint: tool call denied by policy",
84
+ },
85
+ };
86
+ return JSON.stringify(response);
87
+ }
88
+ function extractId(obj) {
89
+ if (typeof obj === "object" && obj !== null && "id" in obj) {
90
+ const id = obj.id;
91
+ return id != null ? String(id) : null;
92
+ }
93
+ return null;
94
+ }
95
+ //# sourceMappingURL=interceptor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interceptor.js","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { type AuditDb, type Verdict, type PolicyConfig } from "@quint-security/core";
2
+ export declare class AuditLogger {
3
+ private db;
4
+ private privateKey;
5
+ private publicKey;
6
+ private policyHash;
7
+ constructor(db: AuditDb, privateKey: string, publicKey: string, policy: PolicyConfig);
8
+ log(opts: {
9
+ serverName: string;
10
+ direction: "request" | "response";
11
+ method: string;
12
+ messageId: string | null;
13
+ toolName: string | null;
14
+ argumentsJson: string | null;
15
+ responseJson: string | null;
16
+ verdict: Verdict;
17
+ riskScore?: number | null;
18
+ riskLevel?: string | null;
19
+ }): number;
20
+ }
21
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,OAAO,EACZ,KAAK,OAAO,EACZ,KAAK,YAAY,EAIlB,MAAM,sBAAsB,CAAC;AAG9B,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAU;IACpB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAS;gBAEf,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY;IAOpF,GAAG,CAAC,IAAI,EAAE;QACR,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,SAAS,GAAG,UAAU,CAAC;QAClC,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,OAAO,EAAE,OAAO,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,GAAG,MAAM;CAiDX"}
package/dist/logger.js ADDED
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuditLogger = void 0;
4
+ const core_1 = require("@quint-security/core");
5
+ const node_crypto_1 = require("node:crypto");
6
+ class AuditLogger {
7
+ db;
8
+ privateKey;
9
+ publicKey;
10
+ policyHash;
11
+ constructor(db, privateKey, publicKey, policy) {
12
+ this.db = db;
13
+ this.privateKey = privateKey;
14
+ this.publicKey = publicKey;
15
+ this.policyHash = (0, core_1.sha256)((0, core_1.canonicalize)(policy));
16
+ }
17
+ log(opts) {
18
+ // Use insertAtomic to read last signature and insert in one transaction,
19
+ // preventing chain breaks when multiple proxy instances share the same DB.
20
+ return this.db.insertAtomic((prevSignature) => {
21
+ const timestamp = new Date().toISOString();
22
+ const nonce = (0, node_crypto_1.randomUUID)();
23
+ const prevHash = prevSignature ? (0, core_1.sha256)(prevSignature) : "";
24
+ const signable = {
25
+ timestamp,
26
+ server_name: opts.serverName,
27
+ direction: opts.direction,
28
+ method: opts.method,
29
+ message_id: opts.messageId,
30
+ tool_name: opts.toolName,
31
+ arguments_json: opts.argumentsJson,
32
+ response_json: opts.responseJson,
33
+ verdict: opts.verdict,
34
+ risk_score: opts.riskScore ?? null,
35
+ risk_level: opts.riskLevel ?? null,
36
+ policy_hash: this.policyHash,
37
+ prev_hash: prevHash,
38
+ nonce,
39
+ public_key: this.publicKey,
40
+ };
41
+ const canonical = (0, core_1.canonicalize)(signable);
42
+ const signature = (0, core_1.signData)(canonical, this.privateKey);
43
+ return {
44
+ timestamp,
45
+ server_name: opts.serverName,
46
+ direction: opts.direction,
47
+ method: opts.method,
48
+ message_id: opts.messageId,
49
+ tool_name: opts.toolName,
50
+ arguments_json: opts.argumentsJson,
51
+ response_json: opts.responseJson,
52
+ verdict: opts.verdict,
53
+ risk_score: opts.riskScore ?? null,
54
+ risk_level: opts.riskLevel ?? null,
55
+ policy_hash: this.policyHash,
56
+ prev_hash: prevHash,
57
+ nonce,
58
+ signature,
59
+ public_key: this.publicKey,
60
+ };
61
+ });
62
+ }
63
+ }
64
+ exports.AuditLogger = AuditLogger;
65
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":";;;AAAA,+CAQ8B;AAC9B,6CAAyC;AAEzC,MAAa,WAAW;IACd,EAAE,CAAU;IACZ,UAAU,CAAS;IACnB,SAAS,CAAS;IAClB,UAAU,CAAS;IAE3B,YAAY,EAAW,EAAE,UAAkB,EAAE,SAAiB,EAAE,MAAoB;QAClF,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,IAAA,aAAM,EAAC,IAAA,mBAAY,EAAC,MAA4C,CAAC,CAAC,CAAC;IACvF,CAAC;IAED,GAAG,CAAC,IAWH;QACC,yEAAyE;QACzE,2EAA2E;QAC3E,OAAO,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,aAA4B,EAAE,EAAE;YAC3D,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAC3C,MAAM,KAAK,GAAG,IAAA,wBAAU,GAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,IAAA,aAAM,EAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAE5D,MAAM,QAAQ,GAA4B;gBACxC,SAAS;gBACT,WAAW,EAAE,IAAI,CAAC,UAAU;gBAC5B,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,UAAU,EAAE,IAAI,CAAC,SAAS;gBAC1B,SAAS,EAAE,IAAI,CAAC,QAAQ;gBACxB,cAAc,EAAE,IAAI,CAAC,aAAa;gBAClC,aAAa,EAAE,IAAI,CAAC,YAAY;gBAChC,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,UAAU,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;gBAClC,UAAU,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;gBAClC,WAAW,EAAE,IAAI,CAAC,UAAU;gBAC5B,SAAS,EAAE,QAAQ;gBACnB,KAAK;gBACL,UAAU,EAAE,IAAI,CAAC,SAAS;aAC3B,CAAC;YAEF,MAAM,SAAS,GAAG,IAAA,mBAAY,EAAC,QAAQ,CAAC,CAAC;YACzC,MAAM,SAAS,GAAG,IAAA,eAAQ,EAAC,SAAS,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;YAEvD,OAAO;gBACL,SAAS;gBACT,WAAW,EAAE,IAAI,CAAC,UAAU;gBAC5B,SAAS,EAAE,IAAI,CAAC,SAAS;gBACzB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,UAAU,EAAE,IAAI,CAAC,SAAS;gBAC1B,SAAS,EAAE,IAAI,CAAC,QAAQ;gBACxB,cAAc,EAAE,IAAI,CAAC,aAAa;gBAClC,aAAa,EAAE,IAAI,CAAC,YAAY;gBAChC,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,UAAU,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;gBAClC,UAAU,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;gBAClC,WAAW,EAAE,IAAI,CAAC,UAAU;gBAC5B,SAAS,EAAE,QAAQ;gBACnB,KAAK;gBACL,SAAS;gBACT,UAAU,EAAE,IAAI,CAAC,SAAS;aAC3B,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAzED,kCAyEC"}
@@ -0,0 +1,34 @@
1
+ import { EventEmitter } from "node:events";
2
+ export interface RelayEvents {
3
+ /** Fired for every line received on stdin (from parent / AI agent) */
4
+ parentMessage: (line: string) => void;
5
+ /** Fired for every line the child process writes to stdout */
6
+ childMessage: (line: string) => void;
7
+ /** Child process exited */
8
+ childExit: (code: number | null, signal: string | null) => void;
9
+ /** Unrecoverable error */
10
+ error: (err: Error) => void;
11
+ }
12
+ /**
13
+ * Relay manages:
14
+ * - Spawning the real MCP server as a child process
15
+ * - Reading JSON-RPC lines from stdin and forwarding to child stdin
16
+ * - Reading JSON-RPC lines from child stdout and forwarding to parent stdout
17
+ *
18
+ * The interceptor hooks into parentMessage/childMessage events to inspect,
19
+ * allow, deny, or modify messages before they are forwarded.
20
+ */
21
+ export declare class Relay extends EventEmitter {
22
+ private child;
23
+ private command;
24
+ private args;
25
+ constructor(command: string, args: string[]);
26
+ start(): void;
27
+ /** Send a line to the child process's stdin */
28
+ sendToChild(line: string): void;
29
+ /** Send a line to the parent process's stdout */
30
+ sendToParent(line: string): void;
31
+ /** Gracefully shut down the child */
32
+ stop(): void;
33
+ }
34
+ //# sourceMappingURL=relay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,8DAA8D;IAC9D,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,2BAA2B;IAC3B,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChE,0BAA0B;IAC1B,KAAK,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,qBAAa,KAAM,SAAQ,YAAY;IACrC,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,IAAI,CAAW;gBAEX,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;IAM3C,KAAK,IAAI,IAAI;IAsCb,+CAA+C;IAC/C,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAM/B,iDAAiD;IACjD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIhC,qCAAqC;IACrC,IAAI,IAAI,IAAI;CAGb"}
package/dist/relay.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Relay = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_readline_1 = require("node:readline");
6
+ const node_events_1 = require("node:events");
7
+ /**
8
+ * Relay manages:
9
+ * - Spawning the real MCP server as a child process
10
+ * - Reading JSON-RPC lines from stdin and forwarding to child stdin
11
+ * - Reading JSON-RPC lines from child stdout and forwarding to parent stdout
12
+ *
13
+ * The interceptor hooks into parentMessage/childMessage events to inspect,
14
+ * allow, deny, or modify messages before they are forwarded.
15
+ */
16
+ class Relay extends node_events_1.EventEmitter {
17
+ child = null;
18
+ command;
19
+ args;
20
+ constructor(command, args) {
21
+ super();
22
+ this.command = command;
23
+ this.args = args;
24
+ }
25
+ start() {
26
+ // Spawn the real MCP server
27
+ this.child = (0, node_child_process_1.spawn)(this.command, this.args, {
28
+ stdio: ["pipe", "pipe", "pipe"],
29
+ env: process.env,
30
+ });
31
+ // Forward child stderr to our stderr (pass through diagnostics)
32
+ this.child.stderr?.pipe(process.stderr);
33
+ this.child.on("error", (err) => {
34
+ this.emit("error", err);
35
+ });
36
+ this.child.on("exit", (code, signal) => {
37
+ this.emit("childExit", code, signal);
38
+ });
39
+ // Read lines from child stdout
40
+ if (this.child.stdout) {
41
+ const childRl = (0, node_readline_1.createInterface)({ input: this.child.stdout });
42
+ childRl.on("line", (line) => {
43
+ this.emit("childMessage", line);
44
+ });
45
+ }
46
+ // Read lines from parent stdin
47
+ const parentRl = (0, node_readline_1.createInterface)({ input: process.stdin });
48
+ parentRl.on("line", (line) => {
49
+ this.emit("parentMessage", line);
50
+ });
51
+ parentRl.on("close", () => {
52
+ // Parent closed stdin — close child's stdin so it can finish and exit
53
+ this.child?.stdin?.end();
54
+ });
55
+ }
56
+ /** Send a line to the child process's stdin */
57
+ sendToChild(line) {
58
+ if (this.child?.stdin?.writable) {
59
+ this.child.stdin.write(line + "\n");
60
+ }
61
+ }
62
+ /** Send a line to the parent process's stdout */
63
+ sendToParent(line) {
64
+ process.stdout.write(line + "\n");
65
+ }
66
+ /** Gracefully shut down the child */
67
+ stop() {
68
+ this.child?.kill();
69
+ }
70
+ }
71
+ exports.Relay = Relay;
72
+ //# sourceMappingURL=relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.js","sourceRoot":"","sources":["../src/relay.ts"],"names":[],"mappings":";;;AAAA,2DAA8D;AAC9D,iDAAgD;AAChD,6CAA2C;AAa3C;;;;;;;;GAQG;AACH,MAAa,KAAM,SAAQ,0BAAY;IAC7B,KAAK,GAAwB,IAAI,CAAC;IAClC,OAAO,CAAS;IAChB,IAAI,CAAW;IAEvB,YAAY,OAAe,EAAE,IAAc;QACzC,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,KAAK;QACH,4BAA4B;QAC5B,IAAI,CAAC,KAAK,GAAG,IAAA,0BAAK,EAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE;YAC1C,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAC/B,GAAG,EAAE,OAAO,CAAC,GAAG;SACjB,CAAC,CAAC;QAEH,gEAAgE;QAChE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAExC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YAC7B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,+BAA+B;QAC/B,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;YACtB,MAAM,OAAO,GAAG,IAAA,+BAAe,EAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;YAC9D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC1B,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;YAClC,CAAC,CAAC,CAAC;QACL,CAAC;QAED,+BAA+B;QAC/B,MAAM,QAAQ,GAAG,IAAA,+BAAe,EAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QAC3D,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YAC3B,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;QAEH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACxB,sEAAsE;YACtE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,+CAA+C;IAC/C,WAAW,CAAC,IAAY;QACtB,IAAI,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;YAChC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,iDAAiD;IACjD,YAAY,CAAC,IAAY;QACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,qCAAqC;IACrC,IAAI;QACF,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC;IACrB,CAAC;CACF;AAjED,sBAiEC"}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@quint-security/proxy",
3
+ "version": "0.3.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Quint-Security/cli.git",
9
+ "directory": "packages/proxy"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc"
16
+ },
17
+ "dependencies": {
18
+ "@quint-security/core": "*"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.0.0",
22
+ "typescript": "^5.4.0"
23
+ }
24
+ }
@@ -0,0 +1,176 @@
1
+ import {
2
+ type PolicyConfig,
3
+ ensureKeyPair,
4
+ openAuditDb,
5
+ openAuthDb,
6
+ authenticateBearer,
7
+ resolveDataDir,
8
+ setLogLevel,
9
+ logDebug,
10
+ logInfo,
11
+ logWarn,
12
+ logError,
13
+ RiskEngine,
14
+ } from "@quint-security/core";
15
+ import { HttpRelay } from "./http-relay.js";
16
+ import { inspectRequest, inspectResponse, buildDenyResponse } from "./interceptor.js";
17
+ import { AuditLogger } from "./logger.js";
18
+
19
+ export interface HttpProxyOptions {
20
+ serverName: string;
21
+ port: number;
22
+ targetUrl: string;
23
+ policy: PolicyConfig;
24
+ requireAuth?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Start the HTTP proxy: run a local HTTP server, intercept all JSON-RPC
29
+ * requests, enforce policy, sign and log everything, forward to remote.
30
+ */
31
+ export async function startHttpProxy(opts: HttpProxyOptions): Promise<void> {
32
+ setLogLevel(opts.policy.log_level);
33
+ const dataDir = resolveDataDir(opts.policy.data_dir);
34
+
35
+ // Ensure signing keys exist
36
+ const kp = ensureKeyPair(dataDir);
37
+
38
+ // Open audit database
39
+ const db = openAuditDb(dataDir);
40
+
41
+ // Create audit logger
42
+ const logger = new AuditLogger(db, kp.privateKey, kp.publicKey, opts.policy);
43
+
44
+ // Create risk engine
45
+ const riskEngine = new RiskEngine();
46
+
47
+ // Create HTTP relay (with optional auth)
48
+ const authDb = opts.requireAuth ? openAuthDb(dataDir) : null;
49
+ const relay = new HttpRelay(opts.port, opts.targetUrl, opts.requireAuth ? (req) => {
50
+ const authHeader = req.headers.authorization;
51
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
52
+ return "Quint: missing or invalid Authorization header. Use: Bearer <api-key>";
53
+ }
54
+ const token = authHeader.slice(7);
55
+ const result = authenticateBearer(authDb!, token);
56
+ if (!result) {
57
+ return "Quint: invalid or expired API key";
58
+ }
59
+ return null;
60
+ } : undefined);
61
+
62
+ // ── Handle requests from agent → remote MCP server ──
63
+
64
+ relay.on("request", (line: string, requestKey: string) => {
65
+ const result = inspectRequest(line, opts.serverName, opts.policy);
66
+
67
+ // Log the request
68
+ logger.log({
69
+ serverName: opts.serverName,
70
+ direction: "request",
71
+ method: result.method,
72
+ messageId: result.messageId,
73
+ toolName: result.toolName,
74
+ argumentsJson: result.argumentsJson,
75
+ responseJson: null,
76
+ verdict: result.verdict,
77
+ });
78
+
79
+ if (result.verdict === "deny") {
80
+ // Send error response back to agent
81
+ const reqId = result.message && "id" in result.message ? result.message.id : null;
82
+ const errorResponse = buildDenyResponse(reqId ?? null);
83
+ relay.respondToClient(requestKey, errorResponse);
84
+ logInfo(`denied ${result.toolName} on ${opts.serverName}`);
85
+
86
+ // Log the synthetic deny response
87
+ logger.log({
88
+ serverName: opts.serverName,
89
+ direction: "response",
90
+ method: result.method,
91
+ messageId: result.messageId,
92
+ toolName: result.toolName,
93
+ argumentsJson: null,
94
+ responseJson: errorResponse,
95
+ verdict: "deny",
96
+ });
97
+ } else if (result.toolName) {
98
+ // Run risk scoring on tool calls that passed policy
99
+ const risk = riskEngine.score(result.toolName, result.argumentsJson, "anonymous");
100
+ const riskAction = riskEngine.evaluate(risk);
101
+
102
+ if (riskAction === "deny") {
103
+ // Risk score too high — auto-deny
104
+ const reqId = result.message && "id" in result.message ? result.message.id : null;
105
+ const errorResponse = buildDenyResponse(reqId ?? null);
106
+ relay.respondToClient(requestKey, errorResponse);
107
+ logWarn(`risk-denied ${result.toolName} (score=${risk.score}, level=${risk.level}): ${risk.reasons.join("; ")}`);
108
+
109
+ logger.log({
110
+ serverName: opts.serverName,
111
+ direction: "response",
112
+ method: result.method,
113
+ messageId: result.messageId,
114
+ toolName: result.toolName,
115
+ argumentsJson: null,
116
+ responseJson: errorResponse,
117
+ verdict: "deny",
118
+ });
119
+ } else {
120
+ if (riskAction === "flag") {
121
+ logWarn(`high-risk ${result.toolName} (score=${risk.score}, level=${risk.level}): ${risk.reasons.join("; ")}`);
122
+ }
123
+ logDebug(`forwarding ${result.method} (risk=${risk.score}) to remote`);
124
+ relay.forwardToRemote(requestKey);
125
+ }
126
+
127
+ // Check for revocation threshold
128
+ if (riskEngine.shouldRevoke("anonymous")) {
129
+ logWarn(`repeated high-risk actions detected — consider revoking agent credentials`);
130
+ }
131
+ } else {
132
+ // Non-tool-call (initialize, tools/list, etc.) — forward directly
133
+ logDebug(`forwarding ${result.method} (${result.verdict}) to remote`);
134
+ relay.forwardToRemote(requestKey);
135
+ }
136
+ });
137
+
138
+ // ── Handle responses from remote MCP server ──
139
+
140
+ relay.on("response", (line: string) => {
141
+ const result = inspectResponse(line);
142
+
143
+ // Log the response
144
+ logger.log({
145
+ serverName: opts.serverName,
146
+ direction: "response",
147
+ method: result.method,
148
+ messageId: result.messageId,
149
+ toolName: null,
150
+ argumentsJson: null,
151
+ responseJson: result.responseJson,
152
+ verdict: "passthrough",
153
+ });
154
+ });
155
+
156
+ // ── Handle errors ──
157
+
158
+ relay.on("error", (err: Error) => {
159
+ logError(`http-proxy error: ${err.message}`);
160
+ });
161
+
162
+ // Handle shutdown
163
+ const shutdown = () => {
164
+ relay.stop();
165
+ db.close();
166
+ authDb?.close();
167
+ process.exit(0);
168
+ };
169
+
170
+ process.on("SIGINT", shutdown);
171
+ process.on("SIGTERM", shutdown);
172
+
173
+ // Start listening
174
+ await relay.start();
175
+ logInfo(`HTTP proxy listening on http://localhost:${opts.port} → ${opts.targetUrl}`);
176
+ }