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"""
📊
總監測量 (本地歷史資料庫)
{total:,} 筆 Log
自系統啟動起統計 (+今日即時)
⚠️
異常率 (平均數值)
{rate:.2f} %
↑即時
共 {abnormal:,} 筆監測到異常行為
{status_icon}
系統狀態
{status_text}
{status_desc}
""" 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("
校園入口網站 Taipei Tech Portal
") 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("
F E I K
") with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)") login_btn = gr.Button("登入 Login", variant="primary") gr.Markdown("---") gr.Markdown("
🛠️ 開發與測試專用捷徑
") 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("動態防護已全面啟動:包含 HDBSCAN 空間異常偵測、IP 聲譽聯防,以及動態視窗頻率攔截。") 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("點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。") 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)