Files
openclaw/src/cli/security-cli.test.ts
Josh Avant a2cb81199e 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>
2026-03-15 21:55:24 -05:00

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",
},
}),
);
});
});