iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0

附上流程圖

https://ithelp.ithome.com.tw/upload/images/20260922/20184295vxCafNaPeG.png

在上一篇我們建構了負責視覺化與狀態管理的 utils.py 後,接下來需要一個強而有力的「大腦中樞」來發號施令。這支名為 main.py 的核心腳本,正是整套自動化情資監控系統的調度指揮(Orchestration)中心。

它將零散的爬蟲模組、分析邏輯與通報機制完美串接,確保每一筆威脅情資都能歷經嚴謹的「抓取、去重、分析、通報」生命週期。最後說明 main.py 的五大架構重點:

1. 記憶喚醒與初始化(Initialization)

系統在啟動時的第一步,必須先確立當下的基準狀態,避免產生歷史資料的重複處理。

  • 程式一開始會呼叫 utils.init_env() 初始化環境,並透過 utils.load_state() 載入系統狀態。
  • 系統會讀取先前記錄的 seen_urls 陣列,並將其轉換為集合(Set)資料結構,名為 seen_urls_set。
  • 這個動作賦予了系統「記憶」的能力,為後續的高效去重打下基礎。

2. 多執行緒並發抓取(Concurrent Fetch)

面對廣泛的開源情報(OSINT),循序抓取會耗費過多時間。因此,系統導入了高效率的平行處理架構。

  • 系統利用 Python 的 ThreadPoolExecutor 實作了多執行緒並行發起爬蟲任務的機制。
  • 程式會預先定義包含 MSRC、iThome、F-ISAC 與 NICS 的來源清單,並依據設定檔(ENABLED_SOURCES)動態篩選出目前啟用的情報來源。
  • 系統會動態分配最多 4 個工作執行緒(Workers)來並發處理這些任務。
  • 抓取完成後,系統會詳細彙整每個來源的成功抓取筆數,將資料匯入 raw_pool 中,並能妥善攔截異常與顯示已關閉的來源。

3. O(1) 複雜度的高效去重(Batch Deduplication)

在情報收集過程中,必然會遇到大量的重複資訊。如何在毫秒間過濾雜訊,是系統效能的關鍵。

  • 系統將新抓取到的網址(URL)與初始化階段建立的 batch_dedup_set 集合進行比對。
  • 受惠於 Set 資料結構 O(1) 的時間複雜度,系統能以極低的效能開銷完成比對。
  • 程式會精準計算出排除的重複資料數量,並篩選出真正需要處理的 new_items_to_process,若無新資料則會直接提前結束任務。

4. 核心處理管線:存檔、分析與預警

篩選出全新情資後,系統將依序執行深度處理管線。

  • 原始紀錄存檔(Log Raw):系統會先將非 MSRC 來源的情資過濾出來,並呼叫 utils.log_raw_discovery 寫入 CSV 檔案,確保數位跡證的完整保存。
  • 關鍵字分析與評分(Analyze):接著,系統會利用迴圈將每一筆新情資送入 processor.process_item_logic 進行邏輯運算。
  • 終端機可視化:在分析過程中,終端機會即時渲染出排版整齊的表格,清楚展示每筆情資的「得分」、「來源」、「標籤」與截斷後的「標題」。
  • 預警決策(Alert):分析完畢後,系統會篩選出帶有 should_send 標記且非 MSRC 的情資。若符合條件,便會觸發郵件發送機制,並同時紀錄預警發送歷史。

5. 狀態收斂與效能防護(State Management)

一個健壯的自動化系統,必須防範長年運作帶來的架構負債(Architectural Debt)與檔案膨脹。

  • 在處理流程的尾聲,系統會強制將 seen_urls_list 收斂,僅保留最新處理的 3000 筆紀錄。
  • 這個機制能有效避免狀態記錄檔(如 JSON 檔案)無限度膨脹,維持系統的輕量與穩定。
  • 最終,程式會呼叫 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()

上一篇
[Day 07]GitHub Actions - utils.py 視覺優化風格
下一篇
[Day 09] GitHub Actions 最後番外篇
系列文
從情資收集到資安鑑識:30 天建構自動化威脅情資與鑑識平台 共 11 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言