fix(tests): remove blind visibility, both teams always see each other's fields
Red and Blue could not see each other's real field data until both reviews passed, showing a placeholder message instead. This kept detection testing blind to what Red actually did; both sides now see the full, live test data at all times.
This commit is contained in:
@@ -163,46 +163,6 @@ from app.services.test_workflow_service import (
|
||||
router = APIRouter(prefix="/tests", tags=["tests"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Blind visibility — hide the other team's fields until both reviews pass
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_RED_ONLY_FIELDS = [
|
||||
"procedure_text", "tool_used", "attack_success",
|
||||
"execution_start_time", "execution_end_time", "red_summary",
|
||||
"red_validation_status", "red_validated_by", "red_validated_at", "red_validation_notes",
|
||||
]
|
||||
_BLUE_ONLY_FIELDS = [
|
||||
"detection_result", "containment_result", "detection_time", "containment_time",
|
||||
"blue_summary", "blue_validation_status", "blue_validated_by", "blue_validated_at",
|
||||
"blue_validation_notes", "system_gaps",
|
||||
]
|
||||
_BLIND_STATES = {"draft", "red_executing", "red_review", "blue_evaluating", "blue_review"}
|
||||
|
||||
|
||||
def _mask_for_team_blindness(test_out: TestOut, *, viewer_role: str) -> TestOut:
|
||||
"""Null out the other team's fields while the test is still blind.
|
||||
|
||||
admin and viewer are never blinded. Once the test reaches in_review or
|
||||
beyond, both sides see everything (existing cross-validation behavior).
|
||||
"""
|
||||
if viewer_role in ("admin", "viewer"):
|
||||
return test_out
|
||||
|
||||
test_state = test_out.state.value if hasattr(test_out.state, "value") else str(test_out.state)
|
||||
if test_state not in _BLIND_STATES:
|
||||
return test_out
|
||||
|
||||
if viewer_role in ("blue_tech", "blue_lead"):
|
||||
hide_fields = _RED_ONLY_FIELDS
|
||||
elif viewer_role in ("red_tech", "red_lead"):
|
||||
hide_fields = _BLUE_ONLY_FIELDS
|
||||
else:
|
||||
return test_out
|
||||
|
||||
return test_out.model_copy(update={f: None for f in hide_fields})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /tests — list with filters
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -504,12 +464,11 @@ def get_test(
|
||||
|
||||
Returns:
|
||||
TestOut: Full test detail including split red/blue evidence lists.
|
||||
Fields belonging to the other team are nulled out while the
|
||||
test is blind (see :func:`_mask_for_team_blindness`).
|
||||
Both teams see each other's fields regardless of state — Red
|
||||
and Blue are meant to see each other's work, not test blind.
|
||||
"""
|
||||
test = crud_get_test_detail(db, test_id)
|
||||
test_out = TestOut.model_validate(test)
|
||||
return _mask_for_team_blindness(test_out, viewer_role=current_user.role)
|
||||
return TestOut.model_validate(test)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Tests for red/blue blind visibility on GET /tests/{id}.
|
||||
"""Regression: Red and Blue must see each other's fields at every stage.
|
||||
|
||||
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.
|
||||
Blind visibility (hiding the other team's fields until both reviews pass)
|
||||
was a deliberate design in an earlier phase, but the org decided both teams
|
||||
should see each other's work throughout — detection testing isn't meant to
|
||||
be blind here. This confirms neither the frontend nor backend hides
|
||||
anything based on role/state anymore.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
@@ -46,11 +47,11 @@ def technique(api, auth_headers):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def blind_test(client, db, api, red_lead_headers, red_lead_user, red_tech_headers, technique):
|
||||
def in_progress_test(client, db, api, red_lead_headers, red_lead_user, red_tech_headers, technique):
|
||||
"""A test in red_executing with red-side content already filled in."""
|
||||
resp = api(
|
||||
"post", "/api/v1/tests", red_lead_headers,
|
||||
json={"technique_id": technique, "name": "Blind visibility test"},
|
||||
json={"technique_id": technique, "name": "Visibility test"},
|
||||
)
|
||||
test_id = resp.json()["id"]
|
||||
|
||||
@@ -62,32 +63,21 @@ def blind_test(client, db, api, red_lead_headers, red_lead_user, red_tech_header
|
||||
return test_id
|
||||
|
||||
|
||||
def test_blue_viewer_cannot_see_red_fields_during_red_executing(
|
||||
client, db, api, blind_test, blue_tech_headers
|
||||
def test_blue_viewer_sees_red_fields_during_red_executing(
|
||||
client, db, api, in_progress_test, blue_tech_headers
|
||||
):
|
||||
resp = api("get", f"/api/v1/tests/{blind_test}", blue_tech_headers)
|
||||
resp = api("get", f"/api/v1/tests/{in_progress_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"
|
||||
assert body["tool_used"] == "mimikatz"
|
||||
assert body["red_summary"] == "dumped creds"
|
||||
|
||||
|
||||
def test_admin_sees_everything_during_blind_states(
|
||||
client, db, api, blind_test, auth_headers
|
||||
def test_red_viewer_sees_own_fields_during_red_executing(
|
||||
client, db, api, in_progress_test, red_tech_headers
|
||||
):
|
||||
resp = api("get", f"/api/v1/tests/{blind_test}", auth_headers)
|
||||
resp = api("get", f"/api/v1/tests/{in_progress_test}", red_tech_headers)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["procedure_text"] == "run mimikatz"
|
||||
@@ -95,9 +85,9 @@ def test_admin_sees_everything_during_blind_states(
|
||||
|
||||
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,
|
||||
blue_tech_headers, blue_lead_headers, red_lead_user, blue_lead_user, in_progress_test,
|
||||
):
|
||||
test_id = blind_test
|
||||
test_id = in_progress_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
|
||||
|
||||
Reference in New Issue
Block a user