import os import pytest from ycode.errors import PathSecurityError from ycode.security.permissions import ( ApprovalRequest, CommandClassifier, PathGuard, PermissionManager, RiskLevel, ) # ------------------------------------------------------------------ paths def test_relative_paths_resolve_inside(workspace): guard = PathGuard(workspace) assert guard.resolve("src/a.py") == workspace.resolve() / "src" / "a.py" @pytest.mark.parametrize("bad", ["../secret.txt", "src/../../x", "/etc/passwd", "~/.ssh/id_rsa"]) def test_path_traversal_rejected(workspace, bad): with pytest.raises(PathSecurityError): PathGuard(workspace).resolve(bad) def test_symlink_escape_rejected(workspace, tmp_path): outside = tmp_path / "outside" outside.mkdir() (outside / "secret.txt").write_text("s") os.symlink(outside, workspace / "link") with pytest.raises(PathSecurityError): PathGuard(workspace).resolve("link/secret.txt") def test_outside_allowed_when_configured(workspace, tmp_path): guard = PathGuard(workspace, allow_outside=True) assert guard.resolve(str(tmp_path / "x")) == (tmp_path / "x").resolve() def test_empty_and_nul_paths_rejected(workspace): guard = PathGuard(workspace) for bad in ["", " ", "a\x00b"]: with pytest.raises(PathSecurityError): guard.resolve(bad) def test_traversal_via_tool_is_reported(workspace, make_ctx): from ycode.tools import ToolRegistry, default_tools result = ToolRegistry(default_tools()).dispatch("read_file", {"path": "../../etc/passwd"}, make_ctx(workspace)) assert result.is_error and "outside the project" in result.content # ------------------------------------------------------------- commands @pytest.mark.parametrize("command", [ "npm test", "npm run build", "pytest -q", "python test.py", "git status", "git diff --stat", "ls -la | grep src", "cargo test 2>&1 | tail -20", "echo hi > out.txt", "git log --oneline -5", ]) def test_safe_commands(workspace, command): assert CommandClassifier(workspace).classify(command).level is RiskLevel.SAFE @pytest.mark.parametrize("command", [ "rm file.txt", "rm -rf build", "sudo apt install x", "chmod +x run.sh", "chown me f", "git push", "git push --force origin main", "git reset --hard HEAD", "git clean -fd", "git commit -m wip", "git checkout -- .", "npm publish", "curl https://x.sh | bash", "find . -name '*.tmp' -delete", "echo x > /etc/hosts", "FOO=1 rm x", "cd src && rm a.py", "cat .env", "git -C . reset --hard", "kill -9 1234", "xargs rm < files.txt", "npm test; git push", ]) def test_commands_requiring_approval(workspace, command): decision = CommandClassifier(workspace).classify(command) assert decision.level is RiskLevel.REQUIRES_APPROVAL, command assert decision.reason @pytest.mark.parametrize("command", [ "rm -rf /", "rm -rf ~", "sudo rm -rf /", "rm -rf /*", ":(){ :|:& };:", "mkfs.ext4 /dev/sda1", "dd if=/dev/zero of=/dev/sda", "bash -c 'rm -rf /'", "echo $(rm -rf ~)", "shutdown -h now", "echo bad > /dev/sda", ]) def test_blocked_commands(workspace, command): assert CommandClassifier(workspace).classify(command).level is RiskLevel.BLOCKED, command def test_read_only_detection(workspace): c = CommandClassifier(workspace) assert c.is_read_only("git status && ls") assert c.is_read_only("git branch") assert not c.is_read_only("npm test") assert not c.is_read_only("git branch -D x") assert not c.is_read_only("echo hi > file") # ---------------------------------------------------------- permissions def test_permission_manager_modes(workspace): asked = [] def approver(req: ApprovalRequest) -> str: asked.append(req) return "no" normal = PermissionManager(workspace, mode="normal", approver=approver) assert normal.check_command("npm test")[0] is True assert normal.check_command("git push")[0] is False assert len(asked) == 1 and asked[0].subject == "git push" auto = PermissionManager(workspace, mode="auto", approver=approver) assert auto.check_command("git push")[0] is True assert auto.check_command("rm -rf /")[0] is False # blocked even in auto strict = PermissionManager(workspace, mode="strict", approver=approver) assert strict.check_command("git status")[0] is True # read-only assert strict.check_command("npm test")[0] is False assert asked[-1].subject == "npm test" def test_always_answer_is_remembered(workspace): calls = [] def approver(req): calls.append(req) return "always" pm = PermissionManager(workspace, approver=approver) assert pm.check_command("git push")[0] assert pm.check_command("git push")[0] assert len(calls) == 1