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

504 lines
20 KiB
Python

"""
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()