BrianChuan commited on
Commit
51c5a22
·
1 Parent(s): 14640e3

完成階段二:偵測觸發、5分鐘計時器與驗證邏輯輯

Browse files
__pycache__/api.cpython-311.pyc CHANGED
Binary files a/__pycache__/api.cpython-311.pyc and b/__pycache__/api.cpython-311.pyc differ
 
__pycache__/auth.cpython-311.pyc CHANGED
Binary files a/__pycache__/auth.cpython-311.pyc and b/__pycache__/auth.cpython-311.pyc differ
 
__pycache__/models.cpython-311.pyc CHANGED
Binary files a/__pycache__/models.cpython-311.pyc and b/__pycache__/models.cpython-311.pyc differ
 
__pycache__/security.cpython-311.pyc CHANGED
Binary files a/__pycache__/security.cpython-311.pyc and b/__pycache__/security.cpython-311.pyc differ
 
__pycache__/ui.cpython-311.pyc CHANGED
Binary files a/__pycache__/ui.cpython-311.pyc and b/__pycache__/ui.cpython-311.pyc differ
 
api.py CHANGED
@@ -7,11 +7,9 @@ from fastapi.responses import JSONResponse
7
  from pydantic import BaseModel
8
  from typing import Optional, List
9
 
10
- from config import SYSTEM_WEIGHTS, ACTION_MAP
11
- from models import global_stats, log_history, baseline_df, AI_READY
12
- from utils import _extract_gr_update_value, update_system_weights
13
- from auth import simulate_click, USER_DB
14
  from security import test_send_security_alert, verify_identity
 
15
  from plotting import get_plot_data_for_api
16
  import pandas as pd
17
 
@@ -148,6 +146,23 @@ def test_send_security_alert_endpoint():
148
  # --- Student Actions ---
149
 
150
  def _run_student_action_helper(ip, c, acc, action_code, tm, ct):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  df_all, df_abn, inp_upd = simulate_click(ip, c, acc, action_code, tm, ct)
152
  val = _extract_gr_update_value(inp_upd)
153
  return {
@@ -189,6 +204,23 @@ def student_sql_injection(req: StudentActionExtendedRequest):
189
  @api_app.post("/api/student/run_action")
190
  def student_run_action(req: GenericActionRequest):
191
  """通用學生操作端點"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  df_all, df_abn, inp_upd = simulate_click(req.ip, req.cookie, req.account, req.action_code, req.time_mode, req.custom_time)
193
  val = _extract_gr_update_value(inp_upd)
194
  return {
 
7
  from pydantic import BaseModel
8
  from typing import Optional, List
9
 
10
+ from auth import simulate_click, USER_DB, check_session_timeouts, get_user_status
 
 
 
11
  from security import test_send_security_alert, verify_identity
12
+ from config import SYSTEM_WEIGHTS, ACTION_MAP, FIXED_TEST_EMAIL
13
  from plotting import get_plot_data_for_api
14
  import pandas as pd
15
 
 
146
  # --- Student Actions ---
147
 
148
  def _run_student_action_helper(ip, c, acc, action_code, tm, ct):
149
+ # 每次請求時檢查超時
150
+ check_session_timeouts(timeout_seconds=300)
151
+
152
+ # 檢查用戶當前狀態 (目前統一使用 FIXED_TEST_EMAIL 作為識別)
153
+ current_status = get_user_status(FIXED_TEST_EMAIL)
154
+
155
+ if current_status == "verifying":
156
+ return {
157
+ "status": "pending_verify",
158
+ "message": "偵測到異常,請於5分鐘內確認 Email 以繼續使用"
159
+ }
160
+ elif current_status == "blocked":
161
+ return {
162
+ "status": "blocked",
163
+ "message": "帳號已被封鎖,請聯絡系統管理員或寄驗證信進行解封"
164
+ }
165
+
166
  df_all, df_abn, inp_upd = simulate_click(ip, c, acc, action_code, tm, ct)
167
  val = _extract_gr_update_value(inp_upd)
168
  return {
 
204
  @api_app.post("/api/student/run_action")
205
  def student_run_action(req: GenericActionRequest):
206
  """通用學生操作端點"""
207
+ # 每次請求時檢查超時
208
+ check_session_timeouts(timeout_seconds=300)
209
+
210
+ # 檢查用戶當前狀態
211
+ current_status = get_user_status(FIXED_TEST_EMAIL)
212
+
213
+ if current_status == "verifying":
214
+ return {
215
+ "status": "pending_verify",
216
+ "message": "偵測到異常,請於5分鐘內確認 Email 以繼續使用"
217
+ }
218
+ elif current_status == "blocked":
219
+ return {
220
+ "status": "blocked",
221
+ "message": "帳號已被封鎖,請聯絡系統管理員或寄驗證信進行解封"
222
+ }
223
+
224
  df_all, df_abn, inp_upd = simulate_click(req.ip, req.cookie, req.account, req.action_code, req.time_mode, req.custom_time)
225
  val = _extract_gr_update_value(inp_upd)
226
  return {
auth.py CHANGED
@@ -18,6 +18,7 @@ session_cache_lock = threading.Lock()
18
 
19
  def _get_or_create_session_entry(user_email: str):
20
  """獲取或創建會話條目"""
 
21
  with session_cache_lock:
22
  if user_email not in session_cache:
23
  session_cache[user_email] = {
@@ -27,8 +28,51 @@ def _get_or_create_session_entry(user_email: str):
27
  }
28
  return session_cache[user_email]
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def _verify_token_to_user(token: str):
31
  """根據 token 驗證用戶"""
 
32
  with session_cache_lock:
33
  for user_email, entry in session_cache.items():
34
  if entry.get("pending_token") == token:
@@ -79,10 +123,20 @@ def logout(ip, cookie, account, time_mode, custom_time):
79
 
80
  def simulate_click(ip, cookie, account, button_name, time_mode, custom_time):
81
  """模擬點擊操作"""
 
 
 
 
 
 
 
 
 
82
  role = USER_DB.get(account, ["", "Unknown"])[1]
83
  return record_log(ip, cookie, account, role, button_name, "success", time_mode, custom_time)
84
 
85
  def test_anomaly(ip, cookie, account, action_name, test_time_str):
86
  """測試異常偵測"""
 
87
  role = USER_DB.get(account, ["", "Unknown"])[1]
88
  return record_log(ip, cookie, account, role, action_name, "success", "自訂時間 (模擬過去/未來)", test_time_str)
 
18
 
19
  def _get_or_create_session_entry(user_email: str):
20
  """獲取或創建會話條目"""
21
+ user_email = user_email.strip().replace("\xa0", " ")
22
  with session_cache_lock:
23
  if user_email not in session_cache:
24
  session_cache[user_email] = {
 
28
  }
29
  return session_cache[user_email]
30
 
31
+ def check_session_timeouts(timeout_seconds=300):
32
+ """檢查會話超時,如果 verifying 超過 timeout_seconds 則改為 blocked"""
33
+ now = datetime.datetime.now()
34
+ with session_cache_lock:
35
+ for user_email, entry in session_cache.items():
36
+ if entry.get("status") == "verifying" and entry.get("anomaly_timestamp"):
37
+ try:
38
+ # 嘗試解析多種可能的格式
39
+ ts_str = entry["anomaly_timestamp"]
40
+ try:
41
+ ts = datetime.datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S")
42
+ except ValueError:
43
+ # 處理可能帶有時區或其他格式的情況
44
+ import pandas as pd
45
+ ts = pd.to_datetime(ts_str).to_pydatetime().replace(tzinfo=None)
46
+
47
+ if (now - ts).total_seconds() > timeout_seconds:
48
+ entry["status"] = "blocked"
49
+ entry["pending_token"] = None
50
+ print(f"⏰ [Timeout] User {user_email} status changed to blocked due to verification timeout.")
51
+ except Exception as e:
52
+ print(f"❌ [Timeout Check Error] {e}")
53
+
54
+ def get_user_status(user_email: str):
55
+ """獲取用戶當前狀態"""
56
+ user_email = user_email.strip().replace("\xa0", " ")
57
+ with session_cache_lock:
58
+ if user_email not in session_cache:
59
+ return "active"
60
+ return session_cache[user_email].get("status", "active")
61
+
62
+ def unblock_user(user_email: str):
63
+ """手動解封用戶"""
64
+ user_email = user_email.strip().replace("\xa0", " ")
65
+ with session_cache_lock:
66
+ if user_email in session_cache:
67
+ session_cache[user_email]["status"] = "active"
68
+ session_cache[user_email]["pending_token"] = None
69
+ session_cache[user_email]["anomaly_timestamp"] = None
70
+ return True
71
+ return False
72
+
73
  def _verify_token_to_user(token: str):
74
  """根據 token 驗證用戶"""
75
+ token = token.strip()
76
  with session_cache_lock:
77
  for user_email, entry in session_cache.items():
78
  if entry.get("pending_token") == token:
 
123
 
124
  def simulate_click(ip, cookie, account, button_name, time_mode, custom_time):
125
  """模擬點擊操作"""
126
+ # 檢查用戶狀態
127
+ status = get_user_status(FIXED_TEST_EMAIL)
128
+ if status == "verifying":
129
+ gr.Info("偵測到異常,請於5分鐘內確認 Email 以繼續使用")
130
+ return gr.update(), gr.update(), gr.update()
131
+ elif status == "blocked":
132
+ gr.Warning("帳號已被封鎖,請聯絡系統管理員或寄驗證信進行解封")
133
+ return gr.update(), gr.update(), gr.update()
134
+
135
  role = USER_DB.get(account, ["", "Unknown"])[1]
136
  return record_log(ip, cookie, account, role, button_name, "success", time_mode, custom_time)
137
 
138
  def test_anomaly(ip, cookie, account, action_name, test_time_str):
139
  """測試異常偵測"""
140
+ # 測試異常時不阻擋,以便觀察結果
141
  role = USER_DB.get(account, ["", "Unknown"])[1]
142
  return record_log(ip, cookie, account, role, action_name, "success", "自訂時間 (模擬過去/未來)", test_time_str)
models.py CHANGED
@@ -254,6 +254,17 @@ def record_log(ip, cookie, account, role, action, status, time_mode, custom_time
254
  ai_judgment = check_anomaly(ip, account, action, log_time)
255
  if ai_judgment != "success":
256
  status = ai_judgment
 
 
 
 
 
 
 
 
 
 
 
257
 
258
  new_log = {
259
  "timestamp": log_time,
 
254
  ai_judgment = check_anomaly(ip, account, action, log_time)
255
  if ai_judgment != "success":
256
  status = ai_judgment
257
+
258
+ # 如果是異常行為,觸發安全警報與驗證流程
259
+ if "異常" in status or "受限" in status:
260
+ try:
261
+ from security import trigger_identity_verification
262
+ from config import FIXED_TEST_EMAIL
263
+ # 我們將 account 映射到測試信箱進行警報寄送
264
+ trigger_identity_verification(FIXED_TEST_EMAIL, log_time)
265
+ print(f"🚨 [Security Alert Triggered] Account: {account}, Status: {status}")
266
+ except Exception as e:
267
+ print(f"❌ [Security Alert Error] {e}")
268
 
269
  new_log = {
270
  "timestamp": log_time,
security.py CHANGED
@@ -102,9 +102,11 @@ def trigger_identity_verification(user_email: str, anomaly_timestamp: str):
102
  return verify_token, success
103
 
104
  def verify_identity(token: str, choice: str):
105
- """郵件鏈接回調"""
 
106
  user_email = _verify_token_to_user(token)
107
  if not user_email:
 
108
  return JSONResponse({"ok": False, "error": "Invalid or expired token"}, status_code=400)
109
 
110
  # 處理選擇
@@ -120,6 +122,8 @@ def verify_identity(token: str, choice: str):
120
  return JSONResponse({"ok": False, "error": "Invalid choice"}, status_code=400)
121
 
122
  with session_cache_lock:
 
 
123
  entry = session_cache.get(user_email)
124
  if not entry:
125
  return JSONResponse({"ok": False, "error": "Session not found"}, status_code=404)
 
102
  return verify_token, success
103
 
104
  def verify_identity(token: str, choice: str):
105
+ """郵件鏈接回調處理"""
106
+ token = token.strip()
107
  user_email = _verify_token_to_user(token)
108
  if not user_email:
109
+ print(f"❌ [Verification Failed] Invalid token: {token}")
110
  return JSONResponse({"ok": False, "error": "Invalid or expired token"}, status_code=400)
111
 
112
  # 處理選擇
 
122
  return JSONResponse({"ok": False, "error": "Invalid choice"}, status_code=400)
123
 
124
  with session_cache_lock:
125
+ # 確保 key 經過正規化
126
+ user_email = user_email.strip().replace("\xa0", " ")
127
  entry = session_cache.get(user_email)
128
  if not entry:
129
  return JSONResponse({"ok": False, "error": "Session not found"}, status_code=404)
ui.py CHANGED
@@ -6,7 +6,7 @@ import gradio as gr
6
  import pandas as pd
7
  from config import ACTION_MAP, USER_DB, SYSTEM_WEIGHTS
8
  from models import log_history, AI_READY
9
- from auth import process_login, fast_login, logout, toggle_role, simulate_click, test_anomaly
10
  from security import explain_abnormal_log
11
  from plotting import get_dashboard_html, get_plot_data, timer_update
12
  from utils import update_system_weights, get_current_time
@@ -238,6 +238,12 @@ def create_demo():
238
  abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入...", scale=4)
239
  btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1)
240
  ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False)
 
 
 
 
 
 
241
 
242
  with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"):
243
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>")
@@ -298,6 +304,17 @@ def create_demo():
298
  action_dropdown.change(fn=full_ui_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot, log_display, abnormal_log_display])
299
  time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
300
  btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  update_outputs = [main_router, role_tabs, login_msg, welcome_text, store_df_all, store_df_abn, abnormal_log_input]
303
 
 
6
  import pandas as pd
7
  from config import ACTION_MAP, USER_DB, SYSTEM_WEIGHTS
8
  from models import log_history, AI_READY
9
+ from auth import process_login, fast_login, logout, toggle_role, simulate_click, test_anomaly, unblock_user
10
  from security import explain_abnormal_log
11
  from plotting import get_dashboard_html, get_plot_data, timer_update
12
  from utils import update_system_weights, get_current_time
 
238
  abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入...", scale=4)
239
  btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1)
240
  ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False)
241
+
242
+ with gr.Accordion("▼ 使用者狀態管理", open=True, elem_classes="weight-card"):
243
+ from config import FIXED_TEST_EMAIL
244
+ gr.Markdown(f"<span style='color:gray; font-size:0.9em;'>手動解除對測試帳號 ({FIXED_TEST_EMAIL}) 的封鎖狀態。</span>")
245
+ btn_unblock = gr.Button("🔓 解除封鎖 (Unblock User)", variant="primary")
246
+ unblock_status = gr.Markdown(value="")
247
 
248
  with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"):
249
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>")
 
304
  action_dropdown.change(fn=full_ui_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot, log_display, abnormal_log_display])
305
  time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
306
  btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
307
+
308
+ def handle_unblock():
309
+ from config import FIXED_TEST_EMAIL
310
+ success = unblock_user(FIXED_TEST_EMAIL)
311
+ if success:
312
+ gr.Info(f"已成功解除 {FIXED_TEST_EMAIL} 的封鎖狀態。")
313
+ return "✅ 狀態已重設為 Active"
314
+ else:
315
+ return "❌ 找不到該使用者或已是 Active 狀態"
316
+
317
+ btn_unblock.click(fn=handle_unblock, inputs=[], outputs=[unblock_status])
318
 
319
  update_outputs = [main_router, role_tabs, login_msg, welcome_text, store_df_all, store_df_abn, abnormal_log_input]
320