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:
parent
988950f845
commit
6a960c5137
@ -154,8 +154,9 @@ describe("exec approvals shell allowlist (chained commands)", () => {
|
||||
|
||||
it("rejects chained commands when any part is not allowlisted", () => {
|
||||
const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/obsidian-cli" }];
|
||||
// Use a non-blocklisted command to test allowlist rejection
|
||||
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,
|
||||
safeBins: new Set(),
|
||||
cwd: "/tmp",
|
||||
@ -164,6 +165,21 @@ describe("exec approvals shell allowlist (chained commands)", () => {
|
||||
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", () => {
|
||||
const allowlist: ExecAllowlistEntry[] = [{ pattern: "/usr/bin/echo" }];
|
||||
const result = evaluateShellAllowlist({
|
||||
|
||||
@ -5,6 +5,24 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
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 ExecSecurity = "deny" | "allowlist" | "full";
|
||||
@ -1045,10 +1063,13 @@ export type ExecAllowlistAnalysis = {
|
||||
allowlistSatisfied: boolean;
|
||||
allowlistMatches: ExecAllowlistEntry[];
|
||||
segments: ExecCommandSegment[];
|
||||
/** Blocklist evaluation result (if blocklist check was performed) */
|
||||
blocklistResult?: BlocklistResult;
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluates allowlist for shell commands (including &&, ||, ;) and returns analysis metadata.
|
||||
* Also checks the security blocklist FIRST to reject dangerous commands.
|
||||
*/
|
||||
export function evaluateShellAllowlist(params: {
|
||||
command: string;
|
||||
@ -1058,6 +1079,46 @@ export function evaluateShellAllowlist(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
skillBins?: Set<string>;
|
||||
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 {
|
||||
const chainParts = splitCommandChain(params.command);
|
||||
if (!chainParts) {
|
||||
|
||||
320
src/infra/exec-blocklist.test.ts
Normal file
320
src/infra/exec-blocklist.test.ts
Normal 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
514
src/infra/exec-blocklist.ts
Normal 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,
|
||||
}));
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user