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 记录
155 lines
5.4 KiB
Python
155 lines
5.4 KiB
Python
from typing import Optional, Dict, Any, List, Tuple
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import desc
|
|
from datetime import datetime
|
|
import json
|
|
|
|
from app.models.audit import AuditLog
|
|
from app.models.auth import User
|
|
|
|
|
|
class AuditService:
|
|
"""审计日志服务"""
|
|
|
|
@staticmethod
|
|
def record(
|
|
db: Session,
|
|
*,
|
|
action: str,
|
|
user: Optional[User] = None,
|
|
resource_type: Optional[str] = None,
|
|
resource_id: Optional[str] = None,
|
|
resource_name: Optional[str] = None,
|
|
method: Optional[str] = None,
|
|
path: Optional[str] = None,
|
|
ip_address: Optional[str] = None,
|
|
user_agent: Optional[str] = None,
|
|
status: str = "success",
|
|
detail: Optional[Any] = None,
|
|
) -> AuditLog:
|
|
"""
|
|
写入一条审计日志。任意字段缺失都安全降级(不抛异常),
|
|
避免审计日志写入失败影响主业务流程。
|
|
"""
|
|
try:
|
|
log = AuditLog(
|
|
user_id=user.id if user else None,
|
|
username=user.username if user else None,
|
|
action=action,
|
|
resource_type=resource_type,
|
|
resource_id=str(resource_id) if resource_id is not None else None,
|
|
resource_name=resource_name,
|
|
method=method,
|
|
path=path,
|
|
ip_address=ip_address,
|
|
user_agent=(user_agent or "")[:500],
|
|
status=status,
|
|
detail=_serialize_detail(detail),
|
|
)
|
|
db.add(log)
|
|
db.commit()
|
|
return log
|
|
except Exception as e:
|
|
# 写审计日志失败不能影响主业务,仅回滚
|
|
db.rollback()
|
|
# 用 print 而非 logger,避免循环依赖(logger 可能未初始化)
|
|
print(f"[audit] failed to write log action={action}: {e}")
|
|
return None
|
|
|
|
@staticmethod
|
|
def get_logs(
|
|
db: Session,
|
|
*,
|
|
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 = 0,
|
|
limit: int = 50,
|
|
) -> Tuple[int, List[AuditLog]]:
|
|
query = db.query(AuditLog)
|
|
|
|
if user_id is not None:
|
|
query = query.filter(AuditLog.user_id == user_id)
|
|
if username:
|
|
query = query.filter(AuditLog.username.like(f"%{username}%"))
|
|
if action:
|
|
query = query.filter(AuditLog.action.like(f"%{action}%"))
|
|
if resource_type:
|
|
query = query.filter(AuditLog.resource_type == resource_type)
|
|
if status:
|
|
query = query.filter(AuditLog.status == status)
|
|
if start_time:
|
|
query = query.filter(AuditLog.created_at >= start_time)
|
|
if end_time:
|
|
query = query.filter(AuditLog.created_at <= end_time)
|
|
|
|
total = query.count()
|
|
items = query.order_by(desc(AuditLog.created_at)).offset(skip).limit(limit).all()
|
|
return total, items
|
|
|
|
@staticmethod
|
|
def get_stats(db: Session) -> Dict[str, Any]:
|
|
"""首页/汇总用:最近 24h 操作数 + 按 action 分组"""
|
|
from datetime import timedelta
|
|
cutoff = datetime.utcnow() - timedelta(hours=24)
|
|
recent = db.query(AuditLog).filter(AuditLog.created_at >= cutoff).count()
|
|
by_action = db.query(AuditLog.action, db.query(AuditLog).filter(AuditLog.created_at >= cutoff).subquery()) # 占位避免循环
|
|
# 简化:用 group by
|
|
from sqlalchemy import func
|
|
rows = db.query(AuditLog.action, func.count(AuditLog.id)).filter(
|
|
AuditLog.created_at >= cutoff
|
|
).group_by(AuditLog.action).all()
|
|
return {
|
|
"recent_24h": recent,
|
|
"by_action_24h": {action: count for action, count in rows},
|
|
}
|
|
|
|
|
|
def _serialize_detail(detail: Any) -> Optional[str]:
|
|
"""把 dict/list 等结构化数据序列化成字符串,便于存储和展示"""
|
|
if detail is None:
|
|
return None
|
|
if isinstance(detail, str):
|
|
return detail[:4000]
|
|
try:
|
|
return json.dumps(detail, ensure_ascii=False, default=str)[:4000]
|
|
except (TypeError, ValueError):
|
|
return str(detail)[:4000]
|
|
|
|
|
|
# 常用 action 字符串集中管理(避免散落各处的魔法字符串)
|
|
class AuditAction:
|
|
# 网络
|
|
NETWORK_CREATE = "network.create"
|
|
NETWORK_UPDATE = "network.update"
|
|
NETWORK_DELETE = "network.delete"
|
|
# IP
|
|
IP_UPDATE = "ip.update"
|
|
IP_RESERVE = "ip.reserve"
|
|
IP_UNRESERVE = "ip.unreserve"
|
|
# 扫描
|
|
SCAN_TRIGGER = "scan.trigger"
|
|
# SNMP
|
|
SNMP_CRED_CREATE = "snmp.credential.create"
|
|
SNMP_CRED_UPDATE = "snmp.credential.update"
|
|
SNMP_CRED_DELETE = "snmp.credential.delete"
|
|
SNMP_DEVICE_CREATE = "snmp.device.create"
|
|
SNMP_DEVICE_UPDATE = "snmp.device.update"
|
|
SNMP_DEVICE_DELETE = "snmp.device.delete"
|
|
# 告警
|
|
ALERT_ACK = "alert.acknowledge"
|
|
ALERT_RESOLVE = "alert.resolve"
|
|
# 用户管理
|
|
USER_LOGIN = "user.login"
|
|
USER_LOGIN_FAILED = "user.login.failed"
|
|
USER_LOGOUT = "user.logout"
|
|
USER_CREATE = "user.create"
|
|
USER_UPDATE_STATUS = "user.update_status"
|
|
USER_UNLOCK = "user.unlock"
|
|
USER_RESET_PASSWORD = "user.reset_password"
|
|
USER_DELETE = "user.delete"
|
|
USER_CHANGE_PASSWORD = "user.change_password" |