Spaces:
Sleeping
Sleeping
BrianChuan commited on
Commit ·
51c5a22
1
Parent(s): 14640e3
完成階段二:偵測觸發、5分鐘計時器與驗證邏輯輯
Browse files- __pycache__/api.cpython-311.pyc +0 -0
- __pycache__/auth.cpython-311.pyc +0 -0
- __pycache__/models.cpython-311.pyc +0 -0
- __pycache__/security.cpython-311.pyc +0 -0
- __pycache__/ui.cpython-311.pyc +0 -0
- api.py +36 -4
- auth.py +54 -0
- models.py +11 -0
- security.py +5 -1
- ui.py +18 -1
__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
|
| 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 |
|