Files
ETB/ETB-API/collaboration_war_rooms/models.py
Iliyan Angelov 6b247e5b9f Updates
2025-09-19 11:58:53 +03:00

1084 lines
36 KiB
Python

"""
Collaboration & War Rooms models for Enterprise Incident Management API
Implements real-time incident rooms, conference bridges, incident command roles, and timeline reconstruction
"""
import uuid
import json
from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List
from django.db import models
from django.contrib.auth import get_user_model
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils import timezone
from django.core.exceptions import ValidationError
User = get_user_model()
class WarRoom(models.Model):
"""Real-time incident collaboration rooms (like Slack channels auto-created per incident)"""
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('ARCHIVED', 'Archived'),
('CLOSED', 'Closed'),
]
PRIVACY_CHOICES = [
('PUBLIC', 'Public'),
('PRIVATE', 'Private'),
('RESTRICTED', 'Restricted'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True)
# Related incident
incident = models.ForeignKey(
'incident_intelligence.Incident',
on_delete=models.CASCADE,
related_name='war_rooms'
)
# Room configuration
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ACTIVE')
privacy_level = models.CharField(max_length=20, choices=PRIVACY_CHOICES, default='PRIVATE')
# Integration configuration
slack_channel_id = models.CharField(max_length=100, blank=True, null=True, help_text="Slack channel ID")
teams_channel_id = models.CharField(max_length=100, blank=True, null=True, help_text="Teams channel ID")
discord_channel_id = models.CharField(max_length=100, blank=True, null=True, help_text="Discord channel ID")
# Access control
allowed_users = models.ManyToManyField(
User,
blank=True,
related_name='accessible_war_rooms',
help_text="Users with access to this war room"
)
required_clearance_level = models.ForeignKey(
'security.DataClassification',
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Required clearance level for access"
)
# Activity tracking
message_count = models.PositiveIntegerField(default=0)
last_activity = models.DateTimeField(null=True, blank=True)
active_participants = models.PositiveIntegerField(default=0)
# Metadata
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_war_rooms')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
archived_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['incident', 'status']),
models.Index(fields=['status', 'privacy_level']),
models.Index(fields=['created_at']),
]
def __str__(self):
return f"War Room: {self.name} ({self.incident.title})"
def can_user_access(self, user: User) -> bool:
"""Check if user can access this war room"""
# Check clearance level requirement
if self.required_clearance_level:
if not user.has_data_access(self.required_clearance_level.level):
return False
# Check if user is explicitly allowed
if self.allowed_users.exists():
return self.allowed_users.filter(id=user.id).exists()
# Check incident access
return self.incident.is_accessible_by_user(user)
def add_participant(self, user: User):
"""Add user to war room participants"""
if not self.allowed_users.filter(id=user.id).exists():
self.allowed_users.add(user)
def remove_participant(self, user: User):
"""Remove user from war room participants"""
self.allowed_users.remove(user)
class ConferenceBridge(models.Model):
"""Conference bridge integration for incident collaboration"""
BRIDGE_TYPES = [
('ZOOM', 'Zoom'),
('TEAMS', 'Microsoft Teams'),
('WEBEX', 'Cisco Webex'),
('GOTO_MEETING', 'GoTo Meeting'),
('CUSTOM', 'Custom Bridge'),
]
STATUS_CHOICES = [
('SCHEDULED', 'Scheduled'),
('ACTIVE', 'Active'),
('ENDED', 'Ended'),
('CANCELLED', 'Cancelled'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True)
# Related incident and war room
incident = models.ForeignKey(
'incident_intelligence.Incident',
on_delete=models.CASCADE,
related_name='conference_bridges'
)
war_room = models.ForeignKey(
WarRoom,
on_delete=models.CASCADE,
related_name='conference_bridges',
null=True,
blank=True
)
# Bridge configuration
bridge_type = models.CharField(max_length=20, choices=BRIDGE_TYPES)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='SCHEDULED')
# Meeting details
meeting_id = models.CharField(max_length=255, blank=True, null=True, help_text="External meeting ID")
meeting_url = models.URLField(blank=True, null=True, help_text="Meeting URL")
dial_in_number = models.CharField(max_length=50, blank=True, null=True, help_text="Dial-in phone number")
access_code = models.CharField(max_length=20, blank=True, null=True, help_text="Access code for dial-in")
# Schedule
scheduled_start = models.DateTimeField(help_text="Scheduled start time")
scheduled_end = models.DateTimeField(help_text="Scheduled end time")
actual_start = models.DateTimeField(null=True, blank=True)
actual_end = models.DateTimeField(null=True, blank=True)
# Participants
invited_participants = models.ManyToManyField(
User,
blank=True,
related_name='invited_conferences',
help_text="Users invited to the conference"
)
active_participants = models.ManyToManyField(
User,
blank=True,
related_name='active_conferences',
help_text="Users currently in the conference"
)
max_participants = models.PositiveIntegerField(default=50)
# Recording and transcription
recording_enabled = models.BooleanField(default=False)
recording_url = models.URLField(blank=True, null=True, help_text="URL to recorded meeting")
transcription_enabled = models.BooleanField(default=False)
transcription_url = models.URLField(blank=True, null=True, help_text="URL to meeting transcription")
# Integration configuration
integration_config = models.JSONField(
default=dict,
help_text="Configuration for external bridge integration"
)
# Metadata
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_conferences')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-scheduled_start']
indexes = [
models.Index(fields=['incident', 'status']),
models.Index(fields=['bridge_type', 'status']),
models.Index(fields=['scheduled_start']),
]
def __str__(self):
return f"Conference: {self.name} ({self.bridge_type})"
def is_active(self) -> bool:
"""Check if conference is currently active"""
now = timezone.now()
return (self.status == 'ACTIVE' and
self.scheduled_start <= now <= self.scheduled_end)
def can_user_join(self, user: User) -> bool:
"""Check if user can join the conference"""
# Check if user is invited
if self.invited_participants.exists():
return self.invited_participants.filter(id=user.id).exists()
# Check incident access
return self.incident.is_accessible_by_user(user)
def add_participant(self, user: User):
"""Add user to active participants"""
if self.can_user_join(user):
self.active_participants.add(user)
if not self.invited_participants.filter(id=user.id).exists():
self.invited_participants.add(user)
class IncidentCommandRole(models.Model):
"""Incident command roles and assignments"""
ROLE_TYPES = [
('INCIDENT_COMMANDER', 'Incident Commander'),
('SCRIBE', 'Scribe'),
('COMMS_LEAD', 'Communications Lead'),
('TECHNICAL_LEAD', 'Technical Lead'),
('BUSINESS_LEAD', 'Business Lead'),
('EXTERNAL_LIAISON', 'External Liaison'),
('OBSERVER', 'Observer'),
]
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('INACTIVE', 'Inactive'),
('REASSIGNED', 'Reassigned'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Related incident and war room
incident = models.ForeignKey(
'incident_intelligence.Incident',
on_delete=models.CASCADE,
related_name='command_roles'
)
war_room = models.ForeignKey(
WarRoom,
on_delete=models.CASCADE,
related_name='command_roles',
null=True,
blank=True
)
# Role assignment
role_type = models.CharField(max_length=30, choices=ROLE_TYPES)
assigned_user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='assigned_command_roles'
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ACTIVE')
# Role responsibilities
responsibilities = models.JSONField(
default=list,
help_text="List of responsibilities for this role"
)
decision_authority = models.JSONField(
default=list,
help_text="Areas where this role has decision authority"
)
# Assignment tracking
assigned_at = models.DateTimeField(auto_now_add=True)
reassigned_at = models.DateTimeField(null=True, blank=True)
reassigned_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='reassigned_command_roles'
)
assignment_notes = models.TextField(blank=True, null=True)
# Performance tracking
decisions_made = models.PositiveIntegerField(default=0)
communications_sent = models.PositiveIntegerField(default=0)
last_activity = models.DateTimeField(null=True, blank=True)
# Metadata
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_command_roles')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-assigned_at']
unique_together = ['incident', 'role_type', 'assigned_user']
indexes = [
models.Index(fields=['incident', 'role_type']),
models.Index(fields=['assigned_user', 'status']),
models.Index(fields=['status', 'assigned_at']),
]
def __str__(self):
return f"{self.get_role_type_display()} - {self.assigned_user.username if self.assigned_user else 'Unassigned'}"
def can_make_decision(self, decision_area: str) -> bool:
"""Check if this role can make decisions in the given area"""
return decision_area in self.decision_authority
def reassign_role(self, new_user: User, reassigned_by: User, notes: str = None):
"""Reassign this role to a new user"""
self.assigned_user = new_user
self.reassigned_at = timezone.now()
self.reassigned_by = reassigned_by
self.assignment_notes = notes
self.status = 'REASSIGNED'
self.save()
class TimelineEvent(models.Model):
"""Timeline reconstruction for postmortems (automatically ordered events + human notes)"""
EVENT_TYPES = [
('INCIDENT_CREATED', 'Incident Created'),
('INCIDENT_UPDATED', 'Incident Updated'),
('ASSIGNMENT_CHANGED', 'Assignment Changed'),
('STATUS_CHANGED', 'Status Changed'),
('SEVERITY_CHANGED', 'Severity Changed'),
('COMMENT_ADDED', 'Comment Added'),
('RUNBOOK_EXECUTED', 'Runbook Executed'),
('AUTO_REMEDIATION_ATTEMPTED', 'Auto-remediation Attempted'),
('SLA_BREACHED', 'SLA Breached'),
('ESCALATION_TRIGGERED', 'Escalation Triggered'),
('WAR_ROOM_CREATED', 'War Room Created'),
('CONFERENCE_STARTED', 'Conference Started'),
('COMMAND_ROLE_ASSIGNED', 'Command Role Assigned'),
('DECISION_MADE', 'Decision Made'),
('COMMUNICATION_SENT', 'Communication Sent'),
('EXTERNAL_INTEGRATION', 'External Integration'),
('MANUAL_EVENT', 'Manual Event'),
]
SOURCE_TYPES = [
('SYSTEM', 'System Generated'),
('USER', 'User Created'),
('INTEGRATION', 'External Integration'),
('AUTOMATION', 'Automation'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Related incident
incident = models.ForeignKey(
'incident_intelligence.Incident',
on_delete=models.CASCADE,
related_name='timeline_events'
)
# Event details
event_type = models.CharField(max_length=30, choices=EVENT_TYPES)
title = models.CharField(max_length=200)
description = models.TextField()
source_type = models.CharField(max_length=20, choices=SOURCE_TYPES, default='SYSTEM')
# Timing
event_time = models.DateTimeField(help_text="When the event occurred")
created_at = models.DateTimeField(auto_now_add=True)
# Related objects
related_user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='timeline_events'
)
related_runbook_execution = models.ForeignKey(
'automation_orchestration.RunbookExecution',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='timeline_events'
)
related_auto_remediation = models.ForeignKey(
'automation_orchestration.AutoRemediationExecution',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='timeline_events'
)
related_sla_instance = models.ForeignKey(
'sla_oncall.SLAInstance',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='timeline_events'
)
related_escalation = models.ForeignKey(
'sla_oncall.EscalationInstance',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='timeline_events'
)
related_war_room = models.ForeignKey(
WarRoom,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='timeline_events'
)
related_conference = models.ForeignKey(
ConferenceBridge,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='timeline_events'
)
related_command_role = models.ForeignKey(
IncidentCommandRole,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='timeline_events'
)
# Event data
event_data = models.JSONField(
default=dict,
help_text="Additional data related to the event"
)
tags = models.JSONField(
default=list,
help_text="Tags for categorization and filtering"
)
# Postmortem relevance
is_critical_event = models.BooleanField(
default=False,
help_text="Whether this event is critical for postmortem analysis"
)
postmortem_notes = models.TextField(
blank=True,
null=True,
help_text="Additional notes added during postmortem"
)
# Metadata
created_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_timeline_events'
)
class Meta:
ordering = ['event_time', 'created_at']
indexes = [
models.Index(fields=['incident', 'event_time']),
models.Index(fields=['event_type', 'event_time']),
models.Index(fields=['source_type', 'event_time']),
models.Index(fields=['is_critical_event', 'event_time']),
]
def __str__(self):
return f"{self.event_time} - {self.title}"
@classmethod
def create_system_event(cls, incident, event_type: str, title: str, description: str,
event_time: datetime = None, event_data: dict = None, **kwargs):
"""Create a system-generated timeline event"""
if event_time is None:
event_time = timezone.now()
return cls.objects.create(
incident=incident,
event_type=event_type,
title=title,
description=description,
event_time=event_time,
source_type='SYSTEM',
event_data=event_data or {},
**kwargs
)
@classmethod
def create_user_event(cls, incident, user: User, event_type: str, title: str,
description: str, event_time: datetime = None, **kwargs):
"""Create a user-generated timeline event"""
if event_time is None:
event_time = timezone.now()
return cls.objects.create(
incident=incident,
event_type=event_type,
title=title,
description=description,
event_time=event_time,
source_type='USER',
related_user=user,
created_by=user,
**kwargs
)
class WarRoomMessage(models.Model):
"""Messages within war rooms for collaboration"""
MESSAGE_TYPES = [
('TEXT', 'Text Message'),
('SYSTEM', 'System Message'),
('COMMAND', 'Command Message'),
('ALERT', 'Alert Message'),
('UPDATE', 'Status Update'),
('FILE', 'File Attachment'),
('BOT', 'Bot Message'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Related war room
war_room = models.ForeignKey(
WarRoom,
on_delete=models.CASCADE,
related_name='messages'
)
# Message details
message_type = models.CharField(max_length=20, choices=MESSAGE_TYPES, default='TEXT')
content = models.TextField()
# Sender information
sender = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='war_room_messages'
)
sender_name = models.CharField(max_length=100, help_text="Display name of sender")
# Message metadata
is_edited = models.BooleanField(default=False)
edited_at = models.DateTimeField(null=True, blank=True)
is_pinned = models.BooleanField(default=False, help_text="Whether this message is pinned")
pinned_at = models.DateTimeField(null=True, blank=True)
pinned_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='pinned_messages'
)
reply_to = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='replies'
)
# File attachments
attachments = models.JSONField(
default=list,
help_text="List of file attachments with metadata"
)
# Mentions and notifications
mentioned_users = models.ManyToManyField(
User,
blank=True,
related_name='mentioned_in_messages',
help_text="Users mentioned in this message"
)
notification_sent = models.BooleanField(default=False)
# Integration data
external_message_id = models.CharField(
max_length=255,
blank=True,
null=True,
help_text="ID in external system (Slack, Teams, etc.)"
)
external_data = models.JSONField(
default=dict,
help_text="Additional data from external system"
)
# Encryption and security
is_encrypted = models.BooleanField(default=False)
encryption_key_id = models.CharField(max_length=255, blank=True, null=True)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['created_at']
indexes = [
models.Index(fields=['war_room', 'created_at']),
models.Index(fields=['sender', 'created_at']),
models.Index(fields=['message_type', 'created_at']),
models.Index(fields=['is_pinned', 'created_at']),
]
def __str__(self):
return f"{self.sender_name}: {self.content[:50]}..."
def pin_message(self, user: User):
"""Pin this message"""
self.is_pinned = True
self.pinned_at = timezone.now()
self.pinned_by = user
self.save()
def unpin_message(self):
"""Unpin this message"""
self.is_pinned = False
self.pinned_at = None
self.pinned_by = None
self.save()
def add_reaction(self, user: User, emoji: str):
"""Add a reaction to this message"""
reaction, created = MessageReaction.objects.get_or_create(
message=self,
user=user,
emoji=emoji
)
return reaction
def remove_reaction(self, user: User, emoji: str):
"""Remove a reaction from this message"""
MessageReaction.objects.filter(
message=self,
user=user,
emoji=emoji
).delete()
def get_reactions_summary(self):
"""Get summary of reactions for this message"""
from django.db.models import Count
return self.reactions.values('emoji').annotate(count=Count('emoji')).order_by('-count')
class MessageReaction(models.Model):
"""Reactions to messages (👍, 🚨, ✅, etc.)"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
message = models.ForeignKey(
WarRoomMessage,
on_delete=models.CASCADE,
related_name='reactions'
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
emoji = models.CharField(max_length=10, help_text="Emoji reaction")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ['message', 'user', 'emoji']
ordering = ['created_at']
indexes = [
models.Index(fields=['message', 'emoji']),
models.Index(fields=['user', 'created_at']),
]
def __str__(self):
return f"{self.user.username} reacted {self.emoji} to message"
class ChatFile(models.Model):
"""File attachments in chat messages with compliance integration"""
FILE_TYPES = [
('IMAGE', 'Image'),
('DOCUMENT', 'Document'),
('LOG', 'Log File'),
('SCREENSHOT', 'Screenshot'),
('EVIDENCE', 'Evidence'),
('OTHER', 'Other'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
message = models.ForeignKey(
WarRoomMessage,
on_delete=models.CASCADE,
related_name='chat_files'
)
# File details
filename = models.CharField(max_length=255)
original_filename = models.CharField(max_length=255)
file_type = models.CharField(max_length=20, choices=FILE_TYPES)
file_size = models.PositiveIntegerField(help_text="File size in bytes")
mime_type = models.CharField(max_length=100)
# Storage
file_path = models.CharField(max_length=500, help_text="Path to stored file")
file_url = models.URLField(blank=True, null=True, help_text="Public URL for file access")
# Security and compliance
data_classification = models.ForeignKey(
'security.DataClassification',
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Data classification level for this file"
)
is_encrypted = models.BooleanField(default=False)
encryption_key_id = models.CharField(max_length=255, blank=True, null=True)
# Chain of custody
file_hash = models.CharField(max_length=64, help_text="SHA-256 hash of file")
uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
uploaded_at = models.DateTimeField(auto_now_add=True)
# Access control
access_log = models.JSONField(
default=list,
help_text="Log of who accessed this file and when"
)
class Meta:
ordering = ['-uploaded_at']
indexes = [
models.Index(fields=['message', 'file_type']),
models.Index(fields=['data_classification']),
models.Index(fields=['uploaded_at']),
]
def __str__(self):
return f"{self.original_filename} ({self.file_type})"
def log_access(self, user: User):
"""Log file access for audit trail"""
access_entry = {
'user_id': str(user.id),
'username': user.username,
'timestamp': timezone.now().isoformat(),
'action': 'access'
}
self.access_log.append(access_entry)
self.save(update_fields=['access_log'])
class ChatCommand(models.Model):
"""ChatOps commands for automation integration"""
COMMAND_TYPES = [
('STATUS', 'Status Check'),
('RUNBOOK', 'Execute Runbook'),
('ESCALATE', 'Escalate Incident'),
('ASSIGN', 'Assign Incident'),
('UPDATE', 'Update Status'),
('CUSTOM', 'Custom Command'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
message = models.ForeignKey(
WarRoomMessage,
on_delete=models.CASCADE,
related_name='chat_commands'
)
# Command details
command_type = models.CharField(max_length=20, choices=COMMAND_TYPES)
command_text = models.CharField(max_length=500, help_text="Full command text")
parameters = models.JSONField(
default=dict,
help_text="Parsed command parameters"
)
# Execution
executed_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
executed_at = models.DateTimeField(null=True, blank=True)
execution_status = models.CharField(
max_length=20,
choices=[
('PENDING', 'Pending'),
('EXECUTING', 'Executing'),
('SUCCESS', 'Success'),
('FAILED', 'Failed'),
('CANCELLED', 'Cancelled'),
],
default='PENDING'
)
execution_result = models.JSONField(
default=dict,
help_text="Result of command execution"
)
error_message = models.TextField(blank=True, null=True)
# Integration
automation_execution = models.ForeignKey(
'automation_orchestration.RunbookExecution',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='chat_commands'
)
class Meta:
ordering = ['-executed_at']
indexes = [
models.Index(fields=['command_type', 'execution_status']),
models.Index(fields=['executed_by', 'executed_at']),
]
def __str__(self):
return f"{self.command_type}: {self.command_text[:50]}"
def execute_command(self, user: User):
"""Execute the chat command"""
self.executed_by = user
self.executed_at = timezone.now()
self.execution_status = 'EXECUTING'
self.save()
try:
# Execute based on command type
if self.command_type == 'STATUS':
result = self._execute_status_command()
elif self.command_type == 'RUNBOOK':
result = self._execute_runbook_command()
elif self.command_type == 'ESCALATE':
result = self._execute_escalate_command()
else:
result = {'error': 'Unknown command type'}
self.execution_result = result
self.execution_status = 'SUCCESS' if 'error' not in result else 'FAILED'
except Exception as e:
self.execution_status = 'FAILED'
self.error_message = str(e)
self.execution_result = {'error': str(e)}
self.save()
return self.execution_result
def _execute_status_command(self):
"""Execute status check command"""
incident = self.message.war_room.incident
return {
'incident_id': str(incident.id),
'title': incident.title,
'status': incident.status,
'severity': incident.severity,
'assigned_to': incident.assigned_to.username if incident.assigned_to else None,
'created_at': incident.created_at.isoformat(),
'updated_at': incident.updated_at.isoformat(),
}
def _execute_runbook_command(self):
"""Execute runbook command"""
from .services.automation_commands import AutomationCommandService
# Parse runbook name from parameters
args = self.parameters.get('args', [])
if not args:
return {'error': 'Runbook name is required. Usage: /run playbook <runbook-name>'}
runbook_name = ' '.join(args)
return AutomationCommandService.execute_runbook_command(self, runbook_name, self.executed_by)
def _execute_escalate_command(self):
"""Execute escalation command"""
from .services.sla_notifications import SLANotificationService
# Get escalation policies for this incident
incident = self.message.war_room.incident
escalation_policies = incident.escalation_instances.filter(status='PENDING')
if not escalation_policies.exists():
return {'message': 'No pending escalations found for this incident'}
# Trigger escalation
escalation = escalation_policies.first()
escalation.status = 'TRIGGERED'
escalation.triggered_at = timezone.now()
escalation.save()
# Send notification
SLANotificationService.send_escalation_notification(escalation)
return {
'success': True,
'escalation_id': str(escalation.id),
'policy_name': escalation.escalation_policy.name,
'level': escalation.escalation_level
}
class ChatBot(models.Model):
"""AI assistant bot for chat rooms"""
BOT_TYPES = [
('INCIDENT_ASSISTANT', 'Incident Assistant'),
('KNOWLEDGE_BOT', 'Knowledge Bot'),
('AUTOMATION_BOT', 'Automation Bot'),
('COMPLIANCE_BOT', 'Compliance Bot'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100)
bot_type = models.CharField(max_length=30, choices=BOT_TYPES)
description = models.TextField()
# Configuration
is_active = models.BooleanField(default=True)
auto_respond = models.BooleanField(default=False)
response_triggers = models.JSONField(
default=list,
help_text="Keywords that trigger bot responses"
)
# Integration
knowledge_base = models.ForeignKey(
'knowledge_learning.KnowledgeBaseArticle',
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Knowledge base article for bot responses"
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['name']
indexes = [
models.Index(fields=['bot_type', 'is_active']),
]
def __str__(self):
return f"{self.name} ({self.get_bot_type_display()})"
def generate_response(self, message: WarRoomMessage, context: dict = None):
"""Generate AI response to a message"""
from .services.ai_assistant import AIAssistantService
return AIAssistantService.generate_response(self, message, context)
class IncidentDecision(models.Model):
"""Decisions made during incident response"""
DECISION_TYPES = [
('TECHNICAL', 'Technical Decision'),
('BUSINESS', 'Business Decision'),
('COMMUNICATION', 'Communication Decision'),
('ESCALATION', 'Escalation Decision'),
('RESOURCE', 'Resource Allocation'),
('TIMELINE', 'Timeline Decision'),
]
STATUS_CHOICES = [
('PENDING', 'Pending'),
('APPROVED', 'Approved'),
('REJECTED', 'Rejected'),
('IMPLEMENTED', 'Implemented'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Related incident and command role
incident = models.ForeignKey(
'incident_intelligence.Incident',
on_delete=models.CASCADE,
related_name='decisions'
)
command_role = models.ForeignKey(
IncidentCommandRole,
on_delete=models.CASCADE,
related_name='decisions'
)
# Decision details
decision_type = models.CharField(max_length=20, choices=DECISION_TYPES)
title = models.CharField(max_length=200)
description = models.TextField()
rationale = models.TextField(help_text="Reasoning behind the decision")
# Decision status
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
requires_approval = models.BooleanField(default=False)
approved_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='approved_decisions'
)
approved_at = models.DateTimeField(null=True, blank=True)
# Implementation
implementation_notes = models.TextField(blank=True, null=True)
implemented_at = models.DateTimeField(null=True, blank=True)
implemented_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='implemented_decisions'
)
# Impact tracking
impact_assessment = models.TextField(blank=True, null=True)
success_metrics = models.JSONField(
default=list,
help_text="Metrics to measure decision success"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['incident', 'status']),
models.Index(fields=['command_role', 'decision_type']),
models.Index(fields=['status', 'created_at']),
]
def __str__(self):
return f"Decision: {self.title} ({self.get_status_display()})"
def approve(self, approver: User):
"""Approve the decision"""
self.status = 'APPROVED'
self.approved_by = approver
self.approved_at = timezone.now()
self.save()
# Create timeline event
TimelineEvent.create_user_event(
incident=self.incident,
user=approver,
event_type='DECISION_MADE',
title=f"Decision Approved: {self.title}",
description=f"Decision '{self.title}' was approved by {approver.username}",
related_command_role=self.command_role,
event_data={'decision_id': str(self.id), 'action': 'approved'}
)
def implement(self, implementer: User, notes: str = None):
"""Mark decision as implemented"""
self.status = 'IMPLEMENTED'
self.implemented_by = implementer
self.implemented_at = timezone.now()
if notes:
self.implementation_notes = notes
self.save()
# Create timeline event
TimelineEvent.create_user_event(
incident=self.incident,
user=implementer,
event_type='DECISION_MADE',
title=f"Decision Implemented: {self.title}",
description=f"Decision '{self.title}' was implemented by {implementer.username}",
related_command_role=self.command_role,
event_data={'decision_id': str(self.id), 'action': 'implemented'}
)