Подробный гайд: Скрипт авто-маппинга групп 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
- Перейдите:
Настройка -> Общие -> API - Включите
Enable REST API - Создайте API-пользователя или используйте свой аккаунт
4. Сгенерируйте:
App Token(глобальный)User Token(для конкретного пользователя)
- Запомните базовый URL:
https://glpi.example.com/apirest.php
Wazuh
- 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. Безопасность и лучшие практики
- HTTPS обязателен для обоих эндпоинтов. В продакшене используйте валидные сертификаты (
verify=Trueвrequests).
2. Минимальные права API:
- GLPI: только чтение групп
- Wazuh:
group:read,group:create
- Ротация токенов: GLPI
user_tokenможно привязать к сервисному аккаунту с истечением срока. - Идемпотентность: Скрипт не создаёт дубли, проверяет существование по
sanitized_name. - Мониторинг: Добавьте отправку статусов в 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"] |