test_muse_sync_security.py
python
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago
| 1 | """Security tests for the muse-sync hard gate (§KH2.8 security tier).""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from pathlib import Path |
| 6 | |
| 7 | from adapters.types import StatusResult |
| 8 | from tests.support import ( |
| 9 | FIXTURES, |
| 10 | load_fixture_config, |
| 11 | make_runner, |
| 12 | ok, |
| 13 | run_cli, |
| 14 | seed_muse_substrate, |
| 15 | ) |
| 16 | from tools.muse_sync import check_muse_sync |
| 17 | |
| 18 | |
| 19 | def test_fails_closed_when_dirty_state_unreadable(tmp_path: Path) -> None: |
| 20 | """Rule §KH2.4: never optimistically report 'synced' when the flags could not be read.""" |
| 21 | config = load_fixture_config(tmp_path, "config-muse-git-mirror.yaml") |
| 22 | status = StatusResult(regime="muse+git-mirror", dirty=False, branch="main", muse_dirty=None, git_dirty=None) |
| 23 | report = check_muse_sync(config, status) |
| 24 | assert report.state == "unreadable" |
| 25 | assert not report.ok |
| 26 | |
| 27 | |
| 28 | def test_remediation_text_is_a_static_string_never_shell_invoked(tmp_path: Path) -> None: |
| 29 | """The remediation hint must be advisory text only — the kit never executes it.""" |
| 30 | config = load_fixture_config(tmp_path, "config-muse-git-mirror.yaml") |
| 31 | status = StatusResult( |
| 32 | regime="muse+git-mirror", dirty=True, branch="main", muse_dirty=True, git_dirty=False |
| 33 | ) |
| 34 | report = check_muse_sync(config, status) |
| 35 | assert report.remediation == 'muse code add -A && muse commit -m "<message>"' |
| 36 | # No shell metacharacter beyond the literal, static advisory string is ever built from |
| 37 | # untrusted input — report.message/remediation never interpolate repo content. |
| 38 | assert "$(" not in report.remediation |
| 39 | assert "`" not in report.remediation |
| 40 | |
| 41 | |
| 42 | def test_muse_sync_payload_never_leaks_command_output_or_paths(tmp_path: Path) -> None: |
| 43 | """The muse_sync report/payload carries only fixed enum-like state + static text — no |
| 44 | raw command stdout, file paths, or secret-shaped values ever flow into it.""" |
| 45 | root = str(tmp_path.resolve()) |
| 46 | runner = make_runner( |
| 47 | { |
| 48 | f"muse -C {root} rev-parse --abbrev-ref HEAD": ok("main"), |
| 49 | f"muse -C {root} status --json": ok( |
| 50 | '{"dirty": true, "secret_token": "sk-should-never-surface-1234567890"}' |
| 51 | ), |
| 52 | "git rev-parse --abbrev-ref HEAD": ok("main"), |
| 53 | "git status --porcelain": ok(""), |
| 54 | } |
| 55 | ) |
| 56 | assert ( |
| 57 | run_cli( |
| 58 | ["init", "--from-config", str(FIXTURES / "config-muse-git-mirror.yaml"), "--non-interactive"], |
| 59 | cwd=tmp_path, |
| 60 | runner=runner, |
| 61 | ) |
| 62 | == 0 |
| 63 | ) |
| 64 | seed_muse_substrate(tmp_path) |
| 65 | |
| 66 | import json as _json |
| 67 | from io import StringIO |
| 68 | from unittest.mock import patch |
| 69 | |
| 70 | from cli.context import CliContext |
| 71 | from cli.main import main |
| 72 | from cli.output import OutputContext |
| 73 | |
| 74 | out = StringIO() |
| 75 | with patch("sys.stdout", out): |
| 76 | main( |
| 77 | ["status", "--json"], |
| 78 | ctx=CliContext.create(cwd=tmp_path, runner=runner, output=OutputContext(json_mode=True)), |
| 79 | ) |
| 80 | payload = _json.loads(out.getvalue()) |
| 81 | assert "sk-should-never-surface" not in _json.dumps(payload["muse_sync"]) |
| 82 | assert set(payload["muse_sync"].keys()) == {"state", "ok", "remediation", "message"} |
| 83 | |
| 84 | |
| 85 | def test_review_freeze_refusal_does_not_run_review_provider(tmp_path: Path) -> None: |
| 86 | """Least privilege: when muse_sync refuses, the freeze-review provider must never be invoked.""" |
| 87 | from cli.kit_root import kit_root |
| 88 | from tests.support import findings_provider_factory, seed_freeze_repo |
| 89 | |
| 90 | root = str(tmp_path.resolve()) |
| 91 | artifact = seed_freeze_repo(tmp_path, config_name="config-muse-git-mirror.yaml") |
| 92 | runner = make_runner( |
| 93 | { |
| 94 | f"muse -C {root} rev-parse --abbrev-ref HEAD": ok("main"), |
| 95 | f"muse -C {root} status --json": ok('{"dirty": true}'), |
| 96 | "git rev-parse --abbrev-ref HEAD": ok("main"), |
| 97 | "git status --porcelain": ok(""), |
| 98 | } |
| 99 | ) |
| 100 | provider_calls: list[str] = [] |
| 101 | |
| 102 | def _tracking_factory(name: str): |
| 103 | provider_calls.append(name) |
| 104 | return findings_provider_factory([])(name) |
| 105 | |
| 106 | code = run_cli( |
| 107 | ["review", "--freeze", str(artifact.relative_to(tmp_path))], |
| 108 | cwd=tmp_path, |
| 109 | runner=runner, |
| 110 | kit=kit_root(), |
| 111 | review_provider_factory=_tracking_factory, |
| 112 | ) |
| 113 | assert code == 2 |
| 114 | assert provider_calls == [] |
File History
1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1
docs: MuseHub-first before ISR #74 — staging solidify NEXT
Human
1 day ago