@poolzin/pool-bot 2026.3.16 → 2026.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/agents/tools/web-fetch.js +1 -1
  3. package/dist/build-info.json +3 -3
  4. package/dist/commands/skills-openclaw.command.js +123 -0
  5. package/dist/config/paths.js +7 -0
  6. package/dist/infra/net/fetch-guard.js +191 -146
  7. package/dist/media/fetch.js +83 -112
  8. package/dist/media/inbound-path-policy.js +90 -97
  9. package/dist/media/read-response-with-limit.js +49 -26
  10. package/dist/media-understanding/attachments.js +1 -1
  11. package/dist/plugin-sdk/audio.js +7 -0
  12. package/dist/plugin-sdk/bluebubbles.js +7 -0
  13. package/dist/plugin-sdk/browser.js +7 -0
  14. package/dist/plugin-sdk/canvas.js +7 -0
  15. package/dist/plugin-sdk/cron.js +7 -0
  16. package/dist/plugin-sdk/discord-actions.js +6 -0
  17. package/dist/plugin-sdk/discord.js +7 -0
  18. package/dist/plugin-sdk/image.js +7 -0
  19. package/dist/plugin-sdk/imessage.js +6 -0
  20. package/dist/plugin-sdk/keyed-async-queue.js +35 -0
  21. package/dist/plugin-sdk/media.js +8 -0
  22. package/dist/plugin-sdk/memory.js +7 -0
  23. package/dist/plugin-sdk/pdf.js +7 -0
  24. package/dist/plugin-sdk/sessions.js +7 -0
  25. package/dist/plugin-sdk/signal.js +6 -0
  26. package/dist/plugin-sdk/slack-actions.js +7 -0
  27. package/dist/plugin-sdk/slack.js +7 -0
  28. package/dist/plugin-sdk/telegram-actions.js +6 -0
  29. package/dist/plugin-sdk/telegram.js +6 -0
  30. package/dist/plugin-sdk/test-utils.js +110 -0
  31. package/dist/plugin-sdk/tts.js +7 -0
  32. package/dist/plugin-sdk/whatsapp.js +6 -0
  33. package/dist/providers/github-copilot-auth.js +53 -76
  34. package/dist/providers/github-copilot-models.js +63 -35
  35. package/dist/providers/github-copilot-token.js +46 -89
  36. package/dist/security/audit-findings.js +165 -0
  37. package/dist/security/audit.js +141 -572
  38. package/dist/skills/openclaw-skill-loader.js +191 -0
  39. package/dist/slack/monitor/media.js +2 -1
  40. package/docs/branding-evaluation-2026-03-12.md +285 -0
  41. package/docs/improvements/OPENCLAW-IMPLEMENTATION.md +45 -0
  42. package/docs/skills/openclaw-integration.md +295 -0
  43. package/docs/testing/TEST-PLAN-2026-03-13.md +338 -0
  44. package/docs/version-2026.3.16-evaluation.md +190 -0
  45. package/extensions/acpx/package.json +19 -0
  46. package/extensions/acpx/poolbot.plugin.json +9 -0
  47. package/extensions/acpx/src/index.ts +34 -0
  48. package/extensions/bluebubbles/src/runtime.ts +1 -0
  49. package/extensions/diffs/package.json +15 -0
  50. package/extensions/diffs/poolbot.plugin.json +10 -0
  51. package/extensions/diffs/src/index.ts +106 -0
  52. package/extensions/discord/src/runtime.ts +1 -0
  53. package/extensions/feishu/src/runtime.ts +1 -0
  54. package/extensions/github-copilot/package.json +28 -0
  55. package/extensions/github-copilot/poolbot.plugin.json +29 -0
  56. package/extensions/github-copilot/src/index.ts +126 -0
  57. package/extensions/github-copilot/tsconfig.json +10 -0
  58. package/extensions/googlechat/src/runtime.ts +1 -0
  59. package/extensions/imessage/src/runtime.ts +1 -0
  60. package/extensions/irc/src/runtime.ts +1 -0
  61. package/extensions/line/src/runtime.ts +1 -0
  62. package/extensions/matrix/src/runtime.ts +1 -0
  63. package/extensions/mattermost/src/mattermost/monitor-helpers.ts +10 -1
  64. package/extensions/mattermost/src/runtime.ts +6 -3
  65. package/extensions/msteams/src/runtime.ts +1 -0
  66. package/extensions/nextcloud-talk/src/runtime.ts +1 -0
  67. package/extensions/nostr/src/runtime.ts +5 -2
  68. package/extensions/ollama/package.json +20 -0
  69. package/extensions/ollama/poolbot.plugin.json +14 -0
  70. package/extensions/ollama/src/index.ts +95 -0
  71. package/extensions/sglang/package.json +18 -0
  72. package/extensions/sglang/poolbot.plugin.json +13 -0
  73. package/extensions/sglang/src/index.ts +62 -0
  74. package/extensions/signal/src/runtime.ts +1 -0
  75. package/extensions/slack/src/runtime.ts +1 -0
  76. package/extensions/telegram/src/runtime.ts +1 -0
  77. package/extensions/test-utils/package.json +17 -0
  78. package/extensions/test-utils/poolbot.plugin.json +16 -0
  79. package/extensions/test-utils/src/index.ts +220 -0
  80. package/extensions/tlon/src/runtime.ts +1 -0
  81. package/extensions/twitch/src/runtime.ts +1 -0
  82. package/extensions/vllm/package.json +19 -0
  83. package/extensions/vllm/poolbot.plugin.json +13 -0
  84. package/extensions/vllm/src/index.ts +90 -0
  85. package/extensions/whatsapp/src/runtime.ts +1 -0
  86. package/extensions/zalo/src/runtime.ts +1 -0
  87. package/extensions/zalouser/src/runtime.ts +1 -0
  88. package/package.json +77 -3
@@ -1,17 +1,11 @@
1
- import { resolveSandboxConfigForAgent } from "../agents/sandbox.js";
2
- import { resolveBrowserConfig, resolveProfile } from "../browser/config.js";
3
- import { resolveBrowserControlAuth } from "../browser/control-auth.js";
4
- import { listChannelPlugins } from "../channels/plugins/index.js";
5
- import { formatCliCommand } from "../cli/command-format.js";
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 { resolveGatewayAuth } from "../gateway/auth.js";
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
- function normalizeAllowFromList(list) {
33
- if (!Array.isArray(list)) {
34
- return [];
35
- }
36
- return list.map((v) => String(v).trim()).filter(Boolean);
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 stateDirPerms = await inspectPathPermissions(params.stateDir, {
64
- env: params.env,
65
- platform: params.platform,
66
- exec: params.execIcacls,
67
- });
68
- if (stateDirPerms.ok) {
69
- if (stateDirPerms.isSymlink) {
70
- findings.push({
71
- checkId: "fs.state_dir.symlink",
72
- severity: "warn",
73
- title: "State dir is a symlink",
74
- detail: `${params.stateDir} is a symlink; treat this as an extra trust boundary.`,
75
- });
76
- }
77
- if (stateDirPerms.worldWritable) {
78
- findings.push({
79
- checkId: "fs.state_dir.perms_world_writable",
80
- severity: "critical",
81
- title: "State dir is world-writable",
82
- detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; other users can write into your Pool Bot state.`,
83
- remediation: formatPermissionRemediation({
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
- if (configPerms.ok) {
129
- const skipReadablePermWarnings = configPerms.isSymlink;
130
- if (configPerms.isSymlink) {
131
- findings.push({
132
- checkId: "fs.config.symlink",
133
- severity: "warn",
134
- title: "Config file is a symlink",
135
- detail: `${params.configPath} is a symlink; make sure you trust its target.`,
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 findings;
64
+ return report;
363
65
  }
364
- function collectBrowserControlFindings(cfg, env) {
365
- const findings = [];
366
- let resolved;
66
+ /**
67
+ * Perform deep gateway audit (optional, time-consuming)
68
+ */
69
+ async function performDeepGatewayAudit(config, timeoutMs) {
70
+ const result = {};
367
71
  try {
368
- resolved = resolveBrowserConfig(cfg.browser, cfg);
369
- }
370
- catch (err) {
371
- findings.push({
372
- checkId: "browser.control_invalid_config",
373
- severity: "warn",
374
- title: "Browser control config looks invalid",
375
- detail: String(err),
376
- remediation: `Fix browser.cdpUrl in ${resolveConfigPath()} and re-run "${formatCliCommand("poolbot security audit --deep")}".`,
377
- });
378
- return findings;
379
- }
380
- if (!resolved.enabled) {
381
- return findings;
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
- url = new URL(profile.cdpUrl);
402
- }
403
- catch {
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
- return findings;
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
- function collectLoggingFindings(cfg) {
419
- const redact = cfg.logging?.redactSensitive;
420
- if (redact !== "off") {
421
- return [];
422
- }
423
- return [
424
- {
425
- checkId: "logging.redact_off",
426
- severity: "warn",
427
- title: "Tool summary redaction is disabled",
428
- detail: `logging.redactSensitive="off" can leak secrets into logs and status output.`,
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
- async function maybeProbeGateway(params) {
502
- const connection = buildGatewayConnectionDetails({ config: params.cfg });
503
- const url = connection.url;
504
- const isRemoteMode = params.cfg.gateway?.mode === "remote";
505
- const remoteUrlRaw = typeof params.cfg.gateway?.remote?.url === "string" ? params.cfg.gateway.remote.url.trim() : "";
506
- const remoteUrlMissing = isRemoteMode && !remoteUrlRaw;
507
- const auth = !isRemoteMode || remoteUrlMissing
508
- ? resolveGatewayProbeAuth({ cfg: params.cfg, mode: "local" })
509
- : resolveGatewayProbeAuth({ cfg: params.cfg, mode: "remote" });
510
- const res = await params.probe({ url, auth, timeoutMs: params.timeoutMs }).catch((err) => ({
511
- ok: false,
512
- url,
513
- connectLatencyMs: null,
514
- error: String(err),
515
- close: null,
516
- health: null,
517
- status: null,
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
- export async function runSecurityAudit(opts) {
532
- const findings = [];
533
- const cfg = opts.config;
534
- const env = opts.env ?? process.env;
535
- const platform = opts.platform ?? process.platform;
536
- const execIcacls = opts.execIcacls;
537
- const stateDir = opts.stateDir ?? resolveStateDir(env);
538
- const configPath = opts.configPath ?? resolveConfigPath(env, stateDir);
539
- findings.push(...collectAttackSurfaceSummaryFindings(cfg));
540
- findings.push(...collectSyncedFolderFindings({ stateDir, configPath }));
541
- findings.push(...collectGatewayConfigFindings(cfg, env));
542
- findings.push(...collectBrowserControlFindings(cfg, env));
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
- if (opts.includeChannelSecurity !== false) {
582
- const plugins = opts.plugins ?? listChannelPlugins();
583
- findings.push(...(await collectChannelSecurityFindings({ cfg, plugins })));
584
- }
585
- const deep = opts.deep === true
586
- ? await maybeProbeGateway({
587
- cfg,
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
  }