
在上一篇我們建構了負責視覺化與狀態管理的 utils.py 後,接下來需要一個強而有力的「大腦中樞」來發號施令。這支名為 main.py 的核心腳本,正是整套自動化情資監控系統的調度指揮(Orchestration)中心。
它將零散的爬蟲模組、分析邏輯與通報機制完美串接,確保每一筆威脅情資都能歷經嚴謹的「抓取、去重、分析、通報」生命週期。最後說明 main.py 的五大架構重點:
系統在啟動時的第一步,必須先確立當下的基準狀態,避免產生歷史資料的重複處理。
utils.init_env() 初始化環境,並透過 utils.load_state() 載入系統狀態。seen_urls 陣列,並將其轉換為集合(Set)資料結構,名為 seen_urls_set。面對廣泛的開源情報(OSINT),循序抓取會耗費過多時間。因此,系統導入了高效率的平行處理架構。
ThreadPoolExecutor 實作了多執行緒並行發起爬蟲任務的機制。ENABLED_SOURCES)動態篩選出目前啟用的情報來源。raw_pool 中,並能妥善攔截異常與顯示已關閉的來源。在情報收集過程中,必然會遇到大量的重複資訊。如何在毫秒間過濾雜訊,是系統效能的關鍵。
batch_dedup_set 集合進行比對。new_items_to_process,若無新資料則會直接提前結束任務。篩選出全新情資後,系統將依序執行深度處理管線。
utils.log_raw_discovery 寫入 CSV 檔案,確保數位跡證的完整保存。processor.process_item_logic 進行邏輯運算。should_send 標記且非 MSRC 的情資。若符合條件,便會觸發郵件發送機制,並同時紀錄預警發送歷史。一個健壯的自動化系統,必須防範長年運作帶來的架構負債(Architectural Debt)與檔案膨脹。
seen_urls_list 收斂,僅保留最新處理的 3000 筆紀錄。utils.save_all_results 寫入狀態,並在終端機印出包含處理筆數、預警數與耗費秒數的統計摘要,為該次任務劃下完美的句點。透過 main.py 嚴密的防護網與調度邏輯,我們的無伺服器情資中心得以具備極高的確定性(Deterministic)與韌性。
# main.py
import utils
import fetchers
import processor
from datetime import datetime
from config import TZ, ENABLED_SOURCES
from concurrent.futures import ThreadPoolExecutor, as_completed
def main():
# 1. 初始化環境與加載狀態
utils.init_env()
state = utils.load_state()
seen_urls_list = state.get('seen_urls', [])
seen_urls_set = set(seen_urls_list)
start_time = datetime.now(TZ)
print(f"\n{'='*60}")
print(f"🚀 資安情資監控系統啟動")
print(f"⏰ 啟動時間: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"{'='*60}\n")
# --- 流程 1: 並發抓取 (Concurrent Fetch) ---
print(">>> [1/5] 執行多執行緒並發資料抓取 (Fetch)...")
raw_pool = []
all_available_sources = [
("MSRC", fetchers.fetch_msrc_updates, "MSRC"),
("iThome", fetchers.fetch_rss_news, "ITHOME"),
("F-ISAC", fetchers.fetch_fisac_news, "FISAC"),
("NICS", fetchers.fetch_nics_news, "NICS")
]
# 篩選啟用的來源
active_sources = [
(display_name, func)
for display_name, func, config_key in all_available_sources
if ENABLED_SOURCES.get(config_key, False)
]
# 多執行緒並行發起爬蟲任務
max_workers = min(len(active_sources), 4) if active_sources else 1
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_source = {executor.submit(func): name for name, func in active_sources}
for future in as_completed(future_to_source):
name = future_to_source[future]
try:
data = future.result()
count = len(data) if data else 0
raw_pool.extend(data or [])
print(f" + {name.ljust(10)} : 成功抓取 {count} 筆")
except Exception as e:
print(f" ! {name.ljust(10)} : 抓取異常 - {e}")
disabled_names = [n for n, f, k in all_available_sources if not ENABLED_SOURCES.get(k)]
if disabled_names:
print(f" 🚫 已跳過 (關閉來源): {', '.join(disabled_names)}")
print(f"\n 📦 總計抓取到 {len(raw_pool)} 筆原始資料。")
# --- 批次去重(O(1) 複雜度)---
new_items_to_process = []
batch_dedup_set = set(seen_urls_set)
for item in raw_pool:
url = item.get('url')
if url and url not in batch_dedup_set:
new_items_to_process.append(item)
batch_dedup_set.add(url)
duplicate_count = len(raw_pool) - len(new_items_to_process)
print(f" ♻️ 過濾重複資料: 排除 {duplicate_count} 筆,剩餘 {len(new_items_to_process)} 筆新資料需處理。")
if not new_items_to_process:
print("\n>>> 狀態報告: 無任何新發布情資,任務提前結束。")
return
# --- 流程 2: 原始紀錄存檔 (Log Raw) ---
print(f"\n>>> [2/5] 執行原始資料存檔紀錄 (Log Raw)...")
non_msrc_items = [i for i in new_items_to_process if i['source'] != "MSRC"]
if non_msrc_items:
utils.log_raw_discovery(non_msrc_items)
print(f" ✅ 已將 {len(non_msrc_items)} 筆非 MSRC 情資寫入 raw_discovery.csv")
else:
print(" ℹ️ 本次無非 MSRC 資料,跳過 CSV 寫入。")
# --- 流程 3: 分析與評分 (Analyze) ---
print("\n>>> [3/5] 執行關鍵字分析與評分 (Analyze)...")
processed_results = []
print(f" {'得分':<4} | {'來源':<8} | {'標籤':<16} | {'標題'}")
print(f" {'-'*75}")
for item in new_items_to_process:
analyzed_item = processor.process_item_logic(item)
processed_results.append(analyzed_item)
seen_urls_list.append(item['url'])
score_str = f"[{analyzed_item['score']}]"
print(f" {score_str:<6} | {analyzed_item['source']:<10} | {analyzed_item['tag']:<16} | {analyzed_item['title'][:38]}...")
# --- 流程 4: 預警發送 (Alert) ---
to_notify = [
i for i in processed_results
if i.get('should_send') and i.get('source') != "MSRC"
]
print(f"\n>>> [4/5] 預警判斷 (Alert)...")
if to_notify:
print(f" 🔔 發現 {len(to_notify)} 筆符合條件情資,準備發送郵件...")
utils.send_visual_email(to_notify)
# --- 流程 5: 發送紀錄 ---
print("\n>>> [5/5] 紀錄預警發送歷史 (Log Alert)...")
utils.log_alert_sent(to_notify)
print(f" ✅ 發送紀錄已更新。")
else:
print(" 🔕 本次無符合預警門檻之情資,跳過發信。")
# --- 狀態更新(保留最新 3000 筆,避免 last_seen.json 膨脹)---
state['seen_urls'] = seen_urls_list[-3000:]
utils.save_all_results(state, processed_results)
# 統計摘要
end_time = datetime.now(TZ)
duration = (end_time - start_time).seconds
print(f"\n{'='*60}")
print(f"🏁 監控任務執行完畢")
print(f"📊 統計摘要:")
print(f" - 處理新資料: {len(new_items_to_process)} 筆")
print(f" - 發送預警數: {len(to_notify)} 筆")
print(f" - 總共耗時: {duration} 秒")
print(f"{'='*60}\n")
if __name__ == "__main__":
main()