mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 05:20:48 +00:00
* 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>
246 lines
6.8 KiB
TypeScript
246 lines
6.8 KiB
TypeScript
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",
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
});
|