1084 lines
36 KiB
Python
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'}
|
|
)
|