@theaux/clawdbot 2026.1.14 → 2026.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/antigravity-oauth.js +24 -32
- package/dist/commands/auth-choice-options.js +10 -0
- package/dist/commands/auth-choice.apply.custom-openai.js +116 -0
- package/dist/commands/auth-choice.apply.js +2 -0
- package/dist/commands/auth-choice.apply.oauth.js +10 -2
- package/dist/security/audit-extra.js +525 -0
- package/dist/security/audit-fs.js +55 -0
- package/dist/security/audit.js +469 -0
- package/dist/security/fix.js +317 -0
- package/package.json +2 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { listChannelPlugins } from "../channels/plugins/index.js";
|
|
2
|
+
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
|
3
|
+
import { resolveBrowserConfig } from "../browser/config.js";
|
|
4
|
+
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
|
5
|
+
import { resolveGatewayAuth } from "../gateway/auth.js";
|
|
6
|
+
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
|
7
|
+
import { probeGateway } from "../gateway/probe.js";
|
|
8
|
+
import { collectAttackSurfaceSummaryFindings, collectExposureMatrixFindings, collectHooksHardeningFindings, collectIncludeFilePermFindings, collectModelHygieneFindings, collectPluginsTrustFindings, collectSecretsInConfigFindings, collectStateDeepFilesystemFindings, collectSyncedFolderFindings, readConfigSnapshotForAudit, } from "./audit-extra.js";
|
|
9
|
+
import { formatOctal, isGroupReadable, isGroupWritable, isWorldReadable, isWorldWritable, modeBits, safeStat, } from "./audit-fs.js";
|
|
10
|
+
function countBySeverity(findings) {
|
|
11
|
+
let critical = 0;
|
|
12
|
+
let warn = 0;
|
|
13
|
+
let info = 0;
|
|
14
|
+
for (const f of findings) {
|
|
15
|
+
if (f.severity === "critical")
|
|
16
|
+
critical += 1;
|
|
17
|
+
else if (f.severity === "warn")
|
|
18
|
+
warn += 1;
|
|
19
|
+
else
|
|
20
|
+
info += 1;
|
|
21
|
+
}
|
|
22
|
+
return { critical, warn, info };
|
|
23
|
+
}
|
|
24
|
+
function normalizeAllowFromList(list) {
|
|
25
|
+
if (!Array.isArray(list))
|
|
26
|
+
return [];
|
|
27
|
+
return list.map((v) => String(v).trim()).filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
function classifyChannelWarningSeverity(message) {
|
|
30
|
+
const s = message.toLowerCase();
|
|
31
|
+
if (s.includes("dms: open") ||
|
|
32
|
+
s.includes('grouppolicy="open"') ||
|
|
33
|
+
s.includes('dmpolicy="open"')) {
|
|
34
|
+
return "critical";
|
|
35
|
+
}
|
|
36
|
+
if (s.includes("allows any") || s.includes("anyone can dm") || s.includes("public")) {
|
|
37
|
+
return "critical";
|
|
38
|
+
}
|
|
39
|
+
if (s.includes("locked") || s.includes("disabled")) {
|
|
40
|
+
return "info";
|
|
41
|
+
}
|
|
42
|
+
return "warn";
|
|
43
|
+
}
|
|
44
|
+
async function collectFilesystemFindings(params) {
|
|
45
|
+
const findings = [];
|
|
46
|
+
const stateDirStat = await safeStat(params.stateDir);
|
|
47
|
+
if (stateDirStat.ok) {
|
|
48
|
+
const bits = modeBits(stateDirStat.mode);
|
|
49
|
+
if (stateDirStat.isSymlink) {
|
|
50
|
+
findings.push({
|
|
51
|
+
checkId: "fs.state_dir.symlink",
|
|
52
|
+
severity: "warn",
|
|
53
|
+
title: "State dir is a symlink",
|
|
54
|
+
detail: `${params.stateDir} is a symlink; treat this as an extra trust boundary.`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (isWorldWritable(bits)) {
|
|
58
|
+
findings.push({
|
|
59
|
+
checkId: "fs.state_dir.perms_world_writable",
|
|
60
|
+
severity: "critical",
|
|
61
|
+
title: "State dir is world-writable",
|
|
62
|
+
detail: `${params.stateDir} mode=${formatOctal(bits)}; other users can write into your Clawdbot state.`,
|
|
63
|
+
remediation: `chmod 700 ${params.stateDir}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else if (isGroupWritable(bits)) {
|
|
67
|
+
findings.push({
|
|
68
|
+
checkId: "fs.state_dir.perms_group_writable",
|
|
69
|
+
severity: "warn",
|
|
70
|
+
title: "State dir is group-writable",
|
|
71
|
+
detail: `${params.stateDir} mode=${formatOctal(bits)}; group users can write into your Clawdbot state.`,
|
|
72
|
+
remediation: `chmod 700 ${params.stateDir}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
else if (isGroupReadable(bits) || isWorldReadable(bits)) {
|
|
76
|
+
findings.push({
|
|
77
|
+
checkId: "fs.state_dir.perms_readable",
|
|
78
|
+
severity: "warn",
|
|
79
|
+
title: "State dir is readable by others",
|
|
80
|
+
detail: `${params.stateDir} mode=${formatOctal(bits)}; consider restricting to 700.`,
|
|
81
|
+
remediation: `chmod 700 ${params.stateDir}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const configStat = await safeStat(params.configPath);
|
|
86
|
+
if (configStat.ok) {
|
|
87
|
+
const bits = modeBits(configStat.mode);
|
|
88
|
+
if (configStat.isSymlink) {
|
|
89
|
+
findings.push({
|
|
90
|
+
checkId: "fs.config.symlink",
|
|
91
|
+
severity: "warn",
|
|
92
|
+
title: "Config file is a symlink",
|
|
93
|
+
detail: `${params.configPath} is a symlink; make sure you trust its target.`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (isWorldWritable(bits) || isGroupWritable(bits)) {
|
|
97
|
+
findings.push({
|
|
98
|
+
checkId: "fs.config.perms_writable",
|
|
99
|
+
severity: "critical",
|
|
100
|
+
title: "Config file is writable by others",
|
|
101
|
+
detail: `${params.configPath} mode=${formatOctal(bits)}; another user could change gateway/auth/tool policies.`,
|
|
102
|
+
remediation: `chmod 600 ${params.configPath}`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else if (isWorldReadable(bits)) {
|
|
106
|
+
findings.push({
|
|
107
|
+
checkId: "fs.config.perms_world_readable",
|
|
108
|
+
severity: "critical",
|
|
109
|
+
title: "Config file is world-readable",
|
|
110
|
+
detail: `${params.configPath} mode=${formatOctal(bits)}; config can contain tokens and private settings.`,
|
|
111
|
+
remediation: `chmod 600 ${params.configPath}`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else if (isGroupReadable(bits)) {
|
|
115
|
+
findings.push({
|
|
116
|
+
checkId: "fs.config.perms_group_readable",
|
|
117
|
+
severity: "warn",
|
|
118
|
+
title: "Config file is group-readable",
|
|
119
|
+
detail: `${params.configPath} mode=${formatOctal(bits)}; config can contain tokens and private settings.`,
|
|
120
|
+
remediation: `chmod 600 ${params.configPath}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return findings;
|
|
125
|
+
}
|
|
126
|
+
function collectGatewayConfigFindings(cfg) {
|
|
127
|
+
const findings = [];
|
|
128
|
+
const bind = typeof cfg.gateway?.bind === "string" ? cfg.gateway.bind : "loopback";
|
|
129
|
+
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
|
130
|
+
const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode });
|
|
131
|
+
if (bind !== "loopback" && auth.mode === "none") {
|
|
132
|
+
findings.push({
|
|
133
|
+
checkId: "gateway.bind_no_auth",
|
|
134
|
+
severity: "critical",
|
|
135
|
+
title: "Gateway binds beyond loopback without auth",
|
|
136
|
+
detail: `gateway.bind="${bind}" but no gateway.auth token/password is configured.`,
|
|
137
|
+
remediation: `Set gateway.auth (token recommended) or bind to loopback.`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (tailscaleMode === "funnel") {
|
|
141
|
+
findings.push({
|
|
142
|
+
checkId: "gateway.tailscale_funnel",
|
|
143
|
+
severity: "critical",
|
|
144
|
+
title: "Tailscale Funnel exposure enabled",
|
|
145
|
+
detail: `gateway.tailscale.mode="funnel" exposes the Gateway publicly; keep auth strict and treat it as internet-facing.`,
|
|
146
|
+
remediation: `Prefer tailscale.mode="serve" (tailnet-only) or set tailscale.mode="off".`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (tailscaleMode === "serve") {
|
|
150
|
+
findings.push({
|
|
151
|
+
checkId: "gateway.tailscale_serve",
|
|
152
|
+
severity: "info",
|
|
153
|
+
title: "Tailscale Serve exposure enabled",
|
|
154
|
+
detail: `gateway.tailscale.mode="serve" exposes the Gateway to your tailnet (loopback behind Tailscale).`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const token = typeof auth.token === "string" && auth.token.trim().length > 0 ? auth.token.trim() : null;
|
|
158
|
+
if (auth.mode === "token" && token && token.length < 24) {
|
|
159
|
+
findings.push({
|
|
160
|
+
checkId: "gateway.token_too_short",
|
|
161
|
+
severity: "warn",
|
|
162
|
+
title: "Gateway token looks short",
|
|
163
|
+
detail: `gateway auth token is ${token.length} chars; prefer a long random token.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return findings;
|
|
167
|
+
}
|
|
168
|
+
function isLoopbackClientHost(hostname) {
|
|
169
|
+
const h = hostname.trim().toLowerCase();
|
|
170
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1";
|
|
171
|
+
}
|
|
172
|
+
function collectBrowserControlFindings(cfg) {
|
|
173
|
+
const findings = [];
|
|
174
|
+
let resolved;
|
|
175
|
+
try {
|
|
176
|
+
resolved = resolveBrowserConfig(cfg.browser);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
findings.push({
|
|
180
|
+
checkId: "browser.control_invalid_config",
|
|
181
|
+
severity: "warn",
|
|
182
|
+
title: "Browser control config looks invalid",
|
|
183
|
+
detail: String(err),
|
|
184
|
+
remediation: `Fix browser.controlUrl/browser.cdpUrl in ${resolveConfigPath()} and re-run "clawdbot security audit --deep".`,
|
|
185
|
+
});
|
|
186
|
+
return findings;
|
|
187
|
+
}
|
|
188
|
+
if (!resolved.enabled)
|
|
189
|
+
return findings;
|
|
190
|
+
const url = new URL(resolved.controlUrl);
|
|
191
|
+
const isLoopback = isLoopbackClientHost(url.hostname);
|
|
192
|
+
const envToken = process.env.CLAWDBOT_BROWSER_CONTROL_TOKEN?.trim();
|
|
193
|
+
const controlToken = (envToken || resolved.controlToken)?.trim() || null;
|
|
194
|
+
if (!isLoopback) {
|
|
195
|
+
if (!controlToken) {
|
|
196
|
+
findings.push({
|
|
197
|
+
checkId: "browser.control_remote_no_token",
|
|
198
|
+
severity: "critical",
|
|
199
|
+
title: "Remote browser control is missing an auth token",
|
|
200
|
+
detail: `browser.controlUrl is non-loopback (${resolved.controlUrl}) but no browser.controlToken (or CLAWDBOT_BROWSER_CONTROL_TOKEN) is configured.`,
|
|
201
|
+
remediation: "Set browser.controlToken (or export CLAWDBOT_BROWSER_CONTROL_TOKEN) and prefer serving over Tailscale Serve or HTTPS reverse proxy.",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (url.protocol === "http:") {
|
|
205
|
+
findings.push({
|
|
206
|
+
checkId: "browser.control_remote_http",
|
|
207
|
+
severity: "warn",
|
|
208
|
+
title: "Remote browser control uses HTTP",
|
|
209
|
+
detail: `browser.controlUrl=${resolved.controlUrl} is http; this is OK only if it's tailnet-only (Tailscale) or behind another encrypted tunnel.`,
|
|
210
|
+
remediation: `Prefer HTTPS termination (Tailscale Serve) and keep the endpoint tailnet-only.`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (controlToken && controlToken.length < 24) {
|
|
214
|
+
findings.push({
|
|
215
|
+
checkId: "browser.control_token_too_short",
|
|
216
|
+
severity: "warn",
|
|
217
|
+
title: "Browser control token looks short",
|
|
218
|
+
detail: `browser control token is ${controlToken.length} chars; prefer a long random token.`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
|
222
|
+
const gatewayAuth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode });
|
|
223
|
+
const gatewayToken = gatewayAuth.mode === "token" &&
|
|
224
|
+
typeof gatewayAuth.token === "string" &&
|
|
225
|
+
gatewayAuth.token.trim()
|
|
226
|
+
? gatewayAuth.token.trim()
|
|
227
|
+
: null;
|
|
228
|
+
if (controlToken && gatewayToken && controlToken === gatewayToken) {
|
|
229
|
+
findings.push({
|
|
230
|
+
checkId: "browser.control_token_reuse_gateway_token",
|
|
231
|
+
severity: "warn",
|
|
232
|
+
title: "Browser control token reuses the Gateway token",
|
|
233
|
+
detail: `browser.controlToken matches gateway.auth token; compromise of browser control expands blast radius to the Gateway API.`,
|
|
234
|
+
remediation: `Use a separate browser.controlToken dedicated to browser control.`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return findings;
|
|
239
|
+
}
|
|
240
|
+
function collectLoggingFindings(cfg) {
|
|
241
|
+
const redact = cfg.logging?.redactSensitive;
|
|
242
|
+
if (redact !== "off")
|
|
243
|
+
return [];
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
checkId: "logging.redact_off",
|
|
247
|
+
severity: "warn",
|
|
248
|
+
title: "Tool summary redaction is disabled",
|
|
249
|
+
detail: `logging.redactSensitive="off" can leak secrets into logs and status output.`,
|
|
250
|
+
remediation: `Set logging.redactSensitive="tools".`,
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
function collectElevatedFindings(cfg) {
|
|
255
|
+
const findings = [];
|
|
256
|
+
const enabled = cfg.tools?.elevated?.enabled;
|
|
257
|
+
const allowFrom = cfg.tools?.elevated?.allowFrom ?? {};
|
|
258
|
+
const anyAllowFromKeys = Object.keys(allowFrom).length > 0;
|
|
259
|
+
if (enabled === false)
|
|
260
|
+
return findings;
|
|
261
|
+
if (!anyAllowFromKeys)
|
|
262
|
+
return findings;
|
|
263
|
+
for (const [provider, list] of Object.entries(allowFrom)) {
|
|
264
|
+
const normalized = normalizeAllowFromList(list);
|
|
265
|
+
if (normalized.includes("*")) {
|
|
266
|
+
findings.push({
|
|
267
|
+
checkId: `tools.elevated.allowFrom.${provider}.wildcard`,
|
|
268
|
+
severity: "critical",
|
|
269
|
+
title: "Elevated exec allowlist contains wildcard",
|
|
270
|
+
detail: `tools.elevated.allowFrom.${provider} includes "*" which effectively approves everyone on that channel for elevated mode.`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
else if (normalized.length > 25) {
|
|
274
|
+
findings.push({
|
|
275
|
+
checkId: `tools.elevated.allowFrom.${provider}.large`,
|
|
276
|
+
severity: "warn",
|
|
277
|
+
title: "Elevated exec allowlist is large",
|
|
278
|
+
detail: `tools.elevated.allowFrom.${provider} has ${normalized.length} entries; consider tightening elevated access.`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return findings;
|
|
283
|
+
}
|
|
284
|
+
async function collectChannelSecurityFindings(params) {
|
|
285
|
+
const findings = [];
|
|
286
|
+
const warnDmPolicy = async (input) => {
|
|
287
|
+
const policyPath = input.policyPath ?? `${input.allowFromPath}policy`;
|
|
288
|
+
const configAllowFrom = normalizeAllowFromList(input.allowFrom);
|
|
289
|
+
const hasWildcard = configAllowFrom.includes("*");
|
|
290
|
+
if (input.dmPolicy === "open") {
|
|
291
|
+
const allowFromKey = `${input.allowFromPath}allowFrom`;
|
|
292
|
+
findings.push({
|
|
293
|
+
checkId: `channels.${input.provider}.dm.open`,
|
|
294
|
+
severity: "critical",
|
|
295
|
+
title: `${input.label} DMs are open`,
|
|
296
|
+
detail: `${policyPath}="open" allows anyone to DM the bot.`,
|
|
297
|
+
remediation: `Use pairing/allowlist; if you really need open DMs, ensure ${allowFromKey} includes "*".`,
|
|
298
|
+
});
|
|
299
|
+
if (!hasWildcard) {
|
|
300
|
+
findings.push({
|
|
301
|
+
checkId: `channels.${input.provider}.dm.open_invalid`,
|
|
302
|
+
severity: "warn",
|
|
303
|
+
title: `${input.label} DM config looks inconsistent`,
|
|
304
|
+
detail: `"open" requires ${allowFromKey} to include "*".`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (input.dmPolicy === "disabled") {
|
|
310
|
+
findings.push({
|
|
311
|
+
checkId: `channels.${input.provider}.dm.disabled`,
|
|
312
|
+
severity: "info",
|
|
313
|
+
title: `${input.label} DMs are disabled`,
|
|
314
|
+
detail: `${policyPath}="disabled" ignores inbound DMs.`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
for (const plugin of params.plugins) {
|
|
319
|
+
if (!plugin.security)
|
|
320
|
+
continue;
|
|
321
|
+
const accountIds = plugin.config.listAccountIds(params.cfg);
|
|
322
|
+
const defaultAccountId = resolveChannelDefaultAccountId({
|
|
323
|
+
plugin,
|
|
324
|
+
cfg: params.cfg,
|
|
325
|
+
accountIds,
|
|
326
|
+
});
|
|
327
|
+
const account = plugin.config.resolveAccount(params.cfg, defaultAccountId);
|
|
328
|
+
const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, params.cfg) : true;
|
|
329
|
+
if (!enabled)
|
|
330
|
+
continue;
|
|
331
|
+
const configured = plugin.config.isConfigured
|
|
332
|
+
? await plugin.config.isConfigured(account, params.cfg)
|
|
333
|
+
: true;
|
|
334
|
+
if (!configured)
|
|
335
|
+
continue;
|
|
336
|
+
const dmPolicy = plugin.security.resolveDmPolicy?.({
|
|
337
|
+
cfg: params.cfg,
|
|
338
|
+
accountId: defaultAccountId,
|
|
339
|
+
account,
|
|
340
|
+
});
|
|
341
|
+
if (dmPolicy) {
|
|
342
|
+
await warnDmPolicy({
|
|
343
|
+
label: plugin.meta.label ?? plugin.id,
|
|
344
|
+
provider: plugin.id,
|
|
345
|
+
dmPolicy: dmPolicy.policy,
|
|
346
|
+
allowFrom: dmPolicy.allowFrom,
|
|
347
|
+
policyPath: dmPolicy.policyPath,
|
|
348
|
+
allowFromPath: dmPolicy.allowFromPath,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (plugin.security.collectWarnings) {
|
|
352
|
+
const warnings = await plugin.security.collectWarnings({
|
|
353
|
+
cfg: params.cfg,
|
|
354
|
+
accountId: defaultAccountId,
|
|
355
|
+
account,
|
|
356
|
+
});
|
|
357
|
+
for (const message of warnings ?? []) {
|
|
358
|
+
const trimmed = String(message).trim();
|
|
359
|
+
if (!trimmed)
|
|
360
|
+
continue;
|
|
361
|
+
findings.push({
|
|
362
|
+
checkId: `channels.${plugin.id}.warning.${findings.length + 1}`,
|
|
363
|
+
severity: classifyChannelWarningSeverity(trimmed),
|
|
364
|
+
title: `${plugin.meta.label ?? plugin.id} security warning`,
|
|
365
|
+
detail: trimmed.replace(/^-\s*/, ""),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return findings;
|
|
371
|
+
}
|
|
372
|
+
async function maybeProbeGateway(params) {
|
|
373
|
+
const connection = buildGatewayConnectionDetails({ config: params.cfg });
|
|
374
|
+
const url = connection.url;
|
|
375
|
+
const isRemoteMode = params.cfg.gateway?.mode === "remote";
|
|
376
|
+
const remoteUrlRaw = typeof params.cfg.gateway?.remote?.url === "string" ? params.cfg.gateway.remote.url.trim() : "";
|
|
377
|
+
const remoteUrlMissing = isRemoteMode && !remoteUrlRaw;
|
|
378
|
+
const resolveAuth = (mode) => {
|
|
379
|
+
const authToken = params.cfg.gateway?.auth?.token;
|
|
380
|
+
const authPassword = params.cfg.gateway?.auth?.password;
|
|
381
|
+
const remote = params.cfg.gateway?.remote;
|
|
382
|
+
const token = mode === "remote"
|
|
383
|
+
? typeof remote?.token === "string" && remote.token.trim()
|
|
384
|
+
? remote.token.trim()
|
|
385
|
+
: undefined
|
|
386
|
+
: process.env.CLAWDBOT_GATEWAY_TOKEN?.trim() ||
|
|
387
|
+
(typeof authToken === "string" && authToken.trim() ? authToken.trim() : undefined);
|
|
388
|
+
const password = process.env.CLAWDBOT_GATEWAY_PASSWORD?.trim() ||
|
|
389
|
+
(mode === "remote"
|
|
390
|
+
? typeof remote?.password === "string" && remote.password.trim()
|
|
391
|
+
? remote.password.trim()
|
|
392
|
+
: undefined
|
|
393
|
+
: typeof authPassword === "string" && authPassword.trim()
|
|
394
|
+
? authPassword.trim()
|
|
395
|
+
: undefined);
|
|
396
|
+
return { token, password };
|
|
397
|
+
};
|
|
398
|
+
const auth = remoteUrlMissing ? resolveAuth("local") : resolveAuth("remote");
|
|
399
|
+
const res = await params.probe({ url, auth, timeoutMs: params.timeoutMs }).catch((err) => ({
|
|
400
|
+
ok: false,
|
|
401
|
+
url,
|
|
402
|
+
connectLatencyMs: null,
|
|
403
|
+
error: String(err),
|
|
404
|
+
close: null,
|
|
405
|
+
health: null,
|
|
406
|
+
status: null,
|
|
407
|
+
presence: null,
|
|
408
|
+
configSnapshot: null,
|
|
409
|
+
}));
|
|
410
|
+
return {
|
|
411
|
+
gateway: {
|
|
412
|
+
attempted: true,
|
|
413
|
+
url,
|
|
414
|
+
ok: res.ok,
|
|
415
|
+
error: res.ok ? null : res.error,
|
|
416
|
+
close: res.close ? { code: res.close.code, reason: res.close.reason } : null,
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
export async function runSecurityAudit(opts) {
|
|
421
|
+
const findings = [];
|
|
422
|
+
const cfg = opts.config;
|
|
423
|
+
const env = process.env;
|
|
424
|
+
const stateDir = opts.stateDir ?? resolveStateDir(env);
|
|
425
|
+
const configPath = opts.configPath ?? resolveConfigPath(env, stateDir);
|
|
426
|
+
findings.push(...collectAttackSurfaceSummaryFindings(cfg));
|
|
427
|
+
findings.push(...collectSyncedFolderFindings({ stateDir, configPath }));
|
|
428
|
+
findings.push(...collectGatewayConfigFindings(cfg));
|
|
429
|
+
findings.push(...collectBrowserControlFindings(cfg));
|
|
430
|
+
findings.push(...collectLoggingFindings(cfg));
|
|
431
|
+
findings.push(...collectElevatedFindings(cfg));
|
|
432
|
+
findings.push(...collectHooksHardeningFindings(cfg));
|
|
433
|
+
findings.push(...collectSecretsInConfigFindings(cfg));
|
|
434
|
+
findings.push(...collectModelHygieneFindings(cfg));
|
|
435
|
+
findings.push(...collectExposureMatrixFindings(cfg));
|
|
436
|
+
const configSnapshot = opts.includeFilesystem !== false
|
|
437
|
+
? await readConfigSnapshotForAudit({ env, configPath }).catch(() => null)
|
|
438
|
+
: null;
|
|
439
|
+
if (opts.includeFilesystem !== false) {
|
|
440
|
+
findings.push(...(await collectFilesystemFindings({ stateDir, configPath })));
|
|
441
|
+
if (configSnapshot) {
|
|
442
|
+
findings.push(...(await collectIncludeFilePermFindings({ configSnapshot })));
|
|
443
|
+
}
|
|
444
|
+
findings.push(...(await collectStateDeepFilesystemFindings({ cfg, env, stateDir })));
|
|
445
|
+
findings.push(...(await collectPluginsTrustFindings({ cfg, stateDir })));
|
|
446
|
+
}
|
|
447
|
+
if (opts.includeChannelSecurity !== false) {
|
|
448
|
+
const plugins = opts.plugins ?? listChannelPlugins();
|
|
449
|
+
findings.push(...(await collectChannelSecurityFindings({ cfg, plugins })));
|
|
450
|
+
}
|
|
451
|
+
const deep = opts.deep === true
|
|
452
|
+
? await maybeProbeGateway({
|
|
453
|
+
cfg,
|
|
454
|
+
timeoutMs: Math.max(250, opts.deepTimeoutMs ?? 5000),
|
|
455
|
+
probe: opts.probeGatewayFn ?? probeGateway,
|
|
456
|
+
})
|
|
457
|
+
: undefined;
|
|
458
|
+
if (deep?.gateway?.attempted && deep.gateway.ok === false) {
|
|
459
|
+
findings.push({
|
|
460
|
+
checkId: "gateway.probe_failed",
|
|
461
|
+
severity: "warn",
|
|
462
|
+
title: "Gateway probe failed (deep)",
|
|
463
|
+
detail: deep.gateway.error ?? "gateway unreachable",
|
|
464
|
+
remediation: `Run "clawdbot status --all" to debug connectivity/auth, then re-run "clawdbot security audit --deep".`,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const summary = countBySeverity(findings);
|
|
468
|
+
return { ts: Date.now(), summary, findings, deep };
|
|
469
|
+
}
|