import test from "node:test"; import assert from "node:assert/strict"; import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { resolve } from "node:path"; import { SecurityRulesEngine, SecureCommandExecutor, SecurityViolationError, parseShellScript, } from "../src/security/index.js"; test("shell parser extracts Command and Word nodes across chained expressions", () => { const parsed = parseShellScript("FOO=bar git status && npm test | cat > logs/output.txt"); assert.equal(parsed.commandCount, 3); assert.deepEqual( parsed.commands.map((command) => command.binary), ["git", "npm", "cat"], ); const gitCommand = parsed.commands[0]; assert.ok(gitCommand); assert.equal(gitCommand.assignments[0]?.key, "FOO"); assert.deepEqual(gitCommand.args, ["status"]); const catCommand = parsed.commands[2]; assert.ok(catCommand); assert.deepEqual(catCommand.redirects, ["logs/output.txt"]); }); test("rules engine enforces binary allowlist, tool policy, and path boundaries", async () => { const worktreeRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-worktree-")); const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-state-")); const projectContextPath = resolve(stateRoot, "project-context.json"); const rules = new SecurityRulesEngine({ allowedBinaries: ["git", "npm", "cat"], worktreeRoot, protectedPaths: [stateRoot, projectContextPath], requireCwdWithinWorktree: true, rejectRelativePathTraversal: true, enforcePathBoundaryOnArguments: true, allowedEnvAssignments: [], blockedEnvAssignments: [], }); const allowed = rules.validateShellCommand({ command: "git status && npm test | cat > logs/output.txt", cwd: worktreeRoot, toolClearance: { allowlist: ["git", "npm", "cat"], banlist: [], }, }); assert.equal(allowed.parsed.commandCount, 3); assert.throws( () => rules.validateShellCommand({ command: "cat ../secrets.txt", cwd: worktreeRoot, }), (error) => error instanceof SecurityViolationError && error.code === "PATH_TRAVERSAL_BLOCKED", ); assert.throws( () => rules.validateShellCommand({ command: "git status", cwd: stateRoot, }), (error) => error instanceof SecurityViolationError && error.code === "CWD_OUTSIDE_WORKTREE", ); }); test("secure executor runs with explicit env policy", async () => { const worktreeRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-exec-")); const rules = new SecurityRulesEngine({ allowedBinaries: ["echo"], worktreeRoot, protectedPaths: [], requireCwdWithinWorktree: true, rejectRelativePathTraversal: true, enforcePathBoundaryOnArguments: true, allowedEnvAssignments: [], blockedEnvAssignments: [], }); const executor = new SecureCommandExecutor({ rulesEngine: rules, timeoutMs: 2000, envPolicy: { inherit: ["PATH", "HOME"], scrub: ["SECRET_VALUE"], inject: { SAFE_VALUE: "ok", }, }, }); let streamedStdout = ""; const result = await executor.execute({ command: "echo \"$SAFE_VALUE|$SECRET_VALUE\"", cwd: worktreeRoot, baseEnv: { PATH: process.env.PATH, HOME: process.env.HOME, SECRET_VALUE: "hidden", }, onStdoutChunk: (chunk) => { streamedStdout += chunk; }, }); assert.equal(result.exitCode, 0); assert.equal(result.stdout, "ok|\n"); assert.equal(streamedStdout, result.stdout); });