diff --git a/five_whys_analyzer/processor.py b/five_whys_analyzer/processor.py index 4e7c0df..cbf9e58 100644 --- a/five_whys_analyzer/processor.py +++ b/five_whys_analyzer/processor.py @@ -4,6 +4,9 @@ from django.conf import settings from .models import FiveWhysAnalyzerRequest, FiveWhysAnalyzerResponse import json import uuid +import logging + +logger = logging.getLogger(__name__) class FiveWhysAnalyzerProcessor(StandardWebhookProcessor): @@ -13,6 +16,43 @@ class FiveWhysAnalyzerProcessor(StandardWebhookProcessor): webhook_url = 'https://m8taq6tk.rpcld.cc/webhook/5-whys-web' agent_id = 'five-whys-001' + # Security settings + webhook_timeout = 30 # seconds + max_retries = 2 + + def make_secure_webhook_request(self, payload): + """Make a secure webhook request with timeout and logging""" + import requests + + try: + logger.info(f"Making webhook request to {self.webhook_url} for agent {self.agent_id}") + + response = requests.post( + self.webhook_url, + json=payload, + timeout=self.webhook_timeout, + headers={ + 'Content-Type': 'application/json', + 'User-Agent': f'QuantumTasksAI-{self.agent_slug}/1.0' + } + ) + + response.raise_for_status() + response_data = response.json() + + logger.info(f"Webhook request successful for agent {self.agent_id}") + return response_data + + except requests.exceptions.Timeout: + logger.error(f"Webhook timeout for agent {self.agent_id}") + raise Exception("Service temporarily unavailable") + except requests.exceptions.RequestException as e: + logger.error(f"Webhook request failed for agent {self.agent_id}: {str(e)}") + raise Exception("External service error") + except ValueError as e: # JSON decode error + logger.error(f"Invalid webhook response format for agent {self.agent_id}: {str(e)}") + raise Exception("Invalid service response") + def process_response(self, response_data, request_obj): """Required implementation of abstract method - delegates to specific handlers""" # This method is required by the base class but we handle responses @@ -41,7 +81,8 @@ class FiveWhysAnalyzerProcessor(StandardWebhookProcessor): try: agent = BaseAgent.objects.get(slug=self.agent_slug) except BaseAgent.DoesNotExist: - raise Exception(f"Agent with slug '{self.agent_slug}' not found") + logger.error(f"Agent with slug '{self.agent_slug}' not found") + raise Exception("Service configuration error") # Get or create request object for this session request_obj, created = FiveWhysAnalyzerRequest.objects.get_or_create( @@ -77,7 +118,7 @@ class FiveWhysAnalyzerProcessor(StandardWebhookProcessor): } # Send to webhook - response_data = self.make_request(chat_payload) + response_data = self.make_secure_webhook_request(chat_payload) # Process chat response (no wallet deduction) return self.process_chat_response(response_data, request_obj, user_message) @@ -95,7 +136,8 @@ class FiveWhysAnalyzerProcessor(StandardWebhookProcessor): try: agent = BaseAgent.objects.get(slug=self.agent_slug) except BaseAgent.DoesNotExist: - raise Exception(f"Agent with slug '{self.agent_slug}' not found") + logger.error(f"Agent with slug '{self.agent_slug}' not found") + raise Exception("Service configuration error") # Get existing session or create new one try: @@ -134,7 +176,7 @@ class FiveWhysAnalyzerProcessor(StandardWebhookProcessor): } # Send to webhook - response_data = self.make_request(report_payload) + response_data = self.make_secure_webhook_request(report_payload) # Process report response (with wallet deduction) return self.process_report_response(response_data, request_obj) @@ -180,9 +222,10 @@ class FiveWhysAnalyzerProcessor(StandardWebhookProcessor): return response_obj except Exception as e: + logger.error(f"Failed to process chat response: {str(e)}") request_obj.status = 'failed' request_obj.save() - raise Exception(f"Failed to process chat response: {e}") + raise Exception("Chat processing failed") def process_report_response(self, response_data, request_obj): """Process report generation response - deduct wallet after success""" @@ -238,9 +281,10 @@ class FiveWhysAnalyzerProcessor(StandardWebhookProcessor): return response_obj except Exception as e: + logger.error(f"Failed to process report response: {str(e)}") request_obj.status = 'failed' request_obj.save() - raise Exception(f"Failed to process report response: {e}") + raise Exception("Report generation failed") def prepare_message_text(self, **kwargs): """Legacy method for compatibility""" diff --git a/five_whys_analyzer/templates/five_whys_analyzer/detail.html b/five_whys_analyzer/templates/five_whys_analyzer/detail.html index 6c434d1..56e8da2 100644 --- a/five_whys_analyzer/templates/five_whys_analyzer/detail.html +++ b/five_whys_analyzer/templates/five_whys_analyzer/detail.html @@ -423,7 +423,19 @@ }); function generateSessionId() { - return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + // Use crypto.randomUUID() if available, fallback to secure random generation + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } else { + // Fallback for older browsers - generate cryptographically secure random string + const array = new Uint8Array(16); + crypto.getRandomValues(array); + return 'session_' + Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); + } + } + + function getCsrfToken() { + return document.querySelector('[name=csrfmiddlewaretoken]').value; } function sendChatMessage() { @@ -453,7 +465,8 @@ fetch("{% url 'five_whys_analyzer:chat' %}", { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken() }, body: JSON.stringify({ session_id: currentSessionId, @@ -663,7 +676,8 @@ fetch("{% url 'five_whys_analyzer:report' %}", { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken() }, body: JSON.stringify({ session_id: currentSessionId, diff --git a/five_whys_analyzer/views.py b/five_whys_analyzer/views.py index 5258281..f033a8a 100644 --- a/five_whys_analyzer/views.py +++ b/five_whys_analyzer/views.py @@ -2,7 +2,8 @@ from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from django.contrib import messages from django.http import JsonResponse -from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.csrf import csrf_protect +from django.middleware.csrf import get_token from django.utils.decorators import method_decorator from django.views import View from agent_base.models import BaseAgent @@ -10,6 +11,69 @@ from .models import FiveWhysAnalyzerRequest, FiveWhysAnalyzerResponse from .processor import FiveWhysAnalyzerProcessor import json import uuid +import logging + +logger = logging.getLogger(__name__) + +# Constants for input validation +MAX_MESSAGE_LENGTH = 5000 +MAX_PROBLEM_STATEMENT_LENGTH = 2000 +MAX_CONTEXT_LENGTH = 3000 +ALLOWED_ANALYSIS_DEPTHS = ['standard', 'detailed', 'comprehensive'] + + +def validate_input_data(data, validation_type="chat"): + """Validate and sanitize input data""" + errors = [] + + if validation_type == "chat": + message = data.get('message', '').strip() + if not message: + errors.append("Message cannot be empty") + elif len(message) > MAX_MESSAGE_LENGTH: + errors.append(f"Message too long (max {MAX_MESSAGE_LENGTH} characters)") + + # Basic HTML/script tag detection + if ' MAX_PROBLEM_STATEMENT_LENGTH: + errors.append(f"Problem statement too long (max {MAX_PROBLEM_STATEMENT_LENGTH} characters)") + + if context_info and len(context_info) > MAX_CONTEXT_LENGTH: + errors.append(f"Context information too long (max {MAX_CONTEXT_LENGTH} characters)") + + if analysis_depth not in ALLOWED_ANALYSIS_DEPTHS: + errors.append("Invalid analysis depth") + + return errors + + +def get_safe_error_response(error, request_type="request"): + """Return sanitized error message for production""" + logger.error(f"5 Whys Analyzer {request_type} error: {str(error)}") + + # Return generic error messages in production + if hasattr(error, '__class__'): + error_type = error.__class__.__name__ + if 'DoesNotExist' in error_type: + return 'Resource not found' + elif 'ValidationError' in error_type: + return 'Invalid input provided' + elif 'PermissionDenied' in error_type: + return 'Access denied' + elif 'IntegrityError' in error_type: + return 'Data conflict occurred' + + # Generic fallback + return 'An error occurred while processing your request' @login_required @@ -41,7 +105,6 @@ def five_whys_analyzer_detail(request): return render(request, 'five_whys_analyzer/detail.html', context) -@method_decorator(csrf_exempt, name='dispatch') class FiveWhysAnalyzerChatView(View): """Handle chat messages - free interactions""" @@ -53,13 +116,15 @@ class FiveWhysAnalyzerChatView(View): # Parse request data data = json.loads(request.body) + # Validate input data + validation_errors = validate_input_data(data, "chat") + if validation_errors: + return JsonResponse({'error': '; '.join(validation_errors)}, status=400) + # Get session ID or create new one session_id = data.get('session_id', str(uuid.uuid4())) user_message = data.get('message', '').strip() - if not user_message: - return JsonResponse({'error': 'Message cannot be empty'}, status=400) - # Process chat message (no wallet deduction) processor = FiveWhysAnalyzerProcessor() result = processor.handle_chat_message( @@ -72,15 +137,14 @@ class FiveWhysAnalyzerChatView(View): 'success': True, 'session_id': session_id, 'response': result.chat_response, - 'message_type': 'chat', - 'wallet_balance': float(request.user.wallet_balance) # No change expected + 'message_type': 'chat' }) except Exception as e: - return JsonResponse({'error': str(e)}, status=500) + error_message = get_safe_error_response(e, "chat") + return JsonResponse({'error': error_message}, status=500) -@method_decorator(csrf_exempt, name='dispatch') class FiveWhysAnalyzerReportView(View): """Generate final report - paid interaction""" @@ -92,6 +156,11 @@ class FiveWhysAnalyzerReportView(View): # Parse request data data = json.loads(request.body) + # Validate input data + validation_errors = validate_input_data(data, "report") + if validation_errors: + return JsonResponse({'error': '; '.join(validation_errors)}, status=400) + # Get report parameters session_id = data.get('session_id') problem_statement = data.get('problem_statement', '').strip() @@ -101,9 +170,6 @@ class FiveWhysAnalyzerReportView(View): if not session_id: return JsonResponse({'error': 'Session ID required'}, status=400) - if not problem_statement: - return JsonResponse({'error': 'Problem statement required'}, status=400) - # Get agent for price checking agent = BaseAgent.objects.get(slug='five-whys-analyzer') @@ -134,9 +200,11 @@ class FiveWhysAnalyzerReportView(View): }) except BaseAgent.DoesNotExist: - return JsonResponse({'error': '5 Whys Analysis Agent not found'}, status=404) + logger.error("5 Whys Analysis Agent not found in database") + return JsonResponse({'error': 'Service temporarily unavailable'}, status=404) except Exception as e: - return JsonResponse({'error': str(e)}, status=500) + error_message = get_safe_error_response(e, "report") + return JsonResponse({'error': error_message}, status=500) @login_required @@ -169,13 +237,14 @@ def five_whys_analyzer_session(request, session_id): }) except FiveWhysAnalyzerRequest.DoesNotExist: + logger.warning(f"Session {session_id} not found for user {request.user.id}") return JsonResponse({'error': 'Session not found'}, status=404) except Exception as e: - return JsonResponse({'error': str(e)}, status=500) + error_message = get_safe_error_response(e, "session") + return JsonResponse({'error': error_message}, status=500) # Legacy view for compatibility -@method_decorator(csrf_exempt, name='dispatch') class FiveWhysAnalyzerProcessView(View): """Legacy process view - redirects to chat interface"""