""" Zero Trust Architecture Service Integrates device posture, geolocation, risk assessment, and adaptive authentication """ import logging import requests from typing import Dict, Any, Optional, List, Tuple from datetime import datetime, timedelta from django.conf import settings from django.utils import timezone from django.contrib.auth import get_user_model from ..models import ( User, DevicePosture, GeolocationRule, AccessPolicy, RiskAssessment, AdaptiveAuthentication, UserBehaviorProfile, AuditLog ) logger = logging.getLogger(__name__) User = get_user_model() class ZeroTrustService: """Main Zero Trust service for comprehensive security decisions""" def __init__(self): self.geo_api_key = getattr(settings, 'GEO_API_KEY', None) self.risk_thresholds = { 'low': 25, 'medium': 50, 'high': 75, 'critical': 100 } def assess_access_request(self, user: User, request_data: Dict[str, Any]) -> Dict[str, Any]: """ Comprehensive access assessment using Zero Trust principles Args: user: User requesting access request_data: Request context including IP, user agent, etc. Returns: Dict with access decision and required actions """ try: # Collect context data context = self._collect_context_data(user, request_data) # Perform risk assessment risk_assessment = self._perform_risk_assessment(user, context) # Evaluate access policies policy_result = self._evaluate_access_policies(user, context) # Determine adaptive authentication requirements auth_requirements = self._determine_auth_requirements(risk_assessment, context) # Make final access decision decision = self._make_access_decision( risk_assessment, policy_result, auth_requirements, context ) # Log the assessment self._log_access_assessment(user, context, decision) return decision except Exception as e: logger.error(f"Zero Trust assessment failed: {e}") return { 'access_granted': False, 'reason': 'Assessment failed', 'required_actions': ['MANUAL_REVIEW'], 'risk_level': 'HIGH', 'error': str(e) } def _collect_context_data(self, user: User, request_data: Dict[str, Any]) -> Dict[str, Any]: """Collect comprehensive context data for assessment""" context = { 'user': user, 'ip_address': request_data.get('ip_address'), 'user_agent': request_data.get('user_agent'), 'timestamp': timezone.now(), 'request_data': request_data } # Get device posture device_id = request_data.get('device_id') if device_id: try: device_posture = DevicePosture.objects.get( device_id=device_id, user=user, is_active=True ) context['device_posture'] = device_posture except DevicePosture.DoesNotExist: context['device_posture'] = None # Get geolocation data if context['ip_address']: location_data = self._get_geolocation_data(context['ip_address']) context['location'] = location_data # Get user behavior profile try: behavior_profile = UserBehaviorProfile.objects.get(user=user) context['behavior_profile'] = behavior_profile except UserBehaviorProfile.DoesNotExist: context['behavior_profile'] = None return context def _get_geolocation_data(self, ip_address: str) -> Dict[str, Any]: """Get geolocation data for IP address""" try: # Use external geolocation service (e.g., ipapi.co, ipinfo.io) if self.geo_api_key: response = requests.get( f'https://ipapi.co/{ip_address}/json/', params={'key': self.geo_api_key}, timeout=5 ) if response.status_code == 200: data = response.json() return { 'latitude': data.get('latitude'), 'longitude': data.get('longitude'), 'country_code': data.get('country_code'), 'region': data.get('region'), 'city': data.get('city'), 'timezone': data.get('timezone'), 'isp': data.get('org'), 'ip_address': ip_address } except Exception as e: logger.warning(f"Geolocation lookup failed for {ip_address}: {e}") return {'ip_address': ip_address} def _perform_risk_assessment(self, user: User, context: Dict[str, Any]) -> RiskAssessment: """Perform comprehensive risk assessment""" assessment = RiskAssessment( user=user, assessment_type='ACCESS', ip_address=context.get('ip_address'), user_agent=context.get('user_agent'), location_data=context.get('location', {}), device_data=context.get('device_posture', {}), behavior_data=context.get('behavior_profile', {}) ) # Calculate individual risk scores assessment.device_risk_score = self._calculate_device_risk(context) assessment.location_risk_score = self._calculate_location_risk(context) assessment.behavior_risk_score = self._calculate_behavior_risk(context) assessment.network_risk_score = self._calculate_network_risk(context) assessment.time_risk_score = self._calculate_time_risk(context) assessment.user_risk_score = self._calculate_user_risk(user, context) # Calculate overall risk assessment.calculate_overall_risk() assessment.determine_access_decision() # Save assessment assessment.save() return assessment def _calculate_device_risk(self, context: Dict[str, Any]) -> int: """Calculate device-based risk score""" device_posture = context.get('device_posture') if not device_posture: return 80 # High risk for unknown devices # Use device posture risk score return device_posture.risk_score def _calculate_location_risk(self, context: Dict[str, Any]) -> int: """Calculate location-based risk score""" location = context.get('location', {}) if not location.get('latitude') or not location.get('longitude'): return 50 # Medium risk for unknown location # Check against geolocation rules rules = GeolocationRule.objects.filter(is_active=True).order_by('priority') for rule in rules: result = rule.evaluate_location(**location) if result['matches']: if rule.rule_type == 'DENY': return 100 # Critical risk elif rule.rule_type == 'REQUIRE_MFA': return 60 # High risk elif rule.rule_type == 'RESTRICT': return 40 # Medium risk # Default location risk based on country/IP country_code = location.get('country_code') if country_code in ['CN', 'RU', 'KP', 'IR']: # High-risk countries return 70 elif country_code in ['US', 'CA', 'GB', 'DE', 'FR', 'BG']: # Trusted countries return 20 else: return 40 # Medium risk for other countries def _calculate_behavior_risk(self, context: Dict[str, Any]) -> int: """Calculate behavior-based risk score""" behavior_profile = context.get('behavior_profile') if not behavior_profile: return 30 # Low risk for new users (learning period) # Calculate anomaly score current_behavior = { 'login_time': context.get('timestamp'), 'location': context.get('location'), 'device_id': context.get('device_posture', {}).get('device_id'), 'ip_address': context.get('ip_address') } anomaly_score = behavior_profile.calculate_anomaly_score(current_behavior) # Convert anomaly score (0-1) to risk score (0-100) return int(anomaly_score * 100) def _calculate_network_risk(self, context: Dict[str, Any]) -> int: """Calculate network-based risk score""" device_posture = context.get('device_posture') location = context.get('location', {}) risk = 0 # VPN connection if device_posture and device_posture.vpn_connected: risk -= 20 # Reduce risk for VPN # Network type if device_posture: if device_posture.network_type == 'Public': risk += 40 elif device_posture.network_type == 'Home': risk += 20 elif device_posture.network_type == 'Corporate': risk -= 10 # ISP reputation (simplified) isp = location.get('isp', '').lower() if any(keyword in isp for keyword in ['tor', 'proxy', 'vpn']): risk += 30 return max(0, min(100, risk)) def _calculate_time_risk(self, context: Dict[str, Any]) -> int: """Calculate time-based risk score""" timestamp = context.get('timestamp', timezone.now()) hour = timestamp.hour weekday = timestamp.weekday() # Business hours (9 AM - 5 PM, Monday-Friday) if 9 <= hour <= 17 and weekday < 5: return 10 # Low risk during business hours elif 6 <= hour <= 22 and weekday < 5: return 30 # Medium risk during extended hours else: return 60 # High risk during unusual hours def _calculate_user_risk(self, user: User, context: Dict[str, Any]) -> int: """Calculate user-based risk score""" risk = 0 # Account age account_age = (timezone.now() - user.date_joined).days if account_age < 7: risk += 30 # New accounts are riskier elif account_age > 365: risk -= 10 # Established accounts are less risky # Failed login attempts if user.failed_login_attempts > 3: risk += 40 elif user.failed_login_attempts > 0: risk += 20 # Account lock status if user.is_account_locked(): risk += 50 # MFA status if not user.mfa_enabled: risk += 20 # Clearance level if user.clearance_level and user.clearance_level.level >= 4: risk -= 10 # High clearance users are less risky return max(0, min(100, risk)) def _evaluate_access_policies(self, user: User, context: Dict[str, Any]) -> Dict[str, Any]: """Evaluate all applicable access policies""" policies = AccessPolicy.objects.filter(is_active=True).order_by('priority') for policy in policies: result = policy.evaluate(user, context=context) if result['required_actions'] or not result['allowed']: return result return {'allowed': True, 'required_actions': []} def _determine_auth_requirements(self, risk_assessment: RiskAssessment, context: Dict[str, Any]) -> List[str]: """Determine required authentication methods based on risk""" try: adaptive_auth = AdaptiveAuthentication.objects.filter(is_active=True).first() if not adaptive_auth: return ['PASSWORD'] # Default to password only # Get required auth methods based on risk score required_methods = adaptive_auth.get_required_auth_methods(risk_assessment.overall_risk_score) # Adjust based on context adjusted_risk = adaptive_auth.calculate_adjusted_risk( risk_assessment.overall_risk_score, context ) # If adjusted risk is higher, require additional methods if adjusted_risk > risk_assessment.overall_risk_score: if 'MFA_TOTP' not in required_methods: required_methods.append('MFA_TOTP') return required_methods except Exception as e: logger.error(f"Failed to determine auth requirements: {e}") return ['PASSWORD', 'MFA_TOTP'] # Default to password + MFA def _make_access_decision(self, risk_assessment: RiskAssessment, policy_result: Dict[str, Any], auth_requirements: List[str], context: Dict[str, Any]) -> Dict[str, Any]: """Make final access decision based on all factors""" decision = { 'access_granted': False, 'reason': '', 'required_actions': [], 'risk_level': risk_assessment.risk_level, 'risk_score': risk_assessment.overall_risk_score, 'auth_requirements': auth_requirements, 'assessment_id': str(risk_assessment.id) } # Check policy restrictions first if not policy_result.get('allowed', True): decision['reason'] = policy_result.get('reason', 'Policy restriction') decision['required_actions'] = policy_result.get('required_actions', []) return decision # Check risk-based decision if risk_assessment.access_decision == 'DENY': decision['reason'] = risk_assessment.decision_reason decision['required_actions'] = ['MANUAL_REVIEW'] return decision elif risk_assessment.access_decision == 'REVIEW': decision['reason'] = risk_assessment.decision_reason decision['required_actions'] = ['MANUAL_REVIEW'] return decision elif risk_assessment.access_decision == 'STEP_UP': decision['access_granted'] = True decision['reason'] = risk_assessment.decision_reason decision['required_actions'] = ['STEP_UP_AUTH'] return decision # Low risk - allow access decision['access_granted'] = True decision['reason'] = 'Access granted - low risk' # Add any required actions from policies if policy_result.get('required_actions'): decision['required_actions'].extend(policy_result['required_actions']) return decision def _log_access_assessment(self, user: User, context: Dict[str, Any], decision: Dict[str, Any]): """Log the access assessment for audit purposes""" try: AuditLog.objects.create( user=user, action_type='ACCESS_ASSESSMENT', resource_type='Zero Trust Assessment', resource_id=decision.get('assessment_id', ''), ip_address=context.get('ip_address'), user_agent=context.get('user_agent'), details={ 'risk_level': decision.get('risk_level'), 'risk_score': decision.get('risk_score'), 'access_granted': decision.get('access_granted'), 'required_actions': decision.get('required_actions'), 'auth_requirements': decision.get('auth_requirements'), 'device_id': context.get('device_posture', {}).get('device_id'), 'location': context.get('location', {}) }, severity='HIGH' if decision.get('risk_level') in ['HIGH', 'CRITICAL'] else 'MEDIUM' ) except Exception as e: logger.error(f"Failed to log access assessment: {e}") def register_device(self, user: User, device_data: Dict[str, Any]) -> DevicePosture: """Register a new device for Zero Trust assessment""" device_id = device_data.get('device_id') if not device_id: raise ValueError("Device ID is required") # Create or update device posture device_posture, created = DevicePosture.objects.get_or_create( device_id=device_id, user=user, defaults={ 'device_name': device_data.get('device_name', ''), 'device_type': device_data.get('device_type', 'UNKNOWN'), 'os_type': device_data.get('os_type', 'UNKNOWN'), 'os_version': device_data.get('os_version', ''), 'browser_info': device_data.get('browser_info', ''), 'is_managed': device_data.get('is_managed', False), 'has_antivirus': device_data.get('has_antivirus', False), 'firewall_enabled': device_data.get('firewall_enabled', False), 'encryption_enabled': device_data.get('encryption_enabled', False), 'screen_lock_enabled': device_data.get('screen_lock_enabled', False), 'biometric_auth': device_data.get('biometric_auth', False), 'ip_address': device_data.get('ip_address'), 'network_type': device_data.get('network_type', ''), 'vpn_connected': device_data.get('vpn_connected', False) } ) if not created: # Update existing device for field, value in device_data.items(): if hasattr(device_posture, field): setattr(device_posture, field, value) # Calculate risk score and update trust level device_posture.risk_score = device_posture.calculate_risk_score() device_posture.update_trust_level() device_posture.save() # Log device registration AuditLog.objects.create( user=user, action_type='DEVICE_REGISTERED', resource_type='Device', resource_id=str(device_posture.id), details={ 'device_id': device_id, 'device_type': device_posture.device_type, 'trust_level': device_posture.trust_level, 'risk_score': device_posture.risk_score, 'is_compliant': device_posture.is_compliant }, severity='MEDIUM' ) return device_posture def update_behavior_profile(self, user: User, behavior_data: Dict[str, Any]): """Update user behavior profile for anomaly detection""" try: profile, created = UserBehaviorProfile.objects.get_or_create(user=user) # Update behavior patterns (simplified) current_time = behavior_data.get('timestamp', timezone.now()) current_location = behavior_data.get('location', {}) current_device = behavior_data.get('device_id') current_ip = behavior_data.get('ip_address') # Add to typical patterns (in production, this would be more sophisticated) if current_time not in profile.typical_login_times: profile.typical_login_times.append(current_time.isoformat()) if current_location and current_location not in profile.typical_login_locations: profile.typical_login_locations.append(current_location) if current_device and current_device not in profile.typical_login_devices: profile.typical_login_devices.append(current_device) if current_ip and current_ip not in profile.typical_ip_ranges: profile.typical_ip_ranges.append(current_ip) # Update sample count profile.sample_count += 1 # Check if learning period is complete if profile.is_learning and profile.sample_count >= 30: profile.is_learning = False profile.learning_complete_date = timezone.now() profile.save() except Exception as e: logger.error(f"Failed to update behavior profile: {e}") # Global instance zero_trust_service = ZeroTrustService()