diff --git a/backend/app/services/jira_service.py b/backend/app/services/jira_service.py index 68b2114..5463a15 100644 --- a/backend/app/services/jira_service.py +++ b/backend/app/services/jira_service.py @@ -508,6 +508,8 @@ def _build_state_comment( "", f"*Detection Result:* {test.detection_result or 'N/A'}", ] + if test.system_gaps: + lines += ["", "h4. ⚠️ System Gap Flagged", test.system_gaps] if test.blue_summary: lines += ["", "h4. Blue Team Summary", test.blue_summary] if test.remediation_steps: @@ -933,6 +935,8 @@ def push_test_event( "Could not unassign %s for state %s: %s", link.jira_issue_key, new_state, exc_a, ) + if test.system_gaps: + _add_jira_label(jira, link.jira_issue_key, "system-gap") link.last_synced_at = datetime.utcnow() db.flush() diff --git a/backend/tests/test_jira_service.py b/backend/tests/test_jira_service.py index 7143cf5..b3b3de8 100644 --- a/backend/tests/test_jira_service.py +++ b/backend/tests/test_jira_service.py @@ -72,6 +72,7 @@ def _make_test(**overrides): t.blue_validation_notes = None t.red_tech_assignee = None t.blue_tech_assignee = None + t.system_gaps = None for k, v in overrides.items(): setattr(t, k, v) return t @@ -302,6 +303,45 @@ def test_push_test_event_clears_assignee_on_in_review(mock_get_client, mock_conf mock_jira.assign_issue.assert_called_once_with(link.jira_issue_key, account_id=None) +@patch("app.services.jira_service.has_admin_jira_configured", return_value=True) +@patch("app.services.jira_service.get_admin_jira_client") +def test_push_test_event_includes_system_gap_and_labels_it(mock_get_client, mock_configured, db): + """A Blue Lead flagging a capability gap must surface it in Jira — both + in the comment (so the reason is visible) and as a label (so gap-flagged + tickets are filterable without opening each one).""" + mock_jira = MagicMock() + mock_jira.issue.return_value = {"fields": {"labels": []}} + mock_get_client.return_value = mock_jira + test = _make_test(system_gaps="Missing EDR agent on host X") + link = _make_link(test.id) + db.add(link) + db.commit() + + jira_service.push_test_event(db, test, MagicMock(username="bluelead", jira_account_id=None), "in_review") + + comment = mock_jira.issue_add_comment.call_args[0][1] + assert "Missing EDR agent on host X" in comment + mock_jira.update_issue_field.assert_called_once_with( + link.jira_issue_key, fields={"labels": ["system-gap"]}, + ) + + +@patch("app.services.jira_service.has_admin_jira_configured", return_value=True) +@patch("app.services.jira_service.get_admin_jira_client") +def test_push_test_event_in_review_without_gap_does_not_add_label(mock_get_client, mock_configured, db): + mock_jira = MagicMock() + mock_get_client.return_value = mock_jira + test = _make_test() + link = _make_link(test.id) + db.add(link) + db.commit() + + jira_service.push_test_event(db, test, MagicMock(username="alice", jira_account_id=None), "in_review") + + mock_jira.issue.assert_not_called() + mock_jira.update_issue_field.assert_not_called() + + @patch("app.services.jira_service.has_admin_jira_configured", return_value=True) @patch("app.services.jira_service.get_admin_jira_client") def test_push_test_event_clears_assignee_on_fresh_blue_queue_entry(mock_get_client, mock_configured, db): diff --git a/frontend/src/components/TestFromTemplateForm.tsx b/frontend/src/components/TestFromTemplateForm.tsx index 932b8d7..5e8ba8c 100644 --- a/frontend/src/components/TestFromTemplateForm.tsx +++ b/frontend/src/components/TestFromTemplateForm.tsx @@ -221,6 +221,19 @@ export default function TestFromTemplateForm({ + {/* Detect Suggested Procedure (read-only reference for Blue Team) */} +
+ +
+

+ {template?.detect_suggested_procedure || "No detection procedure suggested yet."} +

+
+
+ {/* Error */} {createMutation.isError && (