Skip to content

feat(ui): adds more case resolution reasons with descriptions #6066

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions src/dispatch/case/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,52 @@ class CaseStatus(DispatchEnum):


class CaseResolutionReason(DispatchEnum):
benign = "Benign"
contained = "Contained"
escalated = "Escalated"
false_positive = "False Positive"
user_acknowledge = "User Acknowledged"
information_gathered = "Information Gathered"
insufficient_information = "Insufficient Information"
mitigated = "Mitigated"
escalated = "Escalated"
operational_error = "Operational Error"
policy_violation = "Policy Violation"
user_acknowledged = "User Acknowledged"


class CaseResolutionReasonDescription(DispatchEnum):
"""Descriptions for case resolution reasons."""

benign = (
"The event was legitimate but posed no security threat, such as expected behavior "
"from a known application or user."
)
contained = (
"(True positive) The event was a legitimate threat but was contained to prevent "
"further spread or damage."
)
escalated = "There was enough information to create an incident based on the security event."
false_positive = "The event was incorrectly flagged as a security event."
information_gathered = (
"Used when a case was opened with the primary purpose of collecting information."
)
insufficient_information = (
"There was not enough information to determine the nature of the event conclusively."
)
mitigated = (
"(True Positive) The event was a legitimate security threat and was successfully "
"mitigated before causing harm."
)
operational_error = (
"The event was caused by a mistake in system configuration or user operation, "
"not malicious activity."
)
policy_violation = (
"The event was a breach of internal security policies but did not result in a "
"security incident."
)
user_acknowledged = (
"While the event was suspicious it was confirmed by the actor to be intentional."
)


class CostModelType(DispatchEnum):
Expand Down
4 changes: 2 additions & 2 deletions src/dispatch/case/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
send_case_rating_feedback_message,
send_case_update_notifications,
send_event_paging_message,
send_event_update_prompt_reminder
send_event_update_prompt_reminder,
)
from .models import Case
from .service import get
Expand Down Expand Up @@ -210,7 +210,7 @@ def case_auto_close_flow(case: Case, db_session: Session):
"Runs the case auto close flow."
# we mark the case as closed
case.resolution = "Auto closed via case type auto close configuration."
case.resolution_reason = CaseResolutionReason.user_acknowledge
case.resolution_reason = CaseResolutionReason.user_acknowledged
case.status = CaseStatus.closed
db_session.add(case)
db_session.commit()
Expand Down
70 changes: 65 additions & 5 deletions src/dispatch/plugins/dispatch_slack/case/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from dispatch.auth.models import DispatchUser, MfaChallengeStatus
from dispatch.case import flows as case_flows
from dispatch.case import service as case_service
from dispatch.case.enums import CaseResolutionReason, CaseStatus
from dispatch.case.enums import CaseResolutionReason, CaseStatus, CaseResolutionReasonDescription
from dispatch.case.models import Case, CaseCreate, CaseRead, CaseUpdate
from dispatch.case.type import service as case_type_service
from dispatch.config import DISPATCH_UI_URL
Expand Down Expand Up @@ -68,6 +68,7 @@
from dispatch.plugins.dispatch_slack.decorators import message_dispatcher
from dispatch.plugins.dispatch_slack.enums import SlackAPIErrorCode
from dispatch.plugins.dispatch_slack.fields import (
DefaultActionIds,
DefaultBlockIds,
case_priority_select,
case_resolution_reason_select,
Expand Down Expand Up @@ -287,7 +288,7 @@ def handle_update_case_command(
),
case_visibility_select(
initial_option={"text": case.visibility, "value": case.visibility},
)
),
]

modal = Modal(
Expand Down Expand Up @@ -2084,10 +2085,13 @@ def resolve_button_click(
reason = case.resolution_reason
blocks = [
(
case_resolution_reason_select(initial_option={"text": reason, "value": reason})
case_resolution_reason_select(
initial_option={"text": reason, "value": reason}, dispatch_action=True
)
if reason
else case_resolution_reason_select()
else case_resolution_reason_select(dispatch_action=True)
),
Context(elements=[MarkdownText(text="Select a resolution reason to see its description")]),
resolution_input(initial_value=case.resolution),
]

Expand All @@ -2102,6 +2106,62 @@ def resolve_button_click(
client.views_open(trigger_id=body["trigger_id"], view=modal)


@app.action(
DefaultActionIds.case_resolution_reason_select,
middleware=[action_context_middleware, db_middleware],
)
def handle_resolution_reason_select_action(
ack: Ack,
body: dict,
client: WebClient,
context: BoltContext,
db_session: Session,
):
"""Handles the resolution reason select action."""
ack()

# Get the selected resolution reason
values = body["view"]["state"]["values"]
block_id = DefaultBlockIds.case_resolution_reason_select
action_id = DefaultActionIds.case_resolution_reason_select
resolution_reason = values[block_id][action_id]["selected_option"]["value"]

# Get the description for the selected reason
try:
# Map the resolution reason string to the enum key
reason_key = resolution_reason.lower().replace(" ", "_")
description = CaseResolutionReasonDescription[reason_key].value
except KeyError:
description = "No description available"

# Get the current case
case = case_service.get(db_session=db_session, case_id=int(context["subject"].id))

# Rebuild the modal with the updated description
blocks = [
case_resolution_reason_select(
initial_option={"text": resolution_reason, "value": resolution_reason},
dispatch_action=True,
),
Context(elements=[MarkdownText(text=f"*Description:* {description}")]),
resolution_input(initial_value=case.resolution),
]

modal = Modal(
title="Resolve Case",
blocks=blocks,
submit="Resolve",
close="Close",
callback_id=CaseResolveActions.submit,
private_metadata=context["subject"].json(),
).build()

client.views_update(
view_id=body["view"]["id"],
view=modal,
)


@app.action(CaseNotificationActions.triage, middleware=[button_context_middleware, db_middleware])
def triage_button_click(
ack: Ack, body: dict, db_session: Session, context: BoltContext, client: WebClient
Expand Down Expand Up @@ -2884,7 +2944,7 @@ def resolve_case(
)
case_in = CaseUpdate(
title=case.title,
resolution_reason=CaseResolutionReason.user_acknowledge,
resolution_reason=CaseResolutionReason.user_acknowledged,
resolution=context_from_user,
visibility=case.visibility,
status=CaseStatus.closed,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { ref } from "vue"
import type { Ref } from "vue"
import { computed } from "vue"

import CaseApi from "@/case/api"
import SearchPopover from "@/components/SearchPopover.vue"
Expand All @@ -11,12 +10,9 @@ defineProps<{ caseResolution: string }>()

const store = useStore()
const { setSaving } = useSavingState()
const caseResolutions: Ref<string[]> = ref([
"False Positive",
"User Acknowledged",
"Mitigated",
"Escalated",
])

const caseResolutions = computed(() => store.state.case_management.resolutionReasons)
const caseResolutionTooltips = computed(() => store.state.case_management.resolutionTooltips)

const selectCaseResolution = async (caseResolutionName: string) => {
// Get the case details from the Vuex store
Expand All @@ -37,5 +33,6 @@ const selectCaseResolution = async (caseResolutionName: string) => {
@item-selected="selectCaseResolution"
label="Set resolution..."
:hotkeys="[]"
:tooltips="caseResolutionTooltips"
/>
</template>
55 changes: 51 additions & 4 deletions src/dispatch/static/dispatch/src/case/ClosedDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,37 @@
<v-card-actions>
<v-container>
<v-row>
<v-col cols="12">
<v-col cols="12" sm="6">
<v-select
v-model="resolutionReason"
label="Resolution Reason"
:items="resolutionReasons"
:items="$store.state.case_management.resolutionReasons"
hint="The general reason why a given case was resolved."
/>
:menu-props="{ contentClass: 'resolution-menu' }"
>
<template #item="{ item, props }">
<v-list-item v-bind="props">
<template #title>
<div class="d-flex align-center justify-space-between">
{{ item.title }}
<v-tooltip location="right">
<template #activator="{ props }">

Check warning on line 27 in src/dispatch/static/dispatch/src/case/ClosedDialog.vue

View workflow job for this annotation

GitHub Actions / build

Variable 'props' is already declared in the upper scope
<v-icon
v-bind="props"
icon="mdi-information"
size="small"
class="ml-2"
/>
</template>
<span>{{
$store.state.case_management.resolutionTooltips[item.title]
}}</span>
</v-tooltip>
</div>
</template>
</v-list-item>
</template>
</v-select>
</v-col>
<v-col cols="12">
<v-textarea
Expand Down Expand Up @@ -59,7 +83,6 @@
return {
resolutionReason: "False Positive",
resolution: "Description of the actions taken to resolve the case.",
resolutionReasons: ["False Positive", "User Acknowledged", "Mitigated", "Escalated"],
}
},

Expand All @@ -76,3 +99,27 @@
},
}
</script>

<style scoped>
.resolution-menu {
max-width: 300px;
}

:deep(.v-list-item) {
padding: 8px 16px;
}

:deep(.v-select__content) {
max-width: 300px;
}

/* Lighten tooltip info icons */
:deep(.v-tooltip .v-icon) {
color: #cccccc !important;
}

/* Alternative approach - target the icon directly */
:deep(.mdi-information) {
color: #b0b0b0 !important;
}
</style>
50 changes: 47 additions & 3 deletions src/dispatch/static/dispatch/src/case/DetailsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,26 @@
<v-select
v-model="resolution_reason"
label="Resolution Reason"
:items="resolutionReasons"
:items="$store.state.case_management.resolutionReasons"
hint="The general reason why a given case was resolved."
/>
:menu-props="{ contentClass: 'resolution-menu' }"
>
<template #item="{ item, props }">
<v-list-item v-bind="props">
<template #title>
<div class="d-flex align-center justify-space-between">
{{ item.title }}
<v-tooltip location="right">
<template #activator="{ props }">

Check warning on line 40 in src/dispatch/static/dispatch/src/case/DetailsTab.vue

View workflow job for this annotation

GitHub Actions / build

Variable 'props' is already declared in the upper scope
<v-icon v-bind="props" icon="mdi-information" size="small" class="ml-2" />
</template>
<span>{{ $store.state.case_management.resolutionTooltips[item.title] }}</span>
</v-tooltip>
</div>
</template>
</v-list-item>
</template>
</v-select>
</v-col>
<v-col cols="12">
<v-textarea
Expand Down Expand Up @@ -203,7 +220,6 @@
{ title: "Closed", value: "Closed" },
],
visibilities: ["Open", "Restricted"],
resolutionReasons: ["False Positive", "User Acknowledged", "Mitigated", "Escalated"],
only_one: (value) => {
if (value && value.length > 1) {
return "Only one is allowed"
Expand Down Expand Up @@ -251,4 +267,32 @@
opacity: 0.6;
pointer-events: none;
}

.resolution-item {
margin-left: 12px;
margin-bottom: 4px;
padding: 8px 0;
}

.resolution-menu {
max-width: 300px;
}

:deep(.v-list-item) {
padding: 8px 16px;
}

:deep(.v-select__content) {
max-width: 300px;
}

/* Lighten tooltip info icons */
:deep(.v-tooltip .v-icon) {
color: #cccccc !important;
}

/* Alternative approach - target the icon directly */
:deep(.mdi-information) {
color: #b0b0b0 !important;
}
</style>
Loading
Loading