完善权限监控与安全中心
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 {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""监控API测试:权限系统监控API端点验证。"""
|
||||
|
||||
import inspect
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
@@ -12,12 +13,12 @@ from app.api.v1.system_permissions import list_system_permissions
|
||||
|
||||
|
||||
class FakeIpInfo:
|
||||
def __init__(self, province: str, city: str, isp: str = "电信") -> None:
|
||||
self.country = "中国"
|
||||
def __init__(self, province: str, city: str, isp: str = "电信", country: str = "中国") -> None:
|
||||
self.country = country
|
||||
self.province = province
|
||||
self.city = city
|
||||
self.isp = isp
|
||||
self.location = f"中国 / {province} / {city} / {isp}"
|
||||
self.location = " / ".join(part for part in [country, province, city, isp] if part)
|
||||
|
||||
|
||||
class AdminUserStub:
|
||||
@@ -526,6 +527,117 @@ async def test_ip_locations_keeps_private_network_location_label(db_session, mon
|
||||
assert result["items"][0]["city"] == ""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ip_locations_includes_abnormal_security_ips(db_session, monkeypatch):
|
||||
"""来源分析应同时统计安全事件中的异常来源 IP。"""
|
||||
await db_session.execute(text("DELETE FROM permission_access_logs"))
|
||||
await db_session.execute(text("DELETE FROM security_access_logs"))
|
||||
await db_session.commit()
|
||||
|
||||
study_id = "00000000-0000-0000-0000-000000000801"
|
||||
user_id = "00000000-0000-0000-0000-000000000901"
|
||||
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles)
|
||||
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": study_id,
|
||||
"code": "IP-LOCATION-SECURITY-STUDY",
|
||||
"name": "IP Location Security Study",
|
||||
"status": "ACTIVE",
|
||||
"is_locked": False,
|
||||
"visit_schedule": "[]",
|
||||
"active_roles": "[]",
|
||||
},
|
||||
)
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO users (id, email, password_hash, full_name, clinical_department, is_admin, status)
|
||||
VALUES (:id, :email, :password_hash, :full_name, :clinical_department, :is_admin, :status)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": user_id,
|
||||
"email": "source-analysis@example.com",
|
||||
"password_hash": "hash",
|
||||
"full_name": "来源分析用户",
|
||||
"clinical_department": "临床运营",
|
||||
"is_admin": False,
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
)
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO permission_access_logs
|
||||
(id, study_id, user_id, endpoint_key, role, allowed, elapsed_ms, ip_address, created_at)
|
||||
VALUES
|
||||
(:id, :study_id, :user_id, :endpoint_key, :role, :allowed, :elapsed_ms, :ip_address, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"study_id": study_id,
|
||||
"user_id": user_id,
|
||||
"endpoint_key": "admin.permissions.read",
|
||||
"role": "PM",
|
||||
"allowed": True,
|
||||
"elapsed_ms": 3.2,
|
||||
"ip_address": "10.3.1.1",
|
||||
},
|
||||
)
|
||||
for ip_address in ["8.8.8.8", "1.1.1.1"]:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO security_access_logs
|
||||
(id, method, path, status_code, elapsed_ms, client_ip, user_agent, auth_status, user_identifier, created_at)
|
||||
VALUES
|
||||
(:id, :method, :path, :status_code, :elapsed_ms, :client_ip, :user_agent, :auth_status, :user_identifier, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"method": "GET",
|
||||
"path": "/api/v1/admin",
|
||||
"status_code": 403,
|
||||
"elapsed_ms": 8.5,
|
||||
"client_ip": ip_address,
|
||||
"user_agent": "pytest",
|
||||
"auth_status": "ANONYMOUS",
|
||||
"user_identifier": None,
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
ip_info_by_address = {
|
||||
"10.3.1.1": FakeIpInfo("", "", "", "局域网"),
|
||||
"8.8.8.8": FakeIpInfo("California", "Mountain View", "Google", "United States"),
|
||||
"1.1.1.1": FakeIpInfo("Queensland", "Brisbane", "Cloudflare", "Australia"),
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
permission_monitoring,
|
||||
"resolve_ip_location",
|
||||
lambda ip: ip_info_by_address[ip],
|
||||
)
|
||||
|
||||
result = await permission_monitoring.get_ip_locations(db=db_session, _=AdminUserStub(), days=7, limit=10)
|
||||
locations = {item["country"]: item for item in result["items"]}
|
||||
|
||||
assert result["summary"]["total_count"] == 3
|
||||
assert result["summary"]["denied_count"] == 2
|
||||
assert result["summary"]["unique_ip_count"] == 3
|
||||
assert result["summary"]["unique_user_count"] == 1
|
||||
assert locations["United States"]["total_count"] == 1
|
||||
assert locations["United States"]["denied_count"] == 1
|
||||
assert locations["Australia"]["total_count"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_access_logs_include_user_behavior_summary(db_session):
|
||||
"""访问日志应返回用户行为审计汇总和用户排行。"""
|
||||
@@ -637,10 +749,121 @@ async def test_access_logs_include_user_behavior_summary(db_session):
|
||||
assert user_a_ips == {"10.1.1.1", "10.1.1.2"}
|
||||
|
||||
|
||||
def test_access_logs_user_stats_query_avoids_postgresql_uuid_max():
|
||||
"""访问日志用户统计不能对 UUID 字段使用 max,PostgreSQL 不支持 max(uuid)。"""
|
||||
source = inspect.getsource(permission_monitoring.get_access_logs)
|
||||
|
||||
assert "func.max(PermissionAccessLog.user_id)" not in source
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_security_access_logs_include_anonymous_ip_attempts(db_session):
|
||||
async def test_access_logs_ip_ranking_aggregates_by_ip_total(db_session):
|
||||
"""IP 访问排行应按同一 IP 的整体访问量聚合排序,而不是按用户/IP/角色拆分。"""
|
||||
await db_session.execute(text("DELETE FROM permission_access_logs"))
|
||||
await db_session.commit()
|
||||
|
||||
study_id = "00000000-0000-0000-0000-000000001201"
|
||||
user_a = "00000000-0000-0000-0000-000000001301"
|
||||
user_b = "00000000-0000-0000-0000-000000001302"
|
||||
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO studies (id, code, name, status, is_locked, visit_schedule, active_roles)
|
||||
VALUES (:id, :code, :name, :status, :is_locked, :visit_schedule, :active_roles)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": study_id,
|
||||
"code": "ACCESS-IP-RANK-STUDY",
|
||||
"name": "Access IP Rank Study",
|
||||
"status": "ACTIVE",
|
||||
"is_locked": False,
|
||||
"visit_schedule": "[]",
|
||||
"active_roles": "[]",
|
||||
},
|
||||
)
|
||||
for user_id, email, name in [
|
||||
(user_a, "ip-rank-a@example.com", "IP排行用户A"),
|
||||
(user_b, "ip-rank-b@example.com", "IP排行用户B"),
|
||||
]:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO users (id, email, password_hash, full_name, clinical_department, is_admin, status)
|
||||
VALUES (:id, :email, :password_hash, :full_name, :clinical_department, :is_admin, :status)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": user_id,
|
||||
"email": email,
|
||||
"password_hash": "hash",
|
||||
"full_name": name,
|
||||
"clinical_department": "临床运营",
|
||||
"is_admin": False,
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
)
|
||||
|
||||
rows = [
|
||||
(user_a, "183.230.169.20", True),
|
||||
(user_a, "183.230.169.20", True),
|
||||
(user_b, "183.230.169.20", False),
|
||||
(user_a, "45.148.10.95", False),
|
||||
(user_b, "52.53.218.145", False),
|
||||
]
|
||||
for user, ip_address, allowed in rows:
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO permission_access_logs
|
||||
(id, study_id, user_id, endpoint_key, role, allowed, elapsed_ms, ip_address, created_at)
|
||||
VALUES
|
||||
(:id, :study_id, :user_id, :endpoint_key, :role, :allowed, :elapsed_ms, :ip_address, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"study_id": study_id,
|
||||
"user_id": user,
|
||||
"endpoint_key": "admin.permissions.read",
|
||||
"role": "CRA",
|
||||
"allowed": allowed,
|
||||
"elapsed_ms": 4.0,
|
||||
"ip_address": ip_address,
|
||||
},
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
result = await permission_monitoring.get_access_logs(
|
||||
db=db_session,
|
||||
_=AdminUserStub(),
|
||||
study_id=None,
|
||||
user_id=None,
|
||||
endpoint_key=None,
|
||||
role=None,
|
||||
allowed=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
|
||||
assert result["user_stats"][0]["sample_ip_address"] == "183.230.169.20"
|
||||
assert result["user_stats"][0]["total_count"] == 3
|
||||
assert result["user_stats"][0]["denied_count"] == 1
|
||||
assert {stat["sample_ip_address"] for stat in result["user_stats"][1:]} == {"52.53.218.145", "45.148.10.95"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_security_access_logs_include_anonymous_ip_attempts(db_session, monkeypatch):
|
||||
"""安全访问日志应覆盖未登录或未知账号的底层访问尝试。"""
|
||||
await db_session.execute(text("DELETE FROM security_access_logs"))
|
||||
monkeypatch.setattr(
|
||||
permission_monitoring,
|
||||
"resolve_ip_location",
|
||||
lambda ip: FakeIpInfo("上海市", "上海市", "联通"),
|
||||
)
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
@@ -667,5 +890,113 @@ async def test_security_access_logs_include_anonymous_ip_attempts(db_session):
|
||||
assert result["summary"]["anonymous_count"] == 1
|
||||
assert result["summary"]["error_count"] == 1
|
||||
assert result["items"][0]["client_ip"] == "203.0.113.10"
|
||||
assert result["items"][0]["ip_location"] == "中国 / 上海市 / 上海市 / 联通"
|
||||
assert result["items"][0]["ip_country"] == "中国"
|
||||
assert result["items"][0]["ip_province"] == "上海市"
|
||||
assert result["items"][0]["ip_city"] == "上海市"
|
||||
assert result["items"][0]["ip_isp"] == "联通"
|
||||
assert result["items"][0]["account_label"] == "未知账号"
|
||||
assert result["items"][0]["status_code"] == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_security_access_logs_classify_sensitive_path_probe(db_session):
|
||||
"""安全中心应把敏感路径探测识别为严重安全事件。"""
|
||||
await db_session.execute(text("DELETE FROM security_access_logs"))
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO security_access_logs
|
||||
(id, method, path, status_code, elapsed_ms, client_ip, user_agent, auth_status, user_identifier, created_at)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000502', 'GET', '/api/.git/config', 404, 0.7,
|
||||
'198.51.100.20', 'probe-bot/1.0', 'ANONYMOUS', NULL, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
result = await permission_monitoring.get_security_access_logs(
|
||||
db=db_session,
|
||||
_=AdminUserStub(),
|
||||
status_min=400,
|
||||
auth_status=None,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
assert result["items"][0]["category"] == "PROBE"
|
||||
assert result["items"][0]["severity"] == "CRITICAL"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_security_access_logs_classify_non_china_ip_as_abnormal(db_session, monkeypatch):
|
||||
"""安全事件明细应把非中国公网 IP 归类为异常 IP。"""
|
||||
await db_session.execute(text("DELETE FROM security_access_logs"))
|
||||
monkeypatch.setattr(
|
||||
permission_monitoring,
|
||||
"resolve_ip_location",
|
||||
lambda ip: FakeIpInfo("California", "San Jose", "Google", "United States"),
|
||||
)
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO security_access_logs
|
||||
(id, method, path, status_code, elapsed_ms, client_ip, user_agent, auth_status, user_identifier, created_at)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000504', 'GET', '/api/v1/studies/', 401, 8.4,
|
||||
'52.53.218.145', 'foreign-client/1.0', 'INVALID_TOKEN', NULL, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
result = await permission_monitoring.get_security_access_logs(
|
||||
db=db_session,
|
||||
_=AdminUserStub(),
|
||||
status_min=400,
|
||||
auth_status=None,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
item = result["items"][0]
|
||||
assert item["category"] == "ABNORMAL_IP"
|
||||
assert item["severity"] == "HIGH"
|
||||
assert item["ip_location"] == "United States / California / San Jose / Google"
|
||||
assert item["ip_country"] == "United States"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_security_access_logs_resolve_public_ip_with_packaged_ip2region(db_session):
|
||||
"""安全中心公网 IP 应复用 ip2region 解析结果,避免前端只能显示未知位置。"""
|
||||
await db_session.execute(text("DELETE FROM security_access_logs"))
|
||||
await db_session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO security_access_logs
|
||||
(id, method, path, status_code, elapsed_ms, client_ip, user_agent, auth_status, user_identifier, created_at)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000503', 'GET', '/api/v1/auth/login', 404, 1.2,
|
||||
'52.53.218.145', 'probe-bot/1.0', 'ANONYMOUS', NULL, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
)
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
result = await permission_monitoring.get_security_access_logs(
|
||||
db=db_session,
|
||||
_=AdminUserStub(),
|
||||
status_min=400,
|
||||
auth_status=None,
|
||||
page=1,
|
||||
page_size=20,
|
||||
)
|
||||
|
||||
item = result["items"][0]
|
||||
assert item["client_ip"] == "52.53.218.145"
|
||||
assert item["ip_location"] not in {"", "公网", "未知"}
|
||||
assert item["ip_country"] == "United States"
|
||||
assert item["ip_province"] == "California"
|
||||
assert item["ip_city"] == "San Jose"
|
||||
assert item["category"] == "ABNORMAL_IP"
|
||||
|
||||
Reference in New Issue
Block a user