Spaces:
Sleeping
Sleeping
statistics, chart, abnormal log
Browse files
app.py
CHANGED
|
@@ -4,22 +4,74 @@ 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 |
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
|
|
@@ -36,6 +88,8 @@ try:
|
|
| 36 |
print("✅ AI 模組與歷史基準資料載入成功!")
|
| 37 |
except Exception as e:
|
| 38 |
AI_READY = False
|
|
|
|
|
|
|
| 39 |
print(f"⚠️ AI 模組載入失敗,請確認檔案。錯誤: {e}")
|
| 40 |
|
| 41 |
USER_DB = {
|
|
@@ -43,17 +97,188 @@ USER_DB = {
|
|
| 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])
|
|
@@ -81,43 +306,29 @@ def check_anomaly(ip, action, log_time, overall_w, time_w, ip_w, dev_w):
|
|
| 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 |
|
|
@@ -144,77 +355,84 @@ def record_log(ip, cookie, account, role, action, status, time_mode, custom_time
|
|
| 144 |
"account": account, "role": role, "action": action, "status": status
|
| 145 |
}
|
| 146 |
|
| 147 |
-
# 🌟 保持:整合外部日誌服務
|
| 148 |
try:
|
| 149 |
logger_service.push_new_log(new_log)
|
| 150 |
-
except
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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
|
| 170 |
pass
|
| 171 |
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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,
|
| 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,
|
| 185 |
-
return gr.update(visible=True), gr.update(visible=False), gr.update(), msg, gr.update(),
|
| 186 |
|
| 187 |
if account not in USER_DB:
|
| 188 |
-
|
| 189 |
-
return fail_response("帳號不存在!",
|
| 190 |
|
| 191 |
role, name = USER_DB[account][1], USER_DB[account][2]
|
| 192 |
-
|
| 193 |
if captcha.upper() != valid_captcha:
|
| 194 |
-
|
| 195 |
-
return fail_response("驗證碼錯誤!",
|
| 196 |
|
| 197 |
if USER_DB[account][0] == password:
|
| 198 |
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
|
| 199 |
-
|
| 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,
|
| 202 |
|
| 203 |
-
|
| 204 |
-
return fail_response("密碼錯誤!",
|
| 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 |
-
|
| 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,
|
| 213 |
|
| 214 |
def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
|
| 215 |
role = USER_DB.get(account, ["", "Unknown"])[1]
|
| 216 |
-
|
| 217 |
-
return gr.update(visible=True), gr.update(visible=False), "", "", "", "",
|
| 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]
|
|
@@ -235,9 +453,81 @@ custom_css = """
|
|
| 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="模擬校園入口與資安防護系統"
|
|
|
|
|
|
|
|
|
|
| 241 |
with gr.Row():
|
| 242 |
with gr.Column(scale=1, elem_classes="mock-panel"):
|
| 243 |
gr.Markdown("### ⚙️ 環境變數模擬器")
|
|
@@ -291,6 +581,18 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
|
|
| 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():
|
|
@@ -300,6 +602,20 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
|
|
| 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 |
|
|
@@ -334,31 +650,33 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
|
|
| 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 |
-
|
| 356 |
-
|
|
|
|
| 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 = [
|
|
@@ -375,7 +693,7 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
|
|
| 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 = [
|
|
@@ -396,8 +714,8 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
|
|
| 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()
|
|
|
|
| 4 |
import joblib
|
| 5 |
from sklearn.cluster import HDBSCAN
|
| 6 |
import numpy as np
|
| 7 |
+
import logger_service
|
| 8 |
+
import os
|
| 9 |
+
import urllib.request
|
| 10 |
+
import matplotlib.pyplot as plt
|
| 11 |
+
import seaborn as sns
|
| 12 |
+
import matplotlib.font_manager as fm
|
| 13 |
+
|
| 14 |
+
# --- 🌟 解決 Matplotlib 中文顯示問題 ---
|
| 15 |
+
# 啟動時自動下載並套用 SimHei (黑體) 字型,避免圖表出現方塊豆腐字
|
| 16 |
+
font_path = 'SimHei.ttf'
|
| 17 |
+
if not os.path.exists(font_path):
|
| 18 |
+
try:
|
| 19 |
+
print("📥 正在下載中文字型...")
|
| 20 |
+
urllib.request.urlretrieve('https://github.com/StellarCN/scp_zh/raw/master/fonts/SimHei.ttf', font_path)
|
| 21 |
+
except Exception as e:
|
| 22 |
+
print(f"⚠️ 字型下載失敗: {e}")
|
| 23 |
+
|
| 24 |
+
if os.path.exists(font_path):
|
| 25 |
+
fm.fontManager.addfont(font_path)
|
| 26 |
+
plt.rc('font', family='SimHei')
|
| 27 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 28 |
+
|
| 29 |
+
# --- 全域統計狀態變數 ---
|
| 30 |
+
global_stats = {
|
| 31 |
+
"total_count": 0,
|
| 32 |
+
"abnormal_count": 0,
|
| 33 |
+
"start_time": datetime.datetime.now()
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
# --- 13 個系統按鈕的對應字典 ---
|
| 37 |
+
ACTION_MAP = {
|
| 38 |
+
"教務系統: 期末網路教學評量": "click_eval_system",
|
| 39 |
+
"教務系統: 期末網路預選系統": "click_pre_select_system",
|
| 40 |
+
"教務系統: 開學後加退選系統": "click_add_drop_system",
|
| 41 |
+
"教務系統: 北科i學園PLUS": "click_istudy",
|
| 42 |
+
"教務系統: 學生證掛失及補發系統": "click_card_loss",
|
| 43 |
+
"教務系統: 課程系統": "click_course_system",
|
| 44 |
+
"教務系統: 學業成績查詢系統": "click_score_query",
|
| 45 |
+
"學務系統: 學生請假系統": "click_leave_system",
|
| 46 |
+
"學務系統: 學生宿舍登錄(抽籤)系統": "click_dorm_system",
|
| 47 |
+
"學務系統: 獎助學金申請系統": "click_scholarship",
|
| 48 |
+
"資訊服務: 網路郵局 WebMail": "click_webmail",
|
| 49 |
+
"資訊服務: 北科軟體雲": "click_vdesk",
|
| 50 |
+
"惡意測試: 嘗試偷改成績 (SQL Injection)": "malicious_sql_injection"
|
| 51 |
+
}
|
| 52 |
|
|
|
|
| 53 |
log_history = []
|
| 54 |
+
initial_df_abn = pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"])
|
| 55 |
|
| 56 |
# --- 1. 載入模型與歷史資料 ---
|
| 57 |
try:
|
| 58 |
le_action = joblib.load('le_action.pkl')
|
| 59 |
le_ip = joblib.load('le_ip.pkl')
|
| 60 |
|
| 61 |
+
# 🌟 啟動時一次性載入歷史資料,供後續繪圖快速讀取
|
| 62 |
baseline_df = pd.read_csv('baseline_logs.csv')
|
| 63 |
baseline_df['timestamp'] = pd.to_datetime(baseline_df['timestamp'])
|
| 64 |
|
| 65 |
+
global_stats["total_count"] = len(baseline_df)
|
| 66 |
+
global_stats["abnormal_count"] = baseline_df[baseline_df['status'].str.contains("異常", na=False)].shape[0]
|
| 67 |
+
print(f"📊 統計初始化:總監測量 {global_stats['total_count']}, 異常數 {global_stats['abnormal_count']}")
|
| 68 |
+
|
| 69 |
log_history = baseline_df.sort_values(by='timestamp', ascending=False).head(20).to_dict('records')
|
| 70 |
|
| 71 |
+
initial_abnormal_logs = [log for log in log_history if "異常" in str(log.get("status", ""))]
|
| 72 |
+
if initial_abnormal_logs:
|
| 73 |
+
initial_df_abn = pd.DataFrame(initial_abnormal_logs)
|
| 74 |
+
|
| 75 |
baseline_df['month'] = baseline_df['timestamp'].dt.month
|
| 76 |
baseline_df['day'] = baseline_df['timestamp'].dt.day
|
| 77 |
baseline_df['hour'] = baseline_df['timestamp'].dt.hour
|
|
|
|
| 88 |
print("✅ AI 模組與歷史基準資料載入成功!")
|
| 89 |
except Exception as e:
|
| 90 |
AI_READY = False
|
| 91 |
+
global_stats["total_count"] = 0
|
| 92 |
+
global_stats["abnormal_count"] = 0
|
| 93 |
print(f"⚠️ AI 模組載入失敗,請確認檔案。錯誤: {e}")
|
| 94 |
|
| 95 |
USER_DB = {
|
|
|
|
| 97 |
"admin": ["1234", "管理", "管理員"],
|
| 98 |
}
|
| 99 |
|
| 100 |
+
# --- 儀表板 HTML ---
|
| 101 |
+
def get_dashboard_html():
|
| 102 |
+
total = global_stats["total_count"]
|
| 103 |
+
abnormal = global_stats["abnormal_count"]
|
| 104 |
+
rate = (abnormal / total * 100) if total > 0 else 0
|
| 105 |
+
|
| 106 |
+
if not AI_READY:
|
| 107 |
+
status_icon, status_text, status_color, status_desc = "🔴", "系統錯誤", "#ef4444", "AI 模型載入失敗,防護停用中。"
|
| 108 |
+
elif rate > 10:
|
| 109 |
+
status_icon, status_text, status_color, status_desc = "🟡", "高風險警告", "#f59e0b", "近期異常行為頻發,請立刻檢查 Log。"
|
| 110 |
+
else:
|
| 111 |
+
status_icon, status_text, status_color, status_desc = "🟢", "正常執行", "#10b981", "HDBSCAN 與 Agent 服務運作正常。"
|
| 112 |
+
|
| 113 |
+
return f"""
|
| 114 |
+
<div class="stat-dashboard">
|
| 115 |
+
<div class="stat-card card-total">
|
| 116 |
+
<div class="card-icon">📊</div>
|
| 117 |
+
<div class="card-content">
|
| 118 |
+
<div class="card-label">總監測量 (預警流量)</div>
|
| 119 |
+
<div class="card-value">{total:,} <span class="card-unit">筆 Log</span></div>
|
| 120 |
+
<div class="card-sub-text">自系統啟動起統計 (+今日即時)</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
<div class="stat-card card-anomaly">
|
| 124 |
+
<div class="card-icon">⚠️</div>
|
| 125 |
+
<div class="card-content">
|
| 126 |
+
<div class="card-label">異常率 (平均數值)</div>
|
| 127 |
+
<div class="card-value-container">
|
| 128 |
+
<div class="card-value" style="color: #ef4444;">{rate:.2f} %</div>
|
| 129 |
+
<div class="card-trend trend-up">↑即時</div>
|
| 130 |
+
</div>
|
| 131 |
+
<div class="card-sub-text">共 {abnormal:,} 筆監測到異常行為</div>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
<div class="stat-card card-status" style="border-top-color: {status_color};">
|
| 135 |
+
<div class="card-icon">{status_icon}</div>
|
| 136 |
+
<div class="card-content">
|
| 137 |
+
<div class="card-label">系統狀態</div>
|
| 138 |
+
<div class="card-value" style="font-size: 24px; color: {status_color};">{status_text}</div>
|
| 139 |
+
<div class="card-sub-text">{status_desc}</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
# --- 🌟 完美整合 Seaborn 的圖表繪製函式 ---
|
| 146 |
+
def get_plot_data(selected_key):
|
| 147 |
+
if not selected_key:
|
| 148 |
+
return None
|
| 149 |
+
|
| 150 |
+
action_code = ACTION_MAP.get(selected_key)
|
| 151 |
+
try:
|
| 152 |
+
plt.close('all') # 釋放舊記憶體,確保系統不卡頓
|
| 153 |
+
|
| 154 |
+
# 1. 直接從記憶體中提取歷史基準資料 (完全免除讀檔延遲)
|
| 155 |
+
df_hist = baseline_df[baseline_df['action'] == action_code].copy()
|
| 156 |
+
|
| 157 |
+
# 2. 從 log_history 抓出「剛產生的即時紀錄」
|
| 158 |
+
df_live = pd.DataFrame(log_history)
|
| 159 |
+
if not df_live.empty:
|
| 160 |
+
df_live['timestamp'] = pd.to_datetime(df_live['timestamp'])
|
| 161 |
+
df_live = df_live[df_live['action'] == action_code].copy()
|
| 162 |
+
|
| 163 |
+
# 3. 完美合併:將歷史訓練資料與即時資料無縫接合
|
| 164 |
+
if not df_live.empty:
|
| 165 |
+
df_combined = pd.concat([df_hist, df_live], ignore_index=True)
|
| 166 |
+
# 使用 timestamp 移除可能重複計算的資料
|
| 167 |
+
df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action'])
|
| 168 |
+
else:
|
| 169 |
+
df_combined = df_hist.copy()
|
| 170 |
+
|
| 171 |
+
if df_combined.empty:
|
| 172 |
+
fig, ax = plt.subplots(figsize=(12, 6))
|
| 173 |
+
if os.path.exists(font_path): plt.rc('font', family='SimHei')
|
| 174 |
+
ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
|
| 175 |
+
return fig
|
| 176 |
+
|
| 177 |
+
# 4. 計算 X 軸(月份) 與 Y 軸(時間小數)
|
| 178 |
+
df_combined['month'] = df_combined['timestamp'].dt.month
|
| 179 |
+
df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0
|
| 180 |
+
|
| 181 |
+
# 5. 精準分類:區分「綠色正常」與「紅色異常」
|
| 182 |
+
is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False)
|
| 183 |
+
df_normal = df_combined[~is_abnormal]
|
| 184 |
+
df_abnormal = df_combined[is_abnormal]
|
| 185 |
+
|
| 186 |
+
# 開始繪圖
|
| 187 |
+
fig, ax = plt.subplots(figsize=(12, 6))
|
| 188 |
+
sns.set_theme(style="whitegrid")
|
| 189 |
+
if os.path.exists(font_path): plt.rc('font', family='SimHei')
|
| 190 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 191 |
+
|
| 192 |
+
has_data = False
|
| 193 |
+
months_order = list(range(1, 13)) # 確保 1 到 12 月依序排列
|
| 194 |
+
|
| 195 |
+
# 🟢 繪製正常紀錄 (綠色點)
|
| 196 |
+
if not df_normal.empty:
|
| 197 |
+
has_data = True
|
| 198 |
+
sns.stripplot(
|
| 199 |
+
data=df_normal, x='month', y='time_of_day',
|
| 200 |
+
jitter=0.3, alpha=0.4, size=5, color='#10b981',
|
| 201 |
+
order=months_order, ax=ax, label='正常資料'
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
# 🔴 繪製異常紀錄 (紅色大叉叉)
|
| 205 |
+
if not df_abnormal.empty:
|
| 206 |
+
has_data = True
|
| 207 |
+
sns.stripplot(
|
| 208 |
+
data=df_abnormal, x='month', y='time_of_day',
|
| 209 |
+
jitter=0.3, alpha=0.9, size=9, color='#ef4444', marker='X',
|
| 210 |
+
order=months_order, ax=ax, label='異常資料'
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
if not has_data:
|
| 214 |
+
ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
|
| 215 |
+
return fig
|
| 216 |
+
|
| 217 |
+
# 🌟 已經將標題 ax.set_title 移除
|
| 218 |
+
ax.set_xlabel('月份 (1 ~ 12月)', fontsize=12)
|
| 219 |
+
ax.set_ylabel('時間 (24小時制)', fontsize=12)
|
| 220 |
+
|
| 221 |
+
# 設定 Y 軸為 0~24 點並倒轉 (早上在上面,半夜在下面)
|
| 222 |
+
ax.set_yticks(np.arange(0, 25, 2))
|
| 223 |
+
ax.set_ylim(24.5, -0.5)
|
| 224 |
+
|
| 225 |
+
# 整理圖例 (避免 Seaborn 重複生成標籤)
|
| 226 |
+
handles, labels = ax.get_legend_handles_labels()
|
| 227 |
+
by_label = dict(zip(labels, handles))
|
| 228 |
+
if by_label:
|
| 229 |
+
ax.legend(by_label.values(), by_label.keys(), loc='upper right', bbox_to_anchor=(1.15, 1))
|
| 230 |
+
|
| 231 |
+
plt.tight_layout()
|
| 232 |
+
return fig
|
| 233 |
+
except Exception as e:
|
| 234 |
+
print(f"繪圖失敗: {e}")
|
| 235 |
+
return None
|
| 236 |
+
|
| 237 |
+
# --- 定時器觸發同步更新 ---
|
| 238 |
+
def timer_update(selected_key):
|
| 239 |
+
return get_dashboard_html(), get_plot_data(selected_key)
|
| 240 |
+
|
| 241 |
+
# --- Groq AI Agent 呼叫函式 ---
|
| 242 |
+
def explain_abnormal_log(log_text):
|
| 243 |
+
if not log_text or log_text.strip() == "":
|
| 244 |
+
return "⚠️ 請先提供異常 Log 資訊以進行分析。"
|
| 245 |
+
try:
|
| 246 |
+
from groq import Groq
|
| 247 |
+
except ImportError:
|
| 248 |
+
return "⚠️ 未安裝 groq 套件。請在終端機執行 `pip install groq`。"
|
| 249 |
+
|
| 250 |
+
groq_api_key = os.environ.get("GROQ_API_KEY", "")
|
| 251 |
+
if not groq_api_key:
|
| 252 |
+
return "⚠️ 未設定 GROQ_API_KEY。請設定環境變數或直接在程式碼中填寫您的 API Key。"
|
| 253 |
+
|
| 254 |
+
try:
|
| 255 |
+
client = Groq(api_key=groq_api_key)
|
| 256 |
+
response = client.chat.completions.create(
|
| 257 |
+
messages=[
|
| 258 |
+
{
|
| 259 |
+
"role": "system",
|
| 260 |
+
"content": "你是一位專業的資安分析師。請根據使用者提供的系統 Log,用繁體中文簡單解釋為什麼這筆 Log 會被標記為異常,並指出可能的資安風險(例如:撞庫攻擊、自動化腳本、半夜作息異常等)。請保持說明在 100 字以內,精簡扼要。"
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"role": "user",
|
| 264 |
+
"content": f"請分析這筆異常 Log:\n{log_text}"
|
| 265 |
+
}
|
| 266 |
+
],
|
| 267 |
+
model="llama-3.1-8b-instant",
|
| 268 |
+
temperature=0.5,
|
| 269 |
+
max_tokens=300
|
| 270 |
+
)
|
| 271 |
+
return response.choices[0].message.content
|
| 272 |
+
except Exception as e:
|
| 273 |
+
return f"⚠️ 呼叫 API 發生錯誤: {str(e)}"
|
| 274 |
+
|
| 275 |
# --- 2. 核心邏輯與真實 HDBSCAN 異常偵測 ---
|
| 276 |
def check_anomaly(ip, action, log_time, overall_w, time_w, ip_w, dev_w):
|
| 277 |
if not AI_READY: return "success"
|
| 278 |
if action in ["login_attempt", "logout"]: return "success"
|
| 279 |
|
| 280 |
try:
|
| 281 |
+
ow, tw, iw, dw = float(overall_w), float(time_w), float(ip_w), float(dev_w)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
clean_ip = str(ip).split(" ")[0]
|
| 283 |
ip_code = float(le_ip.transform([clean_ip])[0])
|
| 284 |
action_code = float(le_action.transform([action])[0])
|
|
|
|
| 306 |
|
| 307 |
new_data_point = np.array([[v_month, v_day, v_hour, v_ip, v_action]], dtype=float)
|
| 308 |
|
|
|
|
|
|
|
| 309 |
same_action_mask = (baseline_X[:, 4] == action_code)
|
| 310 |
relevant_history = scaled_baseline[same_action_mask]
|
| 311 |
|
|
|
|
| 312 |
other_history = scaled_baseline[~same_action_mask]
|
| 313 |
if len(other_history) > 800:
|
|
|
|
| 314 |
np.random.seed(int(current_day + current_hour))
|
| 315 |
indices = np.random.choice(len(other_history), 800, replace=False)
|
| 316 |
other_history = other_history[indices]
|
| 317 |
|
|
|
|
| 318 |
fit_X = np.vstack((other_history, relevant_history, new_data_point))
|
|
|
|
| 319 |
strictness = max(2, int(2 + (ow / 100.0) * 4))
|
| 320 |
|
| 321 |
+
model = HDBSCAN(min_cluster_size=strictness, min_samples=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
clusters = model.fit_predict(fit_X)
|
| 323 |
|
| 324 |
if clusters[-1] == -1:
|
| 325 |
return "⚠️異常_作息或行為不符"
|
| 326 |
|
|
|
|
| 327 |
if np.any(same_action_mask):
|
| 328 |
history_points = scaled_baseline[same_action_mask]
|
| 329 |
distances = np.linalg.norm(history_points - new_data_point[0], axis=1)
|
| 330 |
min_distance = np.min(distances)
|
|
|
|
| 331 |
max_tolerance_distance = time_multiplier * 15.0 + 1.0
|
|
|
|
| 332 |
if min_distance > max_tolerance_distance:
|
| 333 |
return "⚠️異常_作息或行為不符"
|
| 334 |
|
|
|
|
| 355 |
"account": account, "role": role, "action": action, "status": status
|
| 356 |
}
|
| 357 |
|
|
|
|
| 358 |
try:
|
| 359 |
logger_service.push_new_log(new_log)
|
| 360 |
+
except:
|
| 361 |
+
pass
|
| 362 |
+
|
| 363 |
+
global_stats["total_count"] += 1
|
| 364 |
+
if "異常" in str(status):
|
| 365 |
+
global_stats["abnormal_count"] += 1
|
| 366 |
|
| 367 |
log_history.insert(0, new_log)
|
| 368 |
|
| 369 |
if status == "success" and AI_READY:
|
| 370 |
try:
|
| 371 |
pd.DataFrame([new_log]).to_csv('baseline_logs.csv', mode='a', header=False, index=False, encoding='utf-8-sig')
|
|
|
|
| 372 |
current_time = pd.to_datetime(str(log_time))
|
| 373 |
clean_ip = str(ip).split(" ")[0]
|
| 374 |
new_row = np.array([[
|
| 375 |
+
float(current_time.month), float(current_time.day), float(current_time.hour),
|
| 376 |
+
float(le_ip.transform([clean_ip])[0]), float(le_action.transform([action])[0])
|
|
|
|
|
|
|
|
|
|
| 377 |
]], dtype=float)
|
| 378 |
baseline_X = np.vstack((baseline_X, new_row))
|
| 379 |
+
except:
|
| 380 |
pass
|
| 381 |
|
| 382 |
+
df_all = pd.DataFrame(log_history)
|
| 383 |
+
abnormal_logs = [log for log in log_history if "異常" in str(log.get("status", ""))]
|
| 384 |
+
df_abnormal = pd.DataFrame(abnormal_logs) if abnormal_logs else pd.DataFrame(columns=df_all.columns)
|
| 385 |
+
|
| 386 |
+
if "異常" in str(status):
|
| 387 |
+
latest_str = f"時間: {log_time} | IP: {ip} | 帳號: {account} | 動作: {action} | 狀態: {status}"
|
| 388 |
+
input_update = gr.update(value=latest_str)
|
| 389 |
+
else:
|
| 390 |
+
input_update = gr.update()
|
| 391 |
+
|
| 392 |
+
return df_all, df_abnormal, input_update
|
| 393 |
|
|
|
|
| 394 |
def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
|
| 395 |
role, name = USER_DB[target_account][1], USER_DB[target_account][2]
|
| 396 |
+
df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 397 |
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)"
|
| 398 |
target_tab = "student_tab" if role == "學生" else "admin_tab"
|
| 399 |
+
return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd, target_account
|
| 400 |
|
| 401 |
def process_login(ip, cookie, account, password, captcha, time_mode, custom_time, ow, tw, iw, dw):
|
| 402 |
valid_captcha = "FEIK"
|
| 403 |
+
def fail_response(msg, df_all, df_abn, inp_upd):
|
| 404 |
+
return gr.update(visible=True), gr.update(visible=False), gr.update(), msg, gr.update(), df_all, df_abn, inp_upd
|
| 405 |
|
| 406 |
if account not in USER_DB:
|
| 407 |
+
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)
|
| 408 |
+
return fail_response("帳號不存在!", df_all, df_abn, inp_upd)
|
| 409 |
|
| 410 |
role, name = USER_DB[account][1], USER_DB[account][2]
|
|
|
|
| 411 |
if captcha.upper() != valid_captcha:
|
| 412 |
+
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_captcha", time_mode, custom_time, ow, tw, iw, dw)
|
| 413 |
+
return fail_response("驗證碼錯誤!", df_all, df_abn, inp_upd)
|
| 414 |
|
| 415 |
if USER_DB[account][0] == password:
|
| 416 |
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
|
| 417 |
+
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 418 |
target_tab = "student_tab" if role == "學生" else "admin_tab"
|
| 419 |
+
return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd
|
| 420 |
|
| 421 |
+
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time, ow, tw, iw, dw)
|
| 422 |
+
return fail_response("密碼錯誤!", df_all, df_abn, inp_upd)
|
| 423 |
|
| 424 |
def toggle_role(current_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
|
| 425 |
target_account = "student" if current_account == "admin" else "admin"
|
| 426 |
role, name = USER_DB[target_account][1], USER_DB[target_account][2]
|
| 427 |
+
df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 428 |
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡單鍵切換模式)"
|
| 429 |
target_tab = "student_tab" if role == "學生" else "admin_tab"
|
| 430 |
+
return gr.update(selected=target_tab), welcome_msg, df_all, df_abn, inp_upd, target_account
|
| 431 |
|
| 432 |
def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
|
| 433 |
role = USER_DB.get(account, ["", "Unknown"])[1]
|
| 434 |
+
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 435 |
+
return gr.update(visible=True), gr.update(visible=False), "", "", "", "", df_all, df_abn, inp_upd
|
| 436 |
|
| 437 |
def simulate_click(ip, cookie, account, button_name, time_mode, custom_time, ow, tw, iw, dw):
|
| 438 |
role = USER_DB.get(account, ["", "Unknown"])[1]
|
|
|
|
| 453 |
.weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
|
| 454 |
.headless-tabs > div:first-child { display: none !important; }
|
| 455 |
.headless-tabs { border: none !important; background: transparent !important; }
|
| 456 |
+
|
| 457 |
+
/* 🌟 新增:統計儀表板 CSS,嘗試重現圖片樣式 (已加入深色模式強制白底黑字覆蓋) */
|
| 458 |
+
.stat-dashboard {
|
| 459 |
+
display: flex;
|
| 460 |
+
gap: 15px;
|
| 461 |
+
margin-bottom: 20px;
|
| 462 |
+
justify-content: space-between;
|
| 463 |
+
}
|
| 464 |
+
.stat-card {
|
| 465 |
+
flex: 1;
|
| 466 |
+
background-color: #ffffff !important; /* 強制卡片底色為白色 */
|
| 467 |
+
padding: 15px;
|
| 468 |
+
border-radius: 10px;
|
| 469 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
| 470 |
+
display: flex;
|
| 471 |
+
align-items: center;
|
| 472 |
+
gap: 15px;
|
| 473 |
+
border-top: 4px solid transparent; /* 用於狀態卡的頂部顏色 */
|
| 474 |
+
}
|
| 475 |
+
.card-total { border-left: 4px solid #10b981; } /* Green left border */
|
| 476 |
+
.card-anomaly { border-left: 4px solid #ef4444; } /* Red left border */
|
| 477 |
+
.card-status { border-left: 4px solid #3b82f6; } /* Blue left border */
|
| 478 |
+
|
| 479 |
+
.card-icon {
|
| 480 |
+
font-size: 30px;
|
| 481 |
+
width: 40px;
|
| 482 |
+
text-align: center;
|
| 483 |
+
}
|
| 484 |
+
.card-content {
|
| 485 |
+
flex: 1;
|
| 486 |
+
}
|
| 487 |
+
.card-label {
|
| 488 |
+
color: #374151 !important; /* 強制標題文字為深灰色 */
|
| 489 |
+
font-size: 13px;
|
| 490 |
+
margin-bottom: 2px;
|
| 491 |
+
font-weight: bold !important;
|
| 492 |
+
}
|
| 493 |
+
.card-value-container {
|
| 494 |
+
display: flex;
|
| 495 |
+
align-items: baseline;
|
| 496 |
+
gap: 8px;
|
| 497 |
+
}
|
| 498 |
+
.card-value {
|
| 499 |
+
font-size: 24px;
|
| 500 |
+
font-weight: bold;
|
| 501 |
+
}
|
| 502 |
+
/* 針對「總監測量」的數字強制設為黑色,不影響異常率與狀態的專屬顏色 */
|
| 503 |
+
.card-total .card-value {
|
| 504 |
+
color: #000000 !important;
|
| 505 |
+
}
|
| 506 |
+
.card-unit {
|
| 507 |
+
font-size: 14px;
|
| 508 |
+
color: #4b5563 !important; /* 強制單位文字為深灰色 */
|
| 509 |
+
font-weight: normal;
|
| 510 |
+
}
|
| 511 |
+
.card-trend {
|
| 512 |
+
font-size: 12px;
|
| 513 |
+
padding: 2px 6px;
|
| 514 |
+
border-radius: 4px;
|
| 515 |
+
}
|
| 516 |
+
.trend-up {
|
| 517 |
+
background-color: #fee2e2 !important;
|
| 518 |
+
color: #ef4444 !important;
|
| 519 |
+
}
|
| 520 |
+
.card-sub-text {
|
| 521 |
+
color: #6b7280 !important; /* 強制副標題說明文字為中灰色 */
|
| 522 |
+
font-size: 11px;
|
| 523 |
+
margin-top: 2px;
|
| 524 |
+
}
|
| 525 |
"""
|
| 526 |
|
| 527 |
+
with gr.Blocks(title="模擬校園入口與資安防護系統") as demo:
|
| 528 |
+
|
| 529 |
+
dashboard_timer = gr.Timer(value=60)
|
| 530 |
+
|
| 531 |
with gr.Row():
|
| 532 |
with gr.Column(scale=1, elem_classes="mock-panel"):
|
| 533 |
gr.Markdown("### ⚙️ 環境變數模擬器")
|
|
|
|
| 581 |
btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
|
| 582 |
|
| 583 |
with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
|
| 584 |
+
|
| 585 |
+
stat_dashboard_display = gr.HTML(value=get_dashboard_html())
|
| 586 |
+
|
| 587 |
+
with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"):
|
| 588 |
+
gr.Markdown("<span style='color:gray; font-size:0.9em;'>選擇下方系統以檢視該系統在歷史與即時紀錄中的時間分佈狀況。圖表每分鐘會自動同步更新,歷史紀錄為綠點,即時攔截之異常為紅叉。</span>")
|
| 589 |
+
action_dropdown = gr.Dropdown(
|
| 590 |
+
choices=list(ACTION_MAP.keys()),
|
| 591 |
+
label="選擇要檢視的系統資料分佈",
|
| 592 |
+
value=None
|
| 593 |
+
)
|
| 594 |
+
action_plot = gr.Plot(show_label=False)
|
| 595 |
+
|
| 596 |
with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
|
| 597 |
gr.Markdown("<span style='color:gray; font-size:0.9em;'>手動輸入 0~100 的數值。提高強度會增加誤報率,但能更有效攔截新型攻擊。</span>")
|
| 598 |
with gr.Row():
|
|
|
|
| 602 |
ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100)
|
| 603 |
device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100)
|
| 604 |
|
| 605 |
+
with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"):
|
| 606 |
+
gr.Markdown("<span style='color:gray; font-size:0.9em;'>顯示所有被判定為異常的 Log。點擊分析可讓 AI 代為判讀潛在風險。</span>")
|
| 607 |
+
abnormal_log_display = gr.Dataframe(
|
| 608 |
+
value=initial_df_abn,
|
| 609 |
+
headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"],
|
| 610 |
+
datatype=["str", "str", "str", "str", "str", "str", "str"],
|
| 611 |
+
interactive=False, wrap=True
|
| 612 |
+
)
|
| 613 |
+
with gr.Row():
|
| 614 |
+
abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入,您也可以手動修改...", scale=4)
|
| 615 |
+
btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1)
|
| 616 |
+
|
| 617 |
+
ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False, placeholder="等待分析中...")
|
| 618 |
+
|
| 619 |
with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"):
|
| 620 |
gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>")
|
| 621 |
|
|
|
|
| 650 |
t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
|
| 651 |
|
| 652 |
# --- 綁定事件 ---
|
| 653 |
+
|
| 654 |
+
demo.load(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
|
| 655 |
+
dashboard_timer.tick(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
|
| 656 |
+
|
| 657 |
+
action_dropdown.change(fn=get_plot_data, inputs=[action_dropdown], outputs=[action_plot])
|
| 658 |
+
|
| 659 |
time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
|
| 660 |
+
btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
|
| 661 |
|
| 662 |
weight_inputs = [overall_weight, time_weight, ip_weight, device_weight]
|
| 663 |
+
update_outputs = [login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display, abnormal_log_display, abnormal_log_input]
|
| 664 |
|
| 665 |
login_btn.click(
|
| 666 |
+
fn=process_login, inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input] + weight_inputs, outputs=update_outputs
|
|
|
|
|
|
|
| 667 |
)
|
|
|
|
| 668 |
logout_btn.click(
|
| 669 |
+
fn=logout, inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs, outputs=[login_view, dashboard_view, acc_input, pwd_input, captcha_input, login_msg, log_display, abnormal_log_display, abnormal_log_input]
|
|
|
|
|
|
|
| 670 |
)
|
| 671 |
|
| 672 |
fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs
|
| 673 |
+
fast_outputs = update_outputs + [acc_input]
|
| 674 |
+
|
| 675 |
+
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)
|
| 676 |
+
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)
|
| 677 |
|
| 678 |
btn_toggle_role.click(
|
| 679 |
+
fn=toggle_role, inputs=[acc_input, mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs, outputs=[role_tabs, welcome_text, log_display, abnormal_log_display, abnormal_log_input, acc_input]
|
|
|
|
|
|
|
| 680 |
)
|
| 681 |
|
| 682 |
action_mapping = [
|
|
|
|
| 693 |
btn.click(
|
| 694 |
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),
|
| 695 |
inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
|
| 696 |
+
outputs=[log_display, abnormal_log_display, abnormal_log_input]
|
| 697 |
)
|
| 698 |
|
| 699 |
test_mapping = [
|
|
|
|
| 714 |
btn.click(
|
| 715 |
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),
|
| 716 |
inputs=[mock_ip, mock_cookie, acc_input, overall_weight, time_weight, ip_weight, device_weight],
|
| 717 |
+
outputs=[log_display, abnormal_log_display, abnormal_log_input]
|
| 718 |
)
|
| 719 |
|
| 720 |
if __name__ == "__main__":
|
| 721 |
+
demo.launch(css=custom_css)
|