mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 18:14:58 +00:00
feat(status): add security audit section
This commit is contained in:
@@ -4,6 +4,7 @@ import { buildGatewayConnectionDetails, callGateway } from "../gateway/call.js";
|
|||||||
import { info } from "../globals.js";
|
import { info } from "../globals.js";
|
||||||
import { formatUsageReportLines, loadProviderUsageSummary } from "../infra/provider-usage.js";
|
import { formatUsageReportLines, loadProviderUsageSummary } from "../infra/provider-usage.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import { runSecurityAudit } from "../security/audit.js";
|
||||||
import { renderTable } from "../terminal/table.js";
|
import { renderTable } from "../terminal/table.js";
|
||||||
import { theme } from "../terminal/theme.js";
|
import { theme } from "../terminal/theme.js";
|
||||||
import { formatHealthChannelLines, type HealthSummary } from "./health.js";
|
import { formatHealthChannelLines, type HealthSummary } from "./health.js";
|
||||||
@@ -61,6 +62,21 @@ export async function statusCommand(
|
|||||||
summary,
|
summary,
|
||||||
} = scan;
|
} = scan;
|
||||||
|
|
||||||
|
const securityAudit = await withProgress(
|
||||||
|
{
|
||||||
|
label: "Running security audit…",
|
||||||
|
indeterminate: true,
|
||||||
|
enabled: opts.json !== true,
|
||||||
|
},
|
||||||
|
async () =>
|
||||||
|
await runSecurityAudit({
|
||||||
|
config: cfg,
|
||||||
|
deep: false,
|
||||||
|
includeFilesystem: true,
|
||||||
|
includeChannelSecurity: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const usage = opts.usage
|
const usage = opts.usage
|
||||||
? await withProgress(
|
? await withProgress(
|
||||||
{
|
{
|
||||||
@@ -104,6 +120,7 @@ export async function statusCommand(
|
|||||||
error: gatewayProbe?.error ?? null,
|
error: gatewayProbe?.error ?? null,
|
||||||
},
|
},
|
||||||
agents: agentStatus,
|
agents: agentStatus,
|
||||||
|
securityAudit,
|
||||||
...(health || usage ? { health, usage } : {}),
|
...(health || usage ? { health, usage } : {}),
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
@@ -236,6 +253,44 @@ export async function statusCommand(
|
|||||||
}).trimEnd(),
|
}).trimEnd(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
runtime.log("");
|
||||||
|
runtime.log(theme.heading("Security audit"));
|
||||||
|
const fmtSummary = (value: { critical: number; warn: number; info: number }) => {
|
||||||
|
const parts = [
|
||||||
|
theme.error(`${value.critical} critical`),
|
||||||
|
theme.warn(`${value.warn} warn`),
|
||||||
|
theme.muted(`${value.info} info`),
|
||||||
|
];
|
||||||
|
return parts.join(" · ");
|
||||||
|
};
|
||||||
|
runtime.log(theme.muted(`Summary: ${fmtSummary(securityAudit.summary)}`));
|
||||||
|
const importantFindings = securityAudit.findings.filter(
|
||||||
|
(f) => f.severity === "critical" || f.severity === "warn",
|
||||||
|
);
|
||||||
|
if (importantFindings.length === 0) {
|
||||||
|
runtime.log(theme.muted("No critical or warn findings detected."));
|
||||||
|
} else {
|
||||||
|
const severityLabel = (sev: "critical" | "warn" | "info") => {
|
||||||
|
if (sev === "critical") return theme.error("CRITICAL");
|
||||||
|
if (sev === "warn") return theme.warn("WARN");
|
||||||
|
return theme.muted("INFO");
|
||||||
|
};
|
||||||
|
const sevRank = (sev: "critical" | "warn" | "info") =>
|
||||||
|
sev === "critical" ? 0 : sev === "warn" ? 1 : 2;
|
||||||
|
const sorted = [...importantFindings].sort((a, b) => sevRank(a.severity) - sevRank(b.severity));
|
||||||
|
const shown = sorted.slice(0, 6);
|
||||||
|
for (const f of shown) {
|
||||||
|
runtime.log(` ${severityLabel(f.severity)} ${f.title}`);
|
||||||
|
runtime.log(` ${shortenText(f.detail.replaceAll("\n", " "), 160)}`);
|
||||||
|
if (f.remediation?.trim()) runtime.log(` ${theme.muted(`Fix: ${f.remediation.trim()}`)}`);
|
||||||
|
}
|
||||||
|
if (sorted.length > shown.length) {
|
||||||
|
runtime.log(theme.muted(`… +${sorted.length - shown.length} more`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runtime.log(theme.muted("Full report: clawdbot security audit"));
|
||||||
|
runtime.log(theme.muted("Deep probe: clawdbot security audit --deep"));
|
||||||
|
|
||||||
runtime.log("");
|
runtime.log("");
|
||||||
runtime.log(theme.heading("Channels"));
|
runtime.log(theme.heading("Channels"));
|
||||||
const channelIssuesByChannel = (() => {
|
const channelIssuesByChannel = (() => {
|
||||||
|
|||||||
@@ -32,6 +32,37 @@ const mocks = vi.hoisted(() => ({
|
|||||||
configSnapshot: null,
|
configSnapshot: null,
|
||||||
}),
|
}),
|
||||||
callGateway: vi.fn().mockResolvedValue({}),
|
callGateway: vi.fn().mockResolvedValue({}),
|
||||||
|
runSecurityAudit: vi.fn().mockResolvedValue({
|
||||||
|
ts: 0,
|
||||||
|
summary: { critical: 1, warn: 1, info: 2 },
|
||||||
|
findings: [
|
||||||
|
{
|
||||||
|
checkId: "test.critical",
|
||||||
|
severity: "critical",
|
||||||
|
title: "Test critical finding",
|
||||||
|
detail: "Something is very wrong\nbut on two lines",
|
||||||
|
remediation: "Do the thing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checkId: "test.warn",
|
||||||
|
severity: "warn",
|
||||||
|
title: "Test warning finding",
|
||||||
|
detail: "Something is maybe wrong",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checkId: "test.info",
|
||||||
|
severity: "info",
|
||||||
|
title: "Test info finding",
|
||||||
|
detail: "FYI only",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checkId: "test.info2",
|
||||||
|
severity: "info",
|
||||||
|
title: "Another info finding",
|
||||||
|
detail: "More FYI",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../config/sessions.js", () => ({
|
vi.mock("../config/sessions.js", () => ({
|
||||||
@@ -185,6 +216,9 @@ vi.mock("../daemon/service.js", () => ({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
vi.mock("../security/audit.js", () => ({
|
||||||
|
runSecurityAudit: mocks.runSecurityAudit,
|
||||||
|
}));
|
||||||
|
|
||||||
import { statusCommand } from "./status.js";
|
import { statusCommand } from "./status.js";
|
||||||
|
|
||||||
@@ -206,6 +240,8 @@ describe("statusCommand", () => {
|
|||||||
expect(payload.sessions.recent[0].percentUsed).toBe(50);
|
expect(payload.sessions.recent[0].percentUsed).toBe(50);
|
||||||
expect(payload.sessions.recent[0].remainingTokens).toBe(5000);
|
expect(payload.sessions.recent[0].remainingTokens).toBe(5000);
|
||||||
expect(payload.sessions.recent[0].flags).toContain("verbose:on");
|
expect(payload.sessions.recent[0].flags).toContain("verbose:on");
|
||||||
|
expect(payload.securityAudit.summary.critical).toBe(1);
|
||||||
|
expect(payload.securityAudit.summary.warn).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prints formatted lines otherwise", async () => {
|
it("prints formatted lines otherwise", async () => {
|
||||||
@@ -214,6 +250,9 @@ describe("statusCommand", () => {
|
|||||||
const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0]));
|
const logs = (runtime.log as vi.Mock).mock.calls.map((c) => String(c[0]));
|
||||||
expect(logs.some((l) => l.includes("Clawdbot status"))).toBe(true);
|
expect(logs.some((l) => l.includes("Clawdbot status"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Overview"))).toBe(true);
|
expect(logs.some((l) => l.includes("Overview"))).toBe(true);
|
||||||
|
expect(logs.some((l) => l.includes("Security audit"))).toBe(true);
|
||||||
|
expect(logs.some((l) => l.includes("Summary:"))).toBe(true);
|
||||||
|
expect(logs.some((l) => l.includes("CRITICAL"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Dashboard"))).toBe(true);
|
expect(logs.some((l) => l.includes("Dashboard"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("macos 14.0 (arm64)"))).toBe(true);
|
expect(logs.some((l) => l.includes("macos 14.0 (arm64)"))).toBe(true);
|
||||||
expect(logs.some((l) => l.includes("Channels"))).toBe(true);
|
expect(logs.some((l) => l.includes("Channels"))).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user