PhilipL commited on
Commit
80bb829
·
unverified ·
1 Parent(s): b65ad61

statistics, chart, abnormal log

Browse files
Files changed (1) hide show
  1. app.py +387 -69
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 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]
@@ -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="模擬校園入口與資安防護系統", css=custom_css) as demo:
 
 
 
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
- 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 = [
@@ -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)