iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Security

從1到2的召喚羊駝補破網之旅系列 第 13

Day 13 :鐵人賽第二個週末再來打副本

  • 分享至 

  • xImage
  •  

[鐵人賽] Day 13:把情資自動化 — 用 n8n 每天抓黑名單、產生 FortiGate CLI,並寄報告給我

寫在前面

重點:資料全握在我們手上、本地化自架,不用把情資貼到公開雲端,流程可記錄、可稽核,且可以把格式直接給網安設備。


架構總覽(想像圖,實作在 n8n)

ASCII 流程圖(簡潔版):

[Cron Trigger (每天)] 
        │
        ▼
[HTTP Request *4*]  <-- 4 個來源分別抓 (ThreatView, blocklist.de, FireHOL, DShield)
        │
        ▼
[Merge / Combine]  (合併多來源原始文字)
        │
        ▼
[Function (parse + dedupe + normalize CIDR/IP)]
        │
        ▼
[Function (build FortiGate CLI + CSV)]
        │
        ├──────────────┬──────────────┐
        ▼              ▼              ▼
[Save to S3]    [Create Attachment]  [Email Node (SMTP)]
(or file)         (csv & cli)         (寄出給主管 + SOC)

n8n 節點(Node-by-node 詳細設定)

前提:你已經安裝好 n8n(Docker 或自架),並能修改 credential(SMTP)。

1) Trigger:Cron

  • Node:Cron
  • 設定:每天 06:00(或你想要的時間)
  • 用途:排程啟動整個流程

2) HTTP Request(四個節點或一個多請求集合)

為每個情資來源建立一個 HTTP Request node,或在一個 node 裡用 iteration 取多個 URL。

內建來源(範例):

  • https://threatview.io/Downloads/IP-High-Confidence-Feed.txt
  • https://lists.blocklist.de/lists/all.txt
  • https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset
  • https://feeds.dshield.org/block.txt

HTTP Request Node 設定(單一來源範例):

  • Method: GET
  • URL: 上面其中一個
  • Response Format: String
  • Authentication: none

建議:四個來源分別做 HTTP Request,之後用 MergeSet 合併結果。

3) Merge / Combine(選擇一個)

  • Node:Merge 或 Function(視你寫法)
  • 功能:把四個來源的文字合併到單一輸出(items[0].json.content

4) Function:解析、過濾、normalize(核心)

  • Node:Function

  • 負責把多個來源的 raw text 解析出 IP / CIDR,做去重、排序、過濾 invalid 項目,並回傳兩樣東西:

    1. cidrs:乾淨的 IPv4 CIDR list(例如 ["1.2.3.0/24","5.6.7.8/32", ...]
    2. summary:統計數字(每個來源抓到幾筆、去重後總數)

請在 Function node 中貼下列程式碼(JavaScript)

// n8n Function node: parse & normalize IP/CIDR from multiple source texts
// Input: items[] with .json.body or .json (use the field you set earlier)
// Output: one item with { cidrs: [...], summary: {...} }

const texts = []; // collect all input texts
for (const item of items) {
  // try common fields
  if (item.json && item.json.body) texts.push(String(item.json.body));
  else if (item.json && item.json.data) texts.push(String(item.json.data));
  else if (item.json && item.json) texts.push(Object.values(item.json).join('\n'));
}

// regex to catch ip or ip/cidr (IPv4 only)
const ipCidrRe = /\b(?:(?:\d{1,3}\.){3}\d{1,3})(?:\/\d{1,2})?\b/g;

const seen = new Set();
const sourceCounts = [];
let totalFound = 0;

for (const t of texts) {
  const found = t.match(ipCidrRe) || [];
  sourceCounts.push(found.length);
  for (const f of found) {
    // quick sanity check: discard octets >255 or /prefix>32
    const parts = f.split('/');
    const ip = parts[0];
    const octs = ip.split('.').map(x => Number(x));
    const validIp = octs.length === 4 && octs.every(n => Number.isInteger(n) && n >=0 && n <=255);
    const cid = (parts.length===2) ? Number(parts[1]) : 32;
    if (!validIp) continue;
    if (cid < 0 || cid > 32) continue;
    // normalize: store as CIDR
    const normalized = (cid===32) ? `${ip}/32` : `${ip}/${cid}`;
    seen.add(normalized);
    totalFound++;
  }
}

// convert set to sorted array (by network then prefix)
const cidrs = Array.from(seen);
// sort by IP numeric then prefix
cidrs.sort((a,b)=>{
  const [aIp,aP] = a.split('/');
  const [bIp,bP] = b.split('/');
  const aNums = aIp.split('.').map(x=>Number(x));
  const bNums = bIp.split('.').map(x=>Number(x));
  for (let i=0;i<4;i++){
    if (aNums[i] !== bNums[i]) return aNums[i]-bNums[i];
  }
  return Number(aP)-Number(bP);
});

return [{ json: { cidrs, summary: { sourceCounts, totalFound, unique: cidrs.length } } }];

輸出範例:{ cidrs: [...], summary: { sourceCounts: [120, 5000, 800, 2000], totalFound: ... } }

5) Function:產生 FortiGate CLI + CSV(第二個 Function node)

cidrs 轉成 FortiGate CLI 可貼的 config firewall address + config firewall addrgrp 結構,並建立 CSV(可 attach 到信件)。

把下面程式碼貼到下一個 Function node:

// Input: items[0].json.cidrs (array of "x.x.x.x/nn")
// Config:
const groupName = 'BL_IP_ALL';
const namePrefix = 'BL_IP_';
const maxGroupMembers = 2000; // Forti group internal chunking, if needed

const cidrs = items[0].json.cidrs || [];
const lines = [];

lines.push('config firewall address');

for (let i=0;i<cidrs.length;i++){
  const idx = i+1;
  const objName = `${namePrefix}${idx}`;
  const cidr = cidrs[i];
  const parts = cidr.split('/');
  const ip = parts[0];
  const mask = (parts[1] && parts[1] !== '32') ? (() => {
    // convert prefix to netmask
    const p = Number(parts[1]);
    const maskNum = (~((1<<(32-p))-1))>>>0;
    return [(maskNum>>>24)&255, (maskNum>>>16)&255, (maskNum>>>8)&255, maskNum&255].join('.');
  })() : '255.255.255.255';
  lines.push(`    edit ${objName}`);
  lines.push(`        set type ipmask`);
  lines.push(`        set subnet ${ip} ${mask}`);
  lines.push(`        set comment "TI import ${cidr}"`);
  lines.push('    next');
}

lines.push('end');

// address group
if (cidrs.length>0){
  lines.push('config firewall addrgrp');
  // single group; if huge, can be chunked externally
  lines.push(`    edit ${groupName}`);
  // Forti limit: set member <obj> <obj> ...
  const members = [];
  for (let i=0;i<cidrs.length;i++){
    members.push(`${namePrefix}${i+1}`);
  }
  // Forti will accept multiple set member lines
  const chunkSize = 256;
  for (let i=0;i<members.length;i+=chunkSize){
    const chunk = members.slice(i, i+chunkSize).join(' ');
    lines.push(`        set member ${chunk}`);
  }
  lines.push('    next');
  lines.push('end');
}

// build CSV content
const csvRows = [['Name','Type','Network','Netmask','Comment']];
for (let i=0;i<cidrs.length;i++){
  const idx = i+1;
  const cidr = cidrs[i];
  const parts = cidr.split('/');
  const ip = parts[0];
  const netmask = (parts[1] && parts[1] !== '32') ? (() => {
    const p = Number(parts[1]);
    const maskNum = (~((1<<(32-p))-1))>>>0;
    return [(maskNum>>>24)&255, (maskNum>>>16)&255, (maskNum>>>8)&255, maskNum&255].join('.');
  })() : '255.255.255.255';
  csvRows.push([`${namePrefix}${idx}`,'ipmask', ip, netmask, `TI import ${cidr}`]);
}

// return: cli text & csv text as outputs (will be used by Email and/or File Node)
return [
  {
    json: {
      forti_cli: lines.join('\n') + '\n',
      csv_text: csvRows.map(r=>r.map(c=>`"${String(c).replace(/"/g,'""')}"`).join(',')).join('\n'),
      count: cidrs.length
    }
  }
];

6) (選用) Save to File / S3 / Google Drive

  • Node:AWS S3 / Google Drive / Local File(視你的環境)
  • 用途:把 forti_clicsv_text 存成檔案備查(檔名可用日期:forti_blacklist_YYYYMMDD.cli

7) Email Node(SMTP)

  • Node:SMTP Email
  • 收件人:ops@example.com, security@example.com(依公司)
  • 主旨:[TI] Daily Blacklist → FortiGate CLI (count: {{ $json["count"] }})
  • 內文:使用下方提供的 email 模板(可在 n8n 內使用 HTML)
  • Attachments:把 forti_clicsv_text 以附件形式掛上(檔名 .cli.csv

SMTP 設定範例(在 n8n credential 中設定):

  • Host: smtp.office365.com
  • Port: 587
  • Secure: false (StartTLS)
  • User: svc_monitor@yourdomain
  • Password: your_smtp_password(建議使用 app password / restricted account)

Email 範本(可以直接貼到 n8n 的 Email node body)

Subject:

[TI] Daily Threat Feed → FortiGate Import ({{ $json["count"] }} addresses) - {{ $now.toLocaleDateString() }}

HTML Body:

<p>各位,</p>
<p>本郵件由自動化情資系統產出(來源:ThreatView, blocklist.de, FireHOL, DShield)。</p>
<ul>
  <li>條目數(去重後):<b>{{ $json["count"] }}</b></li>
  <li>CLI 檔:FortiGate 可直接貼入 CLI(檔名:forti_blacklist_{{ $now.toISOString().slice(0,10) }}.cli)</li>
  <li>CSV 備份:forti_blacklist_{{ $now.toISOString().slice(0,10) }}.csv</li>
</ul>
<p>注意事項:</p>
<ol>
  <li>請先在測試 VDOM/測試設備上驗證 CLI 再貼到生產設備。</li>
  <li>若想自動 push 到設備,建議改用管理 API 或有審核流程的自動化工具(避免誤封)。</li>
</ol>
<p>— 自動化小幫手(n8n + Ollama)</p>

在 attachments 中加入從 Function node 輸出的 forti_cli(filename forti_blacklist_YYYYMMDD.cli)與 csv_text(filename forti_blacklist_YYYYMMDD.csv)。n8n 可以接受 binary attachments — 檔案可由 Write Binary File node 產生,或使用 Function node 把文字轉 binary。


測試建議(上線前必做)

  1. 在 n8n 的 Execute Node 逐步跑,先確認 HTTP Request 能抓到資料(Response 為文字)。
  2. 在 Function node 印出 cidrssummary,比對數量與合理性。
  3. 先把 Email 收件改成你自己,確認附件內容、CLI 格式無誤。
  4. 在 FortiGate 測試環境手動貼 CLI(不要直接在生產設備上貼)以確保語法符合版本。
  5. 加入 Error handling(若 HTTP Request 失敗或解析結果為 0,發送警告 email)。

進階建議(讓工作流程更堅固)

  • 在 Function node 裡加入 IP 黑名單白名單過濾(排除我的內網/誤報 IP 段)。
  • 使用 Vault / Secrets manager(或 n8n 的 Credentials)保存 SMTP / S3 憑證。
  • 若要自動 push 到 FortiGate:用 SSH node 或 REST API(注意安全與審批流程)。
  • 增加 Slack/Teams 通知 node:每次有新增超過 N 筆才通知(避免洗版)。
  • 在流程中加入版本記錄(在 S3/GDrive 存檔,並用 hash 判斷是否跟昨天不同,僅在變化時通知主管)。

Day 13 小結

今天把「人工抓 blocklist、處理格式、寄報表」這件事徹底自動化了。使用 n8n 做 Orchestration:每天抓 ThreatView / blocklist.de / FireHOL / DShield → Function node 清洗去重 → 產生 FortiGate CLI + CSV → 寄信給 SOC。
好處:減少手動失誤、快速回應、且產生的 CLI 可直匯入(測試環境先驗證)。若再加上審批流程(或用 API push 並搭配 change control),就可以做到半自動化阻斷。
結語:能自動化的就自動化,剩下的才用手動


上一篇
Day 12 :弱點存在通知
系列文
從1到2的召喚羊駝補破網之旅13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言