feat: add security audit + onboarding checkpoint

This commit is contained in:
Peter Steinberger
2026-01-15 01:25:11 +00:00
parent c91c85532a
commit c2a4f256c8
13 changed files with 902 additions and 12 deletions

View File

@@ -35,6 +35,11 @@ export function registerOnboardCommand(program: Command) {
.option("--workspace <dir>", "Agent workspace directory (default: ~/clawd)")
.option("--reset", "Reset config + credentials + sessions + workspace before running wizard")
.option("--non-interactive", "Run without prompts", false)
.option(
"--accept-risk",
"Acknowledge that agents are powerful and full system access is risky (required for --non-interactive)",
false,
)
.option("--flow <flow>", "Wizard flow: quickstart|advanced")
.option("--mode <mode>", "Wizard mode: local|remote")
.option(
@@ -90,6 +95,7 @@ export function registerOnboardCommand(program: Command) {
{
workspace: opts.workspace as string | undefined,
nonInteractive: Boolean(opts.nonInteractive),
acceptRisk: Boolean(opts.acceptRisk),
flow: opts.flow as "quickstart" | "advanced" | undefined,
mode: opts.mode as "local" | "remote" | undefined,
authChoice: opts.authChoice as AuthChoice | undefined,

View File

@@ -15,6 +15,7 @@ import { registerNodesCli } from "../nodes-cli.js";
import { registerPairingCli } from "../pairing-cli.js";
import { registerPluginsCli } from "../plugins-cli.js";
import { registerSandboxCli } from "../sandbox-cli.js";
import { registerSecurityCli } from "../security-cli.js";
import { registerSkillsCli } from "../skills-cli.js";
import { registerTuiCli } from "../tui-cli.js";
import { registerUpdateCli } from "../update-cli.js";
@@ -35,6 +36,7 @@ export function registerSubCliCommands(program: Command) {
registerPairingCli(program);
registerPluginsCli(program);
registerChannelsCli(program);
registerSecurityCli(program);
registerSkillsCli(program);
registerUpdateCli(program);
registerPluginCliCommands(program, loadConfig());

91
src/cli/security-cli.ts Normal file
View File

@@ -0,0 +1,91 @@
import chalk from "chalk";
import type { Command } from "commander";
import { loadConfig } from "../config/config.js";
import { defaultRuntime } from "../runtime.js";
import { runSecurityAudit } from "../security/audit.js";
import { isRich, theme } from "../terminal/theme.js";
type SecurityAuditOptions = {
json?: boolean;
deep?: boolean;
};
function formatSummary(summary: { critical: number; warn: number; info: number }): string {
const rich = isRich();
const c = summary.critical;
const w = summary.warn;
const i = summary.info;
const parts: string[] = [];
parts.push(rich ? theme.error(`${c} critical`) : `${c} critical`);
parts.push(rich ? theme.warn(`${w} warn`) : `${w} warn`);
parts.push(rich ? theme.muted(`${i} info`) : `${i} info`);
return parts.join(" · ");
}
export function registerSecurityCli(program: Command) {
const security = program.command("security").description("Security tools (audit)");
security
.command("audit")
.description("Audit config + local state for common security foot-guns")
.option("--deep", "Attempt live Gateway probe (best-effort)", false)
.option("--json", "Print JSON", false)
.action(async (opts: SecurityAuditOptions) => {
const cfg = loadConfig();
const report = await runSecurityAudit({
config: cfg,
deep: Boolean(opts.deep),
includeFilesystem: true,
includeChannelSecurity: true,
});
if (opts.json) {
defaultRuntime.log(JSON.stringify(report, null, 2));
return;
}
const rich = isRich();
const heading = (text: string) => (rich ? theme.heading(text) : text);
const muted = (text: string) => (rich ? theme.muted(text) : text);
const lines: string[] = [];
lines.push(heading("Clawdbot security audit"));
lines.push(muted(`Summary: ${formatSummary(report.summary)}`));
lines.push(muted(`Run deeper: clawdbot security audit --deep`));
const bySeverity = (sev: "critical" | "warn" | "info") =>
report.findings.filter((f) => f.severity === sev);
const render = (sev: "critical" | "warn" | "info") => {
const list = bySeverity(sev);
if (list.length === 0) return;
const label =
sev === "critical"
? rich
? theme.error("CRITICAL")
: "CRITICAL"
: sev === "warn"
? rich
? theme.warn("WARN")
: "WARN"
: rich
? theme.muted("INFO")
: "INFO";
lines.push("");
lines.push(heading(label));
for (const f of list) {
lines.push(`${chalk.gray(f.checkId)} ${f.title}`);
lines.push(` ${f.detail}`);
if (f.remediation?.trim()) lines.push(` ${muted(`Fix: ${f.remediation.trim()}`)}`);
}
};
render("critical");
render("warn");
render("info");
defaultRuntime.log(lines.join("\n"));
});
}