File size: 19,963 Bytes
c5a7cf0 b5ae04d e528918 23f2ebe c5a7cf0 ba7b7f3 582b8f8 4e5ef71 6524ee2 c5a7cf0 418b5c8 50c0658 418b5c8 50c0658 418b5c8 c5a7cf0 50c0658 a554b5e c8bbb2d 7877959 49cb911 582b8f8 c5a7cf0 e528918 f1e77b2 e528918 50c0658 418b5c8 c5a7cf0 f45f091 c5a7cf0 f45f091 c5a7cf0 d775ed8 c5a7cf0 d775ed8 c5a7cf0 8e4173a 50c0658 e528918 a554b5e e528918 418b5c8 ef94d18 e528918 50c0658 c5a7cf0 23f2ebe c5a7cf0 23f2ebe ef94d18 23f2ebe c5a7cf0 23f2ebe 50c0658 c5a7cf0 23f2ebe ef94d18 2d7fa4f 23f2ebe c5a7cf0 ef94d18 c5a7cf0 23f2ebe 418b5c8 c5a7cf0 23f2ebe c5a7cf0 6524ee2 e528918 a554b5e c5a7cf0 ef94d18 c5a7cf0 50c0658 c5a7cf0 50c0658 23f2ebe ef94d18 582b8f8 23f2ebe c5a7cf0 e528918 c5a7cf0 e528918 418b5c8 c5a7cf0 e528918 c5a7cf0 418b5c8 e528918 418b5c8 50c0658 ef94d18 418b5c8 c5a7cf0 50c0658 c5a7cf0 a554b5e c5a7cf0 ef94d18 ba7b7f3 a554b5e 50c0658 ba7b7f3 a554b5e e528918 a554b5e c5a7cf0 e528918 a554b5e c5a7cf0 7a2e2d7 1bd9676 c5a7cf0 7877959 c5a7cf0 7877959 c5a7cf0 7877959 c5a7cf0 7877959 c5a7cf0 582b8f8 e528918 a554b5e ba7b7f3 25689f2 ba7b7f3 25689f2 ba7b7f3 7877959 25689f2 7877959 ba7b7f3 c5a7cf0 418b5c8 c5a7cf0 50c0658 a554b5e 50c0658 c5a7cf0 50c0658 c5a7cf0 418b5c8 23f2ebe 418b5c8 23f2ebe c5a7cf0 418b5c8 c5a7cf0 418b5c8 c5a7cf0 23f2ebe c5a7cf0 23f2ebe 418b5c8 c5a7cf0 582b8f8 c5a7cf0 23f2ebe c5a7cf0 23f2ebe c5a7cf0 23f2ebe c5a7cf0 23f2ebe c5a7cf0 23f2ebe ef94d18 c5a7cf0 925eb3e c5a7cf0 23f2ebe ef94d18 c5a7cf0 23f2ebe ef94d18 418b5c8 c5a7cf0 418b5c8 c5a7cf0 50c0658 ef94d18 c5a7cf0 23f2ebe c5a7cf0 ef94d18 c5a7cf0 ef94d18 c5a7cf0 50c0658 c5a7cf0 50c0658 c5a7cf0 ef94d18 2d7fa4f c5a7cf0 ef94d18 c5a7cf0 23f2ebe c5a7cf0 5aa5596 c5a7cf0 23f2ebe c5a7cf0 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 |
import streamlit as st
import os
import io
import json
import csv # <--- 新增:用於處理 CSV
import numpy as np
import faiss
import uuid
import time
import sys
# === HuggingFace 模型相關套件 (替換為 InferenceClient) ===
try:
from huggingface_hub import InferenceClient
except ImportError:
st.error("請檢查是否安裝了所有 Hugging Face 相關依賴:pip install huggingface-hub")
# === LangChain/RAG 相關套件 (保持不變) ===
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_core.documents import Document
from langchain_community.vectorstores import FAISS
from langchain_community.vectorstores.utils import DistanceStrategy
from langchain_community.docstore.in_memory import InMemoryDocstore
# 嘗試匯入 pypdftry
try:
import pypdf
except ImportError:
pypdf = None
# --- 頁面設定 ---
st.set_page_config(page_title="Cybersecurity AI Assistant (Hugging Face RAG & Batch Analysis)", page_icon="🛡️", layout="wide")
st.title("🛡️ Meta-Llama-3-8B-Instruct with FAISS RAG & Batch Analysis (Inference Client)")
st.markdown("已啟用:**IndexFlatIP** + **L2 正規化** + **Hugging Face Inference Client (API)**。支援 JSON/CSV/TXT 執行批量分析。")
if 'execute_batch_analysis' not in st.session_state:
st.session_state.execute_batch_analysis = False
if 'batch_results' not in st.session_state:
st.session_state.batch_results = None
if 'rag_current_file_key' not in st.session_state:
st.session_state.rag_current_file_key = None
if 'batch_current_file_key' not in st.session_state: # 修改變數名稱以反映多格式
st.session_state.batch_current_file_key = None
if 'vector_store' not in st.session_state:
st.session_state.vector_store = None
if 'json_data_for_batch' not in st.session_state: # 變數名稱保留,但內容可能是轉換後的 dict
st.session_state.json_data_for_batch = None
# 設定模型 ID
MODEL_ID = "meta-llama/Llama-4-Scout-17B-16E-Instruct"
WINDOW_SIZE = 8
# --- 側邊欄設定 ---
with st.sidebar:
st.header("⚙️ 設定")
if not os.environ.get("HF_TOKEN"):
st.error("環境變數 **HF_TOKEN** 未設定。請設定後重新啟動應用程式。")
st.info(f"LLM 模型:**{MODEL_ID}** (Hugging Face Inference API)")
st.warning("⚠️ **注意**: 該模型使用 Inference API 呼叫,請確保您的 HF Token 具有存取權限。")
st.divider()
st.subheader("📂 檔案上傳")
# === 1. 批量分析檔案 (修改處:支援多種格式) ===
batch_uploaded_file = st.file_uploader(
"1️⃣ 上傳 **Log/Alert 檔案** (用於批量分析)",
type=['json', 'csv', 'txt'], # <--- 修改:新增 csv 和 txt
key="batch_uploader",
help="支援 JSON (Array), CSV (含標題), TXT (每行一條 Log)"
)
# === 2. RAG 知識庫檔案 ===
rag_uploaded_file = st.file_uploader(
"2️⃣ 上傳 **RAG 參考知識庫** (Logs/PDF/Code 等)",
type=['txt', 'py', 'log', 'csv', 'md', 'pdf'],
key="rag_uploader"
)
st.divider()
st.subheader("💡 批量分析指令")
analysis_prompt = st.text_area(
"針對每個 Log/Alert 執行的指令",
value="You are a security expert in charge of analyzing alerts related to Web Application Attacks and Brute Force & Reconnaissance. Respond with a clear, structured analysis using the following mandatory sections: \n\n- Priority: Provide the overall priority level. (Answer High risk, Medium risk, or Low risk only) \n- Explanation: If this alert is highly related to Web Application Attacks and Brute Force & Reconnaissance, explain the potential impact and why this specific alert requires attention. If not, **omit the explanation section**. \n- Action Plan: If this alert is highly related to Web Application Attacks and Brute Force & Reconnaissance, What should be the immediate steps to address this specific alert? If not, **omit the action plan section**. \n\nStrictly use the information in the provided Log.",
height=200
)
st.markdown("此指令將對檔案中的**每一個 Log 條目**執行一次獨立分析。")
if batch_uploaded_file:
if st.button("🚀 執行批量分析"):
if not os.environ.get("HF_TOKEN"):
st.error("無法執行,環境變數 **HF_TOKEN** 未設定。")
else:
st.session_state.execute_batch_analysis = True
else:
st.info("請上傳 Log 檔案以啟用批量分析按鈕。")
st.divider()
st.subheader("🔍 RAG 檢索設定")
similarity_threshold = st.slider("📐 Cosine Similarity 門檻", 0.0, 1.0, 0.4, 0.01)
st.divider()
st.subheader("模型參數")
system_prompt = st.text_area("System Prompt", value="You are a Senior Security Analyst, named Ernest. You provide expert, authoritative, and concise advice on Information Security. Your analysis must be based strictly on the provided context.", height=100)
max_output_tokens = st.slider("Max Output Tokens", 128, 4096, 2048, 128)
temperature = st.slider("Temperature", 0.0, 1.0, 0.1, 0.1)
top_p = st.slider("Top P", 0.1, 1.0, 0.95, 0.05)
st.divider()
if st.button("🗑️ 清除所有紀錄"):
for key in list(st.session_state.keys()):
del st.session_state[key]
st.rerun()
# --- 初始化 Hugging Face LLM Client ---
@st.cache_resource
def load_inference_client(model_id):
if not os.environ.get("HF_TOKEN"): return None
try:
client = InferenceClient(model_id, token=os.environ.get("HF_TOKEN"))
st.success(f"Hugging Face Inference Client **{model_id}** 載入成功。")
return client
except Exception as e:
st.error(f"Hugging Face Inference Client 載入失敗: {e}")
return None
inference_client = None
if os.environ.get("HF_TOKEN"):
with st.spinner(f"正在連線到 Inference Client: {MODEL_ID}..."):
inference_client = load_inference_client(MODEL_ID)
if inference_client is None and os.environ.get("HF_TOKEN"):
st.warning("Hugging Face Inference Client 無法連線。")
elif not os.environ.get("HF_TOKEN"):
st.error("請在環境變數中設定 HF_TOKEN。")
# === Embedding 模型 (保持不變) ===
@st.cache_resource
def load_embedding_model():
model_kwargs = {'device': 'cpu', 'trust_remote_code': True}
encode_kwargs = {'normalize_embeddings': False}
return HuggingFaceEmbeddings(model_name="BAAI/bge-large-zh-v1.5", model_kwargs=model_kwargs, encode_kwargs=encode_kwargs)
with st.spinner("正在載入 Embedding 模型..."):
embedding_model = load_embedding_model()
# === 建立向量庫 / Search 函數 (保持不變) ===
def process_file_to_faiss(uploaded_file):
text_content = ""
try:
if uploaded_file.type == "application/pdf":
if pypdf:
pdf_reader = pypdf.PdfReader(uploaded_file)
for page in pdf_reader.pages:
text_content += page.extract_text() + "\n"
else: return None, "PDF library missing"
else:
stringio = io.StringIO(uploaded_file.getvalue().decode("utf-8"))
text_content = stringio.read()
if not text_content.strip(): return None, "File is empty"
events = [line for line in text_content.splitlines() if line.strip()]
docs = [Document(page_content=e) for e in events]
if not docs: return None, "No documents created"
embeddings = embedding_model.embed_documents([d.page_content for d in docs])
embeddings_np = np.array(embeddings).astype("float32")
faiss.normalize_L2(embeddings_np)
dimension = embeddings_np.shape[1]
index = faiss.IndexFlatIP(dimension)
index.add(embeddings_np)
doc_ids = [str(uuid.uuid4()) for _ in range(len(docs))]
docstore = InMemoryDocstore({_id: doc for _id, doc in zip(doc_ids, docs)})
index_to_docstore_id = {i: _id for i, _id in enumerate(doc_ids)}
vector_store = FAISS(embedding_function=embedding_model, index=index, docstore=docstore, index_to_docstore_id=index_to_docstore_id, distance_strategy=DistanceStrategy.COSINE)
return vector_store, f"{len(docs)} chunks created."
except Exception as e:
return None, f"Error: {str(e)}"
def faiss_cosine_search_all(vector_store, query, threshold):
q_emb = embedding_model.embed_query(query)
q_emb = np.array([q_emb]).astype("float32")
faiss.normalize_L2(q_emb)
index = vector_store.index
D, I = index.search(q_emb, k=index.ntotal)
selected = []
for score, idx in zip(D[0], I[0]):
if idx == -1: continue
if score >= threshold:
doc_id = vector_store.index_to_docstore_id[idx]
doc = vector_store.docstore.search(doc_id)
selected.append((doc, score))
selected.sort(key=lambda x: x[1], reverse=True)
return selected
# === Hugging Face 生成單一 Log 分析回答 (保持不變) ===
def generate_rag_response_hf_for_log(client, model_id, log_sequence_text, user_prompt, sys_prompt, vector_store, threshold, max_output_tokens, temperature, top_p):
if client is None: return "ERROR: Client Error", ""
context_text = ""
if vector_store:
selected = faiss_cosine_search_all(vector_store, log_sequence_text, threshold)
if selected:
retrieved_contents = [f"--- Reference Chunk (sim={score:.3f}) ---\n{doc.page_content}" for i, (doc, score) in enumerate(selected[:5])]
context_text = "\n".join(retrieved_contents)
rag_instruction = f"""=== RETRIEVED REFERENCE CONTEXT (Cosine ≥ {threshold}) ==={context_text if context_text else 'No relevant reference context found.'}=== END REFERENCE CONTEXT ===\nANALYSIS INSTRUCTION: {user_prompt}\nBased on the provided LOG SEQUENCE and REFERENCE CONTEXT, you must analyze the **entire sequence** to detect any continuous attack chains or evolving threats."""
log_content_section = f"""=== CURRENT LOG SEQUENCE TO ANALYZE (Window Size: {WINDOW_SIZE}) ===\n{log_sequence_text}\n=== END LOG SEQUENCE ==="""
messages = [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": f"{rag_instruction}\n\n{log_content_section}"}
]
try:
response_stream = client.chat_completion(messages, max_tokens=max_output_tokens, temperature=temperature, top_p=top_p, stream=False)
if response_stream and response_stream.choices:
return response_stream.choices[0].message.content.strip(), context_text
else: return "Format Error", context_text
except Exception as e: return f"Model Error: {str(e)}", context_text
# =======================================================================
# === 檔案處理區塊 (RAG 檔案) ===
if rag_uploaded_file:
file_key = f"vs_{rag_uploaded_file.name}_{rag_uploaded_file.size}"
if st.session_state.rag_current_file_key != file_key or 'vector_store' not in st.session_state:
with st.spinner(f"正在建立 RAG 參考知識庫 ({rag_uploaded_file.name})..."):
vs, msg = process_file_to_faiss(rag_uploaded_file)
if vs:
st.session_state.vector_store = vs
st.session_state.rag_current_file_key = file_key
st.toast(f"RAG 參考知識庫已更新!{msg}", icon="✅")
else: st.error(msg)
elif 'vector_store' in st.session_state:
del st.session_state.vector_store
del st.session_state.rag_current_file_key
st.info("RAG 檔案已移除,已清除相關知識庫。")
# === 檔案處理區塊 (批量分析檔案 - 重大修改處) ===
# 支援 JSON, CSV, TXT 並統一轉換為 list of dicts
if batch_uploaded_file:
batch_file_key = f"batch_{batch_uploaded_file.name}_{batch_uploaded_file.size}"
if st.session_state.batch_current_file_key != batch_file_key or 'json_data_for_batch' not in st.session_state:
try:
stringio = io.StringIO(batch_uploaded_file.getvalue().decode("utf-8"))
parsed_data = None
# --- Case 1: JSON ---
if batch_uploaded_file.name.lower().endswith('.json'):
parsed_data = json.load(stringio)
st.toast("JSON 檔案載入成功", icon="📄")
# --- Case 2: CSV ---
elif batch_uploaded_file.name.lower().endswith('.csv'):
# 使用 DictReader 將 CSV 轉為 List of Dicts
reader = csv.DictReader(stringio)
parsed_data = list(reader)
st.toast("CSV 檔案已轉換為 JSON 結構", icon="📊")
# --- Case 3: TXT ---
else: # 預設為 TXT
# 將每一行包裝成一個 JSON 物件: {"raw_content": "line text"}
lines = stringio.readlines()
parsed_data = [{"raw_log_entry": line.strip()} for line in lines if line.strip()]
st.toast("TXT 檔案已轉換為 JSON 結構", icon="📝")
# 儲存處理後的數據
st.session_state.json_data_for_batch = parsed_data
st.session_state.batch_current_file_key = batch_file_key
except Exception as e:
st.error(f"檔案解析錯誤: {e}")
if 'json_data_for_batch' in st.session_state:
del st.session_state.json_data_for_batch
elif 'json_data_for_batch' in st.session_state:
del st.session_state.json_data_for_batch
del st.session_state.batch_current_file_key
if "batch_results" in st.session_state:
del st.session_state.batch_results
st.info("批量分析檔案已移除,已清除相關數據。")
# === 執行批量分析邏輯 ===
if st.session_state.execute_batch_analysis and 'json_data_for_batch' in st.session_state:
st.session_state.execute_batch_analysis = False
start_time = time.time()
st.session_state.batch_results = []
if inference_client is None:
st.error("Client 未連線,無法執行。")
else:
data_to_process = st.session_state.json_data_for_batch
logs_list = []
# 處理不同的 JSON 結構 (Dict vs List)
if isinstance(data_to_process, list):
logs_list = data_to_process
elif isinstance(data_to_process, dict):
# 嘗試尋找常見的 key
if 'alerts' in data_to_process and isinstance(data_to_process['alerts'], list):
logs_list = data_to_process['alerts']
elif 'logs' in data_to_process and isinstance(data_to_process['logs'], list):
logs_list = data_to_process['logs']
else:
logs_list = [data_to_process]
else:
logs_list = [data_to_process]
if logs_list:
vs = st.session_state.get("vector_store", None)
# --- 關鍵:在這裡做 JSON String 的轉換 ---
# 無論來源是 CSV(Dict) 還是 TXT(Dict),都在這裡用 json.dumps 轉成字串
# 這保證了 Prompt 收到的永遠是 JSON 格式的文字
formatted_logs = [json.dumps(log, indent=2, ensure_ascii=False) for log in logs_list]
analysis_sequences = []
for i in range(len(formatted_logs)):
start_index = max(0, i - WINDOW_SIZE + 1)
end_index = i + 1
current_window = formatted_logs[start_index:end_index]
sequence_text = []
for j, log_str in enumerate(current_window):
is_target = " <<< TARGET LOG TO ANALYZE" if j == len(current_window) - 1 else ""
sequence_text.append(f"--- Log Index {i - len(current_window) + j + 1} ({len(current_window)-j} prior logs){is_target} ---\n{log_str}")
analysis_sequences.append({
"sequence_text": "\n\n".join(sequence_text),
"target_log_id": i + 1,
"original_log_entry": logs_list[i]
})
total_sequences = len(analysis_sequences)
st.header(f"⚡ 批量分析執行中 (平移視窗 $N={WINDOW_SIZE}$)...")
progress_bar = st.progress(0, text=f"準備處理 {total_sequences} 個序列...")
results_container = st.container()
full_report_chunks = ["## Cybersecurity Batch Analysis Report\n\n"]
for i, seq_data in enumerate(analysis_sequences):
log_id = seq_data["target_log_id"]
progress_bar.progress((i + 1) / total_sequences, text=f"Processing {i + 1}/{total_sequences} (Log #{log_id})...")
try:
response, retrieved_ctx = generate_rag_response_hf_for_log(
client=inference_client,
model_id=MODEL_ID,
log_sequence_text=seq_data["sequence_text"],
user_prompt=analysis_prompt,
sys_prompt=system_prompt,
vector_store=vs,
threshold=similarity_threshold,
max_output_tokens=max_output_tokens,
temperature=temperature,
top_p=top_p
)
item = {
"log_id": log_id,
"log_content": seq_data["original_log_entry"],
"sequence_analyzed": seq_data["sequence_text"],
"analysis_result": response,
"context": retrieved_ctx
}
st.session_state.batch_results.append(item)
with results_container:
st.subheader(f"Log/Alert #{item['log_id']}")
with st.expander("序列內容 (JSON Format)"):
st.code(item["sequence_analyzed"], language='json') # 這裡顯示的會是 JSON 格式
is_high = any(x in response.lower() for x in ['high risk'])
if is_high: st.error(item['analysis_result'])
else: st.info(item['analysis_result'])
if item['context']:
with st.expander("參考 RAG 片段"): st.code(item['context'])
st.markdown("---")
log_content_str_for_report = json.dumps(item["log_content"], indent=2, ensure_ascii=False).replace("`", "\\`")
full_report_chunks.append(f"---\n\n### Log #{item['log_id']}\n```json\n{log_content_str_for_report}\n```\nResult:\n{item['analysis_result']}\n")
except Exception as e:
st.error(f"Error Log {log_id}: {e}")
end_time = time.time()
progress_bar.empty()
st.success(f"完成!耗時 {end_time - start_time:.2f} 秒。")
else:
st.error("無法提取有效 Log,請檢查檔案格式。")
# === 顯示結果 (歷史紀錄) ===
if st.session_state.get("batch_results") and not st.session_state.execute_batch_analysis:
st.header("⚡ 歷史分析結果")
full_report_chunks = ["## Report\n\n"]
for item in st.session_state.batch_results:
log_content_str_for_report = json.dumps(item["log_content"], indent=2, ensure_ascii=False).replace("`", "\\`")
full_report_chunks.append(f"---\n\n### Log #{item['log_id']}\n```json\n{log_content_str_for_report}\n```\n{item['analysis_result']}\n")
st.download_button("📥 下載完整報告 (.md)", "\n".join(full_report_chunks), "report.md", "text/markdown") |