From cea9047886e4ca5126e2a66233d1428a9a035e8c Mon Sep 17 00:00:00 2001 From: Aaron Herman Date: Tue, 3 Jun 2025 15:17:16 -0500 Subject: [PATCH 1/6] enhancement(events): prompt assignee for triage --- src/dispatch/case/flows.py | 4 ++ src/dispatch/case/messaging.py | 51 +++++++++++++++++++ src/dispatch/conversation/enums.py | 1 + .../plugins/dispatch_slack/case/enums.py | 1 + .../dispatch_slack/case/interactive.py | 38 +++++++++++++- src/dispatch/plugins/dispatch_slack/fields.py | 28 ++++++++++ src/dispatch/plugins/dispatch_slack/plugin.py | 1 + 7 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/dispatch/case/flows.py b/src/dispatch/case/flows.py index 3096f4410267..bf627bd52ca4 100644 --- a/src/dispatch/case/flows.py +++ b/src/dispatch/case/flows.py @@ -42,6 +42,7 @@ send_case_rating_feedback_message, send_case_update_notifications, send_event_paging_message, + send_event_update_prompt_reminder ) from .models import Case from .service import get @@ -309,6 +310,9 @@ def case_new_create_flow( send_event_paging_message(case, db_session, oncall_name) + # send reminder to assignee to update the security event + send_event_update_prompt_reminder(case, db_session) + if case and case.case_type.auto_close: # we transition the case to the closed state if its case type has auto close enabled case_auto_close_flow(case=case, db_session=db_session) diff --git a/src/dispatch/case/messaging.py b/src/dispatch/case/messaging.py index 1bfd8b610add..5e08c8f90a20 100644 --- a/src/dispatch/case/messaging.py +++ b/src/dispatch/case/messaging.py @@ -35,6 +35,7 @@ from dispatch.config import DISPATCH_UI_URL from dispatch.email_templates.models import EmailTemplates from dispatch.plugin import service as plugin_service +from dispatch.plugins.dispatch_slack.models import SubjectMetadata from dispatch.event import service as event_service from dispatch.notification import service as notification_service @@ -377,6 +378,56 @@ def send_case_welcome_participant_message( log.debug(f"Welcome ephemeral message sent to {participant_email}.") +def send_event_update_prompt_reminder(case: Case, db_session: Session) -> None: + """ + Sends an empemeral message to the assignee reminding them to update the visibility, title, priority + """ + message_text = "Event Triage Reminder" + + plugin = plugin_service.get_active_instance( + db_session=db_session, project_id=case.project.id, plugin_type="conversation" + ) + if plugin is None: + log.warning("Event update prompt message not sent. No conversation plugin enabled.") + return + if case.assignee is None: + log.warning(f"Event update prompt message not sent. No assignee for {case.name}.") + return + + button_metadata = SubjectMetadata( + type="case", + organization_slug=case.project.organization.slug, + id=case.id, + ).json() + + plugin.instance.send_ephemeral( + conversation_id=case.conversation.channel_id, + user=case.assignee.individual.email, + text=message_text, + blocks=[ + { + "type": "section", + "text": { + "type": "plain_text", + "text": f"Update the title, priority and visibility during triage of this security event.", # noqa + }, + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": {"type": "plain_text", "text": "Update Case"}, + "action_id": "case-update", + "value": button_metadata + } + ], + }, + ], + ) + + log.debug(f"Security Event update reminder sent to {case.assignee.individual.email}.") + def send_event_paging_message(case: Case, db_session: Session, oncall_name: str) -> None: """ Sends a message to the case conversation channel to notify the reporter that they can engage diff --git a/src/dispatch/conversation/enums.py b/src/dispatch/conversation/enums.py index 643d4767bd7a..7906545515ab 100644 --- a/src/dispatch/conversation/enums.py +++ b/src/dispatch/conversation/enums.py @@ -10,6 +10,7 @@ class ConversationCommands(DispatchEnum): report_incident = "report-incident" tactical_report = "tactical-report" update_incident = "update-incident" + escalate_case = "escalate-case" class ConversationButtonActions(DispatchEnum): diff --git a/src/dispatch/plugins/dispatch_slack/case/enums.py b/src/dispatch/plugins/dispatch_slack/case/enums.py index 5291f82b5130..a55b211df837 100644 --- a/src/dispatch/plugins/dispatch_slack/case/enums.py +++ b/src/dispatch/plugins/dispatch_slack/case/enums.py @@ -15,6 +15,7 @@ class CaseNotificationActions(DispatchEnum): resolve = "case-notification-resolve" triage = "case-notification-triage" user_mfa = "case-notification-user-mfa" + update = "case-update" class CasePaginateActions(DispatchEnum): diff --git a/src/dispatch/plugins/dispatch_slack/case/interactive.py b/src/dispatch/plugins/dispatch_slack/case/interactive.py index 6a865ba0cf92..c7ac50f81af2 100644 --- a/src/dispatch/plugins/dispatch_slack/case/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/case/interactive.py @@ -73,6 +73,7 @@ case_resolution_reason_select, case_status_select, case_type_select, + case_visibility_select, description_input, entity_select, extension_request_checkbox, @@ -265,7 +266,11 @@ def handle_update_case_command( Context( elements=[ MarkdownText( - text=f"Note: Cases cannot be escalated here. Please use the `{context['config'].slack_command_escalate_case}` slash command." + text=( + "Note: Cases cannot be escalated here. Please use the " + f"{SlackConversationConfiguration.model_json_schema()['properties']['slack_command_escalate_case']['default']} " + "slash command." + ) ) ] ), @@ -280,6 +285,11 @@ def handle_update_case_command( project_id=case.project.id, optional=True, ), + case_visibility_select( + db_session=db_session, + initial_option={"text": case.visibility, "value": case.visibility}, + project_id=case.project.id, + ) ] modal = Modal( @@ -1639,6 +1649,26 @@ def create_channel_button_click( client.views_open(trigger_id=body["trigger_id"], view=modal) +@app.action( + CaseNotificationActions.update, + middleware=[button_context_middleware, db_middleware, user_middleware], +) +def update_case_button_click( + ack: Ack, + body: dict, + client: WebClient, + context: BoltContext, + db_session: Session, +): + return handle_update_case_command( + ack=ack, + body=body, + client=client, + context=context, + db_session=db_session, + ) + + @app.action( CaseNotificationActions.user_mfa, middleware=[button_context_middleware, db_middleware, user_middleware], @@ -2007,6 +2037,10 @@ def handle_edit_submission_event( if form_data.get(DefaultBlockIds.case_type_select): case_type = {"name": form_data[DefaultBlockIds.case_type_select]["name"]} + case_visibility = case.visibility + if form_data.get(DefaultBlockIds.case_visibility_select): + case_visibility = form_data[DefaultBlockIds.case_visibility_select]["name"] + assignee_email = None if form_data.get(DefaultBlockIds.case_assignee_select): assignee_email = client.users_info( @@ -2023,7 +2057,7 @@ def handle_edit_submission_event( resolution=form_data[DefaultBlockIds.resolution_input], resolution_reason=resolution_reason, status=form_data[DefaultBlockIds.case_status_select]["name"], - visibility=case.visibility, + visibility=case_visibility, case_priority=case_priority, case_type=case_type, ) diff --git a/src/dispatch/plugins/dispatch_slack/fields.py b/src/dispatch/plugins/dispatch_slack/fields.py index bae73e41a1b2..b54cbe20f9bc 100644 --- a/src/dispatch/plugins/dispatch_slack/fields.py +++ b/src/dispatch/plugins/dispatch_slack/fields.py @@ -55,6 +55,7 @@ class DefaultBlockIds(DispatchEnum): case_status_select = "case-status-select" case_severity_select = "case-severity-select" case_type_select = "case-type-select" + case_visibility_select = "case-visibility-select" case_assignee_select = "case-assignee-select" # entities @@ -94,6 +95,7 @@ class DefaultActionIds(DispatchEnum): case_status_select = "case-status-select" case_severity_select = "case-severity-select" case_type_select = "case-type-select" + case_visibility_select = "case-visibility-select" # entities entity_select = "entity-select" @@ -684,6 +686,32 @@ def case_type_select( ) +def case_visibility_select( + db_session: Session, + action_id: str = DefaultActionIds.case_visibility_select, + block_id: str = DefaultBlockIds.case_visibility_select, + label: str = "Case Visibility", + initial_option: dict | None = None, + project_id: int = None, + **kwargs, +): + """Creates a case visibility select.""" + visibility = [ + {"text": "Restricted", "value": "Restricted"}, + {"text": "Open", "value": "Open"} + ] + + return static_select_block( + placeholder="Select Visibility", + options=visibility, + initial_option=initial_option, + action_id=action_id, + block_id=block_id, + label=label, + **kwargs, + ) + + def entity_select( signal_id: int, db_session: Session, diff --git a/src/dispatch/plugins/dispatch_slack/plugin.py b/src/dispatch/plugins/dispatch_slack/plugin.py index f457674129f0..5e10e0fafc2d 100644 --- a/src/dispatch/plugins/dispatch_slack/plugin.py +++ b/src/dispatch/plugins/dispatch_slack/plugin.py @@ -412,6 +412,7 @@ def get_command_name(self, command: str): ConversationCommands.list_participants: self.configuration.slack_command_list_participants, ConversationCommands.list_tasks: self.configuration.slack_command_list_tasks, ConversationCommands.tactical_report: self.configuration.slack_command_report_tactical, + ConversationCommands.escalate_case: self.configuration.slack_command_escalate_case, } return command_mappings.get(command, []) From 62ee4cc650885f3dc82b9a67643f3a8d2ab2d3be Mon Sep 17 00:00:00 2001 From: Aaron Herman Date: Tue, 3 Jun 2025 15:18:40 -0500 Subject: [PATCH 2/6] update styling of Slack button --- src/dispatch/case/messaging.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dispatch/case/messaging.py b/src/dispatch/case/messaging.py index 5e08c8f90a20..0c8efb6d7f2e 100644 --- a/src/dispatch/case/messaging.py +++ b/src/dispatch/case/messaging.py @@ -419,6 +419,7 @@ def send_event_update_prompt_reminder(case: Case, db_session: Session) -> None: "type": "button", "text": {"type": "plain_text", "text": "Update Case"}, "action_id": "case-update", + "style": "primary", "value": button_metadata } ], From 95b8e12524c0bf021047eec564698e1390ce0db3 Mon Sep 17 00:00:00 2001 From: Aaron Herman Date: Tue, 3 Jun 2025 15:25:46 -0500 Subject: [PATCH 3/6] Update src/dispatch/case/messaging.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Aaron Herman --- src/dispatch/case/messaging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dispatch/case/messaging.py b/src/dispatch/case/messaging.py index 0c8efb6d7f2e..0aa1c5d3768e 100644 --- a/src/dispatch/case/messaging.py +++ b/src/dispatch/case/messaging.py @@ -380,7 +380,7 @@ def send_case_welcome_participant_message( def send_event_update_prompt_reminder(case: Case, db_session: Session) -> None: """ - Sends an empemeral message to the assignee reminding them to update the visibility, title, priority + Sends an ephemeral message to the assignee reminding them to update the visibility, title, priority """ message_text = "Event Triage Reminder" From 6d83dbdbdf48b5b6df2e743c1b837cfb2bbc8a6e Mon Sep 17 00:00:00 2001 From: Aaron Herman Date: Tue, 3 Jun 2025 15:35:00 -0500 Subject: [PATCH 4/6] Clean up --- src/dispatch/case/messaging.py | 5 +++-- src/dispatch/plugins/dispatch_slack/case/interactive.py | 2 -- src/dispatch/plugins/dispatch_slack/fields.py | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/dispatch/case/messaging.py b/src/dispatch/case/messaging.py index 0c8efb6d7f2e..32f1e82a5e4c 100644 --- a/src/dispatch/case/messaging.py +++ b/src/dispatch/case/messaging.py @@ -36,6 +36,7 @@ from dispatch.email_templates.models import EmailTemplates from dispatch.plugin import service as plugin_service from dispatch.plugins.dispatch_slack.models import SubjectMetadata +from dispatch.plugins.dispatch_slack.case.enums import CaseNotificationActions from dispatch.event import service as event_service from dispatch.notification import service as notification_service @@ -409,7 +410,7 @@ def send_event_update_prompt_reminder(case: Case, db_session: Session) -> None: "type": "section", "text": { "type": "plain_text", - "text": f"Update the title, priority and visibility during triage of this security event.", # noqa + "text": f"Update the title, priority, case type and visibility during triage of this security event.", # noqa }, }, { @@ -418,7 +419,7 @@ def send_event_update_prompt_reminder(case: Case, db_session: Session) -> None: { "type": "button", "text": {"type": "plain_text", "text": "Update Case"}, - "action_id": "case-update", + "action_id": CaseNotificationActions.update, "style": "primary", "value": button_metadata } diff --git a/src/dispatch/plugins/dispatch_slack/case/interactive.py b/src/dispatch/plugins/dispatch_slack/case/interactive.py index c7ac50f81af2..6fd4fb19edd5 100644 --- a/src/dispatch/plugins/dispatch_slack/case/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/case/interactive.py @@ -286,9 +286,7 @@ def handle_update_case_command( optional=True, ), case_visibility_select( - db_session=db_session, initial_option={"text": case.visibility, "value": case.visibility}, - project_id=case.project.id, ) ] diff --git a/src/dispatch/plugins/dispatch_slack/fields.py b/src/dispatch/plugins/dispatch_slack/fields.py index b54cbe20f9bc..3460cc348ae1 100644 --- a/src/dispatch/plugins/dispatch_slack/fields.py +++ b/src/dispatch/plugins/dispatch_slack/fields.py @@ -687,12 +687,10 @@ def case_type_select( def case_visibility_select( - db_session: Session, action_id: str = DefaultActionIds.case_visibility_select, block_id: str = DefaultBlockIds.case_visibility_select, label: str = "Case Visibility", initial_option: dict | None = None, - project_id: int = None, **kwargs, ): """Creates a case visibility select.""" From 5e73e2dfb5a05041c1bf0a161eac173265bd1092 Mon Sep 17 00:00:00 2001 From: Aaron Herman Date: Tue, 3 Jun 2025 15:37:43 -0500 Subject: [PATCH 5/6] Update src/dispatch/plugins/dispatch_slack/case/interactive.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Aaron Herman --- src/dispatch/plugins/dispatch_slack/case/interactive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dispatch/plugins/dispatch_slack/case/interactive.py b/src/dispatch/plugins/dispatch_slack/case/interactive.py index c7ac50f81af2..a2baa6da542b 100644 --- a/src/dispatch/plugins/dispatch_slack/case/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/case/interactive.py @@ -2039,7 +2039,7 @@ def handle_edit_submission_event( case_visibility = case.visibility if form_data.get(DefaultBlockIds.case_visibility_select): - case_visibility = form_data[DefaultBlockIds.case_visibility_select]["name"] + case_visibility = form_data[DefaultBlockIds.case_visibility_select]["value"] assignee_email = None if form_data.get(DefaultBlockIds.case_assignee_select): From 0918c984f6991399f167db828d370e1d810e932a Mon Sep 17 00:00:00 2001 From: Aaron Herman Date: Wed, 4 Jun 2025 06:23:24 -0500 Subject: [PATCH 6/6] Use enums for open and restricted --- src/dispatch/plugins/dispatch_slack/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dispatch/plugins/dispatch_slack/fields.py b/src/dispatch/plugins/dispatch_slack/fields.py index 3460cc348ae1..888e6cd505cd 100644 --- a/src/dispatch/plugins/dispatch_slack/fields.py +++ b/src/dispatch/plugins/dispatch_slack/fields.py @@ -18,7 +18,7 @@ from dispatch.case.severity import service as case_severity_service from dispatch.case.type import service as case_type_service from dispatch.entity import service as entity_service -from dispatch.enums import DispatchEnum +from dispatch.enums import DispatchEnum, Visibility from dispatch.incident.enums import IncidentStatus from dispatch.incident.priority import service as incident_priority_service from dispatch.incident.severity import service as incident_severity_service @@ -695,8 +695,8 @@ def case_visibility_select( ): """Creates a case visibility select.""" visibility = [ - {"text": "Restricted", "value": "Restricted"}, - {"text": "Open", "value": "Open"} + {"text": Visibility.restricted, "value": Visibility.restricted}, + {"text": Visibility.open, "value": Visibility.open} ] return static_select_block(