@integsec/agentic-pentest-proxy 0.3.1 → 0.3.2
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/README.md +1 -1
- package/cloudflare/src/worker.mjs +4 -3
- package/cloudflare/wrangler.jsonc +2 -1
- package/dist/bin/integsec-agentic-pentest-bridge.js +2 -2
- package/dist/src/config.js +1 -1
- package/dist/src/gateway/admin.html +10 -11
- package/dist/src/gateway/admin.js +185 -78
- package/dist/src/gateway/runtime.d.ts +0 -1
- package/dist/src/gateway/runtime.d.ts.map +1 -1
- package/dist/src/gateway/runtime.js +18 -16
- package/dist/src/gateway/runtime.js.map +1 -1
- package/dist/src/gateway/server.d.ts.map +1 -1
- package/dist/src/gateway/server.js +97 -32
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/state.d.ts +10 -3
- package/dist/src/gateway/state.d.ts.map +1 -1
- package/dist/src/gateway/state.js +106 -10
- package/dist/src/gateway/state.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/proxy.d.ts +1 -1
- package/dist/src/proxy.d.ts.map +1 -1
- package/dist/src/proxy.js +2 -2
- package/dist/src/proxy.js.map +1 -1
- package/dist/src/validator.d.ts +2 -1
- package/dist/src/validator.d.ts.map +1 -1
- package/dist/src/validator.js +9 -1
- package/dist/src/validator.js.map +1 -1
- package/docs/managed-gateway.md +11 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ An MCP proxy that checks tool calls against a pentest scope manifest before forw
|
|
|
4
4
|
|
|
5
5
|
**Requires Node.js 22 or newer.** The proxy supports stdio tools, SSE upstreams such as Burp Suite, and HTTP upstreams.
|
|
6
6
|
|
|
7
|
-
**Managing several tools?** The [managed gateway](docs/managed-gateway.md) adds a browser UI
|
|
7
|
+
**Managing several tools?** The [managed gateway](docs/managed-gateway.md) adds a browser UI with curated MCP server presets, scope examples and technique checkboxes, administrator approval and assignment, and a separate engagement choice for each user. It supports one hosted MCP URL for ChatGPT and Claude and a local Burp route.
|
|
8
8
|
|
|
9
9
|
## Quick start: add an MCP tool
|
|
10
10
|
|
|
@@ -68,7 +68,7 @@ export class PentestGatewayContainer extends Container {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async persistMutation(request) {
|
|
71
|
-
const previous = await this.ctx.storage.get('gateway-state') ?? { version:
|
|
71
|
+
const previous = await this.ctx.storage.get('gateway-state') ?? { version: 2, upstreams: [], scopes: [], selectedScopeByUser: {} };
|
|
72
72
|
const response = await this.containerFetch(request);
|
|
73
73
|
if (!response.ok) return response;
|
|
74
74
|
const currentResponse = await this.containerFetch('http://localhost:9091/__internal/state', {
|
|
@@ -111,8 +111,9 @@ export class PentestGatewayContainer extends Container {
|
|
|
111
111
|
|
|
112
112
|
async listAudit(request) {
|
|
113
113
|
const check = await this.containerFetch('http://localhost:9091/api/session', { headers: request.headers });
|
|
114
|
-
await check.body?.cancel();
|
|
115
|
-
|
|
114
|
+
if (check.status !== 200) { await check.body?.cancel(); return text('Administrator access required', 403); }
|
|
115
|
+
const session = await check.json();
|
|
116
|
+
if (session.role !== 'admin') return text('Administrator access required', 403);
|
|
116
117
|
const entries = await this.ctx.storage.list({ prefix: 'audit:', reverse: true, limit: 100 });
|
|
117
118
|
return json({ entries: [...entries.values()] });
|
|
118
119
|
}
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
"main": "src/worker.mjs",
|
|
5
5
|
"compatibility_date": "2026-11-07",
|
|
6
6
|
"workers_dev": true,
|
|
7
|
+
"preview_urls": false,
|
|
7
8
|
"routes": [{ "pattern": "agentic-proxy.pentestprepper.com", "custom_domain": true }],
|
|
8
9
|
"observability": { "enabled": true },
|
|
9
10
|
"containers": [
|
|
10
11
|
{
|
|
11
12
|
"class_name": "PentestGatewayContainer",
|
|
12
|
-
"image": "docker.io/integsec/agentic-pentest-mcp-proxy:0.3.
|
|
13
|
+
"image": "docker.io/integsec/agentic-pentest-mcp-proxy:0.3.2",
|
|
13
14
|
"instance_type": "basic",
|
|
14
15
|
"max_instances": 1
|
|
15
16
|
}
|
|
@@ -12,9 +12,9 @@ const url = new URL(process.argv[2] ?? process.env.GATEWAY_URL ?? "http://127.0.
|
|
|
12
12
|
if (!["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) || url.protocol !== "http:") {
|
|
13
13
|
throw new Error("The local bridge only accepts a loopback HTTP gateway URL");
|
|
14
14
|
}
|
|
15
|
-
const upstream = new Client({ name: "agentic-pentest-bridge", version: "0.3.
|
|
15
|
+
const upstream = new Client({ name: "agentic-pentest-bridge", version: "0.3.2" });
|
|
16
16
|
await upstream.connect(new StreamableHTTPClientTransport(url));
|
|
17
|
-
const bridge = new Server({ name: "agentic-pentest-local", version: "0.3.
|
|
17
|
+
const bridge = new Server({ name: "agentic-pentest-local", version: "0.3.2" }, { capabilities: { tools: {} } });
|
|
18
18
|
bridge.setRequestHandler(ListToolsRequestSchema, async (request) => upstream.listTools(request.params));
|
|
19
19
|
bridge.setRequestHandler(CallToolRequestSchema, async (request) => upstream.callTool(request.params));
|
|
20
20
|
await bridge.connect(new StdioServerTransport());
|
package/dist/src/config.js
CHANGED
|
@@ -50,7 +50,7 @@ export function loadConfig() {
|
|
|
50
50
|
techniqueMapPath: process.env.TECHNIQUE_MAP_PATH,
|
|
51
51
|
dnsCacheTtl: parseInt(process.env.DNS_CACHE_TTL || "60", 10),
|
|
52
52
|
logLevel: process.env.LOG_LEVEL || "INFO",
|
|
53
|
-
proxyVersion: process.env.PROXY_VERSION || "0.3.
|
|
53
|
+
proxyVersion: process.env.PROXY_VERSION || "0.3.2",
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
//# sourceMappingURL=config.js.map
|
|
@@ -4,19 +4,18 @@
|
|
|
4
4
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
5
|
<title>IntegSec MCP Gateway</title>
|
|
6
6
|
<style>
|
|
7
|
-
:root{font:15px system-ui,sans-serif;color:#e8f0f2;background:#0b1218}
|
|
8
|
-
*{box-sizing:border-box}body{margin:0}header{padding:22px 5vw;background:#10202b;border-bottom:1px solid #234052;display:flex;justify-content:space-between;align-items:center;gap:20px}
|
|
9
|
-
h1{font-size:21px;margin:0}h2{font-size:18px;margin:0 0 14px}h3{font-size:15px;margin:16px 0 6px}p{color:#a8bdc8;line-height:1.45}main{max-width:1200px;margin:30px auto;padding:0 20px;display:grid;grid-template-columns:1fr 1fr;gap:20px}.card{background:#14222c;border:1px solid #294454;border-radius:13px;padding:24px}.wide{grid-column:1/-1}
|
|
10
|
-
input,select,textarea,button{font:inherit}input,select,textarea{width:100%;background:#0b1720;color:#fff;border:1px solid #426073;border-radius:7px;padding:9px;margin:5px 0 12px}textarea{min-height:70px}label{display:block;color:#c1d0d8;font-weight:600;font-size:13px}button{border:0;border-radius:7px;background:#26bfa4;color:#081914;padding:9px 14px;font-weight:700;cursor:pointer;margin:4px 7px 4px 0}button.secondary{background:#304b5a;color:#e8f0f2}button.danger{background:#6d3541;color:#fff}button:disabled{opacity:.5;cursor:not-allowed}.grid{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}.row{border-top:1px solid #294454;padding:12px 0;display:flex;align-items:center;justify-content:space-between;gap:12px}.muted{font-size:12px;color:#93acb9}#status{padding:8px 5vw;color:#8ee7ce;min-height:35px}#status.error{color:#ff9e9e}code{color:#a6e7db}[hidden]{display:none!important}@media(max-width:800px){main{display:block}.card{margin-bottom:20px}.grid{display:block}}
|
|
7
|
+
:root{font:15px system-ui,sans-serif;color:#e8f0f2;background:#0b1218}*{box-sizing:border-box}body{margin:0}header{padding:22px 5vw;background:#10202b;border-bottom:1px solid #234052;display:flex;justify-content:space-between;align-items:center;gap:20px}h1{font-size:21px;margin:0}h2{font-size:18px;margin:0 0 14px}h3{font-size:15px;margin:16px 0 6px}p{color:#a8bdc8;line-height:1.45}main{max-width:1200px;margin:30px auto;padding:0 20px;display:grid;grid-template-columns:1fr 1fr;gap:20px}.card{background:#14222c;border:1px solid #294454;border-radius:13px;padding:24px}.wide{grid-column:1/-1}input,select,textarea,button{font:inherit}input:not([type=checkbox]),select,textarea{width:100%;background:#0b1720;color:#fff;border:1px solid #426073;border-radius:7px;padding:9px;margin:5px 0 12px}textarea{min-height:70px}label{display:block;color:#c1d0d8;font-weight:600;font-size:13px}button{border:0;border-radius:7px;background:#26bfa4;color:#081914;padding:9px 14px;font-weight:700;cursor:pointer;margin:4px 7px 4px 0}button.secondary{background:#304b5a;color:#e8f0f2}button.danger{background:#6d3541;color:#fff}button:disabled{opacity:.5;cursor:not-allowed}.grid{display:grid;grid-template-columns:1fr 1fr;gap:0 14px}.row{border-top:1px solid #294454;padding:12px 0;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.muted{font-size:12px;color:#93acb9}.choices{display:flex;flex-wrap:wrap;gap:8px 18px;margin:12px 0}.choices label{display:inline-flex;gap:7px;align-items:center;font-weight:400;font-size:14px}.choices input{width:auto;margin:0}.pill{display:inline-block;background:#234051;border-radius:20px;padding:3px 9px;font-size:12px;margin-left:8px}.actions{display:flex;flex-wrap:wrap;gap:3px}.help{background:#0d1e28;border-left:3px solid #26bfa4;padding:10px 14px;margin:12px 0}.review{white-space:pre-wrap;background:#0b1720;border:1px solid #426073;border-radius:7px;padding:14px;line-height:1.5}#status{padding:8px 5vw;color:#8ee7ce;min-height:35px}#status.error{color:#ff9e9e}code{color:#a6e7db}[hidden]{display:none!important}a{color:#8ee7ce}@media(max-width:800px){main{display:block}.card{margin-bottom:20px}.grid{display:block}}
|
|
11
8
|
</style>
|
|
12
|
-
<header><h1>IntegSec MCP Gateway</h1><span class="muted">
|
|
13
|
-
<div id="status" role="status"></div>
|
|
9
|
+
<header><h1>IntegSec MCP Gateway</h1><span id="identity" class="muted">Loading session…</span></header>
|
|
10
|
+
<div id="status" role="status" aria-live="polite"></div>
|
|
14
11
|
<main>
|
|
15
|
-
<section id="token-card" class="card wide" hidden><h2>
|
|
16
|
-
<section id="
|
|
17
|
-
<section class="card"><h2>
|
|
18
|
-
<section class="card"><h2>
|
|
19
|
-
<section class="card
|
|
12
|
+
<section id="token-card" class="card wide" hidden><h2>Local administrator access</h2><p>Enter the local gateway admin token. It stays in this browser tab only.</p><div class="grid"><label>Admin token<input id="token" type="password" autocomplete="off"></label><div><br><button id="connect">Connect</button></div></div></section>
|
|
13
|
+
<section id="my-card" class="card wide" hidden><h2>My engagement</h2><p>Choose an approved engagement assigned to you. Your choice affects only your MCP calls. No engagement means no tools.</p><div id="my-engagements"></div><button id="clear-selection" class="secondary">Clear my selection</button><p id="my-selection" class="help"></p></section>
|
|
14
|
+
<section id="users-card" class="card wide admin-only" hidden><h2>Team users</h2><p>Cloudflare Access handles sign-in. People must also be on this exact-email list and assigned to an approved engagement. Administrators manage users, servers and scopes.</p><div id="users"></div><h3>Add a user</h3><div class="grid"><label>Email<input id="user-email" type="email" autocomplete="off" placeholder="tester@example.com"></label><label>Role<select id="user-role"><option value="tester">Tester</option><option value="admin">Administrator</option></select></label></div><button id="add-user">Add user</button></section>
|
|
15
|
+
<section id="upstreams-card" class="card admin-only" hidden><h2>MCP servers</h2><p>Start with a vetted preset or add an existing server. Adding a preset does not install software or start a tool; new servers begin disabled.</p><div id="upstreams"></div><h3>Curated presets</h3><div id="presets"></div><h3>Add a custom server</h3><div class="grid"><label>Name<input id="up-name" placeholder="burp"></label><label>Transport<select id="up-transport"><option value="stdio">Local process</option><option value="http">Streamable HTTP</option><option value="sse">SSE</option></select></label></div><label>Command or URL<input id="up-target" placeholder="installed-command or https://tools.example.com/mcp"></label><label>Arguments, one per line<textarea id="up-args" placeholder="mcp"></textarea></label><button id="add-upstream">Add disabled server</button></section>
|
|
16
|
+
<section id="scopes-card" class="card admin-only" hidden><h2>Engagement scopes</h2><p>Create a draft, review and approve it, then assign users. Approved scopes are immutable; duplicate one to revise it. Every user chooses their own current engagement above.</p><div id="scopes"></div></section>
|
|
17
|
+
<section id="editor-card" class="card wide admin-only" hidden><h2>Scope editor</h2><div id="editor-help" class="help">Start with a blank form or load an example. Replace all example targets before approval.</div><div class="actions"><button id="example-web" class="secondary">Web example</button><button id="example-internal" class="secondary">Internal network example</button><button id="example-cloud" class="secondary">Cloud example</button></div><div class="grid"><label>Engagement ID<input id="engagement" maxlength="64" placeholder="ENG-2026-001"></label><label>Client<input id="client" maxlength="120" placeholder="Example Client"></label><label>Operator<input id="operator" maxlength="120" placeholder="Test Team"></label><label>Starts (your local time)<input id="starts" type="datetime-local"></label><label>Ends (your local time)<input id="ends" type="datetime-local"></label><label>Execution profile<select id="profile"><option value="isolated">Isolated: local MCP only</option><option value="restricted">Restricted: approved MCP hosts</option><option value="connected">Connected: network MCP allowed</option></select></label></div><p class="muted">Use one authorized item per line. IPs require CIDR notation. Domains are hostnames. URLs must start with https:// or http://. Cloud accounts use aws:123456789012, azure:subscription-uuid, or gcp:project-id.</p><div class="grid"><label>IP ranges<textarea id="ips" placeholder="192.0.2.0/28"></textarea></label><label>Domains<textarea id="domains" placeholder="app.example.com"></textarea></label><label>URLs<textarea id="urls" placeholder="https://app.example.com/test/"></textarea></label><label>Cloud accounts<textarea id="cloud" placeholder="aws:123456789012"></textarea></label><label>Excluded targets<textarea id="excluded-targets" placeholder="admin.example.com"></textarea></label><label>Allowed MCP hosts (restricted profile)<textarea id="hosts" placeholder="tools.example.com"></textarea></label></div><h3>Authorized techniques</h3><p class="muted">Choose only activities the signed rules of engagement permit. Unselected categories are blocked. Unrecognized tool categories are blocked until mapped.</p><div id="techniques" class="choices"></div><h3>Explicit exclusions</h3><div id="excluded-techniques" class="choices"></div><div class="actions"><button id="save-scope">Save draft</button><button id="new-scope" class="secondary">New blank draft</button></div><h3>Review before approval</h3><div id="scope-review" class="review"></div></section>
|
|
18
|
+
<section class="card wide"><h2>Client connection</h2><p id="connection">Sign in to see your MCP endpoint.</p><p class="muted">Local Burp tools run on each tester’s laptop through their local gateway. The hosted gateway cannot reach a tester’s laptop automatically.</p><p class="muted">Cloudflare Access supports passkeys for faster repeat sign-in when enabled for this application. Email codes remain available for enrollment and recovery.</p></section>
|
|
20
19
|
</main>
|
|
21
20
|
<script src="/admin.js" defer></script>
|
|
22
21
|
</html>
|
|
@@ -1,109 +1,216 @@
|
|
|
1
1
|
const $ = (id) => document.getElementById(id);
|
|
2
|
+
const token = $('token');
|
|
3
|
+
token.value = sessionStorage.getItem('gateway-token') || '';
|
|
4
|
+
let mode = 'token';
|
|
5
|
+
let role = 'admin';
|
|
6
|
+
let email = null;
|
|
2
7
|
let state = null;
|
|
3
8
|
let editing = null;
|
|
4
|
-
let
|
|
9
|
+
let viewingStatus = 'draft';
|
|
5
10
|
let bootstrapAdmins = new Set();
|
|
6
|
-
const
|
|
7
|
-
|
|
11
|
+
const techniques = [
|
|
12
|
+
['recon', 'Reconnaissance and discovery'], ['web_app', 'Web and API testing'],
|
|
13
|
+
['exploitation', 'Exploitation'], ['cloud_audit', 'Cloud assessment'],
|
|
14
|
+
['dos', 'Denial of service'], ['destructive', 'Destructive actions'],
|
|
15
|
+
['social_engineering', 'Social engineering'],
|
|
16
|
+
];
|
|
17
|
+
const presets = [
|
|
18
|
+
{ name: 'burp', label: 'Burp Suite', transport: 'sse', url: 'http://127.0.0.1:9876', local: true, source: 'https://github.com/PortSwigger/mcp-server', note: 'Enable the official MCP extension inside the tester’s local Burp.' },
|
|
19
|
+
{ name: 'semgrep', label: 'Semgrep', transport: 'stdio', command: 'semgrep', args: ['mcp'], local: true, source: 'https://github.com/semgrep/semgrep', note: 'Install and configure the official Semgrep CLI first.' },
|
|
20
|
+
{ name: 'trivy', label: 'Trivy', transport: 'stdio', command: 'trivy', args: ['mcp'], local: true, source: 'https://github.com/aquasecurity/trivy-mcp', note: 'Install Trivy and its official MCP plugin first; pin and review your version.' },
|
|
21
|
+
{ name: 'snyk', label: 'Snyk', transport: 'stdio', command: 'snyk', args: ['mcp', '-t', 'stdio'], local: true, source: 'https://github.com/snyk/studio-mcp', note: 'Install the Snyk CLI and authenticate locally first.' },
|
|
22
|
+
{ label: 'OWASP ZAP', source: 'https://www.zaproxy.org/docs/desktop/addons/mcp-integration/mcp-options/', note: 'Reference only: its MCP security key needs authenticated upstream support.' },
|
|
23
|
+
{ label: 'PortSwigger DAST', source: 'https://portswigger.net/burp/documentation/dast/user-guide/using-ai/mcp-server', note: 'Reference only: its hosted MCP endpoint requires an API key.' },
|
|
24
|
+
{ label: 'Prowler Cloud', source: 'https://prowler.com/prowler-cloud', note: 'Reference only: its hosted MCP endpoint requires an API key.' },
|
|
25
|
+
];
|
|
8
26
|
|
|
9
|
-
function status(message, error = false) {
|
|
27
|
+
function status(message, error = false) { $('status').textContent = message; $('status').classList.toggle('error', error); }
|
|
10
28
|
async function api(path, method = 'GET', data) {
|
|
11
|
-
const response = await fetch(path, { method, credentials: 'same-origin', cache: 'no-store', headers: {
|
|
12
|
-
|
|
29
|
+
const response = await fetch(path, { method, credentials: 'same-origin', cache: 'no-store', headers: {
|
|
30
|
+
...(mode === 'token' ? { Authorization: `Bearer ${token.value}` } : {}),
|
|
31
|
+
...(data !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
32
|
+
}, body: data !== undefined ? JSON.stringify(data) : undefined });
|
|
33
|
+
const result = await response.json().catch(() => ({}));
|
|
13
34
|
if (!response.ok) throw new Error(result.error || `HTTP ${response.status}`);
|
|
14
35
|
return result;
|
|
15
36
|
}
|
|
16
|
-
const lines = (id) => $(id).value.split(/\r?\n/).map(
|
|
37
|
+
const lines = (id) => $(id).value.split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
|
|
17
38
|
const setLines = (id, values) => { $(id).value = values.join('\n'); };
|
|
18
39
|
const make = (tag, value) => { const node = document.createElement(tag); node.textContent = value; return node; };
|
|
19
|
-
|
|
40
|
+
function button(label, action, style = 'secondary') {
|
|
41
|
+
const node = make('button', label); node.className = style;
|
|
42
|
+
node.addEventListener('click', () => Promise.resolve().then(action).catch((error) => status(error.message, true)));
|
|
43
|
+
return node;
|
|
44
|
+
}
|
|
45
|
+
function choiceList(container, name) {
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const [value, label] of techniques) {
|
|
48
|
+
const option = make('label', ''); const input = document.createElement('input');
|
|
49
|
+
input.type = 'checkbox'; input.name = name; input.value = value;
|
|
50
|
+
option.append(input, make('span', label)); container.append(option); result.push(input);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
const authorizedChoices = choiceList($('techniques'), 'authorized-technique');
|
|
55
|
+
const excludedChoices = choiceList($('excluded-techniques'), 'excluded-technique');
|
|
56
|
+
const checked = (choices) => choices.filter((item) => item.checked).map((item) => item.value);
|
|
57
|
+
function setChecked(choices, values) { for (const item of choices) item.checked = values.includes(item.value); }
|
|
58
|
+
function localDate(value) { const date = new Date(value); return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16); }
|
|
59
|
+
function draftFromForm() {
|
|
60
|
+
if (!$('starts').value || !$('ends').value) throw new Error('Enter both engagement dates.');
|
|
61
|
+
const start = new Date($('starts').value); const end = new Date($('ends').value);
|
|
62
|
+
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime())) throw new Error('Enter valid engagement dates.');
|
|
63
|
+
return { profile: $('profile').value, allowedHosts: lines('hosts'), manifest: {
|
|
64
|
+
engagement_id: $('engagement').value.trim(), client: $('client').value.trim(), operator: $('operator').value.trim(),
|
|
65
|
+
authorized_targets: { ip_ranges: lines('ips'), domains: lines('domains'), urls: lines('urls'), cloud_accounts: lines('cloud') },
|
|
66
|
+
excluded_targets: lines('excluded-targets'), authorized_techniques: checked(authorizedChoices),
|
|
67
|
+
excluded_techniques: checked(excludedChoices), engagement_window: { start: start.toISOString(), end: end.toISOString() },
|
|
68
|
+
} };
|
|
69
|
+
}
|
|
70
|
+
function summarize(scope) {
|
|
71
|
+
const m = scope.manifest;
|
|
72
|
+
return `Engagement: ${m.engagement_id || '(missing)'}\nClient: ${m.client || '(missing)'} · Operator: ${m.operator || '(missing)'}\nWindow: ${m.engagement_window.start} → ${m.engagement_window.end}\nProfile: ${scope.profile}\nTargets: ${Object.values(m.authorized_targets).flat().join(', ') || '(none)'}\nExcluded targets: ${m.excluded_targets.join(', ') || '(none)'}\nAuthorized techniques: ${m.authorized_techniques.join(', ') || '(none)'}\nExcluded techniques: ${m.excluded_techniques.join(', ') || '(none)'}\nAllowed MCP hosts: ${scope.allowedHosts.join(', ') || '(none)'}`;
|
|
73
|
+
}
|
|
74
|
+
function reviewForm() { try { $('scope-review').textContent = summarize(draftFromForm()); } catch (error) { $('scope-review').textContent = error.message; } }
|
|
75
|
+
for (const field of $('editor-card').querySelectorAll('input,select,textarea')) field.addEventListener('input', reviewForm);
|
|
20
76
|
|
|
21
77
|
async function refresh() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
78
|
+
const mine = await api('/api/engagements');
|
|
79
|
+
$('my-card').hidden = false;
|
|
80
|
+
const list = $('my-engagements'); list.replaceChildren();
|
|
81
|
+
for (const scope of mine.engagements) {
|
|
82
|
+
const active = mine.selectedScopeId === scope.id;
|
|
83
|
+
const windowOpen = Date.now() >= Date.parse(scope.manifest.engagement_window.start) && Date.now() <= Date.parse(scope.manifest.engagement_window.end);
|
|
84
|
+
const row = make('div', ''); row.className = 'row';
|
|
85
|
+
row.append(make('span', `${scope.manifest.engagement_id} · ${scope.manifest.client} · ${scope.profile} · ${windowOpen ? 'window open' : 'window closed'}${active ? ' · SELECTED' : ''}`));
|
|
86
|
+
const select = button(active ? 'Selected' : 'Select', async () => { await api('/api/my-scope', 'POST', { scopeId: scope.id }); await refresh(); });
|
|
87
|
+
select.disabled = active || !windowOpen;
|
|
88
|
+
row.append(select); list.append(row);
|
|
89
|
+
}
|
|
90
|
+
if (!mine.engagements.length) list.append(make('p', 'No approved engagements have been assigned to you yet. Ask an administrator.'));
|
|
91
|
+
const selected = mine.engagements.find((item) => item.id === mine.selectedScopeId);
|
|
92
|
+
const selectedWindowOpen = selected && Date.now() >= Date.parse(selected.manifest.engagement_window.start) && Date.now() <= Date.parse(selected.manifest.engagement_window.end);
|
|
93
|
+
$('my-selection').textContent = selected ? selectedWindowOpen ? `Current engagement: ${selected.manifest.engagement_id}. Its scope applies only to your MCP calls.` : `Engagement ${selected.manifest.engagement_id} is outside its approved window. Your MCP client will see no tools.` : 'No engagement selected. Your MCP client will see no tools.';
|
|
94
|
+
$('connection').textContent = `MCP endpoint: ${location.origin}/mcp. ${selected ? `Current engagement: ${selected.manifest.engagement_id}.` : 'Select an engagement before using tools.'}`;
|
|
95
|
+
if (role === 'admin') {
|
|
96
|
+
state = await api('/api/state');
|
|
97
|
+
renderUsers(); renderUpstreams(); renderPresets(); renderScopes();
|
|
98
|
+
}
|
|
99
|
+
status('Gateway state loaded.');
|
|
100
|
+
}
|
|
101
|
+
function renderUsers() {
|
|
102
|
+
if (mode !== 'access') return;
|
|
103
|
+
const container = $('users'); container.replaceChildren();
|
|
104
|
+
for (const user of state.users) {
|
|
105
|
+
const row = make('div', ''); row.className = 'row'; const bootstrap = bootstrapAdmins.has(user.email);
|
|
106
|
+
row.append(make('span', `${user.email} · ${user.role}${bootstrap ? ' · bootstrap administrator' : ''}`));
|
|
107
|
+
if (!bootstrap) {
|
|
108
|
+
row.append(button(user.role === 'admin' ? 'Make tester' : 'Make admin', async () => { await api(`/api/users/${user.id}`, 'PATCH', { role: user.role === 'admin' ? 'tester' : 'admin' }); await refresh(); }));
|
|
109
|
+
row.append(button('Remove', async () => { if (confirm(`Remove ${user.email} and all engagement assignments?`)) { await api(`/api/users/${user.id}`, 'DELETE'); await refresh(); } }, 'danger'));
|
|
34
110
|
}
|
|
111
|
+
container.append(row);
|
|
35
112
|
}
|
|
36
|
-
|
|
113
|
+
}
|
|
114
|
+
function renderUpstreams() {
|
|
115
|
+
const container = $('upstreams'); container.replaceChildren();
|
|
37
116
|
for (const item of state.upstreams) {
|
|
38
117
|
const row = make('div', ''); row.className = 'row';
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
row.append(
|
|
42
|
-
|
|
43
|
-
|
|
118
|
+
row.append(make('span', `${item.name} · ${item.transport} · ${item.enabled ? 'Enabled' : 'Disabled'} · ${item.transport === 'stdio' ? item.command : item.url}`));
|
|
119
|
+
row.append(button(item.enabled ? 'Disable' : 'Enable', async () => { await api(`/api/upstreams/${item.id}`, 'PATCH', { enabled: !item.enabled }); await refresh(); }));
|
|
120
|
+
row.append(button('Remove', async () => { if (confirm(`Remove server ${item.name}?`)) { await api(`/api/upstreams/${item.id}`, 'DELETE'); await refresh(); } }, 'danger'));
|
|
121
|
+
container.append(row);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function renderPresets() {
|
|
125
|
+
const container = $('presets'); container.replaceChildren();
|
|
126
|
+
for (const preset of presets) {
|
|
127
|
+
const row = make('div', ''); row.className = 'row'; const detail = make('div', `${preset.label} · ${preset.note} `);
|
|
128
|
+
const link = make('a', 'Official information'); link.href = preset.source; link.target = '_blank'; link.rel = 'noopener noreferrer'; detail.append(link);
|
|
129
|
+
row.append(detail);
|
|
130
|
+
if (preset.name) {
|
|
131
|
+
const add = button('Add disabled', async () => { await api('/api/upstreams', 'POST', { name: preset.name, enabled: false, transport: preset.transport, command: preset.command, args: preset.args || [], url: preset.url }); await refresh(); });
|
|
132
|
+
add.disabled = (mode === 'access' && preset.local) || state.upstreams.some((item) => item.name === preset.name);
|
|
133
|
+
if (mode === 'access' && preset.local) add.title = 'This preset runs on a local gateway, not the hosted Cloudflare gateway.';
|
|
134
|
+
row.append(add);
|
|
135
|
+
}
|
|
136
|
+
container.append(row);
|
|
44
137
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
138
|
+
}
|
|
139
|
+
function renderScopes() {
|
|
140
|
+
const container = $('scopes'); container.replaceChildren();
|
|
141
|
+
for (const scope of state.scopes) {
|
|
48
142
|
const row = make('div', ''); row.className = 'row';
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
143
|
+
const label = make('div', `${scope.manifest.engagement_id} · ${scope.manifest.client}`);
|
|
144
|
+
label.append(make('span', scope.status)); label.lastChild.className = 'pill';
|
|
145
|
+
row.append(label);
|
|
146
|
+
const actions = make('div', ''); actions.className = 'actions';
|
|
147
|
+
actions.append(button(scope.status === 'draft' ? 'Edit' : 'View', () => loadScope(scope)));
|
|
148
|
+
actions.append(button('Duplicate', async () => { const copy = await api(`/api/scopes/${scope.id}/duplicate`, 'POST'); await refresh(); loadScope(copy); status('Draft copy created. Edit and approve it when ready.'); }));
|
|
149
|
+
if (scope.status === 'draft') actions.append(button('Review and approve', async () => {
|
|
150
|
+
if (!confirm(`Approve this scope for ${scope.manifest.engagement_id}?\n\n${summarize(scope)}\n\nApproval makes it eligible for assignment. No user is automatically assigned.`)) return;
|
|
151
|
+
await api(`/api/scopes/${scope.id}/approve`, 'POST'); await refresh();
|
|
152
|
+
}));
|
|
153
|
+
actions.append(button('Delete', async () => { if (confirm(`Delete ${scope.manifest.engagement_id} (${scope.status})? Assigned users will lose this engagement.`)) { await api(`/api/scopes/${scope.id}`, 'DELETE'); await refresh(); } }, 'danger'));
|
|
154
|
+
row.append(actions); container.append(row);
|
|
155
|
+
if (scope.status === 'approved' && mode === 'access') {
|
|
156
|
+
const assignment = make('div', ''); assignment.className = 'choices';
|
|
157
|
+
const inputs = state.users.map((user) => { const label = make('label', ''); const input = document.createElement('input'); input.type = 'checkbox'; input.value = user.email; input.checked = scope.assignedUserEmails.includes(user.email); label.append(input, make('span', user.email)); assignment.append(label); return input; });
|
|
158
|
+
assignment.append(button('Save assignments', async () => { await api(`/api/scopes/${scope.id}/assignments`, 'PUT', { emails: inputs.filter((item) => item.checked).map((item) => item.value) }); await refresh(); }));
|
|
159
|
+
container.append(assignment);
|
|
54
160
|
}
|
|
55
|
-
scopes.append(row);
|
|
56
161
|
}
|
|
57
|
-
|
|
58
|
-
status('Gateway state loaded.');
|
|
162
|
+
if (!state.scopes.length) container.append(make('p', 'No scopes yet. Use the editor below to create a draft.'));
|
|
59
163
|
}
|
|
60
|
-
function loadScope(
|
|
61
|
-
editing =
|
|
62
|
-
const m =
|
|
164
|
+
function loadScope(scope) {
|
|
165
|
+
editing = scope.status === 'draft' ? scope.id : null; viewingStatus = scope.status;
|
|
166
|
+
const m = scope.manifest;
|
|
63
167
|
$('engagement').value = m.engagement_id; $('client').value = m.client; $('operator').value = m.operator;
|
|
64
|
-
$('starts').value = m.engagement_window.start
|
|
65
|
-
$('profile').value =
|
|
168
|
+
$('starts').value = localDate(m.engagement_window.start); $('ends').value = localDate(m.engagement_window.end);
|
|
169
|
+
$('profile').value = scope.profile; setLines('ips', m.authorized_targets.ip_ranges); setLines('domains', m.authorized_targets.domains);
|
|
66
170
|
setLines('urls', m.authorized_targets.urls); setLines('cloud', m.authorized_targets.cloud_accounts);
|
|
67
|
-
setLines('excluded-targets', m.excluded_targets); setLines('
|
|
68
|
-
|
|
69
|
-
|
|
171
|
+
setLines('excluded-targets', m.excluded_targets); setLines('hosts', scope.allowedHosts);
|
|
172
|
+
setChecked(authorizedChoices, m.authorized_techniques); setChecked(excludedChoices, m.excluded_techniques);
|
|
173
|
+
$('save-scope').disabled = scope.status !== 'draft';
|
|
174
|
+
$('editor-help').textContent = scope.status === 'draft' ? 'Editing a draft. Review the summary before approval.' : 'Viewing an immutable scope. Choose Duplicate to create an editable draft.';
|
|
175
|
+
reviewForm(); $('editor-card').scrollIntoView({ behavior: 'smooth' });
|
|
176
|
+
}
|
|
177
|
+
function clearEditor() {
|
|
178
|
+
editing = null; viewingStatus = 'draft';
|
|
179
|
+
for (const id of ['engagement', 'client', 'operator', 'starts', 'ends', 'ips', 'domains', 'urls', 'cloud', 'excluded-targets', 'hosts']) $(id).value = '';
|
|
180
|
+
$('profile').value = 'isolated'; setChecked(authorizedChoices, []); setChecked(excludedChoices, []);
|
|
181
|
+
$('save-scope').disabled = false; $('editor-help').textContent = 'New draft. Enter the exact authorized targets, window and techniques.'; reviewForm();
|
|
182
|
+
}
|
|
183
|
+
function loadExample(kind) {
|
|
184
|
+
clearEditor();
|
|
185
|
+
const now = Date.now(); $('starts').value = localDate(now + 3600000); $('ends').value = localDate(now + 25 * 3600000);
|
|
186
|
+
$('engagement').value = `EXAMPLE-${kind.toUpperCase()}`; $('client').value = 'Example Client'; $('operator').value = 'Test Team';
|
|
187
|
+
if (kind === 'web') { setLines('domains', ['app.example.com']); setLines('urls', ['https://app.example.com/test/']); setChecked(authorizedChoices, ['recon', 'web_app']); }
|
|
188
|
+
if (kind === 'internal') { setLines('ips', ['192.0.2.0/28']); setChecked(authorizedChoices, ['recon']); }
|
|
189
|
+
if (kind === 'cloud') { setLines('cloud', ['aws:123456789012']); setChecked(authorizedChoices, ['cloud_audit']); }
|
|
190
|
+
$('editor-help').textContent = 'Example only: replace the client, engagement ID and all example targets with the signed rules of engagement before approval.';
|
|
191
|
+
reviewForm();
|
|
70
192
|
}
|
|
71
|
-
$('connect').onclick = () => { sessionStorage.setItem('gateway-token', token.value); refresh().catch(
|
|
72
|
-
$('
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
} catch (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
$('up-name').value = ''; $('up-target').value = ''; $('up-args').value = ''; await refresh();
|
|
84
|
-
} catch (e) { status(e.message, true); }
|
|
85
|
-
};
|
|
86
|
-
$('new-scope').onclick = () => { editing = null; for (const id of ['engagement','client','operator','starts','ends','ips','domains','urls','cloud','excluded-targets','techniques','excluded-techniques','hosts']) $(id).value = ''; $('profile').value = 'isolated'; status('New draft.'); };
|
|
87
|
-
$('save-scope').onclick = async () => {
|
|
88
|
-
try {
|
|
89
|
-
const scope = { profile: $('profile').value, allowedHosts: lines('hosts'), manifest: {
|
|
90
|
-
engagement_id: $('engagement').value.trim(), client: $('client').value.trim(), operator: $('operator').value.trim(),
|
|
91
|
-
authorized_targets: { ip_ranges: lines('ips'), domains: lines('domains'), urls: lines('urls'), cloud_accounts: lines('cloud') },
|
|
92
|
-
excluded_targets: lines('excluded-targets'), authorized_techniques: lines('techniques'), excluded_techniques: lines('excluded-techniques'),
|
|
93
|
-
engagement_window: { start: new Date($('starts').value).toISOString(), end: new Date($('ends').value).toISOString() },
|
|
94
|
-
} };
|
|
95
|
-
await api(editing ? `/api/scopes/${editing}` : '/api/scopes', editing ? 'PUT' : 'POST', scope);
|
|
96
|
-
editing = null; await refresh(); status('Draft saved. Review it before activation.');
|
|
97
|
-
} catch (e) { status(e.message, true); }
|
|
98
|
-
};
|
|
193
|
+
$('connect').onclick = () => { sessionStorage.setItem('gateway-token', token.value); refresh().catch((error) => status(error.message, true)); };
|
|
194
|
+
$('clear-selection').onclick = () => api('/api/my-scope', 'POST', { scopeId: null }).then(refresh).catch((error) => status(error.message, true));
|
|
195
|
+
$('add-user').onclick = async () => { try { await api('/api/users', 'POST', { email: $('user-email').value.trim(), role: $('user-role').value }); $('user-email').value = ''; await refresh(); } catch (error) { status(error.message, true); } };
|
|
196
|
+
$('add-upstream').onclick = async () => { try { const transport = $('up-transport').value; await api('/api/upstreams', 'POST', { name: $('up-name').value.trim(), enabled: false, transport,
|
|
197
|
+
...(transport === 'stdio' ? { command: $('up-target').value.trim(), args: lines('up-args') } : { url: $('up-target').value.trim(), args: [] }) });
|
|
198
|
+
$('up-name').value = ''; $('up-target').value = ''; $('up-args').value = ''; await refresh(); } catch (error) { status(error.message, true); } };
|
|
199
|
+
$('new-scope').onclick = clearEditor;
|
|
200
|
+
for (const kind of ['web', 'internal', 'cloud']) $(`example-${kind}`).onclick = () => loadExample(kind);
|
|
201
|
+
$('save-scope').onclick = async () => { try { if (viewingStatus !== 'draft') throw new Error('Duplicate an approved or archived scope to edit it.');
|
|
202
|
+
const scope = draftFromForm(); await api(editing ? `/api/scopes/${editing}` : '/api/scopes', editing ? 'PUT' : 'POST', scope);
|
|
203
|
+
clearEditor(); await refresh(); status('Draft saved. Review it, approve it, then assign users.');
|
|
204
|
+
} catch (error) { status(error.message, true); } };
|
|
99
205
|
async function initialize() {
|
|
100
206
|
const response = await fetch('/api/session', { credentials: 'same-origin', cache: 'no-store' });
|
|
101
|
-
const session = await response.json();
|
|
102
|
-
|
|
103
|
-
mode = session.mode;
|
|
207
|
+
const session = await response.json(); if (!response.ok) throw new Error(session.error || `HTTP ${response.status}`);
|
|
208
|
+
mode = session.mode; role = session.role || 'admin'; email = session.email || null;
|
|
104
209
|
bootstrapAdmins = new Set(session.bootstrapAdminEmails || []);
|
|
210
|
+
$('identity').textContent = email ? `${email} · ${role}` : 'Local gateway';
|
|
105
211
|
$('token-card').hidden = mode !== 'token';
|
|
106
|
-
|
|
212
|
+
for (const card of document.querySelectorAll('.admin-only')) card.hidden = role !== 'admin' || (card.id === 'users-card' && mode !== 'access');
|
|
107
213
|
if (mode === 'access' || token.value) await refresh();
|
|
214
|
+
reviewForm();
|
|
108
215
|
}
|
|
109
|
-
initialize().catch(
|
|
216
|
+
initialize().catch((error) => status(error.message, true));
|
|
@@ -9,7 +9,6 @@ export declare class GatewayRuntime {
|
|
|
9
9
|
private clients;
|
|
10
10
|
private pending;
|
|
11
11
|
private proxies;
|
|
12
|
-
private proxyScope?;
|
|
13
12
|
constructor(store: GatewayStore, hosted: boolean, requiredAuditSink?: ((entry: AuditEntry) => Promise<void>) | undefined, isActorAllowed?: ((email?: string) => boolean) | undefined);
|
|
14
13
|
sync(): Promise<void>;
|
|
15
14
|
private scopeProxy;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/gateway/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/gateway/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAKnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAiE,MAAM,YAAY,CAAC;AAIzG,qBAAa,cAAc;IAKb,QAAQ,CAAC,KAAK,EAAE,YAAY;IAAE,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAW,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IAAwC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAJvL,OAAO,CAAC,OAAO,CAA4D;IAC3E,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,OAAO,CAA4C;gBAEtC,KAAK,EAAE,YAAY,EAAmB,MAAM,EAAE,OAAO,EAAmB,iBAAiB,CAAC,GAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,aAAA,EAAmB,cAAc,CAAC,GAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,aAAA;IAE9M,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAY3B,OAAO,CAAC,UAAU;YAWJ,SAAS;IA2BvB,OAAO,CAAC,SAAS;IAOjB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM;IAyE9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
|
|
@@ -6,6 +6,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
6
6
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
import { ScopeEnforcementProxy } from "../proxy.js";
|
|
8
8
|
import { loadConfig } from "../config.js";
|
|
9
|
+
import { TechniqueChecker, loadTechniqueMappings } from "../technique-checker.js";
|
|
9
10
|
import { activeScope, permitsUpstream } from "./state.js";
|
|
10
11
|
const toolError = (message) => ({ content: [{ type: "text", text: message }], isError: true });
|
|
11
12
|
export class GatewayRuntime {
|
|
@@ -16,7 +17,6 @@ export class GatewayRuntime {
|
|
|
16
17
|
clients = new Map();
|
|
17
18
|
pending = new Map();
|
|
18
19
|
proxies = new Map();
|
|
19
|
-
proxyScope;
|
|
20
20
|
constructor(store, hosted, requiredAuditSink, isActorAllowed) {
|
|
21
21
|
this.store = store;
|
|
22
22
|
this.hosted = hosted;
|
|
@@ -33,18 +33,14 @@ export class GatewayRuntime {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
this.proxies.clear();
|
|
36
|
-
this.proxyScope = undefined;
|
|
37
36
|
}
|
|
38
37
|
scopeProxy(scope, serverName) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.proxyScope = scope;
|
|
42
|
-
}
|
|
43
|
-
let proxy = this.proxies.get(serverName);
|
|
38
|
+
const key = `${scope.id}:${scope.updatedAt}:${serverName}`;
|
|
39
|
+
let proxy = this.proxies.get(key);
|
|
44
40
|
if (!proxy) {
|
|
45
|
-
proxy = new ScopeEnforcementProxy(scope.manifest, loadConfig(), this.requiredAuditSink);
|
|
41
|
+
proxy = new ScopeEnforcementProxy(scope.manifest, loadConfig(), this.requiredAuditSink, true);
|
|
46
42
|
proxy.setServerName(serverName);
|
|
47
|
-
this.proxies.set(
|
|
43
|
+
this.proxies.set(key, proxy);
|
|
48
44
|
}
|
|
49
45
|
return proxy;
|
|
50
46
|
}
|
|
@@ -59,7 +55,7 @@ export class GatewayRuntime {
|
|
|
59
55
|
const connection = (async () => {
|
|
60
56
|
if (cached)
|
|
61
57
|
await cached.client.close().catch(() => undefined);
|
|
62
|
-
const client = new Client({ name: "agentic-pentest-gateway", version: "0.3.
|
|
58
|
+
const client = new Client({ name: "agentic-pentest-gateway", version: "0.3.2" });
|
|
63
59
|
const transport = upstream.transport === "stdio"
|
|
64
60
|
? new StdioClientTransport({ command: upstream.command, args: upstream.args, stderr: "pipe" })
|
|
65
61
|
: upstream.transport === "sse"
|
|
@@ -91,11 +87,13 @@ export class GatewayRuntime {
|
|
|
91
87
|
});
|
|
92
88
|
}
|
|
93
89
|
createServer(actor, email) {
|
|
94
|
-
const server = new Server({ name: "agentic-pentest-gateway", version: "0.3.
|
|
90
|
+
const server = new Server({ name: "agentic-pentest-gateway", version: "0.3.2" }, { capabilities: { tools: { listChanged: true } } });
|
|
91
|
+
const config = loadConfig();
|
|
92
|
+
const checker = new TechniqueChecker(config.techniqueMapPath ? loadTechniqueMappings(config.techniqueMapPath) : undefined);
|
|
95
93
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
96
94
|
if (this.isActorAllowed && !this.isActorAllowed(email))
|
|
97
95
|
return { tools: [] };
|
|
98
|
-
const scope = activeScope(this.store.snapshot());
|
|
96
|
+
const scope = activeScope(this.store.snapshot(), email);
|
|
99
97
|
if (!scope)
|
|
100
98
|
return { tools: [] };
|
|
101
99
|
const groups = await Promise.all(this.available(scope).map(async (upstream) => {
|
|
@@ -105,12 +103,16 @@ export class GatewayRuntime {
|
|
|
105
103
|
let cursor;
|
|
106
104
|
do {
|
|
107
105
|
const page = await client.listTools(cursor ? { cursor } : undefined, { timeout: 10_000 });
|
|
108
|
-
for (const tool of page.tools)
|
|
106
|
+
for (const tool of page.tools) {
|
|
107
|
+
const category = checker.getCategory(tool.name);
|
|
108
|
+
if (scope.manifest.authorized_techniques.length && (!category || !scope.manifest.authorized_techniques.includes(category) || scope.manifest.excluded_techniques.includes(category)))
|
|
109
|
+
continue;
|
|
109
110
|
tools.push({
|
|
110
111
|
name: `${upstream.name}__${tool.name}`,
|
|
111
112
|
description: tool.description,
|
|
112
113
|
inputSchema: tool.inputSchema,
|
|
113
114
|
});
|
|
115
|
+
}
|
|
114
116
|
cursor = page.nextCursor;
|
|
115
117
|
} while (cursor);
|
|
116
118
|
}
|
|
@@ -124,9 +126,9 @@ export class GatewayRuntime {
|
|
|
124
126
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
125
127
|
if (this.isActorAllowed && !this.isActorAllowed(email))
|
|
126
128
|
return toolError("User access was revoked.");
|
|
127
|
-
const scope = activeScope(this.store.snapshot());
|
|
129
|
+
const scope = activeScope(this.store.snapshot(), email);
|
|
128
130
|
if (!scope)
|
|
129
|
-
return toolError("No
|
|
131
|
+
return toolError("No approved engagement selected for this user.");
|
|
130
132
|
const upstream = this.available(scope).find((item) => request.params.name.startsWith(`${item.name}__`));
|
|
131
133
|
if (!upstream)
|
|
132
134
|
return toolError("Tool server is disabled or prohibited by the active execution profile.");
|
|
@@ -151,7 +153,7 @@ export class GatewayRuntime {
|
|
|
151
153
|
if (!found)
|
|
152
154
|
return toolError("Tool is no longer advertised by the upstream server.");
|
|
153
155
|
const latest = this.store.snapshot();
|
|
154
|
-
const latestScope = activeScope(latest);
|
|
156
|
+
const latestScope = activeScope(latest, email);
|
|
155
157
|
const latestUpstream = latest.upstreams.find((item) => item.id === upstream.id);
|
|
156
158
|
if (!latestScope || latestScope.id !== scope.id || latestScope.updatedAt !== scope.updatedAt || !latestUpstream || JSON.stringify(latestUpstream) !== JSON.stringify(upstream) || !this.available(latestScope).some((item) => item.id === latestUpstream.id)) {
|
|
157
159
|
return toolError("Scope or tool configuration changed before dispatch.");
|