mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 22:32:12 +00:00
secrets: harden read-only SecretRef command paths and diagnostics (#47794)
* secrets: harden read-only SecretRef resolution for status and audit * CLI: add SecretRef degrade-safe regression coverage * Docs: align SecretRef status and daemon probe semantics * Security audit: close SecretRef review gaps * Security audit: preserve source auth SecretRef configuredness * changelog Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com> --------- Signed-off-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
245
src/cli/security-cli.test.ts
Normal file
245
src/cli/security-cli.test.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { Command } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
|
||||
|
||||
const loadConfig = vi.fn();
|
||||
const runSecurityAudit = vi.fn();
|
||||
const fixSecurityFootguns = vi.fn();
|
||||
const resolveCommandSecretRefsViaGateway = vi.fn();
|
||||
const getSecurityAuditCommandSecretTargetIds = vi.fn(
|
||||
() => new Set(["gateway.auth.token", "gateway.auth.password"]),
|
||||
);
|
||||
|
||||
const { defaultRuntime, runtimeLogs, resetRuntimeCapture } = createCliRuntimeCapture();
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: () => loadConfig(),
|
||||
}));
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
defaultRuntime,
|
||||
}));
|
||||
|
||||
vi.mock("../security/audit.js", () => ({
|
||||
runSecurityAudit: (opts: unknown) => runSecurityAudit(opts),
|
||||
}));
|
||||
|
||||
vi.mock("../security/fix.js", () => ({
|
||||
fixSecurityFootguns: () => fixSecurityFootguns(),
|
||||
}));
|
||||
|
||||
vi.mock("./command-secret-gateway.js", () => ({
|
||||
resolveCommandSecretRefsViaGateway: (opts: unknown) => resolveCommandSecretRefsViaGateway(opts),
|
||||
}));
|
||||
|
||||
vi.mock("./command-secret-targets.js", () => ({
|
||||
getSecurityAuditCommandSecretTargetIds: () => getSecurityAuditCommandSecretTargetIds(),
|
||||
}));
|
||||
|
||||
const { registerSecurityCli } = await import("./security-cli.js");
|
||||
|
||||
function createProgram() {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerSecurityCli(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
describe("security CLI", () => {
|
||||
beforeEach(() => {
|
||||
resetRuntimeCapture();
|
||||
loadConfig.mockReset();
|
||||
runSecurityAudit.mockReset();
|
||||
fixSecurityFootguns.mockReset();
|
||||
resolveCommandSecretRefsViaGateway.mockReset();
|
||||
getSecurityAuditCommandSecretTargetIds.mockClear();
|
||||
fixSecurityFootguns.mockResolvedValue({
|
||||
changes: [],
|
||||
actions: [],
|
||||
errors: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("runs audit with read-only SecretRef resolution and prints JSON diagnostics", async () => {
|
||||
const sourceConfig = {
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_TOKEN" },
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const resolvedConfig = {
|
||||
...sourceConfig,
|
||||
gateway: {
|
||||
...sourceConfig.gateway,
|
||||
auth: {
|
||||
...sourceConfig.gateway.auth,
|
||||
token: "resolved-token",
|
||||
},
|
||||
},
|
||||
};
|
||||
loadConfig.mockReturnValue(sourceConfig);
|
||||
resolveCommandSecretRefsViaGateway.mockResolvedValue({
|
||||
resolvedConfig,
|
||||
diagnostics: [
|
||||
"security audit: gateway secrets.resolve unavailable (gateway closed); resolved command secrets locally.",
|
||||
],
|
||||
targetStatesByPath: {},
|
||||
hadUnresolvedTargets: false,
|
||||
});
|
||||
runSecurityAudit.mockResolvedValue({
|
||||
ts: 0,
|
||||
summary: { critical: 0, warn: 1, info: 0 },
|
||||
findings: [
|
||||
{
|
||||
checkId: "gateway.probe_failed",
|
||||
severity: "warn",
|
||||
title: "Gateway probe failed (deep)",
|
||||
detail: "connect failed: connect ECONNREFUSED 127.0.0.1:18789",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await createProgram().parseAsync(["security", "audit", "--json"], { from: "user" });
|
||||
|
||||
expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: sourceConfig,
|
||||
commandName: "security audit",
|
||||
mode: "read_only_status",
|
||||
targetIds: expect.any(Set),
|
||||
}),
|
||||
);
|
||||
expect(runSecurityAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: resolvedConfig,
|
||||
sourceConfig,
|
||||
deep: false,
|
||||
includeFilesystem: true,
|
||||
includeChannelSecurity: true,
|
||||
}),
|
||||
);
|
||||
const payload = JSON.parse(String(runtimeLogs.at(-1)));
|
||||
expect(payload.secretDiagnostics).toEqual([
|
||||
"security audit: gateway secrets.resolve unavailable (gateway closed); resolved command secrets locally.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("forwards --token to deep probe auth without altering command-level resolver mode", async () => {
|
||||
const sourceConfig = { gateway: { mode: "local" } };
|
||||
loadConfig.mockReturnValue(sourceConfig);
|
||||
resolveCommandSecretRefsViaGateway.mockResolvedValue({
|
||||
resolvedConfig: sourceConfig,
|
||||
diagnostics: [],
|
||||
targetStatesByPath: {},
|
||||
hadUnresolvedTargets: false,
|
||||
});
|
||||
runSecurityAudit.mockResolvedValue({
|
||||
ts: 0,
|
||||
summary: { critical: 0, warn: 0, info: 0 },
|
||||
findings: [],
|
||||
});
|
||||
|
||||
await createProgram().parseAsync(
|
||||
["security", "audit", "--deep", "--token", "explicit-token", "--json"],
|
||||
{
|
||||
from: "user",
|
||||
},
|
||||
);
|
||||
|
||||
expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mode: "read_only_status",
|
||||
}),
|
||||
);
|
||||
expect(runSecurityAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
deep: true,
|
||||
deepProbeAuth: { token: "explicit-token" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards --password to deep probe auth without altering command-level resolver mode", async () => {
|
||||
const sourceConfig = { gateway: { mode: "local" } };
|
||||
loadConfig.mockReturnValue(sourceConfig);
|
||||
resolveCommandSecretRefsViaGateway.mockResolvedValue({
|
||||
resolvedConfig: sourceConfig,
|
||||
diagnostics: [],
|
||||
targetStatesByPath: {},
|
||||
hadUnresolvedTargets: false,
|
||||
});
|
||||
runSecurityAudit.mockResolvedValue({
|
||||
ts: 0,
|
||||
summary: { critical: 0, warn: 0, info: 0 },
|
||||
findings: [],
|
||||
});
|
||||
|
||||
await createProgram().parseAsync(
|
||||
["security", "audit", "--deep", "--password", "explicit-password", "--json"],
|
||||
{
|
||||
from: "user",
|
||||
},
|
||||
);
|
||||
|
||||
expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mode: "read_only_status",
|
||||
}),
|
||||
);
|
||||
expect(runSecurityAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
deep: true,
|
||||
deepProbeAuth: { password: "explicit-password" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards both --token and --password to deep probe auth", async () => {
|
||||
const sourceConfig = { gateway: { mode: "local" } };
|
||||
loadConfig.mockReturnValue(sourceConfig);
|
||||
resolveCommandSecretRefsViaGateway.mockResolvedValue({
|
||||
resolvedConfig: sourceConfig,
|
||||
diagnostics: [],
|
||||
targetStatesByPath: {},
|
||||
hadUnresolvedTargets: false,
|
||||
});
|
||||
runSecurityAudit.mockResolvedValue({
|
||||
ts: 0,
|
||||
summary: { critical: 0, warn: 0, info: 0 },
|
||||
findings: [],
|
||||
});
|
||||
|
||||
await createProgram().parseAsync(
|
||||
[
|
||||
"security",
|
||||
"audit",
|
||||
"--deep",
|
||||
"--token",
|
||||
"explicit-token",
|
||||
"--password",
|
||||
"explicit-password",
|
||||
"--json",
|
||||
],
|
||||
{
|
||||
from: "user",
|
||||
},
|
||||
);
|
||||
|
||||
expect(runSecurityAudit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
deep: true,
|
||||
deepProbeAuth: {
|
||||
token: "explicit-token",
|
||||
password: "explicit-password",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user