前面幾篇講的是防禦節點怎麼把事件送進 BeakPlatform、平台怎麼把決策送回防火牆。這一篇換到平台這一側,講一件更根本的事:資安案件的處置流程是用「節點」拼出來的,而節點是可以自己加的。本篇用兩個真的做出來的節點當例子,並記下一個實驗:把開發指南直接交給 AI 程式助理當指令,看它能不能做出可用的節點、哪裡會做錯。主機沿用防禦節點那一套:主機 A(192.168.0.111)是 BeakPlatform。
先講結論。 一個節點由兩樣引擎認得的東西(資料庫裡的一筆定義、一支 Python 處理器)加十一樣配套組成,漏掉配套不會報錯、只會沒效果。把這 13 層與三個設計決定寫成文件後,AI 照著做出了兩個可用的節點,但人工反向檢查仍抓到三個測試全綠、真實環境會出事的問題。文件寫得到「AI 照著做會做出東西」的粒度,驗收卻不能只看它做了什麼。
平台的流程設計器出廠有 28 種節點:簽核、分支、延遲、Email、Telegram、寫入欄位、人事取值、OS 命令、防禦決策等等。資安案件走的「SOC 團隊版」「單人版」流程,就是這些節點接出來的。
認真用一陣子就會撞到兩個缺口:
| 缺口 | 症狀 |
|---|---|
| 沒有「對外呼叫」 | 通知節點都是寫死對象(Telegram 就是 Telegram、Email 就是 Email)。要建一張 Jira 工單、打一個 Slack webhook、叫自家系統做一件事,沒有任何節點做得到。每多一個對手就得多寫一個節點,寫不完 |
| 沒有「等對方回來」 | 就算有了對外呼叫,對同步 API 夠用;對非同步的對手(工單要人處理、外部批次要跑幾小時、對方回「收到了,稍後通知你」)它只知道「對方收到了」,不知道結果。流程只能瞎等或瞎猜 |
這兩個缺口是一對的。補第一個不補第二個,流程就是半盲的:送得出去、收不回來。所以本篇的兩個範例是 HttpRequest(HTTP 呼叫)與 ExternalWait(等待外部回呼),接起來是完整的 webhook 往返:
HttpRequest(把回呼網址一起送出) → ExternalWait(停住,等對方打回來) → Branch(依回呼內容分路)
節點本身的用法在使用手冊裡,本篇不重複。本篇講的是一個節點由什麼組成、加一個節點要碰哪些地方、哪些地方漏掉會不報錯但沒效果,以及把這些寫成文件之後,AI 照著做會做到什麼程度。
流程引擎只認兩樣東西:資料庫裡的一筆節點定義(設計器靠它畫出工具列、知道這個型別叫什麼、用哪個圖示),與一支 Python 的處理器(執行期靠它跑)。其餘都是讓人用得順的配套。但配套不是可選的:缺了面板,設計者填不了設定;缺了儲存時的收集,面板填了卻存回舊值;缺了種子匯出,你的機器有、別人全新安裝的機器沒有。

(圖01:一個節點在設計期與執行期各由誰負責,右側是加一個節點要碰的 13 層。設計期看到的是資料庫的「定義」與 JS 的「面板」,執行期用到的是 Python 的「處理器」,三者靠 node_type 這個字串對上。)
| 層 | 內容 | 漏掉的症狀 |
|---|---|---|
| 1 處理器 | node_handlers/<name>_handler.py |
執行時拋「找不到節點類型的處理器」,流程卡住 |
| 2 處理器註冊 | factory.py |
同上 |
| 3 節點定義 | 資料表 workflow_node_definitions 一筆 |
工具列沒有這個節點,不報錯 |
| 4 定義匯出成安裝種子 | 重跑匯出腳本 | 你的機器有、全新安裝沒有,不報錯 |
| 5 圖示 | 一個 24×24 的 SVG | 畫布上是空方框 |
| 6 型別正規化 | 設計器的 typeMap | 大小寫變體進來才出事 |
| 7 面板派送 | 設計器的分派表三處 | 點節點只看到名稱與描述 |
| 8 面板本體 | 新檔 render 與 collect 兩個函式 | 同上 |
| 9 儲存時收集面板值 | 儲存函式的 switch 加一個 case | 面板填了、按儲存卻存回舊值。最常見的靜默失敗 |
| 10 模板掛 script | 設計器頁面加一行 | 面板空白,主控台一行 not defined |
| 11 多語系 | 字串包翻譯函式、字典補 key | 英文介面出現中文 |
| 12 測試 | 一支 pytest | 沒有症狀,這正是問題 |
| 13 文件 | 使用手冊一頁;需要主機設定的再加維運文件 | 使用者不知道怎麼用 |
13 層列成表之後,最有用的不是表本身,而是一個找法:挑一個與你最像的既有節點,在整個原始碼目錄把它的型別名稱 grep 一遍。跑出來的每個檔案都問一次「我的節點要不要也出現在這裡」。這比任何清單都可靠,因為清單會過時,grep 不會。
這三個決定不是風格問題,選錯會留下靜默的錯誤。
| 決定 | 選項 | 怎麼選 |
|---|---|---|
| 企業級,還是受限節點 | 企業級:所有有流程設計權限的企業都看得到。受限:只有系統管理員在「節點授權」頁勾選的企業看得到 | 碰主機、碰平台自己的資料庫、等同把主機交出去的功能一律受限(OS 命令、檔案讀寫、SQL 都是)。風險能用白名單圈住的,企業級。本篇兩個節點都是企業級,安全靠白名單 |
| 失敗要紅燈,還是綠燈但寫變數 | 處理器回 error:佇列標失敗並自動重試三次。回 success 並把真相寫進變數:流程管理頁一片綠,要看變數才知道成敗 | 有副作用的動作(送出 HTTP POST、寫檔、寄信)不能重試,重試等於重複副作用,所以一律綠燈寫變數,由下游「分支」依變數分路。只有設定本身就錯(必填沒填)才在驗證階段直接失敗,因為那時還沒有任何副作用 |
| 節點要不要「等」 | 不等:做完回 success。等:回 waiting,之後要靠執行器的某個喚醒條件才會再動 | 這是最容易做出「永遠卡住、不報錯」的地方,第五節專講 |
這顆節點讓流程呼叫任何 HTTP(S) API,把狀態碼、標頭、內容與 JSON 解析結果寫進流程變數。有了它,Slack、Teams、Discord 的 incoming webhook、工單系統建單、自家系統的 REST API,全都不必再各寫一個節點。它刻意不做成「webhook 節點」而是通用的 HTTP 呼叫,因為差別只在 body 範本,而 body 範本是設計者填的。
它的安全設計比功能本身重要,因為「讓流程設計者對任意網址發請求」就是教科書上的 SSRF。平台的做法:
| 規則 | 內容 |
|---|---|
| 目的地白名單,fail-closed | 兩層系統設定:全平台清單與各企業清單取交集。空清單就是全部拒絕,不是全部放行。節點回 not_allowed,而且連 DNS 都不查 |
| 私有位址只能用 IP 明確放行 | 主機名解析出來的每一個 IP,若是 private、loopback、link-local、multicast、reserved,必須落在某個 IP 或 CIDR 的白名單項目內。*.example.com 解析到 10.0.0.5 也會被擋。這一條是 SSRF 防護的核心:攻擊者控制的 DNS 不能把流程帶進內網 |
| 不跟隨轉址 | 3xx 當成 http_error 記下來,Location 留在回應標頭變數裡。跟隨轉址等於讓對方決定下一個目的地,繞過白名單 |
| 回應有上限 | 串流讀取、到上限就截斷並標記;敏感標頭(Authorization、Cookie、含 token、secret、key 的)進 log 前遮蔽;回應內容進 log 最多 500 字 |
| 永遠回成功 | 第三節第 2 點的落實。<前綴>_result 是 ok、http_error、timeout、connection_error、not_allowed、config_error 之一;<前綴>_ok 只有 2xx 才是真 |
這一版沒有秘密儲存。 標頭值寫在流程圖裡,能編輯流程的人都看得到。文件明說了這一點,並建議把長期金鑰放在 API 端只允許特定來源 IP 的設定上。這是刻意留下的邊界:秘密儲存牽涉另一套設定組與加密,不該混進「第一個節點」的範圍。
處理器回 waiting 之後,那個佇列項目進入 WAITING,不會有任何東西自動再碰它,除非符合執行器輪詢的某個條件:
| 條件 | 寫法 | 既有的使用者 |
|---|---|---|
| A 時間到 | 節點型別在執行器的名單內,且 scheduled_at 到期。名單在程式裡出現兩次(主輪詢與備援輪詢),加型別時兩處都要加 |
延遲、並行匯合、OS 命令排隊 |
| B 結果裡的期限到 | 結果 JSON 裡的 timeout_at 到期 |
簽核逾時 |
| C 外部事件 | 某支 API 直接把佇列項目往下推 | 簽核 API |
不在任何條件內的 WAITING 節點就是死的:流程停住、不報錯、流程管理頁一切正常。平台歷史上一個叫 Converge 的節點就是這樣死掉後被刪除的。
ExternalWait 用的是最單純的組合:條件 A 加一個「推一下」的 API。進入等待時把 scheduled_at 設成逾時時刻;回呼端點收到合法回呼時先把內容寫進流程變數,再把 scheduled_at 改成現在;執行器下一輪就把它當成「時間到」撿起來重跑處理器,處理器看到變數裡已有回呼內容就完成。整個機制只多了兩個字串(名單兩處)與一支 API。
回呼網址要在 HttpRequest 送出時就存在,但那時 ExternalWait 還沒跑,沒有機會「發一個 token 存起來」。所以 token 不存資料庫,用可重算的 HMAC:以平台密鑰派生一把 key,對「流程實例代碼|等待節點 id」簽章,取前 32 個十六進位字元。端點拿路徑裡的三段重算一次、常數時間比對。流程實例代碼本身不可猜,再加簽章,拿到網址的只有收到那一則 HttpRequest 的對方;簽章綁定實例與節點,同一流程裡兩個 ExternalWait 的網址互不相通。
設計者這一側只需要在 HttpRequest 的 body 寫 ${cb.node-Wait},變數系統多了一個 cb. 前綴,解析成那顆等待節點在這個實例的回呼網址。
對方處理太快、或流程在兩顆節點之間還夾了別的節點,回呼可能在 ExternalWait 尚未進入等待時就到了。不處理這個 race,症狀是「偶爾卡死、不報錯」,很難重現。解法是端點永遠先寫變數,不管節點到了沒;處理器進入時永遠先查變數,有就直接完成、不等。這樣早到、準時、晚到走的是同一條路。

(圖02:webhook 往返的三條泳道。平台在對方處理期間完全不消耗資源,靠回呼端點把等待中的節點叫醒;回呼若比等待節點更早到,端點照樣寫變數,處理器進入時先查變數就直接完成。)
同一個等待只收一次:第二次打同一網址得到 409,逾時後打回來也是 409。回呼網址等同一次性密碼,不要寫進 log 與聊天工具。
兩個節點的開發指南寫在原始碼目錄的 docs/develop/,分兩篇:第一篇是 13 層、三個決定與 HttpRequest 的完整規格;第二篇是等待型節點、免登入端點的規則與 ExternalWait 的完整規格。寫的時候就決定了驗收方式:把指南原文交給 AI 程式助理當指令,它能據此做出可用的節點,指南才算寫對。 這比請人審閱嚴格,因為 AI 不會腦補,文件沒寫的它就會猜,猜錯就是文件的洞。
兩輪的結果:
| 輪次 | 結果 | AI 自己回報「文件沒寫清楚、得自己猜」的地方 |
|---|---|---|
| 第一篇 HttpRequest | 一次完成。26 條單元測試全綠,自己跑完端對端(白名單內的呼叫成功、白名單外的被拒且對方沒收到請求),設計器面板經人用瀏覽器實測無誤 | 三處:測試指令要在哪個目錄跑;背景啟動測試用伺服器在自動化環境會被 shell 收掉;Python 把文件專用範例網段 203.0.113.0/24 判成特殊位址,拿它當「公網」測資會被 SSRF 規則擋下 |
| 第二篇 ExternalWait | 實作全部寫完、單元測試全綠,也自己跑完準時回呼與 150 秒逾時兩種時序,但在寫回報前撞到 AI 服務的用量上限,回報沒有產出 | 無法得知,由人補驗 |
兩輪都通過了「做得出可用節點」這個門檻,但通過不等於沒錯。人工反向檢查(對每個防線問「有沒有繞過它的直接寫法」)抓到三件 AI 做出來、測試也全綠、卻會在真實環境出事的問題:
| 問題 | 為什麼測試抓不到 | 修法 |
|---|---|---|
把佇列的 scheduled_at 欄位改成可為空,用 NULL 表示「不設逾時」 |
模型改了,資料庫欄位沒改,仍是 NOT NULL。AI 自己的端對端兩種時序都開了逾時,沒走到「不設逾時」的路徑;單元測試用的是乾淨測試庫,模型怎麼寫欄位就怎麼建 | 還原模型,不設逾時放一個遠期哨兵值(9999-12-31),執行器永遠不會當成到期。順帶一個教訓:改 schema 就是改 schema,不能藏在「順手」裡,既有安裝升級時欄位不會自動改 |
回呼端點只在佇列是 WAITING 時才推 scheduled_at |
回呼若落在處理器「讀完變數(沒有)」與「寫回 WAITING」之間,佇列狀態是 RUNNING,這一則回呼會被埋到逾時。窗口只有幾毫秒,測試重現不了 | 兩邊各讓一步:端點對 WAITING、RUNNING、PENDING 都推;處理器在 commit 自己的 scheduled_at 之後再查一次變數。前者保證晚寫的不會被處理器覆蓋,後者保證早寫的會被看到 |
| 測試用伺服器的延遲上限 60 秒 | 文件寫 60,示範的逾時是 2 分鐘,「逾時場景」根本跑不出來。AI 在實作時自己改成 300 並跑過,但文件沒同步 | 統一為 300,文件與示範提示一起改 |
這個實驗真正證明的事:文件寫到「AI 照著做會做出東西」的粒度是做得到的,而且比想像中便宜(兩篇指南各約 250 到 400 行)。但它不會取代反向檢查。AI 的自我檢查表會勾、測試會綠、端對端會過,三者加起來仍然漏掉一個會在升級後炸掉的 schema 問題與一個偶發的 race。驗收的工作不是確認「做了」,是去找「繞過它的寫法」。
docs/develop/custom_node.md 與 custom_node_wait.md。兩篇都是公開文件,與本篇不同的是它們寫到可以照著做的粒度。scripts/examples/http_mock_server.py,不需要任何外部服務。白名單出廠是空的,記得把測試伺服器的位址用 IP 寫法加進去(loopback 屬私有範圍,主機名寫法放不行);ExternalWait 另需要先在主機設定填「系統對外網址」。| 項目 | 現況 |
|---|---|
| 秘密儲存 | 沒有。HttpRequest 的標頭值在流程圖裡;需要時由平台另做設定組與加密,不在這兩個節點的範圍 |
| DNS 重綁定 | 解析 IP 與實際連線之間仍有短暫時間差,節點不做連線層 IP 釘選。高敏感環境請在白名單只寫 IP 或 CIDR |
| 迴圈節點 | 沒有。那是引擎層的變更,不是加一個節點能解決的 |
| 逾時出線 | ExternalWait 刻意不做「逾時走哪條線」的設定,結果一律寫變數、由分支分路,與 HttpRequest 一致,處理器也不必讀流程圖 |