chundev
日期:2026-04-27 技術棧:Cortex XSOAR (Demisto)、Python 3 automation、JavaScript automation、HttpV2、DT (Demisto Transform)
寫一個 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 黏合劑」的組合。常見開發模式:
HttpV2 發 HTTP、用 Set / SetToJSON 暫存、用 Print 渲染、用 closeInvestigation 結案這次的需求很簡單:incident 進來後撈底層事件 → 從 ArcSight/SIEM 拿到來源/目的 IP → 呼叫資產管理 RESTful API 反查 → 把資產資訊渲染到 Print 跟 closeNotes。
聽起來無聊,實際上每一步都踩了 XSOAR 的隱性坑。
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。
想在 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 |
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 的「魔法」展開語意。
EntryContext return 會把 string 包成 arrayDedupAndJoin 內部 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。
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 | JavaScript |
|---|---|---|
| 取 args | args = demisto.args(); args.get('x') |
args.x 或 args['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)。
XSOAR 沒有 stdout。demisto.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 護網建好,後續維護成本低很多。
DeleteContext 或立即 Set 隔離uniq 對 nested-path array 不可靠——對 array of objects 的 nested field 想去重,自己寫 JS 處理EntryContext return 會包 array——要穩定寫 context,用 executeCommand('Set', {append: 'false'})demisto. 前綴——executeCommand, args, logDebug 都是 global,沒有命名空間append=false 強制覆蓋| [Extend Context | Cortex XSOAR](https://xsoar.pan.dev/docs/playbooks/playbooks-extend-context) — 多次執行 command 並把 output 存到不同 key 的官方建議做法 |
type: javascript 的 yml