azure-kusto-graph
用 edges 優先的方式建 Kusto graph,做模式比對、最短路徑和連通元件
中文翻譯
---
name: azure-kusto-graph
description: "用自然語言建立並查詢 Kusto graph。涵蓋暫時性 graph(make-graph)、持久化 graph model/snapshot、模式比對(graph-match)、最短路徑、連通元件,以及 graph 匯出成 table。產生「edges 優先」的思考方式:定義 edges、定義 node lookup、union、make-graph。WHEN: make-graph, graph-match, graph-shortest-paths, graph-to-table, graph-mark-components, persistent graph, graph model, graph snapshot, build a graph from data, find paths between nodes, pattern matching in graph, connected components, transient graph, Kusto graph, KQL graph."
license: MIT
metadata:
author: Microsoft
version: "1.2.1"
---
# Kusto Graph 語意
用 KQL 的 graph 運算子,從表格資料建立暫時性和持久化的 graph。這個 skill 把自然語言轉成「edges 優先」的 graph 建構模式,以及 graph 查詢運算子。
## 啟動條件
使用者有以下情況時用這個 skill:
- 想從表格資料建立 graph(`make-graph`)
- 要找資料裡的模式、路徑或關係
- 提到 `graph-match`、`graph-shortest-paths`、`graph-to-table`、`graph-mark-components`
- 想建立持久化的 graph model 或 snapshot
- 說「build a graph」、「find the shortest path」、「find connected components」、「show relationships」
- 問暫時性 graph 和持久化 graph 的差別
**這不是自然語言轉 KQL 的轉換器。** 輸入通常應該是一個能跑的 KQL 查詢(使用者想把它的結果轉成 graph),加上一段用自然語言描述想要的 graph 結構。只有在能直接對應到已知 table、欄位又很明顯時,才支援基本的自然語言來源需求。一般的自然語言轉 KQL,請用專門的查詢產生 skill(另外提供)。
**搭配的 skill:**
- `azure-kusto-irql`:可組合的資安查詢基本元件,產出 graph 要用的表格輸入
- `azure-kusto-irql-graph`:IRQL 的 `Lift_To_Graph` JSON 對應系統,用於在 Kusto Explorer 裡做出型別豐富、帶圖示的 graph
## Edges 優先的做法
在 Kusto 裡建 graph 的基本模式:
```
1. Define your EDGES -> src --> dest, with relationship type/properties
2. Define your NODE LOOKUPS -> display names, types, properties for each node ID
3. Union edge types -> if you have multiple relationship types
4. Union node lookups -> if you have multiple node types
5. Call make-graph -> edges | make-graph Source --> Target with nodes on nodeId
```
用 `make-graph` 就是這樣思考。Edges 是你在意的關係。Nodes 是 lookup table,替那些 ID 配上臉:顯示名稱、型別、屬性。
## Graph 運算子參考
### `make-graph`:從 table 建立 graph
```kql
Edges | make-graph SourceId --> TargetId with Nodes on NodeId
```
- `Edges`:表格來源,每一列是一條 edge
- `SourceId --> TargetId`:放來源和目標 node ID 的欄位
- `with Nodes on NodeId`:選用的 node 屬性 table,依 ID join
- 支援多個 node table:`with Nodes1 on Id1, Nodes2 on Id2`
- 出現在 edges 裡、但 node table 裡沒有的 node,屬性會是空的
### `graph-match`:找模式
```kql
G | graph-match (a)-[e]->(b) where <constraints> project <output>
```
模式寫法:
| 元素 | 具名 | 匿名 |
|---|---|---|
| Node | `(n)` | `()` |
| Edge 左到右 | `-[e]->` | `-->` |
| Edge 右到左 | `<-[e]-` | `<--` |
| 任一方向 | `-[e]-` | `--` |
| 可變長度 | `-[e*1..5]->` | `-[*1..5]->` |
多跳模式:`(a)-[e1]->(b)-[e2]->(c)`
星狀模式:`(a)--(center)--(b), (c)--(center)--(d)`
環的控制:`cycles = all | none | unique_edges`(預設:`unique_edges`)
### `graph-shortest-paths`:找最短路徑
```kql
G | graph-shortest-paths (start)-[e*1..20]->(end)
where start.name == "Alice" and end.name == "Server01"
project Path = e, Length = array_length(e)
```
- 至少要有一條可變長度的 edge
- `output = any`(預設,每一對回傳一條路徑)或 `output = all`(所有同樣長度的最短路徑)
- 可變長度 edge 的屬性會以 dynamic array 回傳
### `graph-to-table`:把 graph 匯出成 table
```kql
G | graph-to-table nodes // 匯出 nodes
G | graph-to-table edges // 匯出 edges
G | graph-to-table nodes as N, edges as E // 兩個都匯出
G | graph-to-table nodes with_node_id=Id // 包含 node 的 hash ID
G | graph-to-table edges with_source_id=Src with_target_id=Tgt // 包含 edge 兩端的 ID
```
### `graph-mark-components`:找連通元件
```kql
G | graph-mark-components with_component_id=ComponentId
| graph-to-table nodes
| summarize Members = make_list(name) by ComponentId
```
幫每個 node 指派一個 `ComponentId`。同一個連通元件裡的 node 會有相同的 ID。
### `graph()` 函式:查詢持久化 graph
```kql
graph("MyGraphModel") // 最新的 snapshot
graph("MyGraphModel", "Snapshot_2025_01") // 指定的 snapshot
graph("MyGraphModel", true) // 依 model 定義建立暫時性 graph
```
## 暫時性 Graph
在查詢執行期間動態建立。不用事先設定。適合臨時分析、探索和做原型。
### 範本:基本的雙實體 graph
```kql
// 1. 定義 edges
let edges = <SourceTable>
| summarize <aggregations> by SourceCol, TargetCol;
// 2. 定義 node lookup
let source_nodes = edges
| distinct SourceCol
| project nodeId = SourceCol, label = SourceCol, nodeType = "<SourceType>";
let target_nodes = edges
| distinct TargetCol
| project nodeId = TargetCol, label = TargetCol, nodeType = "<TargetType>";
let all_nodes = union source_nodes, target_nodes;
// 3. 建立 graph 並查詢
edges
| make-graph SourceCol --> TargetCol with all_nodes on nodeId
| graph-match (s)-[e]->(t)
where <constraints>
project Source = s.label, Target = t.label, <edge properties>
```
### 範本:多種關係的 graph
```kql
// 多種 edge 類型 -> 用共同的 schema 把它們 union 起來
let auth_edges = AuthEvents
| project Source = username, Target = hostname, edgeType = "authenticates", ts = timestamp;
let net_edges = NetworkEvents
| project Source = src_ip, Target = url, edgeType = "connects", ts = timestamp;
let all_edges = union auth_edges, net_edges;
// 從所有來源建立 node lookup
let user_nodes = Employees | project nodeId = username, label = name, nodeType = "User";
let host_nodes = AuthEvents | distinct hostname | project nodeId = hostname, label = hostname, nodeType = "Host";
let all_nodes = union user_nodes, host_nodes;
all_edges
| make-graph Source --> Target with all_nodes on nodeId
```
## 持久化 Graph
用於大規模、可重複使用的 graph。存在資料庫的 metadata 裡。支援 snapshot,可以做歷史比對。
> **安全:** 建立或修改 graph model 和 snapshot 會改動資料庫。執行 `.create-or-alter graph_model` 或 `.make graph_snapshot` 之前,一定要先顯示完整指令並跟使用者確認。
### Step 1:建立 graph model
```kql
.create-or-alter graph_model SecurityGraph
{
"Schema": {
"Nodes": {
"User": {"name": "string", "role": "string"},
"Host": {"hostname": "string"},
"IP": {"ip": "string"}
},
"Edges": {
"AuthenticatesTo": {"timestamp": "datetime", "result": "string"},
"ConnectsFrom": {"timestamp": "datetime"}
}
},
"Definition": {
"Steps": [
{
"Kind": "AddNodes",
"Query": "Employees | project name, role",
"NodeIdColumn": "name",
"Labels": ["User"]
},
{
"Kind": "AddNodes",
"Query": "AuthenticationEvents | distinct hostname | project hostname",
"NodeIdColumn": "hostname",
"Labels": ["Host"]
},
{
"Kind": "AddEdges",
"Query": "AuthenticationEvents | project username, hostname, timestamp, result",
"SourceColumn": "username",
"TargetColumn": "hostname",
"Labels": ["AuthenticatesTo"]
}
]
}
}
```
### Step 2:建立 snapshot
```kql
.make graph_snapshot SecurityGraph Snapshot_2025_07
```
### Step 3:查詢 snapshot
```kql
graph("SecurityGraph")
| graph-match (user)-[auth]->(host)
where user.role == "Admin" and auth.result == "Failed Login"
project User = user.name, Host = host.hostname, Time = auth.timestamp
```
### 管理指令
> **安全:** 底下所有控制指令都會修改或刪除資料庫物件。絕對不准自動執行 `.drop`、`.create-or-alter graph_model` 或 `.make graph_snapshot`。一定要先顯示完整指令、叢集、資料庫和受影響的物件,取得使用者明確確認後才能執行。
```kql
.show graph_models // 列出所有 model
.show graph_model SecurityGraph // 顯示 model 詳細內容
.show graph_snapshots SecurityGraph // 列出 snapshot
.drop graph_snapshot SecurityGraph Snapshot_2025_07 // 刪除 snapshot(先確認!)
.drop graph_model SecurityGraph // 刪除 model 和所有 snapshot(先確認!)
```
## 暫時性 vs 持久化:何時用哪個
| 因素 | 暫時性(`make-graph`) | 持久化(`graph()`) |
|---|---|---|
| 設定 | 不用,直接寫在查詢裡 | 要建立 model + snapshot |
| 生命週期 | 只在查詢執行期間 | 存在資料庫 metadata 裡 |
| 資料新鮮度 | 永遠是最新的 | 是建立當下的 snapshot |
| 規模 | 受查詢記憶體限制 | 企業等級 |
| 重複使用 | 每次查詢都重建 | 使用者/查詢之間共用 |
| 最適合 | 臨時的 hunt、做原型 | 正式環境的工作流程、dashboard |
## 資安與威脅獵捕範例
### 認證 graph:誰從哪裡登入了什麼
```kql
let auth_edges = AuthenticationEvents
| summarize
logins = count(),
fails = countif(result == "Failed Login")
by src_ip, username, hostname;
let ip_nodes = auth_edges | distinct src_ip
| project nodeId = src_ip, label = src_ip, nodeType = "IP";
let user_nodes = auth_edges | distinct username
| project nodeId = username, label = username, nodeType = "User";
let host_nodes = auth_edges | distinct hostname
| project nodeId = hostname, label = hostname, nodeType = "Host";
let all_nodes = union ip_nodes, user_nodes, host_nodes;
// IP -> User 的 edges
let ip_user = auth_edges
| project Source = src_ip, Target = username, logins, fails;
// User -> Host 的 edges
let user_host = auth_edges
| project Source = username, Target = hostname, logins, fails;
union ip_user, user_host
| make-graph Source --> Target with all_nodes on nodeId
| graph-match (ip)-[e1]->(user)-[e2]->(host)
where e2.fails > 20
project
IP = ip.label,
User = user.label,
Host = host.label,
Failures = e2.fails
| order by Failures desc
```
### 橫向移動偵測:使用者共用被入侵的主機
```kql
// 模式:(user1)-[auth1]->(host)<-[auth2]-(user2)
// 兩個使用者都在同一台主機上登入失敗 = 可能是 credential spray
let edges = AuthenticationEvents
| summarize fails = countif(result == "Failed Login"), logins = count()
by username, hostname;
let nodes = union
(edges | distinct username | project nodeId = username, nodeType = "User"),
(edges | distinct hostname | project nodeId = hostname, nodeType = "Host");
edges
| make-graph username --> hostname with nodes on nodeId
| graph-match (u1)-[e1]->(h)<-[e2]-(u2)
where u1.nodeId != u2.nodeId and e1.fails > 10 and e2.fails > 10
project
User1 = u1.nodeId, User2 = u2.nodeId,
SharedHost = h.nodeId,
User1Fails = e1.fails, User2Fails = e2.fails
| distinct User1, SharedHost, User2, User1Fails, User2Fails
| order by User1Fails + User2Fails desc
```
### 最短攻擊路徑
```kql
let edges = SecurityEvents
| project Source = source_entity, Target = target_entity, action, timestamp;
let nodes = union
(edges | distinct Source | project nodeId = Source),
(edges | distinct Target | project nodeId = Target);
edges
| make-graph Source --> Target with nodes on nodeId
| graph-shortest-paths (start)-[e*1..10]->(end)
where start.nodeId == "ExternalIP_1.2.3.4" and end.nodeId == "DatabaseServer"
project
PathLength = array_length(e),
Actions = e.action,
Hops = e.Target
```
### 連通元件:找出孤立的群集
```kql
let edges = NetworkFlows
| project Source = src_ip, Target = dst_ip;
let nodes = union
(edges | distinct Source | project nodeId = Source),
(edges | distinct Target | project nodeId = Target);
edges
| make-graph Source --> Target with nodes on nodeId
| graph-mark-components with_component_id = ComponentId
| graph-to-table nodes
| summarize Members = make_list(nodeId), Size = count() by ComponentId
| order by Size desc
```
### 在 Kusto Explorer 裡視覺化
查詢在 `make-graph` 就結束(後面不要接 `graph-match`),就會觸發 Kusto Explorer 的互動式 graph 視覺化視窗:
```kql
edges
| make-graph Source --> Target with all_nodes on nodeId
// <- 停在這裡。Kusto Explorer 會把 graph 畫出來。
```
要攤平回 table 給 dashboard 或匯出用,就接 `graph-match | project` 或 `graph-to-table`。
## 搭配 IRQL 使用
處理資安資料時,可以考慮用 `azure-kusto-irql` skill 裡的 IRQL selector(`Get_*`)當資料來源。IRQL 給你統一的 schema,不用背原始的 table 名稱或欄位對應。要有圖示和 node 折疊的豐富視覺化,用 `azure-kusto-irql-graph` skill 的 `Lift_To_Graph` 比較快。
| 做法 | 最適合 |
|---|---|
| 原始 `make-graph`(這個 skill) | 完全掌控、持久化 model、最短路徑、連通元件、自訂 schema |
| `Lift_To_Graph`(`azure-kusto-irql-graph`) | 在 Kusto Explorer 快速做出帶圖示的視覺化、node 折疊 |
| IRQL `Get_*` -> `make-graph` | 用 IRQL 的統一 schema 當輸入,再用原始 graph 運算子分析 |
| IRQL `Get_*` -> `Lift_To_Graph` -> `Graph_Render_View` | 從問題到視覺化 graph 最快的路 |
> **注意:** `Lift_To_Graph`、`Graph_Render_View` 和 `Graph_Fold_By_Property` 是 stored function,不是內建運算子。它們已預先部署在 kc7001 範例叢集上,其他叢集可能需要自己部署。function 定義和部署說明見 `azure-kusto-irql-graph/references/DEPLOY_IRQL_FUNCTIONS.md`。
### 範例:IRQL selector -> make-graph -> 最短路徑
IRQL 負責取資料;`make-graph` 負責 graph 分析。這個範例找出從外部 IP 經過認證事件到郵件伺服器的最短路徑:
```kql
// IRQL 提供統一的欄位(ClientIp, Hostname, Username, Result)
let auth = Get_Event_Authentication_All
| where Result == "Failed Login";
let edges = auth
| summarize Failures = count() by ClientIp, Hostname;
let nodes = union
(edges | distinct ClientIp | project nodeId = ClientIp, nodeType = "IP"),
(edges | distinct Hostname | project nodeId = Hostname, nodeType = "Host");
edges
| make-graph ClientIp --> Hostname with nodes on nodeId
| graph-shortest-paths (src)-[e*1..5]->(dest)
where src.nodeType == "IP" and dest.nodeId == "MAIL-SERVER01"
project
SourceIP = src.nodeId,
PathLength = array_length(e),
Hops = e.Hostname
```
### 範例:IRQL selector -> make-graph -> 連通元件
找出彼此相連的 IP 和網域群集,這些可能是 C2 基礎設施:
```kql
let dns = Get_Dns_All;
let edges = dns | project Source = ClientIp, Target = Domain;
let nodes = union
(edges | distinct Source | project nodeId = Source, nodeType = "IP"),
(edges | distinct Target | project nodeId = Target, nodeType = "Domain");
edges
| make-graph Source --> Target with nodes on nodeId
| graph-mark-components with_component_id = ComponentId
| graph-to-table nodes
| summarize
IPs = make_set_if(nodeId, nodeType == "IP"),
Domains = make_set_if(nodeId, nodeType == "Domain"),
Size = count()
by ComponentId
| where Size > 3
| order by Size desc
```
### 範例:IRQL + make-graph 整合
結合 IRQL selector 和 `make-graph` 的多來源調查 graph,以及 `Lift_To_Graph` 的視覺化 graph 範例,見 [references/EXAMPLES.md](references/EXAMPLES.md)。
## 實際使用情境
完整的實作範例見 [references/SCENARIOS.md](references/SCENARIOS.md),包括:
- 可達性分析(到關鍵資產的最短路徑)
- 網路分段驗證(連通元件)
- 被入侵帳號的爆炸半徑(可變長度路徑比對)
- 給 SOC 團隊的持久化 graph model(graph_model + snapshot)
## 使用的 MCP 工具
| 工具 | 用途 |
|------|---------|
| `kusto_query` | 執行 KQL 查詢,包括 `make-graph`、`graph-match` 和管理指令 |
| `kusto_table_schema_get` | 建立 edge/node 投影之前,先查 table 有哪些欄位 |
| `kusto_cluster_list` | 列出可用的 ADX 叢集 |
| `kusto_database_list` | 列出叢集裡的資料庫 |
## 在 Kusto Explorer 裡開啟查詢(僅限 Windows)
> **選用的便利功能。** 預設流程是在對話裡輸出 KQL,讓使用者自己複製到 Kusto Explorer 或 VS Code 的 Kusto 擴充套件。自動啟動只在使用者選擇啟用時才做。
### 預設:在對話裡輸出 KQL
一律輸出完整的 KQL,並清楚標示 Step 1(連線)和 Step 2(查詢):
```
// Step 1: Connect to your cluster (skip if already connected)
// Example: uncomment to connect to the KC7 training cluster
// #connect cluster('kc7001.eastus.kusto.windows.net').database('ValdyTimes')
// Or replace with your own cluster:
// #connect cluster('<YOUR_CLUSTER>').database('<YOUR_DATABASE>')
// Step 2: Run the query below
<KQL_QUERY ending at make-graph>
```
然後緊接著在下面輸出一個 **ADX Web Explorer 版本**,在後面加上 `| graph-to-table nodes as N, edges as E`,因為 ADX Web Explorer 沒辦法直接渲染 `make-graph`:
```
// ADX Web Explorer version (tabular output):
<SAME_QUERY>
| graph-to-table nodes as N, edges as E
```
這樣輸出在 Kusto Explorer(graph 視覺化)和 ADX Web Explorer(表格結果)都能用,使用者不用改任何東西。
### 選用:儲存並啟動
如果使用者要求把查詢存起來或在 Kusto Explorer 裡開啟,照 [references/KUSTO_EXPLORER_LAUNCH.md](references/KUSTO_EXPLORER_LAUNCH.md) 的流程做。重點規則:
- 寫檔或啟動執行檔之前,**一定要**用 `ask_user` 確認
- **一定要**把檔案內容顯示在對話裡,讓使用者開啟前能先檢查
- **絕對不准**用 shell 插值或 here-string,檔案要用 `Set-Content`/`Add-Content` 寫
- **絕對不准**把查詢編碼進瀏覽器 URL
- 在 macOS/Linux 上,存成 `.kql` 檔,然後建議用 VS Code 的 Kusto 擴充套件或 ADX Web Explorer
`make-graph` 的視覺化(graph 視窗)要求查詢**結束在 `make-graph`**,不要接 `graph-match`。Kusto Explorer 只有在輸出是 graph 物件(不是 table)時,才會開 graph 視覺化視窗。英文原文
---
name: azure-kusto-graph
description: "Build and query Kusto graphs from natural language. Covers transient graphs (make-graph), persistent graph models/snapshots, pattern matching (graph-match), shortest paths, connected components, and graph-to-table export. Generates the edges-first thinking: define edges, define node lookups, union, make-graph. WHEN: make-graph, graph-match, graph-shortest-paths, graph-to-table, graph-mark-components, persistent graph, graph model, graph snapshot, build a graph from data, find paths between nodes, pattern matching in graph, connected components, transient graph, Kusto graph, KQL graph."
license: MIT
metadata:
author: Microsoft
version: "1.2.1"
---
# Kusto Graph Semantics
Build transient and persistent graphs from tabular data using KQL graph operators. This skill translates natural language into the edges-first graph construction pattern and graph query operators.
## Activation Triggers
Use this skill when the user:
- Wants to build a graph from tabular data (`make-graph`)
- Asks to find patterns, paths, or relationships in data
- Mentions `graph-match`, `graph-shortest-paths`, `graph-to-table`, `graph-mark-components`
- Wants to create a persistent graph model or snapshot
- Says "build a graph", "find the shortest path", "find connected components", "show relationships"
- Asks about transient vs persistent graphs
**Not a natural-language-to-KQL converter.** The input should generally be a working KQL query whose results the user wants converted to a graph, plus a natural-language description of the desired graph structure. Basic NL source requests are supported only when they map directly to a known table with obvious columns. For general NL-to-KQL conversion, use a dedicated query-generation skill (available separately).
**Complementary skills:**
- `azure-kusto-irql` -- composable security query primitives that produce the tabular inputs for graphs
- `azure-kusto-irql-graph` -- IRQL's `Lift_To_Graph` JSON mapping system for richly-typed, icon-decorated graphs in Kusto Explorer
## The Edges-First Approach
The fundamental pattern for building graphs in Kusto:
```
1. Define your EDGES -> src --> dest, with relationship type/properties
2. Define your NODE LOOKUPS -> display names, types, properties for each node ID
3. Union edge types -> if you have multiple relationship types
4. Union node lookups -> if you have multiple node types
5. Call make-graph -> edges | make-graph Source --> Target with nodes on nodeId
```
This is how to think in `make-graph`. Edges are the relationships you care about. Nodes are lookup tables that give those IDs a face -- display names, types, properties.
## Graph Operators Reference
### `make-graph` -- Build a graph from tables
```kql
Edges | make-graph SourceId --> TargetId with Nodes on NodeId
```
- `Edges`: tabular source where each row is an edge
- `SourceId --> TargetId`: columns containing source and target node IDs
- `with Nodes on NodeId`: optional node property table joined by ID
- Supports multiple node tables: `with Nodes1 on Id1, Nodes2 on Id2`
- Nodes appearing in edges but missing from the node table get empty properties
### `graph-match` -- Find patterns
```kql
G | graph-match (a)-[e]->(b) where <constraints> project <output>
```
Pattern notation:
| Element | Named | Anonymous |
|---|---|---|
| Node | `(n)` | `()` |
| Edge left->right | `-[e]->` | `-->` |
| Edge right->left | `<-[e]-` | `<--` |
| Any direction | `-[e]-` | `--` |
| Variable length | `-[e*1..5]->` | `-[*1..5]->` |
Multi-hop patterns: `(a)-[e1]->(b)-[e2]->(c)`
Star patterns: `(a)--(center)--(b), (c)--(center)--(d)`
Cycles control: `cycles = all | none | unique_edges` (default: `unique_edges`)
### `graph-shortest-paths` -- Find shortest paths
```kql
G | graph-shortest-paths (start)-[e*1..20]->(end)
where start.name == "Alice" and end.name == "Server01"
project Path = e, Length = array_length(e)
```
- Requires at least one variable-length edge
- `output = any` (default, one path per pair) or `output = all` (all equal-length shortest paths)
- Variable-length edge properties returned as dynamic arrays
### `graph-to-table` -- Export graph to tables
```kql
G | graph-to-table nodes // export nodes
G | graph-to-table edges // export edges
G | graph-to-table nodes as N, edges as E // export both
G | graph-to-table nodes with_node_id=Id // include node hash ID
G | graph-to-table edges with_source_id=Src with_target_id=Tgt // include edge endpoint IDs
```
### `graph-mark-components` -- Find connected components
```kql
G | graph-mark-components with_component_id=ComponentId
| graph-to-table nodes
| summarize Members = make_list(name) by ComponentId
```
Assigns a `ComponentId` to each node. Nodes in the same connected component share the same ID.
### `graph()` function -- Query persistent graphs
```kql
graph("MyGraphModel") // latest snapshot
graph("MyGraphModel", "Snapshot_2025_01") // specific snapshot
graph("MyGraphModel", true) // transient from model definition
```
## Transient Graphs
Created dynamically during query execution. No setup required. Ideal for ad-hoc analysis, exploration, and prototyping.
### Template: Basic two-entity graph
```kql
// 1. Define edges
let edges = <SourceTable>
| summarize <aggregations> by SourceCol, TargetCol;
// 2. Define node lookups
let source_nodes = edges
| distinct SourceCol
| project nodeId = SourceCol, label = SourceCol, nodeType = "<SourceType>";
let target_nodes = edges
| distinct TargetCol
| project nodeId = TargetCol, label = TargetCol, nodeType = "<TargetType>";
let all_nodes = union source_nodes, target_nodes;
// 3. Build and query the graph
edges
| make-graph SourceCol --> TargetCol with all_nodes on nodeId
| graph-match (s)-[e]->(t)
where <constraints>
project Source = s.label, Target = t.label, <edge properties>
```
### Template: Multi-relationship graph
```kql
// Multiple edge types -> union them with a common schema
let auth_edges = AuthEvents
| project Source = username, Target = hostname, edgeType = "authenticates", ts = timestamp;
let net_edges = NetworkEvents
| project Source = src_ip, Target = url, edgeType = "connects", ts = timestamp;
let all_edges = union auth_edges, net_edges;
// Node lookups from all sources
let user_nodes = Employees | project nodeId = username, label = name, nodeType = "User";
let host_nodes = AuthEvents | distinct hostname | project nodeId = hostname, label = hostname, nodeType = "Host";
let all_nodes = union user_nodes, host_nodes;
all_edges
| make-graph Source --> Target with all_nodes on nodeId
```
## Persistent Graphs
For large-scale, reusable graphs. Stored in database metadata. Support snapshots for historical comparison.
> **Safety:** Creating or altering graph models and snapshots modifies the database. Always show the exact command and confirm with the user before executing `.create-or-alter graph_model` or `.make graph_snapshot`.
### Step 1: Create a graph model
```kql
.create-or-alter graph_model SecurityGraph
{
"Schema": {
"Nodes": {
"User": {"name": "string", "role": "string"},
"Host": {"hostname": "string"},
"IP": {"ip": "string"}
},
"Edges": {
"AuthenticatesTo": {"timestamp": "datetime", "result": "string"},
"ConnectsFrom": {"timestamp": "datetime"}
}
},
"Definition": {
"Steps": [
{
"Kind": "AddNodes",
"Query": "Employees | project name, role",
"NodeIdColumn": "name",
"Labels": ["User"]
},
{
"Kind": "AddNodes",
"Query": "AuthenticationEvents | distinct hostname | project hostname",
"NodeIdColumn": "hostname",
"Labels": ["Host"]
},
{
"Kind": "AddEdges",
"Query": "AuthenticationEvents | project username, hostname, timestamp, result",
"SourceColumn": "username",
"TargetColumn": "hostname",
"Labels": ["AuthenticatesTo"]
}
]
}
}
```
### Step 2: Create a snapshot
```kql
.make graph_snapshot SecurityGraph Snapshot_2025_07
```
### Step 3: Query the snapshot
```kql
graph("SecurityGraph")
| graph-match (user)-[auth]->(host)
where user.role == "Admin" and auth.result == "Failed Login"
project User = user.name, Host = host.hostname, Time = auth.timestamp
```
### Management commands
> **Safety:** All control commands below modify or delete database objects. Never execute `.drop`, `.create-or-alter graph_model`, or `.make graph_snapshot` automatically. Always show the exact command, cluster, database, and affected object, then require explicit user confirmation before execution.
```kql
.show graph_models // list all models
.show graph_model SecurityGraph // show model details
.show graph_snapshots SecurityGraph // list snapshots
.drop graph_snapshot SecurityGraph Snapshot_2025_07 // delete a snapshot (CONFIRM FIRST)
.drop graph_model SecurityGraph // delete model and all snapshots (CONFIRM FIRST)
```
## Transient vs Persistent: When to Use Which
| Factor | Transient (`make-graph`) | Persistent (`graph()`) |
|---|---|---|
| Setup | None -- inline in query | Create model + snapshot |
| Lifetime | Query execution only | Stored in database metadata |
| Data freshness | Always current | Snapshot at creation time |
| Scale | Limited by query memory | Enterprise-scale |
| Reuse | Rebuilt every query | Shared across users/queries |
| Best for | Ad-hoc hunts, prototyping | Production workflows, dashboards |
## Security & Threat Hunting Examples
### Authentication graph: who logged into what from where
```kql
let auth_edges = AuthenticationEvents
| summarize
logins = count(),
fails = countif(result == "Failed Login")
by src_ip, username, hostname;
let ip_nodes = auth_edges | distinct src_ip
| project nodeId = src_ip, label = src_ip, nodeType = "IP";
let user_nodes = auth_edges | distinct username
| project nodeId = username, label = username, nodeType = "User";
let host_nodes = auth_edges | distinct hostname
| project nodeId = hostname, label = hostname, nodeType = "Host";
let all_nodes = union ip_nodes, user_nodes, host_nodes;
// IP -> User edges
let ip_user = auth_edges
| project Source = src_ip, Target = username, logins, fails;
// User -> Host edges
let user_host = auth_edges
| project Source = username, Target = hostname, logins, fails;
union ip_user, user_host
| make-graph Source --> Target with all_nodes on nodeId
| graph-match (ip)-[e1]->(user)-[e2]->(host)
where e2.fails > 20
project
IP = ip.label,
User = user.label,
Host = host.label,
Failures = e2.fails
| order by Failures desc
```
### Lateral movement detection: users sharing compromised hosts
```kql
// Pattern: (user1)-[auth1]->(host)<-[auth2]-(user2)
// Two users both failing on the same host = possible credential spray
let edges = AuthenticationEvents
| summarize fails = countif(result == "Failed Login"), logins = count()
by username, hostname;
let nodes = union
(edges | distinct username | project nodeId = username, nodeType = "User"),
(edges | distinct hostname | project nodeId = hostname, nodeType = "Host");
edges
| make-graph username --> hostname with nodes on nodeId
| graph-match (u1)-[e1]->(h)<-[e2]-(u2)
where u1.nodeId != u2.nodeId and e1.fails > 10 and e2.fails > 10
project
User1 = u1.nodeId, User2 = u2.nodeId,
SharedHost = h.nodeId,
User1Fails = e1.fails, User2Fails = e2.fails
| distinct User1, SharedHost, User2, User1Fails, User2Fails
| order by User1Fails + User2Fails desc
```
### Shortest attack path
```kql
let edges = SecurityEvents
| project Source = source_entity, Target = target_entity, action, timestamp;
let nodes = union
(edges | distinct Source | project nodeId = Source),
(edges | distinct Target | project nodeId = Target);
edges
| make-graph Source --> Target with nodes on nodeId
| graph-shortest-paths (start)-[e*1..10]->(end)
where start.nodeId == "ExternalIP_1.2.3.4" and end.nodeId == "DatabaseServer"
project
PathLength = array_length(e),
Actions = e.action,
Hops = e.Target
```
### Connected components: find isolated clusters
```kql
let edges = NetworkFlows
| project Source = src_ip, Target = dst_ip;
let nodes = union
(edges | distinct Source | project nodeId = Source),
(edges | distinct Target | project nodeId = Target);
edges
| make-graph Source --> Target with nodes on nodeId
| graph-mark-components with_component_id = ComponentId
| graph-to-table nodes
| summarize Members = make_list(nodeId), Size = count() by ComponentId
| order by Size desc
```
### Visualize in Kusto Explorer
End a query at `make-graph` (without piping to `graph-match`) to trigger Kusto Explorer's interactive graph visualization window:
```kql
edges
| make-graph Source --> Target with all_nodes on nodeId
// <- stop here. Kusto Explorer renders the graph visually.
```
To flatten back to a table for dashboards or export, pipe through `graph-match | project` or `graph-to-table`.
## Using with IRQL
When working with security data, consider using IRQL selectors (`Get_*`) from the `azure-kusto-irql` skill as the data source. IRQL gives you a unified schema without memorizing raw table names or column mappings. For rich visualization with icons and node folding, the `azure-kusto-irql-graph` skill's `Lift_To_Graph` is the faster path.
| Approach | Best For |
|---|---|
| Raw `make-graph` (this skill) | Full control, persistent models, shortest paths, connected components, custom schemas |
| `Lift_To_Graph` (`azure-kusto-irql-graph`) | Quick icon-decorated visualization in Kusto Explorer, node folding |
| IRQL `Get_*` -> `make-graph` | IRQL's unified schema as input, then raw graph operators for analysis |
| IRQL `Get_*` -> `Lift_To_Graph` -> `Graph_Render_View` | Fastest path from question to visual graph |
> **Note:** `Lift_To_Graph`, `Graph_Render_View`, and `Graph_Fold_By_Property` are stored functions, not built-in operators. They are pre-deployed on the kc7001 example cluster but may need deployment on other clusters. See `azure-kusto-irql-graph/references/DEPLOY_IRQL_FUNCTIONS.md` for function definitions and deployment instructions.
### Example: IRQL selectors -> make-graph -> shortest path
IRQL handles the data retrieval; `make-graph` handles the graph analysis. This finds the shortest path from an external IP to a mail server through auth events:
```kql
// IRQL provides unified columns (ClientIp, Hostname, Username, Result)
let auth = Get_Event_Authentication_All
| where Result == "Failed Login";
let edges = auth
| summarize Failures = count() by ClientIp, Hostname;
let nodes = union
(edges | distinct ClientIp | project nodeId = ClientIp, nodeType = "IP"),
(edges | distinct Hostname | project nodeId = Hostname, nodeType = "Host");
edges
| make-graph ClientIp --> Hostname with nodes on nodeId
| graph-shortest-paths (src)-[e*1..5]->(dest)
where src.nodeType == "IP" and dest.nodeId == "MAIL-SERVER01"
project
SourceIP = src.nodeId,
PathLength = array_length(e),
Hops = e.Hostname
```
### Example: IRQL selectors -> make-graph -> connected components
Find clusters of IPs and domains that are interconnected -- potential C2 infrastructure:
```kql
let dns = Get_Dns_All;
let edges = dns | project Source = ClientIp, Target = Domain;
let nodes = union
(edges | distinct Source | project nodeId = Source, nodeType = "IP"),
(edges | distinct Target | project nodeId = Target, nodeType = "Domain");
edges
| make-graph Source --> Target with nodes on nodeId
| graph-mark-components with_component_id = ComponentId
| graph-to-table nodes
| summarize
IPs = make_set_if(nodeId, nodeType == "IP"),
Domains = make_set_if(nodeId, nodeType == "Domain"),
Size = count()
by ComponentId
| where Size > 3
| order by Size desc
```
### Example: IRQL + make-graph integration
See [references/EXAMPLES.md](references/EXAMPLES.md) for multi-source investigation graphs combining IRQL selectors with `make-graph`, and `Lift_To_Graph` visual graph examples.
## Practical Usage Scenarios
See [references/SCENARIOS.md](references/SCENARIOS.md) for full worked examples including:
- Reachability analysis (shortest paths to critical assets)
- Network segmentation validation (connected components)
- Blast radius of compromised accounts (variable-length path matching)
- Persistent graph models for SOC teams (graph_model + snapshots)
## MCP Tools Used
| Tool | Purpose |
|------|---------|
| `kusto_query` | Execute KQL queries including `make-graph`, `graph-match`, and management commands |
| `kusto_table_schema_get` | Discover table columns before building edge/node projections |
| `kusto_cluster_list` | List available ADX clusters |
| `kusto_database_list` | List databases in a cluster |
## Opening Queries in Kusto Explorer (Windows Only)
> **Optional convenience feature.** The default workflow is to output the KQL in chat and let the user copy it into Kusto Explorer or the VS Code Kusto extension manually. Auto-launch is opt-in only.
### Default: Output KQL in Chat
Always output the complete KQL with Step 1 (connect) and Step 2 (query) clearly labeled:
```
// Step 1: Connect to your cluster (skip if already connected)
// Example: uncomment to connect to the KC7 training cluster
// #connect cluster('kc7001.eastus.kusto.windows.net').database('ValdyTimes')
// Or replace with your own cluster:
// #connect cluster('<YOUR_CLUSTER>').database('<YOUR_DATABASE>')
// Step 2: Run the query below
<KQL_QUERY ending at make-graph>
```
Then immediately below, output an **ADX Web Explorer version** that appends `| graph-to-table nodes as N, edges as E` since ADX Web Explorer cannot render `make-graph` directly:
```
// ADX Web Explorer version (tabular output):
<SAME_QUERY>
| graph-to-table nodes as N, edges as E
```
This ensures the output works in both Kusto Explorer (graph visualization) and ADX Web Explorer (tabular results) without the user having to modify anything.
### Optional: Save and Launch
If the user asks to save or open the query in Kusto Explorer, follow the procedure in [references/KUSTO_EXPLORER_LAUNCH.md](references/KUSTO_EXPLORER_LAUNCH.md). Key rules:
- **Always** use `ask_user` to confirm before writing files or launching executables
- **Always** display the file contents in chat so the user can review before opening
- **Never** use shell interpolation or here-strings — write files via `Set-Content`/`Add-Content`
- **Never** encode queries into browser URLs
- On macOS/Linux, save the `.kql` file and suggest the VS Code Kusto extension or ADX Web Explorer
For `make-graph` visualization (the graph window), the query must **end at `make-graph`** — do not pipe to `graph-match`. Kusto Explorer only opens the graph visualization window when the output is a graph object, not a table.