Merge f3d695fe8b into da71eaebd2
This commit is contained in:
commit
c873a72657
@ -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")
|
||||
|
||||
124
src/cli/security-wrap-cli.ts
Normal file
124
src/cli/security-wrap-cli.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user