完善权限监控与安全中心
This commit is contained in:
@@ -20,7 +20,7 @@ from app.models.permission_access_log import PermissionAccessLog
|
||||
from app.models.permission_metric_snapshot import PermissionMetricSnapshot
|
||||
from app.models.security_access_log import SecurityAccessLog
|
||||
from app.models.user import User
|
||||
from app.services.ip_location import resolve_ip_location
|
||||
from app.services.ip_location import IpLocation, resolve_ip_location
|
||||
|
||||
router = APIRouter(prefix="/permission-monitoring", tags=["permission-monitoring"])
|
||||
|
||||
@@ -266,17 +266,13 @@ async def get_access_logs(
|
||||
).select_from(PermissionAccessLog)
|
||||
user_stats_query = (
|
||||
select(
|
||||
PermissionAccessLog.user_id,
|
||||
User.full_name,
|
||||
PermissionAccessLog.role,
|
||||
PermissionAccessLog.ip_address.label("sample_ip_address"),
|
||||
func.count().label("total_count"),
|
||||
func.count().filter(PermissionAccessLog.allowed.is_(False)).label("denied_count"),
|
||||
func.count(func.distinct(PermissionAccessLog.ip_address)).label("unique_ip_count"),
|
||||
func.max(PermissionAccessLog.created_at).label("last_seen_at"),
|
||||
)
|
||||
.outerjoin(User, PermissionAccessLog.user_id == User.id)
|
||||
.group_by(PermissionAccessLog.ip_address, PermissionAccessLog.user_id, User.full_name, PermissionAccessLog.role)
|
||||
.group_by(PermissionAccessLog.ip_address)
|
||||
.order_by(desc("total_count"), desc("last_seen_at"))
|
||||
.limit(10)
|
||||
)
|
||||
@@ -292,6 +288,26 @@ async def get_access_logs(
|
||||
summary_row = summary_result.one()
|
||||
user_stats_result = await db.execute(user_stats_query)
|
||||
user_stats_rows = user_stats_result.all()
|
||||
latest_user_by_ip = {}
|
||||
if user_stats_rows:
|
||||
ranked_ips = [stat.sample_ip_address for stat in user_stats_rows if stat.sample_ip_address]
|
||||
latest_user_query = (
|
||||
select(
|
||||
PermissionAccessLog.ip_address,
|
||||
PermissionAccessLog.user_id,
|
||||
User.full_name,
|
||||
PermissionAccessLog.role,
|
||||
PermissionAccessLog.created_at,
|
||||
)
|
||||
.outerjoin(User, PermissionAccessLog.user_id == User.id)
|
||||
.where(PermissionAccessLog.ip_address.in_(ranked_ips))
|
||||
.order_by(PermissionAccessLog.ip_address, desc(PermissionAccessLog.created_at))
|
||||
)
|
||||
for condition in conditions:
|
||||
latest_user_query = latest_user_query.where(condition)
|
||||
latest_user_result = await db.execute(latest_user_query)
|
||||
for row in latest_user_result.all():
|
||||
latest_user_by_ip.setdefault(row.ip_address, row)
|
||||
|
||||
query = query.order_by(desc(PermissionAccessLog.created_at))
|
||||
query = query.offset((page - 1) * page_size).limit(page_size)
|
||||
@@ -323,11 +339,12 @@ async def get_access_logs(
|
||||
user_stats = []
|
||||
for stat in user_stats_rows:
|
||||
sample_location = resolve_ip_location(stat.sample_ip_address)
|
||||
latest_user = latest_user_by_ip.get(stat.sample_ip_address)
|
||||
user_stats.append(
|
||||
{
|
||||
"user_id": str(stat.user_id),
|
||||
"user_name": stat.full_name or "未知用户",
|
||||
"role": stat.role,
|
||||
"user_id": str(latest_user.user_id) if latest_user else "",
|
||||
"user_name": latest_user.full_name if latest_user and latest_user.full_name else "未知用户",
|
||||
"role": latest_user.role if latest_user else "",
|
||||
"total_count": stat.total_count,
|
||||
"denied_count": stat.denied_count,
|
||||
"unique_ip_count": stat.unique_ip_count,
|
||||
@@ -361,6 +378,43 @@ def _security_account_label(auth_status: str, user_identifier: str | None, user_
|
||||
return "未知账号"
|
||||
|
||||
|
||||
SENSITIVE_PROBE_MARKERS = (
|
||||
"/.env",
|
||||
".env",
|
||||
"/.git",
|
||||
".git/config",
|
||||
"backup",
|
||||
"config.php",
|
||||
"wp-config",
|
||||
"database.yml",
|
||||
)
|
||||
|
||||
|
||||
CHINA_IP_COUNTRY_LABELS = {"中国", "China", "Mainland China", "中国香港", "中国澳门", "中国台湾"}
|
||||
|
||||
|
||||
def _is_non_china_ip(ip_location: IpLocation) -> bool:
|
||||
country = (ip_location.country or "").strip()
|
||||
return bool(country) and country not in CHINA_IP_COUNTRY_LABELS and not country.startswith("中国")
|
||||
|
||||
|
||||
def _classify_security_access_log(log: SecurityAccessLog, ip_location: IpLocation | None = None) -> dict[str, str]:
|
||||
if ip_location and _is_non_china_ip(ip_location):
|
||||
return {"category": "ABNORMAL_IP", "severity": "HIGH"}
|
||||
path = (log.path or "").lower()
|
||||
if any(marker in path for marker in SENSITIVE_PROBE_MARKERS):
|
||||
return {"category": "PROBE", "severity": "CRITICAL"}
|
||||
if log.status_code >= 500:
|
||||
return {"category": "SERVER_ERROR", "severity": "HIGH"}
|
||||
if log.auth_status == "INVALID_TOKEN":
|
||||
return {"category": "INVALID_TOKEN", "severity": "MEDIUM"}
|
||||
if log.auth_status == "ANONYMOUS" and log.status_code in {401, 403}:
|
||||
return {"category": "ANONYMOUS_API", "severity": "MEDIUM"}
|
||||
if log.status_code == 404:
|
||||
return {"category": "NOT_FOUND_NOISE", "severity": "LOW"}
|
||||
return {"category": "OTHER", "severity": "LOW"}
|
||||
|
||||
|
||||
@router.get("/security-logs", status_code=status.HTTP_200_OK)
|
||||
async def get_security_access_logs(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
@@ -418,6 +472,7 @@ async def get_security_access_logs(
|
||||
|
||||
items = []
|
||||
for log in logs:
|
||||
ip_location = resolve_ip_location(log.client_ip)
|
||||
items.append(
|
||||
{
|
||||
"id": str(log.id),
|
||||
@@ -426,10 +481,16 @@ async def get_security_access_logs(
|
||||
"status_code": log.status_code,
|
||||
"elapsed_ms": round(log.elapsed_ms, 2),
|
||||
"client_ip": log.client_ip,
|
||||
"ip_location": ip_location.location,
|
||||
"ip_country": ip_location.country,
|
||||
"ip_province": ip_location.province,
|
||||
"ip_city": ip_location.city,
|
||||
"ip_isp": ip_location.isp,
|
||||
"user_agent": log.user_agent,
|
||||
"auth_status": log.auth_status,
|
||||
"user_identifier": log.user_identifier,
|
||||
"account_label": _security_account_label(log.auth_status, log.user_identifier, user_names),
|
||||
**_classify_security_access_log(log, ip_location),
|
||||
"created_at": log.created_at.isoformat(),
|
||||
}
|
||||
)
|
||||
@@ -623,14 +684,26 @@ async def get_ip_locations(
|
||||
)
|
||||
)
|
||||
result = await db.execute(_apply_monitoring_scope_to_log_query(query, scope))
|
||||
security_result = await db.execute(
|
||||
select(
|
||||
SecurityAccessLog.client_ip,
|
||||
SecurityAccessLog.user_identifier,
|
||||
SecurityAccessLog.auth_status,
|
||||
SecurityAccessLog.status_code,
|
||||
).where(
|
||||
SecurityAccessLog.created_at >= start_time,
|
||||
SecurityAccessLog.client_ip.is_not(None),
|
||||
)
|
||||
)
|
||||
buckets: dict[tuple[str, str, str], dict] = {}
|
||||
all_ip_addresses: set[str] = set()
|
||||
all_user_ids: set[uuid.UUID] = set()
|
||||
all_user_ids: set[str] = set()
|
||||
total_count = 0
|
||||
allowed_count = 0
|
||||
denied_count = 0
|
||||
|
||||
for ip_address, user_id, allowed in result.all():
|
||||
def add_ip_location_row(ip_address: str, user_id: str | uuid.UUID | None, allowed: bool) -> None:
|
||||
nonlocal total_count, allowed_count, denied_count
|
||||
ip_info = resolve_ip_location(ip_address)
|
||||
key = (ip_info.country, ip_info.province, ip_info.city)
|
||||
location = " / ".join(part for part in [ip_info.country, ip_info.province, ip_info.city] if part) or ip_info.location or "未知"
|
||||
@@ -652,14 +725,28 @@ async def get_ip_locations(
|
||||
bucket["total_count"] += 1
|
||||
bucket["allowed_count" if allowed else "denied_count"] += 1
|
||||
bucket["ip_addresses"].add(ip_address)
|
||||
bucket["user_ids"].add(user_id)
|
||||
if user_id:
|
||||
bucket["user_ids"].add(str(user_id))
|
||||
total_count += 1
|
||||
if allowed:
|
||||
allowed_count += 1
|
||||
else:
|
||||
denied_count += 1
|
||||
all_ip_addresses.add(ip_address)
|
||||
all_user_ids.add(user_id)
|
||||
if user_id:
|
||||
all_user_ids.add(str(user_id))
|
||||
|
||||
for ip_address, user_id, allowed in result.all():
|
||||
add_ip_location_row(ip_address, user_id, allowed)
|
||||
|
||||
for client_ip, user_identifier, auth_status, status_code in security_result.all():
|
||||
user_id = None
|
||||
if auth_status == "AUTHENTICATED" and user_identifier:
|
||||
try:
|
||||
user_id = uuid.UUID(user_identifier)
|
||||
except ValueError:
|
||||
user_id = user_identifier
|
||||
add_ip_location_row(client_ip, user_id, status_code < 400)
|
||||
|
||||
items = sorted(buckets.values(), key=lambda item: item["total_count"], reverse=True)[:limit]
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user