diff --git a/backend/tests/test_blind_visibility.py b/backend/tests/test_blind_visibility.py new file mode 100644 index 0000000..638c7ce --- /dev/null +++ b/backend/tests/test_blind_visibility.py @@ -0,0 +1,129 @@ +"""Tests for red/blue blind visibility on GET /tests/{id}. + +Neither team should see the other team's data while a test is in +draft/red_executing/red_review/blue_evaluating/blue_review. Both sides +see everything once the test reaches in_review (and beyond). admin and +viewer are never blinded. +""" + +import uuid + +import pytest + +from app.models.evidence import Evidence +from app.models.enums import TeamSide + +_RED_FIELDS = [ + "procedure_text", "tool_used", "attack_success", + "execution_start_time", "execution_end_time", "red_summary", +] +_BLUE_FIELDS = [ + "detection_result", "containment_result", "detection_time", + "containment_time", "blue_summary", "system_gaps", +] + + +def _add_evidence(db, test_id, team: TeamSide): + ev = Evidence( + test_id=uuid.UUID(test_id), + file_name="proof.txt", + file_path="s3://bucket/proof.txt", + sha256_hash="a" * 64, + team=team, + ) + db.add(ev) + db.commit() + + +@pytest.fixture +def technique(api, auth_headers): + resp = api( + "post", "/api/v1/techniques", auth_headers, + json={"mitre_id": "T1059.100", "name": "Command Line"}, + ) + assert resp.status_code == 201, resp.text + return resp.json()["id"] + + +@pytest.fixture +def blind_test(client, db, api, auth_headers, red_tech_headers, technique): + """A test in red_executing with red-side content already filled in.""" + resp = api( + "post", "/api/v1/tests", auth_headers, + json={"technique_id": technique, "name": "Blind visibility test"}, + ) + test_id = resp.json()["id"] + + api("post", f"/api/v1/tests/{test_id}/start-execution", red_tech_headers) + api( + "patch", f"/api/v1/tests/{test_id}/red", red_tech_headers, + json={"procedure_text": "run mimikatz", "tool_used": "mimikatz", "red_summary": "dumped creds"}, + ) + return test_id + + +def test_blue_viewer_cannot_see_red_fields_during_red_executing( + client, db, api, blind_test, blue_tech_headers +): + resp = api("get", f"/api/v1/tests/{blind_test}", blue_tech_headers) + assert resp.status_code == 200 + body = resp.json() + for field in _RED_FIELDS: + assert body[field] is None, f"{field} should be hidden from blue viewer" + + +def test_red_viewer_cannot_see_blue_fields_during_red_executing( + client, db, api, blind_test, red_tech_headers +): + resp = api("get", f"/api/v1/tests/{blind_test}", red_tech_headers) + assert resp.status_code == 200 + body = resp.json() + for field in _BLUE_FIELDS: + assert body[field] is None, f"{field} should be hidden from red viewer" + # Red's own fields ARE visible to red + assert body["procedure_text"] == "run mimikatz" + + +def test_admin_sees_everything_during_blind_states( + client, db, api, blind_test, auth_headers +): + resp = api("get", f"/api/v1/tests/{blind_test}", auth_headers) + assert resp.status_code == 200 + body = resp.json() + assert body["procedure_text"] == "run mimikatz" + + +def test_both_sides_see_everything_once_in_review( + client, db, api, auth_headers, red_tech_headers, red_lead_headers, + blue_tech_headers, blue_lead_headers, red_lead_user, blue_lead_user, blind_test, +): + test_id = blind_test + _add_evidence(db, test_id, TeamSide.red) + submit_red = api("post", f"/api/v1/tests/{test_id}/submit-red", red_tech_headers) + assert submit_red.status_code == 200, submit_red.text + + approve_red = api("post", f"/api/v1/tests/{test_id}/review-red", red_lead_headers, json={"decision": "approve"}) + assert approve_red.status_code == 200, approve_red.text + + api("post", f"/api/v1/tests/{test_id}/start-blue-work", blue_tech_headers) + api( + "patch", f"/api/v1/tests/{test_id}/blue", blue_tech_headers, + json={"detection_result": "detected", "blue_summary": "caught it"}, + ) + _add_evidence(db, test_id, TeamSide.blue) + submit_blue = api("post", f"/api/v1/tests/{test_id}/submit-blue", blue_tech_headers) + assert submit_blue.status_code == 200, submit_blue.text + + approve_blue = api("post", f"/api/v1/tests/{test_id}/review-blue", blue_lead_headers, json={"decision": "approve"}) + assert approve_blue.status_code == 200, approve_blue.text + assert approve_blue.json()["state"] == "in_review" + + resp_as_red = api("get", f"/api/v1/tests/{test_id}", red_tech_headers) + body_red = resp_as_red.json() + assert body_red["detection_result"] == "detected" + assert body_red["blue_summary"] == "caught it" + + resp_as_blue = api("get", f"/api/v1/tests/{test_id}", blue_tech_headers) + body_blue = resp_as_blue.json() + assert body_blue["procedure_text"] == "run mimikatz" + assert body_blue["red_summary"] == "dumped creds"