// ABOUTME: /secure command extension — comprehensive AI security sweep and protection installer. // ABOUTME: Scans projects for AI vulnerabilities (prompt injection, credential exposure) and installs portable security guards. /** * /secure — AI Security Sweep & Protection Installer * * Subcommands: * /secure — Run full security sweep (scans project for AI vulnerabilities) * /secure sweep — Same as above * /secure install — Install AI protection files into current project * /secure status — Show current project's security posture (quick check) * /secure report — View last security report * * The sweep detects: * - AI service usage (OpenAI, Anthropic, Cohere, LangChain, etc.) * - Prompt injection vulnerabilities (unsanitized user input → AI) * - Credential exposure (hardcoded API keys, unignored .env files) * - System prompt leakage (prompts in client code or API responses) * - Missing rate limiting on AI endpoints * - Unsafe eval of AI outputs * - Missing output filtering (XSS via AI responses) * * The installer generates: * - Portable AI security guard (JS/TS/Python) * - Security policy YAML * - Framework-specific middleware (Express, Fastify, Next.js, Hono) * - CI/CD security check workflow * - .env.example with secure defaults * * Usage: Loaded via packages in agent/settings.json */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { runSweep, profileProject, formatSweepReport, type SweepResult, } from "./lib/secure-engine.ts"; import { installProtections, formatInstallReport, } from "./lib/secure-installer.ts"; // ═══════════════════════════════════════════════════════════════════ // State // ═══════════════════════════════════════════════════════════════════ let lastSweepResult: SweepResult | null = null; // ═══════════════════════════════════════════════════════════════════ // Extension Entry Point // ═══════════════════════════════════════════════════════════════════ export default function secure(pi: ExtensionAPI) { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // ================================================================ // /secure command // ================================================================ pi.registerCommand("secure", { description: "AI Security — sweep for vulnerabilities, install protections [sweep|install|status|report]", handler: async (args, ctx) => { const subcommand = (args || "sweep").trim().toLowerCase().split(/\s+/)[0]; const subArgs = (args || "").trim().slice(subcommand.length).trim(); const cwd = ctx?.cwd || process.cwd(); switch (subcommand) { case "sweep": case "scan": return handleSweep(cwd, ctx, pi); case "install": case "protect": return handleInstall(cwd, ctx, subArgs, pi); case "status": case "check": return handleStatus(cwd, ctx); case "report": case "last": return handleReport(ctx, pi); case "help": ctx.ui.notify( [ "🛡️ /secure — AI Security Sweep & Protection", "", "Commands:", " /secure Run full security sweep", " /secure sweep Same as above", " /secure install Install AI protections into project", " /secure install --overwrite Overwrite existing files", " /secure status Quick security posture check", " /secure report View last sweep report", " /secure help Show this help", ].join("\n"), "info", ); break; default: // If unrecognized, treat as sweep with scope return handleSweep(cwd, ctx, pi); } }, }); // ================================================================ // Session Lifecycle // ================================================================ pi.on("session_start", async (_event, _ctx) => { lastSweepResult = null; }); } // ═══════════════════════════════════════════════════════════════════ // Command Handlers // ═══════════════════════════════════════════════════════════════════ async function handleSweep(cwd: string, ctx: any, pi: ExtensionAPI) { ctx.ui.notify("🔍 Running AI security sweep...", "info"); try { const result = runSweep(cwd); lastSweepResult = result; // Generate and save report const report = formatSweepReport(result); const reportDir = join(cwd, ".pi"); if (!existsSync(reportDir)) { try { mkdirSync(reportDir, { recursive: true }); } catch {} } const reportPath = join(reportDir, "security-sweep-report.md"); writeFileSync(reportPath, report, "utf-8"); // Summary notification const counts = { critical: 0, high: 0, medium: 0, low: 0 }; for (const f of result.findings) { if (f.severity in counts) counts[f.severity as keyof typeof counts]++; } const scoreIcon = result.score >= 80 ? "🟢" : result.score >= 60 ? "🟡" : result.score >= 40 ? "🟠" : "🔴"; const summaryLines = [ `🛡️ Security Sweep Complete`, ``, `Score: ${scoreIcon} ${result.score}/100`, `Files scanned: ${result.profile.totalFiles}`, `AI services: ${result.profile.aiServices.map((s) => s.name).join(", ") || "None"}`, ``, `Findings:`, ` 🔴 Critical: ${counts.critical}`, ` 🟠 High: ${counts.high}`, ` 🟡 Medium: ${counts.medium}`, ` 🔵 Low: ${counts.low}`, ``, `Report saved to: ${reportPath}`, ]; ctx.ui.notify(summaryLines.join("\n"), counts.critical > 0 ? "error" : counts.high > 0 ? "warning" : "success"); // Inject report as message so the agent can discuss findings pi.sendMessage( { customType: "security-sweep-result", content: report, display: true, }, { deliverAs: "followUp", triggerTurn: true }, ); } catch (err) { ctx.ui.notify(`Security sweep failed: ${err}`, "error"); } } async function handleInstall(cwd: string, ctx: any, args: string, pi: ExtensionAPI) { const overwrite = args.includes("--overwrite") || args.includes("-f"); const dryRun = args.includes("--dry-run") || args.includes("-n"); ctx.ui.notify( dryRun ? "🛡️ Running dry-run installation (no files will be written)..." : "🛡️ Installing AI security protections...", "info", ); try { const profile = profileProject(cwd); const result = installProtections(cwd, profile, { overwrite, dryRun }); // Generate install report const report = formatInstallReport(result); // Save report const reportDir = join(cwd, ".pi"); if (!existsSync(reportDir)) { try { mkdirSync(reportDir, { recursive: true }); } catch {} } const reportPath = join(reportDir, "security-install-report.md"); writeFileSync(reportPath, report, "utf-8"); const created = result.files.filter((f) => f.created).length; const skipped = result.files.filter((f) => !f.created).length; const summaryLines = [ `🛡️ AI Security Protection ${dryRun ? "(Dry Run)" : "Installed"}`, ``, `Files ${dryRun ? "would be " : ""}created: ${created}`, `Files skipped: ${skipped}`, `Warnings: ${result.warnings.length}`, ``, `Report saved to: ${reportPath}`, ]; if (result.warnings.length > 0) { summaryLines.push(``, `Warnings:`); for (const w of result.warnings.slice(0, 5)) { summaryLines.push(` ⚠️ ${w}`); } } ctx.ui.notify(summaryLines.join("\n"), result.warnings.length > 0 ? "warning" : "success"); // Inject report pi.sendMessage( { customType: "security-install-result", content: report, display: true, }, { deliverAs: "followUp", triggerTurn: true }, ); } catch (err) { ctx.ui.notify(`Installation failed: ${err}`, "error"); } } async function handleStatus(cwd: string, ctx: any) { try { const profile = profileProject(cwd); const checks: Array<{ label: string; pass: boolean; detail: string }> = []; // AI services checks.push({ label: "AI Services", pass: profile.aiServices.length > 0, detail: profile.aiServices.length > 0 ? profile.aiServices.map((s) => s.name).join(", ") : "None detected", }); // .gitignore checks.push({ label: ".gitignore", pass: profile.hasGitIgnore, detail: profile.hasGitIgnore ? "Present" : "MISSING — secrets may be committed!", }); // .env check const gitignorePath = join(cwd, ".gitignore"); let envIgnored = false; if (existsSync(gitignorePath)) { const gi = readFileSync(gitignorePath, "utf-8"); envIgnored = /\.env/m.test(gi); } checks.push({ label: ".env in .gitignore", pass: envIgnored, detail: envIgnored ? "Properly ignored" : ".env NOT in .gitignore — keys may leak!", }); // Security guard presence const hasGuard = existsSync(join(cwd, "lib", "security", "ai-security-guard.ts")) || existsSync(join(cwd, "lib", "security", "ai-security-guard.js")) || existsSync(join(cwd, "lib", "security", "ai_security_guard.py")); checks.push({ label: "AI Security Guard", pass: hasGuard, detail: hasGuard ? "Installed" : "Not installed — run /secure install", }); // Security policy const hasPolicy = existsSync(join(cwd, ".ai-security-policy.yaml")); checks.push({ label: "Security Policy", pass: hasPolicy, detail: hasPolicy ? "Present" : "Not found — run /secure install", }); // CI checks checks.push({ label: "CI Security Checks", pass: profile.hasCIConfig, detail: profile.hasCIConfig ? "Present" : "No CI pipeline detected", }); // Rate limiting if (profile.languages.some((l) => l.includes("JavaScript"))) { const pkgPath = join(cwd, "package.json"); let hasRateLimit = false; if (existsSync(pkgPath)) { try { const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); const deps = { ...pkg.dependencies, ...pkg.devDependencies }; hasRateLimit = !!(deps["express-rate-limit"] || deps["rate-limiter-flexible"] || deps["bottleneck"] || deps["@upstash/ratelimit"]); } catch {} } checks.push({ label: "Rate Limiting", pass: hasRateLimit, detail: hasRateLimit ? "Library detected" : "No rate limiting library found", }); } // Format output const passCount = checks.filter((c) => c.pass).length; const totalChecks = checks.length; const score = Math.round((passCount / totalChecks) * 100); const scoreIcon = score >= 80 ? "🟢" : score >= 60 ? "🟡" : score >= 40 ? "🟠" : "🔴"; const lines = [ `🛡️ Security Status — ${profile.name}`, ``, `Posture: ${scoreIcon} ${score}% (${passCount}/${totalChecks} checks passing)`, ``, ]; for (const check of checks) { const icon = check.pass ? "✅" : "❌"; lines.push(`${icon} ${check.label}: ${check.detail}`); } if (lastSweepResult) { lines.push(``); lines.push(`Last sweep: ${lastSweepResult.timestamp} — Score: ${lastSweepResult.score}/100, ${lastSweepResult.findings.length} findings`); } ctx.ui.notify(lines.join("\n"), score >= 80 ? "success" : score >= 60 ? "warning" : "error"); } catch (err) { ctx.ui.notify(`Status check failed: ${err}`, "error"); } } async function handleReport(ctx: any, pi: ExtensionAPI) { if (lastSweepResult) { const report = formatSweepReport(lastSweepResult); pi.sendMessage( { customType: "security-sweep-result", content: report, display: true, }, { deliverAs: "followUp", triggerTurn: true }, ); return; } // Try to load from file const cwd = ctx?.cwd || process.cwd(); const reportPath = join(cwd, ".pi", "security-sweep-report.md"); if (existsSync(reportPath)) { const content = readFileSync(reportPath, "utf-8"); pi.sendMessage( { customType: "security-sweep-result", content, display: true, }, { deliverAs: "followUp", triggerTurn: true }, ); } else { ctx.ui.notify("No security report found. Run /secure sweep first.", "info"); } }