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..8242614ad531 100644 --- a/src/dispatch/case/messaging.py +++ b/src/dispatch/case/messaging.py @@ -35,6 +35,8 @@ 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.plugins.dispatch_slack.case.enums import CaseNotificationActions from dispatch.event import service as event_service from dispatch.notification import service as notification_service @@ -377,6 +379,57 @@ 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 ephemeral 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, case type and visibility during triage of this security event.", # noqa + }, + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": {"type": "plain_text", "text": "Update Case"}, + "action_id": CaseNotificationActions.update, + "style": "primary", + "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..0c79c900ba6e 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,9 @@ def handle_update_case_command( project_id=case.project.id, optional=True, ), + case_visibility_select( + initial_option={"text": case.visibility, "value": case.visibility}, + ) ] modal = Modal( @@ -1639,6 +1647,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 +2035,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]["value"] + assignee_email = None if form_data.get(DefaultBlockIds.case_assignee_select): assignee_email = client.users_info( @@ -2023,7 +2055,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..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 @@ -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,30 @@ def case_type_select( ) +def case_visibility_select( + action_id: str = DefaultActionIds.case_visibility_select, + block_id: str = DefaultBlockIds.case_visibility_select, + label: str = "Case Visibility", + initial_option: dict | None = None, + **kwargs, +): + """Creates a case visibility select.""" + visibility = [ + {"text": Visibility.restricted, "value": Visibility.restricted}, + {"text": Visibility.open, "value": Visibility.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, [])