feat(security): add CLI commands for security management
This commit is contained in:
parent
c2bd42b89f
commit
a7c5fd342d
@ -1,13 +1,18 @@
|
||||
import type { Command } from "commander";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { loadConfig, writeConfigFile } from "../config/config.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { runSecurityAudit } from "../security/audit.js";
|
||||
import { fixSecurityFootguns } from "../security/fix.js";
|
||||
import { ipManager } from "../security/ip-manager.js";
|
||||
import { DEFAULT_LOG_DIR } from "../logging/logger.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { isRich, theme } from "../terminal/theme.js";
|
||||
import { shortenHomeInString, shortenHomePath } from "../utils.js";
|
||||
import { formatCliCommand } from "./command-format.js";
|
||||
import { parseDuration } from "./parse-duration.js";
|
||||
|
||||
type SecurityAuditOptions = {
|
||||
json?: boolean;
|
||||
@ -146,4 +151,264 @@ export function registerSecurityCli(program: Command) {
|
||||
|
||||
defaultRuntime.log(lines.join("\n"));
|
||||
});
|
||||
|
||||
// openclaw security status
|
||||
security
|
||||
.command("status")
|
||||
.description("Show security shield status")
|
||||
.action(async () => {
|
||||
const cfg = loadConfig();
|
||||
const enabled = cfg.security?.shield?.enabled ?? false;
|
||||
const rateLimitingEnabled = cfg.security?.shield?.rateLimiting?.enabled ?? false;
|
||||
const intrusionDetectionEnabled = cfg.security?.shield?.intrusionDetection?.enabled ?? false;
|
||||
const firewallEnabled = cfg.security?.shield?.ipManagement?.firewall?.enabled ?? false;
|
||||
const alertingEnabled = cfg.security?.alerting?.enabled ?? false;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(theme.heading("Security Shield Status"));
|
||||
lines.push("");
|
||||
lines.push(`Shield: ${enabled ? theme.success("ENABLED") : theme.error("DISABLED")}`);
|
||||
lines.push(`Rate Limiting: ${rateLimitingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`);
|
||||
lines.push(`Intrusion Detection: ${intrusionDetectionEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`);
|
||||
lines.push(`Firewall Integration: ${firewallEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`);
|
||||
lines.push(`Alerting: ${alertingEnabled ? theme.success("ENABLED") : theme.muted("disabled")}`);
|
||||
|
||||
if (alertingEnabled && cfg.security?.alerting?.channels?.telegram?.enabled) {
|
||||
lines.push(` Telegram: ${theme.success("ENABLED")}`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push(theme.muted(`Docs: ${formatDocsLink("/security/shield", "docs.openclaw.ai/security/shield")}`));
|
||||
defaultRuntime.log(lines.join("\n"));
|
||||
});
|
||||
|
||||
// openclaw security enable
|
||||
security
|
||||
.command("enable")
|
||||
.description("Enable security shield")
|
||||
.action(async () => {
|
||||
const cfg = loadConfig();
|
||||
cfg.security = cfg.security || {};
|
||||
cfg.security.shield = cfg.security.shield || {};
|
||||
cfg.security.shield.enabled = true;
|
||||
|
||||
await writeConfigFile(cfg);
|
||||
defaultRuntime.log(theme.success("✓ Security shield enabled"));
|
||||
defaultRuntime.log(theme.muted(` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`));
|
||||
});
|
||||
|
||||
// openclaw security disable
|
||||
security
|
||||
.command("disable")
|
||||
.description("Disable security shield")
|
||||
.action(async () => {
|
||||
const cfg = loadConfig();
|
||||
if (!cfg.security?.shield) {
|
||||
defaultRuntime.log(theme.muted("Security shield already disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
cfg.security.shield.enabled = false;
|
||||
await writeConfigFile(cfg);
|
||||
defaultRuntime.log(theme.warn("⚠ Security shield disabled"));
|
||||
defaultRuntime.log(theme.muted(` Restart gateway for changes to take effect: ${formatCliCommand("openclaw gateway restart")}`));
|
||||
});
|
||||
|
||||
// openclaw security logs
|
||||
security
|
||||
.command("logs")
|
||||
.description("View security event logs")
|
||||
.option("-f, --follow", "Follow log output (tail -f)")
|
||||
.option("-n, --lines <number>", "Number of lines to show", "50")
|
||||
.option("--severity <level>", "Filter by severity (critical, warn, info)")
|
||||
.action(async (opts: { follow?: boolean; lines?: string; severity?: string }) => {
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
const logFile = path.join(DEFAULT_LOG_DIR, `security-${today}.jsonl`);
|
||||
|
||||
if (!fs.existsSync(logFile)) {
|
||||
defaultRuntime.log(theme.warn(`No security logs found for today: ${logFile}`));
|
||||
defaultRuntime.log(theme.muted(`Logs are created when security events occur`));
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = parseInt(opts.lines || "50", 10);
|
||||
const severity = opts.severity?.toLowerCase();
|
||||
|
||||
if (opts.follow) {
|
||||
// Tail follow mode
|
||||
const { spawn } = await import("node:child_process");
|
||||
const tail = spawn("tail", ["-f", "-n", String(lines), logFile], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
tail.on("error", (err) => {
|
||||
defaultRuntime.log(theme.error(`Failed to tail logs: ${String(err)}`));
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
// Read last N lines
|
||||
const content = fs.readFileSync(logFile, "utf-8");
|
||||
const allLines = content.trim().split("\n").filter(Boolean);
|
||||
const lastLines = allLines.slice(-lines);
|
||||
|
||||
for (const line of lastLines) {
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
if (severity && event.severity !== severity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const severityLabel =
|
||||
event.severity === "critical"
|
||||
? theme.error("CRITICAL")
|
||||
: event.severity === "warn"
|
||||
? theme.warn("WARN")
|
||||
: theme.muted("INFO");
|
||||
|
||||
const timestamp = new Date(event.timestamp).toLocaleString();
|
||||
defaultRuntime.log(`[${timestamp}] ${severityLabel} ${event.action} (${event.ip})`);
|
||||
|
||||
if (event.details && Object.keys(event.details).length > 0) {
|
||||
defaultRuntime.log(theme.muted(` ${JSON.stringify(event.details)}`));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid lines
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// openclaw blocklist
|
||||
const blocklist = program
|
||||
.command("blocklist")
|
||||
.description("Manage IP blocklist");
|
||||
|
||||
blocklist
|
||||
.command("list")
|
||||
.description("List all blocked IPs")
|
||||
.option("--json", "Print JSON", false)
|
||||
.action(async (opts: { json?: boolean }) => {
|
||||
const entries = ipManager.getBlocklist();
|
||||
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(entries, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
defaultRuntime.log(theme.muted("No blocked IPs"));
|
||||
return;
|
||||
}
|
||||
|
||||
defaultRuntime.log(theme.heading(`Blocked IPs (${entries.length})`));
|
||||
defaultRuntime.log("");
|
||||
|
||||
for (const entry of entries) {
|
||||
const expiresAt = new Date(entry.expiresAt);
|
||||
const now = new Date();
|
||||
const remaining = expiresAt.getTime() - now.getTime();
|
||||
const hours = Math.floor(remaining / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
defaultRuntime.log(`${theme.bold(entry.ip)}`);
|
||||
defaultRuntime.log(` Reason: ${entry.reason}`);
|
||||
defaultRuntime.log(` Source: ${entry.source}`);
|
||||
defaultRuntime.log(` Blocked: ${new Date(entry.blockedAt).toLocaleString()}`);
|
||||
defaultRuntime.log(` Expires: ${expiresAt.toLocaleString()} (${hours}h ${minutes}m remaining)`);
|
||||
defaultRuntime.log("");
|
||||
}
|
||||
});
|
||||
|
||||
blocklist
|
||||
.command("add <ip>")
|
||||
.description("Block an IP address")
|
||||
.option("-r, --reason <reason>", "Block reason", "manual")
|
||||
.option("-d, --duration <duration>", "Block duration (e.g., 24h, 7d, 30d)", "24h")
|
||||
.action(async (ip: string, opts: { reason?: string; duration?: string }) => {
|
||||
const reason = opts.reason || "manual";
|
||||
const durationMs = parseDuration(opts.duration || "24h");
|
||||
|
||||
ipManager.blockIp({
|
||||
ip,
|
||||
reason,
|
||||
durationMs,
|
||||
source: "manual",
|
||||
});
|
||||
|
||||
defaultRuntime.log(theme.success(`✓ Blocked ${ip}`));
|
||||
defaultRuntime.log(theme.muted(` Reason: ${reason}`));
|
||||
defaultRuntime.log(theme.muted(` Duration: ${opts.duration}`));
|
||||
});
|
||||
|
||||
blocklist
|
||||
.command("remove <ip>")
|
||||
.description("Unblock an IP address")
|
||||
.action(async (ip: string) => {
|
||||
const removed = ipManager.unblockIp(ip);
|
||||
|
||||
if (removed) {
|
||||
defaultRuntime.log(theme.success(`✓ Unblocked ${ip}`));
|
||||
} else {
|
||||
defaultRuntime.log(theme.muted(`IP ${ip} was not blocked`));
|
||||
}
|
||||
});
|
||||
|
||||
// openclaw allowlist
|
||||
const allowlist = program
|
||||
.command("allowlist")
|
||||
.description("Manage IP allowlist");
|
||||
|
||||
allowlist
|
||||
.command("list")
|
||||
.description("List all allowed IPs")
|
||||
.option("--json", "Print JSON", false)
|
||||
.action(async (opts: { json?: boolean }) => {
|
||||
const entries = ipManager.getAllowlist();
|
||||
|
||||
if (opts.json) {
|
||||
defaultRuntime.log(JSON.stringify(entries, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
defaultRuntime.log(theme.muted("No allowed IPs"));
|
||||
return;
|
||||
}
|
||||
|
||||
defaultRuntime.log(theme.heading(`Allowed IPs (${entries.length})`));
|
||||
defaultRuntime.log("");
|
||||
|
||||
for (const entry of entries) {
|
||||
defaultRuntime.log(`${theme.bold(entry.ip)}`);
|
||||
defaultRuntime.log(` Reason: ${entry.reason}`);
|
||||
defaultRuntime.log(` Source: ${entry.source}`);
|
||||
defaultRuntime.log(` Added: ${new Date(entry.addedAt).toLocaleString()}`);
|
||||
defaultRuntime.log("");
|
||||
}
|
||||
});
|
||||
|
||||
allowlist
|
||||
.command("add <ip>")
|
||||
.description("Add IP to allowlist (supports CIDR notation)")
|
||||
.option("-r, --reason <reason>", "Allow reason", "manual")
|
||||
.action(async (ip: string, opts: { reason?: string }) => {
|
||||
const reason = opts.reason || "manual";
|
||||
|
||||
ipManager.allowIp({
|
||||
ip,
|
||||
reason,
|
||||
source: "manual",
|
||||
});
|
||||
|
||||
defaultRuntime.log(theme.success(`✓ Added ${ip} to allowlist`));
|
||||
defaultRuntime.log(theme.muted(` Reason: ${reason}`));
|
||||
});
|
||||
|
||||
allowlist
|
||||
.command("remove <ip>")
|
||||
.description("Remove IP from allowlist")
|
||||
.action(async (ip: string) => {
|
||||
ipManager.removeFromAllowlist(ip);
|
||||
defaultRuntime.log(theme.success(`✓ Removed ${ip} from allowlist`));
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user