320 lines
12 KiB
Python
320 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import copy
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from scripts import build_telegram_visible_unseen_chain_packet as packet_builder
|
|
from scripts import collect_telegram_visible_unseen_chain_state as state
|
|
|
|
|
|
def manifest_output(*, row_count: int = 7) -> str:
|
|
rows = [
|
|
{
|
|
"kind": "identity",
|
|
"database": "teleo",
|
|
"current_user": packet_builder.READ_ONLY_DB_ROLE,
|
|
"server_version_num": 160014,
|
|
"server_encoding": "UTF8",
|
|
"database_collation": "C.UTF-8",
|
|
"database_ctype": "C.UTF-8",
|
|
"transaction_read_only": "on",
|
|
}
|
|
]
|
|
for kind in (
|
|
"schemas",
|
|
"extensions",
|
|
"application_roles",
|
|
"columns",
|
|
"constraints",
|
|
"indexes",
|
|
"sequences",
|
|
"views",
|
|
"functions",
|
|
"triggers",
|
|
"types",
|
|
"policies",
|
|
):
|
|
rows.append({"kind": kind, "items": []})
|
|
rows.append(
|
|
{
|
|
"kind": "table",
|
|
"schema": "public",
|
|
"table": "claims",
|
|
"row_count": row_count,
|
|
"rowset_md5": "f" * 32,
|
|
}
|
|
)
|
|
return "BEGIN\nSET\n" + "\n".join(json.dumps(item) for item in rows) + "\nROLLBACK\n"
|
|
|
|
|
|
def write_packet(tmp_path: Path) -> Path:
|
|
out = tmp_path / "packet.json"
|
|
packet_builder.write_json(
|
|
out,
|
|
packet_builder.build_packet(
|
|
handler_receipt_path=packet_builder.DEFAULT_HANDLER_RECEIPT.resolve(),
|
|
git_sha="a" * 40,
|
|
),
|
|
)
|
|
return out
|
|
|
|
|
|
def args_for(packet: Path, tmp_path: Path, *, phase: str = "preflight") -> argparse.Namespace:
|
|
return argparse.Namespace(
|
|
phase=phase,
|
|
packet=packet,
|
|
baseline=None,
|
|
subject_ref=None,
|
|
manifest_sql=Path("ops/postgres_parity_manifest.sql"),
|
|
ssh_target="root@example.invalid",
|
|
ssh_key=tmp_path / "key",
|
|
connect_timeout=1,
|
|
timeout=5,
|
|
fingerprint_timeout=5,
|
|
ssh_attempts=3,
|
|
out=tmp_path / f"{phase}.json",
|
|
markdown_out=tmp_path / f"{phase}.md",
|
|
)
|
|
|
|
|
|
class GoodRunner:
|
|
def __init__(self) -> None:
|
|
self.service_calls = 0
|
|
|
|
def __call__(self, command: list[str], input_text: str, _timeout: int) -> state.CommandResult:
|
|
remote_command = command[-1]
|
|
if "git rev-parse HEAD" in remote_command:
|
|
sha = "b" * 40
|
|
return state.CommandResult(
|
|
0,
|
|
f"deploy_head={sha}\nlast_deploy_sha={sha}\ndeploy_stamp_ancestor=true\n",
|
|
"",
|
|
)
|
|
if "systemctl show" in remote_command:
|
|
self.service_calls += 1
|
|
return state.CommandResult(
|
|
0,
|
|
"\n".join(
|
|
[
|
|
"ActiveState=active",
|
|
"SubState=running",
|
|
"MainPID=123",
|
|
"NRestarts=0",
|
|
"ExecMainStartTimestamp=Wed 2026-07-15 00:00:00 UTC",
|
|
]
|
|
),
|
|
"",
|
|
)
|
|
if "'subject_ref'" in input_text:
|
|
assert "default_transaction_read_only=on" in remote_command
|
|
assert "-c transaction_read_only=on" in remote_command
|
|
assert f"role={packet_builder.READ_ONLY_DB_ROLE}" in remote_command
|
|
assert "psql -X" in remote_command
|
|
payload = {
|
|
"subject_ref": "c0000000",
|
|
"matches": [
|
|
{
|
|
"table": "public.beliefs",
|
|
"row": {"id": "c0000000-0000-4000-8000-000000000001"},
|
|
"evidence_count": None,
|
|
}
|
|
],
|
|
}
|
|
return state.CommandResult(0, f"BEGIN\n{json.dumps(payload)}\nROLLBACK\n", "")
|
|
if "postgres_parity_manifest" not in remote_command and input_text.startswith("\\set ON_ERROR_STOP"):
|
|
assert "default_transaction_read_only=on" in remote_command
|
|
assert "-c transaction_read_only=on" in remote_command
|
|
assert f"role={packet_builder.READ_ONLY_DB_ROLE}" in remote_command
|
|
assert "psql -X" in remote_command
|
|
return state.CommandResult(0, manifest_output(), "")
|
|
raise AssertionError(f"unexpected command: {command} / input={input_text[:80]}")
|
|
|
|
|
|
def test_preflight_passes_with_two_identical_read_only_fingerprints(tmp_path: Path) -> None:
|
|
args = args_for(write_packet(tmp_path), tmp_path)
|
|
|
|
result = state.collect_state(args, runner=GoodRunner())
|
|
|
|
assert result["status"] == "pass"
|
|
assert result["ready_for_action_time_authorization"] is True
|
|
assert result["telegram_visible_messages_sent_by_collector"] is False
|
|
assert result["mutates_db"] is False
|
|
assert all(result["packet_checks"].values())
|
|
assert all(result["remote_checks"].values())
|
|
assert len(result["state_token_sha256"]) == 64
|
|
|
|
|
|
def test_preflight_fails_when_second_fingerprint_drifts(tmp_path: Path) -> None:
|
|
args = args_for(write_packet(tmp_path), tmp_path)
|
|
fingerprint_calls = 0
|
|
good = GoodRunner()
|
|
|
|
def drift_runner(command: list[str], input_text: str, timeout: int) -> state.CommandResult:
|
|
nonlocal fingerprint_calls
|
|
if input_text.startswith("\\set ON_ERROR_STOP") and "'subject_ref'" not in input_text:
|
|
fingerprint_calls += 1
|
|
return state.CommandResult(0, manifest_output(row_count=7 + fingerprint_calls - 1), "")
|
|
return good(command, input_text, timeout)
|
|
|
|
result = state.collect_state(args, runner=drift_runner)
|
|
|
|
assert result["status"] == "fail"
|
|
assert result["remote_checks"]["canonical_fingerprint_unchanged_during_probe"] is False
|
|
|
|
|
|
def test_preflight_accepts_newer_workspace_head_when_deployed_stamp_is_ancestor(tmp_path: Path) -> None:
|
|
args = args_for(write_packet(tmp_path), tmp_path)
|
|
|
|
class AheadRunner(GoodRunner):
|
|
def __call__(self, command: list[str], input_text: str, timeout: int) -> state.CommandResult:
|
|
if "git rev-parse HEAD" in command[-1]:
|
|
return state.CommandResult(
|
|
0,
|
|
f"deploy_head={'c' * 40}\nlast_deploy_sha={'b' * 40}\ndeploy_stamp_ancestor=true\n",
|
|
"",
|
|
)
|
|
return super().__call__(command, input_text, timeout)
|
|
|
|
result = state.collect_state(args, runner=AheadRunner())
|
|
|
|
assert result["status"] == "pass"
|
|
assert result["remote"]["workspace_head_matches_deployed_stamp"] is False
|
|
assert result["remote_checks"]["deployed_stamp_is_ancestor_of_workspace_head"] is True
|
|
|
|
|
|
def test_preflight_retries_transient_ssh_failure(tmp_path: Path) -> None:
|
|
args = args_for(write_packet(tmp_path), tmp_path)
|
|
good = GoodRunner()
|
|
deploy_attempts = 0
|
|
|
|
def transient_runner(command: list[str], input_text: str, timeout: int) -> state.CommandResult:
|
|
nonlocal deploy_attempts
|
|
if "git rev-parse HEAD" in command[-1]:
|
|
deploy_attempts += 1
|
|
if deploy_attempts == 1:
|
|
return state.CommandResult(255, "", "Connection timed out during banner exchange")
|
|
return good(command, input_text, timeout)
|
|
|
|
result = state.collect_state(args, runner=transient_runner)
|
|
|
|
assert result["status"] == "pass"
|
|
assert result["remote"]["readback_attempts"]["deploy"] == 2
|
|
|
|
|
|
def test_preflight_rejects_packet_bound_to_different_tooling_source(tmp_path: Path) -> None:
|
|
packet_path = write_packet(tmp_path)
|
|
packet = json.loads(packet_path.read_text(encoding="utf-8"))
|
|
packet["tooling_binding"]["capture_verifier"]["sha256"] = "0" * 64
|
|
packet_path.write_text(json.dumps(packet), encoding="utf-8")
|
|
|
|
result = state.collect_state(args_for(packet_path, tmp_path), runner=GoodRunner())
|
|
|
|
assert result["status"] == "fail"
|
|
assert result["packet_checks"]["packet_tooling_hashes_match_current_sources"] is False
|
|
|
|
|
|
def test_postflight_binds_baseline_fingerprint_service_and_subject(tmp_path: Path) -> None:
|
|
packet = write_packet(tmp_path)
|
|
pre_args = args_for(packet, tmp_path)
|
|
preflight = state.collect_state(pre_args, runner=GoodRunner())
|
|
baseline = tmp_path / "preflight.json"
|
|
state.write_json(baseline, preflight)
|
|
post_args = args_for(packet, tmp_path, phase="postflight")
|
|
post_args.baseline = baseline
|
|
post_args.subject_ref = "c0000000"
|
|
|
|
postflight = state.collect_state(post_args, runner=GoodRunner())
|
|
|
|
assert postflight["status"] == "pass"
|
|
assert postflight["ready_for_visible_capture_verification"] is True
|
|
assert all(postflight["baseline_checks"].values())
|
|
assert postflight["remote_checks"]["subject_resolved_to_exactly_one_db_object"] is True
|
|
|
|
|
|
def test_postflight_rejects_baseline_fingerprint_mismatch(tmp_path: Path) -> None:
|
|
packet = write_packet(tmp_path)
|
|
pre_args = args_for(packet, tmp_path)
|
|
preflight = state.collect_state(pre_args, runner=GoodRunner())
|
|
preflight = copy.deepcopy(preflight)
|
|
preflight["remote"]["fingerprint_after"]["fingerprint_sha256"] = "0" * 64
|
|
baseline = tmp_path / "preflight.json"
|
|
state.write_json(baseline, preflight)
|
|
post_args = args_for(packet, tmp_path, phase="postflight")
|
|
post_args.baseline = baseline
|
|
post_args.subject_ref = "c0000000"
|
|
|
|
postflight = state.collect_state(post_args, runner=GoodRunner())
|
|
|
|
assert postflight["status"] == "fail"
|
|
assert postflight["baseline_checks"]["canonical_fingerprint_matches_preflight"] is False
|
|
|
|
|
|
def test_subject_sql_rejects_untrusted_input() -> None:
|
|
try:
|
|
state.subject_readback_sql("c0000000'; delete from public.claims; --")
|
|
except ValueError as exc:
|
|
assert "subject_ref" in str(exc)
|
|
else:
|
|
raise AssertionError("unsafe subject ref was accepted")
|
|
|
|
|
|
class NoSshRunner:
|
|
def __init__(self) -> None:
|
|
self.calls = 0
|
|
|
|
def __call__(self, _command: list[str], _input_text: str, _timeout: int) -> state.CommandResult:
|
|
self.calls += 1
|
|
raise AssertionError("SSH runner must not be called after local fail-closed rejection")
|
|
|
|
|
|
def test_write_capable_manifest_override_is_rejected_before_ssh(tmp_path: Path) -> None:
|
|
packet = write_packet(tmp_path)
|
|
malicious = tmp_path / "write.sql"
|
|
malicious.write_text(
|
|
"\\set ON_ERROR_STOP on\nbegin;\nupdate public.claims set status = 'live';\ncommit;\n",
|
|
encoding="utf-8",
|
|
)
|
|
args = args_for(packet, tmp_path)
|
|
args.manifest_sql = malicious
|
|
runner = NoSshRunner()
|
|
|
|
result = state.collect_state(args, runner=runner)
|
|
|
|
assert result["status"] == "fail"
|
|
assert result["packet_checks"]["manifest_override_absent_or_exact_canonical_path"] is False
|
|
assert result["remote"]["readback_attempts"] == {}
|
|
assert runner.calls == 0
|
|
assert state._manifest_sql_is_statically_read_only(malicious.read_text(encoding="utf-8")) is False
|
|
|
|
|
|
def test_manifest_hash_mismatch_is_rejected_before_ssh(tmp_path: Path) -> None:
|
|
packet_path = write_packet(tmp_path)
|
|
packet = json.loads(packet_path.read_text(encoding="utf-8"))
|
|
packet["manifest_binding"]["sha256"] = "0" * 64
|
|
packet_path.write_text(json.dumps(packet), encoding="utf-8")
|
|
runner = NoSshRunner()
|
|
|
|
result = state.collect_state(args_for(packet_path, tmp_path), runner=runner)
|
|
|
|
assert result["status"] == "fail"
|
|
assert result["packet_checks"]["manifest_packet_sha256_matches_canonical_file"] is False
|
|
assert result["remote"]["readback_attempts"] == {}
|
|
assert runner.calls == 0
|
|
|
|
|
|
def test_cli_removes_arbitrary_manifest_override() -> None:
|
|
with pytest.raises(SystemExit):
|
|
state.parse_args(["--manifest-sql", "write.sql"])
|
|
|
|
|
|
def test_fingerprint_command_enforces_read_only_session_role_and_disables_psql_rc() -> None:
|
|
command = state.fingerprint_readback_command()
|
|
|
|
assert "default_transaction_read_only=on" in command
|
|
assert "-c transaction_read_only=on" in command
|
|
assert f"role={packet_builder.READ_ONLY_DB_ROLE}" in command
|
|
assert "psql -X" in command
|