mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 14:14:32 +00:00
CLI: make read-only SecretRef status flows degrade safely (#37023)
* CLI: add read-only SecretRef inspection * CLI: fix read-only SecretRef status regressions * CLI: preserve read-only SecretRef status fallbacks * Docs: document read-only channel inspection hook * CLI: preserve audit coverage for read-only SecretRefs * CLI: fix read-only status account selection * CLI: fix targeted gateway fallback analysis * CLI: fix Slack HTTP read-only inspection * CLI: align audit credential status checks * CLI: restore Telegram read-only fallback semantics
This commit is contained in:
@@ -30,7 +30,10 @@ function stubChannelPlugin(params: {
|
||||
id: "discord" | "slack" | "telegram";
|
||||
label: string;
|
||||
resolveAccount: (cfg: OpenClawConfig, accountId: string | null | undefined) => unknown;
|
||||
inspectAccount?: (cfg: OpenClawConfig, accountId: string | null | undefined) => unknown;
|
||||
listAccountIds?: (cfg: OpenClawConfig) => string[];
|
||||
isConfigured?: (account: unknown, cfg: OpenClawConfig) => boolean;
|
||||
isEnabled?: (account: unknown, cfg: OpenClawConfig) => boolean;
|
||||
}): ChannelPlugin {
|
||||
return {
|
||||
id: params.id,
|
||||
@@ -54,9 +57,10 @@ function stubChannelPlugin(params: {
|
||||
);
|
||||
return enabled ? ["default"] : [];
|
||||
}),
|
||||
inspectAccount: params.inspectAccount,
|
||||
resolveAccount: (cfg, accountId) => params.resolveAccount(cfg, accountId),
|
||||
isEnabled: () => true,
|
||||
isConfigured: () => true,
|
||||
isEnabled: (account, cfg) => params.isEnabled?.(account, cfg) ?? true,
|
||||
isConfigured: (account, cfg) => params.isConfigured?.(account, cfg) ?? true,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1837,6 +1841,247 @@ description: test skill
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps channel security findings when SecretRef credentials are configured but unavailable", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const sourceConfig: OpenClawConfig = {
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"123": {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const resolvedConfig: OpenClawConfig = {
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"123": {
|
||||
channels: {
|
||||
general: { allow: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const inspectableDiscordPlugin = stubChannelPlugin({
|
||||
id: "discord",
|
||||
label: "Discord",
|
||||
inspectAccount: (cfg) => {
|
||||
const channel = cfg.channels?.discord ?? {};
|
||||
const token = channel.token;
|
||||
return {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
configured:
|
||||
Boolean(token) &&
|
||||
typeof token === "object" &&
|
||||
!Array.isArray(token) &&
|
||||
"source" in token,
|
||||
token: "",
|
||||
tokenSource:
|
||||
Boolean(token) &&
|
||||
typeof token === "object" &&
|
||||
!Array.isArray(token) &&
|
||||
"source" in token
|
||||
? "config"
|
||||
: "none",
|
||||
tokenStatus:
|
||||
Boolean(token) &&
|
||||
typeof token === "object" &&
|
||||
!Array.isArray(token) &&
|
||||
"source" in token
|
||||
? "configured_unavailable"
|
||||
: "missing",
|
||||
config: channel,
|
||||
};
|
||||
},
|
||||
resolveAccount: (cfg) => ({ config: cfg.channels?.discord ?? {} }),
|
||||
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
|
||||
});
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: resolvedConfig,
|
||||
sourceConfig,
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: true,
|
||||
plugins: [inspectableDiscordPlugin],
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "channels.discord.commands.native.no_allowlists",
|
||||
severity: "warn",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps Slack HTTP slash-command findings when resolved inspection only exposes signingSecret status", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const sourceConfig: OpenClawConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
mode: "http",
|
||||
groupPolicy: "open",
|
||||
slashCommand: { enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
const resolvedConfig: OpenClawConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
mode: "http",
|
||||
groupPolicy: "open",
|
||||
slashCommand: { enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const inspectableSlackPlugin = stubChannelPlugin({
|
||||
id: "slack",
|
||||
label: "Slack",
|
||||
inspectAccount: (cfg) => {
|
||||
const channel = cfg.channels?.slack ?? {};
|
||||
if (cfg === sourceConfig) {
|
||||
return {
|
||||
accountId: "default",
|
||||
enabled: false,
|
||||
configured: true,
|
||||
mode: "http",
|
||||
botTokenSource: "config",
|
||||
botTokenStatus: "configured_unavailable",
|
||||
signingSecretSource: "config",
|
||||
signingSecretStatus: "configured_unavailable",
|
||||
config: channel,
|
||||
};
|
||||
}
|
||||
return {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
mode: "http",
|
||||
botTokenSource: "config",
|
||||
botTokenStatus: "available",
|
||||
signingSecretSource: "config",
|
||||
signingSecretStatus: "available",
|
||||
config: channel,
|
||||
};
|
||||
},
|
||||
resolveAccount: (cfg) => ({ config: cfg.channels?.slack ?? {} }),
|
||||
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
|
||||
});
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: resolvedConfig,
|
||||
sourceConfig,
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: true,
|
||||
plugins: [inspectableSlackPlugin],
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "channels.slack.commands.slash.no_allowlists",
|
||||
severity: "warn",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps source-configured Slack HTTP findings when resolved inspection is unconfigured", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const sourceConfig: OpenClawConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
mode: "http",
|
||||
groupPolicy: "open",
|
||||
slashCommand: { enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
const resolvedConfig: OpenClawConfig = {
|
||||
channels: {
|
||||
slack: {
|
||||
enabled: true,
|
||||
mode: "http",
|
||||
groupPolicy: "open",
|
||||
slashCommand: { enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const inspectableSlackPlugin = stubChannelPlugin({
|
||||
id: "slack",
|
||||
label: "Slack",
|
||||
inspectAccount: (cfg) => {
|
||||
const channel = cfg.channels?.slack ?? {};
|
||||
if (cfg === sourceConfig) {
|
||||
return {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
mode: "http",
|
||||
botTokenSource: "config",
|
||||
botTokenStatus: "configured_unavailable",
|
||||
signingSecretSource: "config",
|
||||
signingSecretStatus: "configured_unavailable",
|
||||
config: channel,
|
||||
};
|
||||
}
|
||||
return {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
configured: false,
|
||||
mode: "http",
|
||||
botTokenSource: "config",
|
||||
botTokenStatus: "available",
|
||||
signingSecretSource: "config",
|
||||
signingSecretStatus: "missing",
|
||||
config: channel,
|
||||
};
|
||||
},
|
||||
resolveAccount: (cfg) => ({ config: cfg.channels?.slack ?? {} }),
|
||||
isConfigured: (account) => Boolean((account as { configured?: boolean }).configured),
|
||||
});
|
||||
|
||||
const res = await runSecurityAudit({
|
||||
config: resolvedConfig,
|
||||
sourceConfig,
|
||||
includeFilesystem: false,
|
||||
includeChannelSecurity: true,
|
||||
plugins: [inspectableSlackPlugin],
|
||||
});
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
checkId: "channels.slack.commands.slash.no_allowlists",
|
||||
severity: "warn",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not flag Discord slash commands when dm.allowFrom includes a Discord snowflake id", async () => {
|
||||
await withChannelSecurityStateDir(async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
|
||||
Reference in New Issue
Block a user