feat(lab): add isolated iPhone capture evidence
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).parents[1]
|
||||
INTEGRITY_PATH = (
|
||||
REPOSITORY_ROOT / "plugins" / "xgrids-k1" / "lab" / "iphone-capture" / "session_integrity.py"
|
||||
)
|
||||
|
||||
|
||||
def _load_module() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"iphone_capture_session_integrity", INTEGRITY_PATH
|
||||
)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
INTEGRITY = _load_module()
|
||||
|
||||
|
||||
def _sha256(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def _session(tmp_path: Path) -> tuple[Path, Path]:
|
||||
session = tmp_path / "20260716T000000Z_test_abcd"
|
||||
capture_dir = session / "captures"
|
||||
capture_dir.mkdir(parents=True)
|
||||
capture = capture_dir / "iphone-network.pcap"
|
||||
capture_bytes = b"synthetic-private-capture"
|
||||
capture.write_bytes(capture_bytes)
|
||||
manifest = {
|
||||
"session_id": session.name,
|
||||
"status": "completed",
|
||||
"artifacts": [
|
||||
{
|
||||
"kind": "iphone-network-pcap-compatible",
|
||||
"relative_path": "captures/iphone-network.pcap",
|
||||
"size_bytes": len(capture_bytes),
|
||||
"sha256": _sha256(capture_bytes),
|
||||
}
|
||||
],
|
||||
}
|
||||
(session / "manifest.redacted.json").write_text(json.dumps(manifest), encoding="utf-8")
|
||||
(session / "capture.stderr.log").write_bytes(b"")
|
||||
events = session / "operator-events.private.jsonl"
|
||||
events.write_text('{"sequence":1}\n', encoding="utf-8")
|
||||
return session, capture
|
||||
|
||||
|
||||
def test_seal_session_preserves_manifest_and_hashes_late_operator_events(tmp_path: Path) -> None:
|
||||
session, _ = _session(tmp_path)
|
||||
manifest = session / "manifest.redacted.json"
|
||||
before = manifest.read_bytes()
|
||||
|
||||
destination = INTEGRITY.seal_session(session)
|
||||
inventory = json.loads(destination.read_text(encoding="utf-8"))
|
||||
|
||||
assert manifest.read_bytes() == before
|
||||
assert inventory["capture_manifest_preserved"] is True
|
||||
assert inventory["verified_manifest_artifacts"][0]["relative_path"] == (
|
||||
"captures/iphone-network.pcap"
|
||||
)
|
||||
derived = {item["kind"]: item for item in inventory["derived_session_artifacts"]}
|
||||
assert derived["capture-manifest"]["sha256"] == _sha256(before)
|
||||
assert derived["capture-stderr"]["size_bytes"] == 0
|
||||
assert derived["operator-events"]["relative_path"] == "operator-events.private.jsonl"
|
||||
assert destination.stat().st_mode & 0o777 == 0o600
|
||||
|
||||
|
||||
def test_seal_session_rejects_capture_tampering(tmp_path: Path) -> None:
|
||||
session, capture = _session(tmp_path)
|
||||
capture.write_bytes(b"tampered")
|
||||
|
||||
with pytest.raises(INTEGRITY.SessionIntegrityError, match="size mismatch"):
|
||||
INTEGRITY.seal_session(session)
|
||||
|
||||
|
||||
def test_seal_session_refuses_to_replace_inventory_without_explicit_flag(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
session, _ = _session(tmp_path)
|
||||
INTEGRITY.seal_session(session)
|
||||
|
||||
with pytest.raises(INTEGRITY.SessionIntegrityError, match="already exists"):
|
||||
INTEGRITY.seal_session(session)
|
||||
|
||||
|
||||
def test_seal_session_rejects_manifest_path_escape(tmp_path: Path) -> None:
|
||||
session, _ = _session(tmp_path)
|
||||
manifest_path = session / "manifest.redacted.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
manifest["artifacts"][0]["relative_path"] = "../outside.pcap"
|
||||
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
|
||||
with pytest.raises(INTEGRITY.SessionIntegrityError, match="stay inside"):
|
||||
INTEGRITY.seal_session(session)
|
||||
Reference in New Issue
Block a user