Semántica de Grafos en Kusto
Construye grafos transitorios y persistentes a partir de datos tabulares usando operadores de grafos en KQL. Esta habilidad traduce lenguaje natural al patrón de construcción de grafos basado en aristas y operadores de consulta de grafos.
Disparadores de Activación
Usa esta habilidad cuando el usuario:
- Quiere construir un grafo a partir de datos tabulares (
make-graph) - Pide encontrar patrones, caminos o relaciones en los datos
- Menciona
graph-match,graph-shortest-paths,graph-to-table,graph-mark-components - Quiere crear un modelo de grafo persistente o una instantánea
- Dice "construye un grafo", "encuentra el camino más corto", "encuentra componentes conectados", "muestra relaciones"
- Pregunta sobre grafos transitorios vs. persistentes
No es un conversor de lenguaje natural a KQL. La entrada generalmente debe ser una consulta KQL funcional cuyos resultados el usuario quiere convertir a un grafo, más una descripción en lenguaje natural de la estructura de grafo deseada. Las solicitudes básicas en lenguaje natural solo se admiten cuando se asignan directamente a una tabla conocida con columnas obvias. Para la conversión general de NL a KQL, utiliza una habilidad de generación de consultas dedicada (disponible por separado).
Habilidades complementarias:
azure-kusto-irql-- primitivas de consulta de seguridad componibles que producen las entradas tabulares para los grafosazure-kusto-irql-graph-- el sistema de mapeo JSONLift_To_Graphde IRQL para grafos con tipos enriquecidos y decorados con iconos en Kusto Explorer
El Enfoque de Aristas Primero
El patrón fundamental para construir grafos en Kusto:
1. Define tus ARISTAS -> src --> dest, con tipo de relación/propiedades
2. Define tus BÚSQUEDAS DE NODOS -> nombres para mostrar, tipos, propiedades para cada ID de nodo
3. Une los tipos de aristas -> si tienes múltiples tipos de relación
4. Une las búsquedas de nodos -> si tienes múltiples tipos de nodo
5. Llama a make-graph -> edges | make-graph Source --> Target with nodes on nodeId
Así es como se piensa en make-graph. Las aristas son las relaciones que te importan. Los nodos son tablas de búsqueda que dan una cara a esos IDs: nombres para mostrar, tipos, propiedades.
Referencia de Operadores de Grafos
make-graph -- Construye un grafo a partir de tablas
Edges | make-graph SourceId --> TargetId with Nodes on NodeId
Edges: fuente tabular donde cada fila es una aristaSourceId --> TargetId: columnas que contienen los IDs de nodo origen y destinowith Nodes on NodeId: tabla opcional de propiedades de nodo unida por ID- Soporta múltiples tablas de nodos:
with Nodes1 on Id1, Nodes2 on Id2 - Los nodos que aparecen en las aristas pero faltan en la tabla de nodos obtienen propiedades vacías
graph-match -- Busca patrones
G | graph-match (a)-[e]->(b) where <restricciones> project <salida>
Notación de patrones:
| Elemento | Nombrado | Anónimo |
|---|---|---|
| Nodo | (n) | () |
| Arista izquierda->derecha | -[e]-> | --> |
| Arista derecha->izquierda | <-[e]- | <-- |
| Cualquier dirección | -[e]- | -- |
| Longitud variable | -[e*1..5]-> | -[*1..5]-> |
Patrones de múltiples saltos: (a)-[e1]->(b)-[e2]->(c)
Patrones de estrella: (a)--(centro)--(b), (c)--(centro)--(d)
Control de ciclos: cycles = all | none | unique_edges (por defecto: unique_edges)
graph-shortest-paths -- Encuentra caminos más cortos
G | graph-shortest-paths (inicio)-[e*1..20]->(fin)
where inicio.name == "Alice" and fin.name == "Server01"
project Camino = e, Longitud = array_length(e)
- Requiere al menos una arista de longitud variable
output = any(por defecto, un camino por par) ooutput = all(todos los caminos más cortos de igual longitud)- Las propiedades de arista de longitud variable se devuelven como arrays dinámicos
graph-to-table -- Exporta grafo a tablas
G | graph-to-table nodes // exportar nodos
G | graph-to-table edges // exportar aristas
G | graph-to-table nodes as N, edges as E // exportar ambos
G | graph-to-table nodes with_node_id=Id // incluir ID hash del nodo
G | graph-to-table edges with_source_id=Src with_target_id=Tgt // incluir IDs de extremos de arista
graph-mark-components -- Encuentra componentes conectados
G | graph-mark-components with_component_id=ComponentId
| graph-to-table nodes
| summarize Miembros = make_list(name) by ComponentId
Asigna un ComponentId a cada nodo. Los nodos en el mismo componente conectado comparten el mismo ID.
graph() función -- Consulta grafos persistentes
graph("MiModeloDeGrafo") // última instantánea
graph("MiModeloDeGrafo", "Snapshot_2025_01") // instantánea específica
graph("MiModeloDeGrafo", true) // transitorio a partir de la definición del modelo
Grafos Transitorios
Creados dinámicamente durante la ejecución de la consulta. Sin configuración previa. Ideales para análisis ad-hoc, exploración y prototipado.
Plantilla: Grafo básico de dos entidades
// 1. Definir aristas
let edges = <TablaOrigen>
| summarize <agregaciones> by ColOrigen, ColDestino;
// 2. Definir búsquedas de nodos
let source_nodes = edges
| distinct ColOrigen
| project nodeId = ColOrigen, label = ColOrigen, nodeType = "<TipoOrigen>";
let target_nodes = edges
| distinct ColDestino
| project nodeId = ColDestino, label = ColDestino, nodeType = "<TipoDestino>";
let all_nodes = union source_nodes, target_nodes;
// 3. Construir y consultar el grafo
edges
| make-graph ColOrigen --> ColDestino with all_nodes on nodeId
| graph-match (s)-[e]->(t)
where <restricciones>
project Origen = s.label, Destino = t.label, <propiedades de arista>
Plantilla: Grafo multi-relación
// Múltiples tipos de aristas -> unirlos con un esquema común
let auth_edges = EventosAutenticacion
| project Origen = nombre_usuario, Destino = nombre_host, tipoArista = "autentica", ts = timestamp;
let net_edges = EventosRed
| project Origen = src_ip, Destino = url, tipoArista = "conecta", ts = timestamp;
let all_edges = union auth_edges, net_edges;
// Búsquedas de nodos de todas las fuentes
let user_nodes = Empleados | project nodeId = nombre_usuario, label = nombre, nodeType = "Usuario";
let host_nodes = EventosAutenticacion | distinct nombre_host | project nodeId = nombre_host, label = nombre_host, nodeType = "Host";
let all_nodes = union user_nodes, host_nodes;
all_edges
| make-graph Origen --> Destino with all_nodes on nodeId
Grafos Persistentes
Para grafos reutilizables a gran escala. Almacenados en metadatos de la base de datos. Soportan instantáneas para comparación histórica.
Seguridad: Crear o alterar modelos de grafo e instantáneas modifica la base de datos. Siempre muestra el comando exacto y confirma con el usuario antes de ejecutar
.create-or-alter graph_modelo.make graph_snapshot.
Paso 1: Crear un modelo de grafo
.create-or-alter graph_model GrafoSeguridad
{
"Schema": {
"Nodes": {
"Usuario": {"nombre": "string", "rol": "string"},
"Host": {"nombre_host": "string"},
"IP": {"ip": "string"}
},
"Edges": {
"AutenticaEn": {"timestamp": "datetime", "resultado": "string"},
"ConectaDesde": {"timestamp": "datetime"}
}
},
"Definition": {
"Steps": [
{
"Kind": "AddNodes",
"Query": "Empleados | project nombre, rol",
"NodeIdColumn": "nombre",
"Labels": ["Usuario"]
},
{
"Kind": "AddNodes",
"Query": "EventosAutenticacion | distinct nombre_host | project nombre_host",
"NodeIdColumn": "nombre_host",
"Labels": ["Host"]
},
{
"Kind": "AddEdges",
"Query": "EventosAutenticacion | project nombre_usuario, nombre_host, timestamp, resultado",
"SourceColumn": "nombre_usuario",
"TargetColumn": "nombre_host",
"Labels": ["AutenticaEn"]
}
]
}
}
Paso 2: Crear una instantánea
.make graph_snapshot GrafoSeguridad Snapshot_2025_07
Paso 3: Consultar la instantánea
graph("GrafoSeguridad")
| graph-match (usuario)-[auth]->(host)
where usuario.rol == "Admin" and auth.resultado == "Inicio de sesión fallido"
project Usuario = usuario.nombre, Host = host.nombre_host, Hora = auth.timestamp
Comandos de administración
Seguridad: Todos los comandos de control a continuación modifican o eliminan objetos de la base de datos. Nunca ejecutes
.drop,.create-or-alter graph_model, o.make graph_snapshotautomáticamente. Siempre muestra el comando exacto, clúster, base de datos y objeto afectado, luego requiere confirmación explícita del usuario antes de la ejecución.
.show graph_models // listar todos los modelos
.show graph_model GrafoSeguridad // mostrar detalles del modelo
.show graph_snapshots GrafoSeguridad // listar instantáneas
.drop graph_snapshot GrafoSeguridad Snapshot_2025_07 // eliminar una instantánea (CONFIRMAR PRIMERO)
.drop graph_model GrafoSeguridad // eliminar modelo y todas las instantáneas (CONFIRMAR PRIMERO)
Transitorio vs Persistente: Cuándo Usar Cada Uno
| Factor | Transitorio (make-graph) | Persistente (graph()) |
|---|---|---|
| Configuración | Ninguna -- en línea en la consulta | Crear modelo + instantánea |
| Duración | Solo durante la ejecución de la consulta | Almacenado en metadatos de la base de datos |
| Frescura de datos | Siempre actual | Instantánea en el momento de creación |
| Escala | Limitado por la memoria de la consulta | A escala empresarial |
| Reutilización | Reconstruido en cada consulta | Compartido entre usuarios/consultas |
| Mejor para | Investigaciones ad-hoc, prototipado | Flujos de trabajo de producción, paneles |
Ejemplos de Seguridad y Caza de Amenazas
Grafo de autenticación: quién inició sesión en qué y desde dónde
let auth_edges = EventosAutenticacion
| summarize
inicios = count(),
fallos = countif(resultado == "Inicio de sesión fallido")
by src_ip, nombre_usuario, nombre_host;
let ip_nodes = auth_edges | distinct src_ip
| project nodeId = src_ip, label = src_ip, nodeType = "IP";
let user_nodes = auth_edges | distinct nombre_usuario
| project nodeId = nombre_usuario, label = nombre_usuario, nodeType = "Usuario";
let host_nodes = auth_edges | distinct nombre_host
| project nodeId = nombre_host, label = nombre_host, nodeType = "Host";
let all_nodes = union ip_nodes, user_nodes, host_nodes;
// Aristas IP -> Usuario
let ip_user = auth_edges
| project Origen = src_ip, Destino = nombre_usuario, inicios, fallos;
// Aristas Usuario -> Host
let user_host = auth_edges
| project Origen = nombre_usuario, Destino = nombre_host, inicios, fallos;
union ip_user, user_host
| make-graph Origen --> Destino with all_nodes on nodeId
| graph-match (ip)-[e1]->(usuario)-[e2]->(host)
where e2.fallos > 20
project
IP = ip.label,
Usuario = usuario.label,
Host = host.label,
Fallos = e2.fallos
| order by Fallos desc
Detección de movimiento lateral: usuarios que comparten hosts comprometidos
// Patrón: (usuario1)-[auth1]->(host)<-[auth2]-(usuario2)
// Dos usuarios con fallos en el mismo host = posible ataque de credenciales
let edges = EventosAutenticacion
| summarize fallos = countif(resultado == "Inicio de sesión fallido"), inicios = count()
by nombre_usuario, nombre_host;
let nodes = union
(edges | distinct nombre_usuario | project nodeId = nombre_usuario, nodeType = "Usuario"),
(edges | distinct nombre_host | project nodeId = nombre_host, nodeType = "Host");
edges
| make-graph nombre_usuario --> nombre_host with nodes on nodeId
| graph-match (u1)-[e1]->(h)<-[e2]-(u2)
where u1.nodeId != u2.nodeId and e1.fallos > 10 and e2.fallos > 10
project
Usuario1 = u1.nodeId, Usuario2 = u2.nodeId,
HostCompartido = h.nodeId,
FallosU1 = e1.fallos, FallosU2 = e2.fallos
| distinct Usuario1, HostCompartido, Usuario2, FallosU1, FallosU2
| order by FallosU1 + FallosU2 desc
Camino de ataque más corto
let edges = EventosSeguridad
| project Origen = entidad_origen, Destino = entidad_destino, accion, timestamp;
let nodes = union
(edges | distinct Origen | project nodeId = Origen),
(edges | distinct Destino | project nodeId = Destino);
edges
| make-graph Origen --> Destino with nodes on nodeId
| graph-shortest-paths (inicio)-[e*1..10]->(fin)
where inicio.nodeId == "IP_Externa_1.2.3.4" and fin.nodeId == "ServidorBaseDeDatos"
project
LongitudCamino = array_length(e),
Acciones = e.accion,
Saltos = e.Destino
Componentes conectados: encontrar clústeres aislados
let edges = FlujosRed
| project Origen = src_ip, Destino = dst_ip;
let nodes = union
(edges | distinct Origen | project nodeId = Origen),
(edges | distinct Destino | project nodeId = Destino);
edges
| make-graph Origen --> Destino with nodes on nodeId
| graph-mark-components with_component_id = ComponentId
| graph-to-table nodes
| summarize Miembros = make_list(nodeId), Tamaño = count() by ComponentId
| order by Tamaño desc
Visualizar en Kusto Explorer
Termina una consulta en make-graph (sin canalizar a graph-match) para activar la ventana de visualización interactiva de grafos de Kusto Explorer:
edges
| make-graph Origen --> Destino with all_nodes on nodeId
// <- detente aquí. Kusto Explorer renderiza el grafo visualmente.
Para aplanar de nuevo a una tabla para paneles o exportación, canaliza a través de graph-match | project o graph-to-table.
Usando con IRQL
Cuando trabajes con datos de seguridad, considera usar selectores de IRQL (Get_*) de la habilidad azure-kusto-irql como fuente de datos. IRQL te proporciona un esquema unificado sin necesidad de memorizar nombres de tablas sin procesar o asignaciones de columnas. Para visualización enriquecida con iconos y plegado de nodos, el Lift_To_Graph de la habilidad azure-kusto-irql-graph es el camino más rápido.
| Enfoque | Mejor para |
|---|---|
make-graph sin procesar (esta habilidad) | Control total, modelos persistentes, caminos más cortos, componentes conectados, esquemas personalizados |
Lift_To_Graph (azure-kusto-irql-graph) | Visualización rápida con iconos en Kusto Explorer, plegado de nodos |
IRQL Get_* -> make-graph | Esquema unificado de IRQL como entrada, luego operadores de grafo sin procesar para análisis |
IRQL Get_* -> Lift_To_Graph -> Graph_Render_View | Camino más rápido de la pregunta al grafo visual |
Nota:
Lift_To_Graph,Graph_Render_ViewyGraph_Fold_By_Propertyson funciones almacenadas, no operadores integrados. Están preimplementadas en el clúster de ejemplo kc7001 pero pueden necesitar implementación en otros clústeres. Consultaazure-kusto-irql-graph/references/DEPLOY_IRQL_FUNCTIONS.mdpara definiciones de funciones e instrucciones de implementación.
Ejemplo: Selectores IRQL -> make-graph -> camino más corto
IRQL maneja la recuperación de datos; make-graph maneja la construcción del grafo y el análisis. Este ejemplo encuentra todos los caminos desde un punto de entrada externo a un servidor de base de datos usando datos de red de IRQL:
let Get_NetworkTraffic_All = ()
{
// Selector IRQL que devuelve tráfico de red con esquema unificado
cluster("tu_cluster").database("tu_bd").Get_NetworkTraffic_All()
};
let edges = Get_NetworkTraffic_All
| project Origen = SrcIpAddr, Destino = DstIpAddr, BytesTotales = TotalBytes;
let nodes = union
(edges | distinct Origen | project nodeId = Origen),
(edges | distinct Destino | project nodeId = Destino);
edges
| make-graph Origen --> Destino with nodes on nodeId
| graph-shortest-paths (inicio)-[e*1..15]->(fin)
where inicio.nodeId == "IP_Externa_1.2.3.4" and fin.nodeId == "10.0.0.5"
project LongitudCamino = array_length(e), Bytes = e.BytesTotales