504 lines
20 KiB
Python
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()
|