PhilipL commited on
Commit
71996b7
·
verified ·
1 Parent(s): 187b0e5

calculate total data, varient data and evaluate system

Browse files
Files changed (1) hide show
  1. app.py +143 -44
app.py CHANGED
@@ -4,7 +4,12 @@ import pandas as pd
4
  import joblib
5
  from sklearn.cluster import HDBSCAN
6
  import numpy as np
7
- import logger_service # 🌟 保持:匯入外部日誌服務模組
 
 
 
 
 
8
  import os
9
 
10
  # 初始化歷史日誌串列
@@ -50,7 +55,53 @@ USER_DB = {
50
  "admin": ["1234", "管理", "管理員"],
51
  }
52
 
53
- # --- 🌟 新增:Groq AI Agent 呼叫函式 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  def explain_abnormal_log(log_text):
55
  if not log_text or log_text.strip() == "":
56
  return "⚠️ 請先提供異常 Log 資訊以進行分析。"
@@ -58,12 +109,11 @@ def explain_abnormal_log(log_text):
58
  try:
59
  from groq import Groq
60
  except ImportError:
61
- return "⚠️ 未安裝 groq 套件。請在終端機執行 `pip install groq`。"
62
 
63
- # 可以在此直接替換 "" 為您的 API KEY: os.environ.get("GROQ_API_KEY", "gsk_xxxx...")
64
  groq_api_key = os.environ.get("GROQ_API_KEY", "")
65
  if not groq_api_key:
66
- return "⚠️ 未設定 GROQ_API_KEY。請設定環境變數或直接在程式碼中填寫您的 API Key。"
67
 
68
  try:
69
  client = Groq(api_key=groq_api_key)
@@ -71,14 +121,14 @@ def explain_abnormal_log(log_text):
71
  messages=[
72
  {
73
  "role": "system",
74
- "content": "你是一位專業的資安分析師。請根據使用者提供的系統 Log,用繁體中文簡單解釋為什麼這筆 Log 會被標記為異常,並指出可能的資安風險(例如:撞庫攻擊、自動化腳本、半夜作息異常等)。請保持說明在 100 字以內,精簡扼要。"
75
  },
76
  {
77
  "role": "user",
78
  "content": f"請分析這筆異常 Log:\n{log_text}"
79
  }
80
  ],
81
- model="llama-3.1-8b-instant", # Groq 提供的高速模型
82
  temperature=0.5,
83
  max_tokens=300
84
  )
@@ -161,7 +211,7 @@ def check_anomaly(ip, action, log_time, overall_w, time_w, ip_w, dev_w):
161
  except Exception as e:
162
  return f"⚠️系統錯誤: {str(e)}"
163
 
164
- # 🌟 修改:回傳值增加 df_abnormal 與 input_update 以支援異常輸入框自動填寫
165
  def record_log(ip, cookie, account, role, action, status, time_mode, custom_time, ow, tw, iw, dw):
166
  global baseline_X
167
  if time_mode == "自訂時間 (模擬過去/未來)" and custom_time and str(custom_time).strip() != "":
@@ -179,10 +229,11 @@ def record_log(ip, cookie, account, role, action, status, time_mode, custom_time
179
  "account": account, "role": role, "action": action, "status": status
180
  }
181
 
182
- try:
183
- logger_service.push_new_log(new_log)
184
- except Exception as e:
185
- print(f"⚠️ 外部日誌推送失敗: {e}")
 
186
 
187
  log_history.insert(0, new_log)
188
 
@@ -214,54 +265,58 @@ def record_log(ip, cookie, account, role, action, status, time_mode, custom_time
214
  latest_str = f"時間: {log_time} | IP: {ip} | 帳號: {account} | 動作: {action} | 狀態: {status}"
215
  input_update = gr.update(value=latest_str)
216
  else:
217
- input_update = gr.update() # 正常行為則不干涉使用者手動輸入的內容
 
 
 
218
 
219
- return df_all, df_abnormal, input_update
220
 
221
  # --- 3. 視角切換與登入控制邏輯 ---
 
222
  def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
223
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
224
- df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
225
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)"
226
  target_tab = "student_tab" if role == "學生" else "admin_tab"
227
- return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd, target_account
228
 
229
  def process_login(ip, cookie, account, password, captcha, time_mode, custom_time, ow, tw, iw, dw):
230
  valid_captcha = "FEIK"
231
- def fail_response(msg, df_all, df_abn, inp_upd):
232
- return gr.update(visible=True), gr.update(visible=False), gr.update(), msg, gr.update(), df_all, df_abn, inp_upd
233
 
234
  if account not in USER_DB:
235
- df_all, df_abn, inp_upd = record_log(ip, cookie, account, "Unknown", "login_attempt", "failed_no_account", time_mode, custom_time, ow, tw, iw, dw)
236
- return fail_response("帳號不存在!", df_all, df_abn, inp_upd)
237
 
238
  role, name = USER_DB[account][1], USER_DB[account][2]
239
 
240
  if captcha.upper() != valid_captcha:
241
- df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_captcha", time_mode, custom_time, ow, tw, iw, dw)
242
- return fail_response("驗證碼錯誤!", df_all, df_abn, inp_upd)
243
 
244
  if USER_DB[account][0] == password:
245
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
246
- df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
247
  target_tab = "student_tab" if role == "學生" else "admin_tab"
248
- return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd
249
 
250
- df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time, ow, tw, iw, dw)
251
- return fail_response("密碼錯誤!", df_all, df_abn, inp_upd)
252
 
253
  def toggle_role(current_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
254
  target_account = "student" if current_account == "admin" else "admin"
255
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
256
- df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
257
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡單鍵切換模式)"
258
  target_tab = "student_tab" if role == "學生" else "admin_tab"
259
- return gr.update(selected=target_tab), welcome_msg, df_all, df_abn, inp_upd, target_account
260
 
261
  def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
262
  role = USER_DB.get(account, ["", "Unknown"])[1]
263
- df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time, ow, tw, iw, dw)
264
- return gr.update(visible=True), gr.update(visible=False), "", "", "", "", df_all, df_abn, inp_upd
265
 
266
  def simulate_click(ip, cookie, account, button_name, time_mode, custom_time, ow, tw, iw, dw):
267
  role = USER_DB.get(account, ["", "Unknown"])[1]
@@ -274,6 +329,9 @@ def test_anomaly(ip, cookie, account, action_name, test_time_str, ow, tw, iw, dw
274
  def toggle_time_input(mode):
275
  return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)"))
276
 
 
 
 
277
  # --- 4. UI 介面設計 ---
278
  custom_css = """
279
  .mock-panel { background-color: var(--background-fill-secondary); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-color-primary); }
@@ -282,6 +340,36 @@ custom_css = """
282
  .weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
283
  .headless-tabs > div:first-child { display: none !important; }
284
  .headless-tabs { border: none !important; background: transparent !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  """
286
 
287
  with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css) as demo:
@@ -324,7 +412,8 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
324
  gr.Markdown("---")
325
 
326
  with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
327
-
 
328
  with gr.Tab("👨‍🎓 學生專區", id="student_tab"):
329
  with gr.Accordion("▼ 1. 教務系統", open=True):
330
  with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
@@ -338,6 +427,12 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
338
  btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
339
 
340
  with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
 
 
 
 
 
 
341
  with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
342
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>手動輸入 0~100 的數值。提高強度會增加誤報率,但能更有效攔截新型攻擊。</span>")
343
  with gr.Row():
@@ -347,7 +442,7 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
347
  ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100)
348
  device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100)
349
 
350
- # 🌟 新增:異常 Log 分析面板 (含自動更新清單、輸入框與 Groq API 分析)
351
  with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"):
352
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>顯示所有被判定為異常的 Log。點擊分析可讓 AI 代為判讀潛在風險。</span>")
353
  abnormal_log_display = gr.Dataframe(
@@ -369,9 +464,9 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
369
  with gr.Row():
370
  t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm"); t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm")
371
  with gr.Row():
372
- t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm"); t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm")
373
  with gr.Row():
374
- t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm"); t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm")
375
 
376
  gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
377
  with gr.Row():
@@ -381,9 +476,9 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
381
 
382
  gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
383
  with gr.Row():
384
- t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm"); t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm")
385
  with gr.Row():
386
- t_schol_norm = gr.Button("✅ 正常: 獎助學金申請系統 (03-12 14:00)", size="sm"); t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm")
387
  with gr.Row():
388
  t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm"); t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
389
 
@@ -391,7 +486,7 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
391
  with gr.Row():
392
  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")
393
  with gr.Row():
394
- t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm"); t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm")
395
  with gr.Row():
396
  t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
397
 
@@ -402,22 +497,26 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
402
  btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
403
 
404
  weight_inputs = [overall_weight, time_weight, ip_weight, device_weight]
405
- update_outputs = [login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display, abnormal_log_display, abnormal_log_input]
406
 
 
 
 
 
 
407
  login_btn.click(
408
  fn=process_login,
409
  inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input] + weight_inputs,
410
- outputs=update_outputs
411
  )
412
 
413
  logout_btn.click(
414
  fn=logout,
415
  inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
416
- outputs=[login_view, dashboard_view, acc_input, pwd_input, captcha_input, login_msg, log_display, abnormal_log_display, abnormal_log_input]
417
  )
418
 
419
  fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs
420
- fast_outputs = update_outputs + [acc_input]
421
 
422
  btn_fast_student.click(fn=lambda ip, c, tm, ct, ow, tw, iw, dw: fast_login("student", ip, c, tm, ct, ow, tw, iw, dw), inputs=fast_inputs, outputs=fast_outputs)
423
  btn_fast_admin.click(fn=lambda ip, c, tm, ct, ow, tw, iw, dw: fast_login("admin", ip, c, tm, ct, ow, tw, iw, dw), inputs=fast_inputs, outputs=fast_outputs)
@@ -425,7 +524,7 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
425
  btn_toggle_role.click(
426
  fn=toggle_role,
427
  inputs=[acc_input, mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs,
428
- outputs=[role_tabs, welcome_text, log_display, abnormal_log_display, abnormal_log_input, acc_input]
429
  )
430
 
431
  action_mapping = [
@@ -442,7 +541,7 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
442
  btn.click(
443
  fn=lambda ip, c, acc, t_mode, t_custom, ow, tw, iw, dw, n=action_name: simulate_click(ip, c, acc, n, t_mode, t_custom, ow, tw, iw, dw),
444
  inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
445
- outputs=[log_display, abnormal_log_display, abnormal_log_input]
446
  )
447
 
448
  test_mapping = [
@@ -463,7 +562,7 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
463
  btn.click(
464
  fn=lambda ip, c, acc, ow, tw, iw, dw, n=action_name, t_str=test_time_str: test_anomaly(ip, c, acc, n, t_str, ow, tw, iw, dw),
465
  inputs=[mock_ip, mock_cookie, acc_input, overall_weight, time_weight, ip_weight, device_weight],
466
- outputs=[log_display, abnormal_log_display, abnormal_log_input]
467
  )
468
 
469
  if __name__ == "__main__":
 
4
  import joblib
5
  from sklearn.cluster import HDBSCAN
6
  import numpy as np
7
+ # 保持:匯入外部日誌服務模組 (若有的話)
8
+ try:
9
+ import logger_service
10
+ HAS_LOGGER = True
11
+ except ImportError:
12
+ HAS_LOGGER = False
13
  import os
14
 
15
  # 初始化歷史日誌串列
 
55
  "admin": ["1234", "管理", "管理員"],
56
  }
57
 
58
+ # --- 🌟 新增:統計資料計算函式 (HTML 格式) ---
59
+ def calculate_stats(history_list):
60
+ total = len(history_list)
61
+ abnormal_count = len([log for log in history_list if "異常" in str(log.get("status", ""))])
62
+
63
+ # 1. 總監測量 HTML
64
+ html_total = f"""
65
+ <div class="stat-card">
66
+ <div class="stat-title">總監測量</div>
67
+ <div class="stat-value">{total}</div>
68
+ <div class="stat-unit">筆操作</div>
69
+ </div>
70
+ """
71
+
72
+ # 2. 異常率 HTML
73
+ rate = (abnormal_count / total * 100) if total > 0 else 0
74
+ # 根據異常率調整顏色
75
+ rate_color = "#2ecc71" if rate < 5 else ("#f39c12" if rate < 15 else "#e74c3c")
76
+
77
+ html_rate = f"""
78
+ <div class="stat-card">
79
+ <div class="stat-title">異常率</div>
80
+ <div class="stat-value" style="color: {rate_color};">{rate:.1f}%</div>
81
+ <div class="stat-unit">{abnormal_count} 筆警告</div>
82
+ </div>
83
+ """
84
+
85
+ # 3. 系統狀態 HTML
86
+ api_key = os.environ.get("GROQ_API_KEY", "")
87
+ if not AI_READY:
88
+ status_text, status_color = "模型未載入", "#e74c3c"
89
+ elif not api_key:
90
+ status_text, status_color = "AI分析不可用", "#f39c12"
91
+ else:
92
+ status_text, status_color = "正常運作中", "#2ecc71"
93
+
94
+ html_status = f"""
95
+ <div class="stat-card">
96
+ <div class="stat-title">系統狀態</div>
97
+ <div class="stat-value" style="color: {status_color}; font-size: 24px; margin-top: 10px;">{status_text}</div>
98
+ <div class="stat-unit">HDBSCAN + Groq</div>
99
+ </div>
100
+ """
101
+
102
+ return html_total, html_rate, html_status
103
+
104
+ # --- Groq AI Agent 呼叫函式 ---
105
  def explain_abnormal_log(log_text):
106
  if not log_text or log_text.strip() == "":
107
  return "⚠️ 請先提供異常 Log 資訊以進行分析。"
 
109
  try:
110
  from groq import Groq
111
  except ImportError:
112
+ return "⚠️ 未安裝 groq 套件。請在requirements.txt加入 `groq`。"
113
 
 
114
  groq_api_key = os.environ.get("GROQ_API_KEY", "")
115
  if not groq_api_key:
116
+ return "⚠️ 未設定 GROQ_API_KEY。請在環境變數中設定您的 API Key。"
117
 
118
  try:
119
  client = Groq(api_key=groq_api_key)
 
121
  messages=[
122
  {
123
  "role": "system",
124
+ "content": "你是一位專業的資安分析師。請根據使用者提供的系統 Log,用繁體中文簡單解釋為什麼這筆 Log 會被標記為異常,並指出可能的資安風險。請保持說明在 100 字以內,精簡扼要。"
125
  },
126
  {
127
  "role": "user",
128
  "content": f"請分析這筆異常 Log:\n{log_text}"
129
  }
130
  ],
131
+ model="llama-3.1-8b-instant", # 使用最新的支援模型
132
  temperature=0.5,
133
  max_tokens=300
134
  )
 
211
  except Exception as e:
212
  return f"⚠️系統錯誤: {str(e)}"
213
 
214
+ # 🌟 修改:回傳值增加 h_tot, h_rat, h_sta 統計 HTML
215
  def record_log(ip, cookie, account, role, action, status, time_mode, custom_time, ow, tw, iw, dw):
216
  global baseline_X
217
  if time_mode == "自訂時間 (模擬過去/未來)" and custom_time and str(custom_time).strip() != "":
 
229
  "account": account, "role": role, "action": action, "status": status
230
  }
231
 
232
+ if HAS_LOGGER:
233
+ try:
234
+ logger_service.push_new_log(new_log)
235
+ except Exception as e:
236
+ pass
237
 
238
  log_history.insert(0, new_log)
239
 
 
265
  latest_str = f"時間: {log_time} | IP: {ip} | 帳號: {account} | 動作: {action} | 狀態: {status}"
266
  input_update = gr.update(value=latest_str)
267
  else:
268
+ input_update = gr.update()
269
+
270
+ # 🌟 計算統計數據 HTML
271
+ h_tot, h_rat, h_sta = calculate_stats(log_history)
272
 
273
+ return df_all, df_abnormal, input_update, h_tot, h_rat, h_sta
274
 
275
  # --- 3. 視角切換與登入控制邏輯 ---
276
+ # 🌟 修改:所有會產生新 Log 的函式,outputs 都需增加三個統計方塊
277
  def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
278
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
279
+ df_all, df_abn, inp_upd, h_tot, h_rat, h_sta = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
280
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)"
281
  target_tab = "student_tab" if role == "學生" else "admin_tab"
282
+ return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd, target_account, h_tot, h_rat, h_sta
283
 
284
  def process_login(ip, cookie, account, password, captcha, time_mode, custom_time, ow, tw, iw, dw):
285
  valid_captcha = "FEIK"
286
+ def fail_response(msg, df_all, df_abn, inp_upd, h_tot, h_rat, h_sta):
287
+ return gr.update(visible=True), gr.update(visible=False), gr.update(), msg, gr.update(), df_all, df_abn, inp_upd, gr.update(), h_tot, h_rat, h_sta
288
 
289
  if account not in USER_DB:
290
+ df_all, df_abn, inp_upd, h_tot, h_rat, h_sta = record_log(ip, cookie, account, "Unknown", "login_attempt", "failed_no_account", time_mode, custom_time, ow, tw, iw, dw)
291
+ return fail_response("帳號不存在!", df_all, df_abn, inp_upd, h_tot, h_rat, h_sta)
292
 
293
  role, name = USER_DB[account][1], USER_DB[account][2]
294
 
295
  if captcha.upper() != valid_captcha:
296
+ df_all, df_abn, inp_upd, h_tot, h_rat, h_sta = record_log(ip, cookie, account, role, "login_attempt", "failed_captcha", time_mode, custom_time, ow, tw, iw, dw)
297
+ return fail_response("驗證碼錯誤!", df_all, df_abn, inp_upd, h_tot, h_rat, h_sta)
298
 
299
  if USER_DB[account][0] == password:
300
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
301
+ df_all, df_abn, inp_upd, h_tot, h_rat, h_sta = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
302
  target_tab = "student_tab" if role == "學生" else "admin_tab"
303
+ return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd, target_tab, h_tot, h_rat, h_sta
304
 
305
+ df_all, df_abn, inp_upd, h_tot, h_rat, h_sta = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time, ow, tw, iw, dw)
306
+ return fail_response("密碼錯誤!", df_all, df_abn, inp_upd, h_tot, h_rat, h_sta)
307
 
308
  def toggle_role(current_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
309
  target_account = "student" if current_account == "admin" else "admin"
310
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
311
+ df_all, df_abn, inp_upd, h_tot, h_rat, h_sta = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
312
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡單鍵切換模式)"
313
  target_tab = "student_tab" if role == "學生" else "admin_tab"
314
+ return gr.update(selected=target_tab), welcome_msg, df_all, df_abn, inp_upd, target_tab, h_tot, h_rat, h_sta
315
 
316
  def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
317
  role = USER_DB.get(account, ["", "Unknown"])[1]
318
+ df_all, df_abn, inp_upd, h_tot, h_rat, h_sta = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time, ow, tw, iw, dw)
319
+ return gr.update(visible=True), gr.update(visible=False), "", "", "", "", df_all, df_abn, inp_upd, "", h_tot, h_rat, h_sta
320
 
321
  def simulate_click(ip, cookie, account, button_name, time_mode, custom_time, ow, tw, iw, dw):
322
  role = USER_DB.get(account, ["", "Unknown"])[1]
 
329
  def toggle_time_input(mode):
330
  return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)"))
331
 
332
+ # 初始化統計 HTML
333
+ init_h_tot, init_h_rat, init_h_sta = calculate_stats(log_history)
334
+
335
  # --- 4. UI 介面設計 ---
336
  custom_css = """
337
  .mock-panel { background-color: var(--background-fill-secondary); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-color-primary); }
 
340
  .weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
341
  .headless-tabs > div:first-child { display: none !important; }
342
  .headless-tabs { border: none !important; background: transparent !important; }
343
+
344
+ /* 🌟 新增:統計卡片樣式 */
345
+ .stat-card {
346
+ background: white;
347
+ padding: 15px;
348
+ border-radius: 10px;
349
+ box-shadow: 0 2px 5px rgba(0,0,0,0.05);
350
+ text-align: center;
351
+ border: 1px solid #eee;
352
+ height: 120px;
353
+ display: flex;
354
+ flex-direction: column;
355
+ justify-content: center;
356
+ }
357
+ .stat-title {
358
+ color: #7f8c8d;
359
+ font-size: 14px;
360
+ margin-bottom: 5px;
361
+ font-weight: bold;
362
+ }
363
+ .stat-value {
364
+ color: #2c3e50;
365
+ font-size: 32px;
366
+ font-weight: bold;
367
+ }
368
+ .stat-unit {
369
+ color: #bdc3c7;
370
+ font-size: 12px;
371
+ margin-top: 2px;
372
+ }
373
  """
374
 
375
  with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css) as demo:
 
412
  gr.Markdown("---")
413
 
414
  with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
415
+ current_tab_state = gr.Textbox(value="student_tab", visible=False) # 保持 Tab 狀態
416
+
417
  with gr.Tab("👨‍🎓 學生專區", id="student_tab"):
418
  with gr.Accordion("▼ 1. 教務系統", open=True):
419
  with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
 
427
  btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
428
 
429
  with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
430
+ # 🌟 新增:統計功能區塊 (放置於防護強度上方)
431
+ with gr.Row():
432
+ stat_tot_html = gr.HTML(value=init_h_tot)
433
+ stat_rat_html = gr.HTML(value=init_h_rat)
434
+ stat_sta_html = gr.HTML(value=init_h_sta)
435
+
436
  with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
437
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>手動輸入 0~100 的數值。提高強度會增加誤報率,但能更有效攔截新型攻擊。</span>")
438
  with gr.Row():
 
442
  ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100)
443
  device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100)
444
 
445
+ # 異常 Log 分析面板
446
  with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"):
447
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>顯示所有被判定為異常的 Log。點擊分析可讓 AI 代為判讀潛在風險。</span>")
448
  abnormal_log_display = gr.Dataframe(
 
464
  with gr.Row():
465
  t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm"); t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm")
466
  with gr.Row():
467
+ t_course_norm = gr.Button("✅ 正常:課程系統 (02-25 07:50)", size="sm"); t_course_anom = gr.Button("⚠️ 異常:課程系統 (07-15 05:00)", size="sm")
468
  with gr.Row():
469
+ t_add_drop_norm = gr.Button("✅ 正常: 加退選系統 (02-18 14:00)", size="sm"); t_add_drop_anom = gr.Button("⚠️ 異常: 加退選系統 (05-15 12:00)", size="sm")
470
 
471
  gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
472
  with gr.Row():
 
476
 
477
  gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
478
  with gr.Row():
479
+ t_dorm_norm = gr.Button("✅ 正常: 宿舍系統 (07-21 23:10)", size="sm"); t_dorm_anom = gr.Button("⚠️ 異常: 宿舍系統 (11-15 16:00)", size="sm")
480
  with gr.Row():
481
+ t_schol_norm = gr.Button("✅ 正常: 獎學金申請 (03-12 14:00)", size="sm"); t_schol_anom = gr.Button("⚠️ 異常: 獎學金申請 (08-15 02:00)", size="sm")
482
  with gr.Row():
483
  t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm"); t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
484
 
 
486
  with gr.Row():
487
  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")
488
  with gr.Row():
489
+ t_leave_norm = gr.Button("✅ 正常: 請假系統 (03-29 08:45)", size="sm"); t_leave_anom = gr.Button("⚠️ 異常: 請假系統 (08-15 13:00)", size="sm")
490
  with gr.Row():
491
  t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
492
 
 
497
  btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
498
 
499
  weight_inputs = [overall_weight, time_weight, ip_weight, device_weight]
 
500
 
501
+ # 🌟 定義所有需要隨著 Log 更新而更新的 Outputs
502
+ log_updates_outputs = [log_display, abnormal_log_display, abnormal_log_input, stat_tot_html, stat_rat_html, stat_sta_html]
503
+
504
+ login_outputs = [login_view, dashboard_view, role_tabs, login_msg, welcome_text] + log_updates_outputs + [current_tab_state]
505
+
506
  login_btn.click(
507
  fn=process_login,
508
  inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input] + weight_inputs,
509
+ outputs=login_outputs
510
  )
511
 
512
  logout_btn.click(
513
  fn=logout,
514
  inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
515
+ outputs=[login_view, dashboard_view, acc_input, pwd_input, captcha_input, login_msg] + log_updates_outputs + [current_tab_state]
516
  )
517
 
518
  fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs
519
+ fast_outputs = login_outputs + [acc_input]
520
 
521
  btn_fast_student.click(fn=lambda ip, c, tm, ct, ow, tw, iw, dw: fast_login("student", ip, c, tm, ct, ow, tw, iw, dw), inputs=fast_inputs, outputs=fast_outputs)
522
  btn_fast_admin.click(fn=lambda ip, c, tm, ct, ow, tw, iw, dw: fast_login("admin", ip, c, tm, ct, ow, tw, iw, dw), inputs=fast_inputs, outputs=fast_outputs)
 
524
  btn_toggle_role.click(
525
  fn=toggle_role,
526
  inputs=[acc_input, mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs,
527
+ outputs=[role_tabs, welcome_text] + log_updates_outputs + [current_tab_state, acc_input]
528
  )
529
 
530
  action_mapping = [
 
541
  btn.click(
542
  fn=lambda ip, c, acc, t_mode, t_custom, ow, tw, iw, dw, n=action_name: simulate_click(ip, c, acc, n, t_mode, t_custom, ow, tw, iw, dw),
543
  inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
544
+ outputs=log_updates_outputs
545
  )
546
 
547
  test_mapping = [
 
562
  btn.click(
563
  fn=lambda ip, c, acc, ow, tw, iw, dw, n=action_name, t_str=test_time_str: test_anomaly(ip, c, acc, n, t_str, ow, tw, iw, dw),
564
  inputs=[mock_ip, mock_cookie, acc_input, overall_weight, time_weight, ip_weight, device_weight],
565
+ outputs=log_updates_outputs
566
  )
567
 
568
  if __name__ == "__main__":