|
| 1 | +# app/compliance/audit.py |
| 2 | +""" |
| 3 | +Comprehensive Compliance & Audit Module |
| 4 | +GDPR, HIPAA, SOC2 compliance with audit logging and data retention. |
| 5 | +""" |
| 6 | + |
| 7 | +from dataclasses import dataclass, field |
| 8 | +from datetime import datetime, timedelta |
| 9 | +from typing import Dict, List, Optional, Set |
| 10 | +from enum import Enum |
| 11 | +import uuid |
| 12 | + |
| 13 | + |
| 14 | +class ComplianceFramework(Enum): |
| 15 | + """Supported compliance frameworks.""" |
| 16 | + GDPR = "gdpr" # EU General Data Protection Regulation |
| 17 | + HIPAA = "hipaa" # Health Insurance Portability & Accountability Act |
| 18 | + SOC2 = "soc2" # Service Organization Control |
| 19 | + CCPA = "ccpa" # California Consumer Privacy Act |
| 20 | + PCI_DSS = "pci_dss" # Payment Card Industry Data Security Standard |
| 21 | + |
| 22 | + |
| 23 | +class AuditEventType(Enum): |
| 24 | + """Types of audit events.""" |
| 25 | + # Access events |
| 26 | + LOGIN = "login" |
| 27 | + LOGOUT = "logout" |
| 28 | + ACCESS_DENIED = "access_denied" |
| 29 | + PERMISSION_GRANTED = "permission_granted" |
| 30 | + |
| 31 | + # Data events |
| 32 | + DATA_CREATED = "data_created" |
| 33 | + DATA_READ = "data_read" |
| 34 | + DATA_MODIFIED = "data_modified" |
| 35 | + DATA_DELETED = "data_deleted" |
| 36 | + DATA_EXPORTED = "data_exported" |
| 37 | + |
| 38 | + # Security events |
| 39 | + FAILED_AUTH = "failed_auth" |
| 40 | + SECURITY_ALERT = "security_alert" |
| 41 | + PRIVILEGE_ESCALATION = "privilege_escalation" |
| 42 | + |
| 43 | + # Compliance events |
| 44 | + CONSENT_GIVEN = "consent_given" |
| 45 | + CONSENT_WITHDRAWN = "consent_withdrawn" |
| 46 | + DATA_SUBJECT_REQUEST = "data_subject_request" |
| 47 | + POLICY_VIOLATION = "policy_violation" |
| 48 | + |
| 49 | + |
| 50 | +class DataClassification(Enum): |
| 51 | + """Data sensitivity classification.""" |
| 52 | + PUBLIC = "public" |
| 53 | + INTERNAL = "internal" |
| 54 | + CONFIDENTIAL = "confidential" |
| 55 | + RESTRICTED = "restricted" # PII, PHI, etc |
| 56 | + |
| 57 | + |
| 58 | +class RetentionPolicy(Enum): |
| 59 | + """Data retention policies.""" |
| 60 | + DELETE_ON_REQUEST = "delete_on_request" # GDPR - Right to be forgotten |
| 61 | + KEEP_FOR_30_DAYS = "keep_30_days" |
| 62 | + KEEP_FOR_90_DAYS = "keep_90_days" |
| 63 | + KEEP_FOR_1_YEAR = "keep_1_year" |
| 64 | + KEEP_FOR_7_YEARS = "keep_7_years" # HIPAA requirement |
| 65 | + KEEP_INDEFINITELY = "keep_indefinitely" |
| 66 | + |
| 67 | + |
| 68 | +@dataclass |
| 69 | +class AuditLog: |
| 70 | + """Audit log entry.""" |
| 71 | + audit_id: str = field(default_factory=lambda: str(uuid.uuid4())) |
| 72 | + timestamp: datetime = field(default_factory=datetime.utcnow) |
| 73 | + event_type: AuditEventType = AuditEventType.LOGIN |
| 74 | + user_id: str = "" |
| 75 | + resource: str = "" |
| 76 | + action: str = "" |
| 77 | + status: str = "success" # success, failure |
| 78 | + ip_address: str = "" |
| 79 | + user_agent: str = "" |
| 80 | + details: Dict = field(default_factory=dict) |
| 81 | + |
| 82 | + def to_dict(self) -> Dict: |
| 83 | + return { |
| 84 | + 'audit_id': self.audit_id, |
| 85 | + 'timestamp': self.timestamp.isoformat(), |
| 86 | + 'event_type': self.event_type.value, |
| 87 | + 'user_id': self.user_id, |
| 88 | + 'resource': self.resource, |
| 89 | + 'action': self.action, |
| 90 | + 'status': self.status, |
| 91 | + 'details': self.details |
| 92 | + } |
| 93 | + |
| 94 | + |
| 95 | +@dataclass |
| 96 | +class DataSubjectRight: |
| 97 | + """GDPR data subject rights request.""" |
| 98 | + request_id: str = field(default_factory=lambda: str(uuid.uuid4())) |
| 99 | + user_id: str = "" |
| 100 | + request_type: str = "" # access, deletion, portability, rectification |
| 101 | + status: str = "pending" # pending, in_progress, completed, denied |
| 102 | + requested_at: datetime = field(default_factory=datetime.utcnow) |
| 103 | + response_deadline: datetime = field(default_factory=lambda: datetime.utcnow() + timedelta(days=30)) |
| 104 | + completed_at: Optional[datetime] = None |
| 105 | + reason: str = "" |
| 106 | + |
| 107 | + def to_dict(self) -> Dict: |
| 108 | + return { |
| 109 | + 'request_id': self.request_id, |
| 110 | + 'user_id': self.user_id, |
| 111 | + 'request_type': self.request_type, |
| 112 | + 'status': self.status, |
| 113 | + 'requested_at': self.requested_at.isoformat(), |
| 114 | + 'response_deadline': self.response_deadline.isoformat(), |
| 115 | + 'completed_at': self.completed_at.isoformat() if self.completed_at else None |
| 116 | + } |
| 117 | + |
| 118 | + |
| 119 | +@dataclass |
| 120 | +class ConsentRecord: |
| 121 | + """User consent record for GDPR.""" |
| 122 | + consent_id: str = field(default_factory=lambda: str(uuid.uuid4())) |
| 123 | + user_id: str = "" |
| 124 | + purpose: str = "" # marketing, analytics, processing, etc |
| 125 | + given_at: datetime = field(default_factory=datetime.utcnow) |
| 126 | + withdrawn_at: Optional[datetime] = None |
| 127 | + version: str = "1.0" |
| 128 | + ip_address: str = "" |
| 129 | + |
| 130 | + def is_active(self) -> bool: |
| 131 | + """Check if consent is currently active.""" |
| 132 | + return self.withdrawn_at is None |
| 133 | + |
| 134 | + def to_dict(self) -> Dict: |
| 135 | + return { |
| 136 | + 'consent_id': self.consent_id, |
| 137 | + 'user_id': self.user_id, |
| 138 | + 'purpose': self.purpose, |
| 139 | + 'given_at': self.given_at.isoformat(), |
| 140 | + 'withdrawn_at': self.withdrawn_at.isoformat() if self.withdrawn_at else None, |
| 141 | + 'active': self.is_active() |
| 142 | + } |
| 143 | + |
| 144 | + |
| 145 | +class ComplianceAuditEngine: |
| 146 | + """ |
| 147 | + Manages compliance frameworks, audit logging, and data subject rights. |
| 148 | + """ |
| 149 | + |
| 150 | + def __init__(self): |
| 151 | + """Initialize compliance engine.""" |
| 152 | + self.audit_logs: List[AuditLog] = [] |
| 153 | + self.data_subject_requests: Dict[str, DataSubjectRight] = {} |
| 154 | + self.consent_records: Dict[str, ConsentRecord] = {} |
| 155 | + self.retention_policies: Dict[str, RetentionPolicy] = self._default_policies() |
| 156 | + self.enabled_frameworks: Set[ComplianceFramework] = { |
| 157 | + ComplianceFramework.GDPR, |
| 158 | + ComplianceFramework.HIPAA, |
| 159 | + ComplianceFramework.SOC2 |
| 160 | + } |
| 161 | + |
| 162 | + def _default_policies(self) -> Dict[str, RetentionPolicy]: |
| 163 | + """Default data retention policies.""" |
| 164 | + return { |
| 165 | + 'audit_logs': RetentionPolicy.KEEP_FOR_7_YEARS, |
| 166 | + 'user_data': RetentionPolicy.DELETE_ON_REQUEST, |
| 167 | + 'medical_records': RetentionPolicy.KEEP_FOR_7_YEARS, |
| 168 | + 'financial_records': RetentionPolicy.KEEP_FOR_7_YEARS, |
| 169 | + 'consent_records': RetentionPolicy.KEEP_FOR_7_YEARS, |
| 170 | + 'activity_logs': RetentionPolicy.KEEP_FOR_90_DAYS |
| 171 | + } |
| 172 | + |
| 173 | + def log_event(self, event_type: AuditEventType, user_id: str, |
| 174 | + resource: str, action: str, status: str = "success", |
| 175 | + ip_address: str = "", details: Dict = None) -> AuditLog: |
| 176 | + """Log audit event.""" |
| 177 | + log = AuditLog( |
| 178 | + event_type=event_type, |
| 179 | + user_id=user_id, |
| 180 | + resource=resource, |
| 181 | + action=action, |
| 182 | + status=status, |
| 183 | + ip_address=ip_address, |
| 184 | + details=details or {} |
| 185 | + ) |
| 186 | + |
| 187 | + self.audit_logs.append(log) |
| 188 | + return log |
| 189 | + |
| 190 | + def log_data_access(self, user_id: str, resource: str, |
| 191 | + data_type: str, classification: DataClassification) -> AuditLog: |
| 192 | + """Log data access with classification.""" |
| 193 | + return self.log_event( |
| 194 | + AuditEventType.DATA_READ, |
| 195 | + user_id, |
| 196 | + resource, |
| 197 | + 'data_access', |
| 198 | + details={ |
| 199 | + 'data_type': data_type, |
| 200 | + 'classification': classification.value |
| 201 | + } |
| 202 | + ) |
| 203 | + |
| 204 | + def log_data_deletion(self, user_id: str, resource: str, |
| 205 | + reason: str = "") -> AuditLog: |
| 206 | + """Log data deletion.""" |
| 207 | + return self.log_event( |
| 208 | + AuditEventType.DATA_DELETED, |
| 209 | + user_id, |
| 210 | + resource, |
| 211 | + 'data_deletion', |
| 212 | + details={'reason': reason} |
| 213 | + ) |
| 214 | + |
| 215 | + def request_data_subject_right(self, user_id: str, request_type: str, |
| 216 | + reason: str = "") -> DataSubjectRight: |
| 217 | + """Request GDPR data subject right (access, deletion, portability).""" |
| 218 | + request = DataSubjectRight( |
| 219 | + user_id=user_id, |
| 220 | + request_type=request_type, |
| 221 | + reason=reason |
| 222 | + ) |
| 223 | + |
| 224 | + self.data_subject_requests[request.request_id] = request |
| 225 | + |
| 226 | + # Log the request |
| 227 | + self.log_event( |
| 228 | + AuditEventType.DATA_SUBJECT_REQUEST, |
| 229 | + user_id, |
| 230 | + 'user_account', |
| 231 | + request_type, |
| 232 | + details={ |
| 233 | + 'request_id': request.request_id, |
| 234 | + 'deadline_days': 30 |
| 235 | + } |
| 236 | + ) |
| 237 | + |
| 238 | + return request |
| 239 | + |
| 240 | + def complete_data_subject_request(self, request_id: str) -> bool: |
| 241 | + """Mark data subject request as completed.""" |
| 242 | + if request_id not in self.data_subject_requests: |
| 243 | + return False |
| 244 | + |
| 245 | + request = self.data_subject_requests[request_id] |
| 246 | + request.status = 'completed' |
| 247 | + request.completed_at = datetime.utcnow() |
| 248 | + |
| 249 | + return True |
| 250 | + |
| 251 | + def give_consent(self, user_id: str, purpose: str, |
| 252 | + ip_address: str = "") -> ConsentRecord: |
| 253 | + """Record user consent (GDPR).""" |
| 254 | + consent = ConsentRecord( |
| 255 | + user_id=user_id, |
| 256 | + purpose=purpose, |
| 257 | + ip_address=ip_address |
| 258 | + ) |
| 259 | + |
| 260 | + self.consent_records[consent.consent_id] = consent |
| 261 | + |
| 262 | + # Log consent |
| 263 | + self.log_event( |
| 264 | + AuditEventType.CONSENT_GIVEN, |
| 265 | + user_id, |
| 266 | + 'consent_management', |
| 267 | + f'consent_given_{purpose}', |
| 268 | + details={'purpose': purpose} |
| 269 | + ) |
| 270 | + |
| 271 | + return consent |
| 272 | + |
| 273 | + def withdraw_consent(self, user_id: str, purpose: str) -> bool: |
| 274 | + """Withdraw consent.""" |
| 275 | + # Find and withdraw matching consent records |
| 276 | + withdrawn = False |
| 277 | + for consent in self.consent_records.values(): |
| 278 | + if consent.user_id == user_id and consent.purpose == purpose and consent.is_active(): |
| 279 | + consent.withdrawn_at = datetime.utcnow() |
| 280 | + withdrawn = True |
| 281 | + |
| 282 | + if withdrawn: |
| 283 | + self.log_event( |
| 284 | + AuditEventType.CONSENT_WITHDRAWN, |
| 285 | + user_id, |
| 286 | + 'consent_management', |
| 287 | + f'consent_withdrawn_{purpose}', |
| 288 | + details={'purpose': purpose} |
| 289 | + ) |
| 290 | + |
| 291 | + return withdrawn |
| 292 | + |
| 293 | + def check_consent(self, user_id: str, purpose: str) -> bool: |
| 294 | + """Check if user has active consent for purpose.""" |
| 295 | + for consent in self.consent_records.values(): |
| 296 | + if consent.user_id == user_id and consent.purpose == purpose: |
| 297 | + return consent.is_active() |
| 298 | + return False |
| 299 | + |
| 300 | + def get_user_audit_trail(self, user_id: str, limit: int = 100) -> List[Dict]: |
| 301 | + """Get audit log for specific user.""" |
| 302 | + logs = [log for log in self.audit_logs if log.user_id == user_id] |
| 303 | + logs.sort(key=lambda x: x.timestamp, reverse=True) |
| 304 | + return [log.to_dict() for log in logs[:limit]] |
| 305 | + |
| 306 | + def get_audit_logs(self, event_type: AuditEventType = None, |
| 307 | + start_date: datetime = None, |
| 308 | + end_date: datetime = None, |
| 309 | + limit: int = 1000) -> List[Dict]: |
| 310 | + """Get filtered audit logs.""" |
| 311 | + logs = self.audit_logs |
| 312 | + |
| 313 | + if event_type: |
| 314 | + logs = [log for log in logs if log.event_type == event_type] |
| 315 | + |
| 316 | + if start_date: |
| 317 | + logs = [log for log in logs if log.timestamp >= start_date] |
| 318 | + |
| 319 | + if end_date: |
| 320 | + logs = [log for log in logs if log.timestamp <= end_date] |
| 321 | + |
| 322 | + logs.sort(key=lambda x: x.timestamp, reverse=True) |
| 323 | + return [log.to_dict() for log in logs[:limit]] |
| 324 | + |
| 325 | + def cleanup_expired_data(self) -> Dict: |
| 326 | + """Clean up expired data based on retention policies.""" |
| 327 | + before_count = len(self.audit_logs) |
| 328 | + cutoff_date = datetime.utcnow() - timedelta(days=2555) # 7 years |
| 329 | + |
| 330 | + self.audit_logs = [log for log in self.audit_logs if log.timestamp > cutoff_date] |
| 331 | + |
| 332 | + deleted_count = before_count - len(self.audit_logs) |
| 333 | + |
| 334 | + return { |
| 335 | + 'deleted_logs': deleted_count, |
| 336 | + 'remaining_logs': len(self.audit_logs), |
| 337 | + 'cleanup_date': datetime.utcnow().isoformat() |
| 338 | + } |
| 339 | + |
| 340 | + def generate_compliance_report(self, framework: ComplianceFramework) -> Dict: |
| 341 | + """Generate compliance report.""" |
| 342 | + return { |
| 343 | + 'framework': framework.value, |
| 344 | + 'report_date': datetime.utcnow().isoformat(), |
| 345 | + 'total_events': len(self.audit_logs), |
| 346 | + 'data_subject_requests': len(self.data_subject_requests), |
| 347 | + 'active_consents': sum(1 for c in self.consent_records.values() if c.is_active()), |
| 348 | + 'security_events': sum(1 for log in self.audit_logs if 'security' in log.event_type.value), |
| 349 | + 'failed_authentications': sum(1 for log in self.audit_logs if log.event_type == AuditEventType.FAILED_AUTH), |
| 350 | + 'access_denials': sum(1 for log in self.audit_logs if log.event_type == AuditEventType.ACCESS_DENIED), |
| 351 | + 'data_deletions': sum(1 for log in self.audit_logs if log.event_type == AuditEventType.DATA_DELETED), |
| 352 | + 'policy_violations': sum(1 for log in self.audit_logs if log.event_type == AuditEventType.POLICY_VIOLATION) |
| 353 | + } |
| 354 | + |
| 355 | + def get_stats(self) -> Dict: |
| 356 | + """Get compliance statistics.""" |
| 357 | + return { |
| 358 | + 'audit_logs': len(self.audit_logs), |
| 359 | + 'data_subject_requests': len(self.data_subject_requests), |
| 360 | + 'pending_requests': sum(1 for r in self.data_subject_requests.values() if r.status == 'pending'), |
| 361 | + 'consent_records': len(self.consent_records), |
| 362 | + 'active_consents': sum(1 for c in self.consent_records.values() if c.is_active()), |
| 363 | + 'enabled_frameworks': [f.value for f in self.enabled_frameworks], |
| 364 | + 'recent_7_days': sum(1 for log in self.audit_logs |
| 365 | + if log.timestamp > datetime.utcnow() - timedelta(days=7)) |
| 366 | + } |
| 367 | + |
| 368 | + |
| 369 | +# Global compliance audit engine |
| 370 | +compliance_engine = ComplianceAuditEngine() |
0 commit comments