mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 15:34:36 +01:00
650 lines
20 KiB
Python
650 lines
20 KiB
Python
"""
|
|
Audit log query endpoints
|
|
"""
|
|
|
|
from typing import List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, and_, or_
|
|
from datetime import datetime, timedelta
|
|
|
|
from app.db.database import get_db
|
|
from app.models.audit_log import AuditLog
|
|
from app.models.user import User
|
|
from app.core.security import get_current_user
|
|
from app.services.permission_manager import require_permission
|
|
from app.services.audit_service import log_audit_event, get_audit_logs, get_audit_stats
|
|
from app.core.logging import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# Pydantic models
|
|
class AuditLogResponse(BaseModel):
|
|
id: str
|
|
user_id: Optional[str] = None
|
|
api_key_id: Optional[str] = None
|
|
action: str
|
|
resource_type: str
|
|
resource_id: Optional[str] = None
|
|
details: dict
|
|
ip_address: Optional[str] = None
|
|
user_agent: Optional[str] = None
|
|
success: bool
|
|
severity: str
|
|
created_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class AuditLogListResponse(BaseModel):
|
|
logs: List[AuditLogResponse]
|
|
total: int
|
|
page: int
|
|
size: int
|
|
|
|
|
|
class AuditStatsResponse(BaseModel):
|
|
total_events: int
|
|
events_by_action: dict
|
|
events_by_resource_type: dict
|
|
events_by_severity: dict
|
|
success_rate: float
|
|
failure_rate: float
|
|
events_by_user: dict
|
|
events_by_hour: dict
|
|
top_actions: List[dict]
|
|
top_resources: List[dict]
|
|
|
|
|
|
class AuditSearchRequest(BaseModel):
|
|
user_id: Optional[str] = None
|
|
action: Optional[str] = None
|
|
resource_type: Optional[str] = None
|
|
resource_id: Optional[str] = None
|
|
start_date: Optional[datetime] = None
|
|
end_date: Optional[datetime] = None
|
|
success: Optional[bool] = None
|
|
severity: Optional[str] = None
|
|
ip_address: Optional[str] = None
|
|
search_text: Optional[str] = None
|
|
|
|
|
|
class SecurityEventsResponse(BaseModel):
|
|
suspicious_activities: List[dict]
|
|
failed_logins: List[dict]
|
|
unusual_access_patterns: List[dict]
|
|
high_severity_events: List[dict]
|
|
|
|
|
|
# Audit log query endpoints
|
|
@router.get("/", response_model=AuditLogListResponse)
|
|
async def list_audit_logs(
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(50, ge=1, le=1000),
|
|
user_id: Optional[str] = Query(None),
|
|
action: Optional[str] = Query(None),
|
|
resource_type: Optional[str] = Query(None),
|
|
resource_id: Optional[str] = Query(None),
|
|
start_date: Optional[datetime] = Query(None),
|
|
end_date: Optional[datetime] = Query(None),
|
|
success: Optional[bool] = Query(None),
|
|
severity: Optional[str] = Query(None),
|
|
search: Optional[str] = Query(None),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List audit logs with filtering and pagination"""
|
|
|
|
# Check permissions
|
|
require_permission(current_user.get("permissions", []), "platform:audit:read")
|
|
|
|
# Build query
|
|
query = select(AuditLog)
|
|
conditions = []
|
|
|
|
# Apply filters
|
|
if user_id:
|
|
conditions.append(AuditLog.user_id == user_id)
|
|
if action:
|
|
conditions.append(AuditLog.action == action)
|
|
if resource_type:
|
|
conditions.append(AuditLog.resource_type == resource_type)
|
|
if resource_id:
|
|
conditions.append(AuditLog.resource_id == resource_id)
|
|
if start_date:
|
|
conditions.append(AuditLog.created_at >= start_date)
|
|
if end_date:
|
|
conditions.append(AuditLog.created_at <= end_date)
|
|
if success is not None:
|
|
conditions.append(AuditLog.success == success)
|
|
if severity:
|
|
conditions.append(AuditLog.severity == severity)
|
|
if search:
|
|
search_conditions = [
|
|
AuditLog.action.ilike(f"%{search}%"),
|
|
AuditLog.resource_type.ilike(f"%{search}%"),
|
|
AuditLog.details.astext.ilike(f"%{search}%"),
|
|
]
|
|
conditions.append(or_(*search_conditions))
|
|
|
|
if conditions:
|
|
query = query.where(and_(*conditions))
|
|
|
|
# Get total count
|
|
count_query = select(func.count(AuditLog.id))
|
|
if conditions:
|
|
count_query = count_query.where(and_(*conditions))
|
|
|
|
total_result = await db.execute(count_query)
|
|
total = total_result.scalar()
|
|
|
|
# Apply pagination and ordering
|
|
offset = (page - 1) * size
|
|
query = query.offset(offset).limit(size).order_by(AuditLog.created_at.desc())
|
|
|
|
# Execute query
|
|
result = await db.execute(query)
|
|
logs = result.scalars().all()
|
|
|
|
# Log audit event for this query
|
|
await log_audit_event(
|
|
db=db,
|
|
user_id=current_user["id"],
|
|
action="query_audit_logs",
|
|
resource_type="audit_log",
|
|
details={
|
|
"filters": {
|
|
"user_id": user_id,
|
|
"action": action,
|
|
"resource_type": resource_type,
|
|
"start_date": start_date.isoformat() if start_date else None,
|
|
"end_date": end_date.isoformat() if end_date else None,
|
|
"success": success,
|
|
"severity": severity,
|
|
"search": search,
|
|
},
|
|
"page": page,
|
|
"size": size,
|
|
"total_results": total,
|
|
},
|
|
)
|
|
|
|
return AuditLogListResponse(
|
|
logs=[AuditLogResponse.model_validate(log) for log in logs],
|
|
total=total,
|
|
page=page,
|
|
size=size,
|
|
)
|
|
|
|
|
|
@router.post("/search", response_model=AuditLogListResponse)
|
|
async def search_audit_logs(
|
|
search_request: AuditSearchRequest,
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(50, ge=1, le=1000),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Advanced search for audit logs"""
|
|
|
|
# Check permissions
|
|
require_permission(current_user.get("permissions", []), "platform:audit:read")
|
|
|
|
# Use the audit service function
|
|
logs = await get_audit_logs(
|
|
db=db,
|
|
user_id=search_request.user_id,
|
|
action=search_request.action,
|
|
resource_type=search_request.resource_type,
|
|
resource_id=search_request.resource_id,
|
|
start_date=search_request.start_date,
|
|
end_date=search_request.end_date,
|
|
limit=size,
|
|
offset=(page - 1) * size,
|
|
)
|
|
|
|
# Get total count for the search
|
|
total_query = select(func.count(AuditLog.id))
|
|
conditions = []
|
|
|
|
if search_request.user_id:
|
|
conditions.append(AuditLog.user_id == search_request.user_id)
|
|
if search_request.action:
|
|
conditions.append(AuditLog.action == search_request.action)
|
|
if search_request.resource_type:
|
|
conditions.append(AuditLog.resource_type == search_request.resource_type)
|
|
if search_request.resource_id:
|
|
conditions.append(AuditLog.resource_id == search_request.resource_id)
|
|
if search_request.start_date:
|
|
conditions.append(AuditLog.created_at >= search_request.start_date)
|
|
if search_request.end_date:
|
|
conditions.append(AuditLog.created_at <= search_request.end_date)
|
|
if search_request.success is not None:
|
|
conditions.append(AuditLog.success == search_request.success)
|
|
if search_request.severity:
|
|
conditions.append(AuditLog.severity == search_request.severity)
|
|
if search_request.ip_address:
|
|
conditions.append(AuditLog.ip_address == search_request.ip_address)
|
|
if search_request.search_text:
|
|
search_conditions = [
|
|
AuditLog.action.ilike(f"%{search_request.search_text}%"),
|
|
AuditLog.resource_type.ilike(f"%{search_request.search_text}%"),
|
|
AuditLog.details.astext.ilike(f"%{search_request.search_text}%"),
|
|
]
|
|
conditions.append(or_(*search_conditions))
|
|
|
|
if conditions:
|
|
total_query = total_query.where(and_(*conditions))
|
|
|
|
total_result = await db.execute(total_query)
|
|
total = total_result.scalar()
|
|
|
|
# Log audit event
|
|
await log_audit_event(
|
|
db=db,
|
|
user_id=current_user["id"],
|
|
action="advanced_search_audit_logs",
|
|
resource_type="audit_log",
|
|
details={
|
|
"search_criteria": search_request.model_dump(exclude_unset=True),
|
|
"results_count": len(logs),
|
|
"total_matches": total,
|
|
},
|
|
)
|
|
|
|
return AuditLogListResponse(
|
|
logs=[AuditLogResponse.model_validate(log) for log in logs],
|
|
total=total,
|
|
page=page,
|
|
size=size,
|
|
)
|
|
|
|
|
|
@router.get("/stats", response_model=AuditStatsResponse)
|
|
async def get_audit_statistics(
|
|
start_date: Optional[datetime] = Query(None),
|
|
end_date: Optional[datetime] = Query(None),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Get audit log statistics"""
|
|
|
|
# Check permissions
|
|
require_permission(current_user.get("permissions", []), "platform:audit:read")
|
|
|
|
# Default to last 30 days if no dates provided
|
|
if not end_date:
|
|
end_date = datetime.utcnow()
|
|
if not start_date:
|
|
start_date = end_date - timedelta(days=30)
|
|
|
|
# Get basic stats using audit service
|
|
basic_stats = await get_audit_stats(db, start_date, end_date)
|
|
|
|
# Get additional statistics
|
|
conditions = [AuditLog.created_at >= start_date, AuditLog.created_at <= end_date]
|
|
|
|
# Events by user
|
|
user_query = (
|
|
select(AuditLog.user_id, func.count(AuditLog.id).label("count"))
|
|
.where(and_(*conditions))
|
|
.group_by(AuditLog.user_id)
|
|
.order_by(func.count(AuditLog.id).desc())
|
|
.limit(10)
|
|
)
|
|
|
|
user_result = await db.execute(user_query)
|
|
events_by_user = dict(user_result.fetchall())
|
|
|
|
# Events by hour of day
|
|
hour_query = (
|
|
select(
|
|
func.extract("hour", AuditLog.created_at).label("hour"),
|
|
func.count(AuditLog.id).label("count"),
|
|
)
|
|
.where(and_(*conditions))
|
|
.group_by(func.extract("hour", AuditLog.created_at))
|
|
.order_by("hour")
|
|
)
|
|
|
|
hour_result = await db.execute(hour_query)
|
|
events_by_hour = dict(hour_result.fetchall())
|
|
|
|
# Top actions
|
|
top_actions_query = (
|
|
select(AuditLog.action, func.count(AuditLog.id).label("count"))
|
|
.where(and_(*conditions))
|
|
.group_by(AuditLog.action)
|
|
.order_by(func.count(AuditLog.id).desc())
|
|
.limit(10)
|
|
)
|
|
|
|
top_actions_result = await db.execute(top_actions_query)
|
|
top_actions = [
|
|
{"action": row[0], "count": row[1]} for row in top_actions_result.fetchall()
|
|
]
|
|
|
|
# Top resources
|
|
top_resources_query = (
|
|
select(AuditLog.resource_type, func.count(AuditLog.id).label("count"))
|
|
.where(and_(*conditions))
|
|
.group_by(AuditLog.resource_type)
|
|
.order_by(func.count(AuditLog.id).desc())
|
|
.limit(10)
|
|
)
|
|
|
|
top_resources_result = await db.execute(top_resources_query)
|
|
top_resources = [
|
|
{"resource_type": row[0], "count": row[1]}
|
|
for row in top_resources_result.fetchall()
|
|
]
|
|
|
|
# Log audit event
|
|
await log_audit_event(
|
|
db=db,
|
|
user_id=current_user["id"],
|
|
action="get_audit_statistics",
|
|
resource_type="audit_log",
|
|
details={
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"total_events": basic_stats["total_events"],
|
|
},
|
|
)
|
|
|
|
return AuditStatsResponse(
|
|
**basic_stats,
|
|
events_by_user=events_by_user,
|
|
events_by_hour=events_by_hour,
|
|
top_actions=top_actions,
|
|
top_resources=top_resources,
|
|
)
|
|
|
|
|
|
@router.get("/security-events", response_model=SecurityEventsResponse)
|
|
async def get_security_events(
|
|
hours: int = Query(24, ge=1, le=168), # Last 24 hours by default, max 1 week
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Get security-related events and anomalies"""
|
|
|
|
# Check permissions
|
|
require_permission(current_user.get("permissions", []), "platform:audit:read")
|
|
|
|
end_time = datetime.utcnow()
|
|
start_time = end_time - timedelta(hours=hours)
|
|
|
|
# Failed logins
|
|
failed_logins_query = (
|
|
select(AuditLog)
|
|
.where(
|
|
and_(
|
|
AuditLog.created_at >= start_time,
|
|
AuditLog.action == "login",
|
|
AuditLog.success == False,
|
|
)
|
|
)
|
|
.order_by(AuditLog.created_at.desc())
|
|
.limit(50)
|
|
)
|
|
|
|
failed_logins_result = await db.execute(failed_logins_query)
|
|
failed_logins = [
|
|
{
|
|
"timestamp": log.created_at.isoformat(),
|
|
"user_id": log.user_id,
|
|
"ip_address": log.ip_address,
|
|
"user_agent": log.user_agent,
|
|
"details": log.details,
|
|
}
|
|
for log in failed_logins_result.scalars().all()
|
|
]
|
|
|
|
# High severity events
|
|
high_severity_query = (
|
|
select(AuditLog)
|
|
.where(
|
|
and_(
|
|
AuditLog.created_at >= start_time,
|
|
AuditLog.severity.in_(["error", "critical"]),
|
|
)
|
|
)
|
|
.order_by(AuditLog.created_at.desc())
|
|
.limit(50)
|
|
)
|
|
|
|
high_severity_result = await db.execute(high_severity_query)
|
|
high_severity_events = [
|
|
{
|
|
"timestamp": log.created_at.isoformat(),
|
|
"action": log.action,
|
|
"resource_type": log.resource_type,
|
|
"severity": log.severity,
|
|
"user_id": log.user_id,
|
|
"ip_address": log.ip_address,
|
|
"success": log.success,
|
|
"details": log.details,
|
|
}
|
|
for log in high_severity_result.scalars().all()
|
|
]
|
|
|
|
# Suspicious activities (multiple failed attempts from same IP)
|
|
suspicious_ips_query = (
|
|
select(AuditLog.ip_address, func.count(AuditLog.id).label("failed_count"))
|
|
.where(
|
|
and_(
|
|
AuditLog.created_at >= start_time,
|
|
AuditLog.success == False,
|
|
AuditLog.ip_address.isnot(None),
|
|
)
|
|
)
|
|
.group_by(AuditLog.ip_address)
|
|
.having(func.count(AuditLog.id) >= 5)
|
|
.order_by(func.count(AuditLog.id).desc())
|
|
)
|
|
|
|
suspicious_ips_result = await db.execute(suspicious_ips_query)
|
|
suspicious_activities = [
|
|
{
|
|
"ip_address": row[0],
|
|
"failed_attempts": row[1],
|
|
"risk_level": "high" if row[1] >= 10 else "medium",
|
|
}
|
|
for row in suspicious_ips_result.fetchall()
|
|
]
|
|
|
|
# Unusual access patterns (users accessing from multiple IPs)
|
|
unusual_access_query = (
|
|
select(
|
|
AuditLog.user_id,
|
|
func.count(func.distinct(AuditLog.ip_address)).label("ip_count"),
|
|
func.array_agg(func.distinct(AuditLog.ip_address)).label("ip_addresses"),
|
|
)
|
|
.where(
|
|
and_(
|
|
AuditLog.created_at >= start_time,
|
|
AuditLog.user_id.isnot(None),
|
|
AuditLog.ip_address.isnot(None),
|
|
)
|
|
)
|
|
.group_by(AuditLog.user_id)
|
|
.having(func.count(func.distinct(AuditLog.ip_address)) >= 3)
|
|
.order_by(func.count(func.distinct(AuditLog.ip_address)).desc())
|
|
)
|
|
|
|
unusual_access_result = await db.execute(unusual_access_query)
|
|
unusual_access_patterns = [
|
|
{
|
|
"user_id": row[0],
|
|
"unique_ips": row[1],
|
|
"ip_addresses": row[2] if row[2] else [],
|
|
}
|
|
for row in unusual_access_result.fetchall()
|
|
]
|
|
|
|
# Log audit event
|
|
await log_audit_event(
|
|
db=db,
|
|
user_id=current_user["id"],
|
|
action="get_security_events",
|
|
resource_type="audit_log",
|
|
details={
|
|
"time_range_hours": hours,
|
|
"failed_logins_count": len(failed_logins),
|
|
"high_severity_count": len(high_severity_events),
|
|
"suspicious_ips_count": len(suspicious_activities),
|
|
"unusual_access_patterns_count": len(unusual_access_patterns),
|
|
},
|
|
)
|
|
|
|
return SecurityEventsResponse(
|
|
suspicious_activities=suspicious_activities,
|
|
failed_logins=failed_logins,
|
|
unusual_access_patterns=unusual_access_patterns,
|
|
high_severity_events=high_severity_events,
|
|
)
|
|
|
|
|
|
@router.get("/export")
|
|
async def export_audit_logs(
|
|
format: str = Query("csv", pattern="^(csv|json)$"),
|
|
start_date: Optional[datetime] = Query(None),
|
|
end_date: Optional[datetime] = Query(None),
|
|
user_id: Optional[str] = Query(None),
|
|
action: Optional[str] = Query(None),
|
|
resource_type: Optional[str] = Query(None),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Export audit logs in CSV or JSON format"""
|
|
|
|
# Check permissions
|
|
require_permission(current_user.get("permissions", []), "platform:audit:export")
|
|
|
|
# Default to last 30 days if no dates provided
|
|
if not end_date:
|
|
end_date = datetime.utcnow()
|
|
if not start_date:
|
|
start_date = end_date - timedelta(days=30)
|
|
|
|
# Limit export size
|
|
max_records = 10000
|
|
|
|
# Build query
|
|
query = select(AuditLog)
|
|
conditions = [AuditLog.created_at >= start_date, AuditLog.created_at <= end_date]
|
|
|
|
if user_id:
|
|
conditions.append(AuditLog.user_id == user_id)
|
|
if action:
|
|
conditions.append(AuditLog.action == action)
|
|
if resource_type:
|
|
conditions.append(AuditLog.resource_type == resource_type)
|
|
|
|
query = (
|
|
query.where(and_(*conditions))
|
|
.order_by(AuditLog.created_at.desc())
|
|
.limit(max_records)
|
|
)
|
|
|
|
# Execute query
|
|
result = await db.execute(query)
|
|
logs = result.scalars().all()
|
|
|
|
# Log export event
|
|
await log_audit_event(
|
|
db=db,
|
|
user_id=current_user["id"],
|
|
action="export_audit_logs",
|
|
resource_type="audit_log",
|
|
details={
|
|
"format": format,
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"records_exported": len(logs),
|
|
"filters": {
|
|
"user_id": user_id,
|
|
"action": action,
|
|
"resource_type": resource_type,
|
|
},
|
|
},
|
|
)
|
|
|
|
if format == "json":
|
|
from fastapi.responses import JSONResponse
|
|
|
|
export_data = [
|
|
{
|
|
"id": str(log.id),
|
|
"user_id": log.user_id,
|
|
"action": log.action,
|
|
"resource_type": log.resource_type,
|
|
"resource_id": log.resource_id,
|
|
"details": log.details,
|
|
"ip_address": log.ip_address,
|
|
"user_agent": log.user_agent,
|
|
"success": log.success,
|
|
"severity": log.severity,
|
|
"created_at": log.created_at.isoformat(),
|
|
}
|
|
for log in logs
|
|
]
|
|
return JSONResponse(content=export_data)
|
|
|
|
else: # CSV format
|
|
import csv
|
|
import io
|
|
from fastapi.responses import StreamingResponse
|
|
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
|
|
# Write header
|
|
writer.writerow(
|
|
[
|
|
"ID",
|
|
"User ID",
|
|
"Action",
|
|
"Resource Type",
|
|
"Resource ID",
|
|
"IP Address",
|
|
"Success",
|
|
"Severity",
|
|
"Created At",
|
|
"Details",
|
|
]
|
|
)
|
|
|
|
# Write data
|
|
for log in logs:
|
|
writer.writerow(
|
|
[
|
|
str(log.id),
|
|
log.user_id or "",
|
|
log.action,
|
|
log.resource_type,
|
|
log.resource_id or "",
|
|
log.ip_address or "",
|
|
log.success,
|
|
log.severity,
|
|
log.created_at.isoformat(),
|
|
str(log.details),
|
|
]
|
|
)
|
|
|
|
output.seek(0)
|
|
|
|
return StreamingResponse(
|
|
io.BytesIO(output.getvalue().encode()),
|
|
media_type="text/csv",
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename=audit_logs_{start_date.strftime('%Y%m%d')}_{end_date.strftime('%Y%m%d')}.csv"
|
|
},
|
|
)
|