Sémantique des graphes Kusto
Construisez des graphes transitoires et persistants à partir de données tabulaires en utilisant les opérateurs de graphe KQL. Cette compétence traduit le langage naturel en motif de construction de graphe orienté arêtes et opérateurs de requête de graphe.
Déclencheurs d'activation
Utilisez cette compétence quand l'utilisateur :
- Veut construire un graphe à partir de données tabulaires (
make-graph) - Demande de trouver des motifs, chemins ou relations dans les données
- Mentionne
graph-match,graph-shortest-paths,graph-to-table,graph-mark-components - Veut créer un modèle de graphe persistant ou un snapshot
- Dit « construire un graphe », « trouver le chemin le plus court », « trouver les composantes connexes », « montrer les relations »
- Pose des questions sur les graphes transitoires vs persistants
Pas un convertisseur langage naturel vers KQL. L'entrée doit généralement être une requête KQL fonctionnelle dont les résultats doivent être convertis en graphe, plus une description en langage naturel de la structure de graphe souhaitée. Les demandes en langage naturel simple sont supportées seulement si elles correspondent directement à une table connue avec des colonnes évidentes. Pour la conversion générale de langage naturel vers KQL, utilisez une compétence dédiée à la génération de requêtes (disponible séparément).
Compétences complémentaires :
azure-kusto-irql-- primitives de requête de sécurité composables qui produisent les entrées tabulaires pour les graphesazure-kusto-irql-graph-- système de mapping JSONLift_To_Graphde IRQL pour les graphes richement typés et décorés d'icônes dans Kusto Explorer
L'approche orientée arêtes
Le motif fondamental pour construire des graphes dans Kusto :
1. Définissez vos ARÊTES -> src --> dest, avec type/propriétés de relation
2. Définissez vos RECHERCHES DE NŒUDS -> noms d'affichage, types, propriétés pour chaque ID de nœud
3. Unissez les types d'arêtes -> si vous avez plusieurs types de relations
4. Unissez les recherches de nœuds -> si vous avez plusieurs types de nœuds
5. Appelez make-graph -> edges | make-graph Source --> Target with nodes on nodeId
C'est la façon de penser dans make-graph. Les arêtes sont les relations qui vous intéressent. Les nœuds sont des tables de recherche qui donnent à ces ID un visage -- noms d'affichage, types, propriétés.
Référence des opérateurs de graphe
make-graph -- Construire un graphe à partir de tables
Edges | make-graph SourceId --> TargetId with Nodes on NodeId
Edges: source tabulaire où chaque ligne est une arêteSourceId --> TargetId: colonnes contenant les ID de nœud source et ciblewith Nodes on NodeId: table de propriétés de nœud optionnelle jointe par ID- Supporte plusieurs tables de nœuds :
with Nodes1 on Id1, Nodes2 on Id2 - Les nœuds apparaissant dans les arêtes mais manquant de la table de nœuds reçoivent des propriétés vides
graph-match -- Trouver des motifs
G | graph-match (a)-[e]->(b) where <constraints> project <output>
Notation des motifs :
| Élément | Nommé | Anonyme |
|---|---|---|
| Nœud | (n) |
() |
| Arête gauche->droite | -[e]-> |
--> |
| Arête droite->gauche | <-[e]- |
<-- |
| N'importe quelle direction | -[e]- |
-- |
| Longueur variable | -[e*1..5]-> |
-[*1..5]-> |
Motifs multi-sauts : (a)-[e1]->(b)-[e2]->(c)
Motifs en étoile : (a)--(center)--(b), (c)--(center)--(d)
Contrôle des cycles : cycles = all | none | unique_edges (par défaut : unique_edges)
graph-shortest-paths -- Trouver les chemins les plus courts
G | graph-shortest-paths (start)-[e*1..20]->(end)
where start.name == "Alice" and end.name == "Server01"
project Path = e, Length = array_length(e)
- Nécessite au moins une arête de longueur variable
output = any(par défaut, un chemin par paire) ououtput = all(tous les chemins les plus courts de longueur égale)- Les propriétés des arêtes de longueur variable sont retournées en tant que tableaux dynamiques
graph-to-table -- Exporter le graphe vers des tables
G | graph-to-table nodes // exporter les nœuds
G | graph-to-table edges // exporter les arêtes
G | graph-to-table nodes as N, edges as E // exporter les deux
G | graph-to-table nodes with_node_id=Id // inclure l'ID de hash du nœud
G | graph-to-table edges with_source_id=Src with_target_id=Tgt // inclure les ID d'extrémités des arêtes
graph-mark-components -- Trouver les composantes connexes
G | graph-mark-components with_component_id=ComponentId
| graph-to-table nodes
| summarize Members = make_list(name) by ComponentId
Assigne un ComponentId à chaque nœud. Les nœuds dans la même composante connexe partagent le même ID.
Fonction graph() -- Interroger les graphes persistants
graph("MyGraphModel") // snapshot le plus récent
graph("MyGraphModel", "Snapshot_2025_01") // snapshot spécifique
graph("MyGraphModel", true) // transitoire à partir de la définition du modèle
Graphes transitoires
Créés dynamiquement pendant l'exécution de la requête. Aucune configuration requise. Idéal pour l'analyse ad-hoc, l'exploration et le prototypage.
Modèle : Graphe basique de deux entités
// 1. Définissez les arêtes
let edges = <SourceTable>
| summarize <aggregations> by SourceCol, TargetCol;
// 2. Définissez les recherches de nœuds
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. Construisez et interrogez le graphe
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>
Modèle : Graphe multi-relations
// Plusieurs types d'arêtes -> unissez-les avec un schéma commun
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;
// Recherches de nœuds à partir de toutes les 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
Graphes persistants
Pour les graphes à grande échelle et réutilisables. Stockés dans les métadonnées de la base de données. Supportent les snapshots pour la comparaison historique.
Sécurité : La création ou la modification de modèles de graphe et de snapshots modifie la base de données. Montrez toujours la commande exacte et confirmez avec l'utilisateur avant d'exécuter
.create-or-alter graph_modelou.make graph_snapshot.
Étape 1 : Créer un modèle de graphe
.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"]
}
]
}
}
Étape 2 : Créer un snapshot
.make graph_snapshot SecurityGraph Snapshot_2025_07
Étape 3 : Interroger le snapshot
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
Commandes de gestion
Sécurité : Toutes les commandes de contrôle ci-dessous modifient ou suppriment des objets de base de données. N'exécutez jamais automatiquement
.drop,.create-or-alter graph_model, ou.make graph_snapshot. Montrez toujours la commande exacte, le cluster, la base de données et l'objet affecté, puis exigez une confirmation explicite de l'utilisateur avant l'exécution.
.show graph_models // lister tous les modèles
.show graph_model SecurityGraph // afficher les détails du modèle
.show graph_snapshots SecurityGraph // lister les snapshots
.drop graph_snapshot SecurityGraph Snapshot_2025_07 // supprimer un snapshot (CONFIRMATION D'ABORD)
.drop graph_model SecurityGraph // supprimer le modèle et tous les snapshots (CONFIRMATION D'ABORD)
Transitoire vs Persistant : Quand utiliser lequel
| Facteur | Transitoire (make-graph) |
Persistant (graph()) |
|---|---|---|
| Configuration | Aucune -- en ligne dans la requête | Créer modèle + snapshot |
| Durée de vie | Exécution de requête seulement | Stocké dans les métadonnées de la base de données |
| Fraîcheur des données | Toujours actuelle | Snapshot au moment de la création |
| Échelle | Limitée par la mémoire de requête | Échelle d'entreprise |
| Réutilisabilité | Reconstruit à chaque requête | Partagé entre les utilisateurs/requêtes |
| Meilleur pour | Chasses ad-hoc, prototypage | Workflows de production, tableaux de bord |
Sécurité et exemples de threat hunting
Graphe d'authentification : qui s'est connecté à quoi d'où
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;
// Arêtes IP -> User
let ip_user = auth_edges
| project Source = src_ip, Target = username, logins, fails;
// Arêtes 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
Détection de mouvement latéral : utilisateurs partageant des hôtes compromis
// Motif : (user1)-[auth1]->(host)<-[auth2]-(user2)
// Deux utilisateurs échouant sur le même hôte = pulvérisation d'identifiants possible
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
Chemin d'attaque le plus court
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
Composantes connexes : trouver des clusters isolés
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
Visualiser dans Kusto Explorer
Terminez une requête à make-graph (sans tuyau vers graph-match) pour déclencher la fenêtre de visualisation interactive de graphe de Kusto Explorer :
edges
| make-graph Source --> Target with all_nodes on nodeId
// <- arrêtez ici. Kusto Explorer rend le graphe visuellement.
Pour aplatir de nouveau en table pour les tableaux de bord ou l'export, passez par graph-match | project ou graph-to-table.
Utiliser avec IRQL
Quand vous travaillez avec des données de sécurité, envisagez d'utiliser les sélecteurs IRQL (Get_*) de la compétence azure-kusto-irql comme source de données. IRQL vous donne un schéma unifié sans avoir à mémoriser les noms de tables brutes ou les mappages de colonnes. Pour la visualisation riche avec des icônes et le pliage de nœuds, le Lift_To_Graph de la compétence azure-kusto-irql-graph est le chemin le plus rapide.
| Approche | Meilleur pour |
|---|---|
make-graph brut (cette compétence) |
Contrôle complet, modèles persistants, chemins les plus courts, composantes connexes, schémas personnalisés |
Lift_To_Graph (azure-kusto-irql-graph) |
Visualisation rapide décorée d'icônes dans Kusto Explorer, pliage de nœuds |
IRQL Get_* -> make-graph |
Schéma unifié de IRQL comme entrée, puis opérateurs de graphe bruts pour l'analyse |
IRQL Get_* -> Lift_To_Graph -> Graph_Render_View |
Chemin le plus rapide de la question au graphe visuel |
Note :
Lift_To_Graph,Graph_Render_View, etGraph_Fold_By_Propertysont des fonctions stockées, pas des opérateurs intégrés. Elles sont pré-déployées sur le cluster d'exemple kc7001 mais peuvent nécessiter un déploiement sur d'autres clusters. Voirazure-kusto-irql-graph/references/DEPLOY_IRQL_FUNCTIONS.mdpour les définitions de fonctions et les instructions de déploiement.
Exemple : Sélecteurs IRQL -> make-graph -> chemin le plus court
IRQL gère la récupération des données ; make-graph gère l'analyse du graphe. Ceci trouve le chemin le plus court d'une IP externe à un serveur mail via des événements d'authentification :
// IRQL fournit des colonnes unifiées (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
Exemple : Sélecteurs IRQL -> make-graph -> composantes connexes
Trouvez des clusters d'IPs et de domaines qui sont interconnectés -- infrastructure C2 potentielle :
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
Exemple : Intégration IRQL + make-graph
Voir references/EXAMPLES.md pour des graphes d'investigation multi-sources combinant les sélecteurs IRQL avec make-graph, et des exemples de graphe visuel Lift_To_Graph.
Scénarios d'utilisation pratiques
Voir references/SCENARIOS.md pour des exemples complets élaborés incluant :
- Analyse de réachabilité (chemins les plus courts vers les assets critiques)
- Validation de segmentation réseau (composantes connexes)
- Rayon de blast d'un compte compromis (correspondance de chemin de longueur variable)
- Modèles de graphe persistants pour les équipes SOC (graph_model + snapshots)
Outils MCP utilisés
| Outil | Objectif |
|---|---|
kusto_query |
Exécuter des requêtes KQL incluant make-graph, graph-match, et commandes de gestion |
kusto_table_schema_get |
Découvrir les colonnes de table avant de construire les projections d'arête/nœud |
kusto_cluster_list |
Lister les clusters ADX disponibles |
kusto_database_list |
Lister les bases de données dans un cluster |
Ouvrir les requêtes dans Kusto Explorer (Windows uniquement)
Fonctionnalité de commodité optionnelle. Le workflow par défaut est de sortir le KQL en chat et de laisser l'utilisateur le copier manuellement dans Kusto Explorer ou l'extension Kusto de VS Code. Le lancement automatique est opt-in uniquement.
Par défaut : Sortir KQL en chat
Sortez toujours la KQL complète avec Étape 1 (connexion) et Étape 2 (requête) clairement étiquetées :
// Étape 1 : Connectez-vous à votre cluster (ignorez si déjà connecté)
// Exemple : décommentez pour vous connecter au cluster d'entraînement KC7
// #connect cluster('kc7001.eastus.kusto.windows.net').database('ValdyTimes')
// Ou remplacez par votre propre cluster :
// #connect cluster('<YOUR_CLUSTER>').database('<YOUR_DATABASE>')
// Étape 2 : Exécutez la requête ci-dessous
<KQL_QUERY ending at make-graph>
Puis immédiatement en dessous, sortez une version ADX Web Explorer qui ajoute | graph-to-table nodes as N, edges as E puisque ADX Web Explorer ne peut pas rendre make-graph directement :
// Version ADX Web Explorer (sortie tabulaire) :
<SAME_QUERY>
| graph-to-table nodes as N, edges as E
Ceci assure que la sortie fonctionne à la fois dans Kusto Explorer (visualisation de graphe) et ADX Web Explorer (résultats tabulaires) sans que l'utilisateur n'ait rien à modifier.
Optionnel : Enregistrer et lancer
Si l'utilisateur demande d'enregistrer ou d'ouvrir la requête dans Kusto Explorer, suivez la procédure dans references/KUSTO_EXPLORER_LAUNCH.md. Règles clés :
- Toujours utiliser
ask_userpour confirmer avant d'écrire des fichiers ou de lancer des exécutables - Toujours afficher le contenu du fichier en chat pour que l'utilisateur puisse vérifier avant l'ouverture
- Ne jamais utiliser l'interpolation shell ou des here-strings — écrivez les fichiers via
Set-Content/Add-Content - Ne jamais encoder les requêtes dans des URLs de navigateur
- Sur macOS/Linux, enregistrez le fichier
.kqlet suggérez l'extension Kusto de VS Code ou ADX Web Explorer
Pour la visualisation make-graph (la fenêtre de graphe), la requête doit s'arrêter à make-graph — ne passez pas par graph-match. Kusto Explorer n'ouvre la fenêtre de visualisation de graphe que quand la sortie est un objet graphe, pas une table.