@poolzin/pool-bot 2026.3.17 → 2026.3.19
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/CHANGELOG.md +63 -0
- package/dist/agents/tools/web-fetch.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/commands/skills-openclaw.command.js +123 -0
- package/dist/config/paths.js +7 -0
- package/dist/infra/net/fetch-guard.js +191 -146
- package/dist/media/fetch.js +83 -112
- package/dist/media/inbound-path-policy.js +90 -97
- package/dist/media/read-response-with-limit.js +49 -26
- package/dist/media-understanding/attachments.js +1 -1
- package/dist/plugin-sdk/audio.js +7 -0
- package/dist/plugin-sdk/bluebubbles.js +7 -0
- package/dist/plugin-sdk/browser.js +7 -0
- package/dist/plugin-sdk/canvas.js +7 -0
- package/dist/plugin-sdk/cron.js +7 -0
- package/dist/plugin-sdk/discord-actions.js +6 -0
- package/dist/plugin-sdk/discord.js +7 -0
- package/dist/plugin-sdk/image.js +7 -0
- package/dist/plugin-sdk/imessage.js +6 -0
- package/dist/plugin-sdk/keyed-async-queue.js +35 -0
- package/dist/plugin-sdk/media.js +8 -0
- package/dist/plugin-sdk/memory.js +7 -0
- package/dist/plugin-sdk/pdf.js +7 -0
- package/dist/plugin-sdk/sessions.js +7 -0
- package/dist/plugin-sdk/signal.js +6 -0
- package/dist/plugin-sdk/slack-actions.js +7 -0
- package/dist/plugin-sdk/slack.js +7 -0
- package/dist/plugin-sdk/telegram-actions.js +6 -0
- package/dist/plugin-sdk/telegram.js +6 -0
- package/dist/plugin-sdk/test-utils.js +110 -0
- package/dist/plugin-sdk/tts.js +7 -0
- package/dist/plugin-sdk/whatsapp.js +6 -0
- package/dist/providers/github-copilot-auth.js +53 -76
- package/dist/providers/github-copilot-models.js +63 -35
- package/dist/providers/github-copilot-token.js +46 -89
- package/dist/security/audit-findings.js +165 -0
- package/dist/security/audit.js +141 -572
- package/dist/skills/openclaw-skill-loader.js +191 -0
- package/dist/slack/monitor/media.js +2 -1
- package/docs/improvements/OPENCLAW-IMPLEMENTATION.md +45 -0
- package/docs/skills/openclaw-integration.md +295 -0
- package/docs/testing/TEST-PLAN-2026-03-13.md +338 -0
- package/extensions/acpx/package.json +19 -0
- package/extensions/acpx/poolbot.plugin.json +9 -0
- package/extensions/acpx/src/index.ts +34 -0
- package/extensions/bluebubbles/src/runtime.ts +1 -0
- package/extensions/dexter/poolbot.plugin.json +10 -6
- package/extensions/diffs/package.json +15 -0
- package/extensions/diffs/poolbot.plugin.json +10 -0
- package/extensions/diffs/src/index.ts +106 -0
- package/extensions/discord/src/runtime.ts +1 -0
- package/extensions/feishu/src/runtime.ts +1 -0
- package/extensions/github-copilot/package.json +28 -0
- package/extensions/github-copilot/poolbot.plugin.json +33 -0
- package/extensions/github-copilot/src/index.ts +126 -0
- package/extensions/github-copilot/tsconfig.json +10 -0
- package/extensions/googlechat/src/runtime.ts +1 -0
- package/extensions/hackingtool/poolbot.plugin.json +33 -25
- package/extensions/hexstrike-ai/poolbot.plugin.json +16 -8
- package/extensions/imessage/src/runtime.ts +1 -0
- package/extensions/irc/src/runtime.ts +1 -0
- package/extensions/line/src/runtime.ts +1 -0
- package/extensions/matrix/src/runtime.ts +1 -0
- package/extensions/mattermost/src/mattermost/monitor-helpers.ts +10 -1
- package/extensions/mattermost/src/runtime.ts +6 -3
- package/extensions/msteams/src/runtime.ts +1 -0
- package/extensions/nextcloud-talk/src/runtime.ts +1 -0
- package/extensions/nostr/src/runtime.ts +5 -2
- package/extensions/ollama/package.json +20 -0
- package/extensions/ollama/poolbot.plugin.json +18 -0
- package/extensions/ollama/src/index.ts +95 -0
- package/extensions/sglang/package.json +18 -0
- package/extensions/sglang/poolbot.plugin.json +17 -0
- package/extensions/sglang/src/index.ts +62 -0
- package/extensions/signal/src/runtime.ts +1 -0
- package/extensions/slack/src/runtime.ts +1 -0
- package/extensions/telegram/src/runtime.ts +1 -0
- package/extensions/test-utils/package.json +17 -0
- package/extensions/test-utils/poolbot.plugin.json +16 -0
- package/extensions/test-utils/src/index.ts +220 -0
- package/extensions/tlon/src/runtime.ts +1 -0
- package/extensions/twitch/src/runtime.ts +1 -0
- package/extensions/vllm/package.json +19 -0
- package/extensions/vllm/poolbot.plugin.json +17 -0
- package/extensions/vllm/src/index.ts +90 -0
- package/extensions/whatsapp/src/runtime.ts +1 -0
- package/extensions/zalo/src/runtime.ts +1 -0
- package/extensions/zalouser/src/runtime.ts +1 -0
- package/package.json +77 -3
package/dist/security/audit.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Pool Bot Security Audit Framework
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive security auditing for configuration, gateway, channels, and plugins.
|
|
5
|
+
* Ported from OpenClaw with adaptations for Pool Bot architecture.
|
|
6
|
+
*/
|
|
6
7
|
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
|
7
|
-
import {
|
|
8
|
-
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
|
9
|
-
import { resolveGatewayProbeAuth } from "../gateway/probe-auth.js";
|
|
10
|
-
import { probeGateway } from "../gateway/probe.js";
|
|
11
|
-
import { collectChannelSecurityFindings } from "./audit-channel.js";
|
|
12
|
-
import { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectGatewayHttpNoAuthFindings, collectGatewayHttpSessionKeyOverrideFindings, collectHooksHardeningFindings, collectIncludeFilePermFindings, collectInstalledSkillsCodeSafetyFindings, collectSandboxBrowserHashLabelFindings, collectMinimalProfileOverrideFindings, collectModelHygieneFindings, collectNodeDenyCommandPatternFindings, collectSmallModelRiskFindings, collectSandboxDangerousConfigFindings, collectSandboxDockerNoopFindings, collectPluginsTrustFindings, collectSecretsInConfigFindings, collectPluginsCodeSafetyFindings, collectStateDeepFilesystemFindings, collectSyncedFolderFindings, readConfigSnapshotForAudit, } from "./audit-extra.js";
|
|
13
|
-
import { formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions, } from "./audit-fs.js";
|
|
14
|
-
import { DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools.js";
|
|
8
|
+
import { collectSecurityAuditFindings } from "./audit-findings.js";
|
|
15
9
|
function countBySeverity(findings) {
|
|
16
10
|
let critical = 0;
|
|
17
11
|
let warn = 0;
|
|
@@ -29,575 +23,150 @@ function countBySeverity(findings) {
|
|
|
29
23
|
}
|
|
30
24
|
return { critical, warn, info };
|
|
31
25
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
function collectEnabledInsecureOrDangerousFlags(cfg) {
|
|
39
|
-
const enabledFlags = [];
|
|
40
|
-
if (cfg.gateway?.controlUi?.allowInsecureAuth === true) {
|
|
41
|
-
enabledFlags.push("gateway.controlUi.allowInsecureAuth=true");
|
|
42
|
-
}
|
|
43
|
-
if (cfg.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true) {
|
|
44
|
-
enabledFlags.push("gateway.controlUi.dangerouslyDisableDeviceAuth=true");
|
|
45
|
-
}
|
|
46
|
-
if (cfg.hooks?.gmail?.allowUnsafeExternalContent === true) {
|
|
47
|
-
enabledFlags.push("hooks.gmail.allowUnsafeExternalContent=true");
|
|
48
|
-
}
|
|
49
|
-
if (Array.isArray(cfg.hooks?.mappings)) {
|
|
50
|
-
for (const [index, mapping] of cfg.hooks.mappings.entries()) {
|
|
51
|
-
if (mapping?.allowUnsafeExternalContent === true) {
|
|
52
|
-
enabledFlags.push(`hooks.mappings[${index}].allowUnsafeExternalContent=true`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (cfg.tools?.exec?.applyPatch?.workspaceOnly === false) {
|
|
57
|
-
enabledFlags.push("tools.exec.applyPatch.workspaceOnly=false");
|
|
58
|
-
}
|
|
59
|
-
return enabledFlags;
|
|
60
|
-
}
|
|
61
|
-
async function collectFilesystemFindings(params) {
|
|
26
|
+
/**
|
|
27
|
+
* Perform comprehensive security audit
|
|
28
|
+
*/
|
|
29
|
+
export async function performSecurityAudit(options) {
|
|
30
|
+
const ts = Date.now();
|
|
62
31
|
const findings = [];
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
targetPath: params.stateDir,
|
|
85
|
-
perms: stateDirPerms,
|
|
86
|
-
isDir: true,
|
|
87
|
-
posixMode: 0o700,
|
|
88
|
-
env: params.env,
|
|
89
|
-
}),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
else if (stateDirPerms.groupWritable) {
|
|
93
|
-
findings.push({
|
|
94
|
-
checkId: "fs.state_dir.perms_group_writable",
|
|
95
|
-
severity: "warn",
|
|
96
|
-
title: "State dir is group-writable",
|
|
97
|
-
detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; group users can write into your Pool Bot state.`,
|
|
98
|
-
remediation: formatPermissionRemediation({
|
|
99
|
-
targetPath: params.stateDir,
|
|
100
|
-
perms: stateDirPerms,
|
|
101
|
-
isDir: true,
|
|
102
|
-
posixMode: 0o700,
|
|
103
|
-
env: params.env,
|
|
104
|
-
}),
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
else if (stateDirPerms.groupReadable || stateDirPerms.worldReadable) {
|
|
108
|
-
findings.push({
|
|
109
|
-
checkId: "fs.state_dir.perms_readable",
|
|
110
|
-
severity: "warn",
|
|
111
|
-
title: "State dir is readable by others",
|
|
112
|
-
detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; consider restricting to 700.`,
|
|
113
|
-
remediation: formatPermissionRemediation({
|
|
114
|
-
targetPath: params.stateDir,
|
|
115
|
-
perms: stateDirPerms,
|
|
116
|
-
isDir: true,
|
|
117
|
-
posixMode: 0o700,
|
|
118
|
-
env: params.env,
|
|
119
|
-
}),
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
const configPerms = await inspectPathPermissions(params.configPath, {
|
|
124
|
-
env: params.env,
|
|
125
|
-
platform: params.platform,
|
|
126
|
-
exec: params.execIcacls,
|
|
32
|
+
const cfg = options.config;
|
|
33
|
+
const sourceConfig = options.sourceConfig ?? cfg;
|
|
34
|
+
const env = options.env ?? process.env;
|
|
35
|
+
const platform = options.platform ?? process.platform;
|
|
36
|
+
const deep = options.deep ?? false;
|
|
37
|
+
const includeFilesystem = options.includeFilesystem ?? deep;
|
|
38
|
+
const includeChannelSecurity = options.includeChannelSecurity ?? true;
|
|
39
|
+
const stateDir = options.stateDir ?? resolveStateDir();
|
|
40
|
+
const configPath = options.configPath ?? resolveConfigPath();
|
|
41
|
+
const deepTimeoutMs = options.deepTimeoutMs ?? 10000;
|
|
42
|
+
const collectedFindings = await collectSecurityAuditFindings({
|
|
43
|
+
config: cfg,
|
|
44
|
+
sourceConfig,
|
|
45
|
+
env,
|
|
46
|
+
platform,
|
|
47
|
+
stateDir,
|
|
48
|
+
configPath,
|
|
49
|
+
deep,
|
|
50
|
+
includeFilesystem,
|
|
51
|
+
includeChannelSecurity,
|
|
52
|
+
deepTimeoutMs,
|
|
127
53
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
if (configPerms.worldWritable || configPerms.groupWritable) {
|
|
139
|
-
findings.push({
|
|
140
|
-
checkId: "fs.config.perms_writable",
|
|
141
|
-
severity: "critical",
|
|
142
|
-
title: "Config file is writable by others",
|
|
143
|
-
detail: `${formatPermissionDetail(params.configPath, configPerms)}; another user could change gateway/auth/tool policies.`,
|
|
144
|
-
remediation: formatPermissionRemediation({
|
|
145
|
-
targetPath: params.configPath,
|
|
146
|
-
perms: configPerms,
|
|
147
|
-
isDir: false,
|
|
148
|
-
posixMode: 0o600,
|
|
149
|
-
env: params.env,
|
|
150
|
-
}),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
else if (!skipReadablePermWarnings && configPerms.worldReadable) {
|
|
154
|
-
findings.push({
|
|
155
|
-
checkId: "fs.config.perms_world_readable",
|
|
156
|
-
severity: "critical",
|
|
157
|
-
title: "Config file is world-readable",
|
|
158
|
-
detail: `${formatPermissionDetail(params.configPath, configPerms)}; config can contain tokens and private settings.`,
|
|
159
|
-
remediation: formatPermissionRemediation({
|
|
160
|
-
targetPath: params.configPath,
|
|
161
|
-
perms: configPerms,
|
|
162
|
-
isDir: false,
|
|
163
|
-
posixMode: 0o600,
|
|
164
|
-
env: params.env,
|
|
165
|
-
}),
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
else if (!skipReadablePermWarnings && configPerms.groupReadable) {
|
|
169
|
-
findings.push({
|
|
170
|
-
checkId: "fs.config.perms_group_readable",
|
|
171
|
-
severity: "warn",
|
|
172
|
-
title: "Config file is group-readable",
|
|
173
|
-
detail: `${formatPermissionDetail(params.configPath, configPerms)}; config can contain tokens and private settings.`,
|
|
174
|
-
remediation: formatPermissionRemediation({
|
|
175
|
-
targetPath: params.configPath,
|
|
176
|
-
perms: configPerms,
|
|
177
|
-
isDir: false,
|
|
178
|
-
posixMode: 0o600,
|
|
179
|
-
env: params.env,
|
|
180
|
-
}),
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return findings;
|
|
185
|
-
}
|
|
186
|
-
function collectGatewayConfigFindings(cfg, env) {
|
|
187
|
-
const findings = [];
|
|
188
|
-
const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
|
|
189
|
-
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
|
190
|
-
const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode, env });
|
|
191
|
-
const controlUiEnabled = cfg.gateway?.controlUi?.enabled !== false;
|
|
192
|
-
const trustedProxies = Array.isArray(cfg.gateway?.trustedProxies)
|
|
193
|
-
? cfg.gateway.trustedProxies
|
|
194
|
-
: [];
|
|
195
|
-
const hasToken = typeof auth.token === "string" && auth.token.trim().length > 0;
|
|
196
|
-
const hasPassword = typeof auth.password === "string" && auth.password.trim().length > 0;
|
|
197
|
-
const hasSharedSecret = (auth.mode === "token" && hasToken) || (auth.mode === "password" && hasPassword);
|
|
198
|
-
const hasTailscaleAuth = auth.allowTailscale && tailscaleMode === "serve";
|
|
199
|
-
const hasGatewayAuth = hasSharedSecret || hasTailscaleAuth;
|
|
200
|
-
// HTTP /tools/invoke is intended for narrow automation, not session orchestration/admin operations.
|
|
201
|
-
// If operators opt-in to re-enabling these tools over HTTP, warn loudly so the choice is explicit.
|
|
202
|
-
const gatewayToolsAllowRaw = Array.isArray(cfg.gateway?.tools?.allow)
|
|
203
|
-
? cfg.gateway?.tools?.allow
|
|
204
|
-
: [];
|
|
205
|
-
const gatewayToolsAllow = new Set(gatewayToolsAllowRaw
|
|
206
|
-
.map((v) => (typeof v === "string" ? v.trim().toLowerCase() : ""))
|
|
207
|
-
.filter(Boolean));
|
|
208
|
-
const reenabledOverHttp = DEFAULT_GATEWAY_HTTP_TOOL_DENY.filter((name) => gatewayToolsAllow.has(name));
|
|
209
|
-
if (reenabledOverHttp.length > 0) {
|
|
210
|
-
const extraRisk = bind !== "loopback" || tailscaleMode === "funnel";
|
|
211
|
-
findings.push({
|
|
212
|
-
checkId: "gateway.tools_invoke_http.dangerous_allow",
|
|
213
|
-
severity: extraRisk ? "critical" : "warn",
|
|
214
|
-
title: "Gateway HTTP /tools/invoke re-enables dangerous tools",
|
|
215
|
-
detail: `gateway.tools.allow includes ${reenabledOverHttp.join(", ")} which removes them from the default HTTP deny list. ` +
|
|
216
|
-
"This can allow remote session spawning / control-plane actions via HTTP and increases RCE blast radius if the gateway is reachable.",
|
|
217
|
-
remediation: "Remove these entries from gateway.tools.allow (recommended). " +
|
|
218
|
-
"If you keep them enabled, keep gateway.bind loopback-only (or tailnet-only), restrict network exposure, and treat the gateway token/password as full-admin.",
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
if (bind !== "loopback" && !hasSharedSecret && auth.mode !== "trusted-proxy") {
|
|
222
|
-
findings.push({
|
|
223
|
-
checkId: "gateway.bind_no_auth",
|
|
224
|
-
severity: "critical",
|
|
225
|
-
title: "Gateway binds beyond loopback without auth",
|
|
226
|
-
detail: `gateway.bind="${bind}" but no gateway.auth token/password is configured.`,
|
|
227
|
-
remediation: `Set gateway.auth (token recommended) or bind to loopback.`,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
if (bind === "loopback" && controlUiEnabled && trustedProxies.length === 0) {
|
|
231
|
-
findings.push({
|
|
232
|
-
checkId: "gateway.trusted_proxies_missing",
|
|
233
|
-
severity: "warn",
|
|
234
|
-
title: "Reverse proxy headers are not trusted",
|
|
235
|
-
detail: "gateway.bind is loopback and gateway.trustedProxies is empty. " +
|
|
236
|
-
"If you expose the Control UI through a reverse proxy, configure trusted proxies " +
|
|
237
|
-
"so local-client checks cannot be spoofed.",
|
|
238
|
-
remediation: "Set gateway.trustedProxies to your proxy IPs or keep the Control UI local-only.",
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
if (bind === "loopback" && controlUiEnabled && !hasGatewayAuth) {
|
|
242
|
-
findings.push({
|
|
243
|
-
checkId: "gateway.loopback_no_auth",
|
|
244
|
-
severity: "critical",
|
|
245
|
-
title: "Gateway auth missing on loopback",
|
|
246
|
-
detail: "gateway.bind is loopback but no gateway auth secret is configured. " +
|
|
247
|
-
"If the Control UI is exposed through a reverse proxy, unauthenticated access is possible.",
|
|
248
|
-
remediation: "Set gateway.auth (token recommended) or keep the Control UI local-only.",
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
if (tailscaleMode === "funnel") {
|
|
252
|
-
findings.push({
|
|
253
|
-
checkId: "gateway.tailscale_funnel",
|
|
254
|
-
severity: "critical",
|
|
255
|
-
title: "Tailscale Funnel exposure enabled",
|
|
256
|
-
detail: `gateway.tailscale.mode="funnel" exposes the Gateway publicly; keep auth strict and treat it as internet-facing.`,
|
|
257
|
-
remediation: `Prefer tailscale.mode="serve" (tailnet-only) or set tailscale.mode="off".`,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
else if (tailscaleMode === "serve") {
|
|
261
|
-
findings.push({
|
|
262
|
-
checkId: "gateway.tailscale_serve",
|
|
263
|
-
severity: "info",
|
|
264
|
-
title: "Tailscale Serve exposure enabled",
|
|
265
|
-
detail: `gateway.tailscale.mode="serve" exposes the Gateway to your tailnet (loopback behind Tailscale).`,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
if (cfg.gateway?.controlUi?.allowInsecureAuth === true) {
|
|
269
|
-
findings.push({
|
|
270
|
-
checkId: "gateway.control_ui.insecure_auth",
|
|
271
|
-
severity: "warn",
|
|
272
|
-
title: "Control UI insecure auth toggle enabled",
|
|
273
|
-
detail: "gateway.controlUi.allowInsecureAuth=true does not bypass secure context or device identity checks; only dangerouslyDisableDeviceAuth disables Control UI device identity checks.",
|
|
274
|
-
remediation: "Disable it or switch to HTTPS (Tailscale Serve) or localhost.",
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
if (cfg.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true) {
|
|
278
|
-
findings.push({
|
|
279
|
-
checkId: "gateway.control_ui.device_auth_disabled",
|
|
280
|
-
severity: "critical",
|
|
281
|
-
title: "DANGEROUS: Control UI device auth disabled",
|
|
282
|
-
detail: "gateway.controlUi.dangerouslyDisableDeviceAuth=true disables device identity checks for the Control UI.",
|
|
283
|
-
remediation: "Disable it unless you are in a short-lived break-glass scenario.",
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
const enabledDangerousFlags = collectEnabledInsecureOrDangerousFlags(cfg);
|
|
287
|
-
if (enabledDangerousFlags.length > 0) {
|
|
288
|
-
findings.push({
|
|
289
|
-
checkId: "config.insecure_or_dangerous_flags",
|
|
290
|
-
severity: "warn",
|
|
291
|
-
title: "Insecure or dangerous config flags enabled",
|
|
292
|
-
detail: `Detected ${enabledDangerousFlags.length} enabled flag(s): ${enabledDangerousFlags.join(", ")}.`,
|
|
293
|
-
remediation: "Disable these flags when not actively debugging, or keep deployment scoped to trusted/local-only networks.",
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
const token = typeof auth.token === "string" && auth.token.trim().length > 0 ? auth.token.trim() : null;
|
|
297
|
-
if (auth.mode === "token" && token && token.length < 24) {
|
|
298
|
-
findings.push({
|
|
299
|
-
checkId: "gateway.token_too_short",
|
|
300
|
-
severity: "warn",
|
|
301
|
-
title: "Gateway token looks short",
|
|
302
|
-
detail: `gateway auth token is ${token.length} chars; prefer a long random token.`,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
if (auth.mode === "trusted-proxy") {
|
|
306
|
-
const trustedProxies = cfg.gateway?.trustedProxies ?? [];
|
|
307
|
-
const trustedProxyConfig = cfg.gateway?.auth?.trustedProxy;
|
|
308
|
-
findings.push({
|
|
309
|
-
checkId: "gateway.trusted_proxy_auth",
|
|
310
|
-
severity: "critical",
|
|
311
|
-
title: "Trusted-proxy auth mode enabled",
|
|
312
|
-
detail: 'gateway.auth.mode="trusted-proxy" delegates authentication to a reverse proxy. ' +
|
|
313
|
-
"Ensure your proxy (Pomerium, Caddy, nginx) handles auth correctly and that gateway.trustedProxies " +
|
|
314
|
-
"only contains IPs of your actual proxy servers.",
|
|
315
|
-
remediation: "Verify: (1) Your proxy terminates TLS and authenticates users. " +
|
|
316
|
-
"(2) gateway.trustedProxies is restricted to proxy IPs only. " +
|
|
317
|
-
"(3) Direct access to the Gateway port is blocked by firewall. " +
|
|
318
|
-
"See /gateway/trusted-proxy-auth for setup guidance.",
|
|
319
|
-
});
|
|
320
|
-
if (trustedProxies.length === 0) {
|
|
321
|
-
findings.push({
|
|
322
|
-
checkId: "gateway.trusted_proxy_no_proxies",
|
|
323
|
-
severity: "critical",
|
|
324
|
-
title: "Trusted-proxy auth enabled but no trusted proxies configured",
|
|
325
|
-
detail: 'gateway.auth.mode="trusted-proxy" but gateway.trustedProxies is empty. ' +
|
|
326
|
-
"All requests will be rejected.",
|
|
327
|
-
remediation: "Set gateway.trustedProxies to the IP(s) of your reverse proxy.",
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
if (!trustedProxyConfig?.userHeader) {
|
|
331
|
-
findings.push({
|
|
332
|
-
checkId: "gateway.trusted_proxy_no_user_header",
|
|
333
|
-
severity: "critical",
|
|
334
|
-
title: "Trusted-proxy auth missing userHeader config",
|
|
335
|
-
detail: 'gateway.auth.mode="trusted-proxy" but gateway.auth.trustedProxy.userHeader is not configured.',
|
|
336
|
-
remediation: "Set gateway.auth.trustedProxy.userHeader to the header name your proxy uses " +
|
|
337
|
-
'(e.g., "x-forwarded-user", "x-pomerium-claim-email").',
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
const allowUsers = trustedProxyConfig?.allowUsers ?? [];
|
|
341
|
-
if (allowUsers.length === 0) {
|
|
342
|
-
findings.push({
|
|
343
|
-
checkId: "gateway.trusted_proxy_no_allowlist",
|
|
344
|
-
severity: "warn",
|
|
345
|
-
title: "Trusted-proxy auth allows all authenticated users",
|
|
346
|
-
detail: "gateway.auth.trustedProxy.allowUsers is empty, so any user authenticated by your proxy can access the Gateway.",
|
|
347
|
-
remediation: "Consider setting gateway.auth.trustedProxy.allowUsers to restrict access to specific users " +
|
|
348
|
-
'(e.g., ["nick@example.com"]).',
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (bind !== "loopback" && auth.mode !== "trusted-proxy" && !cfg.gateway?.auth?.rateLimit) {
|
|
353
|
-
findings.push({
|
|
354
|
-
checkId: "gateway.auth_no_rate_limit",
|
|
355
|
-
severity: "warn",
|
|
356
|
-
title: "No auth rate limiting configured",
|
|
357
|
-
detail: "gateway.bind is not loopback but no gateway.auth.rateLimit is configured. " +
|
|
358
|
-
"Without rate limiting, brute-force auth attacks are not mitigated.",
|
|
359
|
-
remediation: "Set gateway.auth.rateLimit (e.g. { maxAttempts: 10, windowMs: 60000, lockoutMs: 300000 }).",
|
|
360
|
-
});
|
|
54
|
+
findings.push(...collectedFindings);
|
|
55
|
+
const summary = countBySeverity(findings);
|
|
56
|
+
const report = {
|
|
57
|
+
ts,
|
|
58
|
+
summary,
|
|
59
|
+
findings,
|
|
60
|
+
};
|
|
61
|
+
if (deep) {
|
|
62
|
+
report.deep = await performDeepGatewayAudit(cfg, deepTimeoutMs);
|
|
361
63
|
}
|
|
362
|
-
return
|
|
64
|
+
return report;
|
|
363
65
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Perform deep gateway audit (optional, time-consuming)
|
|
68
|
+
*/
|
|
69
|
+
async function performDeepGatewayAudit(config, timeoutMs) {
|
|
70
|
+
const result = {};
|
|
367
71
|
try {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const browserAuth = resolveBrowserControlAuth(cfg, env);
|
|
384
|
-
if (!browserAuth.token && !browserAuth.password) {
|
|
385
|
-
findings.push({
|
|
386
|
-
checkId: "browser.control_no_auth",
|
|
387
|
-
severity: "critical",
|
|
388
|
-
title: "Browser control has no auth",
|
|
389
|
-
detail: "Browser control HTTP routes are enabled but no gateway.auth token/password is configured. " +
|
|
390
|
-
"Any local process (or SSRF to loopback) can call browser control endpoints.",
|
|
391
|
-
remediation: "Set gateway.auth.token (recommended) or gateway.auth.password so browser control HTTP routes require authentication. Restarting the gateway will auto-generate gateway.auth.token when browser control is enabled.",
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
for (const name of Object.keys(resolved.profiles)) {
|
|
395
|
-
const profile = resolveProfile(resolved, name);
|
|
396
|
-
if (!profile || profile.cdpIsLoopback) {
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
let url;
|
|
72
|
+
const gateway = config.gateway;
|
|
73
|
+
if (!gateway) {
|
|
74
|
+
result.gateway = {
|
|
75
|
+
attempted: false,
|
|
76
|
+
url: null,
|
|
77
|
+
ok: false,
|
|
78
|
+
error: "Gateway not configured",
|
|
79
|
+
};
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
const port = gateway.port ?? 18789;
|
|
83
|
+
const host = gateway.host ?? "127.0.0.1";
|
|
84
|
+
const url = `http://${host}:${port}`;
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
400
87
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
if (url.protocol === "http:") {
|
|
407
|
-
findings.push({
|
|
408
|
-
checkId: "browser.remote_cdp_http",
|
|
409
|
-
severity: "warn",
|
|
410
|
-
title: "Remote CDP uses HTTP",
|
|
411
|
-
detail: `browser profile "${name}" uses http CDP (${profile.cdpUrl}); this is OK only if it's tailnet-only or behind an encrypted tunnel.`,
|
|
412
|
-
remediation: `Prefer HTTPS/TLS or a tailnet-only endpoint for remote CDP.`,
|
|
88
|
+
const response = await fetch(`${url}/probe`, {
|
|
89
|
+
method: "GET",
|
|
90
|
+
signal: controller.signal,
|
|
413
91
|
});
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
92
|
+
clearTimeout(timeoutId);
|
|
93
|
+
result.gateway = {
|
|
94
|
+
attempted: true,
|
|
95
|
+
url,
|
|
96
|
+
ok: response.ok,
|
|
97
|
+
error: response.ok ? null : `HTTP ${response.status}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
clearTimeout(timeoutId);
|
|
102
|
+
result.gateway = {
|
|
103
|
+
attempted: true,
|
|
104
|
+
url,
|
|
105
|
+
ok: false,
|
|
106
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
result.gateway = {
|
|
112
|
+
attempted: false,
|
|
113
|
+
url: null,
|
|
114
|
+
ok: false,
|
|
115
|
+
error: "Gateway audit failed",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
417
119
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
remediation: `Set logging.redactSensitive="tools".`,
|
|
430
|
-
},
|
|
431
|
-
];
|
|
432
|
-
}
|
|
433
|
-
function collectElevatedFindings(cfg) {
|
|
434
|
-
const findings = [];
|
|
435
|
-
const enabled = cfg.tools?.elevated?.enabled;
|
|
436
|
-
const allowFrom = cfg.tools?.elevated?.allowFrom ?? {};
|
|
437
|
-
const anyAllowFromKeys = Object.keys(allowFrom).length > 0;
|
|
438
|
-
if (enabled === false) {
|
|
439
|
-
return findings;
|
|
440
|
-
}
|
|
441
|
-
if (!anyAllowFromKeys) {
|
|
442
|
-
return findings;
|
|
443
|
-
}
|
|
444
|
-
for (const [provider, list] of Object.entries(allowFrom)) {
|
|
445
|
-
const normalized = normalizeAllowFromList(list);
|
|
446
|
-
if (normalized.includes("*")) {
|
|
447
|
-
findings.push({
|
|
448
|
-
checkId: `tools.elevated.allowFrom.${provider}.wildcard`,
|
|
449
|
-
severity: "critical",
|
|
450
|
-
title: "Elevated exec allowlist contains wildcard",
|
|
451
|
-
detail: `tools.elevated.allowFrom.${provider} includes "*" which effectively approves everyone on that channel for elevated mode.`,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
else if (normalized.length > 25) {
|
|
455
|
-
findings.push({
|
|
456
|
-
checkId: `tools.elevated.allowFrom.${provider}.large`,
|
|
457
|
-
severity: "warn",
|
|
458
|
-
title: "Elevated exec allowlist is large",
|
|
459
|
-
detail: `tools.elevated.allowFrom.${provider} has ${normalized.length} entries; consider tightening elevated access.`,
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
return findings;
|
|
464
|
-
}
|
|
465
|
-
function collectExecRuntimeFindings(cfg) {
|
|
466
|
-
const findings = [];
|
|
467
|
-
const globalExecHost = cfg.tools?.exec?.host;
|
|
468
|
-
const defaultSandboxMode = resolveSandboxConfigForAgent(cfg).mode;
|
|
469
|
-
const defaultHostIsExplicitSandbox = globalExecHost === "sandbox";
|
|
470
|
-
if (defaultHostIsExplicitSandbox && defaultSandboxMode === "off") {
|
|
471
|
-
findings.push({
|
|
472
|
-
checkId: "tools.exec.host_sandbox_no_sandbox_defaults",
|
|
473
|
-
severity: "warn",
|
|
474
|
-
title: "Exec host is sandbox but sandbox mode is off",
|
|
475
|
-
detail: "tools.exec.host is explicitly set to sandbox while agents.defaults.sandbox.mode=off. " +
|
|
476
|
-
"In this mode, exec runs directly on the gateway host.",
|
|
477
|
-
remediation: 'Enable sandbox mode (`agents.defaults.sandbox.mode="non-main"` or `"all"`) or set tools.exec.host to "gateway" with approvals.',
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
481
|
-
const riskyAgents = agents
|
|
482
|
-
.filter((entry) => entry &&
|
|
483
|
-
typeof entry === "object" &&
|
|
484
|
-
typeof entry.id === "string" &&
|
|
485
|
-
entry.tools?.exec?.host === "sandbox" &&
|
|
486
|
-
resolveSandboxConfigForAgent(cfg, entry.id).mode === "off")
|
|
487
|
-
.map((entry) => entry.id)
|
|
488
|
-
.slice(0, 5);
|
|
489
|
-
if (riskyAgents.length > 0) {
|
|
490
|
-
findings.push({
|
|
491
|
-
checkId: "tools.exec.host_sandbox_no_sandbox_agents",
|
|
492
|
-
severity: "warn",
|
|
493
|
-
title: "Agent exec host uses sandbox while sandbox mode is off",
|
|
494
|
-
detail: `agents.list.*.tools.exec.host is set to sandbox for: ${riskyAgents.join(", ")}. ` +
|
|
495
|
-
"With sandbox mode off, exec runs directly on the gateway host.",
|
|
496
|
-
remediation: 'Enable sandbox mode for these agents (`agents.list[].sandbox.mode`) or set their tools.exec.host to "gateway".',
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
return findings;
|
|
120
|
+
/**
|
|
121
|
+
* Quick security check (non-deep audit)
|
|
122
|
+
*/
|
|
123
|
+
export async function quickSecurityCheck(config) {
|
|
124
|
+
const report = await performSecurityAudit({
|
|
125
|
+
config,
|
|
126
|
+
deep: false,
|
|
127
|
+
includeFilesystem: false,
|
|
128
|
+
});
|
|
129
|
+
const ok = report.summary.critical === 0;
|
|
130
|
+
return { ok, findings: report.findings };
|
|
500
131
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
presence: null,
|
|
519
|
-
configSnapshot: null,
|
|
520
|
-
}));
|
|
521
|
-
return {
|
|
522
|
-
gateway: {
|
|
523
|
-
attempted: true,
|
|
524
|
-
url,
|
|
525
|
-
ok: res.ok,
|
|
526
|
-
error: res.ok ? null : res.error,
|
|
527
|
-
close: res.close ? { code: res.close.code, reason: res.close.reason } : null,
|
|
528
|
-
},
|
|
132
|
+
/**
|
|
133
|
+
* Format audit report for CLI output
|
|
134
|
+
*/
|
|
135
|
+
export function formatAuditReport(report) {
|
|
136
|
+
const lines = [];
|
|
137
|
+
lines.push(`Security Audit Report (${new Date(report.ts).toISOString()})`);
|
|
138
|
+
lines.push("");
|
|
139
|
+
lines.push(`Summary: ${report.summary.critical} critical, ${report.summary.warn} warnings, ${report.summary.info} info`);
|
|
140
|
+
lines.push("");
|
|
141
|
+
if (report.findings.length === 0) {
|
|
142
|
+
lines.push("✅ No security issues found!");
|
|
143
|
+
return lines.join("\n");
|
|
144
|
+
}
|
|
145
|
+
const bySeverity = {
|
|
146
|
+
critical: report.findings.filter((f) => f.severity === "critical"),
|
|
147
|
+
warn: report.findings.filter((f) => f.severity === "warn"),
|
|
148
|
+
info: report.findings.filter((f) => f.severity === "info"),
|
|
529
149
|
};
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
findings.push(...collectLoggingFindings(cfg));
|
|
544
|
-
findings.push(...collectElevatedFindings(cfg));
|
|
545
|
-
findings.push(...collectExecRuntimeFindings(cfg));
|
|
546
|
-
findings.push(...collectHooksHardeningFindings(cfg, env));
|
|
547
|
-
findings.push(...collectGatewayHttpNoAuthFindings(cfg, env));
|
|
548
|
-
findings.push(...collectGatewayHttpSessionKeyOverrideFindings(cfg));
|
|
549
|
-
findings.push(...collectSandboxDockerNoopFindings(cfg));
|
|
550
|
-
findings.push(...collectSandboxDangerousConfigFindings(cfg));
|
|
551
|
-
findings.push(...collectNodeDenyCommandPatternFindings(cfg));
|
|
552
|
-
findings.push(...collectMinimalProfileOverrideFindings(cfg));
|
|
553
|
-
findings.push(...collectSecretsInConfigFindings(cfg));
|
|
554
|
-
findings.push(...collectModelHygieneFindings(cfg));
|
|
555
|
-
findings.push(...collectSmallModelRiskFindings({ cfg, env }));
|
|
556
|
-
findings.push(...collectExposureMatrixFindings(cfg));
|
|
557
|
-
const configSnapshot = opts.includeFilesystem !== false
|
|
558
|
-
? await readConfigSnapshotForAudit({ env, configPath }).catch(() => null)
|
|
559
|
-
: null;
|
|
560
|
-
if (opts.includeFilesystem !== false) {
|
|
561
|
-
findings.push(...(await collectFilesystemFindings({
|
|
562
|
-
stateDir,
|
|
563
|
-
configPath,
|
|
564
|
-
env,
|
|
565
|
-
platform,
|
|
566
|
-
execIcacls,
|
|
567
|
-
})));
|
|
568
|
-
if (configSnapshot) {
|
|
569
|
-
findings.push(...(await collectIncludeFilePermFindings({ configSnapshot, env, platform, execIcacls })));
|
|
570
|
-
}
|
|
571
|
-
findings.push(...(await collectStateDeepFilesystemFindings({ cfg, env, stateDir, platform, execIcacls })));
|
|
572
|
-
findings.push(...(await collectSandboxBrowserHashLabelFindings({
|
|
573
|
-
execDockerRawFn: opts.execDockerRawFn,
|
|
574
|
-
})));
|
|
575
|
-
findings.push(...(await collectPluginsTrustFindings({ cfg, stateDir })));
|
|
576
|
-
if (opts.deep === true) {
|
|
577
|
-
findings.push(...(await collectPluginsCodeSafetyFindings({ stateDir })));
|
|
578
|
-
findings.push(...(await collectInstalledSkillsCodeSafetyFindings({ cfg, stateDir })));
|
|
150
|
+
for (const [severity, findings] of Object.entries(bySeverity)) {
|
|
151
|
+
if (findings.length === 0)
|
|
152
|
+
continue;
|
|
153
|
+
const icon = severity === "critical" ? "🔴" : severity === "warn" ? "🟡" : "🔵";
|
|
154
|
+
lines.push(`${icon} ${severity.toUpperCase()} (${findings.length})`);
|
|
155
|
+
lines.push("");
|
|
156
|
+
for (const finding of findings) {
|
|
157
|
+
lines.push(` [${finding.checkId}] ${finding.title}`);
|
|
158
|
+
lines.push(` ${finding.detail}`);
|
|
159
|
+
if (finding.remediation) {
|
|
160
|
+
lines.push(` 💡 Fix: ${finding.remediation}`);
|
|
161
|
+
}
|
|
162
|
+
lines.push("");
|
|
579
163
|
}
|
|
580
164
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
timeoutMs: Math.max(250, opts.deepTimeoutMs ?? 5000),
|
|
589
|
-
probe: opts.probeGatewayFn ?? probeGateway,
|
|
590
|
-
})
|
|
591
|
-
: undefined;
|
|
592
|
-
if (deep?.gateway?.attempted && !deep.gateway.ok) {
|
|
593
|
-
findings.push({
|
|
594
|
-
checkId: "gateway.probe_failed",
|
|
595
|
-
severity: "warn",
|
|
596
|
-
title: "Gateway probe failed (deep)",
|
|
597
|
-
detail: deep.gateway.error ?? "gateway unreachable",
|
|
598
|
-
remediation: `Run "${formatCliCommand("poolbot status --all")}" to debug connectivity/auth, then re-run "${formatCliCommand("poolbot security audit --deep")}".`,
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
const summary = countBySeverity(findings);
|
|
602
|
-
return { ts: Date.now(), summary, findings, deep };
|
|
165
|
+
return lines.join("\n");
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* CLI-compatible audit function (alias for performSecurityAudit)
|
|
169
|
+
*/
|
|
170
|
+
export async function runSecurityAudit(options) {
|
|
171
|
+
return performSecurityAudit(options);
|
|
603
172
|
}
|