重點:資料全握在我們手上、本地化自架,不用把情資貼到公開雲端,流程可記錄、可稽核,且可以把格式直接給網安設備。
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(Docker 或自架),並能修改 credential(SMTP)。
為每個情資來源建立一個 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 設定(單一來源範例):
建議:四個來源分別做 HTTP Request,之後用 Merge 或 Set 合併結果。
items[0].json.content
)Node:Function
負責把多個來源的 raw text 解析出 IP / CIDR,做去重、排序、過濾 invalid 項目,並回傳兩樣東西:
cidrs
:乾淨的 IPv4 CIDR list(例如 ["1.2.3.0/24","5.6.7.8/32", ...]
)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: ... } }
把 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
}
}
];
forti_cli
與 csv_text
存成檔案備查(檔名可用日期:forti_blacklist_YYYYMMDD.cli
)ops@example.com, security@example.com
(依公司)[TI] Daily Blacklist → FortiGate CLI (count: {{ $json["count"] }})
forti_cli
及 csv_text
以附件形式掛上(檔名 .cli
與 .csv
)SMTP 設定範例(在 n8n credential 中設定):
smtp.office365.com
svc_monitor@yourdomain
your_smtp_password
(建議使用 app password / restricted account)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。
cidrs
與 summary
,比對數量與合理性。今天把「人工抓 blocklist、處理格式、寄報表」這件事徹底自動化了。使用 n8n 做 Orchestration:每天抓 ThreatView / blocklist.de / FireHOL / DShield → Function node 清洗去重 → 產生 FortiGate CLI + CSV → 寄信給 SOC。
好處:減少手動失誤、快速回應、且產生的 CLI 可直匯入(測試環境先驗證)。若再加上審批流程(或用 API push 並搭配 change control),就可以做到半自動化阻斷。
結語:能自動化的就自動化,剩下的才用手動。