Spaces:
Sleeping
Sleeping
| # 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 |