From 12270f259ac20cd20f7f780767a31a8edbf71fd4 Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Mon, 30 Sep 2024 12:26:53 -0700 Subject: [PATCH 1/5] Enhances Slack Case Handling and Messaging for Improved Clarity and User Interaction --- .../dispatch_slack/case/interactive.py | 16 +- .../plugins/dispatch_slack/case/messages.py | 199 ++++++++---------- src/dispatch/plugins/dispatch_slack/plugin.py | 14 +- 3 files changed, 102 insertions(+), 127 deletions(-) diff --git a/src/dispatch/plugins/dispatch_slack/case/interactive.py b/src/dispatch/plugins/dispatch_slack/case/interactive.py index a4d6bf587424..5385c2458cf6 100644 --- a/src/dispatch/plugins/dispatch_slack/case/interactive.py +++ b/src/dispatch/plugins/dispatch_slack/case/interactive.py @@ -2293,20 +2293,20 @@ def send_engagement_response( if response == MfaChallengeStatus.APPROVED: title = "Approve" text = "Confirmation... Success!" - message_text = f":white_check_mark: {engaged_user} confirmed the behavior *is expected*.\n\n *Context Provided* \n```{context_from_user}```" + message_text = f":white_check_mark: {engaged_user} confirmed the behavior as expected.\n\n *Context Provided* \n```{context_from_user}```" engagement_status = SignalEngagementStatus.approved else: title = "MFA Failed" engagement_status = SignalEngagementStatus.denied if response == MfaChallengeStatus.EXPIRED: - text = "Confirmation failed, the MFA request timed out. Please have your MFA device ready to accept the push notification and try again." + text = "Confirmation failed, the MFA request timed out. Please, have your MFA device ready to accept the push notification and try again." elif response == MfaChallengeStatus.DENIED: - text = "User not found in MFA provider. To validate your identity, please register in Duo and try again." + text = f"User {engaged_user} not found in MFA provider. To validate your identity, please register in Duo and try again." else: text = "Confirmation failed. You must accept the MFA prompt." - message_text = f":warning: {engaged_user} attempted to confirm the behavior *as expected*, but the MFA validation failed.\n\n **Error Reason**: `{response}`\n\n{text}\n\n *Context Provided* \n```{context_from_user}```\n\n" + message_text = f":warning: {engaged_user} attempted to confirm the behavior as expected, but we ran into an error during MFA validation (`{response}`)\n\n{text}\n\n *Context Provided* \n```{context_from_user}```\n\n" send_success_modal( client=client, @@ -2365,7 +2365,7 @@ def resolve_case( case_in = CaseUpdate( title=case.title, resolution_reason=CaseResolutionReason.user_acknowledge, - resolution=f"Case resolved through user engagement. User context: {context_from_user}", + resolution=context_from_user, visibility=case.visibility, status=CaseStatus.closed, closed_at=datetime.utcnow(), @@ -2385,12 +2385,6 @@ def resolve_case( client.chat_update( blocks=blocks, ts=case.conversation.thread_id, channel=case.conversation.channel_id ) - client.chat_postMessage( - text="Case has been resolved.", - channel=case.conversation.channel_id, - thread_ts=case.conversation.thread_id, - ) - @app.action( SignalEngagementActions.deny, diff --git a/src/dispatch/plugins/dispatch_slack/case/messages.py b/src/dispatch/plugins/dispatch_slack/case/messages.py index c4fd2993c601..4a34ed6778e1 100644 --- a/src/dispatch/plugins/dispatch_slack/case/messages.py +++ b/src/dispatch/plugins/dispatch_slack/case/messages.py @@ -65,13 +65,26 @@ def map_priority_color(color: str) -> str: def create_case_message(case: Case, channel_id: str) -> list[Block]: + """ + Creates a Slack message for a given case. + + Args: + case (Case): The case object containing details to be included in the message. + channel_id (str): The ID of the Slack channel where the message will be sent. + + Returns: + list[Block]: A list of Block objects representing the structure of the Slack message. + """ priority_color = map_priority_color(color=case.case_priority.color) + title_prefix = "*Detection*" if case.signal_instances else "*Title*" + title = f"{title_prefix} \n {case.title}." + fields = [ f"*Assignee* \n {case.assignee.individual.email}", f"*Status* \n {case.status}", - f"*Type* \n {case.case_type.name}", - f"*Priority* \n {priority_color} {case.case_priority.name}", + f"*Case Type* \n {case.case_type.name}", + f"*Case Priority* \n {priority_color} {case.case_priority.name}", ] if case.signal_instances: @@ -79,17 +92,17 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: fields.append(f"*Variant* \n {variant}") blocks = [ - Context(elements=[f"* {case.name} - Case Details*"]), Section( - text=f"*Title* \n {case.title}.", + text=title, accessory=Button( - text="Open in Dispatch", + text="View in Dispatch", action_id="button-link", url=f"{DISPATCH_UI_URL}/{case.project.organization.slug}/cases/{case.name}", ), ), Section(text=f"*Description* \n {case.description}"), Section(fields=fields), + Section(text="*Actions*"), ] button_metadata = SubjectMetadata( @@ -102,17 +115,16 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: if case.has_channel: action_buttons = [ - Button(text="Case Channel", style="primary", url=case.conversation.weblink) + Button(text=":slack: Case Channel", style="primary", url=case.conversation.weblink) ] blocks.extend([Actions(elements=action_buttons)]) - elif case.status == CaseStatus.escalated: blocks.extend( [ Actions( elements=[ Button( - text="Join Incident", + text=":siren: Join Incident", action_id=CaseNotificationActions.join_incident, style="primary", value=button_metadata, @@ -121,7 +133,6 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: ) ] ) - elif case.status == CaseStatus.closed: blocks.extend( [ @@ -144,13 +155,13 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: else: action_buttons = [ Button( - text="Resolve", + text=":resolved: Resolve", action_id=CaseNotificationActions.resolve, style="primary", value=button_metadata, ), Button( - text="Edit", + text=":modified: Edit", action_id=CaseNotificationActions.edit, value=button_metadata, ), @@ -160,7 +171,7 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: value=button_metadata, ), Button( - text="🔥 Escalate", + text=":fire: Escalate", action_id=CaseNotificationActions.escalate, style="danger", value=button_metadata, @@ -170,7 +181,7 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: action_buttons.insert( 0, Button( - text="Triage", + text=":mag: Triage", action_id=CaseNotificationActions.triage, style="primary", value=button_metadata, @@ -187,8 +198,17 @@ class EntityGroup(NamedTuple): def create_signal_messages(case_id: int, channel_id: str, db_session: Session) -> list[Message]: - """Creates the signal instance message.""" + """ + Creates signal messages for a given case. + Args: + case_id (int): The ID of the case for which to create signal messages. + channel_id (str): The ID of the Slack channel where the message will be sent. + db_session (Session): The database session to use for querying signal instances. + + Returns: + list[Message]: A list of Message objects representing the structure of the Slack messages. + """ instances = signal_service.get_instances_in_case(db_session=db_session, case_id=case_id) (first_instance_id, first_instance_signal) = instances.first() num_of_instances = instances.count() @@ -203,14 +223,7 @@ def create_signal_messages(case_id: int, channel_id: str, db_session: Session) - channel_id=channel_id, ).json() - # Define the initial elements with "Raw Data" and "Snooze" buttons - elements = [ - Button( - text="💤 Snooze", - action_id=SignalNotificationActions.snooze, - value=button_metadata, - ), - ] + elements = [] # Check if `first_instance_signal.external_url` is not empty if first_instance_signal.external_url: @@ -223,74 +236,21 @@ def create_signal_messages(case_id: int, channel_id: str, db_session: Session) - ) ) - # Create the Actions block with the elements - signal_metadata_blocks = [ - Actions(elements=elements), - Section(text="*Alerts*"), - Divider(), - Section(text=f"{num_of_instances} alerts observed in this case."), - Section(text="\n*Entities*"), - Divider(), - ] - - entities_query = ( - db_session.query(Entity.id, Entity.value, EntityType.name) - .join(EntityType, Entity.entity_type_id == EntityType.id) - .join( - assoc_signal_instance_entities, assoc_signal_instance_entities.c.entity_id == Entity.id - ) - .join( - SignalInstance, assoc_signal_instance_entities.c.signal_instance_id == SignalInstance.id + # Define the initial elements with "Raw Data" and "Snooze" buttons + elements.append( + Button( + text="💤 Snooze Alerts", + action_id=SignalNotificationActions.snooze, + value=button_metadata, ) - .filter(SignalInstance.case_id == case_id) ) - has_entities = db_session.query(entities_query.exists()).scalar() - if not has_entities: - signal_metadata_blocks.append( - Section( - text="No entities found.", - ), - ) - return Message(blocks=signal_metadata_blocks).build()["blocks"] - - entity_groups = defaultdict(list) - processed_entities = set() - - for entity_id, entity_value, entity_type_name in entities_query: - if entity_value not in processed_entities: - processed_entities.add(entity_value) - # Fetch the count of related cases with entities in the past 14 days - entity_case_counts = entity_service.get_case_count_with_entity( - db_session=db_session, entity_id=entity_id, days_back=14 - ) - entity_groups[entity_type_name].append( - EntityGroup( - value=entity_value, - related_case_count=entity_case_counts, - ) - ) - - for k, v in entity_groups.items(): - if v: - entity_group = v[0] - case_message = ( - "First time this entity has been seen in a case." - if entity_group.related_case_count == 1 # the current case counts as 1 - else f"Seen in *{entity_group.related_case_count}* other case(s)." - ) - - # Threaded messages do not overflow text fields, so we hack together the same UI with spaces - signal_metadata_blocks.append( - Context( - elements=[f"*{k}*\n`{', '.join(item.value for item in v)}`\n\n{case_message}"] - ), - ) - signal_metadata_blocks.append(Divider()) + # Create the Actions block with the elements + signal_metadata_blocks = [ + Section(text="*Actions*"), + Actions(elements=elements) + ] - signal_metadata_blocks.append( - Context(elements=["Correlation is based on two weeks of signal data."]), - ) return Message(blocks=signal_metadata_blocks).build()["blocks"] @@ -300,7 +260,22 @@ def create_genai_signal_summary( db_session: Session, client: WebClient, ) -> list[Block]: - """Creates enhanced signal messages with historical context.""" + """ + Creates a signal summary using a generative AI plugin. + + This function generates a summary for a given case by leveraging historical context and + a generative AI plugin. It fetches related cases, their resolutions, and relevant Slack + messages to provide a comprehensive summary. + + Args: + case (Case): The case object containing details to be included in the summary. + channel_id (str): The ID of the Slack channel where the summary will be sent. + db_session (Session): The database session to use for querying signal instances and related cases. + client (WebClient): The Slack WebClient to fetch threaded messages. + + Returns: + list[Block]: A list of Block objects representing the structure of the Slack message. + """ signal_metadata_blocks: list[Block] = [] instances = signal_service.get_instances_in_case(db_session=db_session, case_id=case.id) @@ -317,9 +292,10 @@ def create_genai_signal_summary( # Prepare historical context historical_context = [] for related_case in related_cases: - historical_context.append(f"Case: {related_case.name}") - historical_context.append(f"Resolution: {related_case.resolution}") - historical_context.append(f"Resolution Reason: {related_case.resolution_reason}") + historical_context.append("") + historical_context.append(f"{related_case.name}") + historical_context.append(f"{related_case.resolution}{related_case.resolution_reason}") # Fetch Slack messages for the related case if related_case.conversation and related_case.conversation.channel_id: @@ -332,11 +308,11 @@ def create_genai_signal_summary( # Add relevant messages to the context (e.g., first 5 messages) for message in thread_messages["messages"][:5]: - historical_context.append(f"Slack Message: {message['text']}") + historical_context.append(f"{message['text']}") except SlackApiError as e: log.error(f"Error fetching Slack messages for case {related_case.name}: {e}") - historical_context.append("---") + historical_context.append("") historical_context_str = "\n".join(historical_context) @@ -349,32 +325,37 @@ def create_genai_signal_summary( ) if not genai_plugin: - log.warning("artificial-intelligence plugin not enabled, will not generate signal summary") + log.warning("Unable to generate GenAI signal summary. No artificial-intelligence plugin enabled.") return signal_metadata_blocks if not signal_instance.signal.genai_prompt: - log.warning( - f"artificial-intelligence plugin enabled but no prompt defined for {signal_instance.signal.name}" - ) + log.warning(f"Unable to generate GenAI signal summary. No GenAI prompt defined for {signal_instance.signal.name}") return signal_metadata_blocks response = genai_plugin.instance.chat_completion( - prompt=f"""{signal_instance.signal.genai_prompt} + prompt=f""" + + + {signal_instance.signal.genai_prompt} + - Current Event: + {str(signal_instance.raw)} + - Historical Context: + {historical_context_str} + - Runbook: + {first_instance_signal.runbook} + """ ) message = response["choices"][0]["message"]["content"] signal_metadata_blocks.append( - Context(elements=[message]), + Section(text=f"*GenAI Alert Summary*\n\n{message}") ) return Message(blocks=signal_metadata_blocks).build()["blocks"] @@ -415,22 +396,20 @@ def create_signal_engagement_message( username, _ = user_email.split("@") blocks = [ - Context(elements=[f"Engaged {user_email} associated with {signal_instance.signal.name}"]), - Section(text=f"Hi @{username}, the security team could use your help with this case."), + Section(text=f"@{username}, we could use your help to resolve this case. Please, see additional context below:"), Section( - text=f"*Additional Context*\n\n {engagement.message if engagement.message else 'None provided for this signal.'}" + text=f"{engagement.message if engagement.message else 'No context provided for this alert.'}" ), - Divider(), ] if engagement_status == SignalEngagementStatus.new: blocks.extend( [ - Section(text="Please confirm this is you and the behavior is expected."), + Section(text="Can you please confirm this was you and whether the behavior was expected?"), Actions( elements=[ Button( - text="Approve", + text="Confirm", style="primary", action_id=SignalEngagementActions.approve, value=button_metadata, @@ -449,13 +428,13 @@ def create_signal_engagement_message( elif engagement_status == SignalEngagementStatus.approved: blocks.extend( [ - Section(text=":white_check_mark: This engagement confirmation has been approved."), + Section(text=f":white_check_mark: @{username} confirmed the behavior as expected."), ] ) else: blocks.extend( [ - Section(text=":warning: This engagement confirmation has been denied."), + Section(text=":warning: @{username} denied the behavior as expected. Please investigate the case and escalate to incident if necessary."), ] ) @@ -465,7 +444,7 @@ def create_signal_engagement_message( def create_welcome_ephemeral_message_to_participant(case: Case) -> list[Block]: blocks = [ Section( - text="You've been added to this case, because we think you may be able to help resolve it. Please review the case details below and reach out to the case assignee if you have any questions.", + text="You've been added to this case, because we think you may be able to help resolve it. Please, review the case details below and reach out to the case assignee if you have any questions.", ), Section( text=f"*Title* \n {case.title}", diff --git a/src/dispatch/plugins/dispatch_slack/plugin.py b/src/dispatch/plugins/dispatch_slack/plugin.py index edf04edde695..56fade638a47 100644 --- a/src/dispatch/plugins/dispatch_slack/plugin.py +++ b/src/dispatch/plugins/dispatch_slack/plugin.py @@ -93,6 +93,7 @@ def create_threaded(self, case: Case, conversation_id: str, db_session: Session) client = create_slack_client(self.configuration) blocks = create_case_message(case=case, channel_id=conversation_id) response = send_message(client=client, conversation_id=conversation_id, blocks=blocks) + if case.signal_instances: message = create_signal_messages( case_id=case.id, channel_id=conversation_id, db_session=db_session @@ -104,23 +105,24 @@ def create_threaded(self, case: Case, conversation_id: str, db_session: Session) blocks=message, ) case.signal_thread_ts = signal_response.get("timestamp") + try: client.files_upload( channels=conversation_id, thread_ts=case.signal_thread_ts, - initial_comment=f"First alert in `{case.name}` (see all in <{DISPATCH_UI_URL}/{case.project.organization.slug}/cases/{case.name}|Dispatch UI>):", + initial_comment=f"First alert for this case. View all alerts in <{DISPATCH_UI_URL}/{case.project.organization.slug}/cases/{case.name}|Dispatch>:", filetype="json", file=io.BytesIO(json.dumps(case.signal_instances[0].raw, indent=4).encode()), ) except SlackApiError as e: if e.response["error"] == SlackAPIErrorCode.MISSING_SCOPE: logger.exception( - f"Error uploading alert JSON to the Case thread due to missing scope: {e}" + f"Error uploading alert JSON to the case thread due to a missing scope: {e}" ) else: - logger.exception(f"Error uploading alert JSON to the Case thread: {e}") + logger.exception(f"Error uploading alert JSON to the case thread: {e}") except Exception as e: - logger.exception(f"Error uploading alert JSON to the Case thread: {e}") + logger.exception(f"Error uploading alert JSON to the case thread: {e}") try: send_message( @@ -135,7 +137,7 @@ def create_threaded(self, case: Case, conversation_id: str, db_session: Session) ), ) except Exception as e: - logger.exception(f"Error generating Gen AI response to case: {e}") + logger.exception(f"Error generating GenAI signal summary: {e}") db_session.commit() return response @@ -153,7 +155,7 @@ def create_engagement_threaded( client = create_slack_client(self.configuration) if not does_user_exist(client=client, email=user.email): not_found_msg = ( - f"Unable to engage user ({user.email}). Not found in current slack workspace." + f"Unable to engage user: {user.email}. User not found in the Slack workspace." ) return send_message( client=client, From bfaa7357044c3a49e57358e98004271f88c70141 Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Mon, 30 Sep 2024 12:45:13 -0700 Subject: [PATCH 2/5] improvements --- src/dispatch/plugins/dispatch_slack/case/messages.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/dispatch/plugins/dispatch_slack/case/messages.py b/src/dispatch/plugins/dispatch_slack/case/messages.py index 4a34ed6778e1..906349a2c296 100644 --- a/src/dispatch/plugins/dispatch_slack/case/messages.py +++ b/src/dispatch/plugins/dispatch_slack/case/messages.py @@ -115,7 +115,7 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: if case.has_channel: action_buttons = [ - Button(text=":slack: Case Channel", style="primary", url=case.conversation.weblink) + Button(text=":slack: Case Channel", style="primary", url=case.conversation.weblink if case.conversation else "") ] blocks.extend([Actions(elements=action_buttons)]) elif case.status == CaseStatus.escalated: @@ -278,14 +278,18 @@ def create_genai_signal_summary( """ signal_metadata_blocks: list[Block] = [] - instances = signal_service.get_instances_in_case(db_session=db_session, case_id=case.id) - (first_instance_id, first_instance_signal) = instances.first() + (first_instance_id, first_instance_signal) = signal_service.get_instances_in_case(db_session=db_session, case_id=case.id).first() + + if not first_instance_id or not first_instance_signal: + log.warning("Unable to generate GenAI signal summary. No signal instances found.") + return signal_metadata_blocks + # Fetch related cases related_cases = ( signal_service.get_cases_for_signal( db_session=db_session, signal_id=first_instance_signal.id ) - .from_self() + .from_self() # NOTE: function deprecated in SQLAlchemy 1.4 and removed in 2.0 .filter(Case.id != case.id) ) From a2e408588a907cfcf758f9a11ccb7418f7517ff6 Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Mon, 30 Sep 2024 13:04:27 -0700 Subject: [PATCH 3/5] ruff ruff --- src/dispatch/plugins/dispatch_slack/case/messages.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/dispatch/plugins/dispatch_slack/case/messages.py b/src/dispatch/plugins/dispatch_slack/case/messages.py index 906349a2c296..0fc06c46a758 100644 --- a/src/dispatch/plugins/dispatch_slack/case/messages.py +++ b/src/dispatch/plugins/dispatch_slack/case/messages.py @@ -1,5 +1,4 @@ import logging -from collections import defaultdict from typing import NamedTuple from blockkit import ( @@ -18,9 +17,6 @@ from dispatch.case.enums import CaseStatus from dispatch.case.models import Case from dispatch.config import DISPATCH_UI_URL -from dispatch.entity import service as entity_service -from dispatch.entity.models import Entity -from dispatch.entity_type.models import EntityType from dispatch.messaging.strings import CASE_STATUS_DESCRIPTIONS, CASE_VISIBILITY_DESCRIPTIONS from dispatch.plugin import service as plugin_service from dispatch.plugins.dispatch_slack.case.enums import ( @@ -40,7 +36,6 @@ from dispatch.signal.models import ( SignalEngagement, SignalInstance, - assoc_signal_instance_entities, ) log = logging.getLogger(__name__) @@ -211,7 +206,6 @@ def create_signal_messages(case_id: int, channel_id: str, db_session: Session) - """ instances = signal_service.get_instances_in_case(db_session=db_session, case_id=case_id) (first_instance_id, first_instance_signal) = instances.first() - num_of_instances = instances.count() organization_slug = first_instance_signal.project.organization.slug project_id = first_instance_signal.project.id From 091563b1049fd40ba2bd9ccae6dca5ed891b6997 Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Mon, 30 Sep 2024 15:33:08 -0700 Subject: [PATCH 4/5] cosmetic changes --- src/dispatch/plugins/dispatch_slack/case/messages.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/dispatch/plugins/dispatch_slack/case/messages.py b/src/dispatch/plugins/dispatch_slack/case/messages.py index 0fc06c46a758..00d90f576b0a 100644 --- a/src/dispatch/plugins/dispatch_slack/case/messages.py +++ b/src/dispatch/plugins/dispatch_slack/case/messages.py @@ -150,13 +150,12 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: else: action_buttons = [ Button( - text=":resolved: Resolve", + text=":white_check_mark: Resolve", action_id=CaseNotificationActions.resolve, - style="primary", value=button_metadata, ), Button( - text=":modified: Edit", + text=":pencil: Edit", action_id=CaseNotificationActions.edit, value=button_metadata, ), @@ -168,7 +167,6 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: Button( text=":fire: Escalate", action_id=CaseNotificationActions.escalate, - style="danger", value=button_metadata, ), ] @@ -178,7 +176,6 @@ def create_case_message(case: Case, channel_id: str) -> list[Block]: Button( text=":mag: Triage", action_id=CaseNotificationActions.triage, - style="primary", value=button_metadata, ), ) @@ -353,9 +350,9 @@ def create_genai_signal_summary( message = response["choices"][0]["message"]["content"] signal_metadata_blocks.append( - Section(text=f"*GenAI Alert Summary*\n\n{message}") + Section(text=f":magic_wand: *GenAI Alert Summary*\n\n{message}"), ) - + signal_metadata_blocks.append(Divider()) return Message(blocks=signal_metadata_blocks).build()["blocks"] From 2e5aee3b270d17b43d1d05a247402b5f71927c1c Mon Sep 17 00:00:00 2001 From: Marc Vilanova Date: Tue, 1 Oct 2024 12:53:24 -0700 Subject: [PATCH 5/5] re-adds number of alerts message --- .../plugins/dispatch_slack/case/messages.py | 34 +++++++++++-------- src/dispatch/plugins/dispatch_slack/plugin.py | 4 +-- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/dispatch/plugins/dispatch_slack/case/messages.py b/src/dispatch/plugins/dispatch_slack/case/messages.py index 00d90f576b0a..a751f1965186 100644 --- a/src/dispatch/plugins/dispatch_slack/case/messages.py +++ b/src/dispatch/plugins/dispatch_slack/case/messages.py @@ -201,6 +201,7 @@ def create_signal_messages(case_id: int, channel_id: str, db_session: Session) - Returns: list[Message]: A list of Message objects representing the structure of the Slack messages. """ + # we fetch the first instance to get the organization slug and project id instances = signal_service.get_instances_in_case(db_session=db_session, case_id=case_id) (first_instance_id, first_instance_signal) = instances.first() @@ -214,11 +215,11 @@ def create_signal_messages(case_id: int, channel_id: str, db_session: Session) - channel_id=channel_id, ).json() + + # we create the response plan and the snooze buttons elements = [] - # Check if `first_instance_signal.external_url` is not empty if first_instance_signal.external_url: - # If `first_instance_signal.external_url` is not empty, add the "Response Plan" button elements.append( Button( text="🔖 Response Plan", @@ -227,7 +228,6 @@ def create_signal_messages(case_id: int, channel_id: str, db_session: Session) - ) ) - # Define the initial elements with "Raw Data" and "Snooze" buttons elements.append( Button( text="💤 Snooze Alerts", @@ -236,31 +236,34 @@ def create_signal_messages(case_id: int, channel_id: str, db_session: Session) - ) ) - # Create the Actions block with the elements + # we create the signal metadata blocks signal_metadata_blocks = [ Section(text="*Actions*"), - Actions(elements=elements) + Actions(elements=elements), + Divider(), + Section(text="*Alerts*"), + Section(text=f"We observed {instances.count()} alerts in this case."), ] return Message(blocks=signal_metadata_blocks).build()["blocks"] -def create_genai_signal_summary( +def create_genai_signal_analysis_message( case: Case, channel_id: str, db_session: Session, client: WebClient, ) -> list[Block]: """ - Creates a signal summary using a generative AI plugin. + Creates a signal analysis using a generative AI plugin. - This function generates a summary for a given case by leveraging historical context and + This function generates a analysis for a given case by leveraging historical context and a generative AI plugin. It fetches related cases, their resolutions, and relevant Slack - messages to provide a comprehensive summary. + messages to provide a comprehensive analysis. Args: - case (Case): The case object containing details to be included in the summary. - channel_id (str): The ID of the Slack channel where the summary will be sent. + case (Case): The case object containing details to be included in the analysis. + channel_id (str): The ID of the Slack channel where the analysis will be sent. db_session (Session): The database session to use for querying signal instances and related cases. client (WebClient): The Slack WebClient to fetch threaded messages. @@ -272,7 +275,7 @@ def create_genai_signal_summary( (first_instance_id, first_instance_signal) = signal_service.get_instances_in_case(db_session=db_session, case_id=case.id).first() if not first_instance_id or not first_instance_signal: - log.warning("Unable to generate GenAI signal summary. No signal instances found.") + log.warning("Unable to generate GenAI signal analysis. No signal instances found.") return signal_metadata_blocks # Fetch related cases @@ -320,11 +323,11 @@ def create_genai_signal_summary( ) if not genai_plugin: - log.warning("Unable to generate GenAI signal summary. No artificial-intelligence plugin enabled.") + log.warning("Unable to generate GenAI signal analysis. No artificial-intelligence plugin enabled.") return signal_metadata_blocks if not signal_instance.signal.genai_prompt: - log.warning(f"Unable to generate GenAI signal summary. No GenAI prompt defined for {signal_instance.signal.name}") + log.warning(f"Unable to generate GenAI signal analysis. No GenAI prompt defined for {signal_instance.signal.name}") return signal_metadata_blocks response = genai_plugin.instance.chat_completion( @@ -349,8 +352,9 @@ def create_genai_signal_summary( ) message = response["choices"][0]["message"]["content"] + signal_metadata_blocks.append(Divider()) signal_metadata_blocks.append( - Section(text=f":magic_wand: *GenAI Alert Summary*\n\n{message}"), + Section(text=f":magic_wand: *GenAI Alert Analysis*\n\n{message}"), ) signal_metadata_blocks.append(Divider()) return Message(blocks=signal_metadata_blocks).build()["blocks"] diff --git a/src/dispatch/plugins/dispatch_slack/plugin.py b/src/dispatch/plugins/dispatch_slack/plugin.py index 56fade638a47..624d677b0a7b 100644 --- a/src/dispatch/plugins/dispatch_slack/plugin.py +++ b/src/dispatch/plugins/dispatch_slack/plugin.py @@ -33,7 +33,7 @@ from .case.messages import ( create_case_message, - create_genai_signal_summary, + create_genai_signal_analysis_message, create_signal_engagement_message, create_signal_messages, ) @@ -129,7 +129,7 @@ def create_threaded(self, case: Case, conversation_id: str, db_session: Session) client=client, conversation_id=conversation_id, ts=case.signal_thread_ts, - blocks=create_genai_signal_summary( + blocks=create_genai_signal_analysis_message( case=case, channel_id=conversation_id, db_session=db_session,