FIRE

technical note / Azure Monitor ブック

Splunk の SimpleXML ダッシュボードを Azure Monitor ブックへ移植する

SIEM を Splunk から Microsoft Sentinel へ寄せるとき、検知ルールより先に詰まるのがダッシュボードの移植です。Splunk の SimpleXML と Azure Monitor ブック(Workbook)はどちらも「宣言的なダッシュボード定義」ですが、 粒度も思想もそれなりに違います。

この記事では、Salesforce の設定監査証跡(SetupAuditTrail)を可視化するダッシュボードを 題材に、SimpleXML の各要素をブックの JSON でどう表現するかを整理します。 解説に使うブック定義は記事末尾に全文を掲載しているので、そのままコピーして動かしながら読めます。

1. まず対応関係を押さえる

移植作業のほとんどは、次の対応表に沿った機械的な置き換えになります。

Splunk SimpleXMLAzure Monitor ブック
<input type="time">パラメータ type: 4(時間範囲ピッカー)+ セル側の timeContextFromParameter
<input type="dropdown">パラメータ type: 2 + 選択肢を jsonData に列挙
<single>(単一値パネル)visualization: "tiles"tileSettings
<drilldown> / $click.value$exportedParameters + 受け手セルの conditionalVisibility
timechart span=1dsummarize count() by bin(TimeGenerated, 1d), <分割列>
lookup(CSV ルックアップ)_GetWatchlist('<名前>') lookup kind=leftouter
eval _time = _time + 32400(JST 補正)クエリでは何もしない。表示側の「時間の設定 → タイム ゾーン」で切り替える

とくに最後の行は重要で、Splunk 時代の秒足し算はブックに持ち込まないのが正解です。ブックはクエリ結果の datetime を表示時にタイムゾーン変換するので、クエリ内で +9 時間しておくと二重にずれます。

2. 共通の前処理は各セルの先頭に置く

ブックはセル単位で独立したクエリを投げるため、Splunk の保存済み検索のように 共通処理を 1 箇所へ切り出す仕組みがありません(関数化するなら Log Analytics 側に保存済み関数を作る必要があります)。評価版レベルでは、「環境依存の定義」を各クエリの先頭に固めて貼るのが手っ取り早く、書き換え漏れも起きにくくなります。

// ===== 環境依存の定義(ここだけ書き換える)=====
let SetupAuditTrail = SalesforceServiceCloudV2_CL;
let SfdcUser = _GetWatchlist('sfdc_user')
    | project UserId = tostring(UserId), UserDisplayName = tostring(Name),
              Username = tostring(Username), ProfileId = tostring(ProfileId);
let SfdcProfile = _GetWatchlist('sfdc_profile')
    | project ProfileId = tostring(ProfileId), Permition = tostring(Name);
// Profile の Name は権限を表すため、User の Name と区別してリネームしておく
let Enriched = SetupAuditTrail
    | lookup kind=leftouter SfdcUser on $left.CreatedById == $right.UserId
    | lookup kind=leftouter SfdcProfile on ProfileId;

ポイントは 3 つです。

  • ウォッチリストの列は必ず tostring() する_GetWatchlist() の戻りは動的型を含むため、そのまま lookup のキーにすると型不一致で結合できないことがあります。
  • 列名の衝突は project の時点で潰す: ユーザー表もプロファイル表も Name を持つので、 後者を Permition にリネームしています。衝突したまま結合すると Name1 のような自動採番列ができ、後段の可読性が一気に落ちます。
  • 時刻列の実体を最初に確認するTimeGenerated が「イベント発生時刻」ではなく「Log Analytics への取り込み時刻」になっている コネクタは珍しくありません。その場合は元データ側の列で差し替えます。
// TimeGenerated が取り込み時刻だった場合の差し替え
let SetupAuditTrail = SalesforceServiceCloudV2_CL
    | extend TimeGenerated = todatetime(CreatedDate);

3. パラメータに「KQL の式そのもの」を入れる

移植で一番効くテクニックがこれです。ブックのパラメータは {ParamName} でクエリ文字列に展開されるだけなので、値として条件式を丸ごと持たせられます。 ドロップダウンの選択肢を jsonData に書くとこうなります。

{
  "id": "p-adminuser",
  "version": "KqlParameterItem/1.0",
  "name": "AdminUser",
  "label": "管理者/非管理者",
  "type": 2,
  "isRequired": true,
  "typeSettings": { "showDefault": false },
  "jsonData": "[{\"value\": \"true\", \"label\": \"両方を表示\", \"selected\": true}, {\"value\": \"Permition contains \\\"管理\\\"\", \"label\": \"管理者権限のみ表示\"}, {\"value\": \"Permition !contains \\\"管理\\\"\", \"label\": \"非管理者権限のみ表示\"}]",
  "value": "true"
}

クエリ側は展開位置を 1 行用意するだけです。

Enriched
| where {AdminUser}

「両方を表示」のときの値が true になっているのがミソで、| where true は全件通過するため、条件を外すケースを分岐なしで表現できます。 SimpleXML の <condition> でパネルごとに検索文を分岐させていた箇所は、 だいたいこの形に畳めます。

※ 当然ながら、これはクエリ文字列への値の直接埋め込みです。 選択肢を jsonData の固定リストに限定している限り安全ですが、 自由入力テキスト(type: 1)の値を同じように where 句へ展開するのは避けてください。

4. 複数選択を 1 本の正規表現に畳む

「特定の操作ログを除外したい」という要件は、複数選択パラメータと matches regex の組み合わせで処理できます。delimiter|quote に空文字を指定すると、 選択した値が A|B|C という正規表現の形で展開されます。

{
  "id": "p-exclude",
  "name": "ExcludeDisplay",
  "label": "除外するDisplay",
  "type": 2,
  "isRequired": true,
  "multiSelect": true,
  "quote": "",
  "delimiter": "|",
  "jsonData": "[{\"value\": \"$^\", \"label\": \"なし\", \"selected\": true}, {\"value\": \"に割り当て\", \"label\": \"*に割り当て*\"}, {\"value\": \"を無効化\", \"label\": \"*を無効化*\"}]",
  "value": ["$^"]
}
| where not(Display matches regex "{ExcludeDisplay}")

「なし」を選んだときの値 $^行末の直後に行頭が来ることを要求する、決してマッチしない正規表現です。isRequired: true のパラメータは空値を許さないため、 「何も除外しない」を表す“無害な値”としてこれを置いています。 空文字を許すと matches regex "" が全件マッチしてしまい、 除外なしのつもりで全件消える、という事故になります。

5. ドリルダウンは「エクスポート」と「条件付き表示」で組む

SimpleXML の <drilldown> は、ブックでは 2 つの仕掛けの組み合わせになります。クリックされた行の値をパラメータへエクスポートする側と、そのパラメータが埋まったときだけ現れる側です。

まずタイル側(クリックされる方):

"visualization": "tiles",
"exportedParameters": [
  {
    "fieldName": "Count",
    "parameterName": "ShowTable1",
    "parameterType": 1,
    "defaultValue": ""
  }
]

次に詳細テーブル側(現れる方):

"conditionalVisibility": {
  "parameterName": "ShowTable1",
  "comparison": "isNotEqualTo",
  "value": ""
}

これで「タイルをクリックするまで詳細テーブルは非表示、クリックすると下に開く」 という Splunk 的な挙動になります。詳細テーブル側のクエリは、 集計前の Enriched に同じ絞り込みを掛けて project するだけです。

Enriched
| where Display contains "アドレス"
| where {AdminUser}
| sort by TimeGenerated desc
| project TimeGenerated, CreatedById,
          ["ユーザー名"] = UserDisplayName, ["ユーザー権限"] = Permition,
          Section, Display, DelegateUser

列名を ["日本語"] = 元の列 の形にすると、表示名だけ日本語化できます。ブックの列設定(gridSettings)でラベルを付ける方法もありますが、 クエリ側で済ませたほうが JSON が短く保てます。

6. 時系列グラフから「クリックしたバケット」へ降りる

timechart 相当は bin() で書きます。 集計間隔もパラメータにしておくと、Splunk の span 切り替えと同じ操作感になります。

Enriched
| where {LoginLogout}
| where not(Display matches regex "{ExcludeDisplay}")
| where {AdminUser}
| summarize Count = count() by bin(TimeGenerated, {Span}), Permition
| project TimeGenerated, Permition, Count
"visualization": "linechart",
"chartSettings": {
  "xAxis": "TimeGenerated",
  "yAxis": ["Count"],
  "group": "Permition",
  "showLegend": true
},
"exportedParameters": [
  { "fieldName": "TimeGenerated", "parameterName": "SelectedTime", "parameterType": 1, "defaultValue": "" },
  { "fieldName": "Permition",     "parameterName": "SelectedPermition", "parameterType": 1, "defaultValue": "" }
]

折れ線グラフの点をクリックすると、その点の X 値(バケット開始時刻)と系列名が エクスポートされます。受け側は「バケット開始時刻以上、開始時刻+間隔未満」で絞り込みます。{Span} はここでも同じ値が展開されるので、グラフと詳細で粒度がずれません。

| where TimeGenerated >= todatetime('{SelectedTime}')
    and TimeGenerated <  todatetime('{SelectedTime}') + {Span}
| where Permition == '{SelectedPermition}'

Splunk では case()round() でクリック時刻をバケット境界へ丸める処理を書くことがありますが、 ブックでは X 値がそのままバケット開始時刻なので、その手当ては不要です。

7. ブック定義の全文

ここまでの内容を含んだブック定義の全文です。Azure ポータルでMonitor(または Microsoft Sentinel)→ ブック → 新規 → 詳細エディターを開き、既定の内容をこれで丸ごと置き換えて「適用」すれば、 そのままダッシュボードとして動きます。

workbook.json
{
  "version": "Notebook/1.0",
  "items": [
    {
      "type": 1,
      "content": {
        "json": "## 【SetupAuditTrail】評価版ダッシュボード\nSplunk SimpleXML ダッシュボードの Azure Monitor ブック移植版。\n\n> **要確認:** ウォッチリスト名 (`sfdc_user` / `sfdc_profile`) と、`TimeGenerated` がイベント発生時刻かどうかを実環境に合わせて確認してください。\n> JST 表示は各クエリの「時間の設定 → タイム ゾーン: (UTC+09:00) 大阪、札幌、東京」で行います(Splunk の `_time+32400` 相当のハードコードは不要)。"
      },
      "name": "header"
    },
    {
      "type": 9,
      "content": {
        "version": "KqlParameterItem/1.0",
        "parameters": [
          {
            "id": "p-timerange",
            "version": "KqlParameterItem/1.0",
            "name": "TimeRange",
            "label": "監視期間",
            "type": 4,
            "isRequired": true,
            "typeSettings": {
              "selectableValues": [
                {
                  "durationMs": 3600000
                },
                {
                  "durationMs": 14400000
                },
                {
                  "durationMs": 86400000
                },
                {
                  "durationMs": 604800000
                },
                {
                  "durationMs": 2592000000
                }
              ],
              "allowCustom": true
            },
            "value": {
              "durationMs": 604800000
            }
          },
          {
            "id": "p-span",
            "version": "KqlParameterItem/1.0",
            "name": "Span",
            "label": "期間間隔",
            "type": 2,
            "isRequired": true,
            "typeSettings": {
              "showDefault": false
            },
            "jsonData": "[{\"value\": \"1d\", \"label\": \"日\", \"selected\": true}, {\"value\": \"1h\", \"label\": \"時間\"}, {\"value\": \"5m\", \"label\": \"5分\"}]",
            "value": "1d"
          },
          {
            "id": "p-adminuser",
            "version": "KqlParameterItem/1.0",
            "name": "AdminUser",
            "label": "管理者/非管理者",
            "type": 2,
            "isRequired": true,
            "typeSettings": {
              "showDefault": false
            },
            "jsonData": "[{\"value\": \"true\", \"label\": \"両方を表示\", \"selected\": true}, {\"value\": \"Permition contains \\\"管理\\\"\", \"label\": \"管理者権限のみ表示\"}, {\"value\": \"Permition !contains \\\"管理\\\"\", \"label\": \"非管理者権限のみ表示\"}]",
            "value": "true"
          }
        ],
        "style": "pills",
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces"
      },
      "name": "parameters - global"
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "// ===== 環境依存の定義(ここだけ書き換える)=====\nlet SetupAuditTrail = SalesforceServiceCloudV2_CL;\n// 時刻列: TimeGenerated が取り込み時刻になっている場合は\n//   | extend TimeGenerated = todatetime(CreatedDate)\n// を SetupAuditTrail の直後に挿入して差し替えること\nlet SfdcUser = _GetWatchlist('sfdc_user')\n    | project UserId = tostring(UserId), UserDisplayName = tostring(Name),\n              Username = tostring(Username), ProfileId = tostring(ProfileId);\nlet SfdcProfile = _GetWatchlist('sfdc_profile')\n    | project ProfileId = tostring(ProfileId), Permition = tostring(Name);\n// Profile の Name は権限を表すため、User の Name と区別して Permition にリネーム\nlet Enriched = SetupAuditTrail\n    | lookup kind=leftouter SfdcUser on $left.CreatedById == $right.UserId\n    | lookup kind=leftouter SfdcProfile on ProfileId;\nEnriched\n| where Display contains \"アドレス\"\n| where {AdminUser}\n| summarize Count = count()\n| extend Metric = \"IPアドレス変更に関するログの出力件数\"\n",
        "size": 0,
        "title": "IPアドレス変更に関するログの出力件数(※クリックで詳細にドリルダウン)",
        "timeContextFromParameter": "TimeRange",
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces",
        "visualization": "tiles",
        "exportedParameters": [
          {
            "fieldName": "Count",
            "parameterName": "ShowTable1",
            "parameterType": 1,
            "defaultValue": ""
          }
        ],
        "tileSettings": {
          "titleContent": {
            "columnMatch": "Metric",
            "formatter": 1
          },
          "leftContent": {
            "columnMatch": "Count",
            "formatter": 12,
            "formatOptions": {
              "palette": "auto"
            },
            "numberFormat": {
              "unit": 17,
              "options": {
                "maximumFractionDigits": 0
              }
            }
          },
          "showBorder": true
        }
      },
      "name": "query - count-ipchange",
      "customWidth": "50"
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "// ===== 環境依存の定義(ここだけ書き換える)=====\nlet SetupAuditTrail = SalesforceServiceCloudV2_CL;\n// 時刻列: TimeGenerated が取り込み時刻になっている場合は\n//   | extend TimeGenerated = todatetime(CreatedDate)\n// を SetupAuditTrail の直後に挿入して差し替えること\nlet SfdcUser = _GetWatchlist('sfdc_user')\n    | project UserId = tostring(UserId), UserDisplayName = tostring(Name),\n              Username = tostring(Username), ProfileId = tostring(ProfileId);\nlet SfdcProfile = _GetWatchlist('sfdc_profile')\n    | project ProfileId = tostring(ProfileId), Permition = tostring(Name);\n// Profile の Name は権限を表すため、User の Name と区別して Permition にリネーム\nlet Enriched = SetupAuditTrail\n    | lookup kind=leftouter SfdcUser on $left.CreatedById == $right.UserId\n    | lookup kind=leftouter SfdcProfile on ProfileId;\nEnriched\n| where Display matches regex @\"の.*プロファイル.*管理者.*に変更\"\n| where {AdminUser}\n| summarize Count = count()\n| extend Metric = \"一般ユーザーから管理者権限への昇格に関するログの出力件数\"\n",
        "size": 0,
        "title": "一般ユーザーから管理者権限への昇格に関するログの出力件数(※クリックで詳細にドリルダウン)",
        "timeContextFromParameter": "TimeRange",
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces",
        "visualization": "tiles",
        "exportedParameters": [
          {
            "fieldName": "Count",
            "parameterName": "ShowTable2",
            "parameterType": 1,
            "defaultValue": ""
          }
        ],
        "tileSettings": {
          "titleContent": {
            "columnMatch": "Metric",
            "formatter": 1
          },
          "leftContent": {
            "columnMatch": "Count",
            "formatter": 12,
            "formatOptions": {
              "palette": "auto"
            },
            "numberFormat": {
              "unit": 17,
              "options": {
                "maximumFractionDigits": 0
              }
            }
          },
          "showBorder": true
        }
      },
      "name": "query - count-escalation",
      "customWidth": "50"
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "// ===== 環境依存の定義(ここだけ書き換える)=====\nlet SetupAuditTrail = SalesforceServiceCloudV2_CL;\n// 時刻列: TimeGenerated が取り込み時刻になっている場合は\n//   | extend TimeGenerated = todatetime(CreatedDate)\n// を SetupAuditTrail の直後に挿入して差し替えること\nlet SfdcUser = _GetWatchlist('sfdc_user')\n    | project UserId = tostring(UserId), UserDisplayName = tostring(Name),\n              Username = tostring(Username), ProfileId = tostring(ProfileId);\nlet SfdcProfile = _GetWatchlist('sfdc_profile')\n    | project ProfileId = tostring(ProfileId), Permition = tostring(Name);\n// Profile の Name は権限を表すため、User の Name と区別して Permition にリネーム\nlet Enriched = SetupAuditTrail\n    | lookup kind=leftouter SfdcUser on $left.CreatedById == $right.UserId\n    | lookup kind=leftouter SfdcProfile on ProfileId;\nEnriched\n| where Display contains \"アドレス\"\n| where {AdminUser}\n| sort by TimeGenerated desc\n| project TimeGenerated, CreatedById,\n          [\"ユーザー名\"] = UserDisplayName, [\"ユーザー権限\"] = Permition,\n          Section, Display, DelegateUser\n",
        "size": 0,
        "title": "IPアドレス変更に関するログの詳細",
        "timeContextFromParameter": "TimeRange",
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces",
        "visualization": "table"
      },
      "name": "query - detail-ipchange",
      "conditionalVisibility": {
        "parameterName": "ShowTable1",
        "comparison": "isNotEqualTo",
        "value": ""
      }
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "// ===== 環境依存の定義(ここだけ書き換える)=====\nlet SetupAuditTrail = SalesforceServiceCloudV2_CL;\n// 時刻列: TimeGenerated が取り込み時刻になっている場合は\n//   | extend TimeGenerated = todatetime(CreatedDate)\n// を SetupAuditTrail の直後に挿入して差し替えること\nlet SfdcUser = _GetWatchlist('sfdc_user')\n    | project UserId = tostring(UserId), UserDisplayName = tostring(Name),\n              Username = tostring(Username), ProfileId = tostring(ProfileId);\nlet SfdcProfile = _GetWatchlist('sfdc_profile')\n    | project ProfileId = tostring(ProfileId), Permition = tostring(Name);\n// Profile の Name は権限を表すため、User の Name と区別して Permition にリネーム\nlet Enriched = SetupAuditTrail\n    | lookup kind=leftouter SfdcUser on $left.CreatedById == $right.UserId\n    | lookup kind=leftouter SfdcProfile on ProfileId;\nEnriched\n| where Display matches regex @\"の.*プロファイル.*管理者.*に変更\"\n| where {AdminUser}\n| sort by TimeGenerated desc\n| project TimeGenerated, CreatedById,\n          [\"ユーザー名\"] = UserDisplayName, [\"ユーザー権限\"] = Permition,\n          Section, Display, DelegateUser\n",
        "size": 0,
        "title": "一般ユーザーから管理者権限への昇格に関するログの詳細",
        "timeContextFromParameter": "TimeRange",
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces",
        "visualization": "table"
      },
      "name": "query - detail-escalation",
      "conditionalVisibility": {
        "parameterName": "ShowTable2",
        "comparison": "isNotEqualTo",
        "value": ""
      }
    },
    {
      "type": 9,
      "content": {
        "version": "KqlParameterItem/1.0",
        "parameters": [
          {
            "id": "p-loginlogout",
            "version": "KqlParameterItem/1.0",
            "name": "LoginLogout",
            "label": "ログイン/ログアウトの表示",
            "type": 2,
            "isRequired": true,
            "typeSettings": {
              "showDefault": false
            },
            "jsonData": "[{\"value\": \"true\", \"label\": \"含む\"}, {\"value\": \"Display !contains \\\"ログイン済み\\\" and Display !contains \\\"ログアウト済み\\\"\", \"label\": \"含まない\", \"selected\": true}]",
            "value": "Display !contains \"ログイン済み\" and Display !contains \"ログアウト済み\""
          },
          {
            "id": "p-exclude",
            "version": "KqlParameterItem/1.0",
            "name": "ExcludeDisplay",
            "label": "除外するDisplay",
            "type": 2,
            "isRequired": true,
            "multiSelect": true,
            "quote": "",
            "delimiter": "|",
            "typeSettings": {
              "showDefault": false
            },
            "jsonData": "[{\"value\": \"$^\", \"label\": \"なし\", \"selected\": true}, {\"value\": \"に割り当て\", \"label\": \"*に割り当て*\"}, {\"value\": \"新規ユーザ\", \"label\": \"*新規ユーザ*\"}, {\"value\": \"を無効化\", \"label\": \"*を無効化*\"}]",
            "value": [
              "$^"
            ]
          }
        ],
        "style": "pills",
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces"
      },
      "name": "parameters - chart"
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "// ===== 環境依存の定義(ここだけ書き換える)=====\nlet SetupAuditTrail = SalesforceServiceCloudV2_CL;\n// 時刻列: TimeGenerated が取り込み時刻になっている場合は\n//   | extend TimeGenerated = todatetime(CreatedDate)\n// を SetupAuditTrail の直後に挿入して差し替えること\nlet SfdcUser = _GetWatchlist('sfdc_user')\n    | project UserId = tostring(UserId), UserDisplayName = tostring(Name),\n              Username = tostring(Username), ProfileId = tostring(ProfileId);\nlet SfdcProfile = _GetWatchlist('sfdc_profile')\n    | project ProfileId = tostring(ProfileId), Permition = tostring(Name);\n// Profile の Name は権限を表すため、User の Name と区別して Permition にリネーム\nlet Enriched = SetupAuditTrail\n    | lookup kind=leftouter SfdcUser on $left.CreatedById == $right.UserId\n    | lookup kind=leftouter SfdcProfile on ProfileId;\nEnriched\n| where {LoginLogout}\n| where not(Display matches regex \"{ExcludeDisplay}\")\n| where {AdminUser}\n| summarize Count = count() by bin(TimeGenerated, {Span}), Permition\n| project TimeGenerated, Permition, Count\n",
        "size": 0,
        "title": "設定変更件数の時系列推移(※グラフをクリックで詳細にドリルダウン)",
        "timeContextFromParameter": "TimeRange",
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces",
        "visualization": "linechart",
        "exportedParameters": [
          {
            "fieldName": "TimeGenerated",
            "parameterName": "SelectedTime",
            "parameterType": 1,
            "defaultValue": ""
          },
          {
            "fieldName": "Permition",
            "parameterName": "SelectedPermition",
            "parameterType": 1,
            "defaultValue": ""
          }
        ],
        "chartSettings": {
          "xAxis": "TimeGenerated",
          "yAxis": [
            "Count"
          ],
          "group": "Permition",
          "createOtherGroup": null,
          "showLegend": true
        }
      },
      "name": "query - timechart"
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "// ===== 環境依存の定義(ここだけ書き換える)=====\nlet SetupAuditTrail = SalesforceServiceCloudV2_CL;\n// 時刻列: TimeGenerated が取り込み時刻になっている場合は\n//   | extend TimeGenerated = todatetime(CreatedDate)\n// を SetupAuditTrail の直後に挿入して差し替えること\nlet SfdcUser = _GetWatchlist('sfdc_user')\n    | project UserId = tostring(UserId), UserDisplayName = tostring(Name),\n              Username = tostring(Username), ProfileId = tostring(ProfileId);\nlet SfdcProfile = _GetWatchlist('sfdc_profile')\n    | project ProfileId = tostring(ProfileId), Permition = tostring(Name);\n// Profile の Name は権限を表すため、User の Name と区別して Permition にリネーム\nlet Enriched = SetupAuditTrail\n    | lookup kind=leftouter SfdcUser on $left.CreatedById == $right.UserId\n    | lookup kind=leftouter SfdcProfile on ProfileId;\nEnriched\n| where {LoginLogout}\n| where not(Display matches regex \"{ExcludeDisplay}\")\n| where {AdminUser}\n// チャートでクリックした時間バケット内に絞り込む(Splunk の case()/round() 相当は不要)\n| where TimeGenerated >= todatetime('{SelectedTime}')\n    and TimeGenerated <  todatetime('{SelectedTime}') + {Span}\n| where Permition == '{SelectedPermition}'\n| sort by TimeGenerated desc\n| project TimeGenerated, CreatedById,\n          [\"ユーザー名\"] = UserDisplayName, [\"ユーザー権限\"] = Permition,\n          Section, Display, DelegateUser\n",
        "size": 0,
        "title": "操作履歴の詳細情報",
        "timeContextFromParameter": "TimeRange",
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces",
        "visualization": "table"
      },
      "name": "query - detail-history",
      "conditionalVisibility": {
        "parameterName": "SelectedTime",
        "comparison": "isNotEqualTo",
        "value": ""
      }
    }
  ],
  "isLocked": false,
  "fallbackResourceIds": [],
  "$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json"
}

貼り付けたあとに環境に合わせて確認・変更する箇所は次の 3 つだけです。いずれも各クエリ先頭の「環境依存の定義」ブロックに集めてあります。

  • SalesforceServiceCloudV2_CL:ログの取り込み先テーブル名。 コネクタや取り込み方法によって変わります。
  • 'sfdc_user' / 'sfdc_profile': ウォッチリストのエイリアス。ユーザーマスタとプロファイルマスタを あらかじめ登録しておく必要があります。
  • TimeGenerated:これがイベント発生時刻かどうか。 取り込み時刻だった場合は前述の extend で差し替えます。

※ JST で表示したい場合は、各クエリの「時間の設定 → タイム ゾーン」で「(UTC+09:00) 大阪、札幌、東京」を選びます。 クエリ内で時刻を加算する必要はありません。

移植時に引っかかりやすい点

  • パラメータの評価順:エクスポートされたパラメータを参照するセルは、 初期表示時に空文字が入った状態でも一度評価されます。conditionalVisibility を付け忘れると、空値を todatetime() に渡してエラー表示になります。
  • 時間範囲は 2 系統ある:セルの timeContextFromParameter による絞り込みと、クエリ内の where TimeGenerated ... は別物です。両方効くので、 ドリルダウンの絞り込みが効かないときは前者の範囲を疑ってください。
  • ウォッチリストの規模_GetWatchlist() は大規模なマスタ向きではありません。ユーザーマスタが数万件を超えるなら、 カスタムテーブルへ取り込んで lookup するほうが安定します。
  • ロケール依存の文字列一致:サンプルの Display contains "アドレス" のような条件は、 ログの言語設定に依存します。テナントの言語が変わると無言で 0 件になるため、 移植後は必ず既知のイベントでヒットを確認してください。

まとめ

SimpleXML からブックへの移植は、「パラメータに条件式を持たせる」「エクスポート+条件付き表示でドリルダウンを作る」の 2 つを覚えれば、大半のパネルは素直に置き換えられます。 逆に、Splunk 側で検索文を分岐させて作り込んでいたダッシュボードほど、 ブックでは単純化できる余地が大きい、というのが移植してみての実感です。