Kusto 그래프 의미론(Kusto Graph Semantics)
표 형식 데이터로부터 일시적 그래프와 영구 그래프를 KQL 그래프 연산자를 사용하여 구축합니다. 이 스킬은 자연어를 엣지 우선 그래프 구성 패턴과 그래프 쿼리 연산자로 변환합니다.
활성화 트리거(Activation Triggers)
사용자가 다음과 같은 요청을 할 때 이 스킬을 사용하세요:
- 표 형식 데이터로 그래프를 구축하려는 경우 (
make-graph) - 데이터에서 패턴, 경로 또는 관계를 찾으려는 경우
graph-match,graph-shortest-paths,graph-to-table,graph-mark-components를 언급하는 경우- 영구 그래프 모델 또는 스냅샷을 만들려는 경우
- "그래프 구축", "최단 경로 찾기", "연결 구성 요소 찾기", "관계 표시"라고 말하는 경우
- 일시적 그래프와 영구 그래프의 차이에 대해 묻는 경우
자연어-대-KQL 변환기가 아닙니다. 입력은 일반적으로 사용자가 그래프로 변환하려는 결과를 가진 작동 중인 KQL 쿼리와 원하는 그래프 구조에 대한 자연어 설명이어야 합니다. 기본적인 자연어 소스 요청은 알려진 테이블과 명확한 열에 직접 매핑되는 경우에만 지원됩니다. 일반적인 자연어-대-KQL 변환에는 전용 쿼리 생성 스킬(별도 제공)을 사용하세요.
보완 스킬:
azure-kusto-irql-- 그래프의 표 형식 입력을 생성하는 구성 가능한 보안 쿼리 기본 요소azure-kusto-irql-graph-- Kusto Explorer에서 풍부한 형식과 아이콘으로 장식된 그래프를 위한 IRQL의Lift_To_GraphJSON 매핑 시스템
엣지 우선 접근법(The Edges-First Approach)
Kusto에서 그래프를 구축하기 위한 기본 패턴:
1. 엣지(EDGES) 정의 -> src --> dest, 관계 유형/속성 포함
2. 노드 조회(NODE LOOKUPS) 정의 -> 각 노드 ID에 대한 표시 이름, 유형, 속성
3. 엣지 유형 유니온 -> 여러 관계 유형이 있는 경우
4. 노드 조회 유니온 -> 여러 노드 유형이 있는 경우
5. make-graph 호출 -> edges | make-graph Source --> Target with nodes on nodeId
이것이 make-graph로 생각하는 방식입니다. 엣지는 여러분이 관심 있는 관계입니다. 노드는 해당 ID에 얼굴(표시 이름, 유형, 속성)을 부여하는 조회 테이블입니다.
그래프 연산자 참조(Graph Operators Reference)
make-graph -- 테이블에서 그래프 구축
Edges | make-graph SourceId --> TargetId with Nodes on NodeId
Edges: 각 행이 엣지인 표 형식 소스SourceId --> TargetId: 소스 및 대상 노드 ID를 포함하는 열with Nodes on NodeId: ID로 조인되는 선택적 노드 속성 테이블- 여러 노드 테이블 지원:
with Nodes1 on Id1, Nodes2 on Id2 - 엣지에 나타나지만 노드 테이블에 없는 노드는 빈 속성을 얻습니다.
graph-match -- 패턴 찾기
G | graph-match (a)-[e]->(b) where <constraints> project <output>
패턴 표기법:
| 요소 | 명명됨 | 익명 |
|---|---|---|
| 노드 | (n) | () |
| 엣지 왼쪽->오른쪽 | -[e]-> | --> |
| 엣지 오른쪽->왼쪽 | <-[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 -- 최단 경로 찾기
G | graph-shortest-paths (start)-[e*1..20]->(end)
where start.name == "Alice" and end.name == "Server01"
project Path = e, Length = array_length(e)
- 최소 하나의 가변 길이 엣지 필요
output = any(기본값, 쌍당 하나의 경로) 또는output = all(동일 길이의 모든 최단 경로)- 가변 길이 엣지 속성은 동적 배열로 반환됨
graph-to-table -- 그래프를 테이블로 내보내기
G | graph-to-table nodes // 노드 내보내기
G | graph-to-table edges // 엣지 내보내기
G | graph-to-table nodes as N, edges as E // 둘 다 내보내기
G | graph-to-table nodes with_node_id=Id // 노드 해시 ID 포함
G | graph-to-table edges with_source_id=Src with_target_id=Tgt // 엣지 끝점 ID 포함
graph-mark-components -- 연결 구성 요소 찾기
G | graph-mark-components with_component_id=ComponentId
| graph-to-table nodes
| summarize Members = make_list(name) by ComponentId
각 노드에 ComponentId를 할당합니다. 동일한 연결 구성 요소에 있는 노드는 동일한 ID를 공유합니다.
graph() 함수 -- 영구 그래프 쿼리
graph("MyGraphModel") // 최신 스냅샷
graph("MyGraphModel", "Snapshot_2025_01") // 특정 스냅샷
graph("MyGraphModel", true) // 모델 정의에서 일시적 그래프
일시적 그래프(Transient Graphs)
쿼리 실행 중 동적으로 생성됩니다. 설정이 필요 없습니다. 임시 분석, 탐색 및 프로토타입 제작에 이상적입니다.
템플릿: 기본 두 엔터티 그래프
// 1. 엣지 정의
let edges = <SourceTable>
| summarize <aggregations> by SourceCol, TargetCol;
// 2. 노드 조회 정의
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. 그래프 구축 및 쿼리
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>
템플릿: 다중 관계 그래프
// 여러 엣지 유형 -> 공통 스키마로 유니온
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;
// 모든 소스의 노드 조회
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)
대규모 재사용 가능 그래프용. 데이터베이스 메타데이터에 저장됩니다. 기록 비교를 위한 스냅샷을 지원합니다.
안전: 그래프 모델 또는 스냅샷을 생성하거나 변경하면 데이터베이스가 수정됩니다. 항상 정확한 명령을 표시하고
.create-or-alter graph_model또는.make graph_snapshot을 실행하기 전에 사용자 확인을 받으세요.
1단계: 그래프 모델 생성
.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"]
}
]
}
}
2단계: 스냅샷 생성
.make graph_snapshot SecurityGraph Snapshot_2025_07
3단계: 스냅샷 쿼리
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을 자동으로 실행하지 마세요. 항상 정확한 명령, 클러스터, 데이터베이스 및 영향을 받는 개체를 표시한 다음 실행 전에 명시적인 사용자 확인을 요구하세요.
.show graph_models // 모든 모델 나열
.show graph_model SecurityGraph // 모델 세부 정보 표시
.show graph_snapshots SecurityGraph // 스냅샷 나열
.drop graph_snapshot SecurityGraph Snapshot_2025_07 // 스냅샷 삭제 (먼저 확인)
.drop graph_model SecurityGraph // 모델 및 모든 스냅샷 삭제 (먼저 확인)
일시적 vs 영구: 언제 무엇을 사용할까
| 요소 | 일시적 (make-graph) | 영구 (graph()) |
|---|---|---|
| 설정 | 없음 -- 쿼리 인라인 | 모델 + 스냅샷 생성 |
| 수명 | 쿼리 실행 중에만 | 데이터베이스 메타데이터에 저장 |
| 데이터 신선도 | 항상 최신 | 생성 시점의 스냅샷 |
| 규모 | 쿼리 메모리로 제한 | 엔터프라이즈 규모 |
| 재사용 | 매 쿼리마다 재구축 | 사용자/쿼리 간 공유 |
| 최적 용도 | 임시 헌팅, 프로토타입 제작 | 프로덕션 워크플로우, 대시보드 |
보안 및 위협 헌팅 예제
인증 그래프: 누가 어디서 무엇에 로그인했는가
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 엣지
let ip_user = auth_edges
| project Source = src_ip, Target = username, logins, fails;
// User -> Host 엣지
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
측면 이동 탐지: 손상된 호스트를 공유하는 사용자
// 패턴: (user1)-[auth1]->(host)<-[auth2]-(user2)
// 동일한 호스트에서 실패하는 두 사용자 = 가능한 자격 증명 스프레이
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
최단 공격 경로
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
연결 구성 요소: 격리된 클러스터 찾기
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의 대화형 그래프 시각화 창이 트리거됩니다.
edges
| make-graph Source --> Target with all_nodes on nodeId
// <- 여기서 중지. Kusto Explorer가 그래프를 시각적으로 렌더링합니다.
대시보드 또는 내보내기를 위해 다시 테이블로 평탄화하려면 graph-match | project 또는 graph-to-table을 통해 파이프하세요.
IRQL과 함께 사용하기
보안 데이터로 작업할 때는 azure-kusto-irql 스킬의 IRQL 선택기(Get_*)를 데이터 소스로 사용하는 것을 고려하세요. IRQL은 원시 테이블 이름이나 열 매핑을 기억하지 않고도 통합 스키마를 제공합니다. 아이콘과 노드 접기를 통한 풍부한 시각화를 위해서는 azure-kusto-irql-graph 스킬의 Lift_To_Graph가 더 빠른 경로입니다.
| 접근법 | 최적 용도 |
|---|---|
원시 make-graph (이 스킬) | 완전한 제어, 영구 모델, 최단 경로, 연결 구성 요소, 사용자 정의 스키마 |
Lift_To_Graph (azure-kusto-irql-graph) | Kusto Explorer에서 빠른 아이콘 장식 시각화, 노드 접기 |
IRQL Get_* -> make-graph | 입력으로 IRQL의 통합 스키마, 분석을 위한 원시 그래프 연산자 |
IRQL Get_* -> Lift_To_Graph -> Graph_Render_View | 질문에서 시각적 그래프까지 가장 빠른 경로 |
참고:
Lift_To_Graph,Graph_Render_View및Graph_Fold_By_Property는 기본 제공 연산자가 아닌 저장된 함수입니다. kc7001 예제 클러스터에 사전 배포되어 있지만 다른 클러스터에는 배포가 필요할 수 있습니다. 함수 정의 및 배포 지침은azure-kusto-irql-graph/references/DEPLOY_IRQL_FUNCTIONS.md를 참조하세요.
예제: IRQL 선택기 -> make-graph -> 최단 경로
IRQL이 데이터 검색을 처리하고, make-graph가 그래프를 처리합니다.