Updates
This commit is contained in:
BIN
ETB-API/security/services/__pycache__/zero_trust.cpython-312.pyc
Normal file
BIN
ETB-API/security/services/__pycache__/zero_trust.cpython-312.pyc
Normal file
Binary file not shown.
503
ETB-API/security/services/zero_trust.py
Normal file
503
ETB-API/security/services/zero_trust.py
Normal file
@@ -0,0 +1,503 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user