Spaces:
Paused
Paused
Upload app/routers/compliance.py with huggingface_hub
Browse files- app/routers/compliance.py +1 -248
app/routers/compliance.py
CHANGED
|
@@ -1,248 +1 @@
|
|
| 1 |
-
|
| 2 |
-
from typing import Any, Dict, List, Optional
|
| 3 |
-
|
| 4 |
-
from fastapi import APIRouter, Depends, HTTPException
|
| 5 |
-
from pydantic import BaseModel
|
| 6 |
-
from sqlalchemy.orm import Session
|
| 7 |
-
|
| 8 |
-
from app.services.infrastructure.auth_service import auth_service
|
| 9 |
-
from core.database import (
|
| 10 |
-
AccessReview,
|
| 11 |
-
ComplianceAuditLog,
|
| 12 |
-
FraudAlert,
|
| 13 |
-
RegulatoryReport,
|
| 14 |
-
SecurityIncident,
|
| 15 |
-
TrainingRecord,
|
| 16 |
-
get_db,
|
| 17 |
-
)
|
| 18 |
-
|
| 19 |
-
router = APIRouter()
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
# Models matching Frontend Interfaces
|
| 23 |
-
class SystemMetrics(BaseModel):
|
| 24 |
-
uptime: float
|
| 25 |
-
response_time: float
|
| 26 |
-
error_rate: float
|
| 27 |
-
active_users: int
|
| 28 |
-
compliance_score: float
|
| 29 |
-
last_updated: str
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
class ComplianceAlert(BaseModel):
|
| 33 |
-
id: str
|
| 34 |
-
rule_id: str
|
| 35 |
-
message: str
|
| 36 |
-
severity: str
|
| 37 |
-
timestamp: str
|
| 38 |
-
acknowledged: bool
|
| 39 |
-
resolved: bool
|
| 40 |
-
metadata: Dict[str, Any]
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
class MonitoringDashboard(BaseModel):
|
| 44 |
-
system_health: SystemMetrics
|
| 45 |
-
active_alerts: List[ComplianceAlert]
|
| 46 |
-
recent_incidents: List[Any]
|
| 47 |
-
compliance_trends: List[Dict[str, Any]]
|
| 48 |
-
performance_metrics: Dict[str, Any]
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
class ComplianceMetrics(BaseModel):
|
| 52 |
-
recent_audit_events: int
|
| 53 |
-
pending_regulatory_reports: int
|
| 54 |
-
open_security_incidents: int
|
| 55 |
-
overdue_access_reviews: int
|
| 56 |
-
expiring_training_records: int
|
| 57 |
-
high_risk_events_last_100: int
|
| 58 |
-
overall_compliance_score: float
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
# --- Endpoints ---
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
@router.get("/dashboard", response_model=ComplianceMetrics)
|
| 65 |
-
async def get_compliance_dashboard(db: Session = Depends(get_db), current_user: Any = Depends(auth_service.get_current_user)):
|
| 66 |
-
"""Get high-level compliance metrics backed by real DB data"""
|
| 67 |
-
|
| 68 |
-
# 1. Recent Audit Events (Last 24h)
|
| 69 |
-
last_24h = datetime.utcnow() - timedelta(hours=24)
|
| 70 |
-
recent_audit_events = db.query(ComplianceAuditLog).filter(ComplianceAuditLog.timestamp >= last_24h).count()
|
| 71 |
-
|
| 72 |
-
# 2. Pending Regulatory Reports
|
| 73 |
-
pending_regulatory_reports = db.query(RegulatoryReport).filter(RegulatoryReport.filing_status.in_(["draft", "rejected"])).count()
|
| 74 |
-
|
| 75 |
-
# 3. Open Security Incidents
|
| 76 |
-
open_security_incidents = db.query(SecurityIncident).filter(SecurityIncident.status.in_(["open", "investigating"])).count()
|
| 77 |
-
|
| 78 |
-
# 4. Overdue Access Reviews
|
| 79 |
-
overdue_access_reviews = db.query(AccessReview).filter(AccessReview.review_status == "overdue").count()
|
| 80 |
-
|
| 81 |
-
# 5. Expiring Training Records (Next 30 days)
|
| 82 |
-
next_30d = datetime.utcnow() + timedelta(days=30)
|
| 83 |
-
expiring_training_records = (
|
| 84 |
-
db.query(TrainingRecord)
|
| 85 |
-
.filter(
|
| 86 |
-
TrainingRecord.expiry_date <= next_30d,
|
| 87 |
-
TrainingRecord.expiry_date >= datetime.utcnow(),
|
| 88 |
-
TrainingRecord.completion_status == "completed",
|
| 89 |
-
)
|
| 90 |
-
.count()
|
| 91 |
-
)
|
| 92 |
-
|
| 93 |
-
# 6. High Risk Events (Last 100 Audit Logs)
|
| 94 |
-
# Heuristic: risk_score > 75 in last 100 logs
|
| 95 |
-
last_100_logs = db.query(ComplianceAuditLog.risk_score).order_by(ComplianceAuditLog.timestamp.desc()).limit(100).all()
|
| 96 |
-
|
| 97 |
-
high_risk_events = sum(1 for log in last_100_logs if (log.risk_score or 0) > 75.0)
|
| 98 |
-
|
| 99 |
-
# 7. Calculate Overall Score
|
| 100 |
-
# Start at 100, deduct points for issues
|
| 101 |
-
score = 100.0
|
| 102 |
-
score -= open_security_incidents * 5.0
|
| 103 |
-
score -= overdue_access_reviews * 2.0
|
| 104 |
-
score -= pending_regulatory_reports * 1.0
|
| 105 |
-
score -= high_risk_events * 0.5
|
| 106 |
-
|
| 107 |
-
return {
|
| 108 |
-
"recent_audit_events": recent_audit_events,
|
| 109 |
-
"pending_regulatory_reports": pending_regulatory_reports,
|
| 110 |
-
"open_security_incidents": open_security_incidents,
|
| 111 |
-
"overdue_access_reviews": overdue_access_reviews,
|
| 112 |
-
"expiring_training_records": expiring_training_records,
|
| 113 |
-
"high_risk_events_last_100": high_risk_events,
|
| 114 |
-
"overall_compliance_score": max(0.0, min(100.0, score)),
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
@router.get("/monitoring/dashboard", response_model=MonitoringDashboard)
|
| 119 |
-
async def get_monitoring_dashboard(db: Session = Depends(get_db), current_user: Any = Depends(auth_service.get_current_user)):
|
| 120 |
-
"""Get real-time monitoring dashboard data linked to DB"""
|
| 121 |
-
|
| 122 |
-
# Calculate score again (reuse logic ideally, but replicating for independence)
|
| 123 |
-
open_incidents = db.query(SecurityIncident).filter(SecurityIncident.status.in_(["open", "investigating"])).count()
|
| 124 |
-
compliance_score = max(0.0, 100.0 - (open_incidents * 5.0))
|
| 125 |
-
|
| 126 |
-
# Active Alerts from FraudAlerts table
|
| 127 |
-
active_alerts_db = db.query(FraudAlert).filter(not FraudAlert.is_acknowledged).order_by(FraudAlert.created_at.desc()).limit(10).all()
|
| 128 |
-
|
| 129 |
-
active_alerts = [
|
| 130 |
-
{
|
| 131 |
-
"id": alert.id,
|
| 132 |
-
"rule_id": alert.alert_type,
|
| 133 |
-
"message": alert.title,
|
| 134 |
-
"severity": alert.severity,
|
| 135 |
-
"timestamp": alert.created_at.isoformat(),
|
| 136 |
-
"acknowledged": alert.is_acknowledged,
|
| 137 |
-
"resolved": False, # Basic mapping
|
| 138 |
-
"metadata": alert.alert_metadata or {},
|
| 139 |
-
}
|
| 140 |
-
for alert in active_alerts_db
|
| 141 |
-
]
|
| 142 |
-
|
| 143 |
-
return {
|
| 144 |
-
"system_health": {
|
| 145 |
-
"uptime": 99.98, # This usually comes from infrastructure monitoring, keeping static for app context
|
| 146 |
-
"response_time": 145, # Placeholder for APM data
|
| 147 |
-
"error_rate": 0.01,
|
| 148 |
-
"active_users": db.query(AccessReview).distinct(AccessReview.user_id).count() or 42, # Mock estimation
|
| 149 |
-
"compliance_score": compliance_score,
|
| 150 |
-
"last_updated": datetime.utcnow().isoformat(),
|
| 151 |
-
},
|
| 152 |
-
"active_alerts": active_alerts,
|
| 153 |
-
"recent_incidents": [], # Populate if needed from SecurityIncident
|
| 154 |
-
"compliance_trends": [{"period": "Last 7 days", "score": compliance_score, "alerts_count": len(active_alerts)}],
|
| 155 |
-
"performance_metrics": {"api_response_time": 145, "database_query_time": 22, "error_rate": 0.01},
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
@router.get("/regulatory-reports")
|
| 160 |
-
async def get_regulatory_reports(status: Optional[str] = None, db: Session = Depends(get_db)):
|
| 161 |
-
query = db.query(RegulatoryReport)
|
| 162 |
-
if status:
|
| 163 |
-
query = query.filter(RegulatoryReport.filing_status == status)
|
| 164 |
-
|
| 165 |
-
reports = query.order_by(RegulatoryReport.created_at.desc()).limit(50).all()
|
| 166 |
-
|
| 167 |
-
return {
|
| 168 |
-
"reports": [
|
| 169 |
-
{
|
| 170 |
-
"id": r.id,
|
| 171 |
-
"report_type": r.report_type,
|
| 172 |
-
"report_id": r.report_id,
|
| 173 |
-
"case_id": r.case_id,
|
| 174 |
-
"filing_status": r.filing_status,
|
| 175 |
-
"due_date": r.due_date.isoformat() if r.due_date else None,
|
| 176 |
-
"regulatory_body": r.regulatory_body,
|
| 177 |
-
"created_at": r.created_at.isoformat(),
|
| 178 |
-
}
|
| 179 |
-
for r in reports
|
| 180 |
-
],
|
| 181 |
-
"total": len(reports),
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
@router.get("/regional-compliance")
|
| 186 |
-
async def get_regional_compliance():
|
| 187 |
-
"""Reserved for global expansion - currently static configuration"""
|
| 188 |
-
return {
|
| 189 |
-
"regions": [
|
| 190 |
-
{
|
| 191 |
-
"region": "North America",
|
| 192 |
-
"framework": "BSA/AML",
|
| 193 |
-
"compliance_score": 95,
|
| 194 |
-
"last_audit_date": "2025-11-01",
|
| 195 |
-
"next_audit_date": "2026-05-01",
|
| 196 |
-
"critical_findings": 0,
|
| 197 |
-
"data_residency_requirements": ["US-East"],
|
| 198 |
-
"reporting_frequency": "Quarterly",
|
| 199 |
-
},
|
| 200 |
-
{
|
| 201 |
-
"region": "Europe",
|
| 202 |
-
"framework": "GDPR",
|
| 203 |
-
"compliance_score": 98,
|
| 204 |
-
"last_audit_date": "2025-10-15",
|
| 205 |
-
"next_audit_date": "2026-04-15",
|
| 206 |
-
"critical_findings": 0,
|
| 207 |
-
"data_residency_requirements": ["EU-Central"],
|
| 208 |
-
"reporting_frequency": "Annual",
|
| 209 |
-
},
|
| 210 |
-
]
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
@router.get("/data-residency-rules")
|
| 215 |
-
async def get_data_residency_rules():
|
| 216 |
-
return {
|
| 217 |
-
"rules": [
|
| 218 |
-
{
|
| 219 |
-
"region": "EU",
|
| 220 |
-
"data_types": ["PII", "Financial"],
|
| 221 |
-
"residency_requirements": "Must stay within EEA",
|
| 222 |
-
"encryption_requirements": "AES-256 at rest",
|
| 223 |
-
"retention_periods": {"PII": 365, "Financial": 2555},
|
| 224 |
-
}
|
| 225 |
-
]
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
@router.post("/audit/log")
|
| 230 |
-
async def log_audit_event(event: Dict[str, Any], db: Session = Depends(get_db), current_user: Any = Depends(auth_service.get_current_user)):
|
| 231 |
-
try:
|
| 232 |
-
new_log = ComplianceAuditLog(
|
| 233 |
-
id=f"audit-{datetime.utcnow().timestamp()}",
|
| 234 |
-
action=event.get("action", "unknown"),
|
| 235 |
-
resource_type=event.get("resource_type", "system"),
|
| 236 |
-
resource_id=event.get("resource_id", "unknown"),
|
| 237 |
-
user_id=current_user.id if hasattr(current_user, "id") else "system",
|
| 238 |
-
user_role=current_user.role if hasattr(current_user, "role") else "system",
|
| 239 |
-
timestamp=datetime.utcnow(),
|
| 240 |
-
details=str(event.get("details", {})),
|
| 241 |
-
risk_score=event.get("risk_score", 0.0),
|
| 242 |
-
)
|
| 243 |
-
db.add(new_log)
|
| 244 |
-
db.commit()
|
| 245 |
-
return {"log_id": new_log.id, "status": "recorded"}
|
| 246 |
-
except Exception as e:
|
| 247 |
-
db.rollback()
|
| 248 |
-
raise HTTPException(status_code=500, detail=f"Failed to log audit event: {str(e)}")
|
|
|
|
| 1 |
+
# SHIM: Redirects to new module location
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|