import { describe, expect, it } from "vitest"; // ─── git tool security tests ──────────────────────────────────────────── // re-create the validation logic from git.ts for unit testing const AUTH_REQUIRED_REDIRECT: Record = { push: "Use push_branch tool instead.", fetch: "Use git_fetch tool instead.", pull: "Use git_fetch + git merge instead.", clone: "Repository already cloned. Use checkout_pr for PR branches.", }; // only blocked when shell is disabled — in restricted mode the agent has shell // in a stripped sandbox so blocking these is redundant const NOSHELL_BLOCKED_SUBCOMMANDS: Record = { config: "Blocked: git config can set up filter drivers or hooks that execute arbitrary code.", submodule: "Blocked: git submodule can reference malicious repositories and execute code on update.", "update-index": "Blocked: git update-index can modify index entries in ways that bypass file protections.", "filter-branch": "Blocked: git filter-branch executes arbitrary code on repository history.", replace: "Blocked: git replace can redirect object lookups.", rebase: "Blocked: git rebase --exec can execute arbitrary shell commands.", bisect: "Blocked: git bisect run can execute arbitrary shell commands.", }; const NOSHELL_BLOCKED_ARGS = ["--exec", "--extcmd", "--upload-pack", "--receive-pack"]; type ShellPermission = "disabled" | "restricted" | "enabled"; type ValidateGitParams = { subcommand: string; args: string[]; shellPermission: ShellPermission; }; // matches the arkregex pattern used in the Git schema const SUBCOMMAND_PATTERN = /^[a-z][a-z0-9-]*$/; // mirrors the validation logic in GitTool.execute function validateGitCommand(params: ValidateGitParams): string | null { // schema-level regex validation — applies in ALL modes if (!SUBCOMMAND_PATTERN.test(params.subcommand)) { return `subcommand must be Git subcommand (was "${params.subcommand}")`; } const redirect = AUTH_REQUIRED_REDIRECT[params.subcommand]; if (redirect) { return `git ${params.subcommand} requires authentication. ${redirect}`; } // subcommand and arg blocking only applies when shell is disabled if (params.shellPermission === "disabled") { const blocked = NOSHELL_BLOCKED_SUBCOMMANDS[params.subcommand]; if (blocked) { return blocked; } for (const arg of params.args) { const isBlocked = NOSHELL_BLOCKED_ARGS.some( (flag) => arg === flag || arg.startsWith(flag + "=") ); if (isBlocked) { return `Blocked: '${arg}' flag can execute arbitrary code and is not allowed.`; } } } return null; // no error } describe("git tool security - subcommand regex validation", () => { it("blocks -c flag as subcommand in ALL modes (alias injection)", () => { const modes: ShellPermission[] = ["disabled", "restricted", "enabled"]; for (const mode of modes) { const error = validateGitCommand({ subcommand: "-c", args: ["alias.x=!evil-command", "x"], shellPermission: mode, }); expect(error).toContain("Git subcommand"); } }); it("blocks --exec-path as subcommand", () => { const error = validateGitCommand({ subcommand: "--exec-path=/malicious", args: ["status"], shellPermission: "disabled", }); expect(error).toContain("Git subcommand"); }); it("blocks -C as subcommand (change directory)", () => { const error = validateGitCommand({ subcommand: "-C", args: ["/tmp", "init"], shellPermission: "disabled", }); expect(error).toContain("Git subcommand"); }); it("blocks --config-env as subcommand", () => { const error = validateGitCommand({ subcommand: "--config-env", args: ["core.pager=PATH", "log"], shellPermission: "disabled", }); expect(error).toContain("Git subcommand"); }); it("blocks all flags starting with - as subcommand", () => { const flags = ["-c", "-C", "-p", "--paginate", "--git-dir", "--work-tree", "--bare"]; for (const flag of flags) { const error = validateGitCommand({ subcommand: flag, args: [], shellPermission: "disabled", }); expect(error).toContain("Git subcommand"); } }); it("blocks uppercase subcommands", () => { const error = validateGitCommand({ subcommand: "STATUS", args: [], shellPermission: "disabled", }); expect(error).toContain("Git subcommand"); }); it("blocks subcommands with special characters", () => { const bad = ["git;evil", "status$(cmd)", "log|cat", "diff&bg"]; for (const sub of bad) { const error = validateGitCommand({ subcommand: sub, args: [], shellPermission: "disabled", }); expect(error).toContain("Git subcommand"); } }); it("allows valid subcommands", () => { const safe = ["status", "log", "diff", "show", "branch", "tag", "stash", "blame"]; for (const sub of safe) { const error = validateGitCommand({ subcommand: sub, args: [], shellPermission: "disabled", }); expect(error).toBeNull(); } }); it("allows hyphenated subcommands", () => { const safe = ["filter-branch", "update-index", "ls-remote", "ls-files", "rev-parse"]; for (const sub of safe) { const error = validateGitCommand({ subcommand: sub, args: [], shellPermission: "enabled", }); expect(error).toBeNull(); } }); }); describe("git tool security - blocked subcommands (disabled mode only)", () => { it("blocks config in disabled mode", () => { const error = validateGitCommand({ subcommand: "config", args: ["core.hooksPath", "./hooks"], shellPermission: "disabled", }); expect(error).toContain("git config"); }); it("allows config in restricted mode (agent has shell)", () => { const error = validateGitCommand({ subcommand: "config", args: ["filter.evil.clean", "bash -c 'evil'"], shellPermission: "restricted", }); expect(error).toBeNull(); }); it("blocks submodule in disabled mode", () => { const error = validateGitCommand({ subcommand: "submodule", args: ["add", "https://evil.com/repo.git"], shellPermission: "disabled", }); expect(error).toContain("submodule"); }); it("allows submodule in restricted mode", () => { const error = validateGitCommand({ subcommand: "submodule", args: ["add", "https://example.com/repo.git"], shellPermission: "restricted", }); expect(error).toBeNull(); }); it("blocks rebase in disabled mode", () => { const error = validateGitCommand({ subcommand: "rebase", args: ["--exec", "evil-command", "HEAD~1"], shellPermission: "disabled", }); expect(error).toContain("rebase"); }); it("allows rebase in restricted mode", () => { const error = validateGitCommand({ subcommand: "rebase", args: ["main"], shellPermission: "restricted", }); expect(error).toBeNull(); }); it("blocks bisect in disabled mode", () => { const error = validateGitCommand({ subcommand: "bisect", args: ["run", "evil-command"], shellPermission: "disabled", }); expect(error).toContain("bisect"); }); it("blocks filter-branch in disabled mode", () => { const error = validateGitCommand({ subcommand: "filter-branch", args: ["--tree-filter", "evil-command", "HEAD"], shellPermission: "disabled", }); expect(error).toContain("filter-branch"); }); it("allows blocked subcommands in enabled mode", () => { const blocked = ["config", "submodule", "rebase", "bisect", "filter-branch"]; for (const sub of blocked) { const error = validateGitCommand({ subcommand: sub, args: [], shellPermission: "enabled", }); expect(error).toBeNull(); } }); it("allows blocked subcommands in restricted mode (stripped env is security boundary)", () => { const blocked = ["config", "submodule", "rebase", "bisect", "filter-branch"]; for (const sub of blocked) { const error = validateGitCommand({ subcommand: sub, args: [], shellPermission: "restricted", }); expect(error).toBeNull(); } }); }); describe("git tool security - blocked arg flags (disabled mode only)", () => { it("blocks --exec in args (disabled)", () => { const error = validateGitCommand({ subcommand: "log", args: ["--exec", "evil-command"], shellPermission: "disabled", }); expect(error).toContain("arbitrary code"); }); it("blocks --exec= in args (disabled)", () => { const error = validateGitCommand({ subcommand: "log", args: ["--exec=evil-command"], shellPermission: "disabled", }); expect(error).toContain("arbitrary code"); }); it("blocks --extcmd in args (disabled)", () => { const error = validateGitCommand({ subcommand: "difftool", args: ["--extcmd=evil-command", "HEAD~1"], shellPermission: "disabled", }); expect(error).toContain("arbitrary code"); }); it("blocks --upload-pack in args (disabled)", () => { const error = validateGitCommand({ subcommand: "ls-remote", args: ["--upload-pack=evil"], shellPermission: "disabled", }); expect(error).toContain("arbitrary code"); }); it("allows --exec in restricted mode (agent has shell)", () => { const error = validateGitCommand({ subcommand: "rebase", args: ["--exec", "npm test", "HEAD~1"], shellPermission: "restricted", }); expect(error).toBeNull(); }); it("allows --extcmd in restricted mode", () => { const error = validateGitCommand({ subcommand: "difftool", args: ["--extcmd=less"], shellPermission: "restricted", }); expect(error).toBeNull(); }); it("allows blocked args in enabled mode", () => { const error = validateGitCommand({ subcommand: "difftool", args: ["--extcmd=less"], shellPermission: "enabled", }); expect(error).toBeNull(); }); it("allows normal args in disabled mode", () => { const error = validateGitCommand({ subcommand: "log", args: ["--oneline", "-10", "--format=%H %s"], shellPermission: "disabled", }); expect(error).toBeNull(); }); it("does not false-positive on --exclude-standard (not --exec)", () => { const error = validateGitCommand({ subcommand: "ls-files", args: ["--exclude-standard"], shellPermission: "disabled", }); expect(error).toBeNull(); }); it("does not false-positive on --execute (not --exec=)", () => { const error = validateGitCommand({ subcommand: "log", args: ["--execute-something"], shellPermission: "disabled", }); expect(error).toBeNull(); }); it("does not false-positive on -c (combined diff format for git log)", () => { const error = validateGitCommand({ subcommand: "log", args: ["-c", "--oneline"], shellPermission: "disabled", }); expect(error).toBeNull(); }); }); describe("git tool security - auth redirect", () => { it("redirects push in all modes", () => { const modes: ShellPermission[] = ["disabled", "restricted", "enabled"]; for (const mode of modes) { const error = validateGitCommand({ subcommand: "push", args: [], shellPermission: mode, }); expect(error).toContain("authentication"); } }); it("redirects fetch", () => { const error = validateGitCommand({ subcommand: "fetch", args: [], shellPermission: "enabled", }); expect(error).toContain("authentication"); }); it("redirects pull", () => { const error = validateGitCommand({ subcommand: "pull", args: [], shellPermission: "enabled", }); expect(error).toContain("authentication"); }); it("redirects clone", () => { const error = validateGitCommand({ subcommand: "clone", args: [], shellPermission: "enabled", }); expect(error).toContain("authentication"); }); }); // ─── file tool security tests ─────────────────────────────────────────── const GIT_INTERPRETED_FILES = [".gitattributes", ".gitmodules"]; type ValidateWritePathResult = { allowed: boolean; error?: string; }; // simplified path validation that mirrors the security checks in file.ts // without requiring real filesystem operations (for unit testing) function validateWritePathSecurity( relative: string, shellPermission: ShellPermission ): ValidateWritePathResult { if (relative === ".git" || relative.startsWith(".git/")) { return { allowed: false, error: `writing to .git is not allowed: ${relative}` }; } // only blocked when shell is disabled if (shellPermission === "disabled") { const basename = relative.split("/").pop() || ""; if (GIT_INTERPRETED_FILES.includes(basename)) { return { allowed: false, error: `writing to ${basename} is not allowed when shell is ${shellPermission}`, }; } } return { allowed: true }; } describe("file tool security - .git protection", () => { it("blocks .git directory in all modes", () => { const modes: ShellPermission[] = ["disabled", "restricted", "enabled"]; for (const mode of modes) { const result = validateWritePathSecurity(".git", mode); expect(result.allowed).toBe(false); } }); it("blocks .git/config", () => { const result = validateWritePathSecurity(".git/config", "enabled"); expect(result.allowed).toBe(false); }); it("blocks .git/hooks/pre-commit", () => { const result = validateWritePathSecurity(".git/hooks/pre-commit", "enabled"); expect(result.allowed).toBe(false); }); it("blocks deeply nested .git paths", () => { const result = validateWritePathSecurity(".git/objects/ab/cd1234", "enabled"); expect(result.allowed).toBe(false); }); }); describe("file tool security - git-interpreted files (disabled mode only)", () => { it("blocks .gitattributes in disabled mode", () => { const result = validateWritePathSecurity(".gitattributes", "disabled"); expect(result.allowed).toBe(false); expect(result.error).toContain(".gitattributes"); }); it("allows .gitattributes in restricted mode (agent has shell)", () => { const result = validateWritePathSecurity(".gitattributes", "restricted"); expect(result.allowed).toBe(true); }); it("allows .gitattributes in enabled mode", () => { const result = validateWritePathSecurity(".gitattributes", "enabled"); expect(result.allowed).toBe(true); }); it("blocks .gitmodules in disabled mode", () => { const result = validateWritePathSecurity(".gitmodules", "disabled"); expect(result.allowed).toBe(false); }); it("allows .gitmodules in restricted mode", () => { const result = validateWritePathSecurity(".gitmodules", "restricted"); expect(result.allowed).toBe(true); }); it("allows .gitmodules in enabled mode", () => { const result = validateWritePathSecurity(".gitmodules", "enabled"); expect(result.allowed).toBe(true); }); it("blocks subdirectory .gitattributes in disabled mode", () => { const result = validateWritePathSecurity("src/.gitattributes", "disabled"); expect(result.allowed).toBe(false); }); it("blocks deeply nested .gitattributes in disabled mode", () => { const result = validateWritePathSecurity("a/b/c/.gitattributes", "disabled"); expect(result.allowed).toBe(false); }); it("allows subdirectory .gitattributes in restricted mode", () => { const result = validateWritePathSecurity("src/.gitattributes", "restricted"); expect(result.allowed).toBe(true); }); it("allows normal files in all modes", () => { const files = ["README.md", "src/index.ts", "package.json", ".env", ".gitignore"]; const modes: ShellPermission[] = ["disabled", "restricted", "enabled"]; for (const file of files) { for (const mode of modes) { const result = validateWritePathSecurity(file, mode); expect(result.allowed).toBe(true); } } }); it("does not block .gitignore (not a code execution vector)", () => { const result = validateWritePathSecurity(".gitignore", "disabled"); expect(result.allowed).toBe(true); }); it("does not block .gitkeep (not a code execution vector)", () => { const result = validateWritePathSecurity("dir/.gitkeep", "disabled"); expect(result.allowed).toBe(true); }); }); // ─── dependency install security tests ────────────────────────────────── // mirrors the logic in dependencies.ts startInstallation() function shouldIgnoreScripts(shellPermission: ShellPermission): boolean { return shellPermission === "disabled"; } describe("dependency install - ignore-scripts logic", () => { it("ignoreScripts is true when shell is disabled", () => { expect(shouldIgnoreScripts("disabled")).toBe(true); }); it("ignoreScripts is false when shell is restricted (scripts run in stripped env)", () => { expect(shouldIgnoreScripts("restricted")).toBe(false); }); it("ignoreScripts is false when shell is enabled", () => { expect(shouldIgnoreScripts("enabled")).toBe(false); }); });