From dc50c76193785d92492d1d83dfd8fdb5055a04c0 Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Mon, 12 Aug 2024 15:32:16 -0700 Subject: [PATCH 1/9] Setting up new project level setting for weekly summaries --- .../versions/2024-08-12_f729d61738b0.py | 33 +++++++++++++++++ src/dispatch/project/models.py | 3 ++ .../dispatch/src/notification/Table.vue | 37 ++++++++++++++++++- .../static/dispatch/src/notification/store.js | 23 ++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py diff --git a/src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py b/src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py new file mode 100644 index 000000000000..a19fed304135 --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py @@ -0,0 +1,33 @@ +"""Adds project parameter to send weekly reports + +Revision ID: f729d61738b0 +Revises: 71cd7ed999c4 +Create Date: 2024-08-12 15:22:41.977924 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "f729d61738b0" +down_revision = "71cd7ed999c4" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "project", + sa.Column( + "send_weekly_reports", sa.Boolean(), nullable=True, server_default=sa.text("false") + ), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("project", "send_weekly_reports") + # ### end Alembic commands ### diff --git a/src/dispatch/project/models.py b/src/dispatch/project/models.py index 030e3500ca77..bd5d354de232 100644 --- a/src/dispatch/project/models.py +++ b/src/dispatch/project/models.py @@ -44,6 +44,7 @@ class Project(Base): allow_self_join = Column(Boolean, default=True, server_default="t") send_daily_reports = Column(Boolean) + send_weekly_reports = Column(Boolean) stable_priority_id = Column(Integer, nullable=True) stable_priority = relationship( @@ -80,6 +81,7 @@ class ProjectBase(DispatchBase): default: bool = False color: Optional[str] = Field(None, nullable=True) send_daily_reports: Optional[bool] = Field(True, nullable=True) + send_weekly_reports: Optional[bool] = Field(False, nullable=True) enabled: Optional[bool] = Field(True, nullable=True) storage_folder_one: Optional[str] = Field(None, nullable=True) storage_folder_two: Optional[str] = Field(None, nullable=True) @@ -94,6 +96,7 @@ class ProjectCreate(ProjectBase): class ProjectUpdate(ProjectBase): send_daily_reports: Optional[bool] = Field(True, nullable=True) + send_weekly_reports: Optional[bool] = Field(False, nullable=True) stable_priority_id: Optional[int] diff --git a/src/dispatch/static/dispatch/src/notification/Table.vue b/src/dispatch/static/dispatch/src/notification/Table.vue index 15f3d87b9de4..963e6d5fb8f3 100644 --- a/src/dispatch/static/dispatch/src/notification/Table.vue +++ b/src/dispatch/static/dispatch/src/notification/Table.vue @@ -96,6 +96,34 @@ + + + + + + + + + If activated, Dispatch will send a weekly summary report of incidents that were + active or marked as stable or closed in the last week. + + + + @@ -151,6 +179,7 @@ export default { "table.rows.items", "table.rows.total", "dailyReports", + "weeklyReports", ]), }, @@ -177,7 +206,13 @@ export default { }, methods: { - ...mapActions("notification", ["getAll", "createEditShow", "removeShow", "updateDailyReports"]), + ...mapActions("notification", [ + "getAll", + "createEditShow", + "removeShow", + "updateDailyReports", + "updateWeeklyReports", + ]), }, } diff --git a/src/dispatch/static/dispatch/src/notification/store.js b/src/dispatch/static/dispatch/src/notification/store.js index 3663f5267b1a..23db1f35ed0d 100644 --- a/src/dispatch/static/dispatch/src/notification/store.js +++ b/src/dispatch/static/dispatch/src/notification/store.js @@ -48,6 +48,7 @@ const state = { loading: false, }, dailyReports: null, + weeklyReports: null, } const getters = { @@ -65,6 +66,7 @@ const actions = { const project = response.data.items[0] if (project) { commit("SET_DAILY_REPORT_STATE", project.send_daily_reports ?? true) + commit("SET_WEEKLY_REPORT_STATE", project.send_weekly_reports ?? true) } }) return NotificationApi.getAll(params) @@ -104,6 +106,24 @@ const actions = { } }) }, + updateWeeklyReports({ commit }, value) { + ProjectApi.getAll({ q: state.table.options.filters.project[0].name }).then((response) => { + const project = response.data.items[0] + if (project) { + project.send_weekly_reports = value + ProjectApi.update(project.id, project).then(() => { + commit( + "notification_backend/addBeNotification", + { + text: `Project setting updated.`, + type: "success", + }, + { root: true } + ) + }) + } + }) + }, closeCreateEdit({ commit }) { commit("SET_DIALOG_CREATE_EDIT", false) commit("RESET_SELECTED") @@ -188,6 +208,9 @@ const mutations = { SET_DAILY_REPORT_STATE(state, value) { state.dailyReports = value }, + SET_WEEKLY_REPORT_STATE(state, value) { + state.weeklyReports = value + }, } export default { From 895fbcb84207451dff7d9cb61b7e852f3ef4a143 Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Tue, 13 Aug 2024 10:15:23 -0700 Subject: [PATCH 2/9] Adding weekly summary report generation --- src/dispatch/incident/scheduled.py | 162 ++++++++++++++++++++++++----- src/dispatch/messaging/strings.py | 48 +++++++++ 2 files changed, 186 insertions(+), 24 deletions(-) diff --git a/src/dispatch/incident/scheduled.py b/src/dispatch/incident/scheduled.py index 27376b245da6..ffb052e52352 100644 --- a/src/dispatch/incident/scheduled.py +++ b/src/dispatch/incident/scheduled.py @@ -4,15 +4,18 @@ from datetime import datetime, date from schedule import every -from sqlalchemy import func +from sqlalchemy import func, Session from dispatch.conversation.enums import ConversationButtonActions -from dispatch.database.core import SessionLocal, resolve_attr +from dispatch.database.core import resolve_attr from dispatch.decorators import scheduled_project_task, timer from dispatch.messaging.strings import ( INCIDENT, INCIDENT_DAILY_REPORT, INCIDENT_DAILY_REPORT_TITLE, + INCIDENT_WEEKLY_REPORT, + INCIDENT_WEEKLY_REPORT_TITLE, + INCIDENT_SUMMARY_TEMPLATE, MessageType, ) from dispatch.nlp import build_phrase_matcher, build_term_vocab, extract_terms_from_text @@ -39,7 +42,7 @@ @scheduler.add(every(1).hours, name="incident-auto-tagger") @timer @scheduled_project_task -def incident_auto_tagger(db_session: SessionLocal, project: Project): +def incident_auto_tagger(db_session: Session, project: Project): """Attempts to take existing tags and associate them with incidents.""" plugin = plugin_service.get_active_instance( db_session=db_session, project_id=project.id, plugin_type="storage" @@ -83,10 +86,36 @@ def incident_auto_tagger(db_session: SessionLocal, project: Project): log.debug(f"Associating tags with incident {incident.name}. Tags: {extracted_tags}") +def get_notifications_filters(incidents, db_session: Session, project: Project): + # we map incidents to notification filters + incidents_notification_filters_mapping = defaultdict(lambda: defaultdict(lambda: [])) + notifications = notification_service.get_all_enabled( + db_session=db_session, project_id=project.id + ) + for incident in incidents: + for notification in notifications: + for search_filter in notification.filters: + match = search_filter_service.match( + db_session=db_session, + subject=search_filter.subject, + filter_spec=search_filter.expression, + class_instance=incident, + ) + if match: + incidents_notification_filters_mapping[notification.id][ + search_filter.id + ].append(incident) + + if not notification.filters: + incidents_notification_filters_mapping[notification.id][0].append(incident) + + return incidents_notification_filters_mapping + + @scheduler.add(every(1).day.at("18:00"), name="incident-report-daily") @timer @scheduled_project_task -def incident_report_daily(db_session: SessionLocal, project: Project): +def incident_report_daily(db_session: Session, project: Project): """Creates and sends incident daily reports based on notifications.""" # don't send if set to false @@ -112,26 +141,9 @@ def incident_report_daily(db_session: SessionLocal, project: Project): incidents = active_incidents + stable_incidents + closed_incidents # we map incidents to notification filters - incidents_notification_filters_mapping = defaultdict(lambda: defaultdict(lambda: [])) - notifications = notification_service.get_all_enabled( - db_session=db_session, project_id=project.id + incidents_notification_filters_mapping = get_notifications_filters( + incidents, db_session, project ) - for incident in incidents: - for notification in notifications: - for search_filter in notification.filters: - match = search_filter_service.match( - db_session=db_session, - subject=search_filter.subject, - filter_spec=search_filter.expression, - class_instance=incident, - ) - if match: - incidents_notification_filters_mapping[notification.id][ - search_filter.id - ].append(incident) - - if not notification.filters: - incidents_notification_filters_mapping[notification.id][0].append(incident) # we create and send an incidents daily report for each notification filter for notification_id, search_filter_dict in incidents_notification_filters_mapping.items(): @@ -209,7 +221,7 @@ def incident_report_daily(db_session: SessionLocal, project: Project): @scheduler.add(every(1).day.at("18:00"), name="incident-close-reminder") @timer @scheduled_project_task -def incident_close_reminder(db_session: SessionLocal, project: Project): +def incident_close_reminder(db_session: Session, project: Project): """Sends a reminder to the incident commander to close out their incident.""" incidents = get_all_by_status( db_session=db_session, project_id=project.id, status=IncidentStatus.stable @@ -222,3 +234,105 @@ def incident_close_reminder(db_session: SessionLocal, project: Project): # we only send the reminder for incidents that have been stable # longer than a week and only on Mondays send_incident_close_reminder(incident, db_session) + + +@scheduler.add(every().monday.at("18:00"), name="incident-report-weekly") +@timer +@scheduled_project_task +def incident_report_weekly(db_session: Session, project: Project): + """Creates and sends incident weekly reports based on notifications.""" + + # don't send if set to false + if project.send_weekly_reports is False: + return + + # don't send if no enabled ai plugin + ai_plugin = plugin_service.get_active_instance( + db_session=db_session, plugin_type="artificial-intelligence", project_id=project.id + ) + if not ai_plugin: + log.warning("Incident weekly reports not sent. No AI plugin enabled.") + return + + # we fetch all closed incidents in the last week + incidents = get_all_last_x_hours_by_status( + db_session=db_session, + project_id=project.id, + status=IncidentStatus.closed, + hours=24 * 7, + ) + + # we map incidents to notification filters + incidents_notification_filters_mapping = get_notifications_filters( + incidents, db_session, project + ) + + storage_plugin = plugin_service.get_active_instance( + db_session=db_session, plugin_type="storage", project_id=project.id + ) + + # we create and send an incidents weekly report for each notification filter + for notification_id, search_filter_dict in incidents_notification_filters_mapping.items(): + for _search_filter_id, incidents in search_filter_dict.items(): + items_grouped = [] + items_grouped_template = INCIDENT_SUMMARY_TEMPLATE + + for idx, incident in enumerate(incidents): + try: + pir_doc = storage_plugin.instance.get( + file_id=incident.incident_review_document.resource_id, + mime_type="text/plain", + ) + messages = { + "role": "user", + "content": """Given the text of the security post-incident review document below, + provide answers to the following questions: + 1. What is the summary of what happened? + 2. What were the overall risk(s)? + 3. How were the risk(s) mitigated? + 4. How was the incident resolved? + 5. What are the follow-up tasks? + """ + + pir_doc, + } + + response = ai_plugin.instance.chat(messages) + summary = response["choices"][0]["message"]["content"] + + item = { + "commander_fullname": incident.commander.individual.name, + "commander_team": incident.commander.team, + "commander_weblink": incident.commander.individual.weblink, + "name": incident.name, + "ticket_weblink": resolve_attr(incident, "ticket.weblink"), + "title": incident.title, + "summary": summary, + } + + items_grouped.append(item) + except Exception as e: + log.exception(e) + + notification_kwargs = { + "items_grouped": items_grouped, + "items_grouped_template": items_grouped_template, + } + + notification_title_text = f"{project.name} {INCIDENT_WEEKLY_REPORT_TITLE}" + notification_params = { + "text": notification_title_text, + "type": MessageType.incident_weekly_report, + "template": INCIDENT_WEEKLY_REPORT, + "kwargs": notification_kwargs, + } + + notification = notification_service.get( + db_session=db_session, notification_id=notification_id + ) + + notification_service.send( + db_session=db_session, + project_id=notification.project.id, + notification=notification, + notification_params=notification_params, + ) diff --git a/src/dispatch/messaging/strings.py b/src/dispatch/messaging/strings.py index 1ff5f5832eb5..5e432b7eaee2 100644 --- a/src/dispatch/messaging/strings.py +++ b/src/dispatch/messaging/strings.py @@ -25,6 +25,7 @@ class MessageType(DispatchEnum): incident_closed_information_review_reminder = "incident-closed-information-review-reminder" incident_completed_form_notification = "incident-completed-form-notification" incident_daily_report = "incident-daily-report" + incident_weekly_report = "incident-weekly-report" incident_executive_report = "incident-executive-report" incident_feedback_daily_report = "incident-feedback-daily-report" incident_management_help_tips = "incident-management-help-tips" @@ -78,6 +79,18 @@ class MessageType(DispatchEnum): "\n", " " ).strip() +INCIDENT_WEEKLY_REPORT_TITLE = """ +Incidents Weekly Report""".replace( + "\n", " " +).strip() + +INCIDENT_WEEKLY_REPORT_DESCRIPTION = """ +This is an AI-generated weekly summary of incidents that have been marked as closed in the last week. +NOTE: These summaries may contain errors or inaccuracies. +Please verify the information before relying on it.""".replace( + "\n", " " +).strip() + INCIDENT_DAILY_REPORT_TITLE = """ Incidents Daily Report""".replace( "\n", " " @@ -521,6 +534,14 @@ class MessageType(DispatchEnum): "text": NOTIFICATION_PURPOSES_FYI, } +INCIDENT_NAME_SUMMARY = { + "title": "{{name}} Incident Summary", + "title_link": "{{ticket_weblink}}", + "text": "{{ignore}}", +} + +INCIDENT_SUMMARY = {"title": "Summary", "text": "{{summary}}"} + INCIDENT_TITLE = {"title": "Title", "text": "{{title}}"} CASE_TITLE = {"title": "Title", "text": "{{title}}"} @@ -589,6 +610,12 @@ class MessageType(DispatchEnum): "text": INCIDENT_COMMANDER_DESCRIPTION, } +INCIDENT_COMMANDER_SUMMARY = { + "title": "Commander - {{commander_fullname}}, {{commander_team}}", + "title_link": "{{commander_weblink}}", + "text": "{{ignore}}", +} + INCIDENT_CONFERENCE = { "title": "Conference", "title_link": "{{conference_weblink}}", @@ -948,6 +975,15 @@ class MessageType(DispatchEnum): {"title": "Created At", "text": "", "datetime": "{{ created_at}}"}, ] +INCIDENT_WEEKLY_REPORT_HEADER = { + "type": "header", + "text": INCIDENT_WEEKLY_REPORT_TITLE, +} + +INCIDENT_WEEKLY_REPORT_HEADER_DESCRIPTION = { + "text": INCIDENT_WEEKLY_REPORT_DESCRIPTION, +} + INCIDENT_DAILY_REPORT_HEADER = { "type": "header", "text": INCIDENT_DAILY_REPORT_TITLE, @@ -968,6 +1004,12 @@ class MessageType(DispatchEnum): INCIDENT_DAILY_REPORT_FOOTER, ] +INCIDENT_WEEKLY_REPORT = [ + INCIDENT_WEEKLY_REPORT_HEADER, + INCIDENT_WEEKLY_REPORT_HEADER_DESCRIPTION, + INCIDENT_DAILY_REPORT_FOOTER, +] + INCIDENT = [ INCIDENT_NAME_WITH_ENGAGEMENT_NO_DESCRIPTION, INCIDENT_TITLE, @@ -978,6 +1020,12 @@ class MessageType(DispatchEnum): INCIDENT_COMMANDER, ] +INCIDENT_SUMMARY_TEMPLATE = [ + INCIDENT_NAME_SUMMARY, + INCIDENT_TITLE, + INCIDENT_COMMANDER_SUMMARY, + INCIDENT_SUMMARY, +] INCIDENT_MANAGEMENT_HELP_TIPS_MESSAGE = [ { From 1723046be74b01dccfe06d5cd74c1ae7f7ef5906 Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Tue, 13 Aug 2024 14:37:18 -0700 Subject: [PATCH 3/9] Fixing linting error --- src/dispatch/incident/scheduled.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dispatch/incident/scheduled.py b/src/dispatch/incident/scheduled.py index ffb052e52352..0ed71f74a889 100644 --- a/src/dispatch/incident/scheduled.py +++ b/src/dispatch/incident/scheduled.py @@ -151,7 +151,7 @@ def incident_report_daily(db_session: Session, project: Project): items_grouped = [] items_grouped_template = INCIDENT - for idx, incident in enumerate(incidents): + for _idx, incident in enumerate(incidents): try: item = { "buttons": [], From 2ccdc7620b2ca43e01124f340c36a3cc0ab7a99c Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Tue, 13 Aug 2024 15:56:34 -0700 Subject: [PATCH 4/9] Fixing linting error --- src/dispatch/incident/scheduled.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dispatch/incident/scheduled.py b/src/dispatch/incident/scheduled.py index 0ed71f74a889..005483567faf 100644 --- a/src/dispatch/incident/scheduled.py +++ b/src/dispatch/incident/scheduled.py @@ -151,7 +151,7 @@ def incident_report_daily(db_session: Session, project: Project): items_grouped = [] items_grouped_template = INCIDENT - for _idx, incident in enumerate(incidents): + for idx, incident in enumerate(incidents): try: item = { "buttons": [], @@ -277,7 +277,7 @@ def incident_report_weekly(db_session: Session, project: Project): items_grouped = [] items_grouped_template = INCIDENT_SUMMARY_TEMPLATE - for idx, incident in enumerate(incidents): + for _idx, incident in enumerate(incidents): try: pir_doc = storage_plugin.instance.get( file_id=incident.incident_review_document.resource_id, From b9372a71e1577b2cc2ff2ebfd9219212507ef5a8 Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Thu, 15 Aug 2024 15:01:24 -0700 Subject: [PATCH 5/9] Skip restricted incidents --- src/dispatch/incident/scheduled.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dispatch/incident/scheduled.py b/src/dispatch/incident/scheduled.py index 005483567faf..9b542de9deaa 100644 --- a/src/dispatch/incident/scheduled.py +++ b/src/dispatch/incident/scheduled.py @@ -6,6 +6,7 @@ from schedule import every from sqlalchemy import func, Session +from dispatch.enums import Visibility from dispatch.conversation.enums import ConversationButtonActions from dispatch.database.core import resolve_attr from dispatch.decorators import scheduled_project_task, timer @@ -278,6 +279,9 @@ def incident_report_weekly(db_session: Session, project: Project): items_grouped_template = INCIDENT_SUMMARY_TEMPLATE for _idx, incident in enumerate(incidents): + # Skip restricted incidents + if incident.visibility == Visibility.restricted: + continue try: pir_doc = storage_plugin.instance.get( file_id=incident.incident_review_document.resource_id, From 756a7591364466f9d7ee0e0b06a786b113618738 Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Thu, 15 Aug 2024 17:04:14 -0700 Subject: [PATCH 6/9] Correcting description --- src/dispatch/static/dispatch/src/notification/Table.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dispatch/static/dispatch/src/notification/Table.vue b/src/dispatch/static/dispatch/src/notification/Table.vue index 963e6d5fb8f3..46dabb27928c 100644 --- a/src/dispatch/static/dispatch/src/notification/Table.vue +++ b/src/dispatch/static/dispatch/src/notification/Table.vue @@ -119,7 +119,7 @@ If activated, Dispatch will send a weekly summary report of incidents that were - active or marked as stable or closed in the last week. + marked as closed in the last week. From d93eb8a2fce92c7d6cafc380df3461ef54a61feb Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Mon, 19 Aug 2024 17:25:10 -0700 Subject: [PATCH 7/9] Allowing specific notification channel for weekly report --- .../versions/2024-08-12_f729d61738b0.py | 4 + src/dispatch/incident/scheduled.py | 182 ++++++++---------- src/dispatch/project/models.py | 4 + .../dispatch/src/notification/Table.vue | 19 +- .../static/dispatch/src/notification/store.js | 31 +++ 5 files changed, 142 insertions(+), 98 deletions(-) diff --git a/src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py b/src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py index a19fed304135..417ef6a06ca7 100644 --- a/src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py +++ b/src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py @@ -24,10 +24,14 @@ def upgrade(): "send_weekly_reports", sa.Boolean(), nullable=True, server_default=sa.text("false") ), ) + op.add_column( + "project", sa.Column("weekly_report_notification_id", sa.Integer(), nullable=True) + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_column("project", "send_weekly_reports") + op.drop_column("project", "weekly_report_notification_id") # ### end Alembic commands ### diff --git a/src/dispatch/incident/scheduled.py b/src/dispatch/incident/scheduled.py index 9b542de9deaa..7759acff6ea5 100644 --- a/src/dispatch/incident/scheduled.py +++ b/src/dispatch/incident/scheduled.py @@ -87,32 +87,6 @@ def incident_auto_tagger(db_session: Session, project: Project): log.debug(f"Associating tags with incident {incident.name}. Tags: {extracted_tags}") -def get_notifications_filters(incidents, db_session: Session, project: Project): - # we map incidents to notification filters - incidents_notification_filters_mapping = defaultdict(lambda: defaultdict(lambda: [])) - notifications = notification_service.get_all_enabled( - db_session=db_session, project_id=project.id - ) - for incident in incidents: - for notification in notifications: - for search_filter in notification.filters: - match = search_filter_service.match( - db_session=db_session, - subject=search_filter.subject, - filter_spec=search_filter.expression, - class_instance=incident, - ) - if match: - incidents_notification_filters_mapping[notification.id][ - search_filter.id - ].append(incident) - - if not notification.filters: - incidents_notification_filters_mapping[notification.id][0].append(incident) - - return incidents_notification_filters_mapping - - @scheduler.add(every(1).day.at("18:00"), name="incident-report-daily") @timer @scheduled_project_task @@ -142,9 +116,26 @@ def incident_report_daily(db_session: Session, project: Project): incidents = active_incidents + stable_incidents + closed_incidents # we map incidents to notification filters - incidents_notification_filters_mapping = get_notifications_filters( - incidents, db_session, project + incidents_notification_filters_mapping = defaultdict(lambda: defaultdict(lambda: [])) + notifications = notification_service.get_all_enabled( + db_session=db_session, project_id=project.id ) + for incident in incidents: + for notification in notifications: + for search_filter in notification.filters: + match = search_filter_service.match( + db_session=db_session, + subject=search_filter.subject, + filter_spec=search_filter.expression, + class_instance=incident, + ) + if match: + incidents_notification_filters_mapping[notification.id][ + search_filter.id + ].append(incident) + + if not notification.filters: + incidents_notification_filters_mapping[notification.id][0].append(incident) # we create and send an incidents daily report for each notification filter for notification_id, search_filter_dict in incidents_notification_filters_mapping.items(): @@ -243,8 +234,8 @@ def incident_close_reminder(db_session: Session, project: Project): def incident_report_weekly(db_session: Session, project: Project): """Creates and sends incident weekly reports based on notifications.""" - # don't send if set to false - if project.send_weekly_reports is False: + # don't send if set to false or no notification id is set + if project.send_weekly_reports is False or not project.weekly_report_notification_id: return # don't send if no enabled ai plugin @@ -263,80 +254,77 @@ def incident_report_weekly(db_session: Session, project: Project): hours=24 * 7, ) - # we map incidents to notification filters - incidents_notification_filters_mapping = get_notifications_filters( - incidents, db_session, project - ) + # no incidents closed in the last week + if not incidents: + return storage_plugin = plugin_service.get_active_instance( db_session=db_session, plugin_type="storage", project_id=project.id ) - # we create and send an incidents weekly report for each notification filter - for notification_id, search_filter_dict in incidents_notification_filters_mapping.items(): - for _search_filter_id, incidents in search_filter_dict.items(): - items_grouped = [] - items_grouped_template = INCIDENT_SUMMARY_TEMPLATE - - for _idx, incident in enumerate(incidents): - # Skip restricted incidents - if incident.visibility == Visibility.restricted: - continue - try: - pir_doc = storage_plugin.instance.get( - file_id=incident.incident_review_document.resource_id, - mime_type="text/plain", - ) - messages = { - "role": "user", - "content": """Given the text of the security post-incident review document below, - provide answers to the following questions: - 1. What is the summary of what happened? - 2. What were the overall risk(s)? - 3. How were the risk(s) mitigated? - 4. How was the incident resolved? - 5. What are the follow-up tasks? - """ - + pir_doc, - } - - response = ai_plugin.instance.chat(messages) - summary = response["choices"][0]["message"]["content"] - - item = { - "commander_fullname": incident.commander.individual.name, - "commander_team": incident.commander.team, - "commander_weblink": incident.commander.individual.weblink, - "name": incident.name, - "ticket_weblink": resolve_attr(incident, "ticket.weblink"), - "title": incident.title, - "summary": summary, - } - - items_grouped.append(item) - except Exception as e: - log.exception(e) - - notification_kwargs = { - "items_grouped": items_grouped, - "items_grouped_template": items_grouped_template, + # we create and send an incidents weekly report + for incident in incidents: + items_grouped = [] + items_grouped_template = INCIDENT_SUMMARY_TEMPLATE + + # Skip restricted incidents + if incident.visibility == Visibility.restricted: + continue + try: + pir_doc = storage_plugin.instance.get( + file_id=incident.incident_review_document.resource_id, + mime_type="text/plain", + ) + messages = { + "role": "user", + "content": """Given the text of the security post-incident review document below, + provide answers to the following questions: + 1. What is the summary of what happened? + 2. What were the overall risk(s)? + 3. How were the risk(s) mitigated? + 4. How was the incident resolved? + 5. What are the follow-up tasks? + """ + + pir_doc, } - notification_title_text = f"{project.name} {INCIDENT_WEEKLY_REPORT_TITLE}" - notification_params = { - "text": notification_title_text, - "type": MessageType.incident_weekly_report, - "template": INCIDENT_WEEKLY_REPORT, - "kwargs": notification_kwargs, + response = ai_plugin.instance.chat(messages) + summary = response["choices"][0]["message"]["content"] + + item = { + "commander_fullname": incident.commander.individual.name, + "commander_team": incident.commander.team, + "commander_weblink": incident.commander.individual.weblink, + "name": incident.name, + "ticket_weblink": resolve_attr(incident, "ticket.weblink"), + "title": incident.title, + "summary": summary, } - notification = notification_service.get( - db_session=db_session, notification_id=notification_id - ) + items_grouped.append(item) + except Exception as e: + log.exception(e) + + notification_kwargs = { + "items_grouped": items_grouped, + "items_grouped_template": items_grouped_template, + } + + notification_title_text = f"{project.name} {INCIDENT_WEEKLY_REPORT_TITLE}" + notification_params = { + "text": notification_title_text, + "type": MessageType.incident_weekly_report, + "template": INCIDENT_WEEKLY_REPORT, + "kwargs": notification_kwargs, + } + + notification = notification_service.get( + db_session=db_session, notification_id=project.weekly_report_notification_id + ) - notification_service.send( - db_session=db_session, - project_id=notification.project.id, - notification=notification, - notification_params=notification_params, - ) + notification_service.send( + db_session=db_session, + project_id=notification.project.id, + notification=notification, + notification_params=notification_params, + ) diff --git a/src/dispatch/project/models.py b/src/dispatch/project/models.py index bd5d354de232..de53b781c080 100644 --- a/src/dispatch/project/models.py +++ b/src/dispatch/project/models.py @@ -46,6 +46,8 @@ class Project(Base): send_daily_reports = Column(Boolean) send_weekly_reports = Column(Boolean) + weekly_report_notification_id = Column(Integer, nullable=True) + stable_priority_id = Column(Integer, nullable=True) stable_priority = relationship( IncidentPriority, @@ -82,6 +84,7 @@ class ProjectBase(DispatchBase): color: Optional[str] = Field(None, nullable=True) send_daily_reports: Optional[bool] = Field(True, nullable=True) send_weekly_reports: Optional[bool] = Field(False, nullable=True) + weekly_report_notification_id: Optional[int] = Field(None, nullable=True) enabled: Optional[bool] = Field(True, nullable=True) storage_folder_one: Optional[str] = Field(None, nullable=True) storage_folder_two: Optional[str] = Field(None, nullable=True) @@ -97,6 +100,7 @@ class ProjectCreate(ProjectBase): class ProjectUpdate(ProjectBase): send_daily_reports: Optional[bool] = Field(True, nullable=True) send_weekly_reports: Optional[bool] = Field(False, nullable=True) + weekly_report_notification_id: Optional[int] = Field(None, nullable=True) stable_priority_id: Optional[int] diff --git a/src/dispatch/static/dispatch/src/notification/Table.vue b/src/dispatch/static/dispatch/src/notification/Table.vue index 46dabb27928c..1ce5be2126ee 100644 --- a/src/dispatch/static/dispatch/src/notification/Table.vue +++ b/src/dispatch/static/dispatch/src/notification/Table.vue @@ -97,7 +97,7 @@ - + + + + @@ -180,6 +195,7 @@ export default { "table.rows.total", "dailyReports", "weeklyReports", + "weeklyReportNotificationId", ]), }, @@ -212,6 +228,7 @@ export default { "removeShow", "updateDailyReports", "updateWeeklyReports", + "updateWeeklyReportNotificationId", ]), }, } diff --git a/src/dispatch/static/dispatch/src/notification/store.js b/src/dispatch/static/dispatch/src/notification/store.js index 23db1f35ed0d..e513a7e4fd17 100644 --- a/src/dispatch/static/dispatch/src/notification/store.js +++ b/src/dispatch/static/dispatch/src/notification/store.js @@ -49,6 +49,7 @@ const state = { }, dailyReports: null, weeklyReports: null, + weeklyReportNotificationId: null, } const getters = { @@ -67,12 +68,21 @@ const actions = { if (project) { commit("SET_DAILY_REPORT_STATE", project.send_daily_reports ?? true) commit("SET_WEEKLY_REPORT_STATE", project.send_weekly_reports ?? true) + if (project.weekly_report_notification_id) { + NotificationApi.get(project.weekly_report_notification_id).then((response) => { + commit("SET_WEEKLY_REPORT_TARGET", response.data) + }) + } } }) return NotificationApi.getAll(params) .then((response) => { commit("SET_TABLE_LOADING", false) commit("SET_TABLE_ROWS", response.data) + commit( + "SET_NOTIFICATION_TYPES", + response.data.items.map((item) => item.name) + ) }) .catch(() => { commit("SET_TABLE_LOADING", false) @@ -124,6 +134,24 @@ const actions = { } }) }, + updateWeeklyReportNotificationId({ commit }, value) { + ProjectApi.getAll({ q: state.table.options.filters.project[0].name }).then((response) => { + const project = response.data.items[0] + if (project) { + project.weekly_report_notification_id = value + ProjectApi.update(project.id, project).then(() => { + commit( + "notification_backend/addBeNotification", + { + text: `Project setting updated.`, + type: "success", + }, + { root: true } + ) + }) + } + }) + }, closeCreateEdit({ commit }) { commit("SET_DIALOG_CREATE_EDIT", false) commit("RESET_SELECTED") @@ -211,6 +239,9 @@ const mutations = { SET_WEEKLY_REPORT_STATE(state, value) { state.weeklyReports = value }, + SET_WEEKLY_REPORT_TARGET(state, value) { + state.weeklyReportNotificationId = value + }, } export default { From 2b87d8a18ebc9fc7885ed38c2c984d4fd2d642f2 Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Tue, 20 Aug 2024 17:16:13 -0700 Subject: [PATCH 8/9] Checking to ensure storage plugin exists --- src/dispatch/incident/scheduled.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/dispatch/incident/scheduled.py b/src/dispatch/incident/scheduled.py index 7759acff6ea5..6749acf46beb 100644 --- a/src/dispatch/incident/scheduled.py +++ b/src/dispatch/incident/scheduled.py @@ -262,6 +262,12 @@ def incident_report_weekly(db_session: Session, project: Project): db_session=db_session, plugin_type="storage", project_id=project.id ) + if not storage_plugin: + log.warning( + f"Incident weekly reports not sent. No storage plugin enabled. Project: {project.name}." + ) + return + # we create and send an incidents weekly report for incident in incidents: items_grouped = [] From 575d903fe4213c5c36ca480d01320d44bc4a429d Mon Sep 17 00:00:00 2001 From: David Whittaker Date: Sat, 14 Sep 2024 13:43:57 -0700 Subject: [PATCH 9/9] Resync database migration --- ...{2024-08-12_f729d61738b0.py => 2024-09-14_f729d61738b0.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/dispatch/database/revisions/tenant/versions/{2024-08-12_f729d61738b0.py => 2024-09-14_f729d61738b0.py} (94%) diff --git a/src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py b/src/dispatch/database/revisions/tenant/versions/2024-09-14_f729d61738b0.py similarity index 94% rename from src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py rename to src/dispatch/database/revisions/tenant/versions/2024-09-14_f729d61738b0.py index 417ef6a06ca7..d577128f95cc 100644 --- a/src/dispatch/database/revisions/tenant/versions/2024-08-12_f729d61738b0.py +++ b/src/dispatch/database/revisions/tenant/versions/2024-09-14_f729d61738b0.py @@ -1,7 +1,7 @@ """Adds project parameter to send weekly reports Revision ID: f729d61738b0 -Revises: 71cd7ed999c4 +Revises: 0a6702319f6a Create Date: 2024-08-12 15:22:41.977924 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = "f729d61738b0" -down_revision = "71cd7ed999c4" +down_revision = "0a6702319f6a" branch_labels = None depends_on = None