import pytest from nisse_core.authority.actions import ( CONTAINED, DANGEROUS, HARMLESS, REFUSED, UNKNOWN, Action, Command, Verdict, classify_shell, inside, resolve, split_shell, unwrap, worst, ) def test_an_approval_binds_to_the_exact_tool_and_arguments() -> None: a = Action("run_command", {"command": "git push"}) assert a.key() == Action("run_command", {"command": "git push"}).key() assert a.key() != Action("run_command", {"command": "git push -f"}).key() assert a.key() != Action("write_file", {"command": "git push"}).key() def test_the_worst_verdict_wins() -> None: verdicts = [ Verdict(HARMLESS, "a", ""), Verdict(DANGEROUS, "b", ""), Verdict(UNKNOWN, "c", ""), ] assert worst(verdicts).rule == "b" assert worst([Verdict(DANGEROUS, "d", ""), Verdict(REFUSED, "r", "")]).rule == "r" assert worst([]).effect == HARMLESS @pytest.mark.parametrize( ("line", "commands"), [ ("pytest -q", [(["pytest", "-q"], [])]), ( "pytest 2>/dev/null && git status", [(["pytest"], ["/dev/null"]), (["git", "status"], [])], ), ( "ls | grep x; echo hi >> log", [(["ls"], []), (["grep", "x"], []), (["echo", "hi"], ["log"])], ), ("make test 2>&1 | tail -5", [(["make", "test"], []), (["tail", "-5"], [])]), ("echo 'a > b'", [(["echo", "a > b"], [])]), ("cd sub\nls", [(["cd", "sub"], []), (["ls"], [])]), ("ls # a comment", [(["ls"], [])]), ], ) def test_a_line_splits_into_its_commands(line: str, commands: list) -> None: assert split_shell(line) == commands @pytest.mark.parametrize( "line", ["echo $(whoami)", "echo `id`", "cat < None: assert split_shell(line) is None @pytest.mark.parametrize( ("argv", "command"), [ (["sudo", "apt-get", "install", "x"], ["apt-get", "install", "x"]), (["FOO=1", "BAR=2", "pytest"], ["pytest"]), (["env", "-i", "A=1", "make"], ["make"]), (["timeout", "60", "pytest", "-x"], ["pytest", "-x"]), (["timeout", "-k", "5", "60", "pytest"], ["pytest"]), (["timeout", "--signal=KILL", "60", "make"], ["make"]), (["nohup", "python", "serve.py"], ["python", "serve.py"]), ], ) def test_wrappers_are_seen_through(argv: list[str], command: list[str]) -> None: assert unwrap(argv) == command def test_paths_resolve_without_touching_the_filesystem() -> None: assert resolve("a/../b", "/w") == "/w/b" assert resolve("/etc/passwd", "/w") == "/etc/passwd" assert resolve("$HOME/x", "/w") is None assert resolve("~/x", "/w") is None assert resolve("x", None) is None assert inside("/w/b", "/w") and inside("/w", "/w/") assert not inside("/wx", "/w") and not inside(None, "/w") def judge(command: Command) -> Verdict: """A toy rule set: rm is dangerous outside /w, ls harmless.""" if command.argv[0] == "ls": return Verdict(HARMLESS, "ls", "") if command.argv[0] == "rm": target = resolve(command.argv[-1], command.cwd) if inside(target, "/w"): return Verdict(CONTAINED, "rm-inside", "") return Verdict(DANGEROUS, "rm-outside", "") return Verdict(UNKNOWN, "no-rule", "") @pytest.mark.parametrize( ("line", "rule"), [ ("ls && rm x", "rm-inside"), ("cd /tmp && rm x", "rm-outside"), ("cd sub && rm x", "rm-inside"), ("cd && rm x", "rm-outside"), # cd with no argument: home, unknown ("bash -c 'cd /etc; rm passwd'", "rm-outside"), ("sh -c 'ls'", "ls"), ("ls; frobnicate", "no-rule"), ("ls $(rm -rf /)", "unreadable"), ], ) def test_a_line_is_as_bad_as_its_worst_command(line: str, rule: str) -> None: assert classify_shell(line, "/w", judge).rule == rule