@poolzin/pool-bot 2026.2.24 → 2026.2.25
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 +21 -0
- package/dist/acp/client.js +207 -18
- package/dist/acp/secret-file.js +22 -0
- package/dist/agents/agent-scope.js +10 -0
- package/dist/agents/bash-process-registry.test-helpers.js +29 -0
- package/dist/agents/bash-tools.exec-approval-request.js +20 -0
- package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
- package/dist/agents/bash-tools.exec-host-node.js +235 -0
- package/dist/agents/bash-tools.exec-types.js +1 -0
- package/dist/agents/bash-tools.process.js +224 -218
- package/dist/agents/content-blocks.js +16 -0
- package/dist/agents/model-fallback.js +96 -101
- package/dist/agents/models-config.providers.js +299 -182
- package/dist/agents/pi-embedded-payloads.js +1 -0
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
- package/dist/agents/skills.test-helpers.js +13 -0
- package/dist/agents/stable-stringify.js +12 -0
- package/dist/agents/subagent-registry.mocks.shared.js +12 -0
- package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
- package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
- package/dist/agents/tool-policy-shared.js +108 -0
- package/dist/agents/tools/browser-tool.js +160 -54
- package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
- package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
- package/dist/agents/tools/image-tool.js +214 -99
- package/dist/agents/tools/sessions-history-tool.js +140 -108
- package/dist/agents/workspace.js +222 -46
- package/dist/auto-reply/commands-registry.js +15 -18
- package/dist/auto-reply/fallback-state.js +114 -0
- package/dist/auto-reply/model-runtime.js +68 -0
- package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
- package/dist/auto-reply/reply/agent-runner.js +165 -39
- package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
- package/dist/browser/config.js +26 -0
- package/dist/browser/navigation-guard.js +31 -0
- package/dist/browser/routes/agent.act.js +431 -424
- package/dist/browser/routes/agent.shared.js +47 -3
- package/dist/browser/routes/agent.snapshot.js +122 -116
- package/dist/browser/routes/agent.storage.js +303 -297
- package/dist/browser/routes/tabs.js +154 -100
- package/dist/browser/server-lifecycle.js +37 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/allow-from.js +25 -0
- package/dist/channels/plugins/account-action-gate.js +13 -0
- package/dist/channels/plugins/message-actions.js +10 -0
- package/dist/channels/telegram/api.js +18 -0
- package/dist/cli/argv.js +84 -21
- package/dist/cli/banner.js +2 -1
- package/dist/cli/exec-approvals-cli.js +92 -124
- package/dist/cli/memory-cli.js +158 -61
- package/dist/cli/nodes-cli/register.push.js +63 -0
- package/dist/cli/nodes-media-utils.js +21 -0
- package/dist/cli/plugins-cli.js +245 -61
- package/dist/cli/program/build-program.js +3 -1
- package/dist/cli/program/command-registry.js +223 -136
- package/dist/cli/program/help.js +43 -12
- package/dist/cli/route.js +1 -1
- package/dist/cli/test-runtime-capture.js +24 -0
- package/dist/commands/agent.js +163 -87
- package/dist/commands/channels.mock-harness.js +23 -0
- package/dist/commands/daemon-install-runtime-warning.js +11 -0
- package/dist/commands/sessions.test-helpers.js +61 -0
- package/dist/config/commands.js +3 -0
- package/dist/config/config.js +1 -1
- package/dist/config/env-substitution.js +62 -34
- package/dist/config/env-vars.js +9 -0
- package/dist/config/io.js +571 -171
- package/dist/config/merge-patch.js +50 -4
- package/dist/config/redact-snapshot.js +404 -76
- package/dist/config/schema.js +58 -570
- package/dist/config/validation.js +140 -85
- package/dist/config/zod-schema.hooks.js +40 -11
- package/dist/config/zod-schema.installs.js +20 -0
- package/dist/config/zod-schema.js +8 -7
- package/dist/daemon/cmd-argv.js +21 -0
- package/dist/daemon/cmd-set.js +58 -0
- package/dist/daemon/service-types.js +1 -0
- package/dist/discord/monitor/exec-approvals.js +357 -162
- package/dist/gateway/auth.js +38 -3
- package/dist/gateway/call.js +149 -68
- package/dist/gateway/canvas-capability.js +75 -0
- package/dist/gateway/control-plane-audit.js +28 -0
- package/dist/gateway/control-plane-rate-limit.js +53 -0
- package/dist/gateway/events.js +1 -0
- package/dist/gateway/hooks.js +109 -54
- package/dist/gateway/http-common.js +22 -0
- package/dist/gateway/method-scopes.js +169 -0
- package/dist/gateway/net.js +23 -0
- package/dist/gateway/openresponses-http.js +120 -110
- package/dist/gateway/probe-auth.js +2 -0
- package/dist/gateway/protocol/index.js +3 -2
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
- package/dist/gateway/protocol/schema/push.js +18 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-http.js +236 -52
- package/dist/gateway/server-methods/agent.js +162 -24
- package/dist/gateway/server-methods/chat.js +461 -130
- package/dist/gateway/server-methods/config.js +193 -150
- package/dist/gateway/server-methods/nodes.helpers.js +12 -0
- package/dist/gateway/server-methods/nodes.js +251 -69
- package/dist/gateway/server-methods/push.js +53 -0
- package/dist/gateway/server-reload-handlers.js +2 -3
- package/dist/gateway/server-runtime-config.js +5 -0
- package/dist/gateway/server-runtime-state.js +2 -0
- package/dist/gateway/server-ws-runtime.js +1 -0
- package/dist/gateway/server.impl.js +296 -139
- package/dist/gateway/session-preview.test-helpers.js +11 -0
- package/dist/gateway/startup-auth.js +126 -0
- package/dist/gateway/test-helpers.agent-results.js +15 -0
- package/dist/gateway/test-helpers.mocks.js +37 -14
- package/dist/gateway/test-helpers.server.js +161 -77
- package/dist/hooks/bundled/session-memory/handler.js +165 -34
- package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
- package/dist/infra/archive-path.js +49 -0
- package/dist/infra/device-pairing.js +148 -167
- package/dist/infra/exec-approvals-allowlist.js +19 -70
- package/dist/infra/exec-approvals-analysis.js +44 -17
- package/dist/infra/exec-safe-bin-policy.js +269 -0
- package/dist/infra/fixed-window-rate-limit.js +33 -0
- package/dist/infra/git-root.js +61 -0
- package/dist/infra/heartbeat-active-hours.js +2 -2
- package/dist/infra/heartbeat-reason.js +40 -0
- package/dist/infra/heartbeat-runner.js +72 -32
- package/dist/infra/install-source-utils.js +91 -7
- package/dist/infra/node-pairing.js +50 -105
- package/dist/infra/npm-integrity.js +45 -0
- package/dist/infra/npm-pack-install.js +40 -0
- package/dist/infra/outbound/channel-adapters.js +20 -7
- package/dist/infra/outbound/message-action-runner.js +107 -327
- package/dist/infra/outbound/message.js +59 -36
- package/dist/infra/outbound/outbound-policy.js +52 -25
- package/dist/infra/outbound/outbound-send-service.js +58 -71
- package/dist/infra/pairing-files.js +10 -0
- package/dist/infra/plain-object.js +9 -0
- package/dist/infra/push-apns.js +365 -0
- package/dist/infra/restart-sentinel.js +16 -1
- package/dist/infra/restart.js +229 -26
- package/dist/infra/scp-host.js +54 -0
- package/dist/infra/update-startup.js +86 -9
- package/dist/media/inbound-path-policy.js +114 -0
- package/dist/media/input-files.js +16 -0
- package/dist/memory/test-manager.js +8 -0
- package/dist/plugin-sdk/temp-path.js +47 -0
- package/dist/plugins/discovery.js +217 -23
- package/dist/plugins/hook-runner-global.js +16 -0
- package/dist/plugins/loader.js +192 -26
- package/dist/plugins/logger.js +8 -0
- package/dist/plugins/manifest-registry.js +3 -0
- package/dist/plugins/path-safety.js +34 -0
- package/dist/plugins/registry.js +5 -2
- package/dist/plugins/runtime/index.js +271 -206
- package/dist/providers/github-copilot-models.js +4 -1
- package/dist/security/audit-channel.js +8 -19
- package/dist/security/audit-extra.async.js +354 -182
- package/dist/security/audit-extra.js +11 -1
- package/dist/security/audit-extra.sync.js +340 -33
- package/dist/security/audit-fs.js +31 -13
- package/dist/security/audit.js +145 -371
- package/dist/security/dm-policy-shared.js +24 -0
- package/dist/security/external-content.js +20 -8
- package/dist/security/fix.js +49 -85
- package/dist/security/scan-paths.js +20 -0
- package/dist/security/secret-equal.js +3 -7
- package/dist/security/windows-acl.js +30 -15
- package/dist/shared/node-list-parse.js +13 -0
- package/dist/shared/operator-scope-compat.js +37 -0
- package/dist/shared/text-chunking.js +29 -0
- package/dist/slack/blocks.test-helpers.js +31 -0
- package/dist/slack/monitor/mrkdwn.js +8 -0
- package/dist/telegram/bot-message-dispatch.js +366 -164
- package/dist/telegram/draft-stream.js +30 -7
- package/dist/telegram/reasoning-lane-coordinator.js +128 -0
- package/dist/terminal/prompt-select-styled.js +9 -0
- package/dist/test-utils/command-runner.js +6 -0
- package/dist/test-utils/internal-hook-event-payload.js +10 -0
- package/dist/test-utils/model-auth-mock.js +12 -0
- package/dist/test-utils/provider-usage-fetch.js +14 -0
- package/dist/test-utils/temp-home.js +33 -0
- package/dist/tui/components/chat-log.js +9 -0
- package/dist/tui/tui-command-handlers.js +36 -27
- package/dist/tui/tui-event-handlers.js +122 -32
- package/dist/tui/tui.js +181 -45
- package/dist/utils/mask-api-key.js +10 -0
- package/dist/utils/run-with-concurrency.js +39 -0
- package/dist/web/media.js +4 -0
- package/docs/tools/slash-commands.md +5 -1
- package/extensions/feishu/src/external-keys.ts +19 -0
- package/extensions/lobster/src/windows-spawn.ts +193 -0
- package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
- package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
- package/package.json +1 -1
package/dist/security/audit.js
CHANGED
|
@@ -1,49 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
|
1
|
+
import { resolveSandboxConfigForAgent } from "../agents/sandbox.js";
|
|
3
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";
|
|
4
6
|
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
|
5
7
|
import { resolveGatewayAuth } from "../gateway/auth.js";
|
|
6
|
-
import { formatCliCommand } from "../cli/command-format.js";
|
|
7
8
|
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
|
9
|
+
import { resolveGatewayProbeAuth } from "../gateway/probe-auth.js";
|
|
8
10
|
import { probeGateway } from "../gateway/probe.js";
|
|
9
|
-
import {
|
|
10
|
-
import { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectHooksHardeningFindings, collectIncludeFilePermFindings, collectModelHygieneFindings, collectSmallModelRiskFindings, collectPluginsTrustFindings, collectSecretsInConfigFindings, collectStateDeepFilesystemFindings, collectSyncedFolderFindings, readConfigSnapshotForAudit, } from "./audit-extra.js";
|
|
11
|
-
import { readChannelAllowFromStore } from "../pairing/pairing-store.js";
|
|
12
|
-
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../config/commands.js";
|
|
11
|
+
import { collectChannelSecurityFindings } from "./audit-channel.js";
|
|
12
|
+
import { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectGatewayHttpNoAuthFindings, collectGatewayHttpSessionKeyOverrideFindings, collectHooksHardeningFindings, collectIncludeFilePermFindings, collectInstalledSkillsCodeSafetyFindings, collectMinimalProfileOverrideFindings, collectModelHygieneFindings, collectNodeDenyCommandPatternFindings, collectSmallModelRiskFindings, collectSandboxDangerousConfigFindings, collectSandboxDockerNoopFindings, collectPluginsTrustFindings, collectSecretsInConfigFindings, collectPluginsCodeSafetyFindings, collectStateDeepFilesystemFindings, collectSyncedFolderFindings, readConfigSnapshotForAudit, } from "./audit-extra.js";
|
|
13
13
|
import { formatPermissionDetail, formatPermissionRemediation, inspectPathPermissions, } from "./audit-fs.js";
|
|
14
|
+
import { DEFAULT_GATEWAY_HTTP_TOOL_DENY } from "./dangerous-tools.js";
|
|
14
15
|
function countBySeverity(findings) {
|
|
15
16
|
let critical = 0;
|
|
16
17
|
let warn = 0;
|
|
17
18
|
let info = 0;
|
|
18
19
|
for (const f of findings) {
|
|
19
|
-
if (f.severity === "critical")
|
|
20
|
+
if (f.severity === "critical") {
|
|
20
21
|
critical += 1;
|
|
21
|
-
|
|
22
|
+
}
|
|
23
|
+
else if (f.severity === "warn") {
|
|
22
24
|
warn += 1;
|
|
23
|
-
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
24
27
|
info += 1;
|
|
28
|
+
}
|
|
25
29
|
}
|
|
26
30
|
return { critical, warn, info };
|
|
27
31
|
}
|
|
28
32
|
function normalizeAllowFromList(list) {
|
|
29
|
-
if (!Array.isArray(list))
|
|
33
|
+
if (!Array.isArray(list)) {
|
|
30
34
|
return [];
|
|
31
|
-
return list.map((v) => String(v).trim()).filter(Boolean);
|
|
32
|
-
}
|
|
33
|
-
function classifyChannelWarningSeverity(message) {
|
|
34
|
-
const s = message.toLowerCase();
|
|
35
|
-
if (s.includes("dms: open") ||
|
|
36
|
-
s.includes('grouppolicy="open"') ||
|
|
37
|
-
s.includes('dmpolicy="open"')) {
|
|
38
|
-
return "critical";
|
|
39
|
-
}
|
|
40
|
-
if (s.includes("allows any") || s.includes("anyone can dm") || s.includes("public")) {
|
|
41
|
-
return "critical";
|
|
42
|
-
}
|
|
43
|
-
if (s.includes("locked") || s.includes("disabled")) {
|
|
44
|
-
return "info";
|
|
45
35
|
}
|
|
46
|
-
return
|
|
36
|
+
return list.map((v) => String(v).trim()).filter(Boolean);
|
|
47
37
|
}
|
|
48
38
|
async function collectFilesystemFindings(params) {
|
|
49
39
|
const findings = [];
|
|
@@ -66,7 +56,7 @@ async function collectFilesystemFindings(params) {
|
|
|
66
56
|
checkId: "fs.state_dir.perms_world_writable",
|
|
67
57
|
severity: "critical",
|
|
68
58
|
title: "State dir is world-writable",
|
|
69
|
-
detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; other users can write into your
|
|
59
|
+
detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; other users can write into your Pool Bot state.`,
|
|
70
60
|
remediation: formatPermissionRemediation({
|
|
71
61
|
targetPath: params.stateDir,
|
|
72
62
|
perms: stateDirPerms,
|
|
@@ -81,7 +71,7 @@ async function collectFilesystemFindings(params) {
|
|
|
81
71
|
checkId: "fs.state_dir.perms_group_writable",
|
|
82
72
|
severity: "warn",
|
|
83
73
|
title: "State dir is group-writable",
|
|
84
|
-
detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; group users can write into your
|
|
74
|
+
detail: `${formatPermissionDetail(params.stateDir, stateDirPerms)}; group users can write into your Pool Bot state.`,
|
|
85
75
|
remediation: formatPermissionRemediation({
|
|
86
76
|
targetPath: params.stateDir,
|
|
87
77
|
perms: stateDirPerms,
|
|
@@ -113,6 +103,7 @@ async function collectFilesystemFindings(params) {
|
|
|
113
103
|
exec: params.execIcacls,
|
|
114
104
|
});
|
|
115
105
|
if (configPerms.ok) {
|
|
106
|
+
const skipReadablePermWarnings = configPerms.isSymlink;
|
|
116
107
|
if (configPerms.isSymlink) {
|
|
117
108
|
findings.push({
|
|
118
109
|
checkId: "fs.config.symlink",
|
|
@@ -136,7 +127,7 @@ async function collectFilesystemFindings(params) {
|
|
|
136
127
|
}),
|
|
137
128
|
});
|
|
138
129
|
}
|
|
139
|
-
else if (configPerms.worldReadable) {
|
|
130
|
+
else if (!skipReadablePermWarnings && configPerms.worldReadable) {
|
|
140
131
|
findings.push({
|
|
141
132
|
checkId: "fs.config.perms_world_readable",
|
|
142
133
|
severity: "critical",
|
|
@@ -151,7 +142,7 @@ async function collectFilesystemFindings(params) {
|
|
|
151
142
|
}),
|
|
152
143
|
});
|
|
153
144
|
}
|
|
154
|
-
else if (configPerms.groupReadable) {
|
|
145
|
+
else if (!skipReadablePermWarnings && configPerms.groupReadable) {
|
|
155
146
|
findings.push({
|
|
156
147
|
checkId: "fs.config.perms_group_readable",
|
|
157
148
|
severity: "warn",
|
|
@@ -181,12 +172,13 @@ function collectGatewayConfigFindings(cfg, env) {
|
|
|
181
172
|
const hasToken = typeof auth.token === "string" && auth.token.trim().length > 0;
|
|
182
173
|
const hasPassword = typeof auth.password === "string" && auth.password.trim().length > 0;
|
|
183
174
|
const hasSharedSecret = (auth.mode === "token" && hasToken) || (auth.mode === "password" && hasPassword);
|
|
184
|
-
const hasTailscaleAuth = auth.allowTailscale
|
|
175
|
+
const hasTailscaleAuth = auth.allowTailscale && tailscaleMode === "serve";
|
|
185
176
|
const hasGatewayAuth = hasSharedSecret || hasTailscaleAuth;
|
|
186
|
-
// HTTP /tools/invoke
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
177
|
+
// HTTP /tools/invoke is intended for narrow automation, not session orchestration/admin operations.
|
|
178
|
+
// If operators opt-in to re-enabling these tools over HTTP, warn loudly so the choice is explicit.
|
|
179
|
+
const gatewayToolsAllowRaw = Array.isArray(cfg.gateway?.tools?.allow)
|
|
180
|
+
? cfg.gateway?.tools?.allow
|
|
181
|
+
: [];
|
|
190
182
|
const gatewayToolsAllow = new Set(gatewayToolsAllowRaw
|
|
191
183
|
.map((v) => (typeof v === "string" ? v.trim().toLowerCase() : ""))
|
|
192
184
|
.filter(Boolean));
|
|
@@ -203,7 +195,7 @@ function collectGatewayConfigFindings(cfg, env) {
|
|
|
203
195
|
"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.",
|
|
204
196
|
});
|
|
205
197
|
}
|
|
206
|
-
if (bind !== "loopback" && !hasSharedSecret) {
|
|
198
|
+
if (bind !== "loopback" && !hasSharedSecret && auth.mode !== "trusted-proxy") {
|
|
207
199
|
findings.push({
|
|
208
200
|
checkId: "gateway.bind_no_auth",
|
|
209
201
|
severity: "critical",
|
|
@@ -277,8 +269,54 @@ function collectGatewayConfigFindings(cfg, env) {
|
|
|
277
269
|
detail: `gateway auth token is ${token.length} chars; prefer a long random token.`,
|
|
278
270
|
});
|
|
279
271
|
}
|
|
280
|
-
|
|
281
|
-
|
|
272
|
+
if (auth.mode === "trusted-proxy") {
|
|
273
|
+
const trustedProxies = cfg.gateway?.trustedProxies ?? [];
|
|
274
|
+
const trustedProxyConfig = cfg.gateway?.auth?.trustedProxy;
|
|
275
|
+
findings.push({
|
|
276
|
+
checkId: "gateway.trusted_proxy_auth",
|
|
277
|
+
severity: "critical",
|
|
278
|
+
title: "Trusted-proxy auth mode enabled",
|
|
279
|
+
detail: 'gateway.auth.mode="trusted-proxy" delegates authentication to a reverse proxy. ' +
|
|
280
|
+
"Ensure your proxy (Pomerium, Caddy, nginx) handles auth correctly and that gateway.trustedProxies " +
|
|
281
|
+
"only contains IPs of your actual proxy servers.",
|
|
282
|
+
remediation: "Verify: (1) Your proxy terminates TLS and authenticates users. " +
|
|
283
|
+
"(2) gateway.trustedProxies is restricted to proxy IPs only. " +
|
|
284
|
+
"(3) Direct access to the Gateway port is blocked by firewall. " +
|
|
285
|
+
"See /gateway/trusted-proxy-auth for setup guidance.",
|
|
286
|
+
});
|
|
287
|
+
if (trustedProxies.length === 0) {
|
|
288
|
+
findings.push({
|
|
289
|
+
checkId: "gateway.trusted_proxy_no_proxies",
|
|
290
|
+
severity: "critical",
|
|
291
|
+
title: "Trusted-proxy auth enabled but no trusted proxies configured",
|
|
292
|
+
detail: 'gateway.auth.mode="trusted-proxy" but gateway.trustedProxies is empty. ' +
|
|
293
|
+
"All requests will be rejected.",
|
|
294
|
+
remediation: "Set gateway.trustedProxies to the IP(s) of your reverse proxy.",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (!trustedProxyConfig?.userHeader) {
|
|
298
|
+
findings.push({
|
|
299
|
+
checkId: "gateway.trusted_proxy_no_user_header",
|
|
300
|
+
severity: "critical",
|
|
301
|
+
title: "Trusted-proxy auth missing userHeader config",
|
|
302
|
+
detail: 'gateway.auth.mode="trusted-proxy" but gateway.auth.trustedProxy.userHeader is not configured.',
|
|
303
|
+
remediation: "Set gateway.auth.trustedProxy.userHeader to the header name your proxy uses " +
|
|
304
|
+
'(e.g., "x-forwarded-user", "x-pomerium-claim-email").',
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const allowUsers = trustedProxyConfig?.allowUsers ?? [];
|
|
308
|
+
if (allowUsers.length === 0) {
|
|
309
|
+
findings.push({
|
|
310
|
+
checkId: "gateway.trusted_proxy_no_allowlist",
|
|
311
|
+
severity: "warn",
|
|
312
|
+
title: "Trusted-proxy auth allows all authenticated users",
|
|
313
|
+
detail: "gateway.auth.trustedProxy.allowUsers is empty, so any user authenticated by your proxy can access the Gateway.",
|
|
314
|
+
remediation: "Consider setting gateway.auth.trustedProxy.allowUsers to restrict access to specific users " +
|
|
315
|
+
'(e.g., ["nick@example.com"]).',
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (bind !== "loopback" && auth.mode !== "trusted-proxy" && !cfg.gateway?.auth?.rateLimit) {
|
|
282
320
|
findings.push({
|
|
283
321
|
checkId: "gateway.auth_no_rate_limit",
|
|
284
322
|
severity: "warn",
|
|
@@ -290,7 +328,7 @@ function collectGatewayConfigFindings(cfg, env) {
|
|
|
290
328
|
}
|
|
291
329
|
return findings;
|
|
292
330
|
}
|
|
293
|
-
function collectBrowserControlFindings(cfg) {
|
|
331
|
+
function collectBrowserControlFindings(cfg, env) {
|
|
294
332
|
const findings = [];
|
|
295
333
|
let resolved;
|
|
296
334
|
try {
|
|
@@ -306,12 +344,25 @@ function collectBrowserControlFindings(cfg) {
|
|
|
306
344
|
});
|
|
307
345
|
return findings;
|
|
308
346
|
}
|
|
309
|
-
if (!resolved.enabled)
|
|
347
|
+
if (!resolved.enabled) {
|
|
310
348
|
return findings;
|
|
349
|
+
}
|
|
350
|
+
const browserAuth = resolveBrowserControlAuth(cfg, env);
|
|
351
|
+
if (!browserAuth.token && !browserAuth.password) {
|
|
352
|
+
findings.push({
|
|
353
|
+
checkId: "browser.control_no_auth",
|
|
354
|
+
severity: "critical",
|
|
355
|
+
title: "Browser control has no auth",
|
|
356
|
+
detail: "Browser control HTTP routes are enabled but no gateway.auth token/password is configured. " +
|
|
357
|
+
"Any local process (or SSRF to loopback) can call browser control endpoints.",
|
|
358
|
+
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.",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
311
361
|
for (const name of Object.keys(resolved.profiles)) {
|
|
312
362
|
const profile = resolveProfile(resolved, name);
|
|
313
|
-
if (!profile || profile.cdpIsLoopback)
|
|
363
|
+
if (!profile || profile.cdpIsLoopback) {
|
|
314
364
|
continue;
|
|
365
|
+
}
|
|
315
366
|
let url;
|
|
316
367
|
try {
|
|
317
368
|
url = new URL(profile.cdpUrl);
|
|
@@ -333,8 +384,9 @@ function collectBrowserControlFindings(cfg) {
|
|
|
333
384
|
}
|
|
334
385
|
function collectLoggingFindings(cfg) {
|
|
335
386
|
const redact = cfg.logging?.redactSensitive;
|
|
336
|
-
if (redact !== "off")
|
|
387
|
+
if (redact !== "off") {
|
|
337
388
|
return [];
|
|
389
|
+
}
|
|
338
390
|
return [
|
|
339
391
|
{
|
|
340
392
|
checkId: "logging.redact_off",
|
|
@@ -350,10 +402,12 @@ function collectElevatedFindings(cfg) {
|
|
|
350
402
|
const enabled = cfg.tools?.elevated?.enabled;
|
|
351
403
|
const allowFrom = cfg.tools?.elevated?.allowFrom ?? {};
|
|
352
404
|
const anyAllowFromKeys = Object.keys(allowFrom).length > 0;
|
|
353
|
-
if (enabled === false)
|
|
405
|
+
if (enabled === false) {
|
|
354
406
|
return findings;
|
|
355
|
-
|
|
407
|
+
}
|
|
408
|
+
if (!anyAllowFromKeys) {
|
|
356
409
|
return findings;
|
|
410
|
+
}
|
|
357
411
|
for (const [provider, list] of Object.entries(allowFrom)) {
|
|
358
412
|
const normalized = normalizeAllowFromList(list);
|
|
359
413
|
if (normalized.includes("*")) {
|
|
@@ -375,310 +429,39 @@ function collectElevatedFindings(cfg) {
|
|
|
375
429
|
}
|
|
376
430
|
return findings;
|
|
377
431
|
}
|
|
378
|
-
|
|
432
|
+
function collectExecRuntimeFindings(cfg) {
|
|
379
433
|
const findings = [];
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const configAllowFrom = normalizeAllowFromList(input.allowFrom);
|
|
392
|
-
const hasWildcard = configAllowFrom.includes("*");
|
|
393
|
-
const dmScope = params.cfg.session?.dmScope ?? "main";
|
|
394
|
-
const storeAllowFrom = await readChannelAllowFromStore(input.provider).catch(() => []);
|
|
395
|
-
const normalizeEntry = input.normalizeEntry ?? ((value) => value);
|
|
396
|
-
const normalizedCfg = configAllowFrom
|
|
397
|
-
.filter((value) => value !== "*")
|
|
398
|
-
.map((value) => normalizeEntry(value))
|
|
399
|
-
.map((value) => value.trim())
|
|
400
|
-
.filter(Boolean);
|
|
401
|
-
const normalizedStore = storeAllowFrom
|
|
402
|
-
.map((value) => normalizeEntry(value))
|
|
403
|
-
.map((value) => value.trim())
|
|
404
|
-
.filter(Boolean);
|
|
405
|
-
const allowCount = Array.from(new Set([...normalizedCfg, ...normalizedStore])).length;
|
|
406
|
-
const isMultiUserDm = hasWildcard || allowCount > 1;
|
|
407
|
-
if (input.dmPolicy === "open") {
|
|
408
|
-
const allowFromKey = `${input.allowFromPath}allowFrom`;
|
|
409
|
-
findings.push({
|
|
410
|
-
checkId: `channels.${input.provider}.dm.open`,
|
|
411
|
-
severity: "critical",
|
|
412
|
-
title: `${input.label} DMs are open`,
|
|
413
|
-
detail: `${policyPath}="open" allows anyone to DM the bot.`,
|
|
414
|
-
remediation: `Use pairing/allowlist; if you really need open DMs, ensure ${allowFromKey} includes "*".`,
|
|
415
|
-
});
|
|
416
|
-
if (!hasWildcard) {
|
|
417
|
-
findings.push({
|
|
418
|
-
checkId: `channels.${input.provider}.dm.open_invalid`,
|
|
419
|
-
severity: "warn",
|
|
420
|
-
title: `${input.label} DM config looks inconsistent`,
|
|
421
|
-
detail: `"open" requires ${allowFromKey} to include "*".`,
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
if (input.dmPolicy === "disabled") {
|
|
426
|
-
findings.push({
|
|
427
|
-
checkId: `channels.${input.provider}.dm.disabled`,
|
|
428
|
-
severity: "info",
|
|
429
|
-
title: `${input.label} DMs are disabled`,
|
|
430
|
-
detail: `${policyPath}="disabled" ignores inbound DMs.`,
|
|
431
|
-
});
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
if (dmScope === "main" && isMultiUserDm) {
|
|
435
|
-
findings.push({
|
|
436
|
-
checkId: `channels.${input.provider}.dm.scope_main_multiuser`,
|
|
437
|
-
severity: "warn",
|
|
438
|
-
title: `${input.label} DMs share the main session`,
|
|
439
|
-
detail: "Multiple DM senders currently share the main session, which can leak context across users.",
|
|
440
|
-
remediation: 'Set session.dmScope="per-channel-peer" to isolate DM sessions per sender.',
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
};
|
|
444
|
-
for (const plugin of params.plugins) {
|
|
445
|
-
if (!plugin.security)
|
|
446
|
-
continue;
|
|
447
|
-
const accountIds = plugin.config.listAccountIds(params.cfg);
|
|
448
|
-
const defaultAccountId = resolveChannelDefaultAccountId({
|
|
449
|
-
plugin,
|
|
450
|
-
cfg: params.cfg,
|
|
451
|
-
accountIds,
|
|
434
|
+
const globalExecHost = cfg.tools?.exec?.host;
|
|
435
|
+
const defaultSandboxMode = resolveSandboxConfigForAgent(cfg).mode;
|
|
436
|
+
const defaultHostIsExplicitSandbox = globalExecHost === "sandbox";
|
|
437
|
+
if (defaultHostIsExplicitSandbox && defaultSandboxMode === "off") {
|
|
438
|
+
findings.push({
|
|
439
|
+
checkId: "tools.exec.host_sandbox_no_sandbox_defaults",
|
|
440
|
+
severity: "warn",
|
|
441
|
+
title: "Exec host is sandbox but sandbox mode is off",
|
|
442
|
+
detail: "tools.exec.host is explicitly set to sandbox while agents.defaults.sandbox.mode=off. " +
|
|
443
|
+
"In this mode, exec runs directly on the gateway host.",
|
|
444
|
+
remediation: 'Enable sandbox mode (`agents.defaults.sandbox.mode="non-main"` or `"all"`) or set tools.exec.host to "gateway" with approvals.',
|
|
452
445
|
});
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
providerId: "discord",
|
|
472
|
-
providerSetting: coerceNativeSetting(discordCfg.commands?.nativeSkills),
|
|
473
|
-
globalSetting: params.cfg.commands?.nativeSkills,
|
|
474
|
-
});
|
|
475
|
-
const slashEnabled = nativeEnabled || nativeSkillsEnabled;
|
|
476
|
-
if (slashEnabled) {
|
|
477
|
-
const defaultGroupPolicy = params.cfg.channels?.defaults?.groupPolicy;
|
|
478
|
-
const groupPolicy = discordCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
479
|
-
const guildEntries = discordCfg.guilds ?? {};
|
|
480
|
-
const guildsConfigured = Object.keys(guildEntries).length > 0;
|
|
481
|
-
const hasAnyUserAllowlist = Object.values(guildEntries).some((guild) => {
|
|
482
|
-
if (!guild || typeof guild !== "object")
|
|
483
|
-
return false;
|
|
484
|
-
const g = guild;
|
|
485
|
-
if (Array.isArray(g.users) && g.users.length > 0)
|
|
486
|
-
return true;
|
|
487
|
-
const channels = g.channels;
|
|
488
|
-
if (!channels || typeof channels !== "object")
|
|
489
|
-
return false;
|
|
490
|
-
return Object.values(channels).some((channel) => {
|
|
491
|
-
if (!channel || typeof channel !== "object")
|
|
492
|
-
return false;
|
|
493
|
-
const c = channel;
|
|
494
|
-
return Array.isArray(c.users) && c.users.length > 0;
|
|
495
|
-
});
|
|
496
|
-
});
|
|
497
|
-
const dmAllowFromRaw = discordCfg.dm?.allowFrom;
|
|
498
|
-
const dmAllowFrom = Array.isArray(dmAllowFromRaw) ? dmAllowFromRaw : [];
|
|
499
|
-
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
|
|
500
|
-
const ownerAllowFromConfigured = normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0;
|
|
501
|
-
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
|
502
|
-
if (!useAccessGroups &&
|
|
503
|
-
groupPolicy !== "disabled" &&
|
|
504
|
-
guildsConfigured &&
|
|
505
|
-
!hasAnyUserAllowlist) {
|
|
506
|
-
findings.push({
|
|
507
|
-
checkId: "channels.discord.commands.native.unrestricted",
|
|
508
|
-
severity: "critical",
|
|
509
|
-
title: "Discord slash commands are unrestricted",
|
|
510
|
-
detail: "commands.useAccessGroups=false disables sender allowlists for Discord slash commands unless a per-guild/channel users allowlist is configured; with no users allowlist, any user in allowed guild channels can invoke /… commands.",
|
|
511
|
-
remediation: "Set commands.useAccessGroups=true (recommended), or configure channels.discord.guilds.<id>.users (or channels.discord.guilds.<id>.channels.<channel>.users).",
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
else if (useAccessGroups &&
|
|
515
|
-
groupPolicy !== "disabled" &&
|
|
516
|
-
guildsConfigured &&
|
|
517
|
-
!ownerAllowFromConfigured &&
|
|
518
|
-
!hasAnyUserAllowlist) {
|
|
519
|
-
findings.push({
|
|
520
|
-
checkId: "channels.discord.commands.native.no_allowlists",
|
|
521
|
-
severity: "warn",
|
|
522
|
-
title: "Discord slash commands have no allowlists",
|
|
523
|
-
detail: "Discord slash commands are enabled, but neither an owner allowFrom list nor any per-guild/channel users allowlist is configured; /… commands will be rejected for everyone.",
|
|
524
|
-
remediation: "Add your user id to channels.discord.dm.allowFrom (or approve yourself via pairing), or configure channels.discord.guilds.<id>.users.",
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
if (plugin.id === "slack") {
|
|
530
|
-
const slackCfg = account
|
|
531
|
-
?.config ?? {};
|
|
532
|
-
const nativeEnabled = resolveNativeCommandsEnabled({
|
|
533
|
-
providerId: "slack",
|
|
534
|
-
providerSetting: coerceNativeSetting(slackCfg.commands?.native),
|
|
535
|
-
globalSetting: params.cfg.commands?.native,
|
|
536
|
-
});
|
|
537
|
-
const nativeSkillsEnabled = resolveNativeSkillsEnabled({
|
|
538
|
-
providerId: "slack",
|
|
539
|
-
providerSetting: coerceNativeSetting(slackCfg.commands?.nativeSkills),
|
|
540
|
-
globalSetting: params.cfg.commands?.nativeSkills,
|
|
541
|
-
});
|
|
542
|
-
const slashCommandEnabled = nativeEnabled ||
|
|
543
|
-
nativeSkillsEnabled ||
|
|
544
|
-
slackCfg.slashCommand?.enabled === true;
|
|
545
|
-
if (slashCommandEnabled) {
|
|
546
|
-
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
|
547
|
-
if (!useAccessGroups) {
|
|
548
|
-
findings.push({
|
|
549
|
-
checkId: "channels.slack.commands.slash.useAccessGroups_off",
|
|
550
|
-
severity: "critical",
|
|
551
|
-
title: "Slack slash commands bypass access groups",
|
|
552
|
-
detail: "Slack slash/native commands are enabled while commands.useAccessGroups=false; this can allow unrestricted /… command execution from channels/users you didn't explicitly authorize.",
|
|
553
|
-
remediation: "Set commands.useAccessGroups=true (recommended).",
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
else {
|
|
557
|
-
const dmAllowFromRaw = account?.dm
|
|
558
|
-
?.allowFrom;
|
|
559
|
-
const dmAllowFrom = Array.isArray(dmAllowFromRaw) ? dmAllowFromRaw : [];
|
|
560
|
-
const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []);
|
|
561
|
-
const ownerAllowFromConfigured = normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0;
|
|
562
|
-
const channels = slackCfg.channels ?? {};
|
|
563
|
-
const hasAnyChannelUsersAllowlist = Object.values(channels).some((value) => {
|
|
564
|
-
if (!value || typeof value !== "object")
|
|
565
|
-
return false;
|
|
566
|
-
const channel = value;
|
|
567
|
-
return Array.isArray(channel.users) && channel.users.length > 0;
|
|
568
|
-
});
|
|
569
|
-
if (!ownerAllowFromConfigured && !hasAnyChannelUsersAllowlist) {
|
|
570
|
-
findings.push({
|
|
571
|
-
checkId: "channels.slack.commands.slash.no_allowlists",
|
|
572
|
-
severity: "warn",
|
|
573
|
-
title: "Slack slash commands have no allowlists",
|
|
574
|
-
detail: "Slack slash/native commands are enabled, but neither an owner allowFrom list nor any channels.<id>.users allowlist is configured; /… commands will be rejected for everyone.",
|
|
575
|
-
remediation: "Approve yourself via pairing (recommended), or set channels.slack.dm.allowFrom and/or channels.slack.channels.<id>.users.",
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
const dmPolicy = plugin.security.resolveDmPolicy?.({
|
|
582
|
-
cfg: params.cfg,
|
|
583
|
-
accountId: defaultAccountId,
|
|
584
|
-
account,
|
|
446
|
+
}
|
|
447
|
+
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : [];
|
|
448
|
+
const riskyAgents = agents
|
|
449
|
+
.filter((entry) => entry &&
|
|
450
|
+
typeof entry === "object" &&
|
|
451
|
+
typeof entry.id === "string" &&
|
|
452
|
+
entry.tools?.exec?.host === "sandbox" &&
|
|
453
|
+
resolveSandboxConfigForAgent(cfg, entry.id).mode === "off")
|
|
454
|
+
.map((entry) => entry.id)
|
|
455
|
+
.slice(0, 5);
|
|
456
|
+
if (riskyAgents.length > 0) {
|
|
457
|
+
findings.push({
|
|
458
|
+
checkId: "tools.exec.host_sandbox_no_sandbox_agents",
|
|
459
|
+
severity: "warn",
|
|
460
|
+
title: "Agent exec host uses sandbox while sandbox mode is off",
|
|
461
|
+
detail: `agents.list.*.tools.exec.host is set to sandbox for: ${riskyAgents.join(", ")}. ` +
|
|
462
|
+
"With sandbox mode off, exec runs directly on the gateway host.",
|
|
463
|
+
remediation: 'Enable sandbox mode for these agents (`agents.list[].sandbox.mode`) or set their tools.exec.host to "gateway".',
|
|
585
464
|
});
|
|
586
|
-
if (dmPolicy) {
|
|
587
|
-
await warnDmPolicy({
|
|
588
|
-
label: plugin.meta.label ?? plugin.id,
|
|
589
|
-
provider: plugin.id,
|
|
590
|
-
dmPolicy: dmPolicy.policy,
|
|
591
|
-
allowFrom: dmPolicy.allowFrom,
|
|
592
|
-
policyPath: dmPolicy.policyPath,
|
|
593
|
-
allowFromPath: dmPolicy.allowFromPath,
|
|
594
|
-
normalizeEntry: dmPolicy.normalizeEntry,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
if (plugin.security.collectWarnings) {
|
|
598
|
-
const warnings = await plugin.security.collectWarnings({
|
|
599
|
-
cfg: params.cfg,
|
|
600
|
-
accountId: defaultAccountId,
|
|
601
|
-
account,
|
|
602
|
-
});
|
|
603
|
-
for (const message of warnings ?? []) {
|
|
604
|
-
const trimmed = String(message).trim();
|
|
605
|
-
if (!trimmed)
|
|
606
|
-
continue;
|
|
607
|
-
findings.push({
|
|
608
|
-
checkId: `channels.${plugin.id}.warning.${findings.length + 1}`,
|
|
609
|
-
severity: classifyChannelWarningSeverity(trimmed),
|
|
610
|
-
title: `${plugin.meta.label ?? plugin.id} security warning`,
|
|
611
|
-
detail: trimmed.replace(/^-\s*/, ""),
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (plugin.id === "telegram") {
|
|
616
|
-
const allowTextCommands = params.cfg.commands?.text !== false;
|
|
617
|
-
if (!allowTextCommands)
|
|
618
|
-
continue;
|
|
619
|
-
const telegramCfg = account?.config ??
|
|
620
|
-
{};
|
|
621
|
-
const defaultGroupPolicy = params.cfg.channels?.defaults?.groupPolicy;
|
|
622
|
-
const groupPolicy = telegramCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
623
|
-
const groups = telegramCfg.groups;
|
|
624
|
-
const groupsConfigured = Boolean(groups) && Object.keys(groups ?? {}).length > 0;
|
|
625
|
-
const groupAccessPossible = groupPolicy === "open" || (groupPolicy === "allowlist" && groupsConfigured);
|
|
626
|
-
if (!groupAccessPossible)
|
|
627
|
-
continue;
|
|
628
|
-
const storeAllowFrom = await readChannelAllowFromStore("telegram").catch(() => []);
|
|
629
|
-
const storeHasWildcard = storeAllowFrom.some((v) => String(v).trim() === "*");
|
|
630
|
-
const groupAllowFrom = Array.isArray(telegramCfg.groupAllowFrom)
|
|
631
|
-
? telegramCfg.groupAllowFrom
|
|
632
|
-
: [];
|
|
633
|
-
const groupAllowFromHasWildcard = groupAllowFrom.some((v) => String(v).trim() === "*");
|
|
634
|
-
const anyGroupOverride = Boolean(groups &&
|
|
635
|
-
Object.values(groups).some((value) => {
|
|
636
|
-
if (!value || typeof value !== "object")
|
|
637
|
-
return false;
|
|
638
|
-
const group = value;
|
|
639
|
-
const allowFrom = Array.isArray(group.allowFrom) ? group.allowFrom : [];
|
|
640
|
-
if (allowFrom.length > 0)
|
|
641
|
-
return true;
|
|
642
|
-
const topics = group.topics;
|
|
643
|
-
if (!topics || typeof topics !== "object")
|
|
644
|
-
return false;
|
|
645
|
-
return Object.values(topics).some((topicValue) => {
|
|
646
|
-
if (!topicValue || typeof topicValue !== "object")
|
|
647
|
-
return false;
|
|
648
|
-
const topic = topicValue;
|
|
649
|
-
const topicAllow = Array.isArray(topic.allowFrom) ? topic.allowFrom : [];
|
|
650
|
-
return topicAllow.length > 0;
|
|
651
|
-
});
|
|
652
|
-
}));
|
|
653
|
-
const hasAnySenderAllowlist = storeAllowFrom.length > 0 || groupAllowFrom.length > 0 || anyGroupOverride;
|
|
654
|
-
if (storeHasWildcard || groupAllowFromHasWildcard) {
|
|
655
|
-
findings.push({
|
|
656
|
-
checkId: "channels.telegram.groups.allowFrom.wildcard",
|
|
657
|
-
severity: "critical",
|
|
658
|
-
title: "Telegram group allowlist contains wildcard",
|
|
659
|
-
detail: 'Telegram group sender allowlist contains "*", which allows any group member to run /… commands and control directives.',
|
|
660
|
-
remediation: 'Remove "*" from channels.telegram.groupAllowFrom and pairing store; prefer explicit user ids/usernames.',
|
|
661
|
-
});
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
if (!hasAnySenderAllowlist) {
|
|
665
|
-
const providerSetting = telegramCfg.commands
|
|
666
|
-
?.nativeSkills;
|
|
667
|
-
const skillsEnabled = resolveNativeSkillsEnabled({
|
|
668
|
-
providerId: "telegram",
|
|
669
|
-
providerSetting,
|
|
670
|
-
globalSetting: params.cfg.commands?.nativeSkills,
|
|
671
|
-
});
|
|
672
|
-
findings.push({
|
|
673
|
-
checkId: "channels.telegram.groups.allowFrom.missing",
|
|
674
|
-
severity: "critical",
|
|
675
|
-
title: "Telegram group commands have no sender allowlist",
|
|
676
|
-
detail: `Telegram group access is enabled but no sender allowlist is configured; this allows any group member to invoke /… commands` +
|
|
677
|
-
(skillsEnabled ? " (including skill commands)." : "."),
|
|
678
|
-
remediation: "Approve yourself via pairing (recommended), or set channels.telegram.groupAllowFrom (or per-group groups.<id>.allowFrom).",
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
465
|
}
|
|
683
466
|
return findings;
|
|
684
467
|
}
|
|
@@ -688,29 +471,9 @@ async function maybeProbeGateway(params) {
|
|
|
688
471
|
const isRemoteMode = params.cfg.gateway?.mode === "remote";
|
|
689
472
|
const remoteUrlRaw = typeof params.cfg.gateway?.remote?.url === "string" ? params.cfg.gateway.remote.url.trim() : "";
|
|
690
473
|
const remoteUrlMissing = isRemoteMode && !remoteUrlRaw;
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const remote = params.cfg.gateway?.remote;
|
|
695
|
-
const token = mode === "remote"
|
|
696
|
-
? typeof remote?.token === "string" && remote.token.trim()
|
|
697
|
-
? remote.token.trim()
|
|
698
|
-
: undefined
|
|
699
|
-
: process.env.POOLBOT_GATEWAY_TOKEN?.trim() ||
|
|
700
|
-
process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() ||
|
|
701
|
-
(typeof authToken === "string" && authToken.trim() ? authToken.trim() : undefined);
|
|
702
|
-
const password = process.env.POOLBOT_GATEWAY_PASSWORD?.trim() ||
|
|
703
|
-
process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() ||
|
|
704
|
-
(mode === "remote"
|
|
705
|
-
? typeof remote?.password === "string" && remote.password.trim()
|
|
706
|
-
? remote.password.trim()
|
|
707
|
-
: undefined
|
|
708
|
-
: typeof authPassword === "string" && authPassword.trim()
|
|
709
|
-
? authPassword.trim()
|
|
710
|
-
: undefined);
|
|
711
|
-
return { token, password };
|
|
712
|
-
};
|
|
713
|
-
const auth = !isRemoteMode || remoteUrlMissing ? resolveAuth("local") : resolveAuth("remote");
|
|
474
|
+
const auth = !isRemoteMode || remoteUrlMissing
|
|
475
|
+
? resolveGatewayProbeAuth({ cfg: params.cfg, mode: "local" })
|
|
476
|
+
: resolveGatewayProbeAuth({ cfg: params.cfg, mode: "remote" });
|
|
714
477
|
const res = await params.probe({ url, auth, timeoutMs: params.timeoutMs }).catch((err) => ({
|
|
715
478
|
ok: false,
|
|
716
479
|
url,
|
|
@@ -743,10 +506,17 @@ export async function runSecurityAudit(opts) {
|
|
|
743
506
|
findings.push(...collectAttackSurfaceSummaryFindings(cfg));
|
|
744
507
|
findings.push(...collectSyncedFolderFindings({ stateDir, configPath }));
|
|
745
508
|
findings.push(...collectGatewayConfigFindings(cfg, env));
|
|
746
|
-
findings.push(...collectBrowserControlFindings(cfg));
|
|
509
|
+
findings.push(...collectBrowserControlFindings(cfg, env));
|
|
747
510
|
findings.push(...collectLoggingFindings(cfg));
|
|
748
511
|
findings.push(...collectElevatedFindings(cfg));
|
|
749
|
-
findings.push(...
|
|
512
|
+
findings.push(...collectExecRuntimeFindings(cfg));
|
|
513
|
+
findings.push(...collectHooksHardeningFindings(cfg, env));
|
|
514
|
+
findings.push(...collectGatewayHttpNoAuthFindings(cfg, env));
|
|
515
|
+
findings.push(...collectGatewayHttpSessionKeyOverrideFindings(cfg));
|
|
516
|
+
findings.push(...collectSandboxDockerNoopFindings(cfg));
|
|
517
|
+
findings.push(...collectSandboxDangerousConfigFindings(cfg));
|
|
518
|
+
findings.push(...collectNodeDenyCommandPatternFindings(cfg));
|
|
519
|
+
findings.push(...collectMinimalProfileOverrideFindings(cfg));
|
|
750
520
|
findings.push(...collectSecretsInConfigFindings(cfg));
|
|
751
521
|
findings.push(...collectModelHygieneFindings(cfg));
|
|
752
522
|
findings.push(...collectSmallModelRiskFindings({ cfg, env }));
|
|
@@ -767,6 +537,10 @@ export async function runSecurityAudit(opts) {
|
|
|
767
537
|
}
|
|
768
538
|
findings.push(...(await collectStateDeepFilesystemFindings({ cfg, env, stateDir, platform, execIcacls })));
|
|
769
539
|
findings.push(...(await collectPluginsTrustFindings({ cfg, stateDir })));
|
|
540
|
+
if (opts.deep === true) {
|
|
541
|
+
findings.push(...(await collectPluginsCodeSafetyFindings({ stateDir })));
|
|
542
|
+
findings.push(...(await collectInstalledSkillsCodeSafetyFindings({ cfg, stateDir })));
|
|
543
|
+
}
|
|
770
544
|
}
|
|
771
545
|
if (opts.includeChannelSecurity !== false) {
|
|
772
546
|
const plugins = opts.plugins ?? listChannelPlugins();
|
|
@@ -779,7 +553,7 @@ export async function runSecurityAudit(opts) {
|
|
|
779
553
|
probe: opts.probeGatewayFn ?? probeGateway,
|
|
780
554
|
})
|
|
781
555
|
: undefined;
|
|
782
|
-
if (deep?.gateway?.attempted && deep.gateway.ok
|
|
556
|
+
if (deep?.gateway?.attempted && !deep.gateway.ok) {
|
|
783
557
|
findings.push({
|
|
784
558
|
checkId: "gateway.probe_failed",
|
|
785
559
|
severity: "warn",
|