157 lines
5.3 KiB
Python
157 lines
5.3 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_confidential_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 == "confidential"
|
|
|
|
|
|
def test_create_test_endpoint_uses_tactic_heuristic(client, db, api, auth_headers, technique):
|
|
"""Going through the real create-test flow applies the tactic-based heuristic."""
|
|
resp = api(
|
|
"post", "/api/v1/tests", auth_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 restricted-tier tactic, so this should land on the baseline.
|
|
assert resp.json()["data_classification"] == "confidential"
|
|
|
|
|
|
def test_admin_can_update_classification(client, db, admin_user, admin_token, 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": "restricted"},
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["data_classification"] == "restricted"
|
|
|
|
db.refresh(test)
|
|
assert test.data_classification == "restricted"
|
|
|
|
|
|
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": "restricted"},
|
|
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_confidential():
|
|
assert determine_initial_classification(_technique_stub("execution")) == "confidential"
|
|
assert determine_initial_classification(_technique_stub(None)) == "confidential"
|
|
assert determine_initial_classification(None) == "confidential"
|
|
|
|
|
|
def test_determine_initial_classification_escalates_for_sensitive_tactics():
|
|
assert determine_initial_classification(_technique_stub("exfiltration")) == "restricted"
|
|
assert determine_initial_classification(_technique_stub("collection")) == "restricted"
|
|
assert determine_initial_classification(_technique_stub("credential-access")) == "restricted"
|
|
assert determine_initial_classification(_technique_stub("impact")) == "restricted"
|
|
assert determine_initial_classification(_technique_stub("Exfiltration")) == "restricted"
|