feat(classification): org data classification scheme, tactic-based initial default, operator edit rights
This commit is contained in:
@@ -42,6 +42,7 @@ from app.models.test import Test
|
||||
from app.models.user import User
|
||||
from app.services.audit_service import log_action
|
||||
from app.services.status_service import recalculate_technique_status
|
||||
from app.services.test_crud_service import determine_initial_classification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -631,6 +632,7 @@ def import_evaluation_round(
|
||||
red_validated_by=current_user.id,
|
||||
red_validated_at=datetime.utcnow(),
|
||||
detection_result=detection_result,
|
||||
data_classification=determine_initial_classification(technique),
|
||||
blue_validation_status=None,
|
||||
execution_date=datetime.utcnow(),
|
||||
created_at=datetime.utcnow(),
|
||||
|
||||
@@ -79,6 +79,7 @@ JIRA_FIELD_ATTACK_SUCCESS = "customfield_11815"
|
||||
JIRA_FIELD_ATTACK_DETECTED = "customfield_11809"
|
||||
JIRA_FIELD_TIME_TO_DETECT = "customfield_11810"
|
||||
JIRA_FIELD_TIME_TO_CONTAIN = "customfield_11811"
|
||||
JIRA_FIELD_DATA_SENSITIVITY = "customfield_11233"
|
||||
|
||||
_ATTACK_SUCCESS_TO_JIRA = {
|
||||
"successful": "Yes",
|
||||
@@ -92,6 +93,13 @@ _DETECTION_TO_JIRA = {
|
||||
"partially_detected": "Partial",
|
||||
}
|
||||
|
||||
_DATA_CLASSIFICATION_TO_JIRA = {
|
||||
"public_release": "Public Release",
|
||||
"general_use": "General Use",
|
||||
"confidential": "Confidential",
|
||||
"restricted": "Restricted",
|
||||
}
|
||||
|
||||
# Assign logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -667,6 +675,10 @@ def auto_create_test_issue(
|
||||
JIRA_FIELD_SEVERITY: severity,
|
||||
}
|
||||
|
||||
data_sensitivity = _DATA_CLASSIFICATION_TO_JIRA.get(_enum_value(test.data_classification))
|
||||
if data_sensitivity:
|
||||
fields[JIRA_FIELD_DATA_SENSITIVITY] = data_sensitivity
|
||||
|
||||
# Inherit campaign start date if available, otherwise use today
|
||||
from datetime import date as _date
|
||||
effective_start = campaign_start_date or _date.today()
|
||||
|
||||
@@ -18,7 +18,7 @@ from app.domain.errors import (
|
||||
EntityNotFoundError,
|
||||
PermissionViolation,
|
||||
)
|
||||
from app.models.enums import TestState
|
||||
from app.models.enums import DataClassification, TestState
|
||||
from app.models.technique import Technique
|
||||
from app.models.test import Test
|
||||
from app.models.test_template import TestTemplate
|
||||
@@ -26,6 +26,27 @@ from app.models.campaign import CampaignTest
|
||||
from app.models.audit import AuditLog
|
||||
from app.utils import escape_like
|
||||
|
||||
# Tactics whose test data is more likely to touch customer content, PII, or
|
||||
# high-impact operational detail — these get an initial classification of
|
||||
# 'restricted' rather than the 'confidential' baseline. This is a starting
|
||||
# point only; the assigned operator or lead can always correct it.
|
||||
_RESTRICTED_TACTICS = frozenset({
|
||||
"exfiltration", "collection", "credential-access", "impact",
|
||||
})
|
||||
|
||||
|
||||
def determine_initial_classification(technique: Technique | None) -> str:
|
||||
"""Best-effort initial data classification for a new test.
|
||||
|
||||
Every security test reveals internal attack/defense posture, so the
|
||||
baseline is 'confidential' (internal use only) — never public or
|
||||
general use by default. Escalated to 'restricted' when the technique's
|
||||
tactic implies customer data, PII, or destructive impact.
|
||||
"""
|
||||
if technique and technique.tactic and technique.tactic.lower() in _RESTRICTED_TACTICS:
|
||||
return DataClassification.restricted.value
|
||||
return DataClassification.confidential.value
|
||||
|
||||
|
||||
# Define function list_tests
|
||||
def list_tests(
|
||||
@@ -121,6 +142,8 @@ def create_test(
|
||||
# Raise EntityNotFoundError
|
||||
raise EntityNotFoundError("Technique", str(technique_id))
|
||||
|
||||
data_classification = fields.pop("data_classification", None) or determine_initial_classification(technique)
|
||||
|
||||
# Assign test = Test(
|
||||
test = Test(
|
||||
# Keyword argument: technique_id
|
||||
@@ -130,6 +153,7 @@ def create_test(
|
||||
# Keyword argument: state
|
||||
state=TestState.draft,
|
||||
created_at=datetime.utcnow(), # explicit — DB column has no server default
|
||||
data_classification=data_classification,
|
||||
**fields,
|
||||
)
|
||||
# Stage new record(s) for database insertion
|
||||
@@ -223,6 +247,7 @@ def create_test_from_template(
|
||||
# Keyword argument: state
|
||||
state=TestState.draft,
|
||||
created_at=datetime.utcnow(), # explicit — DB column has no server default
|
||||
data_classification=determine_initial_classification(technique),
|
||||
)
|
||||
# Stage new record(s) for database insertion
|
||||
db.add(test)
|
||||
|
||||
Reference in New Issue
Block a user