test_gsw_write_security.py python
93 lines 4.0 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 22 hours ago
1 """Security tests for the §GSW write path (§GSW.10 security tier).
2
3 (1) Shell-metacharacter branch names stay quoted data; (2) git-only never
4 invokes muse; (3) muse-only never invokes git/gh; (4) no ``--force``
5 checkout in default success or rollback paths.
6 """
7
8 from __future__ import annotations
9
10 from pathlib import Path
11
12 from adapters.runner import quote_arg
13 from cli.kit_root import kit_root
14 from tests.support import adapter_for, gsw_runner, make_runner, ok, run_cli, seed_gsw_repo
15 from tools.governance_hygiene.engine import (
16 BranchState,
17 _ensure_feature_branch,
18 _restore_branch_state,
19 )
20
21 HOSTILE_BRANCH = "feat/x; touch /tmp/pwned $(id)"
22
23
24 def test_hostile_branch_name_is_quoted_in_ensure(git_only_config, repo_root) -> None:
25 """Branch names from config patterns are data — quoted argv, never interpolated."""
26 runner = make_runner({"git checkout": ok("")})
27 adapter = adapter_for(git_only_config, repo_root, runner)
28 state = BranchState(git_branch="main", muse_branch=None)
29 error = _ensure_feature_branch(
30 adapter, runner, repo_root, git_only_config, HOSTILE_BRANCH, state
31 )
32 assert error is None
33 checkout_calls = [c for c, _ in runner.calls if "checkout" in c]
34 assert checkout_calls
35 for command in checkout_calls:
36 assert quote_arg(HOSTILE_BRANCH) in command
37 # The metacharacters never appear outside the quoted argument.
38 assert "; touch" not in command.replace(quote_arg(HOSTILE_BRANCH), "")
39
40
41 def test_hostile_branch_name_is_quoted_in_muse_ensure_and_restore(
42 muse_only_config, repo_root
43 ) -> None:
44 root = str(repo_root)
45 runner = make_runner({f"muse -C {root} checkout": ok(""), f"muse -C {root} rev-parse": ok("main")})
46 adapter = adapter_for(muse_only_config, repo_root, runner)
47 state = BranchState(git_branch=None, muse_branch="main")
48 error = _ensure_feature_branch(
49 adapter, runner, repo_root, muse_only_config, HOSTILE_BRANCH, state
50 )
51 assert error is None
52 hostile_state = BranchState(git_branch=None, muse_branch=HOSTILE_BRANCH)
53 _restore_branch_state(muse_only_config, adapter, runner, repo_root, hostile_state)
54 checkout_calls = [c for c, _ in runner.calls if "checkout" in c]
55 assert checkout_calls
56 for command in checkout_calls:
57 assert quote_arg(HOSTILE_BRANCH) in command
58 assert "; touch" not in command.replace(quote_arg(HOSTILE_BRANCH), "")
59
60
61 def test_git_only_write_call_log_has_zero_muse_argv(tmp_path: Path) -> None:
62 seed_gsw_repo(tmp_path, "git-only")
63 runner = gsw_runner(tmp_path, "git-only", git_dirty=True)
64 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
65 assert code == 0
66 assert not any(c.startswith("muse") for c, _ in runner.calls)
67
68
69 def test_muse_only_write_call_log_has_zero_git_argv(tmp_path: Path) -> None:
70 seed_gsw_repo(tmp_path, "muse-only")
71 runner = gsw_runner(tmp_path, "muse-only", muse_dirty=True)
72 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
73 assert code == 0
74 assert not any(c.startswith(("git ", "gh ")) for c, _ in runner.calls)
75
76
77 def test_no_force_checkout_in_success_or_rollback_paths(tmp_path: Path) -> None:
78 """§GSW.4.3 / §GSW.6.2: --force is forbidden as default in both directions."""
79 seed_gsw_repo(tmp_path, "muse+git-mirror")
80
81 # Success path with dirty tree on both histories.
82 success = gsw_runner(tmp_path, "muse+git-mirror", git_dirty=True, muse_dirty=True)
83 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=success, kit=kit_root())
84 assert code == 0
85 assert not any("--force" in c for c, _ in success.calls)
86
87 # Rollback path after induced commit failure.
88 seed_gsw_repo(tmp_path, "muse+git-mirror")
89 failing = gsw_runner(tmp_path, "muse+git-mirror", muse_commit_fails=True)
90 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=failing, kit=kit_root())
91 assert code == 2
92 assert failing.git_branch == "main" and failing.muse_branch == "main"
93 assert not any("--force" in c for c, _ in failing.calls)
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 22 hours ago