From bdc6579c10264ad9d3509174be58c089a7cbddb8 Mon Sep 17 00:00:00 2001 From: kitos Date: Tue, 14 Jul 2026 12:48:14 +0200 Subject: [PATCH] fix(jira): stop sending numeric hours to Time to Detect/Contain (datetime fields) Confirmed via issue_editmeta and a real production failure log: Time to Detect / Time to Contain are Jira datetime fields despite the name, not numeric duration fields. Pushing a computed hour count made Jira reject the entire fields payload (validated atomically), silently sinking BT End Date and Attack Detected on the same call even though only these two fields were actually invalid. Now pushes the real detection_time/ containment_time timestamps. Removed the now-unused _compute_hours. --- backend/app/services/jira_service.py | 28 +++++++++++++--------------- backend/tests/test_jira_service.py | 13 ++++++++----- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/backend/app/services/jira_service.py b/backend/app/services/jira_service.py index e68fe75..96796b1 100644 --- a/backend/app/services/jira_service.py +++ b/backend/app/services/jira_service.py @@ -1246,14 +1246,6 @@ def _jira_datetime(dt: datetime) -> str: return dt.strftime("%Y-%m-%dT%H:%M:%S.000+0000") -def _compute_hours(start: Optional[datetime], end: Optional[datetime]) -> Optional[float]: - """Return the elapsed time between two timestamps in hours, or None if unavailable.""" - if not start or not end: - return None - delta_hours = (end - start).total_seconds() / 3600 - return round(delta_hours, 2) if delta_hours >= 0 else None - - def _update_test_fields(db: Session, test: Test, fields: dict) -> None: """Best-effort update of custom fields on the Jira issue linked to *test*. @@ -1343,7 +1335,15 @@ def push_bt_started(db: Session, test: Test) -> None: def push_bt_submitted(db: Session, test: Test) -> None: - """Set BT End Date, Attack Detected/Contained, and time-to-detect/contain — Blue submits for review.""" + """Set BT End Date, Attack Detected/Contained, and Time to Detect/Contain — Blue submits for review. + + Time to Detect / Time to Contain are Jira **datetime** fields (confirmed + via issue_editmeta), not numeric duration fields — despite the name. + This used to push a computed hour count, which Jira rejected outright; + since Jira validates the whole fields payload atomically, that one bad + field silently sank BT End Date and Attack Detected too. Now pushes the + actual detection_time/containment_time timestamps. + """ fields: dict = {JIRA_FIELD_BT_END_DATE: _jira_datetime(datetime.utcnow())} detection_result = _enum_value(test.detection_result) @@ -1354,13 +1354,11 @@ def push_bt_submitted(db: Session, test: Test) -> None: if containment_result in _CONTAINMENT_TO_JIRA: fields[JIRA_FIELD_ATTACK_CONTAINED] = _select_field(_CONTAINMENT_TO_JIRA[containment_result]) - time_to_detect = _compute_hours(test.execution_end_time, test.detection_time) - if time_to_detect is not None: - fields[JIRA_FIELD_TIME_TO_DETECT] = time_to_detect + if test.detection_time: + fields[JIRA_FIELD_TIME_TO_DETECT] = _jira_datetime(test.detection_time) - time_to_contain = _compute_hours(test.detection_time, test.containment_time) - if time_to_contain is not None: - fields[JIRA_FIELD_TIME_TO_CONTAIN] = time_to_contain + if test.containment_time: + fields[JIRA_FIELD_TIME_TO_CONTAIN] = _jira_datetime(test.containment_time) _update_test_fields(db, test, fields) diff --git a/backend/tests/test_jira_service.py b/backend/tests/test_jira_service.py index b44aba1..7143cf5 100644 --- a/backend/tests/test_jira_service.py +++ b/backend/tests/test_jira_service.py @@ -570,15 +570,18 @@ def test_push_rt_submitted_rt_start_date_survives_reopen(mock_get_client, mock_c @patch("app.services.jira_service.has_admin_jira_configured", return_value=True) @patch("app.services.jira_service.get_admin_jira_client") -def test_push_bt_submitted_includes_detection_and_hours(mock_get_client, mock_configured, db): +def test_push_bt_submitted_includes_detection_and_timestamps(mock_get_client, mock_configured, db): + """Regression: Time to Detect / Time to Contain are Jira *datetime* + fields, not numeric hour counts — pushing a computed duration made Jira + reject the whole fields payload (atomic validation), silently sinking + BT End Date and Attack Detected too even though only these two were + actually wrong.""" mock_jira = MagicMock() mock_get_client.return_value = mock_jira - exec_end = datetime(2026, 1, 1, 10, 0, 0) detect = datetime(2026, 1, 1, 12, 0, 0) contain = datetime(2026, 1, 1, 13, 30, 0) test = _make_test( detection_result=MagicMock(value="detected"), - execution_end_time=exec_end, detection_time=detect, containment_time=contain, ) @@ -590,8 +593,8 @@ def test_push_bt_submitted_includes_detection_and_hours(mock_get_client, mock_co fields = mock_jira.update_issue_field.call_args[1]["fields"] 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_CONTAIN] == 1.5 + assert fields[jira_service.JIRA_FIELD_TIME_TO_DETECT] == "2026-01-01T12:00:00.000+0000" + assert fields[jira_service.JIRA_FIELD_TIME_TO_CONTAIN] == "2026-01-01T13:30:00.000+0000" @patch("app.services.jira_service.has_admin_jira_configured", return_value=True)