1e1e5957e8
- Ips.vue: el-select 添加 width: 280px,修复网段下拉框过窄 - Ips.vue: query 参数 networkId → network_id(与 FastAPI snake_case 一致) - Scan.vue: el-select 用 network.id 代替整个 network 对象作为 value,修复切换网段不生效 - enhanced_scan_service.py: 替换失效的 scapy ARP 扫描为 arp-scan 命令;扫描结果自动创建缺失 IP 记录
164 lines
5.4 KiB
Python
164 lines
5.4 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from fastapi.responses import StreamingResponse
|
|
from sqlalchemy.orm import Session
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
import csv
|
|
import io
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_user
|
|
from app.models.auth import User
|
|
from app.services.audit_service import AuditService
|
|
|
|
router = APIRouter(
|
|
prefix="/audit",
|
|
tags=["审计日志"],
|
|
dependencies=[Depends(get_current_user)],
|
|
)
|
|
|
|
|
|
@router.get("/logs", summary="查询审计日志")
|
|
def get_audit_logs(
|
|
user_id: Optional[int] = None,
|
|
username: Optional[str] = None,
|
|
action: Optional[str] = None,
|
|
resource_type: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
start_time: Optional[datetime] = None,
|
|
end_time: Optional[datetime] = None,
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=500),
|
|
# 当前用户必须是 admin 或 super_admin 才能看审计
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
# 角色检查:super_admin / admin 可以看所有人的,operator/viewer 只能看自己的
|
|
if current_user.role not in ("super_admin", "admin"):
|
|
user_id = current_user.id
|
|
|
|
total, items = AuditService.get_logs(
|
|
db,
|
|
user_id=user_id,
|
|
username=username,
|
|
action=action,
|
|
resource_type=resource_type,
|
|
status=status,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
skip=skip,
|
|
limit=limit,
|
|
)
|
|
|
|
serialized = []
|
|
for log in items:
|
|
serialized.append({
|
|
"id": log.id,
|
|
"user_id": log.user_id,
|
|
"username": log.username,
|
|
"action": log.action,
|
|
"resource_type": log.resource_type,
|
|
"resource_id": log.resource_id,
|
|
"resource_name": log.resource_name,
|
|
"method": log.method,
|
|
"path": log.path,
|
|
"ip_address": log.ip_address,
|
|
"user_agent": log.user_agent,
|
|
"status": log.status,
|
|
"detail": log.detail,
|
|
"created_at": log.created_at.isoformat() if log.created_at else None,
|
|
})
|
|
|
|
return {"total": total, "items": serialized}
|
|
|
|
|
|
@router.get("/logs/export", summary="导出审计日志为 CSV")
|
|
def export_audit_logs(
|
|
user_id: Optional[int] = None,
|
|
username: Optional[str] = None,
|
|
action: Optional[str] = None,
|
|
resource_type: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
start_time: Optional[datetime] = None,
|
|
end_time: Optional[datetime] = None,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""导出 CSV 流。admin/super_admin 看全部;operator/viewer 仅能导出自己的。"""
|
|
if current_user.role not in ("super_admin", "admin"):
|
|
user_id = current_user.id
|
|
|
|
# 导出上限 50000 行,避免一次拉太多 OOM
|
|
total, items = AuditService.get_logs(
|
|
db,
|
|
user_id=user_id,
|
|
username=username,
|
|
action=action,
|
|
resource_type=resource_type,
|
|
status=status,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
skip=0,
|
|
limit=50000,
|
|
)
|
|
|
|
output = io.StringIO()
|
|
# 写入 UTF-8 BOM 让 Excel 直接打开不乱码
|
|
output.write("\ufeff")
|
|
writer = csv.writer(output)
|
|
writer.writerow([
|
|
"ID", "时间", "用户", "操作", "资源类型", "资源ID", "资源名称",
|
|
"HTTP方法", "路径", "客户端IP", "状态", "详情"
|
|
])
|
|
for log in items:
|
|
writer.writerow([
|
|
log.id,
|
|
log.created_at.isoformat() if log.created_at else "",
|
|
log.username or "",
|
|
log.action or "",
|
|
log.resource_type or "",
|
|
log.resource_id or "",
|
|
log.resource_name or "",
|
|
log.method or "",
|
|
log.path or "",
|
|
log.ip_address or "",
|
|
log.status or "",
|
|
(log.detail or "").replace("\n", " ")[:500],
|
|
])
|
|
|
|
output.seek(0)
|
|
filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
|
return StreamingResponse(
|
|
iter([output.getvalue()]),
|
|
media_type="text/csv; charset=utf-8",
|
|
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
)
|
|
|
|
|
|
@router.get("/stats", summary="审计日志统计")
|
|
def get_audit_stats(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
if current_user.role not in ("super_admin", "admin"):
|
|
# operator/viewer 只能看自己的简单计数
|
|
from app.models.audit import AuditLog
|
|
from sqlalchemy import func
|
|
my_count = db.query(func.count(AuditLog.id)).filter(AuditLog.user_id == current_user.id).scalar()
|
|
return {"recent_24h": 0, "by_action_24h": {}, "my_total": int(my_count or 0)}
|
|
|
|
return AuditService.get_stats(db)
|
|
|
|
|
|
@router.get("/actions", summary="获取所有审计操作类型(用于前端过滤下拉框)")
|
|
def get_action_types():
|
|
"""硬编码返回常用 action 列表,避免每次都查 DB"""
|
|
from app.services.audit_service import AuditAction
|
|
actions = []
|
|
for name in dir(AuditAction):
|
|
if name.startswith("_") or name.isupper() and not name.startswith("__"):
|
|
continue
|
|
val = getattr(AuditAction, name, None)
|
|
if isinstance(val, str):
|
|
actions.append({"code": val, "name": name})
|
|
return actions |