a3f86c7b31
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Admin still bypassed require_any_role (non-strict) on the pure operator actions: start-execution, submit-red, start-blue-work, submit-blue, pause/resume-timer, and the red/blue field-edit + general test create/update/remediation/import-rt/template endpoints all used the loose dependency even where admin wasn't in the role tuple. Switched every one of these to require_any_role_strict, matching the review/validate/dispute/hold/assign endpoints already fixed earlier. Frontend: removed every remaining role === "admin" disjunct gating Start Execution, Submit to Blue Team, blue evaluating actions, timer control, red/blue field editing, test creation, and template creation. Team-blindness visibility (isBlind) deliberately keeps the admin bypass — admin still sees both sides unmasked for oversight, since that's visibility, not operating. Updated ~20 tests whose fixtures used the admin account as a convenience shortcut to create/drive tests through the workflow — switched them to a red_lead account, fixing an incidental fixture-resolution-order cookie bug along the way (client's cookie jar prefers the last-logged-in role's cookie over any Bearer header explicitly passed to a later request).
178 lines
5.9 KiB
Python
178 lines
5.9 KiB
Python
"""Tests for data classification fields and edit permissions."""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from app.models.enums import TestState
|
|
from app.models.test import Test
|
|
from app.models.technique import Technique
|
|
from app.services.test_crud_service import determine_initial_classification
|
|
|
|
|
|
@pytest.fixture
|
|
def technique(client, auth_headers):
|
|
"""Create a technique for test association (mirrors test_tests.py)."""
|
|
response = client.post(
|
|
"/api/v1/techniques",
|
|
json={"mitre_id": "T9998", "name": "Classification Fixture Technique"},
|
|
headers=auth_headers,
|
|
)
|
|
return response.json()
|
|
|
|
|
|
def _seed_technique(db, tactic="execution") -> Technique:
|
|
technique = Technique(
|
|
mitre_id="T9999",
|
|
name="Test Technique",
|
|
tactic=tactic,
|
|
platforms=["linux"],
|
|
)
|
|
db.add(technique)
|
|
db.commit()
|
|
db.refresh(technique)
|
|
return technique
|
|
|
|
|
|
def test_new_test_defaults_to_internal_use_only_via_db_default(db, red_lead_user):
|
|
"""A Test() constructed without going through the create_test service
|
|
still gets a safe classification via the column's server_default."""
|
|
technique = _seed_technique(db)
|
|
test = Test(
|
|
technique_id=technique.id,
|
|
name="Classification test",
|
|
created_by=red_lead_user.id,
|
|
)
|
|
db.add(test)
|
|
db.commit()
|
|
db.refresh(test)
|
|
assert test.data_classification == "internal_use_only"
|
|
|
|
|
|
def test_create_test_endpoint_uses_tactic_heuristic(client, db, api, red_lead_headers, red_lead_user, technique):
|
|
"""Going through the real create-test flow applies the tactic-based heuristic."""
|
|
resp = api(
|
|
"post", "/api/v1/tests", red_lead_headers,
|
|
json={"technique_id": technique["id"], "name": "Heuristic test"},
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
# The shared `technique` fixture (see conftest/test_tests.py) doesn't set
|
|
# a pii-tier tactic, so this should land on the baseline.
|
|
assert resp.json()["data_classification"] == "internal_use_only"
|
|
|
|
|
|
def test_admin_cannot_update_classification(client, db, admin_user, admin_token, red_lead_user):
|
|
"""Admin administers the site, not test content — classification is a
|
|
lead/manager/operator call."""
|
|
technique = _seed_technique(db)
|
|
test = Test(
|
|
technique_id=technique.id,
|
|
name="Classify me",
|
|
created_by=red_lead_user.id,
|
|
state=TestState.draft,
|
|
)
|
|
db.add(test)
|
|
db.commit()
|
|
|
|
response = client.patch(
|
|
f"/api/v1/tests/{test.id}/classification",
|
|
json={"data_classification": "pii"},
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_manager_can_update_classification(client, db, manager_user, manager_headers, red_lead_user):
|
|
technique = _seed_technique(db)
|
|
test = Test(
|
|
technique_id=technique.id,
|
|
name="Classify me",
|
|
created_by=red_lead_user.id,
|
|
state=TestState.draft,
|
|
)
|
|
db.add(test)
|
|
db.commit()
|
|
|
|
response = client.patch(
|
|
f"/api/v1/tests/{test.id}/classification",
|
|
json={"data_classification": "pii"},
|
|
headers=manager_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["data_classification"] == "pii"
|
|
|
|
db.refresh(test)
|
|
assert test.data_classification == "pii"
|
|
|
|
|
|
def test_operator_can_update_classification(client, db, admin_user, red_lead_token, red_lead_user):
|
|
"""Any test participant (not just admin) can correct the initial classification."""
|
|
technique = _seed_technique(db)
|
|
test = Test(
|
|
technique_id=technique.id,
|
|
name="Operator-editable",
|
|
created_by=red_lead_user.id,
|
|
)
|
|
db.add(test)
|
|
db.commit()
|
|
|
|
response = client.patch(
|
|
f"/api/v1/tests/{test.id}/classification",
|
|
json={"data_classification": "general_use"},
|
|
headers={"Authorization": f"Bearer {red_lead_token}"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["data_classification"] == "general_use"
|
|
|
|
|
|
def test_viewer_cannot_update_classification(client, db, admin_user, red_lead_user):
|
|
"""Viewer is not a test participant and should still be forbidden."""
|
|
from app.auth import hash_password
|
|
from app.models.user import User
|
|
|
|
viewer = User(
|
|
username="viewer_classif", email="viewer_classif@test.com",
|
|
hashed_password=hash_password("x"), role="viewer", is_active=True,
|
|
must_change_password=False,
|
|
)
|
|
db.add(viewer)
|
|
db.commit()
|
|
login = client.post("/api/v1/auth/login", data={"username": "viewer_classif", "password": "x"})
|
|
viewer_token = login.json()["access_token"]
|
|
|
|
technique = _seed_technique(db)
|
|
test = Test(
|
|
technique_id=technique.id,
|
|
name="Protected",
|
|
created_by=red_lead_user.id,
|
|
)
|
|
db.add(test)
|
|
db.commit()
|
|
|
|
response = client.patch(
|
|
f"/api/v1/tests/{test.id}/classification",
|
|
json={"data_classification": "pii"},
|
|
headers={"Authorization": f"Bearer {viewer_token}"},
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
def _technique_stub(tactic):
|
|
t = MagicMock()
|
|
t.tactic = tactic
|
|
return t
|
|
|
|
|
|
def test_determine_initial_classification_defaults_to_internal_use_only():
|
|
assert determine_initial_classification(_technique_stub("execution")) == "internal_use_only"
|
|
assert determine_initial_classification(_technique_stub(None)) == "internal_use_only"
|
|
assert determine_initial_classification(None) == "internal_use_only"
|
|
|
|
|
|
def test_determine_initial_classification_escalates_for_sensitive_tactics():
|
|
assert determine_initial_classification(_technique_stub("exfiltration")) == "pii"
|
|
assert determine_initial_classification(_technique_stub("collection")) == "pii"
|
|
assert determine_initial_classification(_technique_stub("credential-access")) == "pii"
|
|
assert determine_initial_classification(_technique_stub("impact")) == "pii"
|
|
assert determine_initial_classification(_technique_stub("Exfiltration")) == "pii"
|