This commit is contained in:
marciob 2026-01-30 11:55:33 +00:00 committed by GitHub
commit c873a72657
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 129 additions and 1 deletions

View File

@ -8,6 +8,7 @@ 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 { registerSecurityWrapCli } from "./security-wrap-cli.js";
type SecurityAuditOptions = {
json?: boolean;
@ -30,13 +31,16 @@ function formatSummary(summary: { critical: number; warn: number; info: number }
export function registerSecurityCli(program: Command) {
const security = program
.command("security")
.description("Security tools (audit)")
.description("Security tools (audit, wrap)")
.addHelpText(
"after",
() =>
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/security", "docs.openclaw.ai/cli/security")}\n`,
);
// Register subcommands
registerSecurityWrapCli(security);
security
.command("audit")
.description("Audit config + local state for common security foot-guns")

View File

@ -0,0 +1,124 @@
import type { Command } from "commander";
import { createInterface } from "readline";
import {
detectSuspiciousPatterns,
wrapExternalContent,
type ExternalContentSource,
} from "../security/external-content.js";
import { defaultRuntime } from "../runtime.js";
import { isRich, theme } from "../terminal/theme.js";
type WrapOptions = {
source?: string;
url?: string;
stdin?: boolean;
json?: boolean;
noWarning?: boolean;
};
async function readStdin(): Promise<string> {
return new Promise((resolve) => {
const lines: string[] = [];
const rl = createInterface({ input: process.stdin });
rl.on("line", (line) => lines.push(line));
rl.on("close", () => resolve(lines.join("\n")));
});
}
async function fetchUrl(url: string): Promise<string> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
}
return response.text();
}
export function registerSecurityWrapCli(securityCmd: Command) {
securityCmd
.command("wrap")
.description("Wrap external content with security boundaries for safe LLM processing")
.option("--source <source>", "Content source label (email, webhook, api, or custom)", "api")
.option("--url <url>", "Fetch content from URL")
.option("--stdin", "Read content from stdin", false)
.option("--no-warning", "Omit security warning header", false)
.option("--json", "Output as JSON with metadata", false)
.addHelpText(
"after",
`
Examples:
echo '{"data": "test"}' | moltbot security wrap --stdin --source api
moltbot security wrap --url https://api.example.com/data
curl -s https://api.example.com | moltbot security wrap --stdin --source "external-api"
Use this when fetching external APIs in skills to protect against prompt injection.
`,
)
.action(async (opts: WrapOptions) => {
const rich = isRich();
// Determine content source
let content: string;
let sourceLabel = opts.source || "api";
if (opts.url) {
try {
content = await fetchUrl(opts.url);
sourceLabel = opts.source || opts.url;
} catch (err) {
defaultRuntime.error(`Error fetching URL: ${err}`);
process.exit(1);
}
} else if (opts.stdin || !process.stdin.isTTY) {
content = await readStdin();
} else {
defaultRuntime.error("No input provided. Use --url or --stdin, or pipe content.");
process.exit(1);
}
// Detect suspicious patterns
const suspiciousPatterns = detectSuspiciousPatterns(content);
if (suspiciousPatterns.length > 0) {
const warn = rich ? theme.warn("⚠️ SUSPICIOUS PATTERNS DETECTED:") : "WARNING: SUSPICIOUS PATTERNS DETECTED:";
defaultRuntime.error(warn);
for (const pattern of suspiciousPatterns) {
defaultRuntime.error(` - ${pattern}`);
}
defaultRuntime.error("");
}
// Map source to type
const sourceType: ExternalContentSource =
sourceLabel === "email"
? "email"
: sourceLabel === "webhook"
? "webhook"
: sourceLabel === "api"
? "api"
: "unknown";
// Wrap content
const wrapped = wrapExternalContent(content, {
source: sourceType,
sender: sourceLabel !== sourceType ? sourceLabel : undefined,
includeWarning: opts.noWarning !== true,
});
if (opts.json) {
const output = {
wrapped,
metadata: {
source: sourceLabel,
sourceType,
timestamp: new Date().toISOString(),
suspiciousPatterns: suspiciousPatterns.length > 0 ? suspiciousPatterns : undefined,
contentLength: content.length,
wrappedLength: wrapped.length,
},
};
defaultRuntime.log(JSON.stringify(output, null, 2));
} else {
defaultRuntime.log(wrapped);
}
});
}