azure-kusto-graph

Par microsoft · azure-skills

Construire et interroger des graphes Kusto en langage naturel. Couvre les graphes transitoires (`make-graph`), les modèles/snapshots de graphes persistants, la correspondance de motifs (`graph-match`), les chemins les plus courts, les composantes connexes et l'export graphe-vers-table. Génère la réflexion edges-first : définir les arêtes, définir les lookups de nœuds, union, `make-graph`. QUAND : `make-graph`, `graph-match`, `graph-shortest-paths`, `graph-to-table`, `graph-mark-components`, persistent graph, graph model, graph snapshot, construire un graphe depuis des données, trouver des chemins entre nœuds, correspondance de motifs dans un graphe, composantes connexes, graphe transitoire, Kusto graph, KQL graph.

npx skills add https://github.com/microsoft/azure-skills --skill azure-kusto-graph

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 graphes
  • azure-kusto-irql-graph -- système de mapping JSON Lift_To_Graph de 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ête
  • SourceId --> TargetId : colonnes contenant les ID de nœud source et cible
  • with 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) ou output = 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_model ou .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, et Graph_Fold_By_Property sont 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. Voir azure-kusto-irql-graph/references/DEPLOY_IRQL_FUNCTIONS.md pour 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_user pour 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 .kql et 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.

Skills similaires