fix(jira): correct Data Sensitivity field ID and replicate Jira's real classification scheme
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
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
The auto-create-ticket call was sending customfield_11233 (a field that doesn't exist in the project) instead of the real Data Sensitivity field customfield_11814, which made Jira reject the WHOLE issue and silently block ticket creation for every new test. Also replaces Aegis's invented 4-tier data_classification scheme (public_release/general_use/confidential/restricted) with the 6 values Jira's Data Sensitivity field actually uses (public/general_use/ internal_use_only/trusted_people/customer_content/pii), since that is the classification the organization actually applies. Includes a data migration remapping existing rows and a defensive retry if Jira ever rejects an unscreened custom field again.
This commit is contained in:
@@ -279,6 +279,110 @@ def test_search_jira_issues_maps_fields(mock_get_client):
|
||||
mock_jira.jql.assert_called_once()
|
||||
|
||||
|
||||
def _make_technique(**overrides):
|
||||
from app.models.technique import Technique
|
||||
tech = MagicMock(spec=Technique)
|
||||
tech.mitre_id = "T1003.006"
|
||||
tech.name = "DCSync"
|
||||
tech.tactic = "credential-access"
|
||||
tech.severity = "high"
|
||||
for k, v in overrides.items():
|
||||
setattr(tech, k, v)
|
||||
return tech
|
||||
|
||||
|
||||
def _make_test_for_issue(**overrides):
|
||||
from app.models.test import Test
|
||||
t = MagicMock(spec=Test)
|
||||
t.id = uuid4()
|
||||
t.name = "Run DSInternals Get-ADReplAccount"
|
||||
t.technique_id = uuid4()
|
||||
t.procedure_text = "Invoke-DSInternals"
|
||||
t.description = "desc"
|
||||
t.platform = "Windows"
|
||||
t.tool_used = "DSInternals"
|
||||
t.data_classification = "pii"
|
||||
for k, v in overrides.items():
|
||||
setattr(t, k, v)
|
||||
return t
|
||||
|
||||
|
||||
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
||||
@patch("app.services.jira_service.get_jira_project_key", return_value="SEC")
|
||||
@patch("app.services.jira_service.get_jira_parent_ticket_standalone", return_value=None)
|
||||
@patch("app.services.jira_service.get_admin_jira_client")
|
||||
def test_auto_create_test_issue_retries_without_unscreened_data_sensitivity_field(
|
||||
mock_get_client, mock_parent, mock_project_key, mock_configured, db
|
||||
):
|
||||
# Regression: Jira rejects the WHOLE issue when the data sensitivity
|
||||
# field isn't on the project's create screen, which would silently
|
||||
# block every test's ticket creation. Must retry once without that field
|
||||
# instead of losing the ticket.
|
||||
mock_jira = MagicMock()
|
||||
mock_jira.issue_create.side_effect = [
|
||||
Exception(
|
||||
f"Field '{jira_service.JIRA_FIELD_DATA_SENSITIVITY}' cannot be set. "
|
||||
"It is not on the appropriate screen, or unknown."
|
||||
),
|
||||
{"key": "SEC-42", "id": "10042"},
|
||||
]
|
||||
mock_get_client.return_value = mock_jira
|
||||
from app.models.user import User
|
||||
test = _make_test_for_issue()
|
||||
technique = _make_technique()
|
||||
actor = User(username="gerardo-ruiz", email="gerardo.ruiz@kaseya.com", hashed_password="x")
|
||||
db.add(actor)
|
||||
db.commit()
|
||||
|
||||
issue_key = jira_service.auto_create_test_issue(db, test, actor, technique=technique)
|
||||
|
||||
assert issue_key == "SEC-42"
|
||||
assert mock_jira.issue_create.call_count == 2
|
||||
# Both calls share the same `fields` dict object (mutated in place), so
|
||||
# only the final state is inspectable here — confirming the retry
|
||||
# dropped the field is what matters.
|
||||
final_call_fields = mock_jira.issue_create.call_args_list[1].kwargs["fields"]
|
||||
assert jira_service.JIRA_FIELD_DATA_SENSITIVITY not in final_call_fields
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data_classification,expected_jira_value",
|
||||
[
|
||||
("public", "Public"),
|
||||
("general_use", "General Use"),
|
||||
("internal_use_only", "Internal Use Only"),
|
||||
("trusted_people", "Trusted People"),
|
||||
("customer_content", "Customer Content"),
|
||||
("pii", "PII"),
|
||||
],
|
||||
)
|
||||
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
||||
@patch("app.services.jira_service.get_jira_project_key", return_value="SEC")
|
||||
@patch("app.services.jira_service.get_jira_parent_ticket_standalone", return_value=None)
|
||||
@patch("app.services.jira_service.get_admin_jira_client")
|
||||
def test_auto_create_test_issue_maps_data_classification_to_real_jira_field(
|
||||
mock_get_client, mock_parent, mock_project_key, mock_configured, db,
|
||||
data_classification, expected_jira_value,
|
||||
):
|
||||
# Regression: the field ID was wrong (customfield_11233, which doesn't
|
||||
# exist in the project) and the classification scheme didn't match
|
||||
# Jira's actual 6-option "Data Sensitivity" field at all.
|
||||
mock_jira = MagicMock()
|
||||
mock_jira.issue_create.return_value = {"key": "SEC-1", "id": "1"}
|
||||
mock_get_client.return_value = mock_jira
|
||||
from app.models.user import User
|
||||
test = _make_test_for_issue(data_classification=data_classification)
|
||||
technique = _make_technique()
|
||||
actor = User(username="gerardo-ruiz", email="gerardo.ruiz@kaseya.com", hashed_password="x")
|
||||
db.add(actor)
|
||||
db.commit()
|
||||
|
||||
jira_service.auto_create_test_issue(db, test, actor, technique=technique)
|
||||
|
||||
fields = mock_jira.issue_create.call_args.kwargs["fields"]
|
||||
assert fields["customfield_11814"] == expected_jira_value
|
||||
|
||||
|
||||
def _make_user(**overrides):
|
||||
from app.models.user import User
|
||||
u = MagicMock(spec=User)
|
||||
|
||||
Reference in New Issue
Block a user