Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- app.py +300 -98
- baseline_logs.csv +0 -0
- hdbscan_model.pkl +2 -2
- le_ip.pkl +2 -2
app.py
CHANGED
|
@@ -4,19 +4,29 @@ import pandas as pd
|
|
| 4 |
import joblib
|
| 5 |
from sklearn.cluster import HDBSCAN
|
| 6 |
import numpy as np
|
| 7 |
-
import logger_service
|
| 8 |
|
| 9 |
-
#
|
|
|
|
|
|
|
|
|
|
| 10 |
try:
|
| 11 |
le_action = joblib.load('le_action.pkl')
|
| 12 |
le_ip = joblib.load('le_ip.pkl')
|
| 13 |
|
| 14 |
-
# 🌟 載入歷史資料作為 HDBSCAN 即時比對的基準
|
| 15 |
baseline_df = pd.read_csv('baseline_logs.csv')
|
| 16 |
-
baseline_df['
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
# 預先將歷史資料轉成特徵矩陣
|
| 19 |
baseline_X = np.column_stack((
|
|
|
|
|
|
|
| 20 |
baseline_df['hour'].values,
|
| 21 |
le_ip.transform(baseline_df['ip_address'].values),
|
| 22 |
le_action.transform(baseline_df['action'].values)
|
|
@@ -26,117 +36,194 @@ try:
|
|
| 26 |
print("✅ AI 模組與歷史基準資料載入成功!")
|
| 27 |
except Exception as e:
|
| 28 |
AI_READY = False
|
| 29 |
-
print(f"⚠️ AI 模組載入失敗,請確認
|
| 30 |
|
| 31 |
USER_DB = {
|
| 32 |
"student": ["1234", "學生", "同學"],
|
| 33 |
-
"
|
| 34 |
-
"admin": ["1234", "行政", "行政人員"],
|
| 35 |
-
"ta": ["1234", "TA", "助教"],
|
| 36 |
-
"sysadmin": ["1234", "系統管理員", "管理員"]
|
| 37 |
}
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
if not AI_READY:
|
| 44 |
-
return "success"
|
| 45 |
-
|
| 46 |
-
if action in ["login_attempt", "logout"]:
|
| 47 |
-
return "success"
|
| 48 |
|
| 49 |
try:
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
combined_X = np.vstack((baseline_X, new_data_point))
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
| 66 |
|
| 67 |
-
#
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
return "⚠️異常_作息或行為不符"
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
except ValueError:
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
if time_mode == "自訂時間 (模擬過去/未來)" and custom_time.strip() != "":
|
| 81 |
-
log_time = custom_time
|
| 82 |
else:
|
| 83 |
tw_tz = datetime.timezone(datetime.timedelta(hours=8))
|
| 84 |
log_time = datetime.datetime.now(tw_tz).strftime("%Y-%m-%d %H:%M:%S")
|
| 85 |
|
| 86 |
-
# 執行 AI 檢查 (現在把 log_time 也傳進去了)
|
| 87 |
if status == "success":
|
| 88 |
-
ai_judgment = check_anomaly(ip, action, log_time)
|
| 89 |
-
if ai_judgment != "success":
|
| 90 |
-
status = ai_judgment
|
| 91 |
|
| 92 |
new_log = {
|
| 93 |
-
"timestamp": log_time,
|
| 94 |
-
"
|
| 95 |
-
"cookie_id": cookie,
|
| 96 |
-
"account": account,
|
| 97 |
-
"role": role,
|
| 98 |
-
"action": action,
|
| 99 |
-
"status": status
|
| 100 |
}
|
| 101 |
-
|
|
|
|
| 102 |
try:
|
| 103 |
-
|
| 104 |
-
new_hour = pd.to_datetime(log_time).hour
|
| 105 |
-
new_ip_code = le_ip.transform([ip])[0]
|
| 106 |
-
new_action_code = le_action.transform([action])[0]
|
| 107 |
-
new_feature = np.array([[new_hour, new_ip_code, new_action_code]])
|
| 108 |
-
|
| 109 |
-
# 🌟 把新特徵壓入全域矩陣中
|
| 110 |
-
baseline_X = np.vstack((baseline_X, new_feature))
|
| 111 |
except Exception as e:
|
| 112 |
-
print(f"⚠️
|
| 113 |
-
|
| 114 |
-
# 呼叫外部 Service 進行存檔與上傳
|
| 115 |
-
logger_service.push_new_log(new_log)
|
| 116 |
|
| 117 |
log_history.insert(0, new_log)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
return pd.DataFrame(log_history)
|
| 119 |
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
valid_captcha = "FEIK"
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
role, name = USER_DB[account][1], USER_DB[account][2]
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
if USER_DB[account][0] == password:
|
| 126 |
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
|
| 127 |
-
df = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time)
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
def
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
def logout(ip, cookie, account, time_mode, custom_time):
|
| 136 |
role = USER_DB.get(account, ["", "Unknown"])[1]
|
| 137 |
-
df = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time)
|
| 138 |
return gr.update(visible=True), gr.update(visible=False), "", "", "", "", df
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
def toggle_time_input(mode):
|
| 141 |
return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)"))
|
| 142 |
|
|
@@ -144,8 +231,10 @@ def toggle_time_input(mode):
|
|
| 144 |
custom_css = """
|
| 145 |
.mock-panel { background-color: var(--background-fill-secondary); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-color-primary); }
|
| 146 |
.login-container { max-width: 500px; margin: 0 auto; padding: 20px; }
|
| 147 |
-
.placeholder-box { background-color: var(--background-fill-secondary); border: 2px dashed var(--border-color-primary); text-align: center; color: var(--body-text-color); padding: 40px 0; margin-bottom: 15px; border-radius: 5px; }
|
| 148 |
.ntut-title { color: #d9534f; font-size: 22px; font-weight: bold; text-align: center; margin-bottom: 20px;}
|
|
|
|
|
|
|
|
|
|
| 149 |
"""
|
| 150 |
|
| 151 |
with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css) as demo:
|
|
@@ -159,43 +248,156 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
|
|
| 159 |
custom_time_input = gr.Textbox(label="輸入自訂時間", placeholder="格式: YYYY-MM-DD HH:MM:SS", value="2026-03-17 03:00:00", visible=False)
|
| 160 |
gr.Markdown("---")
|
| 161 |
gr.Markdown("### 📊 即時 Log 與 AI 判定區")
|
| 162 |
-
log_display = gr.Dataframe(headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True)
|
| 163 |
|
| 164 |
with gr.Column(scale=2):
|
| 165 |
with gr.Column(visible=True, elem_classes="login-container") as login_view:
|
| 166 |
-
gr.HTML("<div class='placeholder-box'>預留位置:台北科大 Logo 與 e化校園圖片</div>")
|
| 167 |
gr.Markdown("<div class='ntut-title'>校園入口網站 Taipei Tech Portal</div>")
|
| 168 |
login_msg = gr.Markdown(value="", visible=True)
|
| 169 |
-
with gr.Row(): acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="
|
| 170 |
-
with gr.Row(): pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="
|
| 171 |
with gr.Row(): gr.HTML("<div style='background:var(--background-fill-secondary); color:var(--body-text-color); padding:10px; font-weight:bold; letter-spacing: 3px; border:1px solid var(--border-color-primary); text-align:center;'>F E I K</div>")
|
| 172 |
with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)")
|
| 173 |
login_btn = gr.Button("登入 Login", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
with gr.Column(visible=False) as dashboard_view:
|
| 176 |
-
gr.HTML("<div class='placeholder-box'>預留位置:學生學習歷程入口網站橫幅</div>")
|
| 177 |
with gr.Row():
|
| 178 |
gr.Markdown("### 資訊系統")
|
| 179 |
welcome_text = gr.Markdown(value="", elem_classes="text-right")
|
| 180 |
logout_btn = gr.Button("登出", size="sm")
|
|
|
|
|
|
|
|
|
|
| 181 |
gr.Markdown("---")
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
with gr.
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
|
|
|
| 193 |
time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
|
| 194 |
-
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
-
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
if __name__ == "__main__":
|
| 201 |
demo.launch()
|
|
|
|
| 4 |
import joblib
|
| 5 |
from sklearn.cluster import HDBSCAN
|
| 6 |
import numpy as np
|
| 7 |
+
import logger_service # 🌟 保持:匯入外部日誌服務模組
|
| 8 |
|
| 9 |
+
# 初始化歷史日誌串列
|
| 10 |
+
log_history = []
|
| 11 |
+
|
| 12 |
+
# --- 1. 載入模型與歷史資料 ---
|
| 13 |
try:
|
| 14 |
le_action = joblib.load('le_action.pkl')
|
| 15 |
le_ip = joblib.load('le_ip.pkl')
|
| 16 |
|
|
|
|
| 17 |
baseline_df = pd.read_csv('baseline_logs.csv')
|
| 18 |
+
baseline_df['timestamp'] = pd.to_datetime(baseline_df['timestamp'])
|
| 19 |
+
|
| 20 |
+
# 🌟 保持:預載歷史日誌顯示
|
| 21 |
+
log_history = baseline_df.sort_values(by='timestamp', ascending=False).head(20).to_dict('records')
|
| 22 |
+
|
| 23 |
+
baseline_df['month'] = baseline_df['timestamp'].dt.month
|
| 24 |
+
baseline_df['day'] = baseline_df['timestamp'].dt.day
|
| 25 |
+
baseline_df['hour'] = baseline_df['timestamp'].dt.hour
|
| 26 |
|
|
|
|
| 27 |
baseline_X = np.column_stack((
|
| 28 |
+
baseline_df['month'].values,
|
| 29 |
+
baseline_df['day'].values,
|
| 30 |
baseline_df['hour'].values,
|
| 31 |
le_ip.transform(baseline_df['ip_address'].values),
|
| 32 |
le_action.transform(baseline_df['action'].values)
|
|
|
|
| 36 |
print("✅ AI 模組與歷史基準資料載入成功!")
|
| 37 |
except Exception as e:
|
| 38 |
AI_READY = False
|
| 39 |
+
print(f"⚠️ AI 模組載入失敗,請確認檔案。錯誤: {e}")
|
| 40 |
|
| 41 |
USER_DB = {
|
| 42 |
"student": ["1234", "學生", "同學"],
|
| 43 |
+
"admin": ["1234", "管理", "管理員"],
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
+
# --- 2. 核心邏輯與真實 HDBSCAN 異常偵測 ---
|
| 47 |
+
def check_anomaly(ip, action, log_time, overall_w, time_w, ip_w, dev_w):
|
| 48 |
+
if not AI_READY: return "success"
|
| 49 |
+
if action in ["login_attempt", "logout"]: return "success"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
try:
|
| 52 |
+
ow = float(overall_w)
|
| 53 |
+
tw = float(time_w)
|
| 54 |
+
iw = float(ip_w)
|
| 55 |
+
dw = float(dev_w)
|
| 56 |
+
|
| 57 |
+
clean_ip = str(ip).split(" ")[0]
|
| 58 |
+
ip_code = float(le_ip.transform([clean_ip])[0])
|
| 59 |
+
action_code = float(le_action.transform([action])[0])
|
| 60 |
|
| 61 |
+
current_time = pd.to_datetime(str(log_time))
|
| 62 |
+
current_month = float(current_time.month)
|
| 63 |
+
current_day = float(current_time.day)
|
| 64 |
+
current_hour = float(current_time.hour)
|
| 65 |
|
| 66 |
+
time_multiplier = tw / 100.0
|
| 67 |
+
action_scale = (dw / 100.0) * 100.0
|
| 68 |
+
|
| 69 |
+
scaled_baseline = baseline_X.copy().astype(float)
|
| 70 |
+
scaled_baseline[:, 0] *= time_multiplier * 10.0
|
| 71 |
+
scaled_baseline[:, 1] *= time_multiplier * 0.5
|
| 72 |
+
scaled_baseline[:, 2] *= time_multiplier * 2.0
|
| 73 |
+
scaled_baseline[:, 3] *= (iw / 100.0) * 10.0
|
| 74 |
+
scaled_baseline[:, 4] *= action_scale
|
| 75 |
+
|
| 76 |
+
v_month = current_month * time_multiplier * 10.0
|
| 77 |
+
v_day = current_day * time_multiplier * 0.5
|
| 78 |
+
v_hour = current_hour * time_multiplier * 2.0
|
| 79 |
+
v_ip = ip_code * (iw / 100.0) * 10.0
|
| 80 |
+
v_action = action_code * action_scale
|
| 81 |
+
|
| 82 |
+
new_data_point = np.array([[v_month, v_day, v_hour, v_ip, v_action]], dtype=float)
|
| 83 |
+
|
| 84 |
+
# 🌟 智能特徵抽樣 (Smart Sampling) 核心邏輯
|
| 85 |
+
# 1. 找出歷史資料中,跟「當下準備執行的行為」完全一樣的紀錄 (保留季節性與週期性記憶)
|
| 86 |
+
same_action_mask = (baseline_X[:, 4] == action_code)
|
| 87 |
+
relevant_history = scaled_baseline[same_action_mask]
|
| 88 |
+
|
| 89 |
+
# 2. 隨機抽取一部分其他的背景資料 (維持模型對「一般常規作息」的認知)
|
| 90 |
+
other_history = scaled_baseline[~same_action_mask]
|
| 91 |
+
if len(other_history) > 800:
|
| 92 |
+
# 固定隨機種子,確保相同情境下的抽樣穩定度
|
| 93 |
+
np.random.seed(int(current_day + current_hour))
|
| 94 |
+
indices = np.random.choice(len(other_history), 800, replace=False)
|
| 95 |
+
other_history = other_history[indices]
|
| 96 |
+
|
| 97 |
+
# 3. 組合出兼顧「運算速度」與「全域歷史視野」的精華資料集
|
| 98 |
+
fit_X = np.vstack((other_history, relevant_history, new_data_point))
|
| 99 |
|
| 100 |
+
strictness = max(2, int(2 + (ow / 100.0) * 4))
|
|
|
|
| 101 |
|
| 102 |
+
model = HDBSCAN(
|
| 103 |
+
min_cluster_size=strictness,
|
| 104 |
+
min_samples=2
|
| 105 |
+
)
|
| 106 |
|
| 107 |
+
# 改用濃縮後的 fit_X 進行快速分群
|
| 108 |
+
clusters = model.fit_predict(fit_X)
|
| 109 |
+
|
| 110 |
+
if clusters[-1] == -1:
|
| 111 |
return "⚠️異常_作息或行為不符"
|
| 112 |
+
|
| 113 |
+
# 距離計算邏輯保持不變
|
| 114 |
+
if np.any(same_action_mask):
|
| 115 |
+
history_points = scaled_baseline[same_action_mask]
|
| 116 |
+
distances = np.linalg.norm(history_points - new_data_point[0], axis=1)
|
| 117 |
+
min_distance = np.min(distances)
|
| 118 |
+
|
| 119 |
+
max_tolerance_distance = time_multiplier * 15.0 + 1.0
|
| 120 |
|
| 121 |
+
if min_distance > max_tolerance_distance:
|
| 122 |
+
return "⚠️異常_作息或行為不符"
|
| 123 |
+
|
| 124 |
+
return "success"
|
| 125 |
except ValueError:
|
| 126 |
+
return "⚠️異常_未知特徵 (出現未授權的 IP 或操作)"
|
| 127 |
+
except Exception as e:
|
| 128 |
+
return f"⚠️系統錯誤: {str(e)}"
|
| 129 |
+
|
| 130 |
+
def record_log(ip, cookie, account, role, action, status, time_mode, custom_time, ow, tw, iw, dw):
|
| 131 |
+
global baseline_X
|
| 132 |
+
if time_mode == "自訂時間 (模擬過去/未來)" and custom_time and str(custom_time).strip() != "":
|
| 133 |
+
log_time = str(custom_time)
|
| 134 |
else:
|
| 135 |
tw_tz = datetime.timezone(datetime.timedelta(hours=8))
|
| 136 |
log_time = datetime.datetime.now(tw_tz).strftime("%Y-%m-%d %H:%M:%S")
|
| 137 |
|
|
|
|
| 138 |
if status == "success":
|
| 139 |
+
ai_judgment = check_anomaly(ip, action, log_time, ow, tw, iw, dw)
|
| 140 |
+
if ai_judgment != "success": status = ai_judgment
|
|
|
|
| 141 |
|
| 142 |
new_log = {
|
| 143 |
+
"timestamp": log_time, "ip_address": ip, "cookie_id": cookie,
|
| 144 |
+
"account": account, "role": role, "action": action, "status": status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
+
|
| 147 |
+
# 🌟 保持:整合外部日誌服務
|
| 148 |
try:
|
| 149 |
+
logger_service.push_new_log(new_log)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
except Exception as e:
|
| 151 |
+
print(f"⚠️ 外部日誌推送失敗: {e}")
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
log_history.insert(0, new_log)
|
| 154 |
+
|
| 155 |
+
if status == "success" and AI_READY:
|
| 156 |
+
try:
|
| 157 |
+
pd.DataFrame([new_log]).to_csv('baseline_logs.csv', mode='a', header=False, index=False, encoding='utf-8-sig')
|
| 158 |
+
|
| 159 |
+
current_time = pd.to_datetime(str(log_time))
|
| 160 |
+
clean_ip = str(ip).split(" ")[0]
|
| 161 |
+
new_row = np.array([[
|
| 162 |
+
float(current_time.month),
|
| 163 |
+
float(current_time.day),
|
| 164 |
+
float(current_time.hour),
|
| 165 |
+
float(le_ip.transform([clean_ip])[0]),
|
| 166 |
+
float(le_action.transform([action])[0])
|
| 167 |
+
]], dtype=float)
|
| 168 |
+
baseline_X = np.vstack((baseline_X, new_row))
|
| 169 |
+
except Exception as e:
|
| 170 |
+
pass
|
| 171 |
+
|
| 172 |
return pd.DataFrame(log_history)
|
| 173 |
|
| 174 |
+
# --- 3. 視角切換與登入控制邏輯 ---
|
| 175 |
+
def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
|
| 176 |
+
role, name = USER_DB[target_account][1], USER_DB[target_account][2]
|
| 177 |
+
df = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 178 |
+
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)"
|
| 179 |
+
target_tab = "student_tab" if role == "學生" else "admin_tab"
|
| 180 |
+
return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df, target_account
|
| 181 |
+
|
| 182 |
+
def process_login(ip, cookie, account, password, captcha, time_mode, custom_time, ow, tw, iw, dw):
|
| 183 |
valid_captcha = "FEIK"
|
| 184 |
+
def fail_response(msg, df):
|
| 185 |
+
return gr.update(visible=True), gr.update(visible=False), gr.update(), msg, gr.update(), df
|
| 186 |
+
|
| 187 |
+
if account not in USER_DB:
|
| 188 |
+
df = record_log(ip, cookie, account, "Unknown", "login_attempt", "failed_no_account", time_mode, custom_time, ow, tw, iw, dw)
|
| 189 |
+
return fail_response("帳號不存在!", df)
|
| 190 |
+
|
| 191 |
role, name = USER_DB[account][1], USER_DB[account][2]
|
| 192 |
+
|
| 193 |
+
if captcha.upper() != valid_captcha:
|
| 194 |
+
df = record_log(ip, cookie, account, role, "login_attempt", "failed_captcha", time_mode, custom_time, ow, tw, iw, dw)
|
| 195 |
+
return fail_response("驗證碼錯誤!", df)
|
| 196 |
+
|
| 197 |
if USER_DB[account][0] == password:
|
| 198 |
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
|
| 199 |
+
df = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 200 |
+
target_tab = "student_tab" if role == "學生" else "admin_tab"
|
| 201 |
+
return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df
|
| 202 |
+
|
| 203 |
+
df = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time, ow, tw, iw, dw)
|
| 204 |
+
return fail_response("密碼錯誤!", df)
|
| 205 |
|
| 206 |
+
def toggle_role(current_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
|
| 207 |
+
target_account = "student" if current_account == "admin" else "admin"
|
| 208 |
+
role, name = USER_DB[target_account][1], USER_DB[target_account][2]
|
| 209 |
+
df = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 210 |
+
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡單鍵切換模式)"
|
| 211 |
+
target_tab = "student_tab" if role == "學生" else "admin_tab"
|
| 212 |
+
return gr.update(selected=target_tab), welcome_msg, df, target_account
|
| 213 |
|
| 214 |
+
def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
|
| 215 |
role = USER_DB.get(account, ["", "Unknown"])[1]
|
| 216 |
+
df = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 217 |
return gr.update(visible=True), gr.update(visible=False), "", "", "", "", df
|
| 218 |
|
| 219 |
+
def simulate_click(ip, cookie, account, button_name, time_mode, custom_time, ow, tw, iw, dw):
|
| 220 |
+
role = USER_DB.get(account, ["", "Unknown"])[1]
|
| 221 |
+
return record_log(ip, cookie, account, role, button_name, "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 222 |
+
|
| 223 |
+
def test_anomaly(ip, cookie, account, action_name, test_time_str, ow, tw, iw, dw):
|
| 224 |
+
role = USER_DB.get(account, ["", "Unknown"])[1]
|
| 225 |
+
return record_log(ip, cookie, account, role, action_name, "success", "自訂時間 (模擬過去/未來)", test_time_str, ow, tw, iw, dw)
|
| 226 |
+
|
| 227 |
def toggle_time_input(mode):
|
| 228 |
return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)"))
|
| 229 |
|
|
|
|
| 231 |
custom_css = """
|
| 232 |
.mock-panel { background-color: var(--background-fill-secondary); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-color-primary); }
|
| 233 |
.login-container { max-width: 500px; margin: 0 auto; padding: 20px; }
|
|
|
|
| 234 |
.ntut-title { color: #d9534f; font-size: 22px; font-weight: bold; text-align: center; margin-bottom: 20px;}
|
| 235 |
+
.weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
|
| 236 |
+
.headless-tabs > div:first-child { display: none !important; }
|
| 237 |
+
.headless-tabs { border: none !important; background: transparent !important; }
|
| 238 |
"""
|
| 239 |
|
| 240 |
with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css) as demo:
|
|
|
|
| 248 |
custom_time_input = gr.Textbox(label="輸入自訂時間", placeholder="格式: YYYY-MM-DD HH:MM:SS", value="2026-03-17 03:00:00", visible=False)
|
| 249 |
gr.Markdown("---")
|
| 250 |
gr.Markdown("### 📊 即時 Log 與 AI 判定區")
|
| 251 |
+
log_display = gr.Dataframe(value=log_history, headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True)
|
| 252 |
|
| 253 |
with gr.Column(scale=2):
|
| 254 |
with gr.Column(visible=True, elem_classes="login-container") as login_view:
|
|
|
|
| 255 |
gr.Markdown("<div class='ntut-title'>校園入口網站 Taipei Tech Portal</div>")
|
| 256 |
login_msg = gr.Markdown(value="", visible=True)
|
| 257 |
+
with gr.Row(): acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="student or admin")
|
| 258 |
+
with gr.Row(): pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="password : 1234")
|
| 259 |
with gr.Row(): gr.HTML("<div style='background:var(--background-fill-secondary); color:var(--body-text-color); padding:10px; font-weight:bold; letter-spacing: 3px; border:1px solid var(--border-color-primary); text-align:center;'>F E I K</div>")
|
| 260 |
with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)")
|
| 261 |
login_btn = gr.Button("登入 Login", variant="primary")
|
| 262 |
+
|
| 263 |
+
gr.Markdown("---")
|
| 264 |
+
gr.Markdown("<div style='text-align:center; color:gray; font-size: 0.9em;'>🛠️ 開發與測試專用捷徑</div>")
|
| 265 |
+
with gr.Row():
|
| 266 |
+
btn_fast_student = gr.Button("🚀 快速登入 (學生)", variant="secondary")
|
| 267 |
+
btn_fast_admin = gr.Button("🚀 快速登入 (管理員)", variant="secondary")
|
| 268 |
|
| 269 |
with gr.Column(visible=False) as dashboard_view:
|
|
|
|
| 270 |
with gr.Row():
|
| 271 |
gr.Markdown("### 資訊系統")
|
| 272 |
welcome_text = gr.Markdown(value="", elem_classes="text-right")
|
| 273 |
logout_btn = gr.Button("登出", size="sm")
|
| 274 |
+
|
| 275 |
+
with gr.Row():
|
| 276 |
+
btn_toggle_role = gr.Button("🔄 Student ↔ Admin", size="sm", variant="secondary")
|
| 277 |
gr.Markdown("---")
|
| 278 |
+
|
| 279 |
+
with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
|
| 280 |
+
|
| 281 |
+
with gr.Tab("👨🎓 學生專區", id="student_tab"):
|
| 282 |
+
with gr.Accordion("▼ 1. 教務系統", open=True):
|
| 283 |
+
with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
|
| 284 |
+
with gr.Row(): btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm"); btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm"); btn_course = gr.Button("▶ 課程系統", size="sm")
|
| 285 |
+
with gr.Row(): btn_score = gr.Button("▶ 學業成績查詢系統", size="sm")
|
| 286 |
+
with gr.Accordion("▼ 2. 學務系統", open=True):
|
| 287 |
+
with gr.Row(): btn_leave = gr.Button("▶ 學生請假系統", size="sm"); btn_dorm = gr.Button("▶ 學生宿舍登錄(抽籤)系統", size="sm"); btn_scholarship = gr.Button("▶ 獎助學金申請系統", size="sm")
|
| 288 |
+
with gr.Accordion("▼ 3. 資訊服務", open=True):
|
| 289 |
+
with gr.Row(): btn_webmail = gr.Button("▶ 網路郵局 WebMail", size="sm"); btn_vdesk = gr.Button("▶ 北科軟體雲", size="sm")
|
| 290 |
+
with gr.Accordion("▼ 惡意操作測試區", open=True):
|
| 291 |
+
btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
|
| 292 |
+
|
| 293 |
+
with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
|
| 294 |
+
with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
|
| 295 |
+
gr.Markdown("<span style='color:gray; font-size:0.9em;'>手動輸入 0~100 的數值。提高強度會增加誤報率,但能更有效攔截新型攻擊。</span>")
|
| 296 |
+
with gr.Row():
|
| 297 |
+
overall_weight = gr.Number(label="總體防護強度 (%)", value=75, minimum=0, maximum=100)
|
| 298 |
+
time_weight = gr.Number(label="時間特徵權重 (%)", value=80, minimum=0, maximum=100)
|
| 299 |
+
with gr.Row():
|
| 300 |
+
ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100)
|
| 301 |
+
device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100)
|
| 302 |
+
|
| 303 |
+
with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"):
|
| 304 |
+
gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>")
|
| 305 |
+
|
| 306 |
+
gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)")
|
| 307 |
+
with gr.Row():
|
| 308 |
+
t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm"); t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm")
|
| 309 |
+
with gr.Row():
|
| 310 |
+
t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm"); t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm")
|
| 311 |
+
with gr.Row():
|
| 312 |
+
t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm"); t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm")
|
| 313 |
+
|
| 314 |
+
gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
|
| 315 |
+
with gr.Row():
|
| 316 |
+
t_eval_norm = gr.Button("✅ 正常: 期末網路教學評量 (12-17 13:20)", size="sm"); t_eval_anom = gr.Button("⚠️ 異常: 期末網路教學評量 (03-15 03:00)", size="sm")
|
| 317 |
+
with gr.Row():
|
| 318 |
+
t_score_norm = gr.Button("✅ 正常: 學業成績查詢系統 (11-17 18:50)", size="sm"); t_score_anom = gr.Button("⚠️ 異常: 學業成績查詢系統 (02-15 03:00)", size="sm")
|
| 319 |
+
|
| 320 |
+
gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
|
| 321 |
+
with gr.Row():
|
| 322 |
+
t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm"); t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm")
|
| 323 |
+
with gr.Row():
|
| 324 |
+
t_schol_norm = gr.Button("✅ 正常: 獎助學金申���系統 (03-12 14:00)", size="sm"); t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm")
|
| 325 |
+
with gr.Row():
|
| 326 |
+
t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm"); t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
|
| 327 |
+
|
| 328 |
+
gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)")
|
| 329 |
+
with gr.Row():
|
| 330 |
+
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")
|
| 331 |
+
with gr.Row():
|
| 332 |
+
t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm"); t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm")
|
| 333 |
+
with gr.Row():
|
| 334 |
+
t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
|
| 335 |
|
| 336 |
+
# --- 綁定事件 ---
|
| 337 |
time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
|
| 338 |
+
|
| 339 |
+
weight_inputs = [overall_weight, time_weight, ip_weight, device_weight]
|
| 340 |
+
|
| 341 |
+
login_btn.click(
|
| 342 |
+
fn=process_login,
|
| 343 |
+
inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input] + weight_inputs,
|
| 344 |
+
outputs=[login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display]
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
logout_btn.click(
|
| 348 |
+
fn=logout,
|
| 349 |
+
inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
|
| 350 |
+
outputs=[login_view, dashboard_view, acc_input, pwd_input, captcha_input, login_msg, log_display]
|
| 351 |
+
)
|
| 352 |
|
| 353 |
+
fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs
|
| 354 |
+
|
| 355 |
+
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=[login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display, acc_input])
|
| 356 |
+
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=[login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display, acc_input])
|
| 357 |
+
|
| 358 |
+
btn_toggle_role.click(
|
| 359 |
+
fn=toggle_role,
|
| 360 |
+
inputs=[acc_input, mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs,
|
| 361 |
+
outputs=[role_tabs, welcome_text, log_display, acc_input]
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
action_mapping = [
|
| 365 |
+
(btn_eval, "click_eval_system"), (btn_pre_select, "click_pre_select_system"),
|
| 366 |
+
(btn_add_drop, "click_add_drop_system"), (btn_istudy, "click_istudy"),
|
| 367 |
+
(btn_card, "click_card_loss"), (btn_course, "click_course_system"),
|
| 368 |
+
(btn_score, "click_score_query"), (btn_leave, "click_leave_system"),
|
| 369 |
+
(btn_dorm, "click_dorm_system"), (btn_scholarship, "click_scholarship"),
|
| 370 |
+
(btn_webmail, "click_webmail"), (btn_vdesk, "click_vdesk"),
|
| 371 |
+
(btn_hack_score, "malicious_sql_injection")
|
| 372 |
+
]
|
| 373 |
+
|
| 374 |
+
for btn, action_name in action_mapping:
|
| 375 |
+
btn.click(
|
| 376 |
+
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),
|
| 377 |
+
inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
|
| 378 |
+
outputs=[log_display]
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
test_mapping = [
|
| 382 |
+
(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"),
|
| 383 |
+
(t_course_norm, "click_course_system", "2026-02-25 07:50:00"), (t_course_anom, "click_course_system", "2026-07-15 05:00:00"),
|
| 384 |
+
(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"),
|
| 385 |
+
(t_eval_norm, "click_eval_system", "2026-12-17 13:20:00"), (t_eval_anom, "click_eval_system", "2026-03-15 03:00:00"),
|
| 386 |
+
(t_score_norm, "click_score_query", "2026-11-17 18:50:00"), (t_score_anom, "click_score_query", "2026-02-15 03:00:00"),
|
| 387 |
+
(t_dorm_norm, "click_dorm_system", "2026-07-21 23:10:00"), (t_dorm_anom, "click_dorm_system", "2026-11-15 16:00:00"),
|
| 388 |
+
(t_schol_norm, "click_scholarship", "2026-03-12 14:00:00"), (t_schol_anom, "click_scholarship", "2026-08-15 02:00:00"),
|
| 389 |
+
(t_card_norm, "click_card_loss", "2026-10-23 13:40:00"), (t_card_anom, "click_card_loss", "2026-08-10 03:00:00"),
|
| 390 |
+
(t_istudy_norm, "click_istudy", "2026-10-18 16:00:00"), (t_istudy_anom, "click_istudy", "2026-08-20 02:00:00"),
|
| 391 |
+
(t_leave_norm, "click_leave_system", "2026-03-29 08:45:00"), (t_leave_anom, "click_leave_system", "2026-08-15 13:00:00"),
|
| 392 |
+
(t_vdesk_norm, "click_vdesk", "2026-03-22 18:00:00"), (t_vdesk_anom, "click_vdesk", "2026-08-10 00:00:00")
|
| 393 |
+
]
|
| 394 |
+
|
| 395 |
+
for btn, action_name, test_time_str in test_mapping:
|
| 396 |
+
btn.click(
|
| 397 |
+
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),
|
| 398 |
+
inputs=[mock_ip, mock_cookie, acc_input, overall_weight, time_weight, ip_weight, device_weight],
|
| 399 |
+
outputs=[log_display]
|
| 400 |
+
)
|
| 401 |
|
| 402 |
if __name__ == "__main__":
|
| 403 |
demo.launch()
|
baseline_logs.csv
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
hdbscan_model.pkl
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ffc4261a64d9a47627fb6dca0def1b439f388a6a6b93be93d016b5b03afc709
|
| 3 |
+
size 3600983
|
le_ip.pkl
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4c3f5241fbb068ed41f7dfa5fab989c781092faccd92092669456eee62ec2da7
|
| 3 |
+
size 559
|