|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import json |
| 4 | +from pathlib import Path |
| 5 | + |
| 6 | +from telemetry_window_demo.config_change_investigation_demo import default_demo_root, run_demo |
| 7 | +from telemetry_window_demo.config_change_investigation_demo.pipeline import ( |
| 8 | + build_investigations, |
| 9 | + evaluate_risky_config_changes, |
| 10 | + load_jsonl, |
| 11 | + load_yaml, |
| 12 | + normalize_config_changes, |
| 13 | + normalize_follow_on_events, |
| 14 | + normalize_policy_denials, |
| 15 | +) |
| 16 | + |
| 17 | + |
| 18 | +def _load_demo_inputs(): |
| 19 | + demo_root = default_demo_root() |
| 20 | + config = load_yaml(demo_root / "config" / "investigation.yaml") |
| 21 | + config_changes = normalize_config_changes( |
| 22 | + load_jsonl(demo_root / "data" / "raw" / "config_changes.jsonl") |
| 23 | + ) |
| 24 | + policy_denials = normalize_policy_denials( |
| 25 | + load_jsonl(demo_root / "data" / "raw" / "policy_denials.jsonl") |
| 26 | + ) |
| 27 | + follow_on_events = normalize_follow_on_events( |
| 28 | + load_jsonl(demo_root / "data" / "raw" / "follow_on_events.jsonl") |
| 29 | + ) |
| 30 | + return demo_root, config, config_changes, policy_denials, follow_on_events |
| 31 | + |
| 32 | + |
| 33 | +def _load_json_file(path: Path): |
| 34 | + return json.loads(path.read_text(encoding="utf-8")) |
| 35 | + |
| 36 | + |
| 37 | +def test_normalize_config_changes_is_sorted_and_complete() -> None: |
| 38 | + _, _, config_changes, _, _ = _load_demo_inputs() |
| 39 | + |
| 40 | + assert [change["change_id"] for change in config_changes] == [ |
| 41 | + "cfg-001", |
| 42 | + "cfg-002", |
| 43 | + "cfg-003", |
| 44 | + "cfg-004", |
| 45 | + ] |
| 46 | + assert config_changes[0]["target_system"] == "identity-proxy" |
| 47 | + assert config_changes[1]["config_key"] == "public_bind_cidr" |
| 48 | + |
| 49 | + |
| 50 | +def test_evaluate_risky_config_changes_flags_expected_changes() -> None: |
| 51 | + _, config, config_changes, _, _ = _load_demo_inputs() |
| 52 | + hits = evaluate_risky_config_changes(config_changes, config["rules"]) |
| 53 | + |
| 54 | + assert [hit["change_event"]["change_id"] for hit in hits] == [ |
| 55 | + "cfg-001", |
| 56 | + "cfg-002", |
| 57 | + "cfg-004", |
| 58 | + ] |
| 59 | + assert [hit["severity"] for hit in hits] == ["critical", "high", "high"] |
| 60 | + |
| 61 | + |
| 62 | +def test_build_investigations_uses_bounded_system_and_time_correlation() -> None: |
| 63 | + _, config, config_changes, policy_denials, follow_on_events = _load_demo_inputs() |
| 64 | + hits = evaluate_risky_config_changes(config_changes, config["rules"]) |
| 65 | + investigations = build_investigations( |
| 66 | + hits, |
| 67 | + policy_denials, |
| 68 | + follow_on_events, |
| 69 | + correlation_minutes=int(config["correlation_minutes"]), |
| 70 | + ) |
| 71 | + |
| 72 | + identity = next(item for item in investigations if item["investigation_id"] == "CCI-001") |
| 73 | + payments = next(item for item in investigations if item["investigation_id"] == "CCI-002") |
| 74 | + vault = next(item for item in investigations if item["investigation_id"] == "CCI-003") |
| 75 | + |
| 76 | + assert identity["evidence_counts"] == {"policy_denials": 2, "follow_on_events": 2} |
| 77 | + assert payments["evidence_counts"] == {"policy_denials": 1, "follow_on_events": 2} |
| 78 | + assert vault["evidence_counts"] == {"policy_denials": 0, "follow_on_events": 0} |
| 79 | + |
| 80 | + assert all( |
| 81 | + denial["target_system"] == "payments-api" |
| 82 | + for denial in payments["attached_policy_denials"] |
| 83 | + ) |
| 84 | + assert all( |
| 85 | + event["event_id"] != "fo-005" for event in vault["attached_follow_on_events"] |
| 86 | + ) |
| 87 | + |
| 88 | + |
| 89 | +def test_run_demo_is_deterministic_and_matches_committed_artifacts(tmp_path) -> None: |
| 90 | + demo_root, _, _, _, _ = _load_demo_inputs() |
| 91 | + first_dir = tmp_path / "run-one" |
| 92 | + second_dir = tmp_path / "run-two" |
| 93 | + |
| 94 | + first_result = run_demo(demo_root=demo_root, artifacts_dir=first_dir) |
| 95 | + second_result = run_demo(demo_root=demo_root, artifacts_dir=second_dir) |
| 96 | + |
| 97 | + assert first_result["change_event_count"] == 4 |
| 98 | + assert first_result["risky_change_count"] == 3 |
| 99 | + assert first_result["investigation_count"] == 3 |
| 100 | + assert second_result["investigation_count"] == first_result["investigation_count"] |
| 101 | + |
| 102 | + for name in ( |
| 103 | + "change_events_normalized.json", |
| 104 | + "investigation_hits.json", |
| 105 | + "investigation_summary.json", |
| 106 | + ): |
| 107 | + expected = _load_json_file(demo_root / "artifacts" / name) |
| 108 | + first = _load_json_file(first_dir / name) |
| 109 | + second = _load_json_file(second_dir / name) |
| 110 | + assert first == expected |
| 111 | + assert second == expected |
| 112 | + |
| 113 | + expected_report = ( |
| 114 | + demo_root / "artifacts" / "investigation_report.md" |
| 115 | + ).read_text(encoding="utf-8") |
| 116 | + assert (first_dir / "investigation_report.md").read_text(encoding="utf-8") == expected_report |
| 117 | + assert (second_dir / "investigation_report.md").read_text(encoding="utf-8") == expected_report |
0 commit comments