@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.
- package/dist/http-proxy.d.ts +14 -0
- package/dist/http-proxy.d.ts.map +1 -0
- package/dist/http-proxy.js +140 -0
- package/dist/http-proxy.js.map +1 -0
- package/dist/http-relay.d.ts +45 -0
- package/dist/http-relay.d.ts.map +1 -0
- package/dist/http-relay.js +193 -0
- package/dist/http-relay.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptor.d.ts +33 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +95 -0
- package/dist/interceptor.js.map +1 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +65 -0
- package/dist/logger.js.map +1 -0
- package/dist/relay.d.ts +34 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +72 -0
- package/dist/relay.js.map +1 -0
- package/package.json +24 -0
- package/src/http-proxy.ts +176 -0
- package/src/http-relay.ts +228 -0
- package/src/index.ts +178 -0
- package/src/interceptor.ts +127 -0
- package/src/logger.ts +85 -0
- package/src/relay.ts +90 -0
- package/tsconfig.json +12 -0
|
@@ -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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"}
|
package/dist/logger.d.ts
ADDED
|
@@ -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"}
|
package/dist/relay.d.ts
ADDED
|
@@ -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
|
+
}
|