test_gsb_reconcile_security.py python
172 lines 6.3 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Security tests for §GSB C0 reconcile (§GSB.8 security tier).
2
3 (1) Shell-metacharacter branch names stay quoted data through the reconcile
4 probes, tip updates, and uniquify; (2) ``git-only`` collision runs zero muse
5 argv; (3) ``muse-only`` collision runs zero git/gh argv; (4) no ``checkout
6 --force`` in the default FF / uniquify / ensure / rollback paths — the
7 ancestor-validated tip moves remain allowed.
8 """
9
10 from __future__ import annotations
11
12 from datetime import date
13 from pathlib import Path
14
15 from adapters.runner import quote_arg
16 from cli.kit_root import kit_root
17 from tests.support import (
18 BranchStateRunner,
19 adapter_for,
20 gsw_runner,
21 run_cli,
22 seed_gsw_repo,
23 )
24 from tools.governance_hygiene.engine import BranchState, _reconcile_feature_branch
25
26 HOSTILE_BRANCH = "feat/x; touch /tmp/pwned $(id)"
27
28
29 def _feature_branch() -> str:
30 return f"feat/governance-sync-{date.today().isoformat()}"
31
32
33 def test_hostile_branch_name_stays_quoted_through_reconcile(
34 git_only_config, repo_root
35 ) -> None:
36 """Branch names from config patterns are data — quoted argv in every
37 reconcile probe and tip update, never interpolated."""
38 runner = BranchStateRunner(
39 str(repo_root),
40 existing_git_branches={HOSTILE_BRANCH},
41 git_tips={HOSTILE_BRANCH: "stale1", "main": "tip2"},
42 git_ancestors={"tip2": {"stale1"}},
43 )
44 adapter = adapter_for(git_only_config, repo_root, runner)
45 state = BranchState(git_branch="main", muse_branch=None)
46 reconciled, error = _reconcile_feature_branch(
47 adapter, runner, repo_root, git_only_config, HOSTILE_BRANCH, state
48 )
49 assert error is None
50 assert reconciled == HOSTILE_BRANCH
51 assert runner.git_tips[HOSTILE_BRANCH] == "tip2"
52 touched = [c for c, _ in runner.calls if "feat/x" in c]
53 assert touched
54 for command in touched:
55 # The metacharacters never appear outside a quoted argument.
56 stripped = command.replace(quote_arg(HOSTILE_BRANCH), "").replace(
57 quote_arg("refs/heads/" + HOSTILE_BRANCH), ""
58 )
59 assert "; touch" not in stripped
60 assert "$(id)" not in stripped
61
62
63 def test_hostile_branch_name_stays_quoted_through_uniquify(
64 git_only_config, repo_root
65 ) -> None:
66 runner = BranchStateRunner(
67 str(repo_root),
68 existing_git_branches={HOSTILE_BRANCH},
69 git_tips={HOSTILE_BRANCH: "divergent", "main": "tip2"},
70 )
71 adapter = adapter_for(git_only_config, repo_root, runner)
72 state = BranchState(git_branch="main", muse_branch=None)
73 reconciled, error = _reconcile_feature_branch(
74 adapter, runner, repo_root, git_only_config, HOSTILE_BRANCH, state
75 )
76 assert error is None
77 assert reconciled == f"{HOSTILE_BRANCH}-2"
78 for command, _ in runner.calls:
79 if "feat/x" not in command:
80 continue
81 stripped = command
82 for name in (reconciled, HOSTILE_BRANCH):
83 stripped = stripped.replace(quote_arg("refs/heads/" + name), "").replace(
84 quote_arg(name), ""
85 )
86 assert "; touch" not in stripped
87 assert "$(id)" not in stripped
88
89
90 def test_git_only_collision_write_zero_muse_argv(tmp_path: Path) -> None:
91 branch = _feature_branch()
92 seed_gsw_repo(tmp_path, "git-only")
93 runner = gsw_runner(
94 tmp_path,
95 "git-only",
96 existing_git_branches={branch},
97 git_tips={branch: "stale1"},
98 git_ancestors={"feedface": {"stale1"}},
99 )
100 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
101 assert code == 0
102 assert not any(c.startswith("muse") for c, _ in runner.calls)
103
104
105 def test_muse_only_collision_write_zero_git_argv(tmp_path: Path) -> None:
106 branch = _feature_branch()
107 seed_gsw_repo(tmp_path, "muse-only")
108 runner = gsw_runner(
109 tmp_path,
110 "muse-only",
111 existing_muse_branches={branch},
112 muse_tips={branch: "sha256:stale"},
113 muse_ancestors={"sha256:musetip": {"sha256:stale"}},
114 )
115 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
116 assert code == 0
117 assert not any(c.startswith(("git ", "gh ")) for c, _ in runner.calls)
118
119
120 def test_no_checkout_force_in_ff_uniquify_ensure_or_rollback(tmp_path: Path) -> None:
121 """§GSB.6: --force never appears — FF success, uniquify success, and the
122 rollback path after an induced commit failure all stay force-free."""
123 branch = _feature_branch()
124
125 # FF success path.
126 seed_gsw_repo(tmp_path, "muse+git-mirror")
127 ff_runner = gsw_runner(
128 tmp_path,
129 "muse+git-mirror",
130 existing_git_branches={branch},
131 existing_muse_branches={branch},
132 git_tips={branch: "gitstale"},
133 muse_tips={branch: "sha256:stale"},
134 git_ancestors={"feedface": {"gitstale"}},
135 muse_ancestors={"sha256:musetip": {"sha256:stale"}},
136 )
137 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=ff_runner, kit=kit_root())
138 assert code == 0
139 assert not any("--force" in c for c, _ in ff_runner.calls)
140
141 # Uniquify success path.
142 seed_gsw_repo(tmp_path, "muse+git-mirror")
143 uniq_runner = gsw_runner(
144 tmp_path,
145 "muse+git-mirror",
146 existing_git_branches={branch},
147 existing_muse_branches={branch},
148 git_tips={branch: "divergent-git"},
149 muse_tips={branch: "sha256:divergent"},
150 )
151 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=uniq_runner, kit=kit_root())
152 assert code == 0
153 assert uniq_runner.git_branch == f"{branch}-2"
154 assert not any("--force" in c for c, _ in uniq_runner.calls)
155
156 # Rollback path after induced commit failure with a collision present.
157 seed_gsw_repo(tmp_path, "muse+git-mirror")
158 failing = gsw_runner(
159 tmp_path,
160 "muse+git-mirror",
161 muse_commit_fails=True,
162 existing_git_branches={branch},
163 existing_muse_branches={branch},
164 git_tips={branch: "gitstale"},
165 muse_tips={branch: "sha256:stale"},
166 git_ancestors={"feedface": {"gitstale"}},
167 muse_ancestors={"sha256:musetip": {"sha256:stale"}},
168 )
169 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=failing, kit=kit_root())
170 assert code == 2
171 assert failing.git_branch == "main" and failing.muse_branch == "main"
172 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 1 day ago