Share Notes

chundev

View the Project on GitHub latteouka/share-notes

Cortex XSOAR Automation 開發踩坑筆記 — Python 與 JavaScript runtime 的隱形差異

日期:2026-04-27 技術棧:Cortex XSOAR (Demisto)、Python 3 automation、JavaScript automation、HttpV2、DT (Demisto Transform)


TL;DR

寫一個 SIEM playbook 整合內網資產 RESTful API,從 v1(純 HttpV2)一路 debug 到 v2(Python automation + JavaScript helper),踩了五個 XSOAR 官方文件不會講的坑:HttpV2 context 累積、DT 箭頭函數不執行、transformer uniq 對 nested array 不可靠、JS automation 的 EntryContext 自動包成 array、以及 JS runtime 沒有 demisto. 前綴(裸 executeCommand)。本文整理踩坑經過、根因、修法,以及 Python vs JavaScript automation 的 API cheat sheet。


背景

XSOAR (前身 Demisto) 的 playbook 是「task DAG + script 黏合劑」的組合。常見開發模式:

  1. Playbook tasks:用內建 HttpV2 發 HTTP、用 Set / SetToJSON 暫存、用 Print 渲染、用 closeInvestigation 結案
  2. Automation script:用 Python 或 JavaScript 寫 helper,負責 parse/dedupe/render 等 playbook 不擅長的事

這次的需求很簡單:incident 進來後撈底層事件 → 從 ArcSight/SIEM 拿到來源/目的 IP → 呼叫資產管理 RESTful API 反查 → 把資產資訊渲染到 Print 跟 closeNotes。

聽起來無聊,實際上每一步都踩了 XSOAR 的隱性坑。


踩坑 1:HttpV2 的 HttpRequest context 在多次呼叫間會累積

症狀

playbook 連續發兩次 HttpV2(查來源 IP、查目的 IP),用 ${HttpRequest.Response.Body} 抓回應,結果目的查詢拿到的是來源的回應

根因

XSOAR HttpV2 把回應寫到固定 context key HttpRequest.Response.Body。但第二次呼叫不會覆蓋,會 append 成陣列

第 1 次 HttpV2 → HttpRequest = [{Response: {Body: 來源回應}}]
讀 HttpRequest.Response.Body → "來源回應" ✓

第 2 次 HttpV2 → HttpRequest = [
  {Response: {Body: 來源回應}},
  {Response: {Body: 目的回應}}
]
讀 HttpRequest.Response.Body → ["來源回應", "目的回應"]   ← 變陣列!
傳給 SetToJSON 取第一筆 → 拿到「來源回應」  ← 完全錯誤

修法

每次 HttpV2 之前用內建 DeleteContext script 清掉:

- name: 清空 HttpRequest context
  scriptName: DeleteContext
  scriptarguments:
    key:
      simple: HttpRequest

- name: HttpV2 查目的 IP
  scriptName: HttpV2
  ...

或者更穩的做法:HttpV2 完立刻 Set / SetToJSON 把當下回應抓到專屬 context key,後續一律用專屬 key 而不是 HttpRequest


踩坑 2:DT (Demisto Transform) 的 inline 箭頭函數不被執行

症狀

想在 Print 模板裡 inline 去除 array 重複:

${ArcSightESM.SecurityEvents.destination.userName=val.filter((v,i,a)=>a.indexOf(v)===i).join(", ")}

預期渲染 user@domain.com,實際渲染:

["user@domain.com","user@domain.com","user@domain.com",..."user@domain.com"]

filter 跟 join 完全沒被執行,array 直接 raw serialize 成 JSON 字串塞進去。

根因(推測)

XSOAR 的 DT 引擎對 ES6 箭頭函數 (v,i,a) => ... 的支援不穩定(runtime 是 Goja 之類的有限 JS subset)。同樣的邏輯改寫成 ES5 function(v,i,a){return a.indexOf(v)===i;} 也不一定 work,因為 inline 的 =expression 整段在 DT runtime 的處理是另一套機制。

修法

放棄 inline DT,改用以下其中一種:

方案 評價
transformer: uniq + join 對「array of strings」可能 work,對「nested-path 展開的 array」不可靠(見踩坑 3)
寫 JavaScript automation 處理 最穩,純 JS Array 操作 deterministic

踩坑 3:transformer uniq 對 nested-path 展開的 array 不可靠

症狀

用 XSOAR 內建 transformer 去重 array of objects 的 nested field:

value:
  complex:
    root: SecurityEvents.destination.userName
    transformers:
    - operator: uniq
    - operator: join

預期 100 筆重複 user 變一筆,實際沒去重

根因

SecurityEvents 是 array of objects:

[
  {"destination": {"userName": "user@x"}, ...},
  {"destination": {"userName": "user@x"}, ...},
  ...
]

DT path SecurityEvents.destination.userName 對 array of objects 的 nested field 展開時,不會自動 flatten 成純 string array——可能展開成 [["user@x"],["user@x"],...] 或保留複合結構,uniq 對非標量陣列就不認識。

修法

寫 JavaScript automation DedupAndJoin,把 array 收進 JS 自己 flatten + dedupe:

var input = args.input;
// XSOAR 把 array 透過 simple: ${...} 傳進來時通常 serialize 成 JSON 字串
if (typeof input === 'string') {
    try { input = JSON.parse(input); }
    catch (e) { input = input.split(',').map(function (s) { return s.trim(); }); }
}

var arr = Array.isArray(input) ? input : [input];

// flatten 一層(防 array of arrays)
var flat = [];
arr.forEach(function (item) {
    if (Array.isArray(item)) item.forEach(function (s) { flat.push(s); });
    else if (item) flat.push(item);
});

// dedupe,保序
var seen = {}, unique = [];
flat.forEach(function (v) {
    var k = String(v);
    if (!(k in seen)) { seen[k] = true; unique.push(v); }
});

var result = unique.map(String).join(args.separator || ', ');

JS 操作可預測、不依賴 XSOAR DT 的「魔法」展開語意。


踩坑 4:JS automation 的 EntryContext return 會把 string 包成 array

症狀

DedupAndJoin 內部 result = "10.x.x.x"(純字串),用 EntryContext return 寫進 context:

return {
    EntryContext: { 'UniqueDestIPs': result }
};

下游 task 讀 ${UniqueDestIPs},預期拿到 10.x.x.x實際拿到 ["10.x.x.x"](被包成 array 的 JSON 字串)。然後串接給其他 task 用就完全壞掉。

根因

XSOAR 對 automation entry 的 EntryContext default 行為是「append 模式」:把每個 entry 的 output 視為「該 key 的一次新值」,多次寫入會累積成 array。即使是第一次寫入,某些 XSOAR 版本對 JS runtime 也會包成 array。

修法

不用 EntryContext return,改用內建 Set script 強制覆蓋:

executeCommand('Set', {
    key: outputKey,
    value: result,
    append: 'false',     // 強制覆蓋
    stringify: 'false'   // 不轉 JSON
});

return {
    ContentsFormat: formats.text,
    Type: entryTypes.note,
    Contents: 'DedupAndJoin: ' + flat.length + '' + unique.length + ' items'
};

Set 是 XSOAR 內建 script,行為穩定且明確支援 append=false


踩坑 5:JavaScript automation 沒有 demisto. 前綴

症狀

JS automation 寫 demisto.executeCommand('Set', {...}),跑起來爆:

ReferenceError: demisto is not defined

根因

demisto 物件只在 Python automation/integration runtime 裡存在。JavaScript runtime 用一組獨立的 globals。

社群討論跟官方範例(demisto/content 的 JS automation)顯示 JS automation 的 globals:

全域 用途
args 對應 Python 的 demisto.args()
executeCommand(name, args) 對應 Python 的 demisto.executeCommand(...)
incidents 取當前 incident 陣列
isError(entry) 檢查 executeCommand 回傳是否錯誤
dq(obj, path) 對應 demisto 的 deep query
formats formats.text, formats.json, formats.markdown
entryTypes entryTypes.note, entryTypes.error
logDebug(msg) / logInfo(msg) 對應 Python 的 demisto.debug / demisto.info

XSOAR 文件主要寫 Python,JS runtime 行為要靠實測或翻 demisto/content 的 JS sample


Python vs JavaScript automation Cheat Sheet

動作 Python JavaScript
取 args args = demisto.args(); args.get('x') args.xargs['x'](global)
執行 command demisto.executeCommand('Set', {...}) executeCommand('Set', {...})
Logging demisto.debug(msg) logDebug(msg)
寫 context(穩定覆蓋) return_results(CommandResults(outputs_prefix=..., outputs={...})) executeCommand('Set', {append:'false', stringify:'false'})
Entry types entryTypes['note'] entryTypes.note(global)
Format formats['text'] formats.text(global)
Helper argToBoolean(), tableToMarkdown(), return_error() 沒有對應,要自己寫
跑在哪 docker container(每次新 process) XSOAR engine(同 HttpV2 的 runtime)
網路堆疊 container network(可能跟 XSOAR engine 不同 routing) host network(跟 HttpV2 一樣)

重要含義:如果你的 automation 要打內網 API,且發現 timeout/connection 有問題,改寫成 JavaScript 通常能解,因為 JS automation 跟 HttpV2 跑在同一個 runtime,不會踩到 docker container 的網路差異(例如 corporate proxy 被自動 inherit 進 container 但 engine 自己 bypass)。


Debug 黃金法則:把所有 raw response + 環境資訊灌進 outputs

XSOAR 沒有 stdoutdemisto.debug 寫的 log 要開 debug mode 才看得到,而且不容易抓出來。

可靠的做法是所有可疑 metadata 都灌進 automation outputs,留在 incident context 永久保留:

return_results(CommandResults(
    outputs_prefix='DRIPAsset',
    outputs={
        # 業務資料
        'Assets': unique_assets,
        'Count': len(unique_assets),

        # === Debug metadata 全部留在 context ===
        'RawResponses': [{
            'IP': ip,
            'HTTPStatus': status_code,
            'ElapsedSec': round(elapsed, 2),
            'Attempts': attempts,
            'BodyPreview': (raw_text or '')[:500],
        } for ip in to_query],
        'Errors': errors,
        'Diagnostic': {
            'BypassProxy': bypass_proxy,
            'ProxyEnv': {k: os.environ.get(k, '') for k in proxy_keys},
            'TCPProbe': {'Reachable': tcp_ok, 'ElapsedSec': tcp_elapsed},
        },
    },
    readable_output=...,
))

這樣未來真的踩到新坑,使用者去 incident 的 Context Data 就能看到完整證據——不用每次都把 log 截圖回來給維護的人猜。

對「內網部署、deploy 要 USB 來回」的環境特別重要:每多一輪「貼 log 給 me、me 想一想、改 code、再 USB 過去」都是時間成本。一次把 debug 護網建好,後續維護成本低很多。


學到的事


參考資料