This commit is contained in:
Iliyan Angelov
2025-09-19 11:58:53 +03:00
parent 306b20e24a
commit 6b247e5b9f
11423 changed files with 1500615 additions and 778 deletions

View File

@@ -0,0 +1,586 @@
# Collaboration & War Rooms API Documentation
## Overview
The Collaboration & War Rooms module provides real-time incident collaboration capabilities including war rooms, conference bridges, incident command roles, and timeline reconstruction for postmortems.
## Features
- **Real-time Incident Rooms**: Auto-created Slack/Teams channels per incident
- **Conference Bridge Integration**: Zoom, Teams, Webex integration
- **Incident Command Roles**: Assign Incident Commander, Scribe, Comms Lead
- **Timeline Reconstruction**: Automatically ordered events + human notes for postmortems
## API Endpoints
### War Rooms
#### List War Rooms
```
GET /api/collaboration-war-rooms/war-rooms/
```
**Query Parameters:**
- `status`: Filter by status (ACTIVE, ARCHIVED, CLOSED)
- `privacy_level`: Filter by privacy level (PUBLIC, PRIVATE, RESTRICTED)
- `incident__severity`: Filter by incident severity
- `search`: Search in name, description, incident title
- `ordering`: Order by created_at, last_activity, message_count
**Response:**
```json
{
"count": 10,
"next": null,
"previous": null,
"results": [
{
"id": "uuid",
"name": "Incident 123 - Database Outage",
"incident_title": "Database Outage",
"incident_severity": "CRITICAL",
"status": "ACTIVE",
"privacy_level": "PRIVATE",
"message_count": 45,
"last_activity": "2024-01-15T10:30:00Z",
"participant_count": 5,
"created_at": "2024-01-15T09:00:00Z"
}
]
}
```
#### Create War Room
```
POST /api/collaboration-war-rooms/war-rooms/
```
**Request Body:**
```json
{
"name": "Incident 123 - Database Outage",
"description": "War room for database outage incident",
"incident_id": "uuid",
"privacy_level": "PRIVATE",
"allowed_user_ids": ["uuid1", "uuid2"]
}
```
#### Get War Room Details
```
GET /api/collaboration-war-rooms/war-rooms/{id}/
```
#### Update War Room
```
PUT /api/collaboration-war-rooms/war-rooms/{id}/
PATCH /api/collaboration-war-rooms/war-rooms/{id}/
```
#### Add Participant
```
POST /api/collaboration-war-rooms/war-rooms/{id}/add_participant/
```
**Request Body:**
```json
{
"user_id": "uuid"
}
```
#### Remove Participant
```
POST /api/collaboration-war-rooms/war-rooms/{id}/remove_participant/
```
**Request Body:**
```json
{
"user_id": "uuid"
}
```
#### Get War Room Messages
```
GET /api/collaboration-war-rooms/war-rooms/{id}/messages/
```
### Conference Bridges
#### List Conference Bridges
```
GET /api/collaboration-war-rooms/conference-bridges/
```
**Query Parameters:**
- `bridge_type`: Filter by bridge type (ZOOM, TEAMS, WEBEX, etc.)
- `status`: Filter by status (SCHEDULED, ACTIVE, ENDED, CANCELLED)
- `incident__severity`: Filter by incident severity
- `search`: Search in name, description, incident title
- `ordering`: Order by scheduled_start, created_at
#### Create Conference Bridge
```
POST /api/collaboration-war-rooms/conference-bridges/
```
**Request Body:**
```json
{
"name": "Incident 123 - Database Outage Call",
"description": "Emergency conference call for database outage",
"incident_id": "uuid",
"war_room_id": "uuid",
"bridge_type": "ZOOM",
"scheduled_start": "2024-01-15T10:00:00Z",
"scheduled_end": "2024-01-15T11:00:00Z",
"invited_participant_ids": ["uuid1", "uuid2"],
"recording_enabled": true,
"transcription_enabled": true
}
```
#### Join Conference
```
POST /api/collaboration-war-rooms/conference-bridges/{id}/join_conference/
```
#### Start Conference
```
POST /api/collaboration-war-rooms/conference-bridges/{id}/start_conference/
```
#### End Conference
```
POST /api/collaboration-war-rooms/conference-bridges/{id}/end_conference/
```
### Incident Command Roles
#### List Command Roles
```
GET /api/collaboration-war-rooms/command-roles/
```
**Query Parameters:**
- `role_type`: Filter by role type (INCIDENT_COMMANDER, SCRIBE, COMMS_LEAD, etc.)
- `status`: Filter by status (ACTIVE, INACTIVE, REASSIGNED)
- `incident__severity`: Filter by incident severity
- `search`: Search in incident title, assigned user username
- `ordering`: Order by assigned_at, created_at
#### Create Command Role
```
POST /api/collaboration-war-rooms/command-roles/
```
**Request Body:**
```json
{
"incident_id": "uuid",
"war_room_id": "uuid",
"role_type": "INCIDENT_COMMANDER",
"assigned_user_id": "uuid",
"responsibilities": [
"Overall incident coordination",
"Decision making authority",
"Communication with stakeholders"
],
"decision_authority": [
"TECHNICAL",
"BUSINESS",
"ESCALATION"
]
}
```
#### Reassign Role
```
POST /api/collaboration-war-rooms/command-roles/{id}/reassign_role/
```
**Request Body:**
```json
{
"new_user_id": "uuid",
"notes": "Reassigning due to shift change"
}
```
### Timeline Events
#### List Timeline Events
```
GET /api/collaboration-war-rooms/timeline-events/
```
**Query Parameters:**
- `event_type`: Filter by event type (INCIDENT_CREATED, STATUS_CHANGED, etc.)
- `source_type`: Filter by source type (SYSTEM, USER, INTEGRATION, AUTOMATION)
- `is_critical_event`: Filter critical events for postmortems
- `incident__severity`: Filter by incident severity
- `search`: Search in title, description, incident title
- `ordering`: Order by event_time, created_at
#### Get Critical Events
```
GET /api/collaboration-war-rooms/timeline-events/critical_events/
```
**Response:**
```json
{
"count": 5,
"results": [
{
"id": "uuid",
"incident_title": "Database Outage",
"event_type": "SLA_BREACHED",
"title": "SLA Breached: Response Time",
"description": "SLA 'Response Time' has been breached",
"source_type": "SYSTEM",
"event_time": "2024-01-15T10:15:00Z",
"related_user_name": null,
"is_critical_event": true,
"created_at": "2024-01-15T10:15:00Z"
}
]
}
```
### War Room Messages
#### List Messages
```
GET /api/collaboration-war-rooms/war-room-messages/
```
**Query Parameters:**
- `message_type`: Filter by message type (TEXT, SYSTEM, COMMAND, ALERT, UPDATE)
- `war_room`: Filter by war room ID
- `sender`: Filter by sender ID
- `search`: Search in content, sender name
- `ordering`: Order by created_at
#### Create Message
```
POST /api/collaboration-war-rooms/war-room-messages/
```
**Request Body:**
```json
{
"war_room_id": "uuid",
"message_type": "TEXT",
"content": "Database connection restored. Monitoring for stability.",
"sender_id": "uuid",
"sender_name": "John Doe"
}
```
### Incident Decisions
#### List Decisions
```
GET /api/collaboration-war-rooms/incident-decisions/
```
**Query Parameters:**
- `decision_type`: Filter by decision type (TECHNICAL, BUSINESS, COMMUNICATION, etc.)
- `status`: Filter by status (PENDING, APPROVED, REJECTED, IMPLEMENTED)
- `incident__severity`: Filter by incident severity
- `search`: Search in title, description, incident title
- `ordering`: Order by created_at, approved_at, implemented_at
#### Create Decision
```
POST /api/collaboration-war-rooms/incident-decisions/
```
**Request Body:**
```json
{
"incident_id": "uuid",
"command_role_id": "uuid",
"decision_type": "TECHNICAL",
"title": "Restart Database Cluster",
"description": "Decision to restart the primary database cluster to resolve connection issues",
"rationale": "Multiple connection timeouts indicate cluster instability. Restart should resolve the issue.",
"requires_approval": true
}
```
#### Approve Decision
```
POST /api/collaboration-war-rooms/incident-decisions/{id}/approve_decision/
```
#### Implement Decision
```
POST /api/collaboration-war-rooms/incident-decisions/{id}/implement_decision/
```
**Request Body:**
```json
{
"notes": "Database cluster restarted successfully. All connections restored."
}
```
## Data Models
### WarRoom
- `id`: UUID primary key
- `name`: War room name
- `description`: War room description
- `incident`: Related incident (ForeignKey)
- `status`: ACTIVE, ARCHIVED, CLOSED
- `privacy_level`: PUBLIC, PRIVATE, RESTRICTED
- `slack_channel_id`: Slack channel ID
- `teams_channel_id`: Teams channel ID
- `discord_channel_id`: Discord channel ID
- `allowed_users`: Users with access (ManyToMany)
- `required_clearance_level`: Required security clearance
- `message_count`: Number of messages
- `last_activity`: Last activity timestamp
- `active_participants`: Number of active participants
- `created_by`: Creator (ForeignKey to User)
- `created_at`: Creation timestamp
- `updated_at`: Last update timestamp
- `archived_at`: Archive timestamp
### ConferenceBridge
- `id`: UUID primary key
- `name`: Conference name
- `description`: Conference description
- `incident`: Related incident (ForeignKey)
- `war_room`: Related war room (ForeignKey)
- `bridge_type`: ZOOM, TEAMS, WEBEX, GOTO_MEETING, CUSTOM
- `status`: SCHEDULED, ACTIVE, ENDED, CANCELLED
- `meeting_id`: External meeting ID
- `meeting_url`: Meeting URL
- `dial_in_number`: Dial-in phone number
- `access_code`: Access code for dial-in
- `scheduled_start`: Scheduled start time
- `scheduled_end`: Scheduled end time
- `actual_start`: Actual start time
- `actual_end`: Actual end time
- `invited_participants`: Invited users (ManyToMany)
- `active_participants`: Active users (ManyToMany)
- `max_participants`: Maximum participants
- `recording_enabled`: Recording enabled flag
- `recording_url`: Recording URL
- `transcription_enabled`: Transcription enabled flag
- `transcription_url`: Transcription URL
- `integration_config`: Integration configuration (JSON)
- `created_by`: Creator (ForeignKey to User)
- `created_at`: Creation timestamp
- `updated_at`: Last update timestamp
### IncidentCommandRole
- `id`: UUID primary key
- `incident`: Related incident (ForeignKey)
- `war_room`: Related war room (ForeignKey)
- `role_type`: INCIDENT_COMMANDER, SCRIBE, COMMS_LEAD, TECHNICAL_LEAD, BUSINESS_LEAD, EXTERNAL_LIAISON, OBSERVER
- `assigned_user`: Assigned user (ForeignKey to User)
- `status`: ACTIVE, INACTIVE, REASSIGNED
- `responsibilities`: List of responsibilities (JSON)
- `decision_authority`: Areas of decision authority (JSON)
- `assigned_at`: Assignment timestamp
- `reassigned_at`: Reassignment timestamp
- `reassigned_by`: User who reassigned (ForeignKey to User)
- `assignment_notes`: Assignment notes
- `decisions_made`: Number of decisions made
- `communications_sent`: Number of communications sent
- `last_activity`: Last activity timestamp
- `created_by`: Creator (ForeignKey to User)
- `created_at`: Creation timestamp
- `updated_at`: Last update timestamp
### TimelineEvent
- `id`: UUID primary key
- `incident`: Related incident (ForeignKey)
- `event_type`: Event type (INCIDENT_CREATED, STATUS_CHANGED, etc.)
- `title`: Event title
- `description`: Event description
- `source_type`: SYSTEM, USER, INTEGRATION, AUTOMATION
- `event_time`: When the event occurred
- `created_at`: Creation timestamp
- `related_user`: Related user (ForeignKey to User)
- `related_runbook_execution`: Related runbook execution (ForeignKey)
- `related_auto_remediation`: Related auto-remediation (ForeignKey)
- `related_sla_instance`: Related SLA instance (ForeignKey)
- `related_escalation`: Related escalation (ForeignKey)
- `related_war_room`: Related war room (ForeignKey)
- `related_conference`: Related conference (ForeignKey)
- `related_command_role`: Related command role (ForeignKey)
- `event_data`: Additional event data (JSON)
- `tags`: Event tags (JSON)
- `is_critical_event`: Critical for postmortem flag
- `postmortem_notes`: Postmortem notes
- `created_by`: Creator (ForeignKey to User)
### WarRoomMessage
- `id`: UUID primary key
- `war_room`: Related war room (ForeignKey)
- `message_type`: TEXT, SYSTEM, COMMAND, ALERT, UPDATE
- `content`: Message content
- `sender`: Sender user (ForeignKey to User)
- `sender_name`: Display name of sender
- `is_edited`: Edited flag
- `edited_at`: Edit timestamp
- `reply_to`: Reply to message (ForeignKey to self)
- `external_message_id`: External system message ID
- `external_data`: External system data (JSON)
- `created_at`: Creation timestamp
- `updated_at`: Last update timestamp
### IncidentDecision
- `id`: UUID primary key
- `incident`: Related incident (ForeignKey)
- `command_role`: Related command role (ForeignKey)
- `decision_type`: TECHNICAL, BUSINESS, COMMUNICATION, ESCALATION, RESOURCE, TIMELINE
- `title`: Decision title
- `description`: Decision description
- `rationale`: Decision rationale
- `status`: PENDING, APPROVED, REJECTED, IMPLEMENTED
- `requires_approval`: Requires approval flag
- `approved_by`: Approver (ForeignKey to User)
- `approved_at`: Approval timestamp
- `implementation_notes`: Implementation notes
- `implemented_at`: Implementation timestamp
- `implemented_by`: Implementer (ForeignKey to User)
- `impact_assessment`: Impact assessment
- `success_metrics`: Success metrics (JSON)
- `created_at`: Creation timestamp
- `updated_at`: Last update timestamp
## Integration Points
### Automatic War Room Creation
- War rooms are automatically created when new incidents are created
- Incident reporter and assignee are automatically added as participants
- Timeline events are created for war room creation
### Timeline Event Integration
- Timeline events are automatically created for:
- Incident status changes
- Severity changes
- Assignment changes
- Runbook executions
- Auto-remediation attempts
- SLA breaches
- Escalation triggers
- Command role assignments
### Security Integration
- War room access is controlled by incident access permissions
- Required clearance levels can be set for war rooms
- All actions are logged for audit purposes
### SLA & On-Call Integration
- Conference bridges can be linked to SLA instances
- Command roles can be assigned to on-call personnel
- Timeline events track SLA breaches and escalations
### Automation Integration
- Timeline events are created for runbook executions
- Auto-remediation attempts are tracked in timeline
- War rooms can be integrated with ChatOps platforms
## Error Handling
### Common Error Responses
#### 400 Bad Request
```json
{
"error": "user_id is required"
}
```
#### 403 Forbidden
```json
{
"error": "You do not have permission to join this conference"
}
```
#### 404 Not Found
```json
{
"error": "User not found"
}
```
## Authentication
All endpoints require authentication. Include the authentication token in the request headers:
```
Authorization: Token your-auth-token-here
```
## Rate Limiting
API requests are rate limited to prevent abuse. Standard rate limits apply:
- 1000 requests per hour per user
- 100 requests per minute per user
## Webhooks
The system supports webhooks for real-time notifications:
### War Room Events
- `war_room.created`: War room created
- `war_room.updated`: War room updated
- `war_room.archived`: War room archived
### Conference Events
- `conference.scheduled`: Conference scheduled
- `conference.started`: Conference started
- `conference.ended`: Conference ended
### Timeline Events
- `timeline_event.created`: Timeline event created
- `timeline_event.critical`: Critical timeline event created
### Decision Events
- `decision.created`: Decision created
- `decision.approved`: Decision approved
- `decision.implemented`: Decision implemented
## Examples
### Complete Incident Response Flow
1. **Incident Created** → War room automatically created
2. **Assign Command Roles** → Incident Commander, Scribe, Comms Lead
3. **Schedule Conference** → Emergency call for critical incidents
4. **Make Decisions** → Track all decisions with approval workflow
5. **Timeline Reconstruction** → Automatic + manual events for postmortem
### Integration with External Systems
```python
# Create war room with Slack integration
war_room = WarRoom.objects.create(
name="Incident 123 - Database Outage",
incident=incident,
slack_channel_id="C1234567890"
)
# Create conference bridge with Zoom
conference = ConferenceBridge.objects.create(
name="Emergency Call - Database Outage",
incident=incident,
war_room=war_room,
bridge_type="ZOOM",
scheduled_start=timezone.now() + timedelta(minutes=5),
scheduled_end=timezone.now() + timedelta(hours=1),
recording_enabled=True
)
```
This module provides comprehensive collaboration capabilities for incident response, ensuring effective communication, decision tracking, and postmortem analysis.

View File

@@ -0,0 +1,425 @@
# Incident-Centric Chat API Documentation
## Overview
The Incident-Centric Chat system provides real-time collaboration capabilities for incident response teams. Every incident automatically gets its own chat room with advanced features including pinned messages, reactions, file sharing, ChatOps commands, and AI assistant integration.
## Key Features
### 1. Incident-Centric Chat Rooms
- **Auto-creation**: Chat rooms are automatically created when incidents are created
- **Cross-linking**: Direct links between incident timeline and chat logs
- **Access Control**: RBAC-based access control with security clearance levels
### 2. Collaboration Features
- **@mentions**: Mention users with notifications
- **Threaded Conversations**: Reply to messages for sub-discussions
- **Reactions**: Emoji reactions (👍, 🚨, ✅) for lightweight feedback
- **Pinned Messages**: Pin important updates for easy reference
### 3. Media & Files
- **File Sharing**: Upload logs, screenshots, evidence files
- **Compliance Integration**: Automatic file classification (PUBLIC/CONFIDENTIAL/etc.)
- **Chain of Custody**: File hashing and access logging for evidence
- **Encryption**: Optional encryption for sensitive files
### 4. ChatOps Integration
- **Commands**: Execute automation commands via chat
- **Status Checks**: `/status incident-123` to fetch incident status
- **Runbook Execution**: `/run playbook ransomware-incident`
- **Escalation**: `/escalate` to trigger escalation procedures
### 5. Security Features
- **Encryption**: Chat logs encrypted at rest and in transit
- **Audit Trail**: Immutable audit trail for compliance
- **RBAC**: Role-based access control for sensitive incidents
- **Data Classification**: Automatic classification of shared content
## API Endpoints
### War Rooms
#### List War Rooms
```http
GET /api/collaboration_war_rooms/api/war-rooms/
```
#### Get War Room Details
```http
GET /api/collaboration_war_rooms/api/war-rooms/{id}/
```
#### Create Chat Room for Incident
```http
POST /api/collaboration_war_rooms/api/war-rooms/{incident_id}/create_chat_room/
```
#### Get War Room Messages
```http
GET /api/collaboration_war_rooms/api/war-rooms/{id}/messages/
```
#### Get Pinned Messages
```http
GET /api/collaboration_war_rooms/api/war-rooms/{id}/pinned_messages/
```
### Messages
#### Send Message
```http
POST /api/collaboration_war_rooms/api/war-room-messages/
Content-Type: application/json
{
"war_room_id": "uuid",
"content": "Message content",
"message_type": "TEXT",
"mentioned_user_ids": ["user-uuid-1", "user-uuid-2"]
}
```
#### Pin Message
```http
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/pin_message/
```
#### Unpin Message
```http
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/unpin_message/
```
#### Add Reaction
```http
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/add_reaction/
Content-Type: application/json
{
"emoji": "👍"
}
```
#### Remove Reaction
```http
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/remove_reaction/
Content-Type: application/json
{
"emoji": "👍"
}
```
#### Execute ChatOps Command
```http
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/execute_command/
Content-Type: application/json
{
"command_text": "/status"
}
```
### File Management
#### Upload File
```http
POST /api/collaboration_war_rooms/api/chat-files/
Content-Type: multipart/form-data
{
"message": "message-uuid",
"file": "file-data",
"file_type": "SCREENSHOT"
}
```
#### Log File Access
```http
POST /api/collaboration_war_rooms/api/chat-files/{id}/log_access/
```
### Chat Bots
#### List Chat Bots
```http
GET /api/collaboration_war_rooms/api/chat-bots/
```
#### Generate AI Response
```http
POST /api/collaboration_war_rooms/api/chat-bots/{id}/generate_response/
Content-Type: application/json
{
"message_id": "message-uuid",
"context": {}
}
```
## WebSocket API
### Connection
```javascript
const ws = new WebSocket('ws://localhost:8000/ws/chat/{room_id}/');
```
### Message Types
#### Send Chat Message
```javascript
ws.send(JSON.stringify({
type: 'chat_message',
content: 'Hello team!',
message_type: 'TEXT',
reply_to_id: 'optional-message-id'
}));
```
#### Add Reaction
```javascript
ws.send(JSON.stringify({
type: 'reaction',
message_id: 'message-uuid',
emoji: '👍',
action: 'add' // or 'remove'
}));
```
#### Execute Command
```javascript
ws.send(JSON.stringify({
type: 'command',
message_id: 'message-uuid',
command_text: '/status'
}));
```
#### Typing Indicator
```javascript
ws.send(JSON.stringify({
type: 'typing',
is_typing: true
}));
```
### Receive Messages
#### Chat Message
```javascript
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'chat_message') {
// Handle new message
console.log('New message:', data.data);
}
};
```
#### Reaction Update
```javascript
if (data.type === 'reaction_update') {
// Handle reaction update
console.log('Reaction update:', data.data);
}
```
#### Command Result
```javascript
if (data.type === 'command_result') {
// Handle command execution result
console.log('Command result:', data.data);
}
```
## ChatOps Commands
### Available Commands
#### Status Check
```
/status
```
Returns current incident status, severity, assignee, and timestamps.
#### Runbook Execution
```
/run playbook <playbook-name>
```
Executes a runbook for the current incident.
#### Escalation
```
/escalate [reason]
```
Triggers escalation procedures for the incident.
#### Assignment
```
/assign <username>
```
Assigns the incident to a specific user.
#### Status Update
```
/update status <new-status>
```
Updates the incident status.
### Command Response Format
```json
{
"command_type": "STATUS",
"execution_status": "SUCCESS",
"execution_result": {
"incident_id": "uuid",
"title": "Incident Title",
"status": "IN_PROGRESS",
"severity": "HIGH",
"assigned_to": "username",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
}
```
## Integration Points
### Incident Intelligence
- Auto-creates chat rooms when incidents are created
- Links chat messages to incident timeline
- Updates incident status via ChatOps commands
### SLA & On-Call
- Sends notifications when SLA thresholds are hit
- Integrates with escalation procedures
- Notifies on-call teams of critical updates
### Automation Orchestration
- Executes runbooks via chat commands
- Triggers auto-remediation procedures
- Provides status updates on automation execution
### Compliance & Governance
- Classifies files automatically
- Maintains audit trails for all chat activity
- Enforces data retention policies
### Security
- Encrypts sensitive messages and files
- Enforces RBAC for incident access
- Logs all security-relevant activities
### Knowledge Learning
- AI assistant provides contextual help
- Suggests similar past incidents
- Learns from chat interactions
## Security Considerations
### Access Control
- Users must have appropriate clearance level for sensitive incidents
- War room access is controlled by incident permissions
- File access is logged and audited
### Encryption
- Messages can be encrypted for sensitive incidents
- Files are encrypted based on classification level
- WebSocket connections use WSS in production
### Audit Trail
- All chat messages are logged with timestamps
- File access is tracked with user and timestamp
- Command executions are logged with results
## Best Practices
### Message Organization
- Use pinned messages for important updates
- Use reactions for quick feedback
- Use threaded replies for focused discussions
### File Management
- Classify files appropriately
- Use descriptive filenames
- Clean up temporary files regularly
### Command Usage
- Use commands for automation, not manual updates
- Verify command results before proceeding
- Document custom commands for team use
### Security
- Be mindful of sensitive information in chat
- Use appropriate classification levels
- Report security incidents immediately
## Error Handling
### Common Error Responses
#### Access Denied
```json
{
"error": "You do not have permission to access this war room"
}
```
#### Invalid Command
```json
{
"error": "Unknown command type"
}
```
#### File Upload Error
```json
{
"error": "File size exceeds limit"
}
```
### WebSocket Errors
```json
{
"type": "error",
"message": "Authentication required"
}
```
## Rate Limiting
- Message sending: 60 messages per minute per user
- File uploads: 10 files per minute per user
- Command execution: 20 commands per minute per user
- WebSocket connections: 5 concurrent connections per user
## Monitoring & Analytics
### Metrics Tracked
- Message volume per incident
- Response times for commands
- File upload/download statistics
- User engagement metrics
- Error rates and types
### Alerts
- High message volume incidents
- Failed command executions
- Security policy violations
- System performance issues
## Future Enhancements
### Planned Features
- Voice messages and video calls
- Advanced AI assistant capabilities
- Integration with external chat platforms
- Mobile app support
- Advanced analytics dashboard
### Integration Roadmap
- Slack/Teams integration
- PagerDuty integration
- Jira integration
- Custom webhook support

View File

@@ -0,0 +1,240 @@
# Incident-Centric Chat System
A comprehensive real-time collaboration platform for incident response teams, integrated with the ETB (Enterprise Incident Management) API.
## 🚀 Features
### Core Chat Functionality
- **Real-time Messaging**: WebSocket-based chat with instant message delivery
- **Incident-Centric Rooms**: Every incident automatically gets its own chat room
- **Cross-linking**: Direct links between incident timeline and chat logs
- **Pinned Messages**: Pin important updates for easy reference
- **Threaded Conversations**: Reply to messages for focused discussions
- **Reactions**: Emoji reactions (👍, 🚨, ✅) for lightweight feedback
### Advanced Collaboration
- **@mentions**: Mention users with notifications
- **File Sharing**: Upload logs, screenshots, evidence files
- **ChatOps Commands**: Execute automation commands via chat
- **AI Assistant**: Intelligent bot for incident guidance and knowledge queries
### Security & Compliance
- **Encryption**: Chat logs encrypted at rest and in transit
- **RBAC**: Role-based access control for sensitive incidents
- **Audit Trail**: Immutable audit trail for compliance
- **Data Classification**: Automatic file classification and retention
### Integrations
- **Incident Intelligence**: Auto-creates chat rooms, links to timeline
- **SLA & On-Call**: SLA threshold notifications, escalation alerts
- **Automation Orchestration**: Execute runbooks via chat commands
- **Compliance Governance**: File classification, audit trails
- **Knowledge Learning**: AI assistant with knowledge base integration
## 📋 Quick Start
### 1. Setup
```bash
# Activate virtual environment
source venv/bin/activate.fish
# Run migrations
python manage.py migrate
# Create default bots and war rooms
python manage.py setup_chat_system --create-bots --create-war-rooms
```
### 2. WebSocket Connection
```javascript
// Connect to chat room
const ws = new WebSocket('ws://localhost:8000/ws/chat/{room_id}/');
// Send message
ws.send(JSON.stringify({
type: 'chat_message',
content: 'Hello team!',
message_type: 'TEXT'
}));
// Add reaction
ws.send(JSON.stringify({
type: 'reaction',
message_id: 'message-uuid',
emoji: '👍',
action: 'add'
}));
```
### 3. ChatOps Commands
```
/status # Get incident status
/run playbook <name> # Execute runbook
/escalate [reason] # Trigger escalation
/assign <username> # Assign incident
/update status <status> # Update incident status
```
## 🏗️ Architecture
### Models
- **WarRoom**: Chat rooms for incidents
- **WarRoomMessage**: Chat messages with reactions, attachments
- **MessageReaction**: Emoji reactions to messages
- **ChatFile**: File attachments with compliance integration
- **ChatCommand**: ChatOps command execution
- **ChatBot**: AI assistant bots
### Services
- **SLANotificationService**: SLA threshold and escalation notifications
- **AutomationCommandService**: ChatOps command execution
- **ComplianceIntegrationService**: File classification and audit trails
- **AIAssistantService**: AI-powered assistance and suggestions
### WebSocket Consumer
- **ChatConsumer**: Real-time chat functionality
- Message broadcasting
- Reaction handling
- Command execution
- Typing indicators
## 🔧 API Endpoints
### War Rooms
```http
GET /api/collaboration_war_rooms/api/war-rooms/
POST /api/collaboration_war_rooms/api/war-rooms/{id}/create_chat_room/
GET /api/collaboration_war_rooms/api/war-rooms/{id}/messages/
GET /api/collaboration_war_rooms/api/war-rooms/{id}/pinned_messages/
```
### Messages
```http
POST /api/collaboration_war_rooms/api/war-room-messages/
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/pin_message/
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/add_reaction/
POST /api/collaboration_war_rooms/api/war-room-messages/{id}/execute_command/
```
### Files
```http
POST /api/collaboration_war_rooms/api/chat-files/
POST /api/collaboration_war_rooms/api/chat-files/{id}/log_access/
```
### AI Assistant
```http
GET /api/collaboration_war_rooms/api/chat-bots/
POST /api/collaboration_war_rooms/api/chat-bots/{id}/generate_response/
```
## 🔐 Security Features
### Access Control
- Users must have appropriate clearance level for sensitive incidents
- War room access controlled by incident permissions
- File access logged and audited
### Encryption
- Messages can be encrypted for sensitive incidents
- Files encrypted based on classification level
- WebSocket connections use WSS in production
### Audit Trail
- All chat messages logged with timestamps
- File access tracked with user and timestamp
- Command executions logged with results
## 📊 Monitoring & Analytics
### Metrics Tracked
- Message volume per incident
- Response times for commands
- File upload/download statistics
- User engagement metrics
- Error rates and types
### Alerts
- High message volume incidents
- Failed command executions
- Security policy violations
- System performance issues
## 🚀 Deployment
### Production Setup
1. Configure WebSocket routing in your ASGI application
2. Set up Redis for WebSocket channel layers
3. Configure file storage for attachments
4. Set up SSL certificates for WSS connections
5. Configure monitoring and alerting
### Environment Variables
```bash
# WebSocket configuration
CHANNEL_LAYERS_REDIS_URL=redis://localhost:6379/1
# File storage
DEFAULT_FILE_STORAGE=django.core.files.storage.FileSystemStorage
MEDIA_ROOT=/var/www/media/
# Security
CHAT_ENCRYPTION_KEY=your-encryption-key
CHAT_AUDIT_LOG_LEVEL=INFO
```
## 🧪 Testing
### Unit Tests
```bash
python manage.py test collaboration_war_rooms
```
### WebSocket Testing
```javascript
// Test WebSocket connection
const ws = new WebSocket('ws://localhost:8000/ws/chat/test-room/');
ws.onopen = () => console.log('Connected');
ws.onmessage = (event) => console.log('Message:', event.data);
```
## 📚 Documentation
- [API Documentation](Documentations/INCIDENT_CENTRIC_CHAT_API.md)
- [WebSocket API Reference](Documentations/INCIDENT_CENTRIC_CHAT_API.md#websocket-api)
- [ChatOps Commands](Documentations/INCIDENT_CENTRIC_CHAT_API.md#chatops-commands)
- [Security Guidelines](Documentations/INCIDENT_CENTRIC_CHAT_API.md#security-considerations)
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests
5. Submit a pull request
## 📄 License
This project is part of the ETB (Enterprise Incident Management) API system.
## 🆘 Support
For support and questions:
- Check the documentation
- Review the API reference
- Contact the development team
## 🔮 Roadmap
### Planned Features
- Voice messages and video calls
- Advanced AI assistant capabilities
- Integration with external chat platforms
- Mobile app support
- Advanced analytics dashboard
### Integration Roadmap
- Slack/Teams integration
- PagerDuty integration
- Jira integration
- Custom webhook support

View File

@@ -0,0 +1,279 @@
"""
Admin configuration for Collaboration & War Rooms module
"""
from django.contrib import admin
from django.utils.html import format_html
from .models import (
WarRoom, ConferenceBridge, IncidentCommandRole,
TimelineEvent, WarRoomMessage, IncidentDecision
)
@admin.register(WarRoom)
class WarRoomAdmin(admin.ModelAdmin):
"""Admin interface for WarRoom model"""
list_display = [
'name', 'incident_title', 'status', 'privacy_level',
'message_count', 'active_participants', 'created_at'
]
list_filter = ['status', 'privacy_level', 'created_at']
search_fields = ['name', 'description', 'incident__title']
readonly_fields = ['id', 'created_at', 'updated_at', 'message_count', 'last_activity']
fieldsets = (
('Basic Information', {
'fields': ('id', 'name', 'description', 'incident')
}),
('Configuration', {
'fields': ('status', 'privacy_level', 'required_clearance_level')
}),
('Integrations', {
'fields': ('slack_channel_id', 'teams_channel_id', 'discord_channel_id'),
'classes': ('collapse',)
}),
('Access Control', {
'fields': ('allowed_users',),
'classes': ('collapse',)
}),
('Activity Tracking', {
'fields': ('message_count', 'last_activity', 'active_participants'),
'classes': ('collapse',)
}),
('Metadata', {
'fields': ('created_by', 'created_at', 'updated_at', 'archived_at'),
'classes': ('collapse',)
}),
)
def incident_title(self, obj):
"""Display incident title"""
return obj.incident.title
incident_title.short_description = 'Incident'
@admin.register(ConferenceBridge)
class ConferenceBridgeAdmin(admin.ModelAdmin):
"""Admin interface for ConferenceBridge model"""
list_display = [
'name', 'incident_title', 'bridge_type', 'status',
'scheduled_start', 'participant_count'
]
list_filter = ['bridge_type', 'status', 'recording_enabled', 'transcription_enabled']
search_fields = ['name', 'description', 'incident__title']
readonly_fields = ['id', 'created_at', 'updated_at', 'actual_start', 'actual_end']
fieldsets = (
('Basic Information', {
'fields': ('id', 'name', 'description', 'incident', 'war_room')
}),
('Bridge Configuration', {
'fields': ('bridge_type', 'status', 'integration_config')
}),
('Meeting Details', {
'fields': ('meeting_id', 'meeting_url', 'dial_in_number', 'access_code')
}),
('Schedule', {
'fields': ('scheduled_start', 'scheduled_end', 'actual_start', 'actual_end')
}),
('Participants', {
'fields': ('invited_participants', 'active_participants', 'max_participants'),
'classes': ('collapse',)
}),
('Recording & Transcription', {
'fields': ('recording_enabled', 'recording_url', 'transcription_enabled', 'transcription_url'),
'classes': ('collapse',)
}),
('Metadata', {
'fields': ('created_by', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def incident_title(self, obj):
"""Display incident title"""
return obj.incident.title
incident_title.short_description = 'Incident'
def participant_count(self, obj):
"""Display participant count"""
return obj.invited_participants.count()
participant_count.short_description = 'Participants'
@admin.register(IncidentCommandRole)
class IncidentCommandRoleAdmin(admin.ModelAdmin):
"""Admin interface for IncidentCommandRole model"""
list_display = [
'role_type', 'incident_title', 'assigned_user', 'status',
'decisions_made', 'assigned_at'
]
list_filter = ['role_type', 'status', 'assigned_at']
search_fields = ['incident__title', 'assigned_user__username']
readonly_fields = [
'id', 'assigned_at', 'reassigned_at', 'reassigned_by',
'decisions_made', 'communications_sent', 'last_activity',
'created_at', 'updated_at'
]
fieldsets = (
('Basic Information', {
'fields': ('id', 'incident', 'war_room', 'role_type', 'assigned_user', 'status')
}),
('Role Configuration', {
'fields': ('responsibilities', 'decision_authority'),
'classes': ('collapse',)
}),
('Assignment Tracking', {
'fields': ('assigned_at', 'reassigned_at', 'reassigned_by', 'assignment_notes'),
'classes': ('collapse',)
}),
('Performance Tracking', {
'fields': ('decisions_made', 'communications_sent', 'last_activity'),
'classes': ('collapse',)
}),
('Metadata', {
'fields': ('created_by', 'created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def incident_title(self, obj):
"""Display incident title"""
return obj.incident.title
incident_title.short_description = 'Incident'
@admin.register(TimelineEvent)
class TimelineEventAdmin(admin.ModelAdmin):
"""Admin interface for TimelineEvent model"""
list_display = [
'event_time', 'title', 'incident_title', 'event_type',
'source_type', 'is_critical_event'
]
list_filter = ['event_type', 'source_type', 'is_critical_event', 'event_time']
search_fields = ['title', 'description', 'incident__title']
readonly_fields = ['id', 'created_at']
fieldsets = (
('Basic Information', {
'fields': ('id', 'incident', 'event_type', 'title', 'description', 'source_type')
}),
('Timing', {
'fields': ('event_time', 'created_at')
}),
('Related Objects', {
'fields': (
'related_user', 'related_runbook_execution', 'related_auto_remediation',
'related_sla_instance', 'related_escalation', 'related_war_room',
'related_conference', 'related_command_role'
),
'classes': ('collapse',)
}),
('Event Data', {
'fields': ('event_data', 'tags'),
'classes': ('collapse',)
}),
('Postmortem', {
'fields': ('is_critical_event', 'postmortem_notes'),
'classes': ('collapse',)
}),
('Metadata', {
'fields': ('created_by',),
'classes': ('collapse',)
}),
)
def incident_title(self, obj):
"""Display incident title"""
return obj.incident.title
incident_title.short_description = 'Incident'
@admin.register(WarRoomMessage)
class WarRoomMessageAdmin(admin.ModelAdmin):
"""Admin interface for WarRoomMessage model"""
list_display = [
'sender_name', 'war_room_name', 'message_type', 'content_preview', 'created_at'
]
list_filter = ['message_type', 'is_edited', 'created_at']
search_fields = ['content', 'sender_name', 'war_room__name']
readonly_fields = ['id', 'created_at', 'updated_at', 'is_edited', 'edited_at']
fieldsets = (
('Basic Information', {
'fields': ('id', 'war_room', 'message_type', 'content')
}),
('Sender Information', {
'fields': ('sender', 'sender_name')
}),
('Message Metadata', {
'fields': ('is_edited', 'edited_at', 'reply_to'),
'classes': ('collapse',)
}),
('Integration Data', {
'fields': ('external_message_id', 'external_data'),
'classes': ('collapse',)
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def war_room_name(self, obj):
"""Display war room name"""
return obj.war_room.name
war_room_name.short_description = 'War Room'
def content_preview(self, obj):
"""Display content preview"""
return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
content_preview.short_description = 'Content Preview'
@admin.register(IncidentDecision)
class IncidentDecisionAdmin(admin.ModelAdmin):
"""Admin interface for IncidentDecision model"""
list_display = [
'title', 'incident_title', 'decision_type', 'status',
'approved_by', 'implemented_by', 'created_at'
]
list_filter = ['decision_type', 'status', 'requires_approval', 'created_at']
search_fields = ['title', 'description', 'incident__title']
readonly_fields = [
'id', 'approved_by', 'approved_at', 'implemented_at',
'implemented_by', 'created_at', 'updated_at'
]
fieldsets = (
('Basic Information', {
'fields': ('id', 'incident', 'command_role', 'decision_type', 'title', 'description', 'rationale')
}),
('Decision Status', {
'fields': ('status', 'requires_approval', 'approved_by', 'approved_at')
}),
('Implementation', {
'fields': ('implementation_notes', 'implemented_at', 'implemented_by'),
'classes': ('collapse',)
}),
('Impact Tracking', {
'fields': ('impact_assessment', 'success_metrics'),
'classes': ('collapse',)
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def incident_title(self, obj):
"""Display incident title"""
return obj.incident.title
incident_title.short_description = 'Incident'

View File

@@ -0,0 +1,11 @@
from django.apps import AppConfig
class CollaborationWarRoomsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'collaboration_war_rooms'
verbose_name = 'Collaboration & War Rooms'
def ready(self):
"""Import signal handlers when the app is ready"""
import collaboration_war_rooms.signals

View File

@@ -0,0 +1,414 @@
"""
WebSocket consumers for real-time chat functionality
"""
import json
import uuid
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
from django.utils import timezone
from .models import WarRoom, WarRoomMessage, MessageReaction, ChatCommand
User = get_user_model()
class ChatConsumer(AsyncWebsocketConsumer):
"""WebSocket consumer for real-time chat in war rooms"""
async def connect(self):
"""Connect to WebSocket"""
self.room_id = self.scope['url_route']['kwargs']['room_id']
self.room_group_name = f'chat_{self.room_id}'
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Send room info
room_info = await self.get_room_info()
await self.send(text_data=json.dumps({
'type': 'room_info',
'data': room_info
}))
async def disconnect(self, close_code):
"""Disconnect from WebSocket"""
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
"""Receive message from WebSocket"""
try:
data = json.loads(text_data)
message_type = data.get('type')
if message_type == 'chat_message':
await self.handle_chat_message(data)
elif message_type == 'reaction':
await self.handle_reaction(data)
elif message_type == 'command':
await self.handle_command(data)
elif message_type == 'typing':
await self.handle_typing(data)
else:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Unknown message type'
}))
except json.JSONDecodeError:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Invalid JSON'
}))
except Exception as e:
await self.send(text_data=json.dumps({
'type': 'error',
'message': str(e)
}))
async def handle_chat_message(self, data):
"""Handle chat message"""
content = data.get('content', '').strip()
message_type = data.get('message_type', 'TEXT')
reply_to_id = data.get('reply_to_id')
if not content:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Message content cannot be empty'
}))
return
# Create message
message = await self.create_message(content, message_type, reply_to_id)
if message:
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': await self.serialize_message(message)
}
)
# Check for mentions and send notifications
await self.handle_mentions(message)
# Check for commands
await self.check_for_commands(message)
async def handle_reaction(self, data):
"""Handle message reaction"""
message_id = data.get('message_id')
emoji = data.get('emoji')
action = data.get('action', 'add') # 'add' or 'remove'
if not message_id or not emoji:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Message ID and emoji are required'
}))
return
# Handle reaction
if action == 'add':
reaction = await self.add_reaction(message_id, emoji)
else:
reaction = await self.remove_reaction(message_id, emoji)
if reaction is not False:
# Send reaction update to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'reaction_update',
'message_id': message_id,
'reaction': await self.serialize_reaction(reaction) if reaction else None,
'action': action
}
)
async def handle_command(self, data):
"""Handle ChatOps command"""
message_id = data.get('message_id')
command_text = data.get('command_text')
if not message_id or not command_text:
await self.send(text_data=json.dumps({
'type': 'error',
'message': 'Message ID and command text are required'
}))
return
# Execute command
result = await self.execute_command(message_id, command_text)
# Send command result to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'command_result',
'message_id': message_id,
'result': result
}
)
async def handle_typing(self, data):
"""Handle typing indicator"""
is_typing = data.get('is_typing', False)
# Send typing indicator to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'typing_indicator',
'user': await self.get_user_info(),
'is_typing': is_typing
}
)
async def chat_message(self, event):
"""Send chat message to WebSocket"""
await self.send(text_data=json.dumps({
'type': 'chat_message',
'data': event['message']
}))
async def reaction_update(self, event):
"""Send reaction update to WebSocket"""
await self.send(text_data=json.dumps({
'type': 'reaction_update',
'data': {
'message_id': event['message_id'],
'reaction': event['reaction'],
'action': event['action']
}
}))
async def command_result(self, event):
"""Send command result to WebSocket"""
await self.send(text_data=json.dumps({
'type': 'command_result',
'data': {
'message_id': event['message_id'],
'result': event['result']
}
}))
async def typing_indicator(self, event):
"""Send typing indicator to WebSocket"""
await self.send(text_data=json.dumps({
'type': 'typing_indicator',
'data': {
'user': event['user'],
'is_typing': event['is_typing']
}
}))
@database_sync_to_async
def get_room_info(self):
"""Get room information"""
try:
room = WarRoom.objects.get(id=self.room_id)
return {
'id': str(room.id),
'name': room.name,
'incident_id': str(room.incident.id),
'incident_title': room.incident.title,
'participant_count': room.allowed_users.count(),
'message_count': room.message_count
}
except WarRoom.DoesNotExist:
return None
@database_sync_to_async
def get_user_info(self):
"""Get current user information"""
user = self.scope['user']
if user.is_authenticated:
return {
'id': str(user.id),
'username': user.username,
'display_name': getattr(user, 'display_name', user.username)
}
return None
@database_sync_to_async
def create_message(self, content, message_type, reply_to_id=None):
"""Create a new message"""
try:
room = WarRoom.objects.get(id=self.room_id)
user = self.scope['user']
if not user.is_authenticated:
return None
# Check if user has access to room
if not room.can_user_access(user):
return None
reply_to = None
if reply_to_id:
try:
reply_to = WarRoomMessage.objects.get(id=reply_to_id)
except WarRoomMessage.DoesNotExist:
pass
message = WarRoomMessage.objects.create(
war_room=room,
content=content,
message_type=message_type,
sender=user,
sender_name=user.username,
reply_to=reply_to
)
# Update room message count
room.message_count += 1
room.last_activity = timezone.now()
room.save(update_fields=['message_count', 'last_activity'])
return message
except WarRoom.DoesNotExist:
return None
@database_sync_to_async
def serialize_message(self, message):
"""Serialize message for WebSocket"""
return {
'id': str(message.id),
'content': message.content,
'message_type': message.message_type,
'sender': {
'id': str(message.sender.id) if message.sender else None,
'username': message.sender.username if message.sender else None,
'display_name': message.sender_name
},
'is_pinned': message.is_pinned,
'reply_to_id': str(message.reply_to.id) if message.reply_to else None,
'created_at': message.created_at.isoformat(),
'reactions': list(message.get_reactions_summary())
}
@database_sync_to_async
def add_reaction(self, message_id, emoji):
"""Add reaction to message"""
try:
message = WarRoomMessage.objects.get(id=message_id)
user = self.scope['user']
if not user.is_authenticated:
return False
reaction = message.add_reaction(user, emoji)
return reaction
except WarRoomMessage.DoesNotExist:
return False
@database_sync_to_async
def remove_reaction(self, message_id, emoji):
"""Remove reaction from message"""
try:
message = WarRoomMessage.objects.get(id=message_id)
user = self.scope['user']
if not user.is_authenticated:
return False
message.remove_reaction(user, emoji)
return True
except WarRoomMessage.DoesNotExist:
return False
@database_sync_to_async
def serialize_reaction(self, reaction):
"""Serialize reaction for WebSocket"""
return {
'id': str(reaction.id),
'emoji': reaction.emoji,
'user': {
'id': str(reaction.user.id),
'username': reaction.user.username
},
'created_at': reaction.created_at.isoformat()
}
@database_sync_to_async
def execute_command(self, message_id, command_text):
"""Execute ChatOps command"""
try:
message = WarRoomMessage.objects.get(id=message_id)
user = self.scope['user']
if not user.is_authenticated:
return {'error': 'Authentication required'}
# Parse command
command_type = self._parse_command_type(command_text)
parameters = self._parse_command_parameters(command_text)
# Create chat command
chat_command = ChatCommand.objects.create(
message=message,
command_type=command_type,
command_text=command_text,
parameters=parameters
)
# Execute command
result = chat_command.execute_command(user)
return result
except WarRoomMessage.DoesNotExist:
return {'error': 'Message not found'}
def _parse_command_type(self, command_text):
"""Parse command type from command text"""
command_text = command_text.lower().strip()
if command_text.startswith('/status'):
return 'STATUS'
elif command_text.startswith('/runbook'):
return 'RUNBOOK'
elif command_text.startswith('/escalate'):
return 'ESCALATE'
elif command_text.startswith('/assign'):
return 'ASSIGN'
elif command_text.startswith('/update'):
return 'UPDATE'
else:
return 'CUSTOM'
def _parse_command_parameters(self, command_text):
"""Parse command parameters from command text"""
parts = command_text.split()
if len(parts) > 1:
return {'args': parts[1:]}
return {}
@database_sync_to_async
def handle_mentions(self, message):
"""Handle user mentions in message"""
# This would integrate with notification system
# For now, just a placeholder
pass
@database_sync_to_async
def check_for_commands(self, message):
"""Check if message contains commands"""
# This would check for command patterns and execute them
# For now, just a placeholder
pass

View File

@@ -0,0 +1,186 @@
"""
Management command to set up the incident-centric chat system
"""
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from django.utils import timezone
from ...models import ChatBot, WarRoom
from incident_intelligence.models import Incident
User = get_user_model()
class Command(BaseCommand):
help = 'Set up the incident-centric chat system with default bots and configurations'
def add_arguments(self, parser):
parser.add_argument(
'--create-bots',
action='store_true',
help='Create default AI assistant bots',
)
parser.add_argument(
'--create-war-rooms',
action='store_true',
help='Create war rooms for existing incidents',
)
parser.add_argument(
'--force',
action='store_true',
help='Force recreation of existing bots and war rooms',
)
def handle(self, *args, **options):
self.stdout.write(
self.style.SUCCESS('Setting up incident-centric chat system...')
)
if options['create_bots']:
self.create_default_bots(options['force'])
if options['create_war_rooms']:
self.create_war_rooms_for_incidents(options['force'])
self.stdout.write(
self.style.SUCCESS('Chat system setup completed successfully!')
)
def create_default_bots(self, force=False):
"""Create default AI assistant bots"""
self.stdout.write('Creating default AI assistant bots...')
bots_config = [
{
'name': 'Incident Assistant',
'bot_type': 'INCIDENT_ASSISTANT',
'description': 'AI assistant for incident management and response guidance',
'auto_respond': True,
'response_triggers': ['help', 'assist', 'guidance', 'incident', 'problem']
},
{
'name': 'Knowledge Bot',
'bot_type': 'KNOWLEDGE_BOT',
'description': 'AI assistant for knowledge base queries and documentation',
'auto_respond': False,
'response_triggers': ['how', 'what', 'where', 'documentation', 'knowledge']
},
{
'name': 'Automation Bot',
'bot_type': 'AUTOMATION_BOT',
'description': 'AI assistant for automation and runbook execution',
'auto_respond': False,
'response_triggers': ['runbook', 'automation', 'execute', 'playbook']
},
{
'name': 'Compliance Bot',
'bot_type': 'COMPLIANCE_BOT',
'description': 'AI assistant for compliance and audit requirements',
'auto_respond': False,
'response_triggers': ['compliance', 'audit', 'policy', 'retention']
}
]
for bot_config in bots_config:
bot, created = ChatBot.objects.get_or_create(
name=bot_config['name'],
defaults={
'bot_type': bot_config['bot_type'],
'description': bot_config['description'],
'is_active': True,
'auto_respond': bot_config['auto_respond'],
'response_triggers': bot_config['response_triggers']
}
)
if created:
self.stdout.write(
self.style.SUCCESS(f'Created bot: {bot.name}')
)
elif force:
bot.bot_type = bot_config['bot_type']
bot.description = bot_config['description']
bot.auto_respond = bot_config['auto_respond']
bot.response_triggers = bot_config['response_triggers']
bot.save()
self.stdout.write(
self.style.WARNING(f'Updated bot: {bot.name}')
)
else:
self.stdout.write(
self.style.WARNING(f'Bot already exists: {bot.name}')
)
def create_war_rooms_for_incidents(self, force=False):
"""Create war rooms for existing incidents"""
self.stdout.write('Creating war rooms for existing incidents...')
incidents = Incident.objects.all()
created_count = 0
updated_count = 0
for incident in incidents:
war_room, created = WarRoom.objects.get_or_create(
incident=incident,
defaults={
'name': f"Incident {incident.id} - {incident.title[:50]}",
'description': f"War room for incident: {incident.title}",
'created_by': incident.reporter,
'privacy_level': 'PRIVATE',
'status': 'ACTIVE'
}
)
if created:
# Add incident reporter and assignee to war room
if incident.reporter:
war_room.add_participant(incident.reporter)
if incident.assigned_to:
war_room.add_participant(incident.assigned_to)
created_count += 1
self.stdout.write(
self.style.SUCCESS(f'Created war room for incident: {incident.title}')
)
elif force:
war_room.name = f"Incident {incident.id} - {incident.title[:50]}"
war_room.description = f"War room for incident: {incident.title}"
war_room.save()
updated_count += 1
self.stdout.write(
self.style.WARNING(f'Updated war room for incident: {incident.title}')
)
self.stdout.write(
self.style.SUCCESS(
f'Created {created_count} new war rooms, updated {updated_count} existing war rooms'
)
)
def create_sample_data(self):
"""Create sample data for testing"""
self.stdout.write('Creating sample data...')
# Create a sample incident if none exist
if not Incident.objects.exists():
sample_incident = Incident.objects.create(
title='Sample Database Connection Issue',
description='Database connection timeout affecting user authentication',
severity='HIGH',
status='OPEN',
category='Database',
subcategory='Connection'
)
# Create war room for sample incident
WarRoom.objects.create(
incident=sample_incident,
name=f"Incident {sample_incident.id} - {sample_incident.title}",
description=f"War room for incident: {sample_incident.title}",
privacy_level='PRIVATE',
status='ACTIVE'
)
self.stdout.write(
self.style.SUCCESS('Created sample incident and war room')
)

View File

@@ -0,0 +1,261 @@
# Generated by Django 5.2.6 on 2025-09-18 16:26
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('automation_orchestration', '0002_autoremediationexecution_sla_instance_and_more'),
('incident_intelligence', '0004_incident_oncall_assignment_incident_sla_override_and_more'),
('security', '0002_user_emergency_contact_user_oncall_preferences_and_more'),
('sla_oncall', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ConferenceBridge',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=200)),
('description', models.TextField(blank=True, null=True)),
('bridge_type', models.CharField(choices=[('ZOOM', 'Zoom'), ('TEAMS', 'Microsoft Teams'), ('WEBEX', 'Cisco Webex'), ('GOTO_MEETING', 'GoTo Meeting'), ('CUSTOM', 'Custom Bridge')], max_length=20)),
('status', models.CharField(choices=[('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('ENDED', 'Ended'), ('CANCELLED', 'Cancelled')], default='SCHEDULED', max_length=20)),
('meeting_id', models.CharField(blank=True, help_text='External meeting ID', max_length=255, null=True)),
('meeting_url', models.URLField(blank=True, help_text='Meeting URL', null=True)),
('dial_in_number', models.CharField(blank=True, help_text='Dial-in phone number', max_length=50, null=True)),
('access_code', models.CharField(blank=True, help_text='Access code for dial-in', max_length=20, null=True)),
('scheduled_start', models.DateTimeField(help_text='Scheduled start time')),
('scheduled_end', models.DateTimeField(help_text='Scheduled end time')),
('actual_start', models.DateTimeField(blank=True, null=True)),
('actual_end', models.DateTimeField(blank=True, null=True)),
('max_participants', models.PositiveIntegerField(default=50)),
('recording_enabled', models.BooleanField(default=False)),
('recording_url', models.URLField(blank=True, help_text='URL to recorded meeting', null=True)),
('transcription_enabled', models.BooleanField(default=False)),
('transcription_url', models.URLField(blank=True, help_text='URL to meeting transcription', null=True)),
('integration_config', models.JSONField(default=dict, help_text='Configuration for external bridge integration')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('active_participants', models.ManyToManyField(blank=True, help_text='Users currently in the conference', related_name='active_conferences', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_conferences', to=settings.AUTH_USER_MODEL)),
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conference_bridges', to='incident_intelligence.incident')),
('invited_participants', models.ManyToManyField(blank=True, help_text='Users invited to the conference', related_name='invited_conferences', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-scheduled_start'],
},
),
migrations.CreateModel(
name='IncidentCommandRole',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('role_type', models.CharField(choices=[('INCIDENT_COMMANDER', 'Incident Commander'), ('SCRIBE', 'Scribe'), ('COMMS_LEAD', 'Communications Lead'), ('TECHNICAL_LEAD', 'Technical Lead'), ('BUSINESS_LEAD', 'Business Lead'), ('EXTERNAL_LIAISON', 'External Liaison'), ('OBSERVER', 'Observer')], max_length=30)),
('status', models.CharField(choices=[('ACTIVE', 'Active'), ('INACTIVE', 'Inactive'), ('REASSIGNED', 'Reassigned')], default='ACTIVE', max_length=20)),
('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')),
('assigned_at', models.DateTimeField(auto_now_add=True)),
('reassigned_at', models.DateTimeField(blank=True, null=True)),
('assignment_notes', models.TextField(blank=True, null=True)),
('decisions_made', models.PositiveIntegerField(default=0)),
('communications_sent', models.PositiveIntegerField(default=0)),
('last_activity', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('assigned_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_command_roles', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_command_roles', to=settings.AUTH_USER_MODEL)),
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='command_roles', to='incident_intelligence.incident')),
('reassigned_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reassigned_command_roles', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-assigned_at'],
},
),
migrations.CreateModel(
name='WarRoom',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=200)),
('description', models.TextField(blank=True, null=True)),
('status', models.CharField(choices=[('ACTIVE', 'Active'), ('ARCHIVED', 'Archived'), ('CLOSED', 'Closed')], default='ACTIVE', max_length=20)),
('privacy_level', models.CharField(choices=[('PUBLIC', 'Public'), ('PRIVATE', 'Private'), ('RESTRICTED', 'Restricted')], default='PRIVATE', max_length=20)),
('slack_channel_id', models.CharField(blank=True, help_text='Slack channel ID', max_length=100, null=True)),
('teams_channel_id', models.CharField(blank=True, help_text='Teams channel ID', max_length=100, null=True)),
('discord_channel_id', models.CharField(blank=True, help_text='Discord channel ID', max_length=100, null=True)),
('message_count', models.PositiveIntegerField(default=0)),
('last_activity', models.DateTimeField(blank=True, null=True)),
('active_participants', models.PositiveIntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('archived_at', models.DateTimeField(blank=True, null=True)),
('allowed_users', models.ManyToManyField(blank=True, help_text='Users with access to this war room', related_name='accessible_war_rooms', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_war_rooms', to=settings.AUTH_USER_MODEL)),
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='war_rooms', to='incident_intelligence.incident')),
('required_clearance_level', models.ForeignKey(blank=True, help_text='Required clearance level for access', null=True, on_delete=django.db.models.deletion.SET_NULL, to='security.dataclassification')),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='TimelineEvent',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('event_type', models.CharField(choices=[('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')], max_length=30)),
('title', models.CharField(max_length=200)),
('description', models.TextField()),
('source_type', models.CharField(choices=[('SYSTEM', 'System Generated'), ('USER', 'User Created'), ('INTEGRATION', 'External Integration'), ('AUTOMATION', 'Automation')], default='SYSTEM', max_length=20)),
('event_time', models.DateTimeField(help_text='When the event occurred')),
('created_at', models.DateTimeField(auto_now_add=True)),
('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')),
('is_critical_event', models.BooleanField(default=False, help_text='Whether this event is critical for postmortem analysis')),
('postmortem_notes', models.TextField(blank=True, help_text='Additional notes added during postmortem', null=True)),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_timeline_events', to=settings.AUTH_USER_MODEL)),
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='timeline_events', to='incident_intelligence.incident')),
('related_auto_remediation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='automation_orchestration.autoremediationexecution')),
('related_command_role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='collaboration_war_rooms.incidentcommandrole')),
('related_conference', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='collaboration_war_rooms.conferencebridge')),
('related_escalation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='sla_oncall.escalationinstance')),
('related_runbook_execution', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='automation_orchestration.runbookexecution')),
('related_sla_instance', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='sla_oncall.slainstance')),
('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to=settings.AUTH_USER_MODEL)),
('related_war_room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='timeline_events', to='collaboration_war_rooms.warroom')),
],
options={
'ordering': ['event_time', 'created_at'],
},
),
migrations.AddField(
model_name='incidentcommandrole',
name='war_room',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='command_roles', to='collaboration_war_rooms.warroom'),
),
migrations.AddField(
model_name='conferencebridge',
name='war_room',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='conference_bridges', to='collaboration_war_rooms.warroom'),
),
migrations.CreateModel(
name='WarRoomMessage',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('message_type', models.CharField(choices=[('TEXT', 'Text Message'), ('SYSTEM', 'System Message'), ('COMMAND', 'Command Message'), ('ALERT', 'Alert Message'), ('UPDATE', 'Status Update')], default='TEXT', max_length=20)),
('content', models.TextField()),
('sender_name', models.CharField(help_text='Display name of sender', max_length=100)),
('is_edited', models.BooleanField(default=False)),
('edited_at', models.DateTimeField(blank=True, null=True)),
('external_message_id', models.CharField(blank=True, help_text='ID in external system (Slack, Teams, etc.)', max_length=255, null=True)),
('external_data', models.JSONField(default=dict, help_text='Additional data from external system')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('reply_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='replies', to='collaboration_war_rooms.warroommessage')),
('sender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='war_room_messages', to=settings.AUTH_USER_MODEL)),
('war_room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='collaboration_war_rooms.warroom')),
],
options={
'ordering': ['created_at'],
},
),
migrations.CreateModel(
name='IncidentDecision',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('decision_type', models.CharField(choices=[('TECHNICAL', 'Technical Decision'), ('BUSINESS', 'Business Decision'), ('COMMUNICATION', 'Communication Decision'), ('ESCALATION', 'Escalation Decision'), ('RESOURCE', 'Resource Allocation'), ('TIMELINE', 'Timeline Decision')], max_length=20)),
('title', models.CharField(max_length=200)),
('description', models.TextField()),
('rationale', models.TextField(help_text='Reasoning behind the decision')),
('status', models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('IMPLEMENTED', 'Implemented')], default='PENDING', max_length=20)),
('requires_approval', models.BooleanField(default=False)),
('approved_at', models.DateTimeField(blank=True, null=True)),
('implementation_notes', models.TextField(blank=True, null=True)),
('implemented_at', models.DateTimeField(blank=True, null=True)),
('impact_assessment', models.TextField(blank=True, null=True)),
('success_metrics', models.JSONField(default=list, help_text='Metrics to measure decision success')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_decisions', to=settings.AUTH_USER_MODEL)),
('command_role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='decisions', to='collaboration_war_rooms.incidentcommandrole')),
('implemented_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='implemented_decisions', to=settings.AUTH_USER_MODEL)),
('incident', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='decisions', to='incident_intelligence.incident')),
],
options={
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['incident', 'status'], name='collaborati_inciden_4f34eb_idx'), models.Index(fields=['command_role', 'decision_type'], name='collaborati_command_81be71_idx'), models.Index(fields=['status', 'created_at'], name='collaborati_status_3f5734_idx')],
},
),
migrations.AddIndex(
model_name='warroom',
index=models.Index(fields=['incident', 'status'], name='collaborati_inciden_bd58db_idx'),
),
migrations.AddIndex(
model_name='warroom',
index=models.Index(fields=['status', 'privacy_level'], name='collaborati_status_649ccc_idx'),
),
migrations.AddIndex(
model_name='warroom',
index=models.Index(fields=['created_at'], name='collaborati_created_e3a240_idx'),
),
migrations.AddIndex(
model_name='timelineevent',
index=models.Index(fields=['incident', 'event_time'], name='collaborati_inciden_3a611f_idx'),
),
migrations.AddIndex(
model_name='timelineevent',
index=models.Index(fields=['event_type', 'event_time'], name='collaborati_event_t_d2100a_idx'),
),
migrations.AddIndex(
model_name='timelineevent',
index=models.Index(fields=['source_type', 'event_time'], name='collaborati_source__0c3cc4_idx'),
),
migrations.AddIndex(
model_name='timelineevent',
index=models.Index(fields=['is_critical_event', 'event_time'], name='collaborati_is_crit_28e610_idx'),
),
migrations.AddIndex(
model_name='incidentcommandrole',
index=models.Index(fields=['incident', 'role_type'], name='collaborati_inciden_7c5ba6_idx'),
),
migrations.AddIndex(
model_name='incidentcommandrole',
index=models.Index(fields=['assigned_user', 'status'], name='collaborati_assigne_e33d48_idx'),
),
migrations.AddIndex(
model_name='incidentcommandrole',
index=models.Index(fields=['status', 'assigned_at'], name='collaborati_status_b2ec4b_idx'),
),
migrations.AlterUniqueTogether(
name='incidentcommandrole',
unique_together={('incident', 'role_type', 'assigned_user')},
),
migrations.AddIndex(
model_name='conferencebridge',
index=models.Index(fields=['incident', 'status'], name='collaborati_inciden_4be2c2_idx'),
),
migrations.AddIndex(
model_name='conferencebridge',
index=models.Index(fields=['bridge_type', 'status'], name='collaborati_bridge__44a9ea_idx'),
),
migrations.AddIndex(
model_name='conferencebridge',
index=models.Index(fields=['scheduled_start'], name='collaborati_schedul_a93d14_idx'),
),
migrations.AddIndex(
model_name='warroommessage',
index=models.Index(fields=['war_room', 'created_at'], name='collaborati_war_roo_6320f9_idx'),
),
migrations.AddIndex(
model_name='warroommessage',
index=models.Index(fields=['sender', 'created_at'], name='collaborati_sender__f499b1_idx'),
),
migrations.AddIndex(
model_name='warroommessage',
index=models.Index(fields=['message_type', 'created_at'], name='collaborati_message_a29f3d_idx'),
),
]

View File

@@ -0,0 +1,215 @@
# Generated by Django 5.2.6 on 2025-09-18 18:10
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('automation_orchestration', '0002_autoremediationexecution_sla_instance_and_more'),
('collaboration_war_rooms', '0001_initial'),
('knowledge_learning', '0001_initial'),
('security', '0003_adaptiveauthentication_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ChatBot',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('bot_type', models.CharField(choices=[('INCIDENT_ASSISTANT', 'Incident Assistant'), ('KNOWLEDGE_BOT', 'Knowledge Bot'), ('AUTOMATION_BOT', 'Automation Bot'), ('COMPLIANCE_BOT', 'Compliance Bot')], max_length=30)),
('description', models.TextField()),
('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')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['name'],
},
),
migrations.CreateModel(
name='ChatCommand',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('command_type', models.CharField(choices=[('STATUS', 'Status Check'), ('RUNBOOK', 'Execute Runbook'), ('ESCALATE', 'Escalate Incident'), ('ASSIGN', 'Assign Incident'), ('UPDATE', 'Update Status'), ('CUSTOM', 'Custom Command')], max_length=20)),
('command_text', models.CharField(help_text='Full command text', max_length=500)),
('parameters', models.JSONField(default=dict, help_text='Parsed command parameters')),
('executed_at', models.DateTimeField(blank=True, null=True)),
('execution_status', models.CharField(choices=[('PENDING', 'Pending'), ('EXECUTING', 'Executing'), ('SUCCESS', 'Success'), ('FAILED', 'Failed'), ('CANCELLED', 'Cancelled')], default='PENDING', max_length=20)),
('execution_result', models.JSONField(default=dict, help_text='Result of command execution')),
('error_message', models.TextField(blank=True, null=True)),
],
options={
'ordering': ['-executed_at'],
},
),
migrations.CreateModel(
name='ChatFile',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('filename', models.CharField(max_length=255)),
('original_filename', models.CharField(max_length=255)),
('file_type', models.CharField(choices=[('IMAGE', 'Image'), ('DOCUMENT', 'Document'), ('LOG', 'Log File'), ('SCREENSHOT', 'Screenshot'), ('EVIDENCE', 'Evidence'), ('OTHER', 'Other')], max_length=20)),
('file_size', models.PositiveIntegerField(help_text='File size in bytes')),
('mime_type', models.CharField(max_length=100)),
('file_path', models.CharField(help_text='Path to stored file', max_length=500)),
('file_url', models.URLField(blank=True, help_text='Public URL for file access', null=True)),
('is_encrypted', models.BooleanField(default=False)),
('encryption_key_id', models.CharField(blank=True, max_length=255, null=True)),
('file_hash', models.CharField(help_text='SHA-256 hash of file', max_length=64)),
('uploaded_at', models.DateTimeField(auto_now_add=True)),
('access_log', models.JSONField(default=list, help_text='Log of who accessed this file and when')),
],
options={
'ordering': ['-uploaded_at'],
},
),
migrations.CreateModel(
name='MessageReaction',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('emoji', models.CharField(help_text='Emoji reaction', max_length=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['created_at'],
},
),
migrations.AddField(
model_name='warroommessage',
name='attachments',
field=models.JSONField(default=list, help_text='List of file attachments with metadata'),
),
migrations.AddField(
model_name='warroommessage',
name='encryption_key_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='warroommessage',
name='is_encrypted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='warroommessage',
name='is_pinned',
field=models.BooleanField(default=False, help_text='Whether this message is pinned'),
),
migrations.AddField(
model_name='warroommessage',
name='mentioned_users',
field=models.ManyToManyField(blank=True, help_text='Users mentioned in this message', related_name='mentioned_in_messages', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='warroommessage',
name='notification_sent',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='warroommessage',
name='pinned_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='warroommessage',
name='pinned_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pinned_messages', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='warroommessage',
name='message_type',
field=models.CharField(choices=[('TEXT', 'Text Message'), ('SYSTEM', 'System Message'), ('COMMAND', 'Command Message'), ('ALERT', 'Alert Message'), ('UPDATE', 'Status Update'), ('FILE', 'File Attachment'), ('BOT', 'Bot Message')], default='TEXT', max_length=20),
),
migrations.AddIndex(
model_name='warroommessage',
index=models.Index(fields=['is_pinned', 'created_at'], name='collaborati_is_pinn_7a25dc_idx'),
),
migrations.AddField(
model_name='chatbot',
name='knowledge_base',
field=models.ForeignKey(blank=True, help_text='Knowledge base article for bot responses', null=True, on_delete=django.db.models.deletion.SET_NULL, to='knowledge_learning.knowledgebasearticle'),
),
migrations.AddField(
model_name='chatcommand',
name='automation_execution',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='chat_commands', to='automation_orchestration.runbookexecution'),
),
migrations.AddField(
model_name='chatcommand',
name='executed_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='chatcommand',
name='message',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_commands', to='collaboration_war_rooms.warroommessage'),
),
migrations.AddField(
model_name='chatfile',
name='data_classification',
field=models.ForeignKey(blank=True, help_text='Data classification level for this file', null=True, on_delete=django.db.models.deletion.SET_NULL, to='security.dataclassification'),
),
migrations.AddField(
model_name='chatfile',
name='message',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_files', to='collaboration_war_rooms.warroommessage'),
),
migrations.AddField(
model_name='chatfile',
name='uploaded_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='messagereaction',
name='message',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reactions', to='collaboration_war_rooms.warroommessage'),
),
migrations.AddField(
model_name='messagereaction',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddIndex(
model_name='chatbot',
index=models.Index(fields=['bot_type', 'is_active'], name='collaborati_bot_typ_6fc3ba_idx'),
),
migrations.AddIndex(
model_name='chatcommand',
index=models.Index(fields=['command_type', 'execution_status'], name='collaborati_command_915116_idx'),
),
migrations.AddIndex(
model_name='chatcommand',
index=models.Index(fields=['executed_by', 'executed_at'], name='collaborati_execute_d1badb_idx'),
),
migrations.AddIndex(
model_name='chatfile',
index=models.Index(fields=['message', 'file_type'], name='collaborati_message_358b62_idx'),
),
migrations.AddIndex(
model_name='chatfile',
index=models.Index(fields=['data_classification'], name='collaborati_data_cl_e26657_idx'),
),
migrations.AddIndex(
model_name='chatfile',
index=models.Index(fields=['uploaded_at'], name='collaborati_uploade_a6b9bd_idx'),
),
migrations.AddIndex(
model_name='messagereaction',
index=models.Index(fields=['message', 'emoji'], name='collaborati_message_817163_idx'),
),
migrations.AddIndex(
model_name='messagereaction',
index=models.Index(fields=['user', 'created_at'], name='collaborati_user_id_2d3a22_idx'),
),
migrations.AlterUniqueTogether(
name='messagereaction',
unique_together={('message', 'user', 'emoji')},
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
"""
WebSocket routing configuration for collaboration_war_rooms
"""
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_id>[0-9a-f-]+)/$', consumers.ChatConsumer.as_asgi()),
]

View File

@@ -0,0 +1 @@
# Serializers for Collaboration & War Rooms module

View File

@@ -0,0 +1,354 @@
"""
Serializers for Collaboration & War Rooms models
"""
from rest_framework import serializers
from django.contrib.auth import get_user_model
from ..models import (
WarRoom, ConferenceBridge, IncidentCommandRole,
TimelineEvent, WarRoomMessage, IncidentDecision,
MessageReaction, ChatFile, ChatCommand, ChatBot
)
from incident_intelligence.serializers.incident import IncidentSerializer
from security.serializers.security import UserSerializer
User = get_user_model()
class WarRoomSerializer(serializers.ModelSerializer):
"""Serializer for WarRoom model"""
incident = IncidentSerializer(read_only=True)
incident_id = serializers.UUIDField(write_only=True)
allowed_users = UserSerializer(many=True, read_only=True)
allowed_user_ids = serializers.ListField(
child=serializers.UUIDField(),
write_only=True,
required=False
)
created_by = UserSerializer(read_only=True)
created_by_id = serializers.UUIDField(write_only=True, required=False)
class Meta:
model = WarRoom
fields = [
'id', 'name', 'description', 'incident', 'incident_id',
'status', 'privacy_level', 'slack_channel_id', 'teams_channel_id',
'discord_channel_id', 'allowed_users', 'allowed_user_ids',
'required_clearance_level', 'message_count', 'last_activity',
'active_participants', 'created_by', 'created_by_id',
'created_at', 'updated_at', 'archived_at'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'message_count', 'last_activity', 'active_participants']
def create(self, validated_data):
"""Create war room with allowed users"""
allowed_user_ids = validated_data.pop('allowed_user_ids', [])
war_room = WarRoom.objects.create(**validated_data)
# Add allowed users
if allowed_user_ids:
users = User.objects.filter(id__in=allowed_user_ids)
war_room.allowed_users.set(users)
return war_room
def update(self, instance, validated_data):
"""Update war room with allowed users"""
allowed_user_ids = validated_data.pop('allowed_user_ids', None)
# Update war room fields
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
# Update allowed users if provided
if allowed_user_ids is not None:
users = User.objects.filter(id__in=allowed_user_ids)
instance.allowed_users.set(users)
return instance
class ConferenceBridgeSerializer(serializers.ModelSerializer):
"""Serializer for ConferenceBridge model"""
incident = IncidentSerializer(read_only=True)
incident_id = serializers.UUIDField(write_only=True)
war_room = WarRoomSerializer(read_only=True)
war_room_id = serializers.UUIDField(write_only=True, required=False)
invited_participants = UserSerializer(many=True, read_only=True)
invited_participant_ids = serializers.ListField(
child=serializers.UUIDField(),
write_only=True,
required=False
)
active_participants = UserSerializer(many=True, read_only=True)
created_by = UserSerializer(read_only=True)
created_by_id = serializers.UUIDField(write_only=True, required=False)
class Meta:
model = ConferenceBridge
fields = [
'id', 'name', 'description', 'incident', 'incident_id',
'war_room', 'war_room_id', 'bridge_type', 'status',
'meeting_id', 'meeting_url', 'dial_in_number', 'access_code',
'scheduled_start', 'scheduled_end', 'actual_start', 'actual_end',
'invited_participants', 'invited_participant_ids',
'active_participants', 'max_participants',
'recording_enabled', 'recording_url', 'transcription_enabled',
'transcription_url', 'integration_config',
'created_by', 'created_by_id', 'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at', 'actual_start', 'actual_end', 'active_participants']
def create(self, validated_data):
"""Create conference bridge with invited participants"""
invited_participant_ids = validated_data.pop('invited_participant_ids', [])
conference = ConferenceBridge.objects.create(**validated_data)
# Add invited participants
if invited_participant_ids:
users = User.objects.filter(id__in=invited_participant_ids)
conference.invited_participants.set(users)
return conference
def update(self, instance, validated_data):
"""Update conference bridge with invited participants"""
invited_participant_ids = validated_data.pop('invited_participant_ids', None)
# Update conference fields
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
# Update invited participants if provided
if invited_participant_ids is not None:
users = User.objects.filter(id__in=invited_participant_ids)
instance.invited_participants.set(users)
return instance
class IncidentCommandRoleSerializer(serializers.ModelSerializer):
"""Serializer for IncidentCommandRole model"""
incident = IncidentSerializer(read_only=True)
incident_id = serializers.UUIDField(write_only=True)
war_room = WarRoomSerializer(read_only=True)
war_room_id = serializers.UUIDField(write_only=True, required=False)
assigned_user = UserSerializer(read_only=True)
assigned_user_id = serializers.UUIDField(write_only=True, required=False)
reassigned_by = UserSerializer(read_only=True)
created_by = UserSerializer(read_only=True)
created_by_id = serializers.UUIDField(write_only=True, required=False)
class Meta:
model = IncidentCommandRole
fields = [
'id', 'incident', 'incident_id', 'war_room', 'war_room_id',
'role_type', 'assigned_user', 'assigned_user_id', 'status',
'responsibilities', 'decision_authority', 'assigned_at',
'reassigned_at', 'reassigned_by', 'assignment_notes',
'decisions_made', 'communications_sent', 'last_activity',
'created_by', 'created_by_id', 'created_at', 'updated_at'
]
read_only_fields = [
'id', 'assigned_at', 'reassigned_at', 'reassigned_by',
'decisions_made', 'communications_sent', 'last_activity',
'created_at', 'updated_at'
]
class TimelineEventSerializer(serializers.ModelSerializer):
"""Serializer for TimelineEvent model"""
incident = IncidentSerializer(read_only=True)
incident_id = serializers.UUIDField(write_only=True)
related_user = UserSerializer(read_only=True)
related_user_id = serializers.UUIDField(write_only=True, required=False)
created_by = UserSerializer(read_only=True)
created_by_id = serializers.UUIDField(write_only=True, required=False)
class Meta:
model = TimelineEvent
fields = [
'id', 'incident', 'incident_id', 'event_type', 'title',
'description', 'source_type', 'event_time', 'created_at',
'related_user', 'related_user_id', 'event_data', 'tags',
'is_critical_event', 'postmortem_notes',
'created_by', 'created_by_id'
]
read_only_fields = ['id', 'created_at']
class MessageReactionSerializer(serializers.ModelSerializer):
"""Serializer for MessageReaction model"""
user = UserSerializer(read_only=True)
class Meta:
model = MessageReaction
fields = ['id', 'user', 'emoji', 'created_at']
read_only_fields = ['id', 'created_at']
class ChatFileSerializer(serializers.ModelSerializer):
"""Serializer for ChatFile model"""
uploaded_by = UserSerializer(read_only=True)
class Meta:
model = ChatFile
fields = [
'id', 'message', 'filename', 'original_filename', 'file_type',
'file_size', 'mime_type', 'file_path', 'file_url',
'data_classification', 'is_encrypted', 'file_hash',
'uploaded_by', 'uploaded_at', 'access_log'
]
read_only_fields = ['id', 'uploaded_at', 'access_log']
class ChatCommandSerializer(serializers.ModelSerializer):
"""Serializer for ChatCommand model"""
executed_by = UserSerializer(read_only=True)
class Meta:
model = ChatCommand
fields = [
'id', 'message', 'command_type', 'command_text', 'parameters',
'executed_by', 'executed_at', 'execution_status',
'execution_result', 'error_message', 'automation_execution'
]
read_only_fields = ['id', 'executed_at']
class ChatBotSerializer(serializers.ModelSerializer):
"""Serializer for ChatBot model"""
class Meta:
model = ChatBot
fields = [
'id', 'name', 'bot_type', 'description', 'is_active',
'auto_respond', 'response_triggers', 'knowledge_base',
'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']
class WarRoomMessageSerializer(serializers.ModelSerializer):
"""Serializer for WarRoomMessage model"""
war_room = WarRoomSerializer(read_only=True)
war_room_id = serializers.UUIDField(write_only=True)
sender = UserSerializer(read_only=True)
sender_id = serializers.UUIDField(write_only=True, required=False)
reply_to = serializers.PrimaryKeyRelatedField(
queryset=WarRoomMessage.objects.all(),
required=False
)
pinned_by = UserSerializer(read_only=True)
mentioned_users = UserSerializer(many=True, read_only=True)
mentioned_user_ids = serializers.ListField(
child=serializers.UUIDField(),
write_only=True,
required=False
)
reactions = MessageReactionSerializer(many=True, read_only=True)
chat_files = ChatFileSerializer(many=True, read_only=True)
chat_commands = ChatCommandSerializer(many=True, read_only=True)
reactions_summary = serializers.SerializerMethodField()
class Meta:
model = WarRoomMessage
fields = [
'id', 'war_room', 'war_room_id', 'message_type', 'content',
'sender', 'sender_id', 'sender_name', 'is_edited', 'edited_at',
'is_pinned', 'pinned_at', 'pinned_by', 'reply_to',
'attachments', 'mentioned_users', 'mentioned_user_ids',
'notification_sent', 'external_message_id', 'external_data',
'is_encrypted', 'encryption_key_id', 'reactions', 'reactions_summary',
'chat_files', 'chat_commands', 'created_at', 'updated_at'
]
read_only_fields = [
'id', 'is_edited', 'edited_at', 'pinned_at', 'pinned_by',
'notification_sent', 'created_at', 'updated_at'
]
def get_reactions_summary(self, obj):
"""Get reactions summary for the message"""
return obj.get_reactions_summary()
def create(self, validated_data):
"""Create message with mentioned users"""
mentioned_user_ids = validated_data.pop('mentioned_user_ids', [])
message = WarRoomMessage.objects.create(**validated_data)
# Add mentioned users
if mentioned_user_ids:
users = User.objects.filter(id__in=mentioned_user_ids)
message.mentioned_users.set(users)
return message
class IncidentDecisionSerializer(serializers.ModelSerializer):
"""Serializer for IncidentDecision model"""
incident = IncidentSerializer(read_only=True)
incident_id = serializers.UUIDField(write_only=True)
command_role = IncidentCommandRoleSerializer(read_only=True)
command_role_id = serializers.UUIDField(write_only=True)
approved_by = UserSerializer(read_only=True)
implemented_by = UserSerializer(read_only=True)
class Meta:
model = IncidentDecision
fields = [
'id', 'incident', 'incident_id', 'command_role', 'command_role_id',
'decision_type', 'title', 'description', 'rationale', 'status',
'requires_approval', 'approved_by', 'approved_at',
'implementation_notes', 'implemented_at', 'implemented_by',
'impact_assessment', 'success_metrics', 'created_at', 'updated_at'
]
read_only_fields = [
'id', 'approved_by', 'approved_at', 'implemented_at',
'implemented_by', 'created_at', 'updated_at'
]
class WarRoomSummarySerializer(serializers.ModelSerializer):
"""Simplified serializer for WarRoom list views"""
incident_title = serializers.CharField(source='incident.title', read_only=True)
incident_severity = serializers.CharField(source='incident.severity', read_only=True)
participant_count = serializers.SerializerMethodField()
class Meta:
model = WarRoom
fields = [
'id', 'name', 'incident_title', 'incident_severity',
'status', 'privacy_level', 'message_count', 'last_activity',
'participant_count', 'created_at'
]
def get_participant_count(self, obj):
"""Get count of allowed users"""
return obj.allowed_users.count()
class TimelineEventSummarySerializer(serializers.ModelSerializer):
"""Simplified serializer for TimelineEvent list views"""
incident_title = serializers.CharField(source='incident.title', read_only=True)
related_user_name = serializers.CharField(source='related_user.username', read_only=True)
class Meta:
model = TimelineEvent
fields = [
'id', 'incident_title', 'event_type', 'title', 'description',
'source_type', 'event_time', 'related_user_name',
'is_critical_event', 'created_at'
]

View File

@@ -0,0 +1,433 @@
"""
AI Assistant Service for Chat Integration
Handles AI-powered assistance, incident suggestions, and knowledge base integration
"""
from django.utils import timezone
from django.contrib.auth import get_user_model
from typing import Dict, Any, Optional, List
import re
from ..models import ChatBot, WarRoomMessage
from knowledge_learning.models import KnowledgeBaseArticle, Postmortem, IncidentPattern
from incident_intelligence.models import Incident
User = get_user_model()
class AIAssistantService:
"""Service for AI-powered chat assistance"""
@staticmethod
def generate_response(bot: ChatBot, message: WarRoomMessage, context: Dict[str, Any] = None) -> Dict[str, Any]:
"""Generate AI response to a chat message"""
try:
incident = message.war_room.incident
user = message.sender
# Analyze message content
message_analysis = AIAssistantService._analyze_message(message.content)
# Determine response type based on analysis
if message_analysis['contains_question']:
response = AIAssistantService._generate_question_response(
bot, message, message_analysis, context
)
elif message_analysis['contains_incident_keywords']:
response = AIAssistantService._generate_incident_response(
bot, message, message_analysis, context
)
elif message_analysis['contains_help_request']:
response = AIAssistantService._generate_help_response(
bot, message, message_analysis, context
)
else:
response = AIAssistantService._generate_general_response(
bot, message, message_analysis, context
)
# Add confidence score and sources
response['confidence'] = AIAssistantService._calculate_confidence(response)
response['sources'] = AIAssistantService._get_response_sources(response)
return response
except Exception as e:
return {
'response': f"I encountered an error while processing your message: {str(e)}",
'confidence': 0.0,
'sources': []
}
@staticmethod
def suggest_similar_incidents(incident: Incident, limit: int = 5) -> List[Dict[str, Any]]:
"""Suggest similar past incidents based on current incident"""
try:
# Find similar incidents based on category and keywords
similar_incidents = []
# Search by category
category_incidents = Incident.objects.filter(
category=incident.category,
status__in=['RESOLVED', 'CLOSED']
).exclude(id=incident.id)[:limit]
for similar_incident in category_incidents:
similarity_score = AIAssistantService._calculate_incident_similarity(
incident, similar_incident
)
if similarity_score > 0.3: # Minimum similarity threshold
similar_incidents.append({
'id': str(similar_incident.id),
'title': similar_incident.title,
'severity': similar_incident.severity,
'status': similar_incident.status,
'created_at': similar_incident.created_at.isoformat(),
'resolved_at': similar_incident.resolved_at.isoformat() if similar_incident.resolved_at else None,
'resolution_time': str(similar_incident.resolution_time) if similar_incident.resolution_time else None,
'similarity_score': similarity_score,
'has_postmortem': similar_incident.postmortems.exists()
})
# Sort by similarity score
similar_incidents.sort(key=lambda x: x['similarity_score'], reverse=True)
return similar_incidents[:limit]
except Exception as e:
return []
@staticmethod
def suggest_knowledge_articles(incident: Incident, message_content: str = None) -> List[Dict[str, Any]]:
"""Suggest relevant knowledge base articles"""
try:
# Extract keywords from incident and message
keywords = AIAssistantService._extract_keywords(incident, message_content)
# Search knowledge base articles
articles = KnowledgeBaseArticle.objects.filter(
is_active=True,
tags__overlap=keywords
).distinct()[:10]
suggested_articles = []
for article in articles:
relevance_score = AIAssistantService._calculate_article_relevance(
article, incident, keywords
)
if relevance_score > 0.2: # Minimum relevance threshold
suggested_articles.append({
'id': str(article.id),
'title': article.title,
'summary': article.summary,
'category': article.category,
'tags': article.tags,
'relevance_score': relevance_score,
'url': f"/knowledge/articles/{article.id}/"
})
# Sort by relevance score
suggested_articles.sort(key=lambda x: x['relevance_score'], reverse=True)
return suggested_articles[:5]
except Exception as e:
return []
@staticmethod
def suggest_runbooks(incident: Incident) -> List[Dict[str, Any]]:
"""Suggest relevant runbooks for the incident"""
try:
from automation_orchestration.models import Runbook
# Find runbooks that match incident characteristics
runbooks = Runbook.objects.filter(
is_active=True,
categories__contains=[incident.category]
)
suggested_runbooks = []
for runbook in runbooks:
if incident.severity in runbook.severity_levels:
suggested_runbooks.append({
'id': str(runbook.id),
'name': runbook.name,
'description': runbook.description,
'category': runbook.category,
'estimated_duration': runbook.estimated_duration,
'success_rate': runbook.success_rate,
'last_used': runbook.last_used.isoformat() if runbook.last_used else None
})
return suggested_runbooks[:5]
except Exception as e:
return []
@staticmethod
def detect_incident_patterns(incident: Incident) -> List[Dict[str, Any]]:
"""Detect patterns in the incident"""
try:
# Find matching patterns
patterns = IncidentPattern.objects.filter(
is_active=True,
incidents=incident
)
detected_patterns = []
for pattern in patterns:
detected_patterns.append({
'id': str(pattern.id),
'name': pattern.name,
'pattern_type': pattern.pattern_type,
'description': pattern.description,
'confidence_score': pattern.confidence_score,
'frequency': pattern.frequency,
'last_occurrence': pattern.last_occurrence.isoformat() if pattern.last_occurrence else None,
'next_predicted_occurrence': pattern.next_predicted_occurrence.isoformat() if pattern.next_predicted_occurrence else None
})
return detected_patterns
except Exception as e:
return []
@staticmethod
def _analyze_message(content: str) -> Dict[str, Any]:
"""Analyze message content to determine intent"""
content_lower = content.lower()
# Check for questions
question_patterns = [
r'\?', r'how\s+', r'what\s+', r'when\s+', r'where\s+', r'why\s+', r'who\s+',
r'can\s+you\s+', r'could\s+you\s+', r'would\s+you\s+'
]
contains_question = any(re.search(pattern, content_lower) for pattern in question_patterns)
# Check for incident-related keywords
incident_keywords = [
'incident', 'issue', 'problem', 'outage', 'error', 'failure', 'down',
'severity', 'priority', 'escalate', 'resolve', 'fix'
]
contains_incident_keywords = any(keyword in content_lower for keyword in incident_keywords)
# Check for help requests
help_patterns = [
r'help\s+', r'assist\s+', r'support\s+', r'guidance\s+', r'advice\s+'
]
contains_help_request = any(re.search(pattern, content_lower) for pattern in help_patterns)
return {
'contains_question': contains_question,
'contains_incident_keywords': contains_incident_keywords,
'contains_help_request': contains_help_request,
'word_count': len(content.split()),
'sentiment': AIAssistantService._analyze_sentiment(content)
}
@staticmethod
def _generate_question_response(bot: ChatBot, message: WarRoomMessage, analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""Generate response to a question"""
incident = message.war_room.incident
# Get relevant knowledge articles
articles = AIAssistantService.suggest_knowledge_articles(incident, message.content)
if articles:
response_text = f"Based on your question, here are some relevant resources:\n\n"
for article in articles[:3]:
response_text += f"• **{article['title']}** - {article['summary'][:100]}...\n"
response_text += f"\nYou can find more information in our knowledge base."
else:
response_text = "I'd be happy to help with your question. Let me search our knowledge base for relevant information."
return {
'response': response_text,
'response_type': 'question_answer',
'suggested_articles': articles[:3]
}
@staticmethod
def _generate_incident_response(bot: ChatBot, message: WarRoomMessage, analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""Generate response related to incident management"""
incident = message.war_room.incident
# Suggest similar incidents
similar_incidents = AIAssistantService.suggest_similar_incidents(incident)
# Suggest runbooks
runbooks = AIAssistantService.suggest_runbooks(incident)
response_text = f"I can help with incident management. Here's what I found:\n\n"
if similar_incidents:
response_text += f"**Similar Past Incidents:**\n"
for incident_data in similar_incidents[:2]:
response_text += f"{incident_data['title']} (Similarity: {incident_data['similarity_score']:.1%})\n"
response_text += "\n"
if runbooks:
response_text += f"**Suggested Runbooks:**\n"
for runbook in runbooks[:2]:
response_text += f"{runbook['name']} - {runbook['description'][:100]}...\n"
return {
'response': response_text,
'response_type': 'incident_assistance',
'similar_incidents': similar_incidents[:3],
'suggested_runbooks': runbooks[:3]
}
@staticmethod
def _generate_help_response(bot: ChatBot, message: WarRoomMessage, analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""Generate help response"""
response_text = (
"I'm here to help! I can assist you with:\n\n"
"• **Incident Management** - Find similar incidents, suggest runbooks\n"
"• **Knowledge Base** - Search for relevant articles and documentation\n"
"• **ChatOps Commands** - Execute automation commands like `/status`, `/run playbook <name>`\n"
"• **Pattern Detection** - Identify recurring issues and patterns\n\n"
"Just ask me a question or mention what you need help with!"
)
return {
'response': response_text,
'response_type': 'help'
}
@staticmethod
def _generate_general_response(bot: ChatBot, message: WarRoomMessage, analysis: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
"""Generate general response"""
incident = message.war_room.incident
# Check for patterns
patterns = AIAssistantService.detect_incident_patterns(incident)
if patterns:
response_text = f"I noticed this incident matches some known patterns:\n\n"
for pattern in patterns[:2]:
response_text += f"• **{pattern['name']}** - {pattern['description'][:100]}...\n"
else:
response_text = "I'm monitoring this incident. Let me know if you need any assistance with incident response or have questions about similar past incidents."
return {
'response': response_text,
'response_type': 'general',
'detected_patterns': patterns
}
@staticmethod
def _calculate_confidence(response: Dict[str, Any]) -> float:
"""Calculate confidence score for the response"""
base_confidence = 0.7
# Increase confidence based on available data
if response.get('suggested_articles'):
base_confidence += 0.1
if response.get('similar_incidents'):
base_confidence += 0.1
if response.get('suggested_runbooks'):
base_confidence += 0.1
return min(base_confidence, 1.0)
@staticmethod
def _get_response_sources(response: Dict[str, Any]) -> List[str]:
"""Get sources for the response"""
sources = []
if response.get('suggested_articles'):
sources.append('Knowledge Base')
if response.get('similar_incidents'):
sources.append('Historical Incidents')
if response.get('suggested_runbooks'):
sources.append('Runbook Library')
if response.get('detected_patterns'):
sources.append('Pattern Analysis')
return sources
@staticmethod
def _calculate_incident_similarity(incident1: Incident, incident2: Incident) -> float:
"""Calculate similarity score between two incidents"""
similarity = 0.0
# Category similarity
if incident1.category == incident2.category:
similarity += 0.3
# Severity similarity
if incident1.severity == incident2.severity:
similarity += 0.2
# Text similarity (simplified)
text1_words = set(incident1.title.lower().split())
text2_words = set(incident2.title.lower().split())
if text1_words and text2_words:
text_similarity = len(text1_words.intersection(text2_words)) / len(text1_words.union(text2_words))
similarity += text_similarity * 0.5
return similarity
@staticmethod
def _extract_keywords(incident: Incident, message_content: str = None) -> List[str]:
"""Extract keywords from incident and message"""
keywords = []
# Add incident keywords
if incident.category:
keywords.append(incident.category.lower())
if incident.title:
keywords.extend(incident.title.lower().split())
# Add message keywords
if message_content:
keywords.extend(message_content.lower().split())
# Remove common words
stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'}
keywords = [word for word in keywords if word not in stop_words and len(word) > 2]
return list(set(keywords)) # Remove duplicates
@staticmethod
def _calculate_article_relevance(article: KnowledgeBaseArticle, incident: Incident, keywords: List[str]) -> float:
"""Calculate relevance score for a knowledge article"""
relevance = 0.0
# Tag overlap
if article.tags:
tag_overlap = len(set(article.tags).intersection(set(keywords)))
relevance += tag_overlap * 0.1
# Category match
if article.category == incident.category:
relevance += 0.3
# Title keyword match
title_words = set(article.title.lower().split())
keyword_overlap = len(title_words.intersection(set(keywords)))
if title_words:
relevance += (keyword_overlap / len(title_words)) * 0.4
return min(relevance, 1.0)
@staticmethod
def _analyze_sentiment(content: str) -> str:
"""Simple sentiment analysis"""
positive_words = ['good', 'great', 'excellent', 'fixed', 'resolved', 'working', 'success']
negative_words = ['bad', 'terrible', 'broken', 'failed', 'error', 'issue', 'problem']
content_lower = content.lower()
positive_count = sum(1 for word in positive_words if word in content_lower)
negative_count = sum(1 for word in negative_words if word in content_lower)
if positive_count > negative_count:
return 'positive'
elif negative_count > positive_count:
return 'negative'
else:
return 'neutral'

View File

@@ -0,0 +1,366 @@
"""
Automation Commands Service for ChatOps Integration
Handles execution of automation commands via chat interface
"""
from django.utils import timezone
from django.contrib.auth import get_user_model
from typing import Dict, Any, Optional, List
from ..models import ChatCommand, WarRoomMessage
from automation_orchestration.models import Runbook, RunbookExecution, AutoRemediation, AutoRemediationExecution
from incident_intelligence.models import Incident
User = get_user_model()
class AutomationCommandService:
"""Service for handling automation commands in chat"""
@staticmethod
def execute_runbook_command(chat_command: ChatCommand, runbook_name: str, user: User) -> Dict[str, Any]:
"""Execute a runbook via chat command"""
try:
incident = chat_command.message.war_room.incident
# Find the runbook
runbook = Runbook.objects.filter(
name__icontains=runbook_name,
is_active=True
).first()
if not runbook:
return {
'error': f'Runbook "{runbook_name}" not found or inactive',
'suggestions': AutomationCommandService._get_runbook_suggestions(runbook_name)
}
# Check if runbook applies to this incident
if not AutomationCommandService._runbook_applies_to_incident(runbook, incident):
return {
'error': f'Runbook "{runbook.name}" does not apply to this incident type',
'incident_category': incident.category,
'incident_severity': incident.severity
}
# Execute the runbook
execution = RunbookExecution.objects.create(
runbook=runbook,
incident=incident,
triggered_by=user,
trigger_type='CHAT_COMMAND',
trigger_data={
'chat_command_id': str(chat_command.id),
'command_text': chat_command.command_text
}
)
# Update chat command with execution reference
chat_command.automation_execution = execution
chat_command.save()
# Create status message in chat
AutomationCommandService._create_execution_status_message(
chat_command.message.war_room,
f"🚀 **Runbook Execution Started**\n\n"
f"**Runbook:** {runbook.name}\n"
f"**Execution ID:** {execution.id}\n"
f"**Triggered by:** {user.username}\n"
f"**Status:** {execution.status}\n\n"
f"Monitor progress in the automation dashboard."
)
return {
'success': True,
'execution_id': str(execution.id),
'runbook_name': runbook.name,
'status': execution.status,
'message': f'Runbook "{runbook.name}" execution started successfully'
}
except Exception as e:
return {
'error': f'Failed to execute runbook: {str(e)}'
}
@staticmethod
def execute_auto_remediation_command(chat_command: ChatCommand, remediation_name: str, user: User) -> Dict[str, Any]:
"""Execute auto-remediation via chat command"""
try:
incident = chat_command.message.war_room.incident
# Find the auto-remediation
remediation = AutoRemediation.objects.filter(
name__icontains=remediation_name,
is_active=True
).first()
if not remediation:
return {
'error': f'Auto-remediation "{remediation_name}" not found or inactive',
'suggestions': AutomationCommandService._get_remediation_suggestions(remediation_name)
}
# Check if remediation applies to this incident
if not AutomationCommandService._remediation_applies_to_incident(remediation, incident):
return {
'error': f'Auto-remediation "{remediation.name}" does not apply to this incident type',
'incident_category': incident.category,
'incident_severity': incident.severity
}
# Execute the auto-remediation
execution = AutoRemediationExecution.objects.create(
auto_remediation=remediation,
incident=incident,
triggered_by=user,
trigger_type='CHAT_COMMAND',
trigger_data={
'chat_command_id': str(chat_command.id),
'command_text': chat_command.command_text
}
)
# Create status message in chat
AutomationCommandService._create_execution_status_message(
chat_command.message.war_room,
f"🔧 **Auto-Remediation Started**\n\n"
f"**Remediation:** {remediation.name}\n"
f"**Execution ID:** {execution.id}\n"
f"**Triggered by:** {user.username}\n"
f"**Status:** {execution.status}\n\n"
f"Monitor progress in the automation dashboard."
)
return {
'success': True,
'execution_id': str(execution.id),
'remediation_name': remediation.name,
'status': execution.status,
'message': f'Auto-remediation "{remediation.name}" execution started successfully'
}
except Exception as e:
return {
'error': f'Failed to execute auto-remediation: {str(e)}'
}
@staticmethod
def get_incident_status(chat_command: ChatCommand) -> Dict[str, Any]:
"""Get comprehensive incident status"""
try:
incident = chat_command.message.war_room.incident
# Get SLA status
from .sla_notifications import SLANotificationService
sla_status = SLANotificationService.get_sla_status_for_incident(incident)
# Get recent runbook executions
recent_executions = RunbookExecution.objects.filter(
incident=incident
).order_by('-created_at')[:5]
executions_data = []
for execution in recent_executions:
executions_data.append({
'id': str(execution.id),
'runbook_name': execution.runbook.name,
'status': execution.status,
'started_at': execution.started_at.isoformat() if execution.started_at else None,
'completed_at': execution.completed_at.isoformat() if execution.completed_at else None
})
return {
'incident_id': str(incident.id),
'title': incident.title,
'status': incident.status,
'severity': incident.severity,
'priority': incident.priority,
'category': incident.category,
'assigned_to': incident.assigned_to.username if incident.assigned_to else None,
'reporter': incident.reporter.username if incident.reporter else None,
'created_at': incident.created_at.isoformat(),
'updated_at': incident.updated_at.isoformat(),
'resolution_time': str(incident.resolution_time) if incident.resolution_time else None,
'sla_status': sla_status,
'recent_executions': executions_data,
'automation_enabled': incident.automation_enabled,
'runbook_suggested': incident.runbook_suggested,
'auto_remediation_attempted': incident.auto_remediation_attempted
}
except Exception as e:
return {
'error': f'Failed to get incident status: {str(e)}'
}
@staticmethod
def list_available_runbooks(incident: Incident) -> List[Dict[str, Any]]:
"""List runbooks available for the incident"""
try:
runbooks = Runbook.objects.filter(is_active=True)
available_runbooks = []
for runbook in runbooks:
if AutomationCommandService._runbook_applies_to_incident(runbook, incident):
available_runbooks.append({
'id': str(runbook.id),
'name': runbook.name,
'description': runbook.description,
'category': runbook.category,
'severity_levels': runbook.severity_levels,
'estimated_duration': runbook.estimated_duration
})
return available_runbooks
except Exception as e:
return []
@staticmethod
def list_available_remediations(incident: Incident) -> List[Dict[str, Any]]:
"""List auto-remediations available for the incident"""
try:
remediations = AutoRemediation.objects.filter(is_active=True)
available_remediations = []
for remediation in remediations:
if AutomationCommandService._remediation_applies_to_incident(remediation, incident):
available_remediations.append({
'id': str(remediation.id),
'name': remediation.name,
'description': remediation.description,
'category': remediation.category,
'severity_levels': remediation.severity_levels,
'estimated_duration': remediation.estimated_duration
})
return available_remediations
except Exception as e:
return []
@staticmethod
def _runbook_applies_to_incident(runbook: Runbook, incident: Incident) -> bool:
"""Check if runbook applies to the incident"""
# Check categories
if runbook.categories and incident.category not in runbook.categories:
return False
# Check severity levels
if runbook.severity_levels and incident.severity not in runbook.severity_levels:
return False
# Check if runbook is active
if not runbook.is_active:
return False
return True
@staticmethod
def _remediation_applies_to_incident(remediation: AutoRemediation, incident: Incident) -> bool:
"""Check if auto-remediation applies to the incident"""
# Check categories
if remediation.categories and incident.category not in remediation.categories:
return False
# Check severity levels
if remediation.severity_levels and incident.severity not in remediation.severity_levels:
return False
# Check if remediation is active
if not remediation.is_active:
return False
return True
@staticmethod
def _get_runbook_suggestions(partial_name: str) -> List[str]:
"""Get runbook name suggestions based on partial input"""
try:
runbooks = Runbook.objects.filter(
name__icontains=partial_name,
is_active=True
).values_list('name', flat=True)[:5]
return list(runbooks)
except:
return []
@staticmethod
def _get_remediation_suggestions(partial_name: str) -> List[str]:
"""Get auto-remediation name suggestions based on partial input"""
try:
remediations = AutoRemediation.objects.filter(
name__icontains=partial_name,
is_active=True
).values_list('name', flat=True)[:5]
return list(remediations)
except:
return []
@staticmethod
def _create_execution_status_message(war_room: 'WarRoom', content: str):
"""Create a status message in the war room"""
try:
WarRoomMessage.objects.create(
war_room=war_room,
content=content,
message_type='SYSTEM',
sender=None,
sender_name='Automation System',
external_data={
'message_type': 'automation_status'
}
)
except Exception as e:
print(f"Error creating status message: {e}")
@staticmethod
def update_execution_status(execution_id: str, status: str, result: Dict[str, Any] = None):
"""Update execution status and notify chat room"""
try:
# Find the chat command that triggered this execution
chat_command = ChatCommand.objects.filter(
automation_execution_id=execution_id
).first()
if not chat_command:
return False
# Update chat command status
chat_command.execution_status = status
if result:
chat_command.execution_result = result
chat_command.save()
# Create status update message
status_emoji = {
'SUCCESS': '',
'FAILED': '',
'RUNNING': '🔄',
'CANCELLED': '⏹️'
}.get(status, '📊')
message_content = (
f"{status_emoji} **Execution Status Update**\n\n"
f"**Status:** {status}\n"
f"**Execution ID:** {execution_id}\n"
)
if result:
if 'error' in result:
message_content += f"**Error:** {result['error']}\n"
if 'output' in result:
message_content += f"**Output:** {result['output'][:200]}...\n"
AutomationCommandService._create_execution_status_message(
chat_command.message.war_room,
message_content
)
return True
except Exception as e:
print(f"Error updating execution status: {e}")
return False

View File

@@ -0,0 +1,351 @@
"""
Compliance Integration Service for Chat
Handles file classification, audit trails, and compliance requirements
"""
from django.utils import timezone
from django.contrib.auth import get_user_model
from typing import Dict, Any, Optional, List
import hashlib
import mimetypes
from ..models import ChatFile, WarRoomMessage
from compliance_governance.models import DataClassification, AuditLog, CompliancePolicy
from security.models import DataClassification as SecurityDataClassification
User = get_user_model()
class ComplianceIntegrationService:
"""Service for handling compliance requirements in chat"""
@staticmethod
def classify_file(file_path: str, filename: str, file_size: int, user: User) -> Dict[str, Any]:
"""Classify a file based on content and context"""
try:
# Get file MIME type
mime_type, _ = mimetypes.guess_type(filename)
# Calculate file hash
file_hash = ComplianceIntegrationService._calculate_file_hash(file_path)
# Determine classification based on file type and content
classification_level = ComplianceIntegrationService._determine_classification(
filename, mime_type, file_size
)
# Get or create data classification
data_classification = ComplianceIntegrationService._get_or_create_classification(
classification_level
)
return {
'classification_level': classification_level,
'data_classification_id': str(data_classification.id) if data_classification else None,
'file_hash': file_hash,
'mime_type': mime_type,
'is_encrypted': classification_level in ['CONFIDENTIAL', 'RESTRICTED', 'TOP_SECRET'],
'retention_period': ComplianceIntegrationService._get_retention_period(classification_level)
}
except Exception as e:
return {
'error': f'Failed to classify file: {str(e)}',
'classification_level': 'PUBLIC', # Default to public on error
'file_hash': None,
'mime_type': None,
'is_encrypted': False
}
@staticmethod
def create_audit_log_entry(
action: str,
user: User,
resource_type: str,
resource_id: str,
details: Dict[str, Any] = None
) -> bool:
"""Create an audit log entry for compliance"""
try:
AuditLog.objects.create(
action=action,
user=user,
resource_type=resource_type,
resource_id=resource_id,
timestamp=timezone.now(),
details=details or {},
ip_address=ComplianceIntegrationService._get_user_ip(user),
user_agent=ComplianceIntegrationService._get_user_agent(user)
)
return True
except Exception as e:
print(f"Error creating audit log entry: {e}")
return False
@staticmethod
def log_chat_message_access(message: WarRoomMessage, user: User, action: str = 'access'):
"""Log access to chat messages for audit trail"""
try:
ComplianceIntegrationService.create_audit_log_entry(
action=f'chat_message_{action}',
user=user,
resource_type='chat_message',
resource_id=str(message.id),
details={
'war_room_id': str(message.war_room.id),
'incident_id': str(message.war_room.incident.id),
'message_type': message.message_type,
'sender': message.sender.username if message.sender else None,
'content_length': len(message.content),
'is_encrypted': message.is_encrypted
}
)
except Exception as e:
print(f"Error logging chat message access: {e}")
@staticmethod
def log_file_access(file_obj: ChatFile, user: User, action: str = 'access'):
"""Log file access for audit trail"""
try:
ComplianceIntegrationService.create_audit_log_entry(
action=f'file_{action}',
user=user,
resource_type='chat_file',
resource_id=str(file_obj.id),
details={
'filename': file_obj.original_filename,
'file_type': file_obj.file_type,
'file_size': file_obj.file_size,
'data_classification': file_obj.data_classification.level if file_obj.data_classification else None,
'is_encrypted': file_obj.is_encrypted,
'message_id': str(file_obj.message.id),
'incident_id': str(file_obj.message.war_room.incident.id)
}
)
except Exception as e:
print(f"Error logging file access: {e}")
@staticmethod
def check_compliance_policies(incident_id: str, user: User) -> Dict[str, Any]:
"""Check compliance policies for incident chat access"""
try:
# Get applicable compliance policies
policies = CompliancePolicy.objects.filter(
is_active=True,
applies_to_incidents=True
)
compliance_status = {
'policies_checked': len(policies),
'violations': [],
'warnings': [],
'recommendations': []
}
for policy in policies:
# Check if policy applies to this incident
if ComplianceIntegrationService._policy_applies_to_incident(policy, incident_id):
violations = ComplianceIntegrationService._check_policy_violations(
policy, incident_id, user
)
if violations:
compliance_status['violations'].extend(violations)
return compliance_status
except Exception as e:
return {
'error': f'Failed to check compliance policies: {str(e)}'
}
@staticmethod
def export_chat_logs_for_compliance(
incident_id: str,
start_date: timezone.datetime,
end_date: timezone.datetime,
user: User
) -> Dict[str, Any]:
"""Export chat logs for compliance reporting"""
try:
from ..models import WarRoom
# Get war room for incident
war_room = WarRoom.objects.filter(incident_id=incident_id).first()
if not war_room:
return {'error': 'War room not found for incident'}
# Get messages in date range
messages = WarRoomMessage.objects.filter(
war_room=war_room,
created_at__gte=start_date,
created_at__lte=end_date
).order_by('created_at')
# Prepare export data
export_data = {
'incident_id': incident_id,
'war_room_id': str(war_room.id),
'export_date': timezone.now().isoformat(),
'exported_by': user.username,
'date_range': {
'start': start_date.isoformat(),
'end': end_date.isoformat()
},
'message_count': messages.count(),
'messages': []
}
for message in messages:
message_data = {
'id': str(message.id),
'timestamp': message.created_at.isoformat(),
'sender': message.sender.username if message.sender else message.sender_name,
'message_type': message.message_type,
'content': message.content,
'is_encrypted': message.is_encrypted,
'is_pinned': message.is_pinned,
'attachments': [
{
'id': str(attachment.id),
'filename': attachment.original_filename,
'file_type': attachment.file_type,
'file_size': attachment.file_size,
'data_classification': attachment.data_classification.level if attachment.data_classification else None,
'is_encrypted': attachment.is_encrypted,
'file_hash': attachment.file_hash
}
for attachment in message.chat_files.all()
]
}
export_data['messages'].append(message_data)
# Log the export action
ComplianceIntegrationService.create_audit_log_entry(
action='export_chat_logs',
user=user,
resource_type='incident',
resource_id=incident_id,
details={
'message_count': export_data['message_count'],
'date_range': export_data['date_range']
}
)
return export_data
except Exception as e:
return {
'error': f'Failed to export chat logs: {str(e)}'
}
@staticmethod
def _calculate_file_hash(file_path: str) -> str:
"""Calculate SHA-256 hash of file"""
try:
hash_sha256 = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_sha256.update(chunk)
return hash_sha256.hexdigest()
except:
return ""
@staticmethod
def _determine_classification(filename: str, mime_type: str, file_size: int) -> str:
"""Determine data classification level based on file characteristics"""
# Check for sensitive file extensions
sensitive_extensions = ['.key', '.pem', '.p12', '.pfx', '.crt', '.cer']
if any(filename.lower().endswith(ext) for ext in sensitive_extensions):
return 'CONFIDENTIAL'
# Check for log files
if filename.lower().endswith('.log') or 'log' in filename.lower():
return 'INTERNAL'
# Check for configuration files
config_extensions = ['.conf', '.config', '.ini', '.yaml', '.yml', '.json']
if any(filename.lower().endswith(ext) for ext in config_extensions):
return 'INTERNAL'
# Check for database files
db_extensions = ['.db', '.sqlite', '.sql', '.dump']
if any(filename.lower().endswith(ext) for ext in db_extensions):
return 'CONFIDENTIAL'
# Check file size (large files might be sensitive)
if file_size > 100 * 1024 * 1024: # 100MB
return 'INTERNAL'
# Default classification
return 'PUBLIC'
@staticmethod
def _get_or_create_classification(level: str):
"""Get or create data classification object"""
try:
# Try to get from security module first
classification = SecurityDataClassification.objects.filter(level=level).first()
if classification:
return classification
# Create new classification if not found
classification = SecurityDataClassification.objects.create(
level=level,
description=f'Data classification level: {level}',
retention_period_days=ComplianceIntegrationService._get_retention_period(level)
)
return classification
except Exception as e:
print(f"Error getting/creating classification: {e}")
return None
@staticmethod
def _get_retention_period(classification_level: str) -> int:
"""Get retention period in days based on classification level"""
retention_periods = {
'PUBLIC': 365, # 1 year
'INTERNAL': 1095, # 3 years
'CONFIDENTIAL': 2555, # 7 years
'RESTRICTED': 3650, # 10 years
'TOP_SECRET': 3650 # 10 years
}
return retention_periods.get(classification_level, 365)
@staticmethod
def _get_user_ip(user: User) -> str:
"""Get user IP address (placeholder implementation)"""
# This would be implemented based on your request handling
return "127.0.0.1"
@staticmethod
def _get_user_agent(user: User) -> str:
"""Get user agent (placeholder implementation)"""
# This would be implemented based on your request handling
return "Chat System"
@staticmethod
def _policy_applies_to_incident(policy: CompliancePolicy, incident_id: str) -> bool:
"""Check if compliance policy applies to incident"""
# This would check policy conditions against incident
# For now, return True for all active policies
return policy.is_active
@staticmethod
def _check_policy_violations(policy: CompliancePolicy, incident_id: str, user: User) -> List[str]:
"""Check for policy violations"""
violations = []
# Example policy checks
if policy.name == "Data Retention Policy":
# Check if chat logs are being retained properly
pass
if policy.name == "Access Control Policy":
# Check if user has appropriate access
pass
return violations

View File

@@ -0,0 +1,287 @@
"""
SLA Notifications Service for Chat Integration
Handles SLA threshold notifications and escalation alerts in chat rooms
"""
from django.utils import timezone
from django.db.models import Q
from typing import Dict, Any, Optional, List
from ..models import WarRoom, WarRoomMessage
from sla_oncall.models import SLAInstance, EscalationInstance, EscalationPolicy
from incident_intelligence.models import Incident
class SLANotificationService:
"""Service for handling SLA-related notifications in chat rooms"""
@staticmethod
def send_sla_warning_notification(sla_instance: SLAInstance, threshold_percent: float = 80.0):
"""Send SLA warning notification to incident chat room"""
try:
war_room = WarRoom.objects.filter(incident=sla_instance.incident).first()
if not war_room:
return False
# Calculate time remaining
time_remaining = sla_instance.time_remaining
time_remaining_minutes = int(time_remaining.total_seconds() / 60)
# Create warning message
message_content = (
f"🚨 **SLA Warning** 🚨\n\n"
f"**SLA:** {sla_instance.sla_definition.name}\n"
f"**Type:** {sla_instance.sla_definition.get_sla_type_display()}\n"
f"**Time Remaining:** {time_remaining_minutes} minutes\n"
f"**Threshold:** {threshold_percent}% reached\n\n"
f"Please take immediate action to meet the SLA target."
)
# Create system message
WarRoomMessage.objects.create(
war_room=war_room,
content=message_content,
message_type='ALERT',
sender=None,
sender_name='SLA Monitor',
external_data={
'sla_instance_id': str(sla_instance.id),
'notification_type': 'sla_warning',
'threshold_percent': threshold_percent
}
)
return True
except Exception as e:
print(f"Error sending SLA warning notification: {e}")
return False
@staticmethod
def send_sla_breach_notification(sla_instance: SLAInstance):
"""Send SLA breach notification to incident chat room"""
try:
war_room = WarRoom.objects.filter(incident=sla_instance.incident).first()
if not war_room:
return False
# Calculate breach time
breach_time = sla_instance.breach_time
breach_minutes = int(breach_time.total_seconds() / 60)
# Create breach message
message_content = (
f"🚨 **SLA BREACHED** 🚨\n\n"
f"**SLA:** {sla_instance.sla_definition.name}\n"
f"**Type:** {sla_instance.sla_definition.get_sla_type_display()}\n"
f"**Breach Time:** {breach_minutes} minutes ago\n"
f"**Target Time:** {sla_instance.target_time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
f"**IMMEDIATE ACTION REQUIRED**\n"
f"Escalation procedures have been triggered."
)
# Create system message
WarRoomMessage.objects.create(
war_room=war_room,
content=message_content,
message_type='ALERT',
sender=None,
sender_name='SLA Monitor',
external_data={
'sla_instance_id': str(sla_instance.id),
'notification_type': 'sla_breach',
'breach_minutes': breach_minutes
}
)
return True
except Exception as e:
print(f"Error sending SLA breach notification: {e}")
return False
@staticmethod
def send_escalation_notification(escalation_instance: EscalationInstance):
"""Send escalation notification to incident chat room"""
try:
war_room = WarRoom.objects.filter(incident=escalation_instance.incident).first()
if not war_room:
return False
# Create escalation message
message_content = (
f"📢 **ESCALATION TRIGGERED** 📢\n\n"
f"**Policy:** {escalation_instance.escalation_policy.name}\n"
f"**Level:** {escalation_instance.escalation_level}\n"
f"**Trigger:** {escalation_instance.escalation_policy.get_trigger_condition_display()}\n"
f"**Time:** {escalation_instance.triggered_at.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
f"**Actions Taken:**\n"
)
# Add actions taken
for action in escalation_instance.actions_taken:
message_content += f"{action}\n"
# Create system message
WarRoomMessage.objects.create(
war_room=war_room,
content=message_content,
message_type='ALERT',
sender=None,
sender_name='Escalation System',
external_data={
'escalation_instance_id': str(escalation_instance.id),
'notification_type': 'escalation',
'escalation_level': escalation_instance.escalation_level
}
)
return True
except Exception as e:
print(f"Error sending escalation notification: {e}")
return False
@staticmethod
def send_oncall_handoff_notification(incident: Incident, old_oncall_user, new_oncall_user):
"""Send on-call handoff notification to incident chat room"""
try:
war_room = WarRoom.objects.filter(incident=incident).first()
if not war_room:
return False
# Create handoff message
message_content = (
f"🔄 **ON-CALL HANDOFF** 🔄\n\n"
f"**From:** {old_oncall_user.username if old_oncall_user else 'System'}\n"
f"**To:** {new_oncall_user.username}\n"
f"**Time:** {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
f"Please review the incident status and continue response activities."
)
# Create system message
WarRoomMessage.objects.create(
war_room=war_room,
content=message_content,
message_type='UPDATE',
sender=None,
sender_name='On-Call System',
external_data={
'notification_type': 'oncall_handoff',
'old_oncall_user_id': str(old_oncall_user.id) if old_oncall_user else None,
'new_oncall_user_id': str(new_oncall_user.id)
}
)
return True
except Exception as e:
print(f"Error sending on-call handoff notification: {e}")
return False
@staticmethod
def send_sla_met_notification(sla_instance: SLAInstance):
"""Send SLA met notification to incident chat room"""
try:
war_room = WarRoom.objects.filter(incident=sla_instance.incident).first()
if not war_room:
return False
# Calculate response time
response_time = sla_instance.response_time
response_minutes = int(response_time.total_seconds() / 60) if response_time else 0
# Create success message
message_content = (
f"✅ **SLA MET** ✅\n\n"
f"**SLA:** {sla_instance.sla_definition.name}\n"
f"**Type:** {sla_instance.sla_definition.get_sla_type_display()}\n"
f"**Response Time:** {response_minutes} minutes\n"
f"**Target Time:** {sla_instance.target_time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
f"Great job meeting the SLA target!"
)
# Create system message
WarRoomMessage.objects.create(
war_room=war_room,
content=message_content,
message_type='UPDATE',
sender=None,
sender_name='SLA Monitor',
external_data={
'sla_instance_id': str(sla_instance.id),
'notification_type': 'sla_met',
'response_minutes': response_minutes
}
)
return True
except Exception as e:
print(f"Error sending SLA met notification: {e}")
return False
@staticmethod
def get_sla_status_for_incident(incident: Incident) -> Dict[str, Any]:
"""Get SLA status summary for an incident"""
try:
sla_instances = SLAInstance.objects.filter(incident=incident)
status_summary = {
'total_slas': sla_instances.count(),
'active_slas': sla_instances.filter(status='ACTIVE').count(),
'met_slas': sla_instances.filter(status='MET').count(),
'breached_slas': sla_instances.filter(status='BREACHED').count(),
'sla_details': []
}
for sla in sla_instances:
sla_detail = {
'id': str(sla.id),
'name': sla.sla_definition.name,
'type': sla.sla_definition.get_sla_type_display(),
'status': sla.status,
'target_time': sla.target_time.isoformat(),
'time_remaining': int(sla.time_remaining.total_seconds() / 60) if sla.status == 'ACTIVE' else 0,
'breach_time': int(sla.breach_time.total_seconds() / 60) if sla.is_breached else 0
}
status_summary['sla_details'].append(sla_detail)
return status_summary
except Exception as e:
print(f"Error getting SLA status: {e}")
return {'error': str(e)}
@staticmethod
def check_and_send_threshold_notifications():
"""Check all active SLAs and send threshold notifications"""
try:
active_slas = SLAInstance.objects.filter(status='ACTIVE')
notifications_sent = 0
for sla in active_slas:
# Check if SLA is approaching threshold
if sla.sla_definition.escalation_enabled:
threshold_percent = sla.sla_definition.escalation_threshold_percent
time_elapsed = timezone.now() - sla.started_at
total_duration = sla.target_time - sla.started_at
elapsed_percent = (time_elapsed.total_seconds() / total_duration.total_seconds()) * 100
if elapsed_percent >= threshold_percent:
# Check if we haven't already sent a warning
existing_warning = WarRoomMessage.objects.filter(
war_room__incident=sla.incident,
message_type='ALERT',
external_data__notification_type='sla_warning',
external_data__sla_instance_id=str(sla.id)
).exists()
if not existing_warning:
if SLANotificationService.send_sla_warning_notification(sla, threshold_percent):
notifications_sent += 1
return notifications_sent
except Exception as e:
print(f"Error checking SLA thresholds: {e}")
return 0

View File

@@ -0,0 +1,261 @@
"""
Signals for Collaboration & War Rooms module
Handles automatic war room creation, timeline events, and integration with other modules
"""
from django.db.models.signals import post_save, pre_save, post_delete
from django.dispatch import receiver
from django.utils import timezone
from .models import WarRoom, TimelineEvent, IncidentCommandRole, WarRoomMessage, MessageReaction, ChatCommand
from incident_intelligence.models import Incident
from automation_orchestration.models import RunbookExecution, AutoRemediationExecution
from sla_oncall.models import SLAInstance, EscalationInstance
@receiver(post_save, sender=Incident)
def create_war_room_for_incident(sender, instance, created, **kwargs):
"""Automatically create a war room when a new incident is created"""
if created:
# Create war room for the incident
war_room = WarRoom.objects.create(
name=f"Incident {instance.id} - {instance.title[:50]}",
description=f"War room for incident: {instance.title}",
incident=instance,
created_by=instance.reporter,
privacy_level='PRIVATE'
)
# Add incident reporter and assignee to war room
if instance.reporter:
war_room.add_participant(instance.reporter)
if instance.assigned_to:
war_room.add_participant(instance.assigned_to)
# Create timeline event for war room creation
TimelineEvent.create_system_event(
incident=instance,
event_type='WAR_ROOM_CREATED',
title='War Room Created',
description=f'War room "{war_room.name}" was automatically created for this incident',
related_war_room=war_room,
event_data={'war_room_id': str(war_room.id)}
)
@receiver(post_save, sender=Incident)
def create_timeline_events_for_incident_changes(sender, instance, created, **kwargs):
"""Create timeline events for incident changes"""
if not created:
# Check for status changes
if hasattr(instance, '_old_status') and instance._old_status != instance.status:
TimelineEvent.create_system_event(
incident=instance,
event_type='STATUS_CHANGED',
title=f'Status Changed to {instance.get_status_display()}',
description=f'Incident status changed from {instance._old_status} to {instance.status}',
event_data={
'old_status': instance._old_status,
'new_status': instance.status
}
)
# Check for severity changes
if hasattr(instance, '_old_severity') and instance._old_severity != instance.severity:
TimelineEvent.create_system_event(
incident=instance,
event_type='SEVERITY_CHANGED',
title=f'Severity Changed to {instance.get_severity_display()}',
description=f'Incident severity changed from {instance._old_severity} to {instance.severity}',
event_data={
'old_severity': instance._old_severity,
'new_severity': instance.severity
}
)
# Check for assignment changes
if hasattr(instance, '_old_assigned_to') and instance._old_assigned_to != instance.assigned_to:
old_user = instance._old_assigned_to.username if instance._old_assigned_to else 'Unassigned'
new_user = instance.assigned_to.username if instance.assigned_to else 'Unassigned'
TimelineEvent.create_system_event(
incident=instance,
event_type='ASSIGNMENT_CHANGED',
title=f'Assignment Changed to {new_user}',
description=f'Incident assignment changed from {old_user} to {new_user}',
event_data={
'old_assigned_to': str(instance._old_assigned_to.id) if instance._old_assigned_to else None,
'new_assigned_to': str(instance.assigned_to.id) if instance.assigned_to else None
}
)
@receiver(pre_save, sender=Incident)
def store_old_values_for_incident(sender, instance, **kwargs):
"""Store old values before saving to detect changes"""
if instance.pk:
try:
old_instance = Incident.objects.get(pk=instance.pk)
instance._old_status = old_instance.status
instance._old_severity = old_instance.severity
instance._old_assigned_to = old_instance.assigned_to
except Incident.DoesNotExist:
pass
@receiver(post_save, sender=RunbookExecution)
def create_timeline_event_for_runbook_execution(sender, instance, created, **kwargs):
"""Create timeline event when runbook is executed"""
if created and instance.incident:
TimelineEvent.create_system_event(
incident=instance.incident,
event_type='RUNBOOK_EXECUTED',
title=f'Runbook Executed: {instance.runbook.name}',
description=f'Runbook "{instance.runbook.name}" was executed for this incident',
related_runbook_execution=instance,
event_data={
'runbook_id': str(instance.runbook.id),
'runbook_name': instance.runbook.name,
'triggered_by': str(instance.triggered_by.id) if instance.triggered_by else None
}
)
@receiver(post_save, sender=AutoRemediationExecution)
def create_timeline_event_for_auto_remediation(sender, instance, created, **kwargs):
"""Create timeline event when auto-remediation is executed"""
if created:
TimelineEvent.create_system_event(
incident=instance.incident,
event_type='AUTO_REMEDIATION_ATTEMPTED',
title=f'Auto-remediation Attempted: {instance.auto_remediation.name}',
description=f'Auto-remediation "{instance.auto_remediation.name}" was attempted for this incident',
related_auto_remediation=instance,
event_data={
'auto_remediation_id': str(instance.auto_remediation.id),
'auto_remediation_name': instance.auto_remediation.name,
'status': instance.status
}
)
@receiver(post_save, sender=SLAInstance)
def create_timeline_event_for_sla_breach(sender, instance, created, **kwargs):
"""Create timeline event when SLA is breached"""
if not created and instance.status == 'BREACHED':
TimelineEvent.create_system_event(
incident=instance.incident,
event_type='SLA_BREACHED',
title=f'SLA Breached: {instance.sla_definition.name}',
description=f'SLA "{instance.sla_definition.name}" has been breached',
related_sla_instance=instance,
event_data={
'sla_definition_id': str(instance.sla_definition.id),
'sla_definition_name': instance.sla_definition.name,
'breach_time': instance.breached_at.isoformat() if instance.breached_at else None
}
)
@receiver(post_save, sender=EscalationInstance)
def create_timeline_event_for_escalation(sender, instance, created, **kwargs):
"""Create timeline event when escalation is triggered"""
if created:
TimelineEvent.create_system_event(
incident=instance.incident,
event_type='ESCALATION_TRIGGERED',
title=f'Escalation Triggered: {instance.escalation_policy.name}',
description=f'Escalation policy "{instance.escalation_policy.name}" was triggered',
related_escalation=instance,
event_data={
'escalation_policy_id': str(instance.escalation_policy.id),
'escalation_policy_name': instance.escalation_policy.name,
'escalation_level': instance.escalation_level
}
)
@receiver(post_save, sender=IncidentCommandRole)
def create_timeline_event_for_command_role_assignment(sender, instance, created, **kwargs):
"""Create timeline event when command role is assigned"""
if created and instance.assigned_user:
TimelineEvent.create_system_event(
incident=instance.incident,
event_type='COMMAND_ROLE_ASSIGNED',
title=f'Command Role Assigned: {instance.get_role_type_display()}',
description=f'{instance.get_role_type_display()} role assigned to {instance.assigned_user.username}',
related_command_role=instance,
event_data={
'role_type': instance.role_type,
'assigned_user_id': str(instance.assigned_user.id),
'assigned_user_name': instance.assigned_user.username
}
)
@receiver(post_save, sender=WarRoomMessage)
def create_timeline_event_for_important_messages(sender, instance, created, **kwargs):
"""Create timeline event for important messages (pinned, commands, etc.)"""
if created:
# Create timeline event for command messages
if instance.message_type == 'COMMAND':
TimelineEvent.create_user_event(
incident=instance.war_room.incident,
user=instance.sender,
event_type='MANUAL_EVENT',
title='Chat Command Executed',
description=f'Command "{instance.content[:50]}..." was executed in war room',
event_data={
'message_id': str(instance.id),
'command_type': 'chat_command'
}
)
# Create timeline event for system messages
elif instance.message_type == 'SYSTEM':
TimelineEvent.create_system_event(
incident=instance.war_room.incident,
event_type='MANUAL_EVENT',
title='System Message Posted',
description=f'System message posted in war room: {instance.content[:50]}...',
event_data={
'message_id': str(instance.id),
'message_type': 'system'
}
)
@receiver(post_save, sender=MessageReaction)
def create_timeline_event_for_reactions(sender, instance, created, **kwargs):
"""Create timeline event for reactions on important messages"""
if created and instance.message.is_pinned:
TimelineEvent.create_user_event(
incident=instance.message.war_room.incident,
user=instance.user,
event_type='MANUAL_EVENT',
title='Reaction Added to Pinned Message',
description=f'User {instance.user.username} reacted {instance.emoji} to pinned message',
event_data={
'message_id': str(instance.message.id),
'reaction_emoji': instance.emoji,
'user_id': str(instance.user.id)
}
)
@receiver(post_save, sender=ChatCommand)
def create_timeline_event_for_chat_commands(sender, instance, created, **kwargs):
"""Create timeline event when chat commands are executed"""
if created:
TimelineEvent.create_user_event(
incident=instance.message.war_room.incident,
user=instance.executed_by,
event_type='MANUAL_EVENT',
title=f'ChatOps Command Executed: {instance.command_type}',
description=f'ChatOps command "{instance.command_text}" was executed',
event_data={
'command_id': str(instance.id),
'command_type': instance.command_type,
'command_text': instance.command_text,
'execution_status': instance.execution_status
}
)

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,30 @@
"""
URL configuration for Collaboration & War Rooms module
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views.collaboration import (
WarRoomViewSet, ConferenceBridgeViewSet, IncidentCommandRoleViewSet,
TimelineEventViewSet, WarRoomMessageViewSet, IncidentDecisionViewSet,
MessageReactionViewSet, ChatFileViewSet, ChatCommandViewSet, ChatBotViewSet
)
# Create router and register viewsets
router = DefaultRouter()
router.register(r'war-rooms', WarRoomViewSet, basename='warroom')
router.register(r'conference-bridges', ConferenceBridgeViewSet, basename='conferencebridge')
router.register(r'command-roles', IncidentCommandRoleViewSet, basename='incidentcommandrole')
router.register(r'timeline-events', TimelineEventViewSet, basename='timelineevent')
router.register(r'war-room-messages', WarRoomMessageViewSet, basename='warroommessage')
router.register(r'incident-decisions', IncidentDecisionViewSet, basename='incidentdecision')
router.register(r'message-reactions', MessageReactionViewSet, basename='messagereaction')
router.register(r'chat-files', ChatFileViewSet, basename='chatfile')
router.register(r'chat-commands', ChatCommandViewSet, basename='chatcommand')
router.register(r'chat-bots', ChatBotViewSet, basename='chatbot')
app_name = 'collaboration_war_rooms'
urlpatterns = [
path('api/', include(router.urls)),
]

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1 @@
# Views for Collaboration & War Rooms module

View File

@@ -0,0 +1,646 @@
"""
Views for Collaboration & War Rooms module
"""
from rest_framework import viewsets, status, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter
from django.db.models import Q
from django.utils import timezone
from ..models import (
WarRoom, ConferenceBridge, IncidentCommandRole,
TimelineEvent, WarRoomMessage, IncidentDecision,
MessageReaction, ChatFile, ChatCommand, ChatBot
)
from ..serializers.collaboration import (
WarRoomSerializer, ConferenceBridgeSerializer, IncidentCommandRoleSerializer,
TimelineEventSerializer, WarRoomMessageSerializer, IncidentDecisionSerializer,
WarRoomSummarySerializer, TimelineEventSummarySerializer,
MessageReactionSerializer, ChatFileSerializer, ChatCommandSerializer, ChatBotSerializer
)
from incident_intelligence.models import Incident
class WarRoomViewSet(viewsets.ModelViewSet):
"""ViewSet for WarRoom model"""
queryset = WarRoom.objects.all()
serializer_class = WarRoomSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['status', 'privacy_level', 'incident__severity']
search_fields = ['name', 'description', 'incident__title']
ordering_fields = ['created_at', 'last_activity', 'message_count']
ordering = ['-created_at']
def get_queryset(self):
"""Filter war rooms based on user access"""
queryset = super().get_queryset()
user = self.request.user
# Filter by user access
accessible_war_rooms = []
for war_room in queryset:
if war_room.can_user_access(user):
accessible_war_rooms.append(war_room.id)
return queryset.filter(id__in=accessible_war_rooms)
def get_serializer_class(self):
"""Return appropriate serializer based on action"""
if self.action == 'list':
return WarRoomSummarySerializer
return WarRoomSerializer
@action(detail=True, methods=['post'])
def add_participant(self, request, pk=None):
"""Add a participant to the war room"""
war_room = self.get_object()
user_id = request.data.get('user_id')
if not user_id:
return Response(
{'error': 'user_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.get(id=user_id)
if war_room.can_user_access(user):
war_room.add_participant(user)
return Response({'message': 'Participant added successfully'})
else:
return Response(
{'error': 'User does not have access to this war room'},
status=status.HTTP_403_FORBIDDEN
)
except User.DoesNotExist:
return Response(
{'error': 'User not found'},
status=status.HTTP_404_NOT_FOUND
)
@action(detail=True, methods=['post'])
def remove_participant(self, request, pk=None):
"""Remove a participant from the war room"""
war_room = self.get_object()
user_id = request.data.get('user_id')
if not user_id:
return Response(
{'error': 'user_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.get(id=user_id)
war_room.remove_participant(user)
return Response({'message': 'Participant removed successfully'})
except User.DoesNotExist:
return Response(
{'error': 'User not found'},
status=status.HTTP_404_NOT_FOUND
)
@action(detail=True, methods=['get'])
def messages(self, request, pk=None):
"""Get messages for a war room"""
war_room = self.get_object()
messages = war_room.messages.all().order_by('created_at')
# Apply pagination
page = self.paginate_queryset(messages)
if page is not None:
serializer = WarRoomMessageSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = WarRoomMessageSerializer(messages, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def pinned_messages(self, request, pk=None):
"""Get pinned messages for a war room"""
war_room = self.get_object()
pinned_messages = war_room.messages.filter(is_pinned=True).order_by('-pinned_at')
serializer = WarRoomMessageSerializer(pinned_messages, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def create_chat_room(self, request, pk=None):
"""Auto-create chat room for incident"""
incident = self.get_object()
# Check if war room already exists for this incident
existing_war_room = WarRoom.objects.filter(incident=incident).first()
if existing_war_room:
return Response(
{'message': 'War room already exists for this incident', 'war_room_id': existing_war_room.id},
status=status.HTTP_200_OK
)
# Create new war room
war_room = WarRoom.objects.create(
name=f"Incident Chat - {incident.title}",
description=f"Chat room for incident: {incident.title}",
incident=incident,
created_by=request.user,
privacy_level='PRIVATE'
)
# Add incident reporter and assignee to war room
if incident.reporter:
war_room.add_participant(incident.reporter)
if incident.assigned_to:
war_room.add_participant(incident.assigned_to)
# Create timeline event
TimelineEvent.create_system_event(
incident=incident,
event_type='WAR_ROOM_CREATED',
title='War Room Created',
description=f'War room "{war_room.name}" was automatically created for incident collaboration',
event_data={'war_room_id': str(war_room.id)}
)
serializer = WarRoomSerializer(war_room)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class ConferenceBridgeViewSet(viewsets.ModelViewSet):
"""ViewSet for ConferenceBridge model"""
queryset = ConferenceBridge.objects.all()
serializer_class = ConferenceBridgeSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['bridge_type', 'status', 'incident__severity']
search_fields = ['name', 'description', 'incident__title']
ordering_fields = ['scheduled_start', 'created_at']
ordering = ['-scheduled_start']
def get_queryset(self):
"""Filter conference bridges based on user access"""
queryset = super().get_queryset()
user = self.request.user
# Filter by user access
accessible_conferences = []
for conference in queryset:
if conference.can_user_join(user):
accessible_conferences.append(conference.id)
return queryset.filter(id__in=accessible_conferences)
@action(detail=True, methods=['post'])
def join_conference(self, request, pk=None):
"""Join a conference"""
conference = self.get_object()
user = request.user
if conference.can_user_join(user):
conference.add_participant(user)
return Response({'message': 'Successfully joined conference'})
else:
return Response(
{'error': 'You do not have permission to join this conference'},
status=status.HTTP_403_FORBIDDEN
)
@action(detail=True, methods=['post'])
def start_conference(self, request, pk=None):
"""Start a conference"""
conference = self.get_object()
if conference.status == 'SCHEDULED':
conference.status = 'ACTIVE'
conference.actual_start = timezone.now()
conference.save()
return Response({'message': 'Conference started successfully'})
else:
return Response(
{'error': 'Conference cannot be started in current status'},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=True, methods=['post'])
def end_conference(self, request, pk=None):
"""End a conference"""
conference = self.get_object()
if conference.status == 'ACTIVE':
conference.status = 'ENDED'
conference.actual_end = timezone.now()
conference.save()
return Response({'message': 'Conference ended successfully'})
else:
return Response(
{'error': 'Conference cannot be ended in current status'},
status=status.HTTP_400_BAD_REQUEST
)
class IncidentCommandRoleViewSet(viewsets.ModelViewSet):
"""ViewSet for IncidentCommandRole model"""
queryset = IncidentCommandRole.objects.all()
serializer_class = IncidentCommandRoleSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['role_type', 'status', 'incident__severity']
search_fields = ['incident__title', 'assigned_user__username']
ordering_fields = ['assigned_at', 'created_at']
ordering = ['-assigned_at']
def get_queryset(self):
"""Filter command roles based on user access"""
queryset = super().get_queryset()
user = self.request.user
# Filter by incident access
accessible_incidents = []
for role in queryset:
if role.incident.is_accessible_by_user(user):
accessible_incidents.append(role.incident.id)
return queryset.filter(incident_id__in=accessible_incidents)
@action(detail=True, methods=['post'])
def reassign_role(self, request, pk=None):
"""Reassign a command role to a new user"""
command_role = self.get_object()
new_user_id = request.data.get('new_user_id')
notes = request.data.get('notes', '')
if not new_user_id:
return Response(
{'error': 'new_user_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
from django.contrib.auth import get_user_model
User = get_user_model()
new_user = User.objects.get(id=new_user_id)
command_role.reassign_role(new_user, request.user, notes)
return Response({'message': 'Role reassigned successfully'})
except User.DoesNotExist:
return Response(
{'error': 'User not found'},
status=status.HTTP_404_NOT_FOUND
)
class TimelineEventViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for TimelineEvent model (read-only)"""
queryset = TimelineEvent.objects.all()
serializer_class = TimelineEventSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['event_type', 'source_type', 'is_critical_event', 'incident__severity']
search_fields = ['title', 'description', 'incident__title']
ordering_fields = ['event_time', 'created_at']
ordering = ['event_time']
def get_serializer_class(self):
"""Return appropriate serializer based on action"""
if self.action == 'list':
return TimelineEventSummarySerializer
return TimelineEventSerializer
def get_queryset(self):
"""Filter timeline events based on user access"""
queryset = super().get_queryset()
user = self.request.user
# Filter by incident access
accessible_incidents = []
for event in queryset:
if event.incident.is_accessible_by_user(user):
accessible_incidents.append(event.incident.id)
return queryset.filter(incident_id__in=accessible_incidents)
@action(detail=False, methods=['get'])
def critical_events(self, request):
"""Get critical events for postmortem analysis"""
queryset = self.get_queryset().filter(is_critical_event=True)
# Apply pagination
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class WarRoomMessageViewSet(viewsets.ModelViewSet):
"""ViewSet for WarRoomMessage model"""
queryset = WarRoomMessage.objects.all()
serializer_class = WarRoomMessageSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['message_type', 'war_room', 'sender', 'is_pinned']
search_fields = ['content', 'sender_name']
ordering_fields = ['created_at', 'pinned_at']
ordering = ['created_at']
def get_queryset(self):
"""Filter messages based on war room access"""
queryset = super().get_queryset()
user = self.request.user
# Filter by war room access
accessible_war_rooms = []
for message in queryset:
if message.war_room.can_user_access(user):
accessible_war_rooms.append(message.war_room.id)
return queryset.filter(war_room_id__in=accessible_war_rooms)
@action(detail=True, methods=['post'])
def pin_message(self, request, pk=None):
"""Pin a message"""
message = self.get_object()
message.pin_message(request.user)
# Create timeline event
TimelineEvent.create_user_event(
incident=message.war_room.incident,
user=request.user,
event_type='MANUAL_EVENT',
title='Message Pinned',
description=f'Message "{message.content[:50]}..." was pinned by {request.user.username}',
event_data={'message_id': str(message.id), 'action': 'pinned'}
)
return Response({'message': 'Message pinned successfully'})
@action(detail=True, methods=['post'])
def unpin_message(self, request, pk=None):
"""Unpin a message"""
message = self.get_object()
message.unpin_message()
return Response({'message': 'Message unpinned successfully'})
@action(detail=True, methods=['post'])
def add_reaction(self, request, pk=None):
"""Add a reaction to a message"""
message = self.get_object()
emoji = request.data.get('emoji')
if not emoji:
return Response(
{'error': 'emoji is required'},
status=status.HTTP_400_BAD_REQUEST
)
reaction = message.add_reaction(request.user, emoji)
serializer = MessageReactionSerializer(reaction)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def remove_reaction(self, request, pk=None):
"""Remove a reaction from a message"""
message = self.get_object()
emoji = request.data.get('emoji')
if not emoji:
return Response(
{'error': 'emoji is required'},
status=status.HTTP_400_BAD_REQUEST
)
message.remove_reaction(request.user, emoji)
return Response({'message': 'Reaction removed successfully'})
@action(detail=True, methods=['post'])
def execute_command(self, request, pk=None):
"""Execute a ChatOps command"""
message = self.get_object()
command_text = request.data.get('command_text')
if not command_text:
return Response(
{'error': 'command_text is required'},
status=status.HTTP_400_BAD_REQUEST
)
# Parse command
command_type = self._parse_command_type(command_text)
parameters = self._parse_command_parameters(command_text)
# Create chat command
chat_command = ChatCommand.objects.create(
message=message,
command_type=command_type,
command_text=command_text,
parameters=parameters
)
# Execute command
result = chat_command.execute_command(request.user)
serializer = ChatCommandSerializer(chat_command)
return Response(serializer.data)
def _parse_command_type(self, command_text):
"""Parse command type from command text"""
command_text = command_text.lower().strip()
if command_text.startswith('/status'):
return 'STATUS'
elif command_text.startswith('/runbook'):
return 'RUNBOOK'
elif command_text.startswith('/escalate'):
return 'ESCALATE'
elif command_text.startswith('/assign'):
return 'ASSIGN'
elif command_text.startswith('/update'):
return 'UPDATE'
else:
return 'CUSTOM'
def _parse_command_parameters(self, command_text):
"""Parse command parameters from command text"""
parts = command_text.split()
if len(parts) > 1:
return {'args': parts[1:]}
return {}
class IncidentDecisionViewSet(viewsets.ModelViewSet):
"""ViewSet for IncidentDecision model"""
queryset = IncidentDecision.objects.all()
serializer_class = IncidentDecisionSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['decision_type', 'status', 'incident__severity']
search_fields = ['title', 'description', 'incident__title']
ordering_fields = ['created_at', 'approved_at', 'implemented_at']
ordering = ['-created_at']
def get_queryset(self):
"""Filter decisions based on incident access"""
queryset = super().get_queryset()
user = self.request.user
# Filter by incident access
accessible_incidents = []
for decision in queryset:
if decision.incident.is_accessible_by_user(user):
accessible_incidents.append(decision.incident.id)
return queryset.filter(incident_id__in=accessible_incidents)
@action(detail=True, methods=['post'])
def approve_decision(self, request, pk=None):
"""Approve a decision"""
decision = self.get_object()
if decision.status == 'PENDING':
decision.approve(request.user)
return Response({'message': 'Decision approved successfully'})
else:
return Response(
{'error': 'Decision cannot be approved in current status'},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=True, methods=['post'])
def implement_decision(self, request, pk=None):
"""Mark a decision as implemented"""
decision = self.get_object()
notes = request.data.get('notes', '')
if decision.status == 'APPROVED':
decision.implement(request.user, notes)
return Response({'message': 'Decision implemented successfully'})
else:
return Response(
{'error': 'Decision must be approved before implementation'},
status=status.HTTP_400_BAD_REQUEST
)
class MessageReactionViewSet(viewsets.ModelViewSet):
"""ViewSet for MessageReaction model"""
queryset = MessageReaction.objects.all()
serializer_class = MessageReactionSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['message', 'user', 'emoji']
ordering_fields = ['created_at']
ordering = ['created_at']
class ChatFileViewSet(viewsets.ModelViewSet):
"""ViewSet for ChatFile model"""
queryset = ChatFile.objects.all()
serializer_class = ChatFileSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['message', 'file_type', 'data_classification']
search_fields = ['filename', 'original_filename']
ordering_fields = ['uploaded_at', 'file_size']
ordering = ['-uploaded_at']
def get_queryset(self):
"""Filter files based on message access"""
queryset = super().get_queryset()
user = self.request.user
# Filter by message access
accessible_messages = []
for file_obj in queryset:
if file_obj.message.war_room.can_user_access(user):
accessible_messages.append(file_obj.message.id)
return queryset.filter(message_id__in=accessible_messages)
@action(detail=True, methods=['post'])
def log_access(self, request, pk=None):
"""Log file access for audit trail"""
file_obj = self.get_object()
file_obj.log_access(request.user)
return Response({'message': 'Access logged successfully'})
class ChatCommandViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for ChatCommand model (read-only)"""
queryset = ChatCommand.objects.all()
serializer_class = ChatCommandSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['command_type', 'execution_status', 'executed_by']
search_fields = ['command_text']
ordering_fields = ['executed_at', 'created_at']
ordering = ['-executed_at']
def get_queryset(self):
"""Filter commands based on message access"""
queryset = super().get_queryset()
user = self.request.user
# Filter by message access
accessible_messages = []
for command in queryset:
if command.message.war_room.can_user_access(user):
accessible_messages.append(command.message.id)
return queryset.filter(message_id__in=accessible_messages)
class ChatBotViewSet(viewsets.ModelViewSet):
"""ViewSet for ChatBot model"""
queryset = ChatBot.objects.all()
serializer_class = ChatBotSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['bot_type', 'is_active']
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at']
ordering = ['name']
@action(detail=True, methods=['post'])
def generate_response(self, request, pk=None):
"""Generate AI response to a message"""
bot = self.get_object()
message_id = request.data.get('message_id')
if not message_id:
return Response(
{'error': 'message_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
message = WarRoomMessage.objects.get(id=message_id)
response = bot.generate_response(message, request.data.get('context', {}))
return Response(response)
except WarRoomMessage.DoesNotExist:
return Response(
{'error': 'Message not found'},
status=status.HTTP_404_NOT_FOUND
)