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)