information system interface update
Browse files
app.py
CHANGED
|
@@ -12,7 +12,6 @@ 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:
|
|
@@ -58,7 +57,6 @@ 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 |
|
|
@@ -142,28 +140,22 @@ def get_dashboard_html():
|
|
| 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()
|
|
@@ -174,25 +166,21 @@ def get_plot_data(selected_key):
|
|
| 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))
|
| 194 |
|
| 195 |
-
# 🟢 繪製正常紀錄 (綠色點)
|
| 196 |
if not df_normal.empty:
|
| 197 |
has_data = True
|
| 198 |
sns.stripplot(
|
|
@@ -201,7 +189,6 @@ def get_plot_data(selected_key):
|
|
| 201 |
order=months_order, ax=ax, label='正常資料'
|
| 202 |
)
|
| 203 |
|
| 204 |
-
# 🔴 繪製異常紀錄 (紅色大叉叉)
|
| 205 |
if not df_abnormal.empty:
|
| 206 |
has_data = True
|
| 207 |
sns.stripplot(
|
|
@@ -214,15 +201,11 @@ def get_plot_data(selected_key):
|
|
| 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:
|
|
@@ -234,11 +217,9 @@ def get_plot_data(selected_key):
|
|
| 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 資訊以進行分析。"
|
|
@@ -391,17 +372,19 @@ def record_log(ip, cookie, account, role, action, status, time_mode, custom_time
|
|
| 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 |
-
|
|
|
|
| 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(
|
| 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)
|
|
@@ -416,7 +399,8 @@ def process_login(ip, cookie, account, password, captcha, time_mode, custom_time
|
|
| 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 |
-
|
|
|
|
| 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)
|
|
@@ -432,7 +416,8 @@ def toggle_role(current_account, ip, cookie, time_mode, custom_time, ow, tw, iw,
|
|
| 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 |
-
|
|
|
|
| 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,75 +438,21 @@ custom_css = """
|
|
| 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 |
-
|
| 458 |
-
.
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
}
|
| 464 |
-
.
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 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:
|
|
@@ -541,132 +472,140 @@ with gr.Blocks(title="模擬校園入口與資安防護系統") as demo:
|
|
| 541 |
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)
|
| 542 |
|
| 543 |
with gr.Column(scale=2):
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
with gr.Row(): acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="student or admin")
|
| 548 |
-
with gr.Row(): pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="password : 1234")
|
| 549 |
-
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>")
|
| 550 |
-
with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)")
|
| 551 |
-
login_btn = gr.Button("登入 Login", variant="primary")
|
| 552 |
-
|
| 553 |
-
gr.Markdown("---")
|
| 554 |
-
gr.Markdown("<div style='text-align:center; color:gray; font-size: 0.9em;'>🛠️ 開發與測試專用捷徑</div>")
|
| 555 |
-
with gr.Row():
|
| 556 |
-
btn_fast_student = gr.Button("🚀 快速登入 (學生)", variant="secondary")
|
| 557 |
-
btn_fast_admin = gr.Button("🚀 快速登入 (管理員)", variant="secondary")
|
| 558 |
-
|
| 559 |
-
with gr.Column(visible=False) as dashboard_view:
|
| 560 |
-
with gr.Row():
|
| 561 |
-
gr.Markdown("### 資訊系統")
|
| 562 |
-
welcome_text = gr.Markdown(value="", elem_classes="text-right")
|
| 563 |
-
logout_btn = gr.Button("登出", size="sm")
|
| 564 |
-
|
| 565 |
-
with gr.Row():
|
| 566 |
-
btn_toggle_role = gr.Button("🔄 Student ↔ Admin", size="sm", variant="secondary")
|
| 567 |
-
gr.Markdown("---")
|
| 568 |
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
with gr.
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
with gr.
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
with gr.Row(): btn_webmail = gr.Button("▶ 網路郵局 WebMail", size="sm"); btn_vdesk = gr.Button("▶ 北科軟體雲", size="sm")
|
| 580 |
-
with gr.Accordion("▼ 惡意操作測試區", open=True):
|
| 581 |
-
btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
|
| 582 |
-
|
| 583 |
-
with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
|
| 584 |
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
with gr.
|
| 588 |
-
gr.
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
gr.
|
| 598 |
-
with gr.Row():
|
| 599 |
-
overall_weight = gr.Number(label="總體防護強度 (%)", value=75, minimum=0, maximum=100)
|
| 600 |
-
time_weight = gr.Number(label="時間特徵權重 (%)", value=80, minimum=0, maximum=100)
|
| 601 |
-
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.
|
| 606 |
-
gr.
|
| 607 |
-
|
| 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.
|
| 620 |
-
gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>")
|
| 621 |
|
| 622 |
-
gr.
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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,
|
|
|
|
| 670 |
)
|
| 671 |
|
| 672 |
fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs
|
|
|
|
| 12 |
import matplotlib.font_manager as fm
|
| 13 |
|
| 14 |
# --- 🌟 解決 Matplotlib 中文顯示問題 ---
|
|
|
|
| 15 |
font_path = 'SimHei.ttf'
|
| 16 |
if not os.path.exists(font_path):
|
| 17 |
try:
|
|
|
|
| 57 |
le_action = joblib.load('le_action.pkl')
|
| 58 |
le_ip = joblib.load('le_ip.pkl')
|
| 59 |
|
|
|
|
| 60 |
baseline_df = pd.read_csv('baseline_logs.csv')
|
| 61 |
baseline_df['timestamp'] = pd.to_datetime(baseline_df['timestamp'])
|
| 62 |
|
|
|
|
| 140 |
</div>
|
| 141 |
"""
|
| 142 |
|
|
|
|
| 143 |
def get_plot_data(selected_key):
|
| 144 |
if not selected_key:
|
| 145 |
return None
|
| 146 |
|
| 147 |
action_code = ACTION_MAP.get(selected_key)
|
| 148 |
try:
|
| 149 |
+
plt.close('all')
|
|
|
|
|
|
|
| 150 |
df_hist = baseline_df[baseline_df['action'] == action_code].copy()
|
| 151 |
|
|
|
|
| 152 |
df_live = pd.DataFrame(log_history)
|
| 153 |
if not df_live.empty:
|
| 154 |
df_live['timestamp'] = pd.to_datetime(df_live['timestamp'])
|
| 155 |
df_live = df_live[df_live['action'] == action_code].copy()
|
| 156 |
|
|
|
|
| 157 |
if not df_live.empty:
|
| 158 |
df_combined = pd.concat([df_hist, df_live], ignore_index=True)
|
|
|
|
| 159 |
df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action'])
|
| 160 |
else:
|
| 161 |
df_combined = df_hist.copy()
|
|
|
|
| 166 |
ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
|
| 167 |
return fig
|
| 168 |
|
|
|
|
| 169 |
df_combined['month'] = df_combined['timestamp'].dt.month
|
| 170 |
df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0
|
| 171 |
|
|
|
|
| 172 |
is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False)
|
| 173 |
df_normal = df_combined[~is_abnormal]
|
| 174 |
df_abnormal = df_combined[is_abnormal]
|
| 175 |
|
|
|
|
| 176 |
fig, ax = plt.subplots(figsize=(12, 6))
|
| 177 |
sns.set_theme(style="whitegrid")
|
| 178 |
if os.path.exists(font_path): plt.rc('font', family='SimHei')
|
| 179 |
plt.rcParams['axes.unicode_minus'] = False
|
| 180 |
|
| 181 |
has_data = False
|
| 182 |
+
months_order = list(range(1, 13))
|
| 183 |
|
|
|
|
| 184 |
if not df_normal.empty:
|
| 185 |
has_data = True
|
| 186 |
sns.stripplot(
|
|
|
|
| 189 |
order=months_order, ax=ax, label='正常資料'
|
| 190 |
)
|
| 191 |
|
|
|
|
| 192 |
if not df_abnormal.empty:
|
| 193 |
has_data = True
|
| 194 |
sns.stripplot(
|
|
|
|
| 201 |
ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
|
| 202 |
return fig
|
| 203 |
|
|
|
|
| 204 |
ax.set_xlabel('月份 (1 ~ 12月)', fontsize=12)
|
| 205 |
ax.set_ylabel('時間 (24小時制)', fontsize=12)
|
|
|
|
|
|
|
| 206 |
ax.set_yticks(np.arange(0, 25, 2))
|
| 207 |
ax.set_ylim(24.5, -0.5)
|
| 208 |
|
|
|
|
| 209 |
handles, labels = ax.get_legend_handles_labels()
|
| 210 |
by_label = dict(zip(labels, handles))
|
| 211 |
if by_label:
|
|
|
|
| 217 |
print(f"繪圖失敗: {e}")
|
| 218 |
return None
|
| 219 |
|
|
|
|
| 220 |
def timer_update(selected_key):
|
| 221 |
return get_dashboard_html(), get_plot_data(selected_key)
|
| 222 |
|
|
|
|
| 223 |
def explain_abnormal_log(log_text):
|
| 224 |
if not log_text or log_text.strip() == "":
|
| 225 |
return "⚠️ 請先提供異常 Log 資訊以進行分析。"
|
|
|
|
| 372 |
|
| 373 |
return df_all, df_abnormal, input_update
|
| 374 |
|
| 375 |
+
# --- 🎯 路由更新邏輯:現在統一回傳 main_router 狀態 ---
|
| 376 |
def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
|
| 377 |
role, name = USER_DB[target_account][1], USER_DB[target_account][2]
|
| 378 |
df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 379 |
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)"
|
| 380 |
target_tab = "student_tab" if role == "學生" else "admin_tab"
|
| 381 |
+
# 🌟 修改:切換至 dashboard_route 而不再修改 visible
|
| 382 |
+
return gr.update(selected="dashboard_route"), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd, target_account
|
| 383 |
|
| 384 |
def process_login(ip, cookie, account, password, captcha, time_mode, custom_time, ow, tw, iw, dw):
|
| 385 |
valid_captcha = "FEIK"
|
| 386 |
def fail_response(msg, df_all, df_abn, inp_upd):
|
| 387 |
+
return gr.update(selected="login_route"), gr.update(), msg, gr.update(), df_all, df_abn, inp_upd
|
| 388 |
|
| 389 |
if account not in USER_DB:
|
| 390 |
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)
|
|
|
|
| 399 |
welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
|
| 400 |
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 401 |
target_tab = "student_tab" if role == "學生" else "admin_tab"
|
| 402 |
+
# 🌟 修改:登入成功切換路由
|
| 403 |
+
return gr.update(selected="dashboard_route"), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd
|
| 404 |
|
| 405 |
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time, ow, tw, iw, dw)
|
| 406 |
return fail_response("密碼錯誤!", df_all, df_abn, inp_upd)
|
|
|
|
| 416 |
def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
|
| 417 |
role = USER_DB.get(account, ["", "Unknown"])[1]
|
| 418 |
df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time, ow, tw, iw, dw)
|
| 419 |
+
# 🌟 修改:登出時切回 login_route 路由
|
| 420 |
+
return gr.update(selected="login_route"), "", "", "", "", df_all, df_abn, inp_upd
|
| 421 |
|
| 422 |
def simulate_click(ip, cookie, account, button_name, time_mode, custom_time, ow, tw, iw, dw):
|
| 423 |
role = USER_DB.get(account, ["", "Unknown"])[1]
|
|
|
|
| 438 |
.weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
|
| 439 |
.headless-tabs > div:first-child { display: none !important; }
|
| 440 |
.headless-tabs { border: none !important; background: transparent !important; }
|
| 441 |
+
.stat-dashboard { display: flex; gap: 15px; margin-bottom: 20px; justify-content: space-between; }
|
| 442 |
+
.stat-card { flex: 1; background-color: #ffffff !important; padding: 15px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); display: flex; align-items: center; gap: 15px; border-top: 4px solid transparent; }
|
| 443 |
+
.card-total { border-left: 4px solid #10b981; }
|
| 444 |
+
.card-anomaly { border-left: 4px solid #ef4444; }
|
| 445 |
+
.card-status { border-left: 4px solid #3b82f6; }
|
| 446 |
+
.card-icon { font-size: 30px; width: 40px; text-align: center; }
|
| 447 |
+
.card-content { flex: 1; }
|
| 448 |
+
.card-label { color: #374151 !important; font-size: 13px; margin-bottom: 2px; font-weight: bold !important; }
|
| 449 |
+
.card-value-container { display: flex; align-items: baseline; gap: 8px; }
|
| 450 |
+
.card-value { font-size: 24px; font-weight: bold; }
|
| 451 |
+
.card-total .card-value { color: #000000 !important; }
|
| 452 |
+
.card-unit { font-size: 14px; color: #4b5563 !important; font-weight: normal; }
|
| 453 |
+
.card-trend { font-size: 12px; padding: 2px 6px; border-radius: 4px; }
|
| 454 |
+
.trend-up { background-color: #fee2e2 !important; color: #ef4444 !important; }
|
| 455 |
+
.card-sub-text { color: #6b7280 !important; font-size: 11px; margin-top: 2px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
"""
|
| 457 |
|
| 458 |
with gr.Blocks(title="模擬校園入口與資安防護系統") as demo:
|
|
|
|
| 472 |
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)
|
| 473 |
|
| 474 |
with gr.Column(scale=2):
|
| 475 |
+
|
| 476 |
+
# 🌟 核心修改:使用無頭標籤頁 (Headless Tabs) 做最外層的頁面路由
|
| 477 |
+
with gr.Tabs(selected="login_route", elem_classes="headless-tabs") as main_router:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
+
# 路由 1:登入畫面
|
| 480 |
+
with gr.Tab("Login", id="login_route"):
|
| 481 |
+
with gr.Column(elem_classes="login-container"):
|
| 482 |
+
gr.Markdown("<div class='ntut-title'>校園入口網站 Taipei Tech Portal</div>")
|
| 483 |
+
login_msg = gr.Markdown(value="", visible=True)
|
| 484 |
+
with gr.Row(): acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="student or admin")
|
| 485 |
+
with gr.Row(): pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="password : 1234")
|
| 486 |
+
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>")
|
| 487 |
+
with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)")
|
| 488 |
+
login_btn = gr.Button("登入 Login", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
|
| 490 |
+
gr.Markdown("---")
|
| 491 |
+
gr.Markdown("<div style='text-align:center; color:gray; font-size: 0.9em;'>🛠️ 開發與測試專用捷徑</div>")
|
| 492 |
+
with gr.Row():
|
| 493 |
+
btn_fast_student = gr.Button("🚀 快速登入 (學生)", variant="secondary")
|
| 494 |
+
btn_fast_admin = gr.Button("🚀 快速登入 (管理員)", variant="secondary")
|
| 495 |
+
|
| 496 |
+
# 路由 2:資訊系統儀表板 (不再使用 visible=False)
|
| 497 |
+
with gr.Tab("Dashboard", id="dashboard_route"):
|
| 498 |
+
with gr.Column():
|
| 499 |
+
with gr.Row():
|
| 500 |
+
gr.Markdown("### 資訊系統")
|
| 501 |
+
welcome_text = gr.Markdown(value="", elem_classes="text-right")
|
| 502 |
+
logout_btn = gr.Button("登出", size="sm")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
|
| 504 |
+
with gr.Row():
|
| 505 |
+
btn_toggle_role = gr.Button("🔄 Student ↔ Admin", size="sm", variant="secondary")
|
| 506 |
+
gr.Markdown("---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
+
with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
|
|
|
|
| 509 |
|
| 510 |
+
with gr.Tab("👨🎓 學生專區", id="student_tab"):
|
| 511 |
+
with gr.Accordion("▼ 1. 教務系統", open=True):
|
| 512 |
+
with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
|
| 513 |
+
with gr.Row(): btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm"); btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm"); btn_course = gr.Button("▶ 課程系統", size="sm")
|
| 514 |
+
with gr.Row(): btn_score = gr.Button("▶ 學業成績查詢系統", size="sm")
|
| 515 |
+
with gr.Accordion("▼ 2. 學務系統", open=True):
|
| 516 |
+
with gr.Row(): btn_leave = gr.Button("▶ 學生請假系統", size="sm"); btn_dorm = gr.Button("▶ 學生宿舍登錄(抽籤)系統", size="sm"); btn_scholarship = gr.Button("▶ 獎助學金申請系統", size="sm")
|
| 517 |
+
with gr.Accordion("▼ 3. 資訊服務", open=True):
|
| 518 |
+
with gr.Row(): btn_webmail = gr.Button("▶ 網路郵局 WebMail", size="sm"); btn_vdesk = gr.Button("▶ 北科軟體雲", size="sm")
|
| 519 |
+
with gr.Accordion("▼ 惡意操作測試區", open=True):
|
| 520 |
+
btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
|
| 521 |
+
|
| 522 |
+
with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
|
| 523 |
|
| 524 |
+
stat_dashboard_display = gr.HTML(value=get_dashboard_html())
|
| 525 |
+
|
| 526 |
+
with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"):
|
| 527 |
+
gr.Markdown("<span style='color:gray; font-size:0.9em;'>選擇下方系統以檢視該系統在歷史與即時紀錄中的時間分佈狀況。圖表每分鐘會自動同步更新,歷史紀錄為綠點,即時攔截之異常為紅叉。</span>")
|
| 528 |
+
action_dropdown = gr.Dropdown(
|
| 529 |
+
choices=list(ACTION_MAP.keys()),
|
| 530 |
+
label="選擇要檢視的系統資料分佈",
|
| 531 |
+
value=None
|
| 532 |
+
)
|
| 533 |
+
action_plot = gr.Plot(show_label=False)
|
| 534 |
+
|
| 535 |
+
with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
|
| 536 |
+
gr.Markdown("<span style='color:gray; font-size:0.9em;'>手動輸入 0~100 的數值。提高強度會增加誤報率,但能更有效攔截新型攻擊。</span>")
|
| 537 |
+
with gr.Row():
|
| 538 |
+
overall_weight = gr.Number(label="總體防護強度 (%)", value=75, minimum=0, maximum=100)
|
| 539 |
+
time_weight = gr.Number(label="時間特徵權重 (%)", value=80, minimum=0, maximum=100)
|
| 540 |
+
with gr.Row():
|
| 541 |
+
ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100)
|
| 542 |
+
device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100)
|
| 543 |
|
| 544 |
+
with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"):
|
| 545 |
+
gr.Markdown("<span style='color:gray; font-size:0.9em;'>顯示所有被判定為異常的 Log。點擊分析可讓 AI 代為判讀潛在風險。</span>")
|
| 546 |
+
abnormal_log_display = gr.Dataframe(
|
| 547 |
+
value=initial_df_abn,
|
| 548 |
+
headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"],
|
| 549 |
+
datatype=["str", "str", "str", "str", "str", "str", "str"],
|
| 550 |
+
interactive=False, wrap=True
|
| 551 |
+
)
|
| 552 |
+
with gr.Row():
|
| 553 |
+
abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入,您也可以手動修改...", scale=4)
|
| 554 |
+
btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1)
|
| 555 |
+
|
| 556 |
+
ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False, placeholder="等待分析中...")
|
| 557 |
|
| 558 |
+
# ... (原有的自動化測試區按鈕保留不變) ...
|
| 559 |
+
with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"):
|
| 560 |
+
gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>")
|
| 561 |
+
gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)")
|
| 562 |
+
with gr.Row():
|
| 563 |
+
t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm"); t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm")
|
| 564 |
+
with gr.Row():
|
| 565 |
+
t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm"); t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm")
|
| 566 |
+
with gr.Row():
|
| 567 |
+
t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm"); t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm")
|
| 568 |
+
gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
|
| 569 |
+
with gr.Row():
|
| 570 |
+
t_eval_norm = gr.Button("✅ 正常: 期末網路教學評量 (12-17 13:20)", size="sm"); t_eval_anom = gr.Button("⚠️ 異常: 期末網路教學評量 (03-15 03:00)", size="sm")
|
| 571 |
+
with gr.Row():
|
| 572 |
+
t_score_norm = gr.Button("✅ 正常: 學業成績查詢系統 (11-17 18:50)", size="sm"); t_score_anom = gr.Button("⚠️ 異常: 學業成績查詢系統 (02-15 03:00)", size="sm")
|
| 573 |
+
gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
|
| 574 |
+
with gr.Row():
|
| 575 |
+
t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm"); t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm")
|
| 576 |
+
with gr.Row():
|
| 577 |
+
t_schol_norm = gr.Button("✅ 正常: 獎助學金申請系統 (03-12 14:00)", size="sm"); t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm")
|
| 578 |
+
with gr.Row():
|
| 579 |
+
t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm"); t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
|
| 580 |
+
gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)")
|
| 581 |
+
with gr.Row():
|
| 582 |
+
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")
|
| 583 |
+
with gr.Row():
|
| 584 |
+
t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm"); t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm")
|
| 585 |
+
with gr.Row():
|
| 586 |
+
t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
|
| 587 |
|
| 588 |
# --- 綁定事件 ---
|
| 589 |
|
| 590 |
demo.load(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
|
| 591 |
dashboard_timer.tick(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
|
|
|
|
| 592 |
action_dropdown.change(fn=get_plot_data, inputs=[action_dropdown], outputs=[action_plot])
|
|
|
|
| 593 |
time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
|
| 594 |
btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
|
| 595 |
|
| 596 |
weight_inputs = [overall_weight, time_weight, ip_weight, device_weight]
|
| 597 |
+
|
| 598 |
+
# 🎯 這裡改成更新 main_router,而不是舊的 login_view, dashboard_view
|
| 599 |
+
update_outputs = [main_router, role_tabs, login_msg, welcome_text, log_display, abnormal_log_display, abnormal_log_input]
|
| 600 |
|
| 601 |
login_btn.click(
|
| 602 |
fn=process_login, inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input] + weight_inputs, outputs=update_outputs
|
| 603 |
)
|
| 604 |
+
|
| 605 |
+
# 登出時多清除原本帳密的輸入框
|
| 606 |
logout_btn.click(
|
| 607 |
+
fn=logout, inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
|
| 608 |
+
outputs=[main_router, acc_input, pwd_input, captcha_input, login_msg, log_display, abnormal_log_display, abnormal_log_input]
|
| 609 |
)
|
| 610 |
|
| 611 |
fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs
|