Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
Closed
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
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ RUN buildDeps="" \
&& rm -rf /var/lib/apt/lists/* \
# mjml has to be installed differently here because
# after node 14, docker will install npm files at the
# root directoy and fail, so we have to create a new
# root directory and fail, so we have to create a new
# directory and use it for the install then copy the
# files to the root directory to maintain backwards
# compatibility for email generation
Expand Down
7 changes: 5 additions & 2 deletions src/dispatch/ai/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,24 @@ class TacticalReport(DispatchBase):
Model for structured tactical report output from AI analysis. Enforces the presence of fields
dedicated to the incident's conditions, actions, and needs.
"""

conditions: str = Field(
description="Summary of incident circumstances, with focus on scope and impact", default=""
)
actions: str | list[str] = Field(
description="Chronological list of actions and analysis by both the party instigating the incident and the response team",
default_factory=list
default_factory=list,
)
needs: str | list[str] = Field(
description="Identified and unresolved action items from the incident, or an indication that the incident is at resolution", default=""
description="Identified and unresolved action items from the incident, or an indication that the incident is at resolution",
default="",
)


class TacticalReportResponse(DispatchBase):
"""
Response model for tactical report generation. Includes the structured summary and any error messages.
"""

tactical_report: TacticalReport | None = None
error_message: str | None = None
9 changes: 6 additions & 3 deletions src/dispatch/ai/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

log = logging.getLogger(__name__)


def get_model_token_limit(model_name: str, buffer_percentage: float = 0.05) -> int:
"""
Returns the maximum token limit for a given LLM model with a safety buffer.
Expand Down Expand Up @@ -736,7 +737,9 @@ def generate_tactical_report(
return TacticalReportResponse(error_message=message)

conversation = conversation_plugin.instance.get_conversation(
conversation_id=incident.conversation.channel_id, include_user_details=True, important_reaction=important_reaction
conversation_id=incident.conversation.channel_id,
include_user_details=True,
important_reaction=important_reaction,
)
if not conversation:
message = f"Tactical report not generated for {incident.name}. No conversation found."
Expand Down Expand Up @@ -782,12 +785,12 @@ def generate_tactical_report(
),
incident_id=incident.id,
details=result.dict(),
type=EventType.other
type=EventType.other,
)

return TacticalReportResponse(tactical_report=result)

except Exception as e:
error_message = f"Error generating tactical report: {str(e)}"
log.exception(error_message)
return TacticalReportResponse(error_message = error_message)
return TacticalReportResponse(error_message=error_message)
17 changes: 9 additions & 8 deletions src/dispatch/case/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,19 +198,20 @@ def case_remove_participant_flow(
return

slack_conversation_plugin.instance.remove_user(
conversation_id=case.conversation.channel_id,
user_email=user_email
conversation_id=case.conversation.channel_id, user_email=user_email
)

event_service.log_case_event(
db_session=db_session,
source=slack_conversation_plugin.plugin.title,
description=f"{user_email} removed from conversation (channel ID: {case.conversation.channel_id})",
case_id=case.id,
type=EventType.participant_updated,
db_session=db_session,
source=slack_conversation_plugin.plugin.title,
description=f"{user_email} removed from conversation (channel ID: {case.conversation.channel_id})",
case_id=case.id,
type=EventType.participant_updated,
)

log.info(f"Removed {user_email} from conversation in channel {case.conversation.channel_id}")
log.info(
f"Removed {user_email} from conversation in channel {case.conversation.channel_id}"
)

except Exception as e:
log.exception(f"Failed to remove user from Slack conversation: {e}")
Expand Down
3 changes: 2 additions & 1 deletion src/dispatch/case/messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ def send_event_update_prompt_reminder(case: Case, db_session: Session) -> None:
"text": {"type": "plain_text", "text": "Update Case"},
"action_id": CaseNotificationActions.update,
"style": "primary",
"value": button_metadata
"value": button_metadata,
}
],
},
Expand All @@ -439,6 +439,7 @@ def send_event_update_prompt_reminder(case: Case, db_session: Session) -> None:

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
Expand Down
1 change: 1 addition & 0 deletions src/dispatch/case/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ class CaseReadMinimal(CaseBase):
project: ProjectRead
assignee: ParticipantReadMinimal | None = None
case_costs: list[CaseCostReadMinimal] = []
incidents: list[IncidentReadBasic] | None = []


class CaseReadMinimalWithExtras(CaseBase):
Expand Down
11 changes: 2 additions & 9 deletions src/dispatch/case/scheduled.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@
def case_close_reminder(db_session: Session, project: Project):
"""Sends a reminder to the case assignee to close out their case."""
cases = get_all_by_status(
db_session=db_session,
project_id=project.id,
statuses=[CaseStatus.triage]
db_session=db_session, project_id=project.id, statuses=[CaseStatus.triage]
)

for case in cases:
Expand All @@ -61,12 +59,7 @@ def case_triage_reminder(db_session: Session, project: Project):
db_session.query(Case)
.filter(Case.project_id == project.id)
.filter(Case.status != CaseStatus.closed)
.filter(
or_(
Case.title == "Security Event Triage",
Case.status == CaseStatus.new
)
)
.filter(or_(Case.title == "Security Event Triage", Case.status == CaseStatus.new))
.all()
)

Expand Down
8 changes: 2 additions & 6 deletions src/dispatch/case_cost_type/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ def get(*, db_session, case_cost_type_id: int) -> CaseCostType | None:
return db_session.query(CaseCostType).filter(CaseCostType.id == case_cost_type_id).one_or_none()


def get_response_cost_type(
*, db_session, project_id: int, model_type: str
) -> CaseCostType | None:
def get_response_cost_type(*, db_session, project_id: int, model_type: str) -> CaseCostType | None:
"""Gets the default response cost type."""
return (
db_session.query(CaseCostType)
Expand Down Expand Up @@ -52,9 +50,7 @@ def get_or_create_response_cost_type(
return case_cost_type


def get_all_response_case_cost_types(
*, db_session, project_id: int
) -> list[CaseCostType | None]:
def get_all_response_case_cost_types(*, db_session, project_id: int) -> list[CaseCostType | None]:
"""Returns all response case cost types.

This function queries the database for all case cost types that are marked as the response cost type.
Expand Down
1 change: 0 additions & 1 deletion src/dispatch/conference/service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

from .models import Conference, ConferenceCreate


Expand Down
8 changes: 6 additions & 2 deletions src/dispatch/conversation/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,9 @@ def add_case_participants(
case_id=case.id,
type=EventType.participant_updated,
)
log.info(f"{', '.join(participant_emails)} added to conversation (channel ID: {case.conversation.channel_id}, thread ID: {case.conversation.thread_id})")
log.info(
f"{', '.join(participant_emails)} added to conversation (channel ID: {case.conversation.channel_id}, thread ID: {case.conversation.thread_id})"
)
elif case.has_channel:
plugin.instance.add(case.conversation.channel_id, participant_emails)

Expand All @@ -512,7 +514,9 @@ def add_case_participants(
case_id=case.id,
type=EventType.participant_updated,
)
log.info(f"{', '.join(participant_emails)} added to conversation (channel ID: {case.conversation.channel_id})")
log.info(
f"{', '.join(participant_emails)} added to conversation (channel ID: {case.conversation.channel_id})"
)
except Exception as e:
event_service.log_case_event(
db_session=db_session,
Expand Down
6 changes: 6 additions & 0 deletions src/dispatch/conversation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

class Conversation(Base, ResourceMixin):
"""SQLAlchemy model for conversation resources."""

id = Column(Integer, primary_key=True)
channel_id = Column(String)
thread_id = Column(String)
Expand All @@ -22,22 +23,26 @@ class Conversation(Base, ResourceMixin):
# Pydantic models...
class ConversationBase(ResourceBase):
"""Base Pydantic model for conversation resources."""

channel_id: str | None = None
thread_id: str | None = None


class ConversationCreate(ConversationBase):
"""Pydantic model for creating a conversation resource."""

pass


class ConversationUpdate(ConversationBase):
"""Pydantic model for updating a conversation resource."""

pass


class ConversationRead(ConversationBase):
"""Pydantic model for reading a conversation resource."""

id: PrimaryKey
description: str | None = None

Expand All @@ -50,4 +55,5 @@ def set_description(cls, _):

class ConversationNested(ConversationBase):
"""Pydantic model for a nested conversation resource."""

pass
1 change: 0 additions & 1 deletion src/dispatch/data/alert/service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

from pydantic import ValidationError


Expand Down
18 changes: 10 additions & 8 deletions src/dispatch/data/query/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ def get_by_name_or_raise(*, db_session, query_in: QueryRead, project_id: int) ->
query = get_by_name(db_session=db_session, name=query_in.name, project_id=project_id)

if not query:
raise ValidationError([
{
"loc": ("query",),
"msg": f"Query not found: {query_in.name}",
"type": "value_error",
"input": query_in.name,
}
])
raise ValidationError(
[
{
"loc": ("query",),
"msg": f"Query not found: {query_in.name}",
"type": "value_error",
"input": query_in.name,
}
]
)

return query

Expand Down
18 changes: 10 additions & 8 deletions src/dispatch/data/source/data_format/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,16 @@ def get_by_name_or_raise(
)

if not data_format:
raise ValidationError([
{
"loc": ("dataFormat",),
"msg": f"SourceDataFormat not found: {source_data_format_in.name}",
"type": "value_error",
"input": source_data_format_in.name,
}
])
raise ValidationError(
[
{
"loc": ("dataFormat",),
"msg": f"SourceDataFormat not found: {source_data_format_in.name}",
"type": "value_error",
"input": source_data_format_in.name,
}
]
)

return data_format

Expand Down
18 changes: 10 additions & 8 deletions src/dispatch/data/source/environment/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ def get_by_name_or_raise(
)

if not source:
raise ValidationError([
{
"loc": ("source",),
"msg": f"Source environment not found: {source_environment_in.name}",
"type": "value_error",
"input": source_environment_in.name,
}
])
raise ValidationError(
[
{
"loc": ("source",),
"msg": f"Source environment not found: {source_environment_in.name}",
"type": "value_error",
"input": source_environment_in.name,
}
]
)

return source

Expand Down
18 changes: 10 additions & 8 deletions src/dispatch/data/source/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@ def get_by_name_or_raise(*, db_session, project_id, source_in: SourceRead) -> So
source = get_by_name(db_session=db_session, project_id=project_id, name=source_in.name)

if not source:
raise ValidationError([
{
"loc": ("source",),
"msg": f"Source not found: {source_in.name}",
"type": "value_error",
"input": source_in.name,
}
])
raise ValidationError(
[
{
"loc": ("source",),
"msg": f"Source not found: {source_in.name}",
"type": "value_error",
"input": source_in.name,
}
]
)

return source

Expand Down
18 changes: 10 additions & 8 deletions src/dispatch/data/source/status/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ def get_by_name_or_raise(
status = get_by_name(db_session=db_session, project_id=project_id, name=source_status_in.name)

if not status:
raise ValidationError([
{
"loc": ("status",),
"msg": f"SourceStatus not found: {source_status_in.name}",
"type": "value_error",
"input": source_status_in.name,
}
])
raise ValidationError(
[
{
"loc": ("status",),
"msg": f"SourceStatus not found: {source_status_in.name}",
"type": "value_error",
"input": source_status_in.name,
}
]
)

return status

Expand Down
18 changes: 10 additions & 8 deletions src/dispatch/data/source/transport/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,16 @@ def get_by_name_or_raise(
)

if not source:
raise ValidationError([
{
"loc": ("source",),
"msg": f"SourceTransport not found: {source_transport_in.name}",
"type": "value_error",
"input": source_transport_in.name,
}
])
raise ValidationError(
[
{
"loc": ("source",),
"msg": f"SourceTransport not found: {source_transport_in.name}",
"type": "value_error",
"input": source_transport_in.name,
}
]
)

return source

Expand Down
Loading
Loading