security: add command execution blocklist

- Add tiered blocklist (critical/high/medium) for dangerous commands
- Block: rm -rf /, dd to disk, mkfs, halt/reboot, fork bombs, etc.
- Block: sudo, passwd, visudo, iptables, user management
- Warn but allow: command substitution, eval, curl POST, chmod 777
- Integrate blocklist into evaluateShellAllowlist (checked before allowlist)
- Add comprehensive test suite (47 tests)
- Export blocklist utilities for use by other modules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ronitchidara 2026-01-27 12:48:34 +05:30
parent 988950f845
commit 6a960c5137
4 changed files with 912 additions and 1 deletions

View File

@ -154,8 +154,9 @@ describe("exec approvals shell allowlist (chained commands)", () => {
it("rejects chained commands when any part is not allowlisted", () => { it("rejects chained commands when any part is not allowlisted", () => {
const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/obsidian-cli" }]; const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/obsidian-cli" }];
// Use a non-blocklisted command to test allowlist rejection
const result = evaluateShellAllowlist({ const result = evaluateShellAllowlist({
command: "/usr/bin/obsidian-cli print-default && /usr/bin/rm -rf /", command: "/usr/bin/obsidian-cli print-default && /usr/bin/unknown-cmd",
allowlist, allowlist,
safeBins: new Set(), safeBins: new Set(),
cwd: "/tmp", cwd: "/tmp",
@ -164,6 +165,21 @@ describe("exec approvals shell allowlist (chained commands)", () => {
expect(result.allowlistSatisfied).toBe(false); expect(result.allowlistSatisfied).toBe(false);
}); });
it("rejects blocklisted commands before checking allowlist", () => {
const allowlist: ExecAllowlistEntry[] = [{ pattern: "rm" }];
// Even with 'rm' on allowlist, 'rm -rf /' should be blocked by blocklist
const result = evaluateShellAllowlist({
command: "rm -rf /",
allowlist,
safeBins: new Set(),
cwd: "/tmp",
});
// Blocklist rejection sets analysisOk to false
expect(result.analysisOk).toBe(false);
expect(result.blocklistResult?.blocked).toBe(true);
expect(result.blocklistResult?.severity).toBe("critical");
});
it("returns analysisOk=false for malformed chains", () => { it("returns analysisOk=false for malformed chains", () => {
const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/echo" }]; const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/echo" }];
const result = evaluateShellAllowlist({ const result = evaluateShellAllowlist({

View File

@ -5,6 +5,24 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import { DEFAULT_AGENT_ID } from "../routing/session-key.js"; import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
import {
evaluateBlocklist,
formatBlocklistReason,
isOnBlocklist,
getBlocklistPatterns,
type BlocklistConfig,
type BlocklistResult,
} from "./exec-blocklist.js";
// Re-export blocklist utilities for consumers
export {
evaluateBlocklist,
formatBlocklistReason,
isOnBlocklist,
getBlocklistPatterns,
type BlocklistConfig,
type BlocklistResult,
} from "./exec-blocklist.js";
export type ExecHost = "sandbox" | "gateway" | "node"; export type ExecHost = "sandbox" | "gateway" | "node";
export type ExecSecurity = "deny" | "allowlist" | "full"; export type ExecSecurity = "deny" | "allowlist" | "full";
@ -1045,10 +1063,13 @@ export type ExecAllowlistAnalysis = {
allowlistSatisfied: boolean; allowlistSatisfied: boolean;
allowlistMatches: ExecAllowlistEntry[]; allowlistMatches: ExecAllowlistEntry[];
segments: ExecCommandSegment[]; segments: ExecCommandSegment[];
/** Blocklist evaluation result (if blocklist check was performed) */
blocklistResult?: BlocklistResult;
}; };
/** /**
* Evaluates allowlist for shell commands (including &&, ||, ;) and returns analysis metadata. * Evaluates allowlist for shell commands (including &&, ||, ;) and returns analysis metadata.
* Also checks the security blocklist FIRST to reject dangerous commands.
*/ */
export function evaluateShellAllowlist(params: { export function evaluateShellAllowlist(params: {
command: string; command: string;
@ -1058,6 +1079,46 @@ export function evaluateShellAllowlist(params: {
env?: NodeJS.ProcessEnv; env?: NodeJS.ProcessEnv;
skillBins?: Set<string>; skillBins?: Set<string>;
autoAllowSkills?: boolean; autoAllowSkills?: boolean;
/** Configuration for blocklist evaluation (optional) */
blocklistConfig?: Partial<BlocklistConfig>;
/** Skip blocklist check (use with extreme caution, only for sandbox environments) */
skipBlocklist?: boolean;
}): ExecAllowlistAnalysis {
// SECURITY: Check blocklist FIRST before any other evaluation
// This ensures dangerous commands are blocked regardless of allowlist
if (!params.skipBlocklist) {
const blocklistResult = evaluateBlocklist(params.command, params.blocklistConfig);
if (blocklistResult.blocked) {
return {
analysisOk: false, // Treat blocked commands as analysis failure
allowlistSatisfied: false,
allowlistMatches: [],
segments: [],
blocklistResult,
};
}
// Even if not blocked, include the result for logging/audit
if (blocklistResult.matchedPatterns.length > 0) {
// Continue with allowlist evaluation but include blocklist result
const result = evaluateShellAllowlistInternal(params);
return { ...result, blocklistResult };
}
}
return evaluateShellAllowlistInternal(params);
}
/**
* Internal allowlist evaluation (called after blocklist check).
*/
function evaluateShellAllowlistInternal(params: {
command: string;
allowlist: ExecAllowlistEntry[];
safeBins: Set<string>;
cwd?: string;
env?: NodeJS.ProcessEnv;
skillBins?: Set<string>;
autoAllowSkills?: boolean;
}): ExecAllowlistAnalysis { }): ExecAllowlistAnalysis {
const chainParts = splitCommandChain(params.command); const chainParts = splitCommandChain(params.command);
if (!chainParts) { if (!chainParts) {

View File

@ -0,0 +1,320 @@
import { describe, expect, test } from "vitest";
import {
evaluateBlocklist,
isOnBlocklist,
formatBlocklistReason,
getBlocklistPatterns,
} from "./exec-blocklist.js";
describe("evaluateBlocklist", () => {
describe("critical severity commands", () => {
test("blocks rm -rf /", () => {
const result = evaluateBlocklist("rm -rf /");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("critical");
expect(result.matchedPatterns.length).toBeGreaterThan(0);
});
test("blocks rm -rf ~", () => {
const result = evaluateBlocklist("rm -rf ~");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("critical");
});
test("blocks dd to disk device", () => {
const result = evaluateBlocklist("dd if=/dev/zero of=/dev/sda");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("critical");
expect(result.matchedPatterns).toContain("dd to raw disk device");
});
test("blocks mkfs on disk", () => {
const result = evaluateBlocklist("mkfs.ext4 /dev/sda1");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("critical");
});
test("blocks halt command", () => {
const result = evaluateBlocklist("halt");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("critical");
});
test("blocks reboot command", () => {
const result = evaluateBlocklist("reboot");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("critical");
});
test("blocks shutdown command", () => {
const result = evaluateBlocklist("shutdown -h now");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("critical");
});
test("blocks systemctl poweroff", () => {
const result = evaluateBlocklist("systemctl poweroff");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("critical");
});
test("blocks fork bomb", () => {
const result = evaluateBlocklist(":(){ :|:& };:");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("critical");
expect(result.matchedPatterns).toContain("fork bomb");
});
});
describe("high severity commands", () => {
test("blocks sudo", () => {
const result = evaluateBlocklist("sudo rm -rf /tmp/test");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
expect(result.matchedPatterns).toContain("sudo (privilege escalation)");
});
test("blocks su to root", () => {
const result = evaluateBlocklist("su - root");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
});
test("blocks passwd", () => {
const result = evaluateBlocklist("passwd");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
});
test("blocks visudo", () => {
const result = evaluateBlocklist("visudo");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
});
test("blocks iptables", () => {
const result = evaluateBlocklist("iptables -F");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
});
test("blocks useradd", () => {
const result = evaluateBlocklist("useradd newuser");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
});
test("blocks userdel", () => {
const result = evaluateBlocklist("userdel olduser");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
});
test("blocks modprobe", () => {
const result = evaluateBlocklist("modprobe some_module");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
});
test("blocks redirect to /etc/passwd", () => {
const result = evaluateBlocklist("echo 'malicious' > /etc/passwd");
expect(result.blocked).toBe(true);
expect(result.severity).toBe("high");
});
});
describe("medium severity commands (not blocked by default)", () => {
test("does not block command substitution by default", () => {
const result = evaluateBlocklist("echo $(date)");
expect(result.blocked).toBe(false);
expect(result.severity).toBe("medium");
expect(result.matchedPatterns).toContain("command substitution $()");
});
test("does not block backtick substitution by default", () => {
const result = evaluateBlocklist("echo `whoami`");
expect(result.blocked).toBe(false);
expect(result.severity).toBe("medium");
});
test("does not block eval by default", () => {
const result = evaluateBlocklist("eval 'echo hello'");
expect(result.blocked).toBe(false);
expect(result.severity).toBe("medium");
});
test("does not block curl POST by default", () => {
const result = evaluateBlocklist("curl -d 'data' https://example.com");
expect(result.blocked).toBe(false);
expect(result.severity).toBe("medium");
});
test("does not block killall by default", () => {
const result = evaluateBlocklist("killall node");
expect(result.blocked).toBe(false);
expect(result.severity).toBe("medium");
});
test("does not block chmod 777 by default", () => {
const result = evaluateBlocklist("chmod 777 /tmp/test");
expect(result.blocked).toBe(false);
expect(result.severity).toBe("medium");
});
test("blocks medium severity when configured", () => {
const result = evaluateBlocklist("echo $(date)", { blockMedium: true });
expect(result.blocked).toBe(true);
expect(result.severity).toBe("medium");
});
});
describe("safe commands", () => {
test("does not block ls", () => {
const result = evaluateBlocklist("ls -la");
expect(result.blocked).toBe(false);
expect(result.severity).toBe(null);
});
test("does not block cat", () => {
const result = evaluateBlocklist("cat /etc/hosts");
expect(result.blocked).toBe(false);
expect(result.severity).toBe(null);
});
test("does not block grep", () => {
const result = evaluateBlocklist("grep -r pattern .");
expect(result.blocked).toBe(false);
expect(result.severity).toBe(null);
});
test("does not block normal rm", () => {
const result = evaluateBlocklist("rm /tmp/test.txt");
expect(result.blocked).toBe(false);
expect(result.severity).toBe(null);
});
test("does not block npm install", () => {
const result = evaluateBlocklist("npm install");
expect(result.blocked).toBe(false);
expect(result.severity).toBe(null);
});
test("does not block git operations", () => {
const result = evaluateBlocklist("git push origin main");
expect(result.blocked).toBe(false);
expect(result.severity).toBe(null);
});
test("does not block curl GET", () => {
const result = evaluateBlocklist("curl https://example.com");
expect(result.blocked).toBe(false);
expect(result.severity).toBe(null);
});
});
describe("configuration options", () => {
test("can disable critical blocking", () => {
const result = evaluateBlocklist("rm -rf /", { blockCritical: false });
expect(result.blocked).toBe(false);
expect(result.severity).toBe("critical");
expect(result.matchedPatterns.length).toBeGreaterThan(0);
});
test("can disable high blocking", () => {
const result = evaluateBlocklist("sudo ls", { blockHigh: false });
expect(result.blocked).toBe(false);
expect(result.severity).toBe("high");
});
test("can enable medium blocking", () => {
const result = evaluateBlocklist("eval 'test'", { blockMedium: true });
expect(result.blocked).toBe(true);
expect(result.severity).toBe("medium");
});
});
});
describe("isOnBlocklist", () => {
test("returns true for blocked commands", () => {
expect(isOnBlocklist("rm -rf /")).toBe(true);
expect(isOnBlocklist("sudo ls")).toBe(true);
expect(isOnBlocklist("echo $(date)")).toBe(true);
});
test("returns false for safe commands", () => {
expect(isOnBlocklist("ls -la")).toBe(false);
expect(isOnBlocklist("cat file.txt")).toBe(false);
expect(isOnBlocklist("npm install")).toBe(false);
});
});
describe("formatBlocklistReason", () => {
test("formats blocked command reason", () => {
const result = evaluateBlocklist("rm -rf /");
const formatted = formatBlocklistReason(result);
expect(formatted).toContain("blocked");
expect(formatted).toContain("critical");
expect(formatted).toContain("catastrophic");
});
test("formats allowed command", () => {
const result = evaluateBlocklist("ls -la");
const formatted = formatBlocklistReason(result);
expect(formatted).toContain("allowed");
});
});
describe("getBlocklistPatterns", () => {
test("returns all patterns", () => {
const patterns = getBlocklistPatterns();
expect(Array.isArray(patterns)).toBe(true);
expect(patterns.length).toBeGreaterThan(0);
// Check structure
const first = patterns[0];
expect(first).toHaveProperty("pattern");
expect(first).toHaveProperty("description");
expect(first).toHaveProperty("severity");
});
test("includes critical patterns", () => {
const patterns = getBlocklistPatterns();
const critical = patterns.filter((p) => p.severity === "critical");
expect(critical.length).toBeGreaterThan(0);
});
test("includes high patterns", () => {
const patterns = getBlocklistPatterns();
const high = patterns.filter((p) => p.severity === "high");
expect(high.length).toBeGreaterThan(0);
});
test("includes medium patterns", () => {
const patterns = getBlocklistPatterns();
const medium = patterns.filter((p) => p.severity === "medium");
expect(medium.length).toBeGreaterThan(0);
});
});
describe("edge cases", () => {
test("handles empty command", () => {
const result = evaluateBlocklist("");
expect(result.blocked).toBe(false);
expect(result.matchedPatterns.length).toBe(0);
});
test("handles command with spaces only", () => {
const result = evaluateBlocklist(" ");
expect(result.blocked).toBe(false);
});
test("handles multiline command", () => {
const result = evaluateBlocklist("ls -la\nrm -rf /");
expect(result.blocked).toBe(true);
});
test("handles case variations", () => {
const result = evaluateBlocklist("SUDO ls");
expect(result.blocked).toBe(true);
});
});

514
src/infra/exec-blocklist.ts Normal file
View File

@ -0,0 +1,514 @@
/**
* Command Execution Blocklist
*
* This module provides a security layer that blocks dangerous commands before
* they reach the allowlist evaluation. Commands matching blocklist patterns
* are ALWAYS blocked regardless of allowlist settings or elevated permissions.
*
* The blocklist is designed to prevent catastrophic system damage from
* commands that should almost never be run via an AI assistant.
*/
import { createSubsystemLogger } from "../logging/subsystem.js";
const log = createSubsystemLogger("exec").child("blocklist");
/** Result of blocklist evaluation */
export type BlocklistResult = {
blocked: boolean;
reason: string | null;
matchedPatterns: string[];
severity: "critical" | "high" | "medium" | null;
};
/** Blocklist entry with pattern and metadata */
type BlocklistEntry = {
pattern: RegExp;
description: string;
severity: "critical" | "high" | "medium";
/** If true, requires explicit user approval even in elevated mode */
requiresExplicitApproval: boolean;
};
/**
* CRITICAL: Commands that can cause catastrophic, irreversible system damage.
* These are ALWAYS blocked without any override option.
*/
const CRITICAL_BLOCKLIST: BlocklistEntry[] = [
// Destructive filesystem operations
{
pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?[\/~]\s*$/i,
description: "rm -rf / (root filesystem deletion)",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\brm\s+(-[a-zA-Z]*[rf]+[a-zA-Z]*\s+)+\s*\/\s*$/i,
description: "rm -rf / (root filesystem deletion)",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\brm\s+(-[a-zA-Z]*[rf]+[a-zA-Z]*\s+)+\s*~\s*$/i,
description: "rm -rf ~ (home directory deletion)",
severity: "critical",
requiresExplicitApproval: true,
},
// Disk/filesystem destruction
{
pattern: /\bdd\s+.*\bof\s*=\s*\/dev\/(sd[a-z]|hd[a-z]|nvme\d+n\d+|disk\d+)\b/i,
description: "dd to raw disk device",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bmkfs(\.[a-z0-9]+)?\s+.*\/dev\/(sd[a-z]|hd[a-z]|nvme\d+n\d+|disk\d+)/i,
description: "mkfs on disk device (filesystem destruction)",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bfdisk\s+.*\/dev\/(sd[a-z]|hd[a-z]|nvme\d+n\d+|disk\d+)/i,
description: "fdisk partition manipulation",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bparted\s+.*\/dev\/(sd[a-z]|hd[a-z]|nvme\d+n\d+|disk\d+)/i,
description: "parted partition manipulation",
severity: "critical",
requiresExplicitApproval: true,
},
// System control commands
{
pattern: /\b(halt|poweroff|reboot|shutdown)\b/i,
description: "system halt/reboot command",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\binit\s+[0-6]\b/i,
description: "init runlevel change",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bsystemctl\s+(halt|poweroff|reboot|suspend|hibernate)\b/i,
description: "systemctl power control",
severity: "critical",
requiresExplicitApproval: true,
},
// Fork bomb patterns
{
pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
description: "fork bomb",
severity: "critical",
requiresExplicitApproval: true,
},
{
pattern: /\bwhile\s+true\s*;\s*do\s*:\s*;\s*done\s*&/i,
description: "infinite loop fork",
severity: "critical",
requiresExplicitApproval: true,
},
];
/**
* HIGH: Commands that can cause significant security issues or data loss.
* These require explicit user approval.
*/
const HIGH_BLOCKLIST: BlocklistEntry[] = [
// Privilege escalation
{
pattern: /\bsudo\s+/i,
description: "sudo (privilege escalation)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bsu\s+(-\s+)?root\b/i,
description: "su to root",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bdoas\s+/i,
description: "doas (privilege escalation)",
severity: "high",
requiresExplicitApproval: true,
},
// Credential manipulation
{
pattern: /\bpasswd\b/i,
description: "passwd (password change)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bchpasswd\b/i,
description: "chpasswd (bulk password change)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bvisudo\b/i,
description: "visudo (sudoers modification)",
severity: "high",
requiresExplicitApproval: true,
},
// Firewall and network security
{
pattern: /\biptables\s+/i,
description: "iptables (firewall rules)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bip6tables\s+/i,
description: "ip6tables (IPv6 firewall rules)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bnft\s+/i,
description: "nftables (firewall rules)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bufw\s+(disable|reset|delete)/i,
description: "ufw firewall disable/reset",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bfirewall-cmd\s+/i,
description: "firewalld manipulation",
severity: "high",
requiresExplicitApproval: true,
},
// User/group manipulation
{
pattern: /\buseradd\b/i,
description: "useradd (user creation)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\buserdel\b/i,
description: "userdel (user deletion)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\busermod\b/i,
description: "usermod (user modification)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bgroupadd\b/i,
description: "groupadd (group creation)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bgroupdel\b/i,
description: "groupdel (group deletion)",
severity: "high",
requiresExplicitApproval: true,
},
// SSH key manipulation
{
pattern: /\bssh-keygen\s+.*(-[a-zA-Z]*f[a-zA-Z]*\s+)?~\/\.ssh\/(id_|authorized_keys)/i,
description: "SSH key overwrite",
severity: "high",
requiresExplicitApproval: true,
},
// Kernel/boot manipulation
{
pattern: /\bmodprobe\s+/i,
description: "modprobe (kernel module loading)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\binsmod\s+/i,
description: "insmod (kernel module insertion)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\brmmod\s+/i,
description: "rmmod (kernel module removal)",
severity: "high",
requiresExplicitApproval: true,
},
// Cron manipulation (persistence)
{
pattern: /\bcrontab\s+-[a-zA-Z]*e/i,
description: "crontab edit",
severity: "high",
requiresExplicitApproval: true,
},
// System config files
{
pattern: />\s*\/etc\/(passwd|shadow|sudoers|hosts|fstab|ssh)/i,
description: "redirect to system config file",
severity: "high",
requiresExplicitApproval: true,
},
// macOS specific
{
pattern: /\bcsrutil\s+(disable|enable)\b/i,
description: "csrutil (SIP modification)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bspctl\s+/i,
description: "spctl (Gatekeeper manipulation)",
severity: "high",
requiresExplicitApproval: true,
},
{
pattern: /\bdscl\s+.*-passwd/i,
description: "dscl password change",
severity: "high",
requiresExplicitApproval: true,
},
];
/**
* MEDIUM: Commands that should be reviewed but may have legitimate uses.
* These log warnings but don't block by default.
*/
const MEDIUM_BLOCKLIST: BlocklistEntry[] = [
// Shell operators that could enable injection
{
pattern: /\$\([^)]+\)/,
description: "command substitution $()",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /`[^`]+`/,
description: "backtick command substitution",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /\beval\s+/i,
description: "eval (arbitrary code execution)",
severity: "medium",
requiresExplicitApproval: false,
},
// Network data exfiltration
{
pattern: /\bcurl\s+.*-[a-zA-Z]*d\s+/i,
description: "curl POST data",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /\bwget\s+.*--post/i,
description: "wget POST",
severity: "medium",
requiresExplicitApproval: false,
},
// Process manipulation
{
pattern: /\bkillall\s+/i,
description: "killall (mass process termination)",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /\bpkill\s+-9\s+/i,
description: "pkill -9 (force kill)",
severity: "medium",
requiresExplicitApproval: false,
},
// File permission changes
{
pattern: /\bchmod\s+777\s+/i,
description: "chmod 777 (world-writable)",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /\bchmod\s+-[a-zA-Z]*R[a-zA-Z]*\s+/i,
description: "chmod recursive",
severity: "medium",
requiresExplicitApproval: false,
},
{
pattern: /\bchown\s+-[a-zA-Z]*R[a-zA-Z]*\s+/i,
description: "chown recursive",
severity: "medium",
requiresExplicitApproval: false,
},
];
/** Combined blocklist for evaluation */
const BLOCKLIST = [...CRITICAL_BLOCKLIST, ...HIGH_BLOCKLIST, ...MEDIUM_BLOCKLIST];
/**
* Configuration for blocklist evaluation behavior.
*/
export type BlocklistConfig = {
/** Block critical severity commands (default: true) */
blockCritical: boolean;
/** Block high severity commands (default: true) */
blockHigh: boolean;
/** Block medium severity commands (default: false, just warn) */
blockMedium: boolean;
/** Log all blocklist matches */
logMatches: boolean;
/** Allow bypass for explicitly approved commands */
allowExplicitApproval: boolean;
};
const DEFAULT_BLOCKLIST_CONFIG: BlocklistConfig = {
blockCritical: true,
blockHigh: true,
blockMedium: false,
logMatches: true,
allowExplicitApproval: false, // Even explicit approval cannot bypass critical/high by default
};
/**
* Evaluates a command against the blocklist.
* Returns whether the command should be blocked and why.
*/
export function evaluateBlocklist(
command: string,
config: Partial<BlocklistConfig> = {},
): BlocklistResult {
const cfg = { ...DEFAULT_BLOCKLIST_CONFIG, ...config };
const matchedPatterns: string[] = [];
let highestSeverity: "critical" | "high" | "medium" | null = null;
let blocked = false;
let reason: string | null = null;
for (const entry of BLOCKLIST) {
if (entry.pattern.test(command)) {
matchedPatterns.push(entry.description);
// Track highest severity
if (
highestSeverity === null ||
entry.severity === "critical" ||
(entry.severity === "high" && highestSeverity !== "critical")
) {
highestSeverity = entry.severity;
}
// Determine if we should block
if (entry.severity === "critical" && cfg.blockCritical) {
blocked = true;
reason = `BLOCKED: ${entry.description} (critical severity)`;
} else if (entry.severity === "high" && cfg.blockHigh) {
blocked = true;
reason = `BLOCKED: ${entry.description} (high severity)`;
} else if (entry.severity === "medium" && cfg.blockMedium) {
blocked = true;
reason = `BLOCKED: ${entry.description} (medium severity)`;
}
}
}
// Log matches if configured
if (cfg.logMatches && matchedPatterns.length > 0) {
const logLevel = blocked ? "error" : highestSeverity === "medium" ? "warn" : "error";
const preview = command.length > 80 ? command.slice(0, 80) + "..." : command;
if (logLevel === "error") {
log.error("Command blocklist match", {
blocked,
severity: highestSeverity,
patterns: matchedPatterns,
command: preview,
});
} else {
log.warn("Command blocklist match", {
blocked,
severity: highestSeverity,
patterns: matchedPatterns,
command: preview,
});
}
}
return {
blocked,
reason,
matchedPatterns,
severity: highestSeverity,
};
}
/**
* Quick check if a command is on the blocklist (any severity).
* Use evaluateBlocklist for full analysis.
*/
export function isOnBlocklist(command: string): boolean {
return BLOCKLIST.some((entry) => entry.pattern.test(command));
}
/**
* Returns a human-readable explanation of why a command was blocked.
*/
export function formatBlocklistReason(result: BlocklistResult): string {
if (!result.blocked) {
return "Command is allowed";
}
const lines = [
`Command blocked due to security policy.`,
`Severity: ${result.severity}`,
`Matched patterns:`,
...result.matchedPatterns.map((p) => ` - ${p}`),
];
if (result.severity === "critical") {
lines.push("");
lines.push("Critical severity commands cannot be approved and are always blocked.");
lines.push("These commands can cause catastrophic, irreversible system damage.");
} else if (result.severity === "high") {
lines.push("");
lines.push("High severity commands require explicit user approval.");
lines.push("If you need to run this command, please do so directly in your terminal.");
}
return lines.join("\n");
}
/**
* Get all blocklist patterns for documentation/display purposes.
*/
export function getBlocklistPatterns(): Array<{
pattern: string;
description: string;
severity: "critical" | "high" | "medium";
}> {
return BLOCKLIST.map((entry) => ({
pattern: entry.pattern.source,
description: entry.description,
severity: entry.severity,
}));
}