Подробный гайд: Скрипт авто-маппинга групп GLPI <-> Wazuh

Python-скрипт для авто-маппинга групп GLPI в Wazuh через REST API. Идемпотентная синхронизация, dry-run, логирование, готовая настройка cron/systemd.

2026.04.19                  


Подробный гайд: Скрипт авто-маппинга групп GLPI <-> WazuhПодробный гайд: Скрипт авто-маппинга групп GLPI <-> Wazuh Этот гайд описывает создание production-ready Python-скрипта для автоматической синхронизации групп между GLPI (ITSM/Asset Management) и Wazuh (SIEM/XDR). Скрипт работает по принципу GLPI -> Wazuh (GLPI как источник достоверности), создаёт отсутствующие группы, обновляет описания и опционально привязывает агентов.

Протестировано на: GLPI 10.0.x, Wazuh 4.4.x+, Python 3.9+


1. Архитектура и логика работы

Шаг Описание
1 Аутентификация в GLPI API -> получение session_token
2 Аутентификация в Wazuh API -> получение Bearer token
3 Выгрузка списка групп из GLPI (фильтрация по сущности/паттерну)
4 Выгрузка списка групп из Wazuh
5 Сравнение списков, санитизация имён (пробелы, спецсимволы)
6 Создание отсутствующих групп в Wazuh, обновление описаний
7 Логирование, dry-run режим, обработка ошибок

Направление синхронизации по умолчанию: GLPI -> Wazuh. При необходимости можно добавить обратный поток или двустороннюю логику.


2. Подготовка API

GLPI

  1. Перейдите: Настройка -> Общие -> API
  2. Включите Enable REST API
  3. Создайте API-пользователя или используйте свой аккаунт

4. Сгенерируйте:

  • App Token (глобальный)
  • User Token (для конкретного пользователя)
  1. Запомните базовый URL: https://glpi.example.com/apirest.php

Wazuh

  1. API включён по умолчанию на порту 55000

2. Создайте пользователя с правами group и agent:

   # В консоли Wazuh manager
   /var/ossec/bin/wazuh-control start
   /var/ossec/bin/manage_agents -a # (если нужно)

Или через UI: Management -> API -> Users

3. Проверьте доступ:

   curl -k -u wazuh:wazuh "https://localhost:55000/"

3. Установка зависимостей

mkdir glpi-wazuh-sync && cd glpi-wazuh-sync
python3 -m venv venv
source venv/bin/activate  # Linux/macOS
# venv\Scripts\activate  # Windows

pip install requests python-dotenv tenacity

4. Полный код скрипта (sync_groups.py)

#!/usr/bin/env python3
"""
GLPI <-> Wazuh Group Auto-Mapping Script
Версия: 1.0
Зависимости: requests, tenacity, python-dotenv
"""

import os
import re
import json
import logging
import requests
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from dotenv import load_dotenv

# Загрузка .env
load_dotenv()

# ================= НАСТРОЙКИ =================
GLPI_URL = os.getenv("GLPI_URL", "https://glpi.example.com/apirest.php")
GLPI_APP_TOKEN = os.getenv("GLPI_APP_TOKEN")
GLPI_USER_TOKEN = os.getenv("GLPI_USER_TOKEN")

WAZUH_URL = os.getenv("WAZUH_URL", "https://wazuh.example.com:55000")
WAZUH_USER = os.getenv("WAZUH_USER", "wazuh")
WAZUH_PASSWORD = os.getenv("WAZUH_PASSWORD", "wazuh")

DRY_RUN = os.getenv("DRY_RUN", "false").lower() == "true"
GLPI_ENTITY_ID = os.getenv("GLPI_ENTITY_ID")  # Опционально: фильтр по сущности
SYNC_DESCRIPTIONS = os.getenv("SYNC_DESCRIPTIONS", "true").lower() == "true"

# ================= LOGGING =================
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[logging.StreamHandler()]
)
logger = logging.getLogger(__name__)

# ================= УТИЛИТЫ =================
def sanitize_group_name(name: str) -> str:
    """Приводит имя группы к формату Wazuh: a-zA-Z0-9_-"""
    name = re.sub(r"[^a-zA-Z0-9_-]", "_", name)
    return name[:128].lower()

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry=retry_if_exception_type(requests.RequestException)
)
def safe_request(method: str, url: str, headers: dict = None, **kwargs) -> requests.Response:
    resp = requests.request(method, url, headers=headers, verify=False, **kwargs)
    resp.raise_for_status()
    return resp

# ================= GLPI API =================
class GLPIAPI:
    def __init__(self):
        self.base = GLPI_URL
        self.session = requests.Session()
        self.session.headers.update({"App-Token": GLPI_APP_TOKEN})
        self._init_session()

    def _init_session(self):
        url = f"{self.base}/initSession?app_token={GLPI_APP_TOKEN}&user_token={GLPI_USER_TOKEN}"
        resp = safe_request("GET", url)
        self.session.headers["Session-Token"] = resp.json()["session_token"]
        logger.info("GLPI session established")

    def get_groups(self) -> list:
        params = {"expand_dropdowns": "true", "get_hateoas": "false"}
        if GLPI_ENTITY_ID:
            params["entity_id"] = GLPI_ENTITY_ID
        resp = safe_request("GET", f"{self.base}/Group", params=params)
        groups = resp.json()
        logger.info(f"Получено {len(groups)} групп из GLPI")
        return groups

# ================= WAZUH API =================
class WazuhAPI:
    def __init__(self):
        self.base = WAZUH_URL
        self.token = None
        self._authenticate()

    def _authenticate(self):
        url = f"{self.base}/security/user/authenticate"
        auth = requests.auth.HTTPBasicAuth(WAZUH_USER, WAZUH_PASSWORD)
        resp = safe_request("POST", url, auth=auth)
        self.token = resp.json()["data"]["token"]
        logger.info("Wazuh token obtained")

    def _headers(self):
        return {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }

    def get_groups(self) -> list:
        resp = safe_request("GET", f"{self.base}/groups", headers=self._headers())
        data = resp.json()["data"]["affected_items"]
        logger.info(f"Получено {len(data)} групп из Wazuh")
        return data

    def create_group(self, group_id: str, description: str = ""):
        if DRY_RUN:
            logger.info(f"[DRY-RUN] Создание группы: {group_id}")
            return
        payload = {"group_id": group_id, "description": description}
        try:
            safe_request("POST", f"{self.base}/groups", headers=self._headers(), json=payload)
            logger.info(f"Создана группа Wazuh: {group_id}")
        except requests.exceptions.HTTPError as e:
            if e.response.status_code == 400 and "already exists" in e.response.text.lower():
                logger.warning(f"Группа {group_id} уже существует")
            else:
                raise

    def update_group_description(self, group_id: str, description: str):
        if DRY_RUN or not SYNC_DESCRIPTIONS:
            return
        # Wazuh не поддерживает прямое PATCH description, обновляем через конфигурацию
        logger.debug(f"Обновление описания для {group_id} пока не поддерживается API напрямую")

# ================= СИНХРОНИЗАЦИЯ =================
def sync_groups():
    glpi = GLPIAPI()
    wazuh = WazuhAPI()

    glpi_groups = glpi.get_groups()
    wazuh_groups = wazuh.get_groups()

    # Маппинг: sanitized_name -> original_data
    wazuh_map = {g["name"]: g for g in wazuh_groups}

    created = 0
    skipped = 0

    for g in glpi_groups:
        raw_name = g.get("name", "Unknown")
        sanitized = sanitize_group_name(raw_name)
        desc = g.get("comment", "")

        if sanitized in wazuh_map:
            logger.debug(f"Группа уже существует: {sanitized}")
            skipped += 1
            continue

        wazuh.create_group(sanitized, desc)
        created += 1

    logger.info(f"Синхронизация завершена. Создано: {created}, Пропущено: {skipped}, Dry-run: {DRY_RUN}")

if __name__ == "__main__":
    try:
        sync_groups()
    except KeyboardInterrupt:
        logger.info("Остановлено пользователем")
    except Exception as e:
        logger.error(f"Критическая ошибка: {e}", exc_info=True)
        exit(1)

5. Настройка конфигурации

Создайте файл .env в той же директории:

GLPI_URL=https://glpi.yourdomain.com/apirest.php
GLPI_APP_TOKEN=your_glpi_app_token
GLPI_USER_TOKEN=your_glpi_user_token

WAZUH_URL=https://wazuh.yourdomain.com:55000
WAZUH_USER=wazuh
WAZUH_PASSWORD=StrongPassword123!

DRY_RUN=false
GLPI_ENTITY_ID=0
SYNC_DESCRIPTIONS=true

Никогда не коммитьте .env в Git. Добавьте его в .gitignore.


6. Запуск и расписание

Ручной запуск

python sync_groups.py

Системный таймер (systemd)

/etc/systemd/system/glpi-wazuh-sync.timer

[Unit]
Description=Run GLPI-Wazuh group sync every 6 hours

[Timer]
OnCalendar=*-*-* 00/6:00:00
Persistent=true

[Install]
WantedBy=timers.target

/etc/systemd/system/glpi-wazuh-sync.service

[Unit]
Description=GLPI-Wazuh Group Sync Script

[Service]
Type=oneshot
WorkingDirectory=/opt/glpi-wazuh-sync
ExecStart=/opt/glpi-wazuh-sync/venv/bin/python sync_groups.py
User=syncuser
Environment=PATH=/opt/glpi-wazuh-sync/venv/bin
sudo systemctl daemon-reload
sudo systemctl enable --now glpi-wazuh-sync.timer

7. Тестирование и отладка

Действие Команда
Dry-run режим DRY_RUN=true python sync_groups.py
Вывод подробных логов export LOGLEVEL=DEBUG (добавьте в код logging.getLogger().setLevel(os.getenv("LOGLEVEL","INFO")))
Проверка API GLPI curl -H "App-Token: X" -H "User-Token: Y" "https://glpi/apirest.php/initSession"
Проверка API Wazuh curl -k -u wazuh:wazuh "https://wazuh:55000/groups?pretty=true"

Частые ошибки

Ошибка Причина Решение
401 Unauthorized Неверные токены/права Проверьте App-Token и права API-пользователя в GLPI
403 Forbidden Нет прав на группы в Wazuh Дайте пользователю роль group и agent
400 Bad Request: group_id invalid Спецсимволы/пробелы Скрипт уже санитизирует, проверьте логи
Connection refused Неверный URL/порт Проверьте firewall, WAZUH_URL должен включать порт 55000

8. Безопасность и лучшие практики

  1. HTTPS обязателен для обоих эндпоинтов. В продакшене используйте валидные сертификаты (verify=True в requests).

2. Минимальные права API:

  • GLPI: только чтение групп
  • Wazuh: group:read, group:create
  1. Ротация токенов: GLPI user_token можно привязать к сервисному аккаунту с истечением срока.
  2. Идемпотентность: Скрипт не создаёт дубли, проверяет существование по sanitized_name.
  3. Мониторинг: Добавьте отправку статусов в Telegram/Slack/Prometheus при ошибках.

9. Возможные расширения

Функция Как реализовать
Синхронизация агентов Добавить маппинг hostname/IP из GLPI -> Wazuh, использовать POST /groups/{id}/assign
Обратная синхронизация Читать группы Wazuh -> создавать в GLPI через POST /Group
Кастомные поля Использовать plugin_fields в GLPI для хранения Wazuh group_id
Вебхуки Подписаться на события GLPI after_add/after_update через плагины или REST hooks
Docker-упаковка FROM python:3.11-slim, копировать скрипт, ENTRYPOINT ["python", "sync_groups.py"]