Spaces:
Sleeping
Sleeping
File size: 12,437 Bytes
7d67a69 14640e3 7d67a69 fb04509 7d67a69 fb04509 7d67a69 fb04509 7d67a69 14640e3 7d67a69 14640e3 fb04509 7d67a69 fb04509 7d67a69 fb04509 7d67a69 14640e3 fb04509 7d67a69 51c5a22 97a461c 51c5a22 97a461c 51c5a22 7d67a69 14640e3 7d67a69 14640e3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | # models.py
# 此檔案負責機器學習模型的載入、資料處理和異常偵測邏輯
# 包含 HDBSCAN 模型、特徵工程和預測功能
import os
import pandas as pd
import numpy as np
import joblib
import datetime
from config import MODEL_FILES, ACTION_MAP, SYSTEM_WEIGHTS, ENABLE_HDBSCAN
from utils import get_ip_category, get_geo_level, get_current_time
import gradio as gr
# 全域變數
le_action = None
baseline_df = None
baseline_X = None
AI_READY = False
DATA_READY = False # 新增:標記資料是否載入成功
# 全域統計與狀態變數
global_stats = {
"total_count": 0,
"abnormal_count": 0,
"start_time": datetime.datetime.now()
}
# 日誌歷史
log_history = []
def load_models_and_data():
"""載入模型和歷史資料"""
global le_action, baseline_df, baseline_X, AI_READY, DATA_READY, global_stats, log_history
# 1. 優先載入資料 (圖表顯示核心)
try:
print("📂 正在從雲端獲取最新資料 (Hugging Face API)...")
try:
import logger_service
cloud_df = logger_service.sync_from_hf()
if cloud_df is not None:
baseline_df = cloud_df
else:
raise Exception("Cloud sync returned None")
except Exception as e:
print(f"ℹ️ 使用本地備份資料庫 (雲端同步跳過: {e})")
if os.path.exists(MODEL_FILES["baseline_logs"]):
baseline_df = pd.read_csv(MODEL_FILES["baseline_logs"])
else:
baseline_df = pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"])
baseline_df.to_csv(MODEL_FILES["baseline_logs"], index=False)
baseline_df['timestamp'] = pd.to_datetime(baseline_df['timestamp'])
baseline_df['clean_ip'] = baseline_df['ip_address'].astype(str).apply(lambda x: x.split(" ")[0])
global_stats["total_count"] = len(baseline_df)
global_stats["abnormal_count"] = baseline_df[baseline_df['status'].str.contains("異常", na=False)].shape[0]
print(f"📊 統計初始化:總監測量 {global_stats['total_count']}, 異常數 {global_stats['abnormal_count']}")
# 準備顯示資料
display_df = baseline_df.sort_values(by='timestamp', ascending=False).head(20).copy()
display_df['timestamp'] = display_df['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S')
new_logs = display_df[['timestamp', 'ip_address', 'cookie_id', 'account', 'role', 'action', 'status']].to_dict('records')
log_history.clear()
log_history.extend(new_logs)
DATA_READY = True
print("✅ 歷史基準資料載入成功!")
except Exception as e:
DATA_READY = False
print(f"❌ 資料載入失敗: {e}")
# 2. 嘗試載入 AI 模型 (異常偵測核心)
try:
# 載入標籤編碼器
le_action = joblib.load(MODEL_FILES["label_encoder"])
# 準備特徵矩陣
baseline_df['month'] = baseline_df['timestamp'].dt.month
baseline_df['day'] = baseline_df['timestamp'].dt.day
baseline_df['hour'] = baseline_df['timestamp'].dt.hour
ip_codes = np.array([get_ip_category(ip) for ip in baseline_df['clean_ip'].values])
actions = baseline_df['action'].astype(str).values
known_actions = set(le_action.classes_)
unknown_actions = set(actions) - known_actions
if unknown_actions:
le_action.classes_ = np.append(le_action.classes_, list(unknown_actions))
baseline_X = np.column_stack((
baseline_df['month'].values, baseline_df['day'].values, baseline_df['hour'].values,
ip_codes, le_action.transform(actions)
))
AI_READY = True
print("✅ AI 模組載入成功!")
except Exception as e:
AI_READY = False
if "KeyError: 118" in str(e) or "version https://git-lfs" in str(e):
print(f"⚠️ AI 模組載入失敗:偵測到 Git LFS 指標檔案。請確保已安裝 git-lfs 並執行 `git lfs pull` 下載實際模型檔。")
else:
print(f"⚠️ AI 模組載入失敗 (不影響基本圖表顯示)。錯誤: {e}")
def check_anomaly(ip, account, action, log_time):
"""核心異常偵測邏輯"""
if not AI_READY:
return "success"
if action in ["login_attempt", "logout"]:
return "success"
if not ENABLE_HDBSCAN:
return "success"
try:
import hdbscan # lazy import
except Exception:
return "success"
try:
ow = float(SYSTEM_WEIGHTS["ow"])
tw = float(SYSTEM_WEIGHTS["tw"])
iw = float(SYSTEM_WEIGHTS["iw"])
dw = float(SYSTEM_WEIGHTS["dw"])
gw = float(SYSTEM_WEIGHTS["gw"])
fw = float(SYSTEM_WEIGHTS["fw"])
geo_level = get_geo_level(ip)
# 地理位置檢查
if gw == 100 and geo_level > 0:
return "⚠️異常_地理位置受限 (100%絕對鎖定: 僅限校內專網存取)"
elif gw >= 90 and geo_level > 1:
return "⚠️異常_地理位置受限 (高敏感防護: 僅限校園網路與宿舍)"
elif gw >= 70 and geo_level > 2:
return "⚠️異常_地理位置受限 (進階防護: 禁止公共場所與海外連線)"
elif gw >= 50 and geo_level > 3:
return "⚠️異常_地理位置受限 (預設防護: 禁止海外異常 IP)"
current_dt = pd.to_datetime(str(log_time))
cutoff_dt = current_dt - pd.Timedelta(seconds=60)
# 頻率檢查
recent_clicks = 0
for log in log_history:
log_dt = pd.to_datetime(log['timestamp'])
if log_dt >= cutoff_dt:
if log.get('account') == account and log.get('action') == action:
recent_clicks += 1
else:
break
if fw <= 0:
max_allowed_clicks = 100
elif fw <= 10:
max_allowed_clicks = 90
elif fw <= 20:
max_allowed_clicks = 80
elif fw <= 30:
max_allowed_clicks = 70
elif fw <= 40:
max_allowed_clicks = 60
elif fw <= 50:
max_allowed_clicks = 50
elif fw <= 60:
max_allowed_clicks = 40
elif fw <= 70:
max_allowed_clicks = 30
elif fw <= 80:
max_allowed_clicks = 15
elif fw <= 90:
max_allowed_clicks = 9
else:
max_allowed_clicks = 3
if (recent_clicks + 1) > max_allowed_clicks:
return f"⚠️異常_單一操作頻率過高 ({recent_clicks+1}次/分)"
# HDBSCAN 異常偵測
ip_code = float(get_ip_category(ip))
action_code = float(le_action.transform([action])[0])
current_month = float(current_dt.month)
current_day = float(current_dt.day)
current_hour = float(current_dt.hour)
time_multiplier = tw / 100.0
action_scale = (dw / 100.0) * 100.0
scaled_baseline = baseline_X.copy().astype(float)
scaled_baseline[:, 0] *= time_multiplier * 10.0
scaled_baseline[:, 1] *= time_multiplier * 0.5
scaled_baseline[:, 2] *= time_multiplier * 2.0
scaled_baseline[:, 3] *= (iw / 100.0) * 10.0
scaled_baseline[:, 4] *= action_scale
v_month = current_month * time_multiplier * 10.0
v_day = current_day * time_multiplier * 0.5
v_hour = current_hour * time_multiplier * 2.0
v_ip = ip_code * (iw / 100.0) * 10.0
v_action = action_code * action_scale
new_data_point = np.array([[v_month, v_day, v_hour, v_ip, v_action]], dtype=float)
same_action_mask = (baseline_X[:, 4] == action_code)
relevant_history = scaled_baseline[same_action_mask]
other_history = scaled_baseline[~same_action_mask]
if len(other_history) > 800:
np.random.seed(int(current_day + current_hour))
indices = np.random.choice(len(other_history), 800, replace=False)
other_history = other_history[indices]
fit_X = np.vstack((other_history, relevant_history, new_data_point))
strictness = max(2, int(2 + (ow / 100.0) * 4))
model = hdbscan.HDBSCAN(min_cluster_size=strictness, min_samples=2)
clusters = model.fit_predict(fit_X)
if clusters[-1] == -1:
return "⚠️異常_作息或行為不符"
if np.any(same_action_mask):
distances = np.linalg.norm(relevant_history - new_data_point[0], axis=1)
min_distance = np.min(distances)
max_tolerance_distance = time_multiplier * 15.0 + 1.0
if ip_code >= 3.0:
max_tolerance_distance *= (1 - (iw / 100.0) * 0.6)
if min_distance > max_tolerance_distance:
return "⚠️異常_作息或行為不符"
return "success"
except ValueError:
return "⚠️異常_未知特徵 (出現未授權的 IP 或操作)"
except Exception as e:
return f"⚠️系統錯誤: {str(e)}"
def record_log(ip, cookie, account, role, action, status, time_mode, custom_time):
"""記錄日誌"""
global global_stats, log_history, baseline_X
if time_mode == "自訂時間 (模擬過去/未來)" and custom_time and str(custom_time).strip() != "":
try:
valid_time = pd.to_datetime(str(custom_time))
log_time = valid_time.strftime("%Y-%m-%d %H:%M:%S")
except Exception:
log_time = get_current_time()
status = "⚠️格式錯誤_無效的自訂時間"
else:
log_time = get_current_time()
if status == "success":
ai_judgment = check_anomaly(ip, account, action, log_time)
if ai_judgment != "success":
status = ai_judgment
# 如果是異常行為,觸發安全警報與驗證流程(以帳戶名稱作為 session key)
if "異常" in status or "受限" in status:
try:
from security import trigger_identity_verification
from config import FIXED_TEST_EMAIL
# 以帳戶名稱作為識別 key,信件收件人統一使用 FIXED_TEST_EMAIL
trigger_identity_verification(account, FIXED_TEST_EMAIL, log_time)
print(f"🚨 [Security Alert Triggered] Account: {account}, Status: {status}")
except Exception as e:
print(f"❌ [Security Alert Error] {e}")
new_log = {
"timestamp": log_time,
"ip_address": ip,
"cookie_id": cookie,
"account": account,
"role": role,
"action": action,
"status": status
}
try:
import logger_service
logger_service.push_new_log(new_log)
except:
pass
global_stats["total_count"] += 1
if "異常" in str(status) or "錯誤" in str(status) or "受限" in str(status):
global_stats["abnormal_count"] += 1
log_history.insert(0, new_log)
if status == "success" and AI_READY:
try:
current_time = pd.to_datetime(str(log_time))
if action not in le_action.classes_:
le_action.classes_ = np.append(le_action.classes_, action)
new_row = np.array([[
float(current_time.month),
float(current_time.day),
float(current_time.hour),
float(get_ip_category(ip)),
float(le_action.transform([action])[0])
]], dtype=float)
baseline_X = np.vstack((baseline_X, new_row))
except:
pass
df_all = pd.DataFrame(log_history)
abnormal_logs = [log for log in log_history if "異常" in str(log.get("status", "")) or "錯誤" in str(log.get("status", "")) or "受限" in str(log.get("status", ""))]
df_abnormal = pd.DataFrame(abnormal_logs) if abnormal_logs else pd.DataFrame(columns=df_all.columns)
# 建立 Gradio 更新物件,用於更新管理員介面的異常 Log 文字框
if "異常" in str(status) or "錯誤" in str(status) or "受限" in str(status):
inp_upd = gr.update(value=f"時間: {log_time} | IP: {ip} | 帳號: {account} | 動作: {action} | 狀態: {status}")
else:
inp_upd = gr.update(value="")
return df_all, df_abnormal, inp_upd |