Semântica de Grafos Kusto
Construa grafos transitórios e persistentes a partir de dados tabulares usando operadores de grafo KQL. Esta skill traduz linguagem natural para o padrão de construção de grafos 'arestas-primeiro' e operadores de consulta de grafo.
Gatilhos de Ativação
Use esta skill quando o usuário:
- Deseja construir um grafo a partir de dados tabulares (
make-graph) - Pede para encontrar padrões, caminhos ou relacionamentos em dados
- Menciona
graph-match,graph-shortest-paths,graph-to-table,graph-mark-components - Deseja criar um modelo de grafo persistente ou snapshot
- Diz "construir um grafo", "encontrar o caminho mais curto", "encontrar componentes conectados", "mostrar relacionamentos"
- Pergunta sobre grafos transitórios vs. persistentes
Não é um conversor de linguagem natural para KQL. A entrada geralmente deve ser uma consulta KQL funcional cujos resultados o usuário deseja converter em um grafo, além de uma descrição em linguagem natural da estrutura de grafo desejada. Solicitações básicas de fonte em linguagem natural são suportadas apenas quando mapeiam diretamente para uma tabela conhecida com colunas óbvias. Para conversão geral de NL para KQL, use uma skill dedicada de geração de consultas (disponível separadamente).
Skills complementares:
azure-kusto-irql-- primitivas de consulta de segurança combináveis que produzem as entradas tabulares para grafosazure-kusto-irql-graph-- sistema de mapeamento JSONLift_To_Graphdo IRQL para grafos ricamente tipados e decorados com ícones no Kusto Explorer
A Abordagem Arestas-Primeiro
O padrão fundamental para construir grafos no Kusto:
1. Defina suas ARESTAS -> src --> dest, com tipo de relacionamento/propriedades
2. Defina suas CONSULTAS DE NÓS -> nomes de exibição, tipos, propriedades para cada ID de nó
3. Unifique tipos de arestas -> se você tiver vários tipos de relacionamento
4. Unifique consultas de nós -> se você tiver vários tipos de nó
5. Chame make-graph -> arestas | make-graph Origem --> Destino com nós em nodeId
É assim que se pensa em make-graph. As arestas são os relacionamentos que você se importa. Os nós são tabelas de consulta que dão a esses IDs uma cara -- nomes de exibição, tipos, propriedades.
Referência de Operadores de Grafo
make-graph -- Construir um grafo a partir de tabelas
Arestas | make-graph SourceId --> TargetId com Nós em NodeId
Arestas: fonte tabular onde cada linha é uma arestaSourceId --> TargetId: colunas contendo IDs de nó de origem e destinocom Nós em NodeId: tabela opcional de propriedades de nó unida por ID- Suporta múltiplas tabelas de nós:
com Nós1 em Id1, Nós2 em Id2 - Nós que aparecem em arestas mas estão ausentes na tabela de nós recebem propriedades vazias
graph-match -- Encontrar padrões
G | graph-match (a)-[e]->(b) onde <restricoes> project <saida>
Notação de padrão:
| Elemento | Nomeado | Anônimo |
|---|---|---|
| Nó | (n) | () |
| Aresta esquerda->direita | -[e]-> | --> |
| Aresta direita->esquerda | <-[e]- | <-- |
| Qualquer direção | -[e]- | -- |
| Comprimento variável | -[e*1..5]-> | -[*1..5]-> |
Padrões multi-salto: (a)-[e1]->(b)-[e2]->(c)
Padrões estrela: (a)--(center)--(b), (c)--(center)--(d)
Controle de ciclos: cycles = all | none | unique_edges (padrão: unique_edges)
graph-shortest-paths -- Encontrar caminhos mais curtos
G | graph-shortest-paths (inicio)-[e*1..20]->(fim)
onde inicio.nome == "Alice" e fim.nome == "Servidor01"
project Caminho = e, Tamanho = array_length(e)
- Requer pelo menos uma aresta de comprimento variável
output = any(padrão, um caminho por par) ououtput = all(todos os caminhos mais curtos de igual comprimento)- Propriedades de aresta de comprimento variável retornadas como arrays dinâmicos
graph-to-table -- Exportar grafo para tabelas
G | graph-to-table nodes // exportar nós
G | graph-to-table edges // exportar arestas
G | graph-to-table nodes as N, edges as E // exportar ambos
G | graph-to-table nodes with_node_id=Id // incluir ID hash do nó
G | graph-to-table edges with_source_id=Src with_target_id=Tgt // incluir IDs de ponta da aresta
graph-mark-components -- Encontrar componentes conectados
G | graph-mark-components with_component_id=ComponentId
| graph-to-table nodes
| summarize Membros = make_list(nome) by ComponentId
Atribui um ComponentId a cada nó. Nós no mesmo componente conectado compartilham o mesmo ID.
Função graph() -- Consultar grafos persistentes
graph("MeuModeloGrafo") // snapshot mais recente
graph("MeuModeloGrafo", "Snapshot_2025_01") // snapshot específico
graph("MeuModeloGrafo", true) // transitório a partir da definição do modelo
Grafos Transitórios
Criados dinamicamente durante a execução da consulta. Nenhuma configuração necessária. Ideal para análises ad-hoc, exploração e prototipagem.
Template: Grafo básico de duas entidades
// 1. Definir arestas
let arestas = <TabelaFonte>
| summarize <agregacoes> by ColOrigem, ColDestino;
// 2. Definir consultas de nós
let nos_origem = arestas
| distinct ColOrigem
| project nodeId = ColOrigem, rotulo = ColOrigem, tipoNo = "<TipoOrigem>";
let nos_destino = arestas
| distinct ColDestino
| project nodeId = ColDestino, rotulo = ColDestino, tipoNo = "<TipoDestino>";
let todos_nos = union nos_origem, nos_destino;
// 3. Construir e consultar o grafo
arestas
| make-graph ColOrigem --> ColDestino com todos_nos em nodeId
| graph-match (s)-[e]->(t)
onde <restricoes>
project Origem = s.rotulo, Destino = t.rotulo, <propriedades da aresta>
Template: Grafo multi-relacionamento
// Múltiplos tipos de aresta -> unificá-los com um esquema comum
let arestas_auth = EventosAutenticacao
| project Fonte = usuario, Destino = hostname, tipoAresta = "autentica", ts = timestamp;
let arestas_rede = EventosRede
| project Fonte = src_ip, Destino = url, tipoAresta = "conecta", ts = timestamp;
let todas_arestas = union arestas_auth, arestas_rede;
// Consultas de nós de todas as fontes
let nos_usuario = Funcionarios | project nodeId = usuario, rotulo = nome, tipoNo = "Usuario";
let nos_host = EventosAutenticacao | distinct hostname | project nodeId = hostname, rotulo = hostname, tipoNo = "Host";
let todos_nos = union nos_usuario, nos_host;
todas_arestas
| make-graph Fonte --> Destino com todos_nos em nodeId
Grafos Persistentes
Para grafos reutilizáveis em larga escala. Armazenados em metadados de banco de dados. Suportam snapshots para comparação histórica.
Segurança: Criar ou alterar modelos de grafo e snapshots modifica o banco de dados. Sempre mostre o comando exato e confirme com o usuário antes de executar
.create-or-alter graph_modelou.make graph_snapshot.
Passo 1: Criar um modelo de grafo
.create-or-alter graph_model GrafoSeguranca
{
"Schema": {
"Nodes": {
"Usuario": {"nome": "string", "papel": "string"},
"Host": {"hostname": "string"},
"IP": {"ip": "string"}
},
"Edges": {
"AutenticaPara": {"timestamp": "datetime", "resultado": "string"},
"ConectaDe": {"timestamp": "datetime"}
}
},
"Definition": {
"Steps": [
{
"Kind": "AddNodes",
"Query": "Funcionarios | project nome, papel",
"NodeIdColumn": "nome",
"Labels": ["Usuario"]
},
{
"Kind": "AddNodes",
"Query": "EventosAutenticacao | distinct hostname | project hostname",
"NodeIdColumn": "hostname",
"Labels": ["Host"]
},
{
"Kind": "AddEdges",
"Query": "EventosAutenticacao | project usuario, hostname, timestamp, resultado",
"SourceColumn": "usuario",
"TargetColumn": "hostname",
"Labels": ["AutenticaPara"]
}
]
}
}
Passo 2: Criar um snapshot
.make graph_snapshot GrafoSeguranca Snapshot_2025_07
Passo 3: Consultar o snapshot
graph("GrafoSeguranca")
| graph-match (usuario)-[auth]->(host)
onde usuario.papel == "Admin" e auth.resultado == "Falha de Login"
project Usuario = usuario.nome, Host = host.hostname, Hora = auth.timestamp
Comandos de gerenciamento
Segurança: Todos os comandos de controle abaixo modificam ou excluem objetos do banco de dados. Nunca execute
.drop,.create-or-alter graph_modelou.make graph_snapshotautomaticamente. Sempre mostre o comando exato, cluster, banco de dados e objeto afetado, e exija confirmação explícita do usuário antes da execução.
.show graph_models // listar todos os modelos
.show graph_model GrafoSeguranca // mostrar detalhes do modelo
.show graph_snapshots GrafoSeguranca // listar snapshots
.drop graph_snapshot GrafoSeguranca Snapshot_2025_07 // excluir um snapshot (CONFIRME PRIMEIRO)
.drop graph_model GrafoSeguranca // excluir modelo e todos os snapshots (CONFIRME PRIMEIRO)
Transitório vs Persistente: Quando Usar Cada Um
| Fator | Transitório (make-graph) | Persistente (graph()) |
|---|---|---|
| Configuração | Nenhuma -- embutido na consulta | Criar modelo + snapshot |
| Tempo de vida | Apenas execução da consulta | Armazenado em metadados do banco |
| Atualização dos dados | Sempre atual | Snapshot no momento da criação |
| Escala | Limitado pela memória da consulta | Escala empresarial |
| Reuso | Reconstruído a cada consulta | Compartilhado entre usuários/consultas |
| Melhor para | Caças ad-hoc, prototipagem | Workflows de produção, dashboards |
Exemplos de Segurança & Caça a Ameaças
Grafo de autenticação: quem logou no que e de onde
let arestas_auth = EventosAutenticacao
| summarize
logins = count(),
falhas = countif(resultado == "Falha de Login")
by src_ip, usuario, hostname;
let nos_ip = arestas_auth | distinct src_ip
| project nodeId = src_ip, rotulo = src_ip, tipoNo = "IP";
let nos_usuario = arestas_auth | distinct usuario
| project nodeId = usuario, rotulo = usuario, tipoNo = "Usuario";
let nos_host = arestas_auth | distinct hostname
| project nodeId = hostname, rotulo = hostname, tipoNo = "Host";
let todos_nos = union nos_ip, nos_usuario, nos_host;
// Arestas IP -> Usuario
let ip_usuario = arestas_auth
| project Fonte = src_ip, Destino = usuario, logins, falhas;
// Arestas Usuario -> Host
let usuario_host = arestas_auth
| project Fonte = usuario, Destino = hostname, logins, falhas;
union ip_usuario, usuario_host
| make-graph Fonte --> Destino com todos_nos em nodeId
| graph-match (ip)-[e1]->(usuario)-[e2]->(host)
onde e2.falhas > 20
project
IP = ip.rotulo,
Usuario = usuario.rotulo,
Host = host.rotulo,
Falhas = e2.falhas
| order by Falhas desc
Detecção de movimento lateral: usuários compartilhando hosts comprometidos
// Padrão: (usuario1)-[auth1]->(host)<-[auth2]-(usuario2)
// Dois usuários falhando no mesmo host = possível spray de credenciais
let arestas = EventosAutenticacao
| summarize falhas = countif(resultado == "Falha de Login"), logins = count()
by usuario, hostname;
let nos = union
(arestas | distinct usuario | project nodeId = usuario, tipoNo = "Usuario"),
(arestas | distinct hostname | project nodeId = hostname, tipoNo = "Host");
arestas
| make-graph usuario --> hostname com nos em nodeId
| graph-match (u1)-[e1]->(h)<-[e2]-(u2)
onde u1.nodeId != u2.nodeId and e1.falhas > 10 and e2.falhas > 10
project
Usuario1 = u1.nodeId, Usuario2 = u2.nodeId,
HostCompartilhado = h.nodeId,
FalhasU1 = e1.falhas, FalhasU2 = e2.falhas
| distinct Usuario1, HostCompartilhado, Usuario2, FalhasU1, FalhasU2
| order by FalhasU1 + FalhasU2 desc
Caminho de ataque mais curto
let arestas = EventosSeguranca
| project Fonte = entidade_origem, Destino = entidade_destino, acao, timestamp;
let nos = union
(arestas | distinct Fonte | project nodeId = Fonte),
(arestas | distinct Destino | project nodeId = Destino);
arestas
| make-graph Fonte --> Destino com nos em nodeId
| graph-shortest-paths (inicio)-[e*1..10]->(fim)
onde inicio.nodeId == "IPExterno_1.2.3.4" e fim.nodeId == "ServidorBD"
project
TamanhoCaminho = array_length(e),
Acoes = e.acao,
Saltos = e.Destino
Componentes conectados: encontrar clusters isolados
let arestas = FluxosRede
| project Fonte = src_ip, Destino = dst_ip;
let nos = union
(arestas | distinct Fonte | project nodeId = Fonte),
(arestas | distinct Destino | project nodeId = Destino);
arestas
| make-graph Fonte --> Destino com nos em nodeId
| graph-mark-components with_component_id = ComponentId
| graph-to-table nodes
| summarize Membros = make_list(nodeId), Tamanho = count() by ComponentId
| order by Tamanho desc
Visualizar no Kusto Explorer
Termine uma consulta em make-graph (sem canalizar para graph-match) para acionar a janela de visualização interativa de grafos do Kusto Explorer:
arestas
| make-graph Fonte --> Destino com todos_nos em nodeId
// <- pare aqui. O Kusto Explorer renderiza o grafo visualmente.
Para achatar de volta para uma tabela para dashboards ou exportação, canalize através de graph-match | project ou graph-to-table.
Usando com IRQL
Ao trabalhar com dados de segurança, considere usar seletores IRQL (Get_*) da skill azure-kusto-irql como fonte de dados. O IRQL fornece um esquema unificado sem memorizar nomes de tabelas brutas ou mapeamentos de colunas. Para visualização rica com ícones e dobramento de nós, o Lift_To_Graph da skill azure-kusto-irql-graph é o caminho mais rápido.
| Abordagem | Melhor Para |
|---|---|
make-graph bruto (esta skill) | Controle total, modelos persistentes, caminhos mais curtos, componentes conectados, esquemas personalizados |
Lift_To_Graph (azure-kusto-irql-graph) | Visualização rápida decorada com ícones no Kusto Explorer, dobramento de nós |
IRQL Get_* -> make-graph | Esquema unificado do IRQL como entrada, depois operadores de grafo brutos para análise |
IRQL Get_* -> Lift_To_Graph -> Graph_Render_View | Caminho mais rápido da pergunta ao grafo visual |
Nota:
Lift_To_Graph,Graph_Render_VieweGraph_Fold_By_Propertysão funções armazenadas, não operadores internos. Elas são pré-implantadas no cluster de exemplo kc7001, mas podem precisar de implantação em outros clusters. Vejaazure-kusto-irql-graph/references/DEPLOY_IRQL_FUNCTIONS.mdpara definições de funções e instruções de implantação.
Exemplo: Seletores IRQL -> make-graph -> caminho mais curto
O IRQL lida com a recuperação de dados; make-graph lida com a análise de grafo.