Déployer les fonctionnalités d'IA générative d'Elastic Stack sur GDC sous air gap

Présentation

Ce guide décrit le déploiement de bout en bout d'Elastic Stack (Elasticsearch et Kibana) sur un cluster partagé isolé de Google Distributed Cloud (GDC) et explique comment l'intégrer à un service de grand modèle de langage (LLM) pour évaluer les fonctionnalités d'IA générative d'Elastic.

Le déploiement utilise l'opérateur Elastic Cloud sur Kubernetes (ECK) officiel disponible sur la place de marché GDC sous air gap. L'opérateur s'installe dans un espace de noms de projet sur un cluster Kubernetes partagé, et automatise le provisionnement et la gestion du cycle de vie des ressources Elasticsearch et Kibana. Une fois déployé, Kibana se connecte à un point de terminaison d'inférence LLM (comme Gemini géré sur GDC ou un service compatible avec OpenAI autogéré comme Ollama ou vLLM). Cette intégration alimente les capacités d'IA générative d'Elastic, y compris l'agent IA Elastic, les agents personnalisés créés avec Agent Builder et Attack Discovery.

Ce guide a été rédigé pour Elastic v9.4.0, l'opérateur Elastic Kubernetes v3.4.0 et GDC v1.16.2. Certaines instructions peuvent différer légèrement si vous utilisez des versions différentes.

Architecture

L'architecture se compose d'un déploiement Elastic Stack géré par l'opérateur ECK dans un espace de noms de projet sur un cluster d'utilisateur partagé GDC. Kibana communique sur le réseau GDC interne avec un point de terminaison d'inférence LLM.

Schéma de l'architecture Elastic Stack.

Voici les principaux composants de cette architecture :

  • Cluster partagé GDC : cluster Kubernetes partagé au niveau de l'organisation, associé à plusieurs projets, hébergeant des packages logiciels et des charges de travail d'application GDC Marketplace.
  • Projet GDC : espace de noms du projet sur le cluster partagé qui isole l'opérateur ECK, le cluster Elasticsearch, l'instance Kibana, ainsi que les secrets et les NetworkPolicies Kubernetes associés.
  • Opérateur Elastic Cloud sur Kubernetes (ECK) : installé depuis GDC Marketplace dans l'espace de noms du projet, l'opérateur gère les ressources personnalisées Elasticsearch et Kibana, et rapproche la licence Elastic requise pour les fonctionnalités d'IA générative et les connecteurs LLM externes.
  • Elasticsearch : stocke et indexe les données opérationnelles et de sécurité, et exécute les requêtes de recherche et ES|QL générées par les fonctionnalités d'IA et les agents de Kibana.
  • Kibana : héberge les fonctionnalités d'IA générative d'Elastic, y compris l'agent IA Elastic (pour l'investigation des journaux en langage naturel et la génération ES|QL), Agent Builder (pour créer des agents conversationnels et des outils personnalisés sur les données Elasticsearch) et Attack Discovery (pour la corrélation des alertes de sécurité et l'analyse de la chaîne d'attaque). Kibana gère également les connecteurs d'action LLM qui orchestrent l'appel d'outils en plusieurs étapes entre l'utilisateur, Elasticsearch et le service LLM.
  • Service LLM : service LLM géré (tel que Gemini sur GDC) ou serveur d'inférence autogéré (tel qu'Ollama ou vLLM) qui fournit des fonctionnalités de complétion de chat et d'appel d'outils à Kibana sur le réseau GDC interne.

Avant de commencer

Avant de commencer le déploiement, assurez-vous que votre environnement GDC remplit les conditions préalables et les hypothèses suivantes :

  • Cluster Kubernetes partagé et projet GDC : vous avez accès à un cluster partagé GDC existant et à un projet GDC associé à ce cluster. Le projet sert de conteneur d'espace de noms pour toutes les ressources Kubernetes déployées dans ce guide. Assurez-vous que le cluster dispose de suffisamment de ressources de calcul et de stockage persistant pour héberger l'opérateur ECK, Elasticsearch et les charges de travail Kibana.
  • Service LLM : vous disposez d'un accès réseau à un service LLM, qu'il s'agisse du service Gemini géré (s'il est disponible dans votre environnement GDC) ou d'un service LLM autogéré compatible avec OpenAI (par exemple, gemma4:31b diffusé via Ollama ou vLLM dans un projet GPU). Le déploiement de l'infrastructure LLM elle-même n'entre pas dans le cadre de ce guide, mais les exigences spécifiques du modèle et de l'API sont détaillées dans la section Exigences pour le service LLM.
  • Récupérez la version d'Elastic fournie avec votre environnement GDC :

    export ELASTIC_VERSION=$(curl -sk "https://harbor.${ORG}.${ZONE}.${GDCH_DOMAIN}/api/v2.0/projects/library/repositories/elasticsearch%252Felasticsearch/artifacts" | jq -r '[.[].tags[]?.name] | sort_by(split(".") | map(tonumber)) | last')
    
  • Configurez les variables d'environnement qui seront référencées tout au long de ce guide. Modifiez ces valeurs selon vos besoins pour qu'elles correspondent à votre environnement spécifique :

    # GDC environment configuration
    export PROJECT_ID="your-project-id"
    export CLUSTER_NAME="your-shared-cluster-name"
    export ZONE="your-zone-name"
    export USER_NAME="your-gdc-user-email"
    
    # Marketplace package and Elastic Stack configuration
    export MARKETPLACE_SERVICE_NAME="elastic"
    
    # LLM service configuration
    export LLM_CONNECTOR_NAME="your-llm-connector-name"
    export LLM_API_URL="http://your-llm-service-host:11434/v1/chat/completions"
    export LLM_MODEL_NAME="your-llm-model-name"
    export LLM_API_KEY="your-llm-api-key"
    
  • Configurez gdcloud :

    gdcloud config set project ${PROJECT_ID}
    gdcloud config set core/zone ${ZONE}
    
  • Demandez ou associez les rôles IAM requis au niveau du projet pour votre utilisateur dans le projet GDC cible (${PROJECT_ID}). D'après la documentation sur le cycle de vie de GDC Marketplace et les exigences de mise en réseau de GDC, au moins les rôles de projet suivants sont requis :

    • Administrateur de l'espace de noms (namespace-admin) : accès administratif complet pour créer et gérer des charges de travail, des secrets, des services et des ressources personnalisées Kubernetes dans l'espace de noms du projet sur le cluster partagé.
    • Lecteur de projet (project-viewer) : accès en lecture pour afficher le projet existant et ses ressources dans la console et la CLI GDC.
    • Éditeur Marketplace (marketplace-editor) : autorisation d'installer, de mettre à niveau et de désinstaller des packages logiciels GDC Marketplace dans le projet.
    • Administrateur NetworkPolicy du projet (project-networkpolicy-admin) : autorisation de configurer des règles ProjectNetworkPolicy globales permettant au trafic interprojets et externe d'atteindre les services LoadBalancer Kibana et Elasticsearch.

    Demandez à l'administrateur IAM de votre organisation ou de votre projet de vous accorder ces rôles (ou exécutez les commandes suivantes si vous disposez de droits d'administrateur IAM) :

    gdcloud projects add-iam-policy-binding ${PROJECT_ID} \
      --member="user:${USER_NAME}" \
      --role=namespace-admin
    
    gdcloud projects add-iam-policy-binding ${PROJECT_ID} \
      --member="user:${USER_NAME}" \
      --role=project-viewer
    
    gdcloud projects add-iam-policy-binding ${PROJECT_ID} \
      --member="user:${USER_NAME}" \
      --role=marketplace-editor
    
    gdcloud projects add-iam-policy-binding ${PROJECT_ID} \
      --member="user:${USER_NAME}" \
      --role=project-networkpolicy-admin
    
  • Récupérez le fichier kubeconfig du cluster partagé et configurez un alias d'interface système ku pour kubectl :

    KUBECONFIG=kubeconfig-${CLUSTER_NAME}.yaml gdcloud clusters \
      get-credentials ${CLUSTER_NAME} \
      --zone ${ZONE}
    
    alias ku="kubectl --kubeconfig=kubeconfig-${CLUSTER_NAME}.yaml"
    

Installer le package de place de marché Elastic

Dans cette section, vous allez installer l'opérateur Elastic Cloud sur Kubernetes (ECK) dans l'espace de noms de votre projet à partir de la place de marché GDC sous air gap en suivant le guide du cycle de vie de la place de marché GDC.

Installer via la CLI gdcloud (ou la console GDC)

  1. Listez les packages logiciels Marketplace disponibles pour confirmer le nom du service et la version disponible :

    gdcloud marketplace services list
    

    Le résultat doit ressembler à ce qui suit :

    SERVICE      LATEST VERSION
    confluent    <CONFLUENT_VERSION>
    dataproc     <DATAPROC_VERSION>
    elastic      <ECK_OPERATOR_VERSION>
    mariadb      <MARIADB_VERSION>
    mattermost   <MATTERMOST_VERSION>
    mongodb      <MONGODB_VERSION>
    neo4j        <NEO4J_VERSION>
    redis        <REDIS_VERSION>
    starboard    <STARBOARD_VERSION>
    starburst    <STARBURST_VERSION>
    tes          <TES_VERSION>
    vault        <VAULT_VERSION>
    
  2. Installez le package Elastic Marketplace sur votre cluster partagé cible :

    gdcloud marketplace services install ${MARKETPLACE_SERVICE_NAME} \
      --cluster=${CLUSTER_NAME} \
      --zone=${ZONE} \
      --project ${PROJECT_ID}
    

    *(Vous pouvez également sélectionner votre projet dans la console GDC ${PROJECT_ID}, accéder à Marketplace, sélectionner l'annonce Elastic Cloud on Kubernetes, cliquer sur Installer, sélectionner le cluster ${CLUSTER_NAME}, puis cliquer sur Installer.)*

  3. Vérifiez que l'objet StatefulSet et le pod elastic-operator sont en cours d'exécution dans l'espace de noms de votre projet (${PROJECT_ID}) :

    ku get statefulset,pods -n ${PROJECT_ID}
    

    Le résultat doit ressembler à ce qui suit :

    NAME                                READY   AGE
    statefulset.apps/elastic-operator   1/1     3m
    
    NAME                   READY   STATUS    RESTARTS   AGE
    pod/elastic-operator-0 1/1     Running   0          3m
    

Déployer la pile Elastic Stack

Créer le fichier manifeste Elastic Stack

Le package GDC Marketplace déploie l'opérateur ECK préconfiguré pour extraire les images de conteneurs Elasticsearch et Kibana du registre Harbor local sous air gap.

Créez elastic-stack.yaml pour déployer un cluster Elasticsearch à un nœud, une instance d'interface utilisateur Kibana, des services HTTP LoadBalancer et un allow-all NetworkPolicy dans l'espace de noms de votre projet (${PROJECT_ID}). Le fichier manifeste inclut également une licence Secret rapprochée par l'opérateur ECK, configurée ici en tant qu'essai Enterprise de 30 jours (eck-trial-license) si vous ne disposez pas encore de votre propre licence Enterprise :

cat <<EOF > elastic-stack.yaml
apiVersion: v1
kind: Secret
metadata:
  name: eck-trial-license
  namespace: ${PROJECT_ID}
  labels:
    license.k8s.elastic.co/type: enterprise_trial
  annotations:
    elastic.co/eula: accepted
---
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: elastic-cluster
  namespace: ${PROJECT_ID}
spec:
  version: ${ELASTIC_VERSION}
  http:
    service:
      spec:
        type: LoadBalancer
  nodeSets:
    - name: default
      count: 1
      config:
        # Disable mmap to avoid requiring privileged host sysctl modifications
        node.store.allow_mmap: false
      podTemplate:
        spec:
          initContainers:
            - name: sysctl
              securityContext:
                privileged: false
                allowPrivilegeEscalation: false
                runAsNonRoot: true
                capabilities:
                  drop:
                    - ALL
              command: ["sh", "-c", "true"]
          containers:
            - name: elasticsearch
              resources:
                requests:
                  memory: 2Gi
                  cpu: 500m
                limits:
                  memory: 4Gi
                  cpu: 2
      volumeClaimTemplates:
        - metadata:
            name: elasticsearch-data
          spec:
            accessModes:
              - ReadWriteOnce
            resources:
              requests:
                storage: 10Gi
---
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: kibana-ui
  namespace: ${PROJECT_ID}
spec:
  version: ${ELASTIC_VERSION}
  count: 1
  elasticsearchRef:
    name: elastic-cluster
  http:
    service:
      spec:
        type: LoadBalancer
  podTemplate:
    spec:
      containers:
        - name: kibana
          resources:
            requests:
              memory: 1Gi
              cpu: 200m
            limits:
              memory: 2Gi
              cpu: 1
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all
  namespace: ${PROJECT_ID}
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - ipBlock:
            cidr: 0.0.0.0/0
EOF

Appliquer et vérifier le déploiement

Appliquez le fichier manifeste :

ku apply -f elastic-stack.yaml

Appliquez un ProjectNetworkPolicy global pour que les règles de pare-feu GDC autorisent le trafic entrant vers les adresses IP de l'équilibreur de charge, ainsi que le trafic interprojets (comme la communication avec le service LLM) :

kubectl --kubeconfig=kubeconfig-global.yaml apply -f - <<EOF
apiVersion: networking.global.gdc.goog/v1
kind: ProjectNetworkPolicy
metadata:
  namespace: ${PROJECT_ID}
  name: allow-inbound-traffic-from-external
spec:
  policyType: Ingress
  subject:
    subjectType: UserWorkload
  ingress:
    - from:
        - ipBlock:
            cidr: 0.0.0.0/0
---
apiVersion: networking.global.gdc.goog/v1
kind: ProjectNetworkPolicy
metadata:
  namespace: ${PROJECT_ID}
  name: allow-inbound-traffic-from-all-projects
spec:
  policyType: Ingress
  subject:
    subjectType: UserWorkload
  ingress:
    - from:
        - projects: {}
EOF

Surveillez les pods, les services et les ressources personnalisées jusqu'à ce qu'Elasticsearch et Kibana atteignent l'état Running et Ready, et obtiennent des adresses IP d'équilibreur de charge sur ${CLUSTER_NAME} :

ku get pods,svc,elasticsearch,kibana -n ${PROJECT_ID}

Une fois les ressources prêtes, le résultat devrait ressembler à ceci :

NAME                                READY   STATUS    RESTARTS   AGE
pod/elastic-cluster-es-default-0    1/1     Running   0          3m
pod/elastic-operator-0              1/1     Running   0          15m
pod/kibana-ui-kb-869844478d-59ptw   1/1     Running   0          3m

NAME                                                                      TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)          AGE
service/elastic-cluster-es-default                                        ClusterIP      None            <none>         9200/TCP         11m
service/elastic-cluster-es-http                                           LoadBalancer   10.101.18.181   <ELASTIC_EXTERNAL_IP>   9200:32159/TCP   11m
service/elastic-cluster-es-internal-http                                  ClusterIP      10.101.18.81    <none>         9200/TCP         11m
service/elastic-cluster-es-transport                                      ClusterIP      None            <none>         9300/TCP         11m
service/g-svc-g-fre-elastic-cluster-es-http-shared-clus-extern-56d5eb67   ClusterIP      None            <none>         9200/TCP         11m
service/g-svc-g-fre-kibana-ui-kb-http-shared-cluster-0b-extern-75a78b1f   ClusterIP      None            <none>         5601/TCP         11m
service/kibana-ui-kb-http                                                 LoadBalancer   10.101.19.86    <KIBANA_EXTERNAL_IP>   5601:32735/TCP   11m

NAME                                                         HEALTH   NODES   VERSION             PHASE   AGE
elasticsearch.elasticsearch.k8s.elastic.co/elastic-cluster   green    1       <ELASTIC_VERSION>   Ready   3m

NAME                                     HEALTH   NODES   VERSION             AGE
kibana.kibana.k8s.elastic.co/kibana-ui   green    1       <ELASTIC_VERSION>   3m

Exportez les adresses IP LoadBalancer externes attribuées à Elasticsearch et Kibana pour les utiliser lors des étapes suivantes :

export ELASTICSEARCH_IP=$(ku get svc elastic-cluster-es-http \
  -n ${PROJECT_ID} \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

export KIBANA_IP=$(ku get svc kibana-ui-kb-http \
  -n ${PROJECT_ID} \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

echo "Kibana External URL:        https://${KIBANA_IP}:5601"
echo "Elasticsearch External URL: https://${ELASTICSEARCH_IP}:9200"

Récupérer les identifiants et accéder à l'interface utilisateur de Kibana

Récupérer le mot de passe du super-utilisateur elastic

ECK génère automatiquement un secret Kubernetes (elastic-cluster-es-elastic-user) contenant le mot de passe du super-utilisateur elastic par défaut. Récupérez-la et décodez-la avec la commande suivante :

export ELASTIC_PASSWORD=$(ku get secret elastic-cluster-es-elastic-user \
  -n ${PROJECT_ID} \
  -o jsonpath='{.data.elastic}' | base64 -d)
echo "${ELASTIC_PASSWORD}"

Vérifier la connectivité et se connecter

  1. Vérifiez qu'Elasticsearch est accessible via HTTPS depuis votre poste de travail :

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" "https://${ELASTICSEARCH_IP}:9200"
    
  2. Ouvrez https://${KIBANA_IP}:5601 dans votre navigateur, acceptez l'avertissement concernant le certificat autosigné, puis connectez-vous avec les identifiants suivants :

    • Nom d'utilisateur : elastic
    • Mot de passe : <output from ELASTIC_PASSWORD above>

Vérifier la licence Elastic

Les fonctionnalités d'IA générative d'Elastic (y compris l'agent IA Elastic, Agent Builder et Attack Discovery) et les connecteurs LLM externes nécessitent une licence Enterprise (ou une licence d'essai Enterprise de 30 jours active).

Lorsqu'un cluster Elasticsearch est géré par l'opérateur ECK, le license-controller d'ECK rapproche en permanence la licence du cluster des secrets Kubernetes dans l'espace de noms de l'opérateur (${PROJECT_ID}). Si vous ne disposez pas encore d'une licence Enterprise de production, le secret eck-trial-license inclus dans elastic-stack.yaml (license.k8s.elastic.co/type: enterprise_trial) indique à ECK de provisionner et de gérer automatiquement une licence d'essai Enterprise de 30 jours. (Notez que si vous démarrez un essai manuellement via POST /_license/start_trial sans secret de licence ECK, ECK rétablira le cluster sur basic lors du prochain cycle de rapprochement.)

Pour vérifier la licence active à l'aide de la CLI :

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  "https://${ELASTICSEARCH_IP}:9200/_license" | jq .

*(Vous pouvez également ouvrir le menu de navigation dans Kibana, accéder à Management > Dev Tools (Gestion > Outils de développement), puis exécuter GET /_license dans l'onglet "Console".)*

Le résultat doit ressembler à ce qui suit, avec "status": "active" et "type": "enterprise" (ou "type": "trial" si vous utilisez la licence d'essai) :

{
  "license": {
    "status": "active",
    "uid": "d8c2b566-8c2b-436c-a1ea-5502d6790505",
    "type": "trial",
    "issue_date": "2026-09-17T23:19:25.053Z",
    "issue_date_in_millis": 1789687165053,
    "expiry_date": "2026-10-17T23:19:25.053Z",
    "expiry_date_in_millis": 1792279165053,
    "max_nodes": 1000,
    "max_resource_units": null,
    "issued_to": "elastic-cluster",
    "issuer": "elasticsearch",
    "start_date_in_millis": -1
  }
}

Charger un exemple d'ensemble de données intégré

Pour évaluer les fonctionnalités d'IA d'Elastic sans écrire de pipelines d'ingestion de données personnalisés ni nécessiter d'accès à Internet externe, vous pouvez charger l'ensemble de données d'exemples de journaux Web intégré de Kibana (kibana_sample_data_logs). Comme l'archive de données d'exemple est intégrée à l'image de conteneur Kibana, elle s'installe en quelques secondes dans un environnement sous air gap. L'ensemble de données contient 14 074 documents de journaux d'accès Web contenant des adresses IP client (clientip), des codes d'état HTTP (response), des pays source et de destination (geo.src, geo.dest), des octets transférés (bytes), des URL et des chaînes user-agent.

Consultez Ajouter des exemples de données dans Kibana et elastic-datasets-builtin.md pour obtenir des ensembles de données intégrés supplémentaires (flights et ecommerce).

Charger l'ensemble de données

Exécutez la commande suivante pour charger kibana_sample_data_logs via le point de terminaison d'exemples de données internes de Kibana :

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  -X POST "https://${KIBANA_IP}:5601/api/sample_data/logs" \
  -H "kbn-xsrf: true" \
  -H "x-elastic-internal-origin: Kibana"

Vérifier l'exemple d'ensemble de données

Vérifiez que l'exemple d'ensemble de données logs est marqué comme installed dans Kibana et que le flux de données de sauvegarde contient des documents 14,074 dans Elasticsearch :

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  "https://${KIBANA_IP}:5601/api/sample_data" \
  -H "kbn-xsrf: true" \
  -H "x-elastic-internal-origin: Kibana" \
  | jq '.[] | select(.id == "logs") | {id, name, status}'

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  "https://${ELASTICSEARCH_IP}:9200/_cat/indices/kibana_sample_data_logs*?v"

Le résultat doit ressembler à ce qui suit :

{
  "id": "logs",
  "name": "Sample web logs",
  "status": "installed"
}
health status index                                         uuid                   pri rep docs.count docs.deleted store.size pri.store.size dataset.size
yellow open   .ds-kibana_sample_data_logs-2026.09.22-000001 0YivG3oFQQmb-mYqmULr_g   1   1      14074            0      8.7mb          8.7mb        8.7mb

Exigences concernant le service LLM

Dans un environnement GDC sous air gap, les LLM gérés Elastic hébergés dans le cloud et les fournisseurs de LLM SaaS publics externes sont inaccessibles. Selon votre environnement GDC :

  • Gemini géré sur GDC : dans les environnements GDC qui proposent Gemini en tant que service LLM géré, vous pouvez connecter Kibana directement au service Gemini géré, s'il est disponible.
  • Service LLM autogéré : vous pouvez également connecter Elastic à un service LLM autogéré hébergé dans votre environnement GDC (par exemple, sur des nœuds GPU GDC).

Le déploiement d'un service LLM autogéré n'entre pas dans le cadre de ce guide. Lorsque vous provisionnez ou sélectionnez un service LLM autogéré pour Elastic, assurez-vous qu'il répond aux exigences issues des guides officiels d'Elastic :

Résumé des exigences techniques

Les points suivants résument les exigences clés concernant l'API, les capacités du modèle, la fenêtre de contexte et l'environnement d'exécution nécessaires pour qu'un service LLM autogéré fonctionne de manière fiable avec l'agent IA, Agent Builder et Attack Discovery d'Elastic. Notez que les noms de paramètres de configuration spécifiques cités dans les sections suivantes (tels que num_ctx, num_predict, repeat_penalty, repeat_last_n, OLLAMA_FLASH_ATTENTION et OLLAMA_NUM_PARALLEL) sont fournis en utilisant Ollama comme exemple. Appliquez les paramètres équivalents si vous utilisez un autre moteur d'inférence, tel que vLLM.

  • API de complétion de chat compatible avec OpenAI (/v1/chat/completions) avec appel de fonction natif :
    • Tout serveur d'inférence autogéré (tel qu'Ollama ou vLLM) qui expose un point de terminaison /v1/chat/completions compatible avec OpenAI (y compris le streaming d'événements envoyés par le serveur via "stream": true) peut être intégré à l'aide du connecteur OpenAI de Kibana, avec le fournisseur défini sur Autre (service compatible avec OpenAI).
    • Activez l'appel de fonction natif ("enableNativeFunctionCalling": true) sur le connecteur Kibana lorsque vous utilisez des modèles compatibles avec l'appel d'outil cloud natif (comme gemma4:31b). Par défaut ("enableNativeFunctionCalling": false), Kibana utilise l'appel de fonction simulé basé sur un prompt, qui est moins fiable lors des appels d'outils imbriqués et peut échouer avec des erreurs d'analyse (comme Error: Missing name for tool use). L'activation de l'appel de fonction natif délègue la mise en forme de l'appel d'outil directement au protocole OpenAI tools du serveur d'inférence.
  • Modèle réglé par instructions avec des fonctionnalités d'appel de fonction et d'outil natif :
    • L'agent IA Elastic (elastic-ai-agent) et les agents Agent Builder personnalisés s'appuient sur l'appel d'outils en plusieurs étapes (platform.core.list_indices, platform.core.get_index_mapping, platform.core.search, platform.core.execute_esql et platform.core.cases). Ils utilisent ces outils pour découvrir les mappages d'index, générer et exécuter des requêtes ES|QL sur Elasticsearch, et synthétiser les résultats.
    • Le modèle choisi doit être réglé par instructions et capable de générer des appels d'outils cloud natifs valides, des sorties JSON structurées et des requêtes ES|QL. Pour connaître les critères d'évaluation des modèles, consultez la matrice de performances des LLM pour l'observabilité et la matrice de performances des LLM pour Elastic Security d'Elastic.
  • Fenêtre de contexte d'au moins 128,000 jetons (par exemple, num_ctx: 131072 dans Ollama) :
    • Bien que la documentation générale d'Elastic sur les LLM personnalisés autogérés indique un minimum de 64 000 jetons, une fenêtre de contexte de 128 000 jetons (131,072 jetons) est vivement recommandée pour l'agent IA Elastic (elastic-ai-agent).
    • Lors d'investigations en plusieurs étapes, l'agent combine les instructions système et les définitions d'outils avec les schémas d'index, la documentation ES|QL et les résultats de recherche bruts (par exemple, en récupérant jusqu'à 100 exemples de documents pour inspecter les valeurs des champs avant l'agrégation). Une seule investigation peut dépasser 70 000 jetons dans l'historique de la conversation, ce qui entraîne la troncature de la requête en milieu de conversation par une fenêtre de contexte de 64 000 jetons.
    • Notez que dans Ollama, une directive PARAMETER num_ctx intégrée au Modelfile d'un modèle remplace la variable d'environnement OLLAMA_CONTEXT_LENGTH du conteneur. Assurez-vous que OLLAMA_CONTEXT_LENGTH=131072 et PARAMETER num_ctx 131072 sont configurés dans le Modelfile du modèle (sur un GPU NVIDIA A100 ou H100 de 80 Go, gemma4:31b à un contexte de 128K tient confortablement dans la VRAM).
  • Limite de jetons de sortie et pénalité de répétition légère (par exemple, num_predict: 8192 et repeat_penalty: 1.05 dans Ollama) :
    • L'adaptateur de flux OpenAI de Kibana traite "finish_reason": "length" comme une erreur Token limit reached fatale et envoie "temperature": 0 (décodage gourmand) dans chaque requête.
    • Pour les modèles capables de raisonner (comme gemma4:31b, qui génère des jetons de réflexion internes avant d'émettre des appels d'outils), configurez une limite de jetons de sortie d'au moins 8,192 jetons et une pénalité de répétition légère. Par exemple, dans Ollama, définissez num_predict: 8192, repeat_penalty: 1.05 et repeat_last_n: 64. Évitez les pénalités de répétition trop agressives (comme 1.15+ sur de grandes fenêtres), car elles pénalisent le modèle lors de la copie d'une requête ES|QL de son bloc de raisonnement interne vers le JSON d'appel d'outil final.
  • Accélération GPU, Flash Attention et emplacements de cache KV parallèles (< 60 s par tour) :
    • Le connecteur OpenAI de Kibana applique un délai avant expiration HTTP par défaut de 60 secondes par tour de streaming.
    • Étant donné qu'un seul tour d'agent peut déclencher des sous-appels LLM imbriqués (par exemple, pour traduire des requêtes de recherche en langage naturel en requêtes ES|QL pendant que la conversation de l'agent principal est active), l'exécution d'un serveur d'inférence avec un seul emplacement de cache KV peut entraîner l'éviction du contexte mis en cache de la conversation principale par les sous-appels et ralentir les tours suivants.
    • Dans Ollama, définissez OLLAMA_FLASH_ATTENTION=1 et OLLAMA_NUM_PARALLEL=2 pour que Flash Attention accélère l'évaluation des contextes longs, et que la conversation principale et les sous-appels imbriqués conservent des emplacements de cache KV distincts dans la mémoire du GPU.

Configurer le connecteur LLM dans Kibana

Une fois que votre service LLM compatible avec OpenAI est en cours d'exécution et accessible depuis le pod Kibana, configurez un connecteur OpenAI dans Kibana à l'aide de la CLI ou de l'interface utilisateur de Kibana.

Option 1 : Créer le connecteur via la CLI

  1. Exécutez la commande POST /api/actions/connector suivante pour créer un connecteur OpenAI (.gen-ai) pointant vers votre service LLM (${LLM_API_URL}) avec l'appel de fonction natif activé ("enableNativeFunctionCalling": true). Kibana génère et attribue automatiquement un ID de connecteur unique :

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/actions/connector" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d "{
        \"name\": \"${LLM_CONNECTOR_NAME}\",
        \"connector_type_id\": \".gen-ai\",
        \"config\": {
          \"apiProvider\": \"Other\",
          \"apiUrl\": \"${LLM_API_URL}\",
          \"defaultModel\": \"${LLM_MODEL_NAME}\",
          \"enableNativeFunctionCalling\": true
        },
        \"secrets\": {
          \"apiKey\": \"${LLM_API_KEY}\"
        }
      }" | jq .
    
  2. Vérifiez que le connecteur ${LLM_CONNECTOR_NAME} existe et qu'aucun secret n'est manquant en listant les connecteurs de Kibana et en filtrant par nom :

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      "https://${KIBANA_IP}:5601/api/actions/connectors" \
      -H "kbn-xsrf: true" \
      | jq ".[] | select(.name == \"${LLM_CONNECTOR_NAME}\")"
    

    Le résultat doit ressembler à ce qui suit :

    {
      "id": "<GENERATED_CONNECTOR_ID>",
      "name": "<LLM_CONNECTOR_NAME>",
      "config": {
        "apiProvider": "Other",
        "apiUrl": "<LLM_API_URL>",
        "defaultModel": "<LLM_MODEL_NAME>",
        "enableNativeFunctionCalling": true
      },
      "connector_type_id": ".gen-ai",
      "is_preconfigured": false,
      "is_deprecated": false,
      "referenced_by_count": 0,
      "is_missing_secrets": false,
      "is_system_action": false
    }
    
  3. Pour tester la connectivité de bout en bout entre Kibana et le service LLM à partir de la CLI, recherchez l'ID du connecteur généré automatiquement par son nom et appelez son point de terminaison _execute :

    export CONNECTOR_ID=$(curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      "https://${KIBANA_IP}:5601/api/actions/connectors" \
      -H "kbn-xsrf: true" \
      | jq -r ".[] | select(.name == \"${LLM_CONNECTOR_NAME}\") | .id")
    
    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/actions/connector/${CONNECTOR_ID}/_execute" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d "{
        \"params\": {
          \"subAction\": \"invokeAI\",
          \"subActionParams\": {
            \"model\": \"${LLM_MODEL_NAME}\",
            \"messages\": [
              {
                \"role\": \"user\",
                \"content\": \"Reply with the single word Pong.\"
              }
            ]
          }
        }
      }" | jq .
    

    Le résultat doit ressembler à ce qui suit, avec "status": "ok" et la réponse du modèle :

    {
      "status": "ok",
      "data": {
        "message": "Pong",
        "usage": {
          "prompt_tokens": 23,
          "prompt_tokens_details": {
            "cached_tokens": 0
          },
          "completion_tokens": 38,
          "total_tokens": 61
        }
      },
      "connector_id": "<GENERATED_CONNECTOR_ID>"
    }
    

Option 2 : Créer le connecteur via l'interface utilisateur de Kibana

  1. Connectez-vous à Kibana (https://${KIBANA_IP}:5601).
  2. Ouvrez le menu de navigation principal, puis accédez à Gestion > Alertes et insights > Connecteurs.
  3. Cliquez sur Créer un connecteur, puis sélectionnez OpenAI.
  4. Configurez les champs du connecteur :
    • Nom du connecteur : saisissez la valeur de ${LLM_CONNECTOR_NAME} (par exemple, Gemma4-31B).
    • Sélectionner un fournisseur OpenAI : sélectionnez Autre (service compatible avec OpenAI).
    • URL : saisissez la valeur de ${LLM_API_URL} (le point de terminaison /v1/chat/completions de votre service LLM).
    • Modèle par défaut : saisissez la valeur de ${LLM_MODEL_NAME} (par exemple, gemma4:31b).
    • Clé API : saisissez la valeur de ${LLM_API_KEY} (toute chaîne non vide si votre serveur n'applique pas l'authentification du porteur, ou la clé API de votre serveur).
    • Activer l'appel de fonction natif : activez le bouton.
  5. Cliquez sur Enregistrer et tester pour enregistrer le connecteur et ouvrir immédiatement l'onglet Tester.
  6. Dans l'onglet Test, saisissez un exemple de prompt (par exemple, Reply with the single word Pong.) et cliquez sur Run (Exécuter) pour vérifier que Kibana reçoit une réponse valide du service LLM.

Tester les intégrations de LLM

Une fois l'ensemble de données kibana_sample_data_logs installé et votre connecteur LLM validé, vous pouvez tester les fonctionnalités d'IA d'Elastic dans Kibana.

Agent IA Elastic

L'agent IA Elastic (elastic-ai-agent) est l'agent conversationnel par défaut intégré à Kibana pour explorer et analyser les données dans Elasticsearch à l'aide du langage naturel. Grâce au connecteur LLM que vous avez configuré et aux outils Agent Builder intégrés, l'agent découvre automatiquement les index et les mappages Elasticsearch (platform.core.list_indices, platform.core.get_index_mapping), génère et exécute des requêtes ES|QL (platform.core.search, platform.core.execute_esql), et synthétise les résultats sans que vous ayez besoin de connaître le schéma sous-jacent à l'avance. Vous pouvez également créer vos propres agents personnalisés avec des instructions et des outils adaptés. Pour en savoir plus, consultez Agents personnalisés avec Agent Builder.

Pour ouvrir et configurer l'agent IA Elastic dans l'interface utilisateur de Kibana :

  1. Ouvrez le menu de navigation principal dans Kibana et sélectionnez Agent IA (ou cliquez sur le bouton Demander à l'IA dans la barre d'en-tête supérieure, ou accédez directement à https://${KIBANA_IP}:5601/app/chat).
  2. Assurez-vous qu'Elastic agent IA est sélectionné comme agent actif et que votre connecteur LLM (${LLM_CONNECTOR_NAME}) est sélectionné comme connecteur de modèle.

Exemple 1 : Examiner les requêtes HTTP ayant échoué et les scanners potentiels

Demandez à l'agent IA Elastic d'identifier les principales adresses IP client générant des réponses HTTP autres que 200 dans kibana_sample_data_logs, leurs pays d'origine et si l'une de ces adresses IP a également généré du trafic HTTP 200 réussi :

  • Option 1 : Via l'UI Kibana : saisissez la question suivante dans le prompt de chat de l'agent IA :

    In kibana_sample_data_logs, which client IP addresses had the most failed HTTP requests (non-200 status codes), what countries did they come from, and did any of those IPs also have successful HTTP 200 requests?

  • Option 2 : via la CLI

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/agent_builder/converse" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d "{
        \"agent_id\": \"elastic-ai-agent\",
        \"connector_id\": \"${CONNECTOR_ID}\",
        \"input\": \"In kibana_sample_data_logs, which client IP addresses had the most failed HTTP requests (non-200 status codes), what countries did they come from, and did any of those IPs also have successful HTTP 200 requests?\"
      }" | jq '{
        status: .status,
        tools_called: [.steps[] | select(.type == "tool_call") | {tool_id: .tool_id, params: .params}],
        response: .response.message
      }'
    

Que se passe-t-il en arrière-plan ?

  1. L'agent appelle platform.core.get_index_mapping(indices=["kibana_sample_data_logs"]) pour inspecter les champs disponibles (clientip, geo.src, response, bytes, request, etc.).
  2. Il appelle platform.core.search pour inspecter des exemples de documents et confirmer que les codes d'état HTTP sont stockés dans response, puis exécute des agrégations ES|QL pour classer les requêtes non-200 par clientip et geo.src, et vérifier lesquelles des principales adresses IP ont également enregistré du trafic response == "200" :

    FROM kibana_sample_data_logs
    | WHERE response != "200"
    | STATS failed_count = COUNT(*) BY clientip, geo.src
    | SORT failed_count DESC
    | LIMIT 10
    
    FROM kibana_sample_data_logs
    | WHERE clientip IN ("30.156.16.164", "213.235.30.99", "221.241.228.46", "209.94.234.133", "7.127.73.138", "247.240.202.244", "130.125.65.24", "164.85.94.243", "81.194.200.150", "30.145.143.210") AND response == "200"
    | STATS success_count = COUNT(*) BY clientip
    

La réponse de l'agent IA Elastic doit ressembler à ceci :

L'adresse IP client avec le plus de requêtes HTTP ayant échoué (codes d'état autres que 200) dans kibana_sample_data_logs est 30.156.16.164, qui provient des États-Unis et a enregistré 100 requêtes ayant échoué.

Voici d'autres adresses IP principales ayant généré des requêtes ayant échoué, toutes provenant également des États-Unis :

  • 213.235.30.99 : 6 échecs
  • 221.241.228.46 : 6 échecs
  • 209.94.234.133 : 6 échecs
  • 7.127.73.138 : 5 échecs
  • 247.240.202.244 : 5 échecs
  • 130.125.65.24 : 5 échecs
  • 164.85.94.243 : 4 échecs
  • 81.194.200.150 : 4 échecs
  • 30.145.143.210 : 4 échecs

En ce qui concerne les requêtes réussies, toutes les adresses IP listées ci-dessus ont reçu au moins quelques réponses HTTP 200 réussies, à l'exception de 30.156.16.164, qui n'a reçu aucune requête réussie.

Exemple 2 : Analyser le volume de trafic et les octets de charge utile par code d'état HTTP

Demandez à l'agent IA Elastic la répartition des codes d'état de réponse HTTP pour toutes les requêtes dans kibana_sample_data_logs et le nombre total d'octets transférés pour chaque code d'état :

  • Option 1 : Via l'UI Kibana : démarrez une nouvelle conversation dans l'agent IA et saisissez la question suivante :

    In kibana_sample_data_logs, what is the breakdown of HTTP response status codes across all requests, and how much total data (in bytes) was transferred for each status code?

  • Option 2 : via la CLI

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/agent_builder/converse" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d "{
        \"agent_id\": \"elastic-ai-agent\",
        \"connector_id\": \"${CONNECTOR_ID}\",
        \"input\": \"In kibana_sample_data_logs, what is the breakdown of HTTP response status codes across all requests, and how much total data (in bytes) was transferred for each status code?\"
      }" | jq '{
        status: .status,
        tools_called: [.steps[] | select(.type == "tool_call") | {tool_id: .tool_id, params: .params}],
        response: .response.message
      }'
    

Que se passe-t-il en arrière-plan ?

  1. L'agent inspecte le schéma kibana_sample_data_logs via platform.core.get_index_mapping, en identifiant les champs response.keyword et bytes.
  2. Il appelle platform.core.search, qui génère et exécute l'agrégation ES|QL suivante :

    FROM kibana_sample_data_logs
    | STATS request_count = COUNT(*), total_bytes = SUM(bytes) BY response.keyword
    | LIMIT 100
    

La réponse de l'agent IA Elastic doit ressembler à ceci :

La répartition des codes d'état de réponse HTTP et le total des données transférées dans kibana_sample_data_logs sont les suivants :

Code d'état Nombre de requêtes Total des données transférées (octets)
200 12 832 75 681 246
404 801 4 044 443
503 441 0

Agents personnalisés avec Agent Builder

Agent Builder vous permet de créer des agents IA personnalisés adaptés à des workflows opérationnels spécifiques en combinant des instructions système personnalisées avec des outils intégrés (comme platform.core.cases, platform.core.execute_esql et platform.core.search) et des outils définis par l'utilisateur (esql, index_search, workflow et mcp).

L'exemple suivant crée un agent Web Security Incident Responder qui opère directement sur l'ensemble de données kibana_sample_data_logs. À partir d'une requête en langage naturel, l'agent :

  1. Exécute un outil ES|QL personnalisé (investigate_suspicious_web_clients) pour agréger les réponses HTTP non-200 dans kibana_sample_data_logs par adresse IP du client.
  2. Distingue les activités de reconnaissance et d'exploitation malveillantes (30.156.16.164) des échecs de téléchargement bénins.
  3. Appelle un outil de workflow Kibana personnalisé (create_security_case) pour ouvrir automatiquement un cas de sécurité Kibana structuré avec ses résultats et ses étapes de correction.

Créer des outils personnalisés de création de requêtes ES|QL et de cas

  1. Créez un outil esql personnalisé nommé investigate_suspicious_web_clients qui interroge kibana_sample_data_logs pour obtenir les principales adresses IP client générant des codes d'état HTTP autres que 200, ainsi que leurs codes d'état, leur pays d'origine (geo.src), leurs user-agents (agent) et les URL demandées (request) :

    • Option 1 : Via l'interface utilisateur de Kibana

      1. Dans Kibana, ouvrez le menu de navigation principal et accédez à Elasticsearch > Agents, puis cliquez sur Gérer tous les outils (ou accédez directement à https://${KIBANA_IP}:5601/app/agent_builder/tools) et cliquez sur Nouvel outil.
      2. Configurez les champs de l'outil :

        • ID de l'outil : investigate_suspicious_web_clients
        • Type : ES|QL
        • Description : Finds the top client IP addresses generating non-200 HTTP error responses in kibana_sample_data_logs, returning their error count, status codes, source country, user agents, and requested URL paths.
        • Étiquettes : security, logs
        • Requête ES|QL :

          FROM kibana_sample_data_logs | WHERE response != "200" | STATS error_count = COUNT(*), status_codes = VALUES(response), country = VALUES(geo.src), user_agents = VALUES(agent), sample_requests = VALUES(request) BY clientip | SORT error_count DESC | LIMIT 5
          
      3. Cliquez sur Enregistrer.

    • Option 2 : via la CLI

      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        -X POST "https://${KIBANA_IP}:5601/api/agent_builder/tools" \
        -H "kbn-xsrf: true" \
        -H "Content-Type: application/json" \
        -d '{
          "id": "investigate_suspicious_web_clients",
          "type": "esql",
          "description": "Finds the top client IP addresses generating non-200 HTTP error responses in kibana_sample_data_logs, returning their error count, status codes, source country, user agents, and requested URL paths.",
          "tags": ["security", "logs"],
          "configuration": {
            "query": "FROM kibana_sample_data_logs | WHERE response != \"200\" | STATS error_count = COUNT(*), status_codes = VALUES(response), country = VALUES(geo.src), user_agents = VALUES(agent), sample_requests = VALUES(request) BY clientip | SORT error_count DESC | LIMIT 5",
            "params": {}
          }
        }' | jq '{id: .id, type: .type}'
      
  2. Étant donné que l'outil platform.core.cases intégré d'Agent Builder est en lecture seule (utilisé pour rechercher et récupérer des demandes existantes), créez un workflow Kibana (create-security-case) qui appelle l'étape cases.createCase native de Kibana, puis enregistrez-le en tant qu'outil workflow nommé create_security_case :

    • Option 1 : Via l'interface utilisateur de Kibana

      1. Ouvrez le menu de navigation principal, accédez à Gestion > Workflows (ou accédez directement à https://${KIBANA_IP}:5601/app/workflows), puis cliquez sur Créer un workflow.
      2. Dans l'éditeur YAML, collez la définition de workflow suivante, puis cliquez sur Enregistrer :

        version: "1"
        name: Create Security Case
        description: Creates a new Kibana Case in Elastic Security
        enabled: true
        triggers:
          - type: manual
        inputs:
          - name: title
            type: string
            required: true
            description: Title of the security case
          - name: description
            type: string
            required: true
            description: Detailed markdown description of the incident findings
          - name: severity
            type: string
            default: high
            description: Severity of the case (low, medium, high, critical)
        steps:
          - name: create_case
            type: cases.createCase
            with:
              title: ${{ inputs.title }}
              description: ${{ inputs.description }}
              severity: ${{ inputs.severity }}
              owner: securitySolution
              tags:
                - agent-builder
                - web-logs
        
      3. Revenez à la page Outils d'Agent Builder (https://${KIBANA_IP}:5601/app/agent_builder/tools ou accédez à Elasticsearch > Agents, puis cliquez sur Gérer tous les outils) et cliquez sur Nouvel outil.

      4. Configurez les champs de l'outil de workflow, puis cliquez sur Enregistrer :

        • ID de l'outil : create_security_case
        • Type : Workflow
        • Workflow : sélectionnez Créer une demande de sécurité (create-security-case).
        • Description : Creates a new Kibana Case in Elastic Security with a title, markdown description of the incident findings, and severity (low, medium, high, critical).
        • Étiquettes : security, cases
    • Option 2 : via la CLI

      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        -X POST "https://${KIBANA_IP}:5601/api/workflows" \
        -H "kbn-xsrf: true" \
        -H "Content-Type: application/json" \
        -d '{
          "workflows": [
            {
              "yaml": "version: \"1\"\nname: Create Security Case\ndescription: Creates a new Kibana Case in Elastic Security\nenabled: true\ntriggers:\n  - type: manual\ninputs:\n  - name: title\n    type: string\n    required: true\n    description: Title of the security case\n  - name: description\n    type: string\n    required: true\n    description: Detailed markdown description of the incident findings\n  - name: severity\n    type: string\n    default: high\n    description: Severity of the case (low, medium, high, critical)\nsteps:\n  - name: create_case\n    type: cases.createCase\n    with:\n      title: ${{ inputs.title }}\n      description: ${{ inputs.description }}\n      severity: ${{ inputs.severity }}\n      owner: securitySolution\n      tags:\n        - agent-builder\n        - web-logs\n"
            }
          ]
        }' | jq '.created[] | {id: .id, valid: .valid}'
      
      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        -X POST "https://${KIBANA_IP}:5601/api/agent_builder/tools" \
        -H "kbn-xsrf: true" \
        -H "Content-Type: application/json" \
        -d '{
          "id": "create_security_case",
          "type": "workflow",
          "description": "Creates a new Kibana Case in Elastic Security with a title, markdown description of the incident findings, and severity (low, medium, high, critical).",
          "tags": ["security", "cases"],
          "configuration": {
            "workflow_id": "create-security-case"
          }
        }' | jq '{id: .id, type: .type}'
      

Créer l'agent personnalisé de gestion des incidents

Créez l'agent web-security-incident-responder et attribuez-lui les outils investigate_suspicious_web_clients, create_security_case et platform.core.cases :

  • Option 1 : Via l'interface utilisateur Kibana

    1. Ouvrez le menu de navigation principal, puis accédez à Elasticsearch > Agents (ou accédez directement à https://${KIBANA_IP}:5601/app/agent_builder/agents), puis cliquez sur New agent (Nouvel agent).
    2. Configurez les détails de l'agent :

      • ID de l'agent : web-security-incident-responder
      • Display name (Nom à afficher) : Web Security Incident Responder
      • Description à afficher : Investigates suspicious HTTP error activity in web access logs and opens Kibana Security Cases with the findings.
      • Libellés : security, incident-response
      • Instructions personnalisées :

        You are an automated Web Security Incident Responder. When asked to investigate suspicious web traffic:
        1. Call the `investigate_suspicious_web_clients` tool to inspect the top client IPs generating non-200 HTTP responses in `kibana_sample_data_logs`.
        2. Identify any client IP exhibiting malicious reconnaissance or exploitation behavior (such as Nmap scans, WordPress login probing, `/etc/passwd` path traversal, or `/level/15/exec` command execution attempts) versus benign download errors.
        3. Call the `create_security_case` tool to open a new Kibana Security Case for the malicious client IP. Set an appropriate `title`, `severity` (`high`), and a well-structured Markdown `description` summarizing the source IP, geolocation, total failed requests, observed attack categories (mapped to MITRE ATT&CK where applicable), sample malicious URLs, and recommended remediation steps.
        4. Return a concise summary of your investigation and confirm the created Kibana Case details.
        
      • Outils : sélectionnez investigate_suspicious_web_clients, create_security_case et platform.core.cases.

    3. Cliquez sur Enregistrer.

  • Option 2 : via la CLI

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/agent_builder/agents" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d '{
        "id": "web-security-incident-responder",
        "name": "Web Security Incident Responder",
        "description": "Investigates suspicious HTTP error activity in web access logs and opens Kibana Security Cases with the findings.",
        "labels": ["security", "incident-response"],
        "avatar_color": "#E7664C",
        "avatar_symbol": "IR",
        "configuration": {
          "instructions": "You are an automated Web Security Incident Responder. When asked to investigate suspicious web traffic:\n1. Call the `investigate_suspicious_web_clients` tool to inspect the top client IPs generating non-200 HTTP responses in `kibana_sample_data_logs`.\n2. Identify any client IP exhibiting malicious reconnaissance or exploitation behavior (such as Nmap scans, WordPress login probing, `/etc/passwd` path traversal, or `/level/15/exec` command execution attempts) versus benign download errors.\n3. Call the `create_security_case` tool to open a new Kibana Security Case for the malicious client IP. Set an appropriate `title`, `severity` (`high`), and a well-structured Markdown `description` summarizing the source IP, geolocation, total failed requests, observed attack categories (mapped to MITRE ATT&CK where applicable), sample malicious URLs, and recommended remediation steps.\n4. Return a concise summary of your investigation and confirm the created Kibana Case details.",
          "tools": [
            {
              "tool_ids": [
                "investigate_suspicious_web_clients",
                "create_security_case",
                "platform.core.cases"
              ]
            }
          ]
        }
      }' | jq '{id: .id, name: .name}'
    

Exécuter l'agent et vérifier le cas Kibana

  1. Lancez une conversation avec l'agent Web Security Incident Responder :

    • Option 1 : Via l'interface utilisateur Kibana

      1. Ouvrez le menu de navigation principal, puis accédez à Elasticsearch > Agents (ou accédez directement à https://${KIBANA_IP}:5601/app/agent_builder).
      2. Sélectionnez Web Security Incident Responder (Répondeur aux incidents de sécurité Web) dans le sélecteur d'agents et assurez-vous que votre connecteur LLM (${LLM_CONNECTOR_NAME}) est sélectionné.
      3. Saisissez le prompt suivant :

        Investigate the top client IPs generating HTTP errors in our web logs, identify any attacker performing reconnaissance or exploitation, and open a high-severity Kibana Security Case with your findings.

    • Option 2 : via la CLI

      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        -X POST "https://${KIBANA_IP}:5601/api/agent_builder/converse" \
        -H "kbn-xsrf: true" \
        -H "Content-Type: application/json" \
        -d "{
          \"agent_id\": \"web-security-incident-responder\",
          \"connector_id\": \"${CONNECTOR_ID}\",
          \"input\": \"Investigate the top client IPs generating HTTP errors in our web logs, identify any attacker performing reconnaissance or exploitation, and open a high-severity Kibana Security Case with your findings.\"
        }" | jq '{
          status: .status,
          tools_called: [.steps[] | select(.type == "tool_call") | .tool_id],
          response: .response.message
        }'
      

      Le résultat doit ressembler à ce qui suit :

      {
        "status": "completed",
        "tools_called": [
          "investigate_suspicious_web_clients",
          "create_security_case"
        ],
        "response": "I have completed the investigation into the web logs. \n\nThe analysis identified client IP **`30.156.16.164`** (located in the US) as a malicious actor. This IP generated 100 failed requests (HTTP 404) exhibiting clear patterns of reconnaissance and exploitation:\n\n*   **Reconnaissance:** Extensive probing for WordPress installations and plugins, as well as Nmap scan signatures (e.g., `/nmaplowercheck...`).\n*   **Path Traversal:** Attempts to access sensitive system files via `/etc/passwd`.\n*   **Remote Command Execution (RCE):** Attempts to execute commands through paths such as `/level/15/exec`.\n*   **Application Probing:** Targeting common management interfaces including Tomcat, Axis2, and JMX consoles.\n\nAs a result, I have opened a **high-severity** Kibana Security Case to track this incident:\n\n*   **Case ID:** `1f0e00a5-cc89-4f62-b305-f286fae3d70e`\n*   **Title:** Malicious Reconnaissance and Exploitation Attempt from 30.156.16.164\n*   **Recommended Action:** Immediately block the IP at the firewall/WAF and verify that no unauthorized access was achieved."
      }
      
  2. Vérifiez le cas de sécurité Kibana que vous venez de créer :

    • Option 1 : Via l'interface utilisateur de Kibana

      1. Ouvrez le menu de navigation principal, puis accédez à Sécurité > Demandes (ou accédez directement à https://${KIBANA_IP}:5601/app/security/cases).
      2. Cliquez sur Malicious Reconnaissance and Exploitation Attempt from 30.156.16.164 (Tentative de reconnaissance et d'exploitation malveillante depuis l'adresse 30.156.16.164) pour afficher le résumé de l'incident au format Markdown, l'analyse MITRE ATT&CK, des exemples d'URL malveillantes et la checklist de correction.
    • Option 2 : via la CLI

      curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
        "https://${KIBANA_IP}:5601/api/cases/_find" \
        -H "kbn-xsrf: true" \
        | jq '.cases[0] | {id: .id, title: .title, severity: .severity, status: .status, tags: .tags, description: .description}'
      

      Le résultat doit ressembler à ce qui suit :

      {
        "id": "1f0e00a5-cc89-4f62-b305-f286fae3d70e",
        "title": "Malicious Reconnaissance and Exploitation Attempt from 30.156.16.164",
        "severity": "high",
        "status": "open",
        "tags": [
          "agent-builder",
          "web-logs"
        ],
        "description": "### Incident Summary\n**Source IP:** `30.156.16.164`\n**Geolocation:** US\n**Total Failed Requests:** 100\n**Status Codes Observed:** 404\n\n### Analysis\nThe client IP exhibited clear patterns of malicious reconnaissance and exploitation attempts:\n\n- **Reconnaissance (MITRE ATT&CK T1595):** Extensive probing for WordPress installations (`/wp-login.php`, various plugin paths) and Nmap scan signatures (`/nmaplowercheck...`).\n- **Path Traversal (MITRE ATT&CK T1083):** Attempted to access sensitive system files via `/cgi-bin/mj_wwwusr?passw=&list=GLOBAL&user=&func=help&extra=/../../../../../../../../etc/passwd`.\n- **Remote Command Execution (MITRE ATT&CK T1203):** Attempts to execute commands via paths like `/level/15/exec/-/configure/http`.\n- **Application Probing:** Targeted common management interfaces such as `/tomcat/manager/html/`, `/axis2/axis2-admin/`, and `/jmx-console/`.\n\n### Sample Malicious URLs\n- `/cgi-bin/mj_wwwusr?passw=&list=GLOBAL&user=&func=help&extra=/../../../../../../../../etc/passwd`\n- `/level/15/exec/-/configure/http`\n- `/nmaplowercheck1331872976`\n- `/wp-login.php`\n\n### Recommended Remediation\n1. **Block IP:** Immediately block `30.156.16.164` at the network firewall/WAF.\n2. **Log Review:** Verify that all requests from this IP resulted in 404 errors and no unauthorized access was gained.\n3. **Hardening:** Ensure that sensitive directories (e.g., `/cgi-bin/`, `/manager/`) are properly restricted."
      }
      

Détection des attaques

Attack Discovery est une fonctionnalité d'IA d'Elastic Security qui analyse les alertes de sécurité ouvertes, les met en correspondance avec le framework MITRE ATT&CK et met en corrélation les alertes, les adresses IP et les comptes utilisateur associés dans des chaînes d'attaque visuelles. Étant donné qu'Attack Discovery nécessite un modèle volumineux pour synthétiser de manière fiable des sorties JSON structurées multi-alertes, utilisez un modèle de classe 30B ou plus (tel que gemma4:31b).

Générer des alertes de sécurité à partir des journaux d'exemple

Par défaut, l'ensemble de données kibana_sample_data_logs contient des journaux d'accès Web bruts et ne remplit pas l'index des alertes Elastic Security (.alerts-security.alerts-default). De plus, le générateur d'exemples de données de Kibana attribue des valeurs @timestamp futures (jusqu'à 30 jours à l'avance) aux 100 requêtes d'analyse et d'exploitation de 30.156.16.164, ce qui les place en dehors de la fenêtre now-30d à now interrogée par les règles de détection Elastic Security.

Pour illustrer la découverte d'attaques avec les journaux exemples, décalons les @timestamp des enregistrements de 30.156.16.164 de 30 jours, enrichissons-les avec les champs source.ip et user.name d'Elastic Common Schema (ECS), et créons deux règles de détection qui génèrent des alertes de sécurité à partir de ces événements :

  1. Mappez clientip sur source.ip, définissez user.name (www-data) et décalez @timestamp de 30 jours en arrière sur les enregistrements 30.156.16.164 dans kibana_sample_data_logs* :

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${ELASTICSEARCH_IP}:9200/kibana_sample_data_logs*/_update_by_query?conflicts=proceed&refresh=true" \
      -H "Content-Type: application/json" \
      -d '{
        "query": {
          "term": {
            "clientip": "30.156.16.164"
          }
        },
        "script": {
          "source": "ctx._source.source = [\"ip\": ctx._source.clientip]; ctx._source.user = [\"name\": \"www-data\"]; ctx._source[\"@timestamp\"] = ZonedDateTime.parse(ctx._source[\"@timestamp\"]).minusDays(30).toString();",
          "lang": "painless"
        }
      }' | jq '{updated: .updated, failures: .failures}'
    
  2. Créez deux règles de détection Elastic Security ciblant kibana_sample_data_logs*. La première règle détecte les tentatives de connexion Nmap et WordPress (Reconnaissance), et la seconde détecte les tentatives de traversée de chemin d'accès et d'exécution de commandes à distance (Initial Access) :/etc/passwd

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/detection_engine/rules" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Suspicious Web Reconnaissance and Vulnerability Scanning",
        "description": "Detects Nmap active scanning and WordPress login/plugin probing in web access logs.",
        "risk_score": 47,
        "severity": "medium",
        "type": "query",
        "language": "kuery",
        "query": "clientip: \"30.156.16.164\" and (request: *nmap* or request: *wp-login*)",
        "index": ["kibana_sample_data_logs*"],
        "from": "now-30d",
        "interval": "1h",
        "max_signals": 2,
        "enabled": true,
        "threat": [
          {
            "framework": "MITRE ATT&CK",
            "tactic": {
              "id": "TA0043",
              "name": "Reconnaissance",
              "reference": "https://attack.mitre.org/tactics/TA0043/"
            },
            "technique": [
              {
                "id": "T1595",
                "name": "Active Scanning",
                "reference": "https://attack.mitre.org/techniques/T1595/"
              }
            ]
          }
        ]
      }' | jq '{id: .id, name: .name, enabled: .enabled}'
    
    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      -X POST "https://${KIBANA_IP}:5601/api/detection_engine/rules" \
      -H "kbn-xsrf: true" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Web Path Traversal and Remote Command Execution Attempt",
        "description": "Detects path traversal attempts targeting /etc/passwd and Cisco IOS privilege level 15 remote command execution (/level/15/exec).",
        "risk_score": 73,
        "severity": "high",
        "type": "query",
        "language": "kuery",
        "query": "clientip: \"30.156.16.164\" and (request: *passwd* or request: *exec*)",
        "index": ["kibana_sample_data_logs*"],
        "from": "now-30d",
        "interval": "1h",
        "max_signals": 2,
        "enabled": true,
        "threat": [
          {
            "framework": "MITRE ATT&CK",
            "tactic": {
              "id": "TA0001",
              "name": "Initial Access",
              "reference": "https://attack.mitre.org/tactics/TA0001/"
            },
            "technique": [
              {
                "id": "T1190",
                "name": "Exploit Public-Facing Application",
                "reference": "https://attack.mitre.org/techniques/T1190/"
              }
            ]
          }
        ]
      }' | jq '{id: .id, name: .name, enabled: .enabled}'
    
  3. Attendez environ 60 secondes que l'exécution initiale de la règle soit terminée, puis vérifiez que quatre alertes ont été indexées dans .alerts-security.alerts-default :

    curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
      "https://${ELASTICSEARCH_IP}:9200/.alerts-security.alerts-default/_search?size=4" \
      | jq '.hits.hits[]._source | {rule: .["kibana.alert.rule.name"], source_ip: .source.ip, user_name: .user.name, request: .request}'
    

    Le résultat doit ressembler à ce qui suit :

    {
      "rule": "Web Path Traversal and Remote Command Execution Attempt",
      "source_ip": "30.156.16.164",
      "user_name": "www-data",
      "request": "/cgi-bin/mj_wwwusr?passw=&list=GLOBAL&user=&func=help&extra=/../../../../../../../../etc/passwd"
    }
    {
      "rule": "Web Path Traversal and Remote Command Execution Attempt",
      "source_ip": "30.156.16.164",
      "user_name": "www-data",
      "request": "/cgi-bin/mj_wwwusr?passw=&list=GLOBAL&user=&func=help&extra=/../../../../../../../../etc/passwd"
    }
    {
      "rule": "Suspicious Web Reconnaissance and Vulnerability Scanning",
      "source_ip": "30.156.16.164",
      "user_name": "www-data",
      "request": "/nmaplowercheck1331872976"
    }
    {
      "rule": "Suspicious Web Reconnaissance and Vulnerability Scanning",
      "source_ip": "30.156.16.164",
      "user_name": "www-data",
      "request": "/nmaplowercheck1331873003"
    }
    

Exécuter Attack Discovery

  1. Dans Kibana, ouvrez le menu de navigation principal et accédez à Sécurité > Détection des attaques (ou accédez directement à https://${KIBANA_IP}:5601/app/security/attack_discovery).
  2. Sélectionnez votre connecteur LLM (${LLM_CONNECTOR_NAME}) dans le menu des connecteurs en haut de la page.
  3. Cliquez sur Générer pour analyser les alertes ouvertes dans .alerts-security.alerts-default.

Vous pouvez également déclencher et inspecter la découverte d'attaques à partir de la CLI :

export EXECUTION_UUID=$(curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  -X POST "https://${KIBANA_IP}:5601/api/attack_discovery/_generate" \
  -H "kbn-xsrf: true" \
  -H "elastic-api-version: 2023-10-31" \
  -H "Content-Type: application/json" \
  -d "{
    \"alertsIndexPattern\": \".alerts-security.alerts-default\",
    \"anonymizationFields\": [
      {\"id\": \"_id\", \"field\": \"_id\", \"allowed\": true},
      {\"id\": \"@timestamp\", \"field\": \"@timestamp\", \"allowed\": true},
      {\"id\": \"kibana.alert.original_time\", \"field\": \"kibana.alert.original_time\", \"allowed\": true},
      {\"id\": \"message\", \"field\": \"message\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.name\", \"field\": \"kibana.alert.rule.name\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.description\", \"field\": \"kibana.alert.rule.description\", \"allowed\": true},
      {\"id\": \"kibana.alert.severity\", \"field\": \"kibana.alert.severity\", \"allowed\": true},
      {\"id\": \"kibana.alert.risk_score\", \"field\": \"kibana.alert.risk_score\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.threat.tactic.id\", \"field\": \"kibana.alert.rule.threat.tactic.id\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.threat.tactic.name\", \"field\": \"kibana.alert.rule.threat.tactic.name\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.threat.technique.id\", \"field\": \"kibana.alert.rule.threat.technique.id\", \"allowed\": true},
      {\"id\": \"kibana.alert.rule.threat.technique.name\", \"field\": \"kibana.alert.rule.threat.technique.name\", \"allowed\": true},
      {\"id\": \"source.ip\", \"field\": \"source.ip\", \"allowed\": true},
      {\"id\": \"user.name\", \"field\": \"user.name\", \"allowed\": true}
    ],
    \"apiConfig\": {
      \"actionTypeId\": \".gen-ai\",
      \"connectorId\": \"${CONNECTOR_ID}\"
    },
    \"replacements\": {},
    \"size\": 20,
    \"subAction\": \"invokeAI\"
  }" | jq -r '.execution_uuid')

echo "Attack Discovery execution UUID: ${EXECUTION_UUID}"

Une fois la génération terminée (environ 60 à 90 secondes), récupérez la chaîne d'attaque découverte :

curl -sk -u "elastic:${ELASTIC_PASSWORD}" \
  "https://${KIBANA_IP}:5601/api/attack_discovery/_find" \
  -H "kbn-xsrf: true" \
  -H "elastic-api-version: 2023-10-31" \
  | jq '.data[0]'

La découverte d'attaque générée met en corrélation les quatre alertes en un seul incident avec l'adresse IP source, le compte utilisateur et les tactiques MITRE ATT&CK associés :

{
  "alert_ids": [
    "05c30118398ea2a8b70c55ce81fdb559dbe14c460a8a95253bd81719415a28b5",
    "73ebaeca440b463ffa5812da5b68a2485d465a648ffaf16385a54d8bae9a53d2",
    "60ae9f16b40cec1bdc27674ade0e5eb69680ccca3d956fbf2a75418843799987",
    "de503ae52444db7712d062c54750f413b90a75521cdfff0de9bdf41a64141346"
  ],
  "alert_rule_uuid": "attack_discovery_ad_hoc_rule_id",
  "alert_start": "2026-10-03T01:05:00.384Z",
  "alert_updated_at": "2026-10-03T01:05:03.460Z",
  "alert_workflow_status": "open",
  "alert_workflow_status_updated_at": "2026-10-03T01:05:03.460Z",
  "connector_id": "<GENERATED_CONNECTOR_ID>",
  "connector_name": "<GENERATED_CONNECTOR_ID>",
  "details_markdown": "- The attack sequence began at `2026-10-01T12:43:41.756Z` when an actor using IP `30.156.16.164` performed active reconnaissance scanning against the server.\n- Following the initial scan, the attacker attempted to exploit a path traversal vulnerability at `2026-10-01T12:43:51.756Z` and again at `2026-10-01T12:44:02.756Z`, specifically targeting the `/etc/passwd` file via the `mj_wwwusr` CGI endpoint.\n- The activity concluded with further reconnaissance scanning at `2026-10-01T12:44:21.756Z` from the same source IP `30.156.16.164`.\n- All requests were associated with the user `www-data` and resulted in 404 errors, suggesting the attempts were unsuccessful.",
  "entity_summary_markdown": "Activity targeting user `www-data` from source IP `30.156.16.164`.",
  "generation_uuid": "<EXECUTION_UUID>",
  "id": "1f71ed004c7340d5088d242ca2483525a164a9ea9a80a3f90bd1f514d43231bc",
  "mitre_attack_tactics": [
    "Reconnaissance",
    "Initial Access"
  ],
  "risk_score": 240,
  "summary_markdown": "An attacker from `30.156.16.164` performed reconnaissance and path traversal attempts against the system.",
  "timestamp": "2026-10-03T01:05:00.384Z",
  "title": "Web Reconnaissance and Exploitation Attempt",
  "user_id": "elastic",
  "user_name": "elastic",
  "users": [
    {
      "id": "elastic",
      "name": "elastic"
    }
  ],
  "index": ".internal.adhoc.alerts-security.attack.discovery.alerts-default-000001"
}