Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import datetime | |
| import pandas as pd | |
| import warnings | |
| import joblib | |
| import hdbscan | |
| import numpy as np | |
| import logger_service | |
| import os | |
| import urllib.request | |
| import matplotlib.pyplot as plt | |
| import seaborn as sns | |
| import matplotlib.font_manager as fm | |
| import shutil | |
| import io | |
| import base64 | |
| # --- 🌟 解決 Matplotlib 中文顯示問題 --- | |
| font_path = 'SimHei.ttf' | |
| if not os.path.exists(font_path): | |
| try: | |
| print("📥 正在下載中文字型...") | |
| urllib.request.urlretrieve('https://github.com/StellarCN/scp_zh/raw/master/fonts/SimHei.ttf', font_path) | |
| except Exception as e: | |
| print(f"⚠️ 字型下載失敗: {e}") | |
| if os.path.exists(font_path): | |
| fm.fontManager.addfont(font_path) | |
| plt.rc('font', family='SimHei') | |
| plt.rcParams['axes.unicode_minus'] = False | |
| # Seaborn 近期會對舊的 pandas option 發出 FutureWarning(純警告、非功能錯誤) | |
| # 重整前端會觸發繪圖端點,因此你會在 terminal 看到重複訊息。 | |
| warnings.filterwarnings( | |
| "ignore", | |
| category=FutureWarning, | |
| message="use_inf_as_na option is deprecated*", | |
| ) | |
| # --- 全域統計與狀態變數 --- | |
| global_stats = { | |
| "total_count": 0, | |
| "abnormal_count": 0, | |
| "start_time": datetime.datetime.now() | |
| } | |
| system_weights = { | |
| "ow": 75.0, "tw": 80.0, "iw": 65.0, | |
| "dw": 45.0, "gw": 50.0, "fw": 60.0 | |
| } | |
| def update_system_weights(ow, tw, iw, dw, gw, fw): | |
| system_weights.update({ | |
| "ow": float(ow), "tw": float(tw), "iw": float(iw), | |
| "dw": float(dw), "gw": float(gw), "fw": float(fw) | |
| }) | |
| return f"✅ 系統權重已更新 - 總體: {ow}%, 時間: {tw}%, IP: {iw}%, 裝置: {dw}%, 地理: {gw}%, 頻率: {fw}%" | |
| ACTION_MAP = { | |
| "教務系統: 期末網路教學評量": "click_eval_system", | |
| "教務系統: 期末網路預選系統": "click_pre_select_system", | |
| "教務系統: 開學後加退選系統": "click_add_drop_system", | |
| "教務系統: 北科i學園PLUS": "click_istudy", | |
| "教務系統: 學生證掛失及補發系統": "click_card_loss", | |
| "教務系統: 課程系統": "click_course_system", | |
| "教務系統: 學業成績查詢系統": "click_score_query", | |
| "學務系統: 學生請假系統": "click_leave_system", | |
| "學務系統: 學生宿舍登錄(抽籤)系統": "click_dorm_system", | |
| "學務系統: 獎助學金申請系統": "click_scholarship", | |
| "資訊服務: 網路郵局 WebMail": "click_webmail", | |
| "資訊服務: 北科軟體雲": "click_vdesk", | |
| "惡意測試: 嘗試偷改成績 (SQL Injection)": "malicious_sql_injection" | |
| } | |
| log_history = [] | |
| initial_df_abn = pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"]) | |
| # HDBSCAN 特徵分群用的 IP 分類 | |
| def get_ip_category(ip_str): | |
| ip = str(ip_str).split(" ")[0] | |
| if ip.startswith("140.124."): return 0.0 | |
| elif ip.startswith("61.228."): return 1.0 | |
| elif ip.startswith("210.61."): return 2.0 | |
| elif ip.startswith("45.33."): return 3.0 | |
| else: return 4.0 | |
| def get_geo_level(ip_str): | |
| ip = str(ip_str).split(" ")[0] | |
| if ip == "140.124.71.55": return 0 | |
| elif ip == "140.124.18.22": return 1 | |
| elif ip.startswith("61.228."): return 2 | |
| elif ip.startswith("210.61."): return 3 | |
| elif ip.startswith("45.33."): return 4 | |
| else: return 5 | |
| # --- 1. 載入模型與歷史資料 --- | |
| try: | |
| le_action = joblib.load('le_action.pkl') | |
| print("📂 正在讀取本地歷史資料庫...") | |
| if os.path.exists('baseline_logs.csv'): | |
| baseline_df = pd.read_csv('baseline_logs.csv') | |
| else: | |
| baseline_df = pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"]) | |
| baseline_df.to_csv('baseline_logs.csv', 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') | |
| log_history = display_df[['timestamp', 'ip_address', 'cookie_id', 'account', 'role', 'action', 'status']].to_dict('records') | |
| abnormal_mask = baseline_df['status'].astype(str).str.contains("異常", na=False) | |
| if abnormal_mask.any(): | |
| abn_df_temp = baseline_df[abnormal_mask].sort_values(by='timestamp', ascending=False).copy() | |
| abn_df_temp['timestamp'] = abn_df_temp['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S') | |
| initial_df_abn = abn_df_temp[['timestamp', 'ip_address', 'cookie_id', 'account', 'role', 'action', 'status']] | |
| 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 | |
| global_stats["total_count"] = 0; global_stats["abnormal_count"] = 0 | |
| print(f"⚠️ AI 模組載入失敗,請確認檔案。錯誤: {e}") | |
| USER_DB = {"student": ["1234", "學生", "同學"], "admin": ["1234", "管理", "管理員"]} | |
| # ===================================================================== | |
| # 👉 新增:純淨 API 端點專用函式 (只回傳 JSON 字典,給隊友抓資料用) | |
| # ===================================================================== | |
| def api_get_stats(): | |
| """取得系統狀態與權重""" | |
| total = global_stats["total_count"] | |
| abnormal = global_stats["abnormal_count"] | |
| rate = round((abnormal / total * 100), 2) if total > 0 else 0 | |
| return { | |
| "status": "success", | |
| "ai_ready": AI_READY, | |
| "data": { | |
| "total_logs": total, | |
| "abnormal_logs": abnormal, | |
| "anomaly_rate_percent": rate, | |
| "current_weights": system_weights | |
| } | |
| } | |
| def api_get_logs(limit=20): | |
| """取得最新的 N 筆 Log""" | |
| try: limit = int(limit) | |
| except: limit = 20 | |
| return { | |
| "status": "success", | |
| "count": len(log_history[:limit]), | |
| "data": log_history[:limit] | |
| } | |
| def api_get_chart_data(action_name): | |
| """取得特定系統的圖表原始數據 (供前端自行繪圖)""" | |
| if not AI_READY: return {"status": "error", "message": "AI 模型未載入"} | |
| action_code = ACTION_MAP.get(action_name) | |
| if not action_code: return {"status": "error", "message": f"找不到系統: {action_name}"} | |
| df_hist = baseline_df[baseline_df['action'] == action_code].copy() | |
| df_live = pd.DataFrame(log_history) | |
| if not df_live.empty: | |
| df_live['timestamp'] = pd.to_datetime(df_live['timestamp'], errors='coerce') | |
| df_live = df_live.dropna(subset=['timestamp']) | |
| df_live = df_live[df_live['action'] == action_code].copy() | |
| if not df_live.empty: | |
| df_combined = pd.concat([df_hist, df_live], ignore_index=True) | |
| df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action']) | |
| else: | |
| df_combined = df_hist.copy() | |
| if df_combined.empty: | |
| return {"status": "success", "action": action_code, "data": {"normal": [], "abnormal": []}} | |
| df_combined['month'] = df_combined['timestamp'].dt.month | |
| df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0 | |
| is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False) | |
| normal_data = df_combined[~is_abnormal][['month', 'time_of_day', 'timestamp']].astype(str).to_dict('records') | |
| abnormal_data = df_combined[is_abnormal][['month', 'time_of_day', 'timestamp', 'status']].astype(str).to_dict('records') | |
| return { | |
| "status": "success", | |
| "action_code": action_code, | |
| "data": {"normal": normal_data, "abnormal": abnormal_data} | |
| } | |
| def api_get_abnormal_logs(): | |
| """取得所有異常 Log""" | |
| 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", ""))] | |
| return { | |
| "status": "success", | |
| "count": len(abnormal_logs), | |
| "data": abnormal_logs | |
| } | |
| # ===================================================================== | |
| def get_dashboard_html(): | |
| total = global_stats["total_count"] | |
| abnormal = global_stats["abnormal_count"] | |
| rate = (abnormal / total * 100) if total > 0 else 0 | |
| if not AI_READY: status_icon, status_text, status_color, status_desc = "🔴", "系統錯誤", "#ef4444", "AI 模型載入失敗,防護停用中。" | |
| elif rate > 10: status_icon, status_text, status_color, status_desc = "🟡", "高風險警告", "#f59e0b", "近期異常行為頻發,請立刻檢查 Log。" | |
| else: status_icon, status_text, status_color, status_desc = "🟢", "正常執行", "#10b981", "HDBSCAN 與 Agent 服務運作正常。" | |
| return f""" | |
| <div class="stat-dashboard"> | |
| <div class="stat-card card-total"> | |
| <div class="card-icon">📊</div> | |
| <div class="card-content"> | |
| <div class="card-label">總監測量 (本地歷史資料庫)</div> | |
| <div class="card-value">{total:,} <span class="card-unit">筆 Log</span></div> | |
| <div class="card-sub-text">自系統啟動起統計 (+今日即時)</div> | |
| </div> | |
| </div> | |
| <div class="stat-card card-anomaly"> | |
| <div class="card-icon">⚠️</div> | |
| <div class="card-content"> | |
| <div class="card-label">異常率 (平均數值)</div> | |
| <div class="card-value-container"> | |
| <div class="card-value" style="color: #ef4444;">{rate:.2f} %</div> | |
| <div class="card-trend trend-up">↑即時</div> | |
| </div> | |
| <div class="card-sub-text">共 {abnormal:,} 筆監測到異常行為</div> | |
| </div> | |
| </div> | |
| <div class="stat-card card-status" style="border-top-color: {status_color};"> | |
| <div class="card-icon">{status_icon}</div> | |
| <div class="card-content"> | |
| <div class="card-label">系統狀態</div> | |
| <div class="card-value" style="font-size: 24px; color: {status_color};">{status_text}</div> | |
| <div class="card-sub-text">{status_desc}</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| def get_plot_data_for_api(selected_key): | |
| """API專用版本,返回Base64圖片字符串""" | |
| if not selected_key or not AI_READY: return None | |
| action_code = ACTION_MAP.get(selected_key) | |
| try: | |
| plt.close('all') | |
| df_hist = baseline_df[baseline_df['action'] == action_code].copy() | |
| df_live = pd.DataFrame(log_history) | |
| if not df_live.empty: | |
| df_live['timestamp'] = pd.to_datetime(df_live['timestamp'], errors='coerce') | |
| df_live = df_live.dropna(subset=['timestamp']) | |
| df_live = df_live[df_live['action'] == action_code].copy() | |
| if not df_live.empty: | |
| df_combined = pd.concat([df_hist, df_live], ignore_index=True) | |
| df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action']) | |
| else: | |
| df_combined = df_hist.copy() | |
| if df_combined.empty: | |
| fig, ax = plt.subplots(figsize=(12, 6)) | |
| if os.path.exists(font_path): plt.rc('font', family='SimHei') | |
| ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15) | |
| # 將圖片轉換為 Base64 | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format='png', dpi=100, bbox_inches='tight') | |
| buf.seek(0) | |
| img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8') | |
| buf.close() | |
| plt.close(fig) | |
| return f"data:image/png;base64,{img_base64}" | |
| df_combined['month'] = df_combined['timestamp'].dt.month | |
| df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0 | |
| is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False) | |
| df_normal = df_combined[~is_abnormal] | |
| df_abnormal = df_combined[is_abnormal] | |
| fig, ax = plt.subplots(figsize=(12, 6)) | |
| sns.set_theme(style="whitegrid") | |
| if os.path.exists(font_path): plt.rc('font', family='SimHei') | |
| plt.rcParams['axes.unicode_minus'] = False | |
| has_data = False | |
| months_order = list(range(1, 13)) | |
| if not df_normal.empty: | |
| has_data = True | |
| sns.stripplot(data=df_normal, x='month', y='time_of_day', jitter=0.3, alpha=0.4, size=5, color='#10b981', order=months_order, ax=ax, label='正常資料') | |
| if not df_abnormal.empty: | |
| has_data = True | |
| sns.stripplot(data=df_abnormal, x='month', y='time_of_day', jitter=0.3, alpha=0.9, size=9, color='#ef4444', marker='X', order=months_order, ax=ax, label='異常資料') | |
| if not has_data: | |
| ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15) | |
| # 將圖片轉換為 Base64 | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format='png', dpi=100, bbox_inches='tight') | |
| buf.seek(0) | |
| img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8') | |
| buf.close() | |
| plt.close(fig) | |
| return f"data:image/png;base64,{img_base64}" | |
| ax.set_xlabel('月份 (1 ~ 12月)', fontsize=12) | |
| ax.set_ylabel('時間 (24小時制)', fontsize=12) | |
| ax.set_yticks(np.arange(0, 25, 2)) | |
| ax.set_ylim(24.5, -0.5) | |
| handles, labels = ax.get_legend_handles_labels() | |
| by_label = dict(zip(labels, handles)) | |
| if by_label: ax.legend(by_label.values(), by_label.keys(), loc='upper right', bbox_to_anchor=(1.15, 1)) | |
| plt.tight_layout() | |
| # 將圖片轉換為 Base64 | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format='png', dpi=100, bbox_inches='tight') | |
| buf.seek(0) | |
| img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8') | |
| buf.close() | |
| plt.close(fig) | |
| return f"data:image/png;base64,{img_base64}" | |
| except Exception as e: | |
| print(f"繪圖失敗: {e}") | |
| # 返回一個簡單的錯誤圖片 | |
| fig, ax = plt.subplots(figsize=(8, 4)) | |
| ax.text(0.5, 0.5, f"圖表生成失敗: {str(e)}", ha='center', va='center', fontsize=12, color='red') | |
| ax.set_xlim(0, 1) | |
| ax.set_ylim(0, 1) | |
| ax.axis('off') | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format='png', dpi=100, bbox_inches='tight') | |
| buf.seek(0) | |
| img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8') | |
| buf.close() | |
| plt.close(fig) | |
| return f"data:image/png;base64,{img_base64}" | |
| def get_plot_data(selected_key): | |
| """給Gradio界面使用,返回Figure對象""" | |
| if not selected_key or not AI_READY: return None | |
| action_code = ACTION_MAP.get(selected_key) | |
| try: | |
| plt.close('all') | |
| df_hist = baseline_df[baseline_df['action'] == action_code].copy() | |
| df_live = pd.DataFrame(log_history) | |
| if not df_live.empty: | |
| df_live['timestamp'] = pd.to_datetime(df_live['timestamp'], errors='coerce') | |
| df_live = df_live.dropna(subset=['timestamp']) | |
| df_live = df_live[df_live['action'] == action_code].copy() | |
| if not df_live.empty: | |
| df_combined = pd.concat([df_hist, df_live], ignore_index=True) | |
| df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action']) | |
| else: | |
| df_combined = df_hist.copy() | |
| if df_combined.empty: | |
| fig, ax = plt.subplots(figsize=(12, 6)) | |
| if os.path.exists(font_path): plt.rc('font', family='SimHei') | |
| ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15) | |
| return fig | |
| df_combined['month'] = df_combined['timestamp'].dt.month | |
| df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0 | |
| is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False) | |
| df_normal = df_combined[~is_abnormal] | |
| df_abnormal = df_combined[is_abnormal] | |
| fig, ax = plt.subplots(figsize=(12, 6)) | |
| sns.set_theme(style="whitegrid") | |
| if os.path.exists(font_path): plt.rc('font', family='SimHei') | |
| plt.rcParams['axes.unicode_minus'] = False | |
| has_data = False | |
| months_order = list(range(1, 13)) | |
| if not df_normal.empty: | |
| has_data = True | |
| sns.stripplot(data=df_normal, x='month', y='time_of_day', jitter=0.3, alpha=0.4, size=5, color='#10b981', order=months_order, ax=ax, label='正常資料') | |
| if not df_abnormal.empty: | |
| has_data = True | |
| sns.stripplot(data=df_abnormal, x='month', y='time_of_day', jitter=0.3, alpha=0.9, size=9, color='#ef4444', marker='X', order=months_order, ax=ax, label='異常資料') | |
| if not has_data: | |
| ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15) | |
| return fig | |
| ax.set_xlabel('月份 (1 ~ 12月)', fontsize=12) | |
| ax.set_ylabel('時間 (24小時制)', fontsize=12) | |
| ax.set_yticks(np.arange(0, 25, 2)) | |
| ax.set_ylim(24.5, -0.5) | |
| handles, labels = ax.get_legend_handles_labels() | |
| by_label = dict(zip(labels, handles)) | |
| if by_label: ax.legend(by_label.values(), by_label.keys(), loc='upper right', bbox_to_anchor=(1.15, 1)) | |
| plt.tight_layout() | |
| return fig | |
| except Exception as e: | |
| print(f"繪圖失敗: {e}") | |
| return None | |
| def timer_update(selected_key): return get_dashboard_html(), get_plot_data(selected_key) | |
| def explain_abnormal_log(log_text): | |
| if not log_text or log_text.strip() == "": return "⚠️ 請先提供異常 Log 資訊以進行分析。" | |
| try: from groq import Groq | |
| except ImportError: return "⚠️ 未安裝 groq 套件。請在終端機執行 `pip install groq`。" | |
| groq_api_key = os.environ.get("GROQ_API_KEY", "") | |
| if not groq_api_key: return "⚠️ 未設定 GROQ_API_KEY。" | |
| try: | |
| client = Groq(api_key=groq_api_key) | |
| response = client.chat.completions.create( | |
| messages=[ | |
| {"role": "system", "content": "你是一位專業的資安分析師。請簡單解釋為什麼這筆 Log 異常,並指出風險(如撞庫攻擊、作息異常等)。"}, | |
| {"role": "user", "content": f"請分析這筆異常 Log:\n{log_text}"} | |
| ], | |
| model="llama-3.1-8b-instant", temperature=0.5, max_tokens=300 | |
| ) | |
| return response.choices[0].message.content | |
| except Exception as e: return f"⚠️ 呼叫 API 發生錯誤: {str(e)}" | |
| # --- 2. 核心邏輯與真實 HDBSCAN 異常偵測 --- | |
| def check_anomaly(ip, account, action, log_time): | |
| if not AI_READY: return "success" | |
| if action in ["login_attempt", "logout"]: 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}次/分)" | |
| 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 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 = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))).strftime("%Y-%m-%d %H:%M:%S") | |
| status = "⚠️格式錯誤_無效的自訂時間" | |
| else: | |
| log_time = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))).strftime("%Y-%m-%d %H:%M:%S") | |
| if status == "success": | |
| ai_judgment = check_anomaly(ip, account, action, log_time) | |
| if ai_judgment != "success": status = ai_judgment | |
| new_log = {"timestamp": log_time, "ip_address": ip, "cookie_id": cookie, "account": account, "role": role, "action": action, "status": status} | |
| new_df = pd.DataFrame([new_log]) | |
| new_df.to_csv('baseline_logs.csv', mode='a', header=not os.path.exists('baseline_logs.csv'), index=False) | |
| try: 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) | |
| input_update = gr.update(value=f"時間: {log_time} | IP: {ip} | 帳號: {account} | 動作: {action} | 狀態: {status}") if "異常" in str(status) or "錯誤" in str(status) or "受限" in str(status) else gr.update() | |
| return df_all, df_abnormal, input_update | |
| def fast_login(target_account, ip, cookie, time_mode, custom_time): | |
| role, name = USER_DB[target_account][1], USER_DB[target_account][2] | |
| df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time) | |
| target_tab = "student_tab" if role == "學生" else "admin_tab" | |
| return gr.update(selected="dashboard_route"), gr.update(selected=target_tab), "", f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)", df_all, df_abn, inp_upd, target_account | |
| def process_login(ip, cookie, account, password, captcha, time_mode, custom_time): | |
| def fail_response(msg, df_all, df_abn, inp_upd): return gr.update(selected="login_route"), gr.update(), msg, gr.update(), df_all, df_abn, inp_upd | |
| if account not in USER_DB: | |
| df_all, df_abn, inp_upd = record_log(ip, cookie, account, "Unknown", "login_attempt", "failed_no_account", time_mode, custom_time) | |
| return fail_response("帳號不存在!", df_all, df_abn, inp_upd) | |
| role, name = USER_DB[account][1], USER_DB[account][2] | |
| if captcha.upper() != "FEIK": | |
| df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_captcha", time_mode, custom_time) | |
| return fail_response("驗證碼錯誤!", df_all, df_abn, inp_upd) | |
| if USER_DB[account][0] == password: | |
| df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time) | |
| return gr.update(selected="dashboard_route"), gr.update(selected="student_tab" if role == "學生" else "admin_tab"), "", f"**{name}** 歡迎您 | 線上人數: 938", df_all, df_abn, inp_upd | |
| df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time) | |
| return fail_response("密碼錯誤!", df_all, df_abn, inp_upd) | |
| def toggle_role(current_account, ip, cookie, time_mode, custom_time): | |
| target_account = "student" if current_account == "admin" else "admin" | |
| role, name = USER_DB[target_account][1], USER_DB[target_account][2] | |
| df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time) | |
| return gr.update(selected="student_tab" if role == "學生" else "admin_tab"), f"**{name}** 歡迎您 | 線上人數: 938 (⚡單鍵切換模式)", df_all, df_abn, inp_upd, target_account | |
| def logout(ip, cookie, account, time_mode, custom_time): | |
| role = USER_DB.get(account, ["", "Unknown"])[1] | |
| df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time) | |
| return gr.update(selected="login_route"), "", "", "", "", df_all, df_abn, inp_upd | |
| def simulate_click(ip, cookie, account, button_name, time_mode, custom_time): | |
| role = USER_DB.get(account, ["", "Unknown"])[1] | |
| return record_log(ip, cookie, account, role, button_name, "success", time_mode, custom_time) | |
| def _extract_gr_update_value(update_obj): | |
| """ | |
| Gradio 的 gr.update(...) 可能回傳 dict 形式或物件形式。 | |
| 這裡統一抓出其中的 `value`,否則回傳 None。 | |
| """ | |
| if update_obj is None: | |
| return None | |
| if isinstance(update_obj, dict): | |
| return update_obj.get("value") | |
| # Fallback: try attribute access | |
| try: | |
| return getattr(update_obj, "value", None) | |
| except Exception: | |
| return None | |
| def api_run_student_action(ip, c, acc, button_name, tm, ct): | |
| """ | |
| 統一的 student action API handler: | |
| - 呼叫 simulate_click 真正寫入 log | |
| - 只回傳異常/受限/錯誤時 abnormal_log_input 的字串(正常時回傳空字串) | |
| """ | |
| df_all, df_abn, inp_upd = simulate_click(ip, c, acc, button_name, tm, ct) | |
| val = _extract_gr_update_value(inp_upd) | |
| return val or "" | |
| def api_run_student_action_code(ip, c, acc, action_code, tm, ct): | |
| """ | |
| Generic student action API. | |
| Vue 會直接傳 action_code(例如 click_eval_system / malicious_sql_injection) | |
| """ | |
| df_all, df_abn, inp_upd = simulate_click(ip, c, acc, action_code, tm, ct) | |
| val = _extract_gr_update_value(inp_upd) | |
| # 回傳 store 的新資料(讓前端 update_dataframes 能讀到),以及異常字串給 Vue 顯示 | |
| return df_all, df_abn, val or "" | |
| def api_lambda(ip, c, tm, ct): | |
| # Vue: lambda(期末教學評量) 沒有傳 acc => 預設用學生帳號 | |
| default_acc = USER_DB["student"][0] | |
| return api_run_student_action(ip, c, default_acc, "click_eval_system", tm, ct) | |
| def api_lambda_1(ip, c, tm, ct): | |
| default_acc = USER_DB["student"][0] | |
| return api_run_student_action(ip, c, default_acc, "click_pre_select_system", tm, ct) | |
| def api_lambda_7(ip, c, acc, tm, ct): | |
| return api_run_student_action(ip, c, acc, "click_leave_system", tm, ct) | |
| def api_lambda_8(ip, c, acc, tm, ct): | |
| return api_run_student_action(ip, c, acc, "click_dorm_system", tm, ct) | |
| def api_lambda_10(ip, c, acc, tm, ct): | |
| return api_run_student_action(ip, c, acc, "click_webmail", tm, ct) | |
| def api_lambda_11(ip, c, acc, tm, ct): | |
| return api_run_student_action(ip, c, acc, "click_vdesk", tm, ct) | |
| def api_lambda_12(ip, c, acc, tm, ct): | |
| return api_run_student_action(ip, c, acc, "malicious_sql_injection", tm, ct) | |
| def test_anomaly(ip, cookie, account, action_name, test_time_str): | |
| role = USER_DB.get(account, ["", "Unknown"])[1] | |
| return record_log(ip, cookie, account, role, action_name, "success", "自訂時間 (模擬過去/未來)", test_time_str) | |
| def toggle_time_input(mode): return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)")) | |
| # --- 4. UI 介面設計 --- | |
| custom_css = """ | |
| .mock-panel { background-color: var(--background-fill-secondary); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-color-primary); } | |
| .login-container { max-width: 500px; margin: 0 auto; padding: 20px; } | |
| .ntut-title { color: #d9534f; font-size: 22px; font-weight: bold; text-align: center; margin-bottom: 20px;} | |
| .weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; } | |
| .headless-tabs > div:first-child { display: none !important; } | |
| .headless-tabs { border: none !important; background: transparent !important; } | |
| .stat-dashboard { display: flex; gap: 15px; margin-bottom: 20px; justify-content: space-between; } | |
| .stat-card { flex: 1; background-color: #ffffff !important; padding: 15px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); display: flex; align-items: center; gap: 15px; border-top: 4px solid transparent; } | |
| .card-total { border-left: 4px solid #10b981; } | |
| .card-anomaly { border-left: 4px solid #ef4444; } | |
| .card-status { border-left: 4px solid #3b82f6; } | |
| .card-icon { font-size: 30px; width: 40px; text-align: center; } | |
| .card-content { flex: 1; } | |
| .card-label { color: #374151 !important; font-size: 13px; margin-bottom: 2px; font-weight: bold !important; } | |
| .card-value-container { display: flex; align-items: baseline; gap: 8px; } | |
| .card-value { font-size: 24px; font-weight: bold; } | |
| .card-total .card-value { color: #000000 !important; } | |
| .card-unit { font-size: 14px; color: #4b5563 !important; font-weight: normal; } | |
| .card-trend { font-size: 12px; padding: 2px 6px; border-radius: 4px; } | |
| .trend-up { background-color: #fee2e2 !important; color: #ef4444 !important; } | |
| .card-sub-text { color: #6b7280 !important; font-size: 11px; margin-top: 2px; } | |
| """ | |
| with gr.Blocks(title="模擬校園入口與資安防護系統") as demo: | |
| store_df_all = gr.State(value=pd.DataFrame(log_history)) | |
| store_df_abn = gr.State(value=initial_df_abn) | |
| dashboard_timer = gr.Timer(value=60) | |
| with gr.Row(): | |
| with gr.Column(scale=1, elem_classes="mock-panel"): | |
| gr.Markdown("### ⚙️ 環境變數模擬器") | |
| mock_ip = gr.Dropdown(choices=["140.124.71.55 (校內預設)", "140.124.18.22 (宿舍)", "61.228.45.112 (家裡)", "210.61.47.88 (公共場所)", "45.33.22.11 (國外異常IP)"], value="140.124.71.55 (校內預設)", allow_custom_value=True, label="模擬來源 IP") | |
| mock_cookie = gr.Textbox(label="模擬 Cookie Session ID", value="sess_baseline") | |
| gr.Markdown("---") | |
| time_mode = gr.Radio(label="時間設定模式", choices=["真實時間 (目前時間)", "自訂時間 (模擬過去/未來)"], value="真實時間 (目前時間)") | |
| custom_time_input = gr.Textbox(label="輸入自訂時間", placeholder="格式: YYYY-MM-DD HH:MM:SS", value="2026-03-17 03:00:00", visible=False) | |
| gr.Markdown("---") | |
| gr.Markdown("### 📊 即時 Log 與 AI 判定區") | |
| log_display = gr.Dataframe(value=pd.DataFrame(log_history), headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True) | |
| with gr.Column(scale=2): | |
| with gr.Tabs(selected="login_route", elem_classes="headless-tabs") as main_router: | |
| with gr.Tab("Login", id="login_route"): | |
| with gr.Column(elem_classes="login-container"): | |
| gr.Markdown("<div class='ntut-title'>校園入口網站 Taipei Tech Portal</div>") | |
| login_msg = gr.Markdown(value="", visible=True) | |
| with gr.Row(): acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="student or admin") | |
| with gr.Row(): pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="password : 1234") | |
| with gr.Row(): gr.HTML("<div style='background:var(--background-fill-secondary); color:var(--body-text-color); padding:10px; font-weight:bold; letter-spacing: 3px; border:1px solid var(--border-color-primary); text-align:center;'>F E I K</div>") | |
| with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)") | |
| login_btn = gr.Button("登入 Login", variant="primary") | |
| gr.Markdown("---") | |
| gr.Markdown("<div style='text-align:center; color:gray; font-size: 0.9em;'>🛠️ 開發與測試專用捷徑</div>") | |
| with gr.Row(): | |
| btn_fast_student = gr.Button("🚀 快速登入 (學生)", variant="secondary") | |
| btn_fast_admin = gr.Button("🚀 快速登入 (管理員)", variant="secondary") | |
| with gr.Tab("Dashboard", id="dashboard_route"): | |
| with gr.Column(): | |
| with gr.Row(): | |
| gr.Markdown("### 資訊系統") | |
| welcome_text = gr.Markdown(value="", elem_classes="text-right") | |
| logout_btn = gr.Button("登出", size="sm") | |
| with gr.Row(): btn_toggle_role = gr.Button("🔄 Student ↔ Admin", size="sm", variant="secondary") | |
| gr.Markdown("---") | |
| with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs: | |
| with gr.Tab("👨🎓 學生專區", id="student_tab"): | |
| with gr.Accordion("▼ 1. 教務系統", open=True): | |
| with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm") | |
| with gr.Row(): btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm"); btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm"); btn_course = gr.Button("▶ 課程系統", size="sm") | |
| with gr.Row(): btn_score = gr.Button("▶ 學業成績查詢系統", size="sm") | |
| with gr.Accordion("▼ 2. 學務系統", open=True): | |
| with gr.Row(): btn_leave = gr.Button("▶ 學生請假系統", size="sm"); btn_dorm = gr.Button("▶ 學生宿舍登錄(抽籤)系統", size="sm"); btn_scholarship = gr.Button("▶ 獎助學金申請系統", size="sm") | |
| with gr.Accordion("▼ 3. 資訊服務", open=True): | |
| with gr.Row(): btn_webmail = gr.Button("▶ 網路郵局 WebMail", size="sm"); btn_vdesk = gr.Button("▶ 北科軟體雲", size="sm") | |
| with gr.Accordion("▼ 惡意操作測試區", open=True): | |
| btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm") | |
| with gr.Tab("🛡️ 管理員專區", id="admin_tab"): | |
| stat_dashboard_display = gr.HTML(value=get_dashboard_html()) | |
| with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"): | |
| action_dropdown = gr.Dropdown(choices=list(ACTION_MAP.keys()), label="選擇要檢視的系統資料分佈", value=None) | |
| action_plot = gr.Plot(show_label=False) | |
| with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"): | |
| gr.Markdown("<span style='color:gray; font-size:0.9em;'>動態防護已全面啟動:包含 HDBSCAN 空間異常偵測、IP 聲譽聯防,以及動態視窗頻率攔截。</span>") | |
| with gr.Row(): | |
| overall_weight = gr.Number(label="總體防護強度 (%)", value=75, minimum=0, maximum=100) | |
| time_weight = gr.Number(label="時間特徵權重 (%)", value=80, minimum=0, maximum=100) | |
| ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100) | |
| with gr.Row(): | |
| device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100) | |
| geo_weight = gr.Number(label="🌍 地理偏移敏感度 (%)", value=50, minimum=0, maximum=100) | |
| freq_weight = gr.Number(label="⚡ 行為頻率敏感度 (%)", value=60, minimum=0, maximum=100) | |
| weights_status = gr.Textbox(value="", visible=False) | |
| with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"): | |
| abnormal_log_display = gr.Dataframe(value=initial_df_abn, headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True) | |
| with gr.Row(): | |
| abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入...", scale=4) | |
| btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1) | |
| ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False) | |
| with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"): | |
| gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>") | |
| gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)") | |
| with gr.Row(): | |
| t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm"); t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm") | |
| with gr.Row(): | |
| t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm"); t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm") | |
| with gr.Row(): | |
| t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm"); t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm") | |
| gr.Markdown("#### 2. 期末與成績相關 (具備週期性)") | |
| with gr.Row(): | |
| t_eval_norm = gr.Button("✅ 正常: 期末網路教學評量 (12-17 13:20)", size="sm"); t_eval_anom = gr.Button("⚠️ 異常: 期末網路教學評量 (03-15 03:00)", size="sm") | |
| with gr.Row(): | |
| t_score_norm = gr.Button("✅ 正常: 學業成績查詢系統 (11-17 18:50)", size="sm"); t_score_anom = gr.Button("⚠️ 異常: 學業成績查詢系統 (02-15 03:00)", size="sm") | |
| gr.Markdown("#### 3. 校務與行政服務 (生活與常規)") | |
| with gr.Row(): | |
| t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm"); t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm") | |
| with gr.Row(): | |
| t_schol_norm = gr.Button("✅ 正常: 獎助學金申請系統 (03-12 14:00)", size="sm"); t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm") | |
| with gr.Row(): | |
| t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm"); t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm") | |
| gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)") | |
| with gr.Row(): | |
| t_istudy_norm = gr.Button("✅ 正常: 北科i學園PLUS (10-18 16:00)", size="sm"); t_istudy_anom = gr.Button("⚠️ 異常: 北科i學園PLUS (08-20 02:00)", size="sm") | |
| with gr.Row(): | |
| t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm"); t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm") | |
| with gr.Row(): | |
| t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm") | |
| def update_dataframes(df_all, df_abn): return gr.update(value=df_all), gr.update(value=df_abn) | |
| store_df_all.change(fn=update_dataframes, inputs=[store_df_all, store_df_abn], outputs=[log_display, abnormal_log_display]) | |
| demo.load(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot]) | |
| dashboard_timer.tick(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot]) | |
| action_dropdown.change(fn=get_plot_data, inputs=[action_dropdown], outputs=[action_plot], api_name="getPlotData") | |
| time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input) | |
| btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output]) | |
| # weight_inputs = [overall_weight, time_weight, ip_weight, device_weight, geo_weight, freq_weight] | |
| # 移除所有 UI 欄位的 update_system_weights 綁定,僅保留 API 專用端點 | |
| update_outputs = [main_router, role_tabs, login_msg, welcome_text, store_df_all, store_df_abn, abnormal_log_input] | |
| login_btn.click(fn=process_login, inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input], outputs=update_outputs) | |
| logout_btn.click(fn=logout, inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input], outputs=[main_router, acc_input, pwd_input, captcha_input, login_msg, store_df_all, store_df_abn, abnormal_log_input]) | |
| fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] | |
| fast_outputs = update_outputs + [acc_input] | |
| btn_fast_student.click( | |
| fn=lambda ip, c, tm, ct: fast_login("student", ip, c, tm, ct), | |
| inputs=fast_inputs, | |
| outputs=fast_outputs, | |
| api_name=None | |
| ) | |
| btn_fast_admin.click( | |
| fn=lambda ip, c, tm, ct: fast_login("admin", ip, c, tm, ct), | |
| inputs=fast_inputs, | |
| outputs=fast_outputs, | |
| api_name=None | |
| ) | |
| btn_toggle_role.click(fn=toggle_role, inputs=[acc_input, mock_ip, mock_cookie, time_mode, custom_time_input], outputs=[role_tabs, welcome_text, store_df_all, store_df_abn, abnormal_log_input, acc_input]) | |
| action_mapping = [ | |
| (btn_eval, "click_eval_system"), (btn_pre_select, "click_pre_select_system"), (btn_add_drop, "click_add_drop_system"), | |
| (btn_istudy, "click_istudy"), (btn_card, "click_card_loss"), (btn_course, "click_course_system"), | |
| (btn_score, "click_score_query"), (btn_leave, "click_leave_system"), (btn_dorm, "click_dorm_system"), | |
| (btn_scholarship, "click_scholarship"), (btn_webmail, "click_webmail"), (btn_vdesk, "click_vdesk"), | |
| (btn_hack_score, "malicious_sql_injection") | |
| ] | |
| for btn, action_name in action_mapping: | |
| btn.click( | |
| fn=lambda ip, c, acc, tm, ct, n=action_name: simulate_click(ip, c, acc, n, tm, ct), | |
| inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input], | |
| outputs=[store_df_all, store_df_abn, abnormal_log_input], | |
| api_name=None | |
| ) | |
| test_mapping = [ | |
| (t_pre_sel_norm, "click_pre_select_system", "2026-06-13 11:00:00"), (t_pre_sel_anom, "click_pre_select_system", "2026-10-15 13:00:00"), | |
| (t_course_norm, "click_course_system", "2026-02-25 07:50:00"), (t_course_anom, "click_course_system", "2026-07-15 05:00:00"), | |
| (t_add_drop_norm, "click_add_drop_system", "2026-02-18 14:00:00"), (t_add_drop_anom, "click_add_drop_system", "2026-05-15 12:00:00"), | |
| (t_eval_norm, "click_eval_system", "2026-12-17 13:20:00"), (t_eval_anom, "click_eval_system", "2026-03-15 03:00:00"), | |
| (t_score_norm, "click_score_query", "2026-11-17 18:50:00"), (t_score_anom, "click_score_query", "2026-02-15 03:00:00") | |
| ] | |
| for btn, action_name, test_time_str in test_mapping: | |
| btn.click( | |
| fn=lambda ip, c, acc, n=action_name, t_str=test_time_str: test_anomaly(ip, c, acc, n, t_str), | |
| inputs=[mock_ip, mock_cookie, acc_input], | |
| outputs=[store_df_all, store_df_abn, abnormal_log_input], | |
| api_name=None | |
| ) | |
| # ===================================================================== | |
| # 👉 新增:隱藏的 UI 綁定區 (讓外部能透過 api_name 存取資料) | |
| # ===================================================================== | |
| with gr.Group(visible=False): | |
| api_stats_btn = gr.Button("API_Stats") | |
| api_stats_out = gr.JSON() | |
| api_stats_btn.click(fn=api_get_stats, inputs=[], outputs=[api_stats_out], api_name="get_stats") | |
| api_logs_btn = gr.Button("API_Logs") | |
| api_logs_in = gr.Number(value=20) | |
| api_logs_out = gr.JSON() | |
| api_logs_btn.click(fn=api_get_logs, inputs=[api_logs_in], outputs=[api_logs_out], api_name="get_logs") | |
| api_chart_btn = gr.Button("API_Chart") | |
| api_chart_in = gr.Textbox() | |
| api_chart_out = gr.JSON() | |
| api_chart_btn.click(fn=api_get_chart_data, inputs=[api_chart_in], outputs=[api_chart_out], api_name="get_chart_data") | |
| api_abnormal_logs_btn = gr.Button("API_Abnormal_Logs") | |
| api_abnormal_logs_out = gr.JSON() | |
| api_abnormal_logs_btn.click(fn=api_get_abnormal_logs, inputs=[], outputs=[api_abnormal_logs_out], api_name="get_abnormal_logs") | |
| api_plot_btn = gr.Button("API_Plot") | |
| api_plot_in = gr.Textbox() | |
| api_plot_out = gr.Textbox() | |
| api_plot_btn.click(fn=get_plot_data_for_api, inputs=[api_plot_in], outputs=[api_plot_out], api_name="get_plot_data_api") | |
| # System weights update API endpoint | |
| api_update_weights_btn = gr.Button("API_Update_Weights") | |
| api_update_weights_ow = gr.Number(label="Overall Weight") | |
| api_update_weights_tw = gr.Number(label="Time Weight") | |
| api_update_weights_iw = gr.Number(label="IP Weight") | |
| api_update_weights_dw = gr.Number(label="Device Weight") | |
| api_update_weights_gw = gr.Number(label="Geo Weight") | |
| api_update_weights_fw = gr.Number(label="Frequency Weight") | |
| api_update_weights_in = [api_update_weights_ow, api_update_weights_tw, api_update_weights_iw, api_update_weights_dw, api_update_weights_gw, api_update_weights_fw] | |
| api_update_weights_out = gr.Textbox() | |
| api_update_weights_btn.click(fn=update_system_weights, inputs=api_update_weights_in, outputs=[api_update_weights_out], api_name="update_system_weights") | |
| # ================================================================ | |
| # Vue student actions API endpoints | |
| # - 對應 Digital-Bodyguard-Fontend/src/App.vue 裡的 ACTION_ENDPOINTS | |
| # - 對應 Digital-Bodyguard-Fontend/src/services/gradioService.ts 的 lambda* | |
| # ================================================================ | |
| api_student_ip = gr.Textbox(label="ip") | |
| api_student_cookie = gr.Textbox(label="c") | |
| api_student_time_mode = gr.Radio(label="tm", choices=["真實時間 (目前時間)", "自訂時間 (模擬過去/未來)"], value="真實時間 (目前時間)") | |
| api_student_custom_time = gr.Textbox(label="ct") | |
| # lambda (期末教學評量) => api_name="lambda" | |
| api_lambda_out = gr.Textbox() | |
| gr.Button("API_lambda", visible=False).click( | |
| fn=api_lambda, | |
| inputs=[api_student_ip, api_student_cookie, api_student_time_mode, api_student_custom_time], | |
| outputs=[api_lambda_out], | |
| api_name="lambda", | |
| ) | |
| # lambda_1 (預選系統) | |
| api_lambda_1_out = gr.Textbox() | |
| gr.Button("API_lambda_1", visible=False).click( | |
| fn=api_lambda_1, | |
| inputs=[api_student_ip, api_student_cookie, api_student_time_mode, api_student_custom_time], | |
| outputs=[api_lambda_1_out], | |
| api_name="lambda_1", | |
| ) | |
| # lambda_7 (請假系統) | |
| api_lambda_acc = gr.Textbox(label="acc") | |
| api_lambda_7_out = gr.Textbox() | |
| gr.Button("API_lambda_7", visible=False).click( | |
| fn=api_lambda_7, | |
| inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time], | |
| outputs=[api_lambda_7_out], | |
| api_name="lambda_7", | |
| ) | |
| # lambda_8 (宿舍登錄) | |
| api_lambda_8_out = gr.Textbox() | |
| gr.Button("API_lambda_8", visible=False).click( | |
| fn=api_lambda_8, | |
| inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time], | |
| outputs=[api_lambda_8_out], | |
| api_name="lambda_8", | |
| ) | |
| # lambda_10 (網路郵局) | |
| api_lambda_10_out = gr.Textbox() | |
| gr.Button("API_lambda_10", visible=False).click( | |
| fn=api_lambda_10, | |
| inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time], | |
| outputs=[api_lambda_10_out], | |
| api_name="lambda_10", | |
| ) | |
| # lambda_11 (軟體雲) | |
| api_lambda_11_out = gr.Textbox() | |
| gr.Button("API_lambda_11", visible=False).click( | |
| fn=api_lambda_11, | |
| inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time], | |
| outputs=[api_lambda_11_out], | |
| api_name="lambda_11", | |
| ) | |
| # lambda_12 (SQL Injection) | |
| api_lambda_12_out = gr.Textbox() | |
| gr.Button("API_lambda_12", visible=False).click( | |
| fn=api_lambda_12, | |
| inputs=[api_student_ip, api_student_cookie, api_lambda_acc, api_student_time_mode, api_student_custom_time], | |
| outputs=[api_lambda_12_out], | |
| api_name="lambda_12", | |
| ) | |
| # ================================================================ | |
| # Generic student action API (Vue 一次呼叫任意系統按鈕) | |
| # ================================================================ | |
| api_student_action_code = gr.Textbox(label="action_code") | |
| api_student_run_out = gr.Textbox() | |
| gr.Button("API_RunStudentAction", visible=False).click( | |
| fn=api_run_student_action_code, | |
| inputs=[ | |
| api_student_ip, | |
| api_student_cookie, | |
| api_lambda_acc, | |
| api_student_action_code, | |
| api_student_time_mode, | |
| api_student_custom_time, | |
| ], | |
| outputs=[store_df_all, store_df_abn, api_student_run_out], | |
| api_name="run_student_action", | |
| ) | |
| # ===================================================================== | |
| if __name__ == "__main__": | |
| demo.launch(css=custom_css) |