v3.1 + 批次0: 智能回复重构基线 - ApprovalMatcher + 关键词降级 + 文档速修 + v4.0任务书面化
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — Token异常检测定时任务
|
||||
# =============================================================================
|
||||
# 说明:定时检测Token异常使用行为:
|
||||
# 1. 同一Token在多个不同IP使用(可能凭证泄露)
|
||||
# 2. 检测时间窗口:1小时内
|
||||
# 3. 告警阈值:>=3个不同IP
|
||||
# 运行频率:每5分钟执行一次
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
from app.config import settings
|
||||
from app.services.token_service import TokenService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 异常检测配置
|
||||
TOKEN_ANOMALY_IP_THRESHOLD = 3 # 同一Token使用不同IP数量阈值
|
||||
TOKEN_ANOMALY_TIME_WINDOW = 3600 # 检测时间窗口(秒)
|
||||
|
||||
# Redis Key前缀
|
||||
TOKEN_IP_PREFIX = "token_ip:"
|
||||
|
||||
|
||||
async def detect_token_ip_anomaly():
|
||||
"""检测Token异常使用行为。
|
||||
|
||||
检测逻辑:
|
||||
1. 扫描所有 token_ip:* 的Key
|
||||
2. 解析IP列表,统计不同IP数量
|
||||
3. 超过阈值则触发告警
|
||||
"""
|
||||
try:
|
||||
# 创建Redis客户端
|
||||
redis_client = settings.create_redis_client()
|
||||
token_service = TokenService(redis_client)
|
||||
|
||||
# 扫描所有Token IP记录
|
||||
cursor = 0
|
||||
anomaly_count = 0
|
||||
anomalies = []
|
||||
|
||||
while True:
|
||||
cursor, keys = await redis_client.scan(
|
||||
cursor=cursor,
|
||||
match=f"{TOKEN_IP_PREFIX}*",
|
||||
count=100
|
||||
)
|
||||
|
||||
for key in keys:
|
||||
# 解析key获取token_hash
|
||||
key_str = key.decode("utf-8") if isinstance(key, bytes) else key
|
||||
token_hash = key_str.replace(f"{TOKEN_IP_PREFIX}", "")
|
||||
|
||||
# 获取IP记录
|
||||
data = await redis_client.get(key)
|
||||
if not data:
|
||||
continue
|
||||
|
||||
data_str = data.decode("utf-8") if isinstance(data, bytes) else data
|
||||
|
||||
# 解析IP列表
|
||||
ips = set()
|
||||
for entry in data_str.split(","):
|
||||
if "@" in entry:
|
||||
ip, _ = entry.rsplit("@", 1)
|
||||
ips.add(ip)
|
||||
|
||||
# 检测异常
|
||||
if len(ips) >= TOKEN_ANOMALY_IP_THRESHOLD:
|
||||
anomaly_count += 1
|
||||
anomalies.append({
|
||||
"token_hash": token_hash,
|
||||
"ip_count": len(ips),
|
||||
"ips": list(ips),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
logger.warning(
|
||||
f"检测到Token异常使用: token_hash={token_hash}, "
|
||||
f"ip_count={len(ips)}, ips={list(ips)}"
|
||||
)
|
||||
|
||||
if cursor == 0:
|
||||
break
|
||||
|
||||
# 发送告警
|
||||
if anomalies:
|
||||
await _send_anomaly_alert(anomalies)
|
||||
|
||||
logger.info(f"Token异常检测完成: 检测到 {anomaly_count} 个异常")
|
||||
|
||||
await redis_client.aclose()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Token异常检测任务执行失败: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def _send_anomaly_alert(anomalies: list):
|
||||
"""发送Token异常告警。
|
||||
|
||||
通过企微机器人发送告警消息。
|
||||
|
||||
Args:
|
||||
anomalies: 异常列表
|
||||
"""
|
||||
if not anomalies:
|
||||
return
|
||||
|
||||
# 检查是否配置了webhook
|
||||
webhook = getattr(settings, "content_audit_webhook", None)
|
||||
if not webhook:
|
||||
logger.warning("未配置 content_audit_webhook,跳过告警")
|
||||
return
|
||||
|
||||
try:
|
||||
import httpx
|
||||
|
||||
# 构建告警消息
|
||||
lines = ["🚨 **Token异常使用告警**\n"]
|
||||
lines.append(f"**检测时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
lines.append(f"**异常数量**: {len(anomalies)}")
|
||||
lines.append("\n**异常详情:**")
|
||||
|
||||
for i, a in enumerate(anomalies[:5], 1): # 最多显示5条
|
||||
ips_str = ", ".join(a["ips"][:3])
|
||||
if len(a["ips"]) > 3:
|
||||
ips_str += f" ... (+{len(a['ips']) - 3} more)"
|
||||
lines.append(f"{i}. Token: `{a['token_hash'][:8]}...`")
|
||||
lines.append(f" IP数量: {a['ip_count']}, IPs: {ips_str}")
|
||||
|
||||
if len(anomalies) > 5:
|
||||
lines.append(f"\n... 还有 {len(anomalies) - 5} 条异常")
|
||||
|
||||
lines.append("\n⚠️ **建议**: 立即检查是否为凭证泄露,必要时禁用相关账户")
|
||||
|
||||
content = "\n".join(lines)
|
||||
|
||||
# 发送企微机器人消息
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
await client.post(
|
||||
webhook,
|
||||
json={
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": content
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Token异常告警已发送: {len(anomalies)} 条")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送Token异常告警失败: {e}")
|
||||
|
||||
|
||||
async def test_token_anomaly_detection():
|
||||
"""测试用:模拟Token多IP使用场景。
|
||||
|
||||
创建测试数据,验证检测逻辑。
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
redis_client = settings.create_redis_client()
|
||||
token_service = TokenService(redis_client)
|
||||
|
||||
# 生成测试token
|
||||
test_token = "test_token_anomaly_12345"
|
||||
token_hash = hashlib.sha256(test_token.encode()).hexdigest()[:16]
|
||||
|
||||
# 模拟同一token使用3个不同IP
|
||||
key = f"{TOKEN_IP_PREFIX}{token_hash}"
|
||||
test_data = (
|
||||
"192.168.1.100@2026-07-14T10:00:00,"
|
||||
"192.168.1.101@2026-07-14T10:05:00,"
|
||||
"192.168.1.102@2026-07-14T10:10:00"
|
||||
)
|
||||
|
||||
await redis_client.setex(key, 3600, test_data)
|
||||
logger.info(f"测试数据已创建: key={key}, data={test_data}")
|
||||
|
||||
await redis_client.aclose()
|
||||
logger.info("测试数据创建完成,请运行检测任务验证")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
# 直接运行时执行测试
|
||||
asyncio.run(test_token_anomaly_detection())
|
||||
Reference in New Issue
Block a user