fix(jira): wrap select-field values in {value: ...} for Jira REST API
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

Jira Cloud's REST API rejects a bare string for select-type custom
fields with 'Specify a valid id or name for <field>' — confirmed live
against the real Jira instance. This silently broke Severity, Data
Sensitivity, Attack success, Attack detected, and Attack contained on
every ticket create/update, masked until now by the non-fatal
exception handling around each call.
This commit is contained in:
kitos
2026-07-09 17:08:53 +02:00
parent d726e3adfe
commit 119db6f91d
2 changed files with 19 additions and 9 deletions
+15 -5
View File
@@ -706,12 +706,12 @@ def auto_create_test_issue(
# customfield_10309 = Proof of Concept field (required by team's Jira config) # customfield_10309 = Proof of Concept field (required by team's Jira config)
"customfield_10309": f"{{code}}{poc}{{code}}", "customfield_10309": f"{{code}}{poc}{{code}}",
JIRA_FIELD_TTP: mitre_id, JIRA_FIELD_TTP: mitre_id,
JIRA_FIELD_SEVERITY: severity, JIRA_FIELD_SEVERITY: _select_field(severity),
} }
data_sensitivity = _DATA_CLASSIFICATION_TO_JIRA.get(_enum_value(test.data_classification)) data_sensitivity = _DATA_CLASSIFICATION_TO_JIRA.get(_enum_value(test.data_classification))
if data_sensitivity: if data_sensitivity:
fields[JIRA_FIELD_DATA_SENSITIVITY] = data_sensitivity fields[JIRA_FIELD_DATA_SENSITIVITY] = _select_field(data_sensitivity)
# Inherit campaign start date if available, otherwise use today # Inherit campaign start date if available, otherwise use today
from datetime import date as _date from datetime import date as _date
@@ -1027,6 +1027,16 @@ def _enum_value(v) -> Optional[str]:
return v.value if hasattr(v, "value") else str(v) return v.value if hasattr(v, "value") else str(v)
def _select_field(value: str) -> dict:
"""Wrap a value for a Jira single-select custom field.
Jira's REST API rejects a bare string for select-type custom fields
("Specify a valid 'id' or 'name' for <field>") — it must be posted as
``{"value": ...}``.
"""
return {"value": value}
def _compute_hours(start: Optional[datetime], end: Optional[datetime]) -> Optional[float]: def _compute_hours(start: Optional[datetime], end: Optional[datetime]) -> Optional[float]:
"""Return the elapsed time between two timestamps in hours, or None if unavailable.""" """Return the elapsed time between two timestamps in hours, or None if unavailable."""
if not start or not end: if not start or not end:
@@ -1079,7 +1089,7 @@ def push_rt_submitted(db: Session, test: Test) -> None:
fields: dict = {JIRA_FIELD_RT_END_DATE: datetime.utcnow().strftime("%Y-%m-%d")} fields: dict = {JIRA_FIELD_RT_END_DATE: datetime.utcnow().strftime("%Y-%m-%d")}
attack_success = _enum_value(test.attack_success) attack_success = _enum_value(test.attack_success)
if attack_success in _ATTACK_SUCCESS_TO_JIRA: if attack_success in _ATTACK_SUCCESS_TO_JIRA:
fields[JIRA_FIELD_ATTACK_SUCCESS] = _ATTACK_SUCCESS_TO_JIRA[attack_success] fields[JIRA_FIELD_ATTACK_SUCCESS] = _select_field(_ATTACK_SUCCESS_TO_JIRA[attack_success])
_update_test_fields(db, test, fields) _update_test_fields(db, test, fields)
@@ -1096,11 +1106,11 @@ def push_bt_submitted(db: Session, test: Test) -> None:
detection_result = _enum_value(test.detection_result) detection_result = _enum_value(test.detection_result)
if detection_result in _DETECTION_TO_JIRA: if detection_result in _DETECTION_TO_JIRA:
fields[JIRA_FIELD_ATTACK_DETECTED] = _DETECTION_TO_JIRA[detection_result] fields[JIRA_FIELD_ATTACK_DETECTED] = _select_field(_DETECTION_TO_JIRA[detection_result])
containment_result = _enum_value(test.containment_result) containment_result = _enum_value(test.containment_result)
if containment_result in _CONTAINMENT_TO_JIRA: if containment_result in _CONTAINMENT_TO_JIRA:
fields[JIRA_FIELD_ATTACK_CONTAINED] = _CONTAINMENT_TO_JIRA[containment_result] fields[JIRA_FIELD_ATTACK_CONTAINED] = _select_field(_CONTAINMENT_TO_JIRA[containment_result])
time_to_detect = _compute_hours(test.execution_end_time, test.detection_time) time_to_detect = _compute_hours(test.execution_end_time, test.detection_time)
if time_to_detect is not None: if time_to_detect is not None:
+4 -4
View File
@@ -200,7 +200,7 @@ def test_push_rt_submitted_includes_attack_success(mock_get_client, mock_configu
fields = mock_jira.update_issue_field.call_args[1]["fields"] fields = mock_jira.update_issue_field.call_args[1]["fields"]
assert fields[jira_service.JIRA_FIELD_RT_END_DATE] assert fields[jira_service.JIRA_FIELD_RT_END_DATE]
assert fields[jira_service.JIRA_FIELD_ATTACK_SUCCESS] == "Yes" assert fields[jira_service.JIRA_FIELD_ATTACK_SUCCESS] == {"value": "Yes"}
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True) @patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
@@ -224,7 +224,7 @@ def test_push_bt_submitted_includes_detection_and_hours(mock_get_client, mock_co
jira_service.push_bt_submitted(db, test) jira_service.push_bt_submitted(db, test)
fields = mock_jira.update_issue_field.call_args[1]["fields"] fields = mock_jira.update_issue_field.call_args[1]["fields"]
assert fields[jira_service.JIRA_FIELD_ATTACK_DETECTED] == "Yes" assert fields[jira_service.JIRA_FIELD_ATTACK_DETECTED] == {"value": "Yes"}
assert fields[jira_service.JIRA_FIELD_TIME_TO_DETECT] == 2.0 assert fields[jira_service.JIRA_FIELD_TIME_TO_DETECT] == 2.0
assert fields[jira_service.JIRA_FIELD_TIME_TO_CONTAIN] == 1.5 assert fields[jira_service.JIRA_FIELD_TIME_TO_CONTAIN] == 1.5
@@ -242,7 +242,7 @@ def test_push_bt_submitted_includes_attack_contained(mock_get_client, mock_confi
jira_service.push_bt_submitted(db, test) jira_service.push_bt_submitted(db, test)
fields = mock_jira.update_issue_field.call_args[1]["fields"] fields = mock_jira.update_issue_field.call_args[1]["fields"]
assert fields[jira_service.JIRA_FIELD_ATTACK_CONTAINED] == "Partial" assert fields[jira_service.JIRA_FIELD_ATTACK_CONTAINED] == {"value": "Partial"}
def test_update_test_fields_noop_when_not_configured(db): def test_update_test_fields_noop_when_not_configured(db):
@@ -380,7 +380,7 @@ def test_auto_create_test_issue_maps_data_classification_to_real_jira_field(
jira_service.auto_create_test_issue(db, test, actor, technique=technique) jira_service.auto_create_test_issue(db, test, actor, technique=technique)
fields = mock_jira.issue_create.call_args.kwargs["fields"] fields = mock_jira.issue_create.call_args.kwargs["fields"]
assert fields["customfield_11814"] == expected_jira_value assert fields["customfield_11814"] == {"value": expected_jira_value}
def _make_user(**overrides): def _make_user(**overrides):