Привет. Меня зовут Андрей Урывко, я инженер ИБ.
За несколько месяцев эксплуатации Wazuh я упёрся в классическую для небольших отделов мониторинга проблему: рост числа алертов при отсутствии ресурсов на их обработку. При 150–200 оповещениях в день и одном инженере в штате значительная часть времени уходила на ручную проверку однотипных сработок, а развитие инфраструктуры фактически остановилось.
В этой статье я расскажу, как я построил цепочку автоматизации обработки алертов с использованием IRIS (case management), Cortex, MISP и n8n после перехода с Wazuh на коммерческий SIEM
Это будет цикл статей, в котором я последовательно разберу построение цепочки автоматизированной обработки инцидентов:
Установка и первичная настройка инфраструктуры (IRIS, Cortex, MISP, n8n) и интеграция с SIEM
Автоматизация обогащения и создание алертов/кейсов в IRIS
Проверка ассетов на IOC через Cortex и MISP
Отправка уведомлений через Telegram/Mattermost
Немного предыстории
Wazuh достался мне по наследству и некоторое время успешно использовался как основное средство мониторинга. Однако по мере роста числа источников и правил корреляции система перестала масштабироваться с точки зрения операционной нагрузки: количество алертов росло быстрее, чем возможности их обработки.
Сложности так же добавляло отсутствие техподдержки. Нестандартные источники требовали много времени на интеграцию и нами было принято решение перейти на коммерческий SIEM. После месяцев пилотирования мы, на основе наших критериев, выделили для себя главные преимущества в выбранном SIEM:
Горизонтальная масштабируемость без сложных доработок архитектуры;
Простой в освоении VRL;
Визуальные конвейеры обработки событий;
Graphql API;
Отзывчивая техподдержка.
На момент начала внедрения агент SIEM представлял собой форк wazuh-agent и не позволял полноценно забирать события напрямую с wazuh-server. После выхода нового агента, написанного на собственном движке, появилась возможность вычитывать сырые журналы событий Wazuh и отправлять их напрямую в SIEM.В дальнейшем мы вообще заменили Wazuh на WEC.
После начала внедрения коммерческого SIEM Wazuh использовался исключительно как агент сбора логов с хостов и серверов.
Когда состоялся полный переход, я задумался об автоматизации процессов обработки алертов. В SIEM есть сущность в виде "Сервиса оповещения", которая после применения правил сегментации, выдавала "Оповещение" на основе правил корреляции.


По мере подключения новых источников количество оповещений в день стабильно превышало 150–200. При одном инженере в штате это означало постоянную ручную проверку: просмотр заявок в Jira, проверку статуса файлов в антивирусе, уточнение контекста у смежных команд.
Даже после настройки исключений и активных списков количество оповещений удалось снизить лишь до ~50 в день и все они по-прежнему требовали ручного анализа.
Исключения в SIEM можно настроить через "Активные списки" - еще одна сущность системы.

Активные списки можно наполнять как вручную, так и с помощью правил обогащения. Далее необходимо поставить фильтр, удобно, что благодаря визуальному конвейеру видно, как события идет от точки входа до БД и Сервиса оповещений, отсекая фолзы в исключениях.
В этот момент стало очевидно, что без автоматизации обогащения и предварительного анализа алертов дальнейшее развитие отдела мониторинга невозможно.
На рынке существуют коммерческие системы класса IRP (Incident Response Platform) и SOAR (Security Orchestration, Automation and Response), которые помогли бы мне, у той же компании есть свой SOAR, но как это обычно и бывает, я столкнулся с отсутствием бюджета и пришлось смотреть в стороны open source решений.
Поиск IRP
Начал свой пусть поиска нужной мне системы с трио The Hive + Cortex + MISP, позже оказалось, что компания перешла на коммерческую версию, а коммьюнити версии были сильно урезаны. Следующий раз мой взгляд пал на IRIS-DFIR.
Ключевыми критериями выбора были:
Отсутствие лицензирования
Возможность доработки под свои процессы
Наличие API для интеграции с внешними системами Да, были нюансы в виде отдельной карточки на всю страницу для каждого инцидента, но с этим я смог в дальнейшем свыкнуться. В итоге я решил оставить Cortex и MISP для проверки IOC добавил IRIS и n8n. Так же в процессе поиска я наткнулся на Shuffle, несмотря на то что Shuffle изначально ориентирован на автоматизацию реагирования на инциденты, выбор был сделан в пользу n8n из-за уже имеющегося опыта эксплуатации и наличия готовых нод для интеграции с используемыми сервисами.

Установка IRIS
В официальной документации есть полный план того, как можно установить IRIS, я немного подтюнил docker-compose.yml
services: rabbitmq: extends: file: docker-compose.base.yml service: rabbitmq db: extends: file: docker-compose.base.yml service: db build: context: docker/db image: iriswebapp_db:v2.4.7 ports: - "127.0.0.1:5432:5432" app: extends: file: docker-compose.base.yml service: app build: context: . dockerfile: docker/webApp/Dockerfile image: iriswebapp_app:v2.4.7 ports: - "127.0.0.1:8000:8000" worker: extends: file: docker-compose.base.yml service: worker build: context: . dockerfile: docker/webApp/Dockerfile image: iriswebapp_app:v2.4.7 volumes: iris-downloads: user_templates: server_data: db_data: networks: iris_backend: name: iris_backend iris_frontend: name: iris_frontend SOC_NET: external: true name: SOC_NET
docker-compose.base.yml
services: rabbitmq: image: rabbitmq:3-management-alpine container_name: iriswebapp_rabbitmq restart: always networks: - iris_backend db: container_name: iriswebapp_db restart: always environment: - POSTGRES_USER - POSTGRES_PASSWORD - POSTGRES_ADMIN_USER - POSTGRES_ADMIN_PASSWORD - POSTGRES_DB networks: - iris_backend volumes: - db_data:/var/lib/postgresql/data app: container_name: iriswebapp_app command: ['nohup', './iris-entrypoint.sh', 'iriswebapp'] volumes: - /certificates/rootCA/irisRootCACert.pem:/etc/irisRootCACert.pem:ro - ./certificates/:/home/iris/certificates/:ro - ./certificates/ldap/:/iriswebapp/certificates/ldap/:ro - iris-downloads:/home/iris/downloads - user_templates:/home/iris/user_templates - server_data:/home/iris/server_data restart: always depends_on: - "rabbitmq" - "db" env_file: - .env environment: - POSTGRES_USER - POSTGRES_PASSWORD - POSTGRES_ADMIN_USER - POSTGRES_ADMIN_PASSWORD - POSTGRES_SERVER - POSTGRES_PORT - IRIS_SECRET_KEY - IRIS_SECURITY_PASSWORD_SALT networks: - iris_backend - iris_frontend - SOC_NET worker: container_name: iriswebapp_worker restart: always command: ['./wait-for-iriswebapp.sh', 'app:8000', './iris-entrypoint.sh', 'iris-worker'] volumes: - /certificates/rootCA/irisRootCACert.pem:/etc/irisRootCACert.pem:ro - ./certificates/:/home/iris/certificates/:ro - ./certificates/ldap/:/iriswebapp/certificates/ldap/:ro - iris-downloads:/home/iris/downloads - user_templates:/home/iris/user_templates - server_data:/home/iris/server_data depends_on: - "rabbitmq" - "db" - "app" env_file: - .env environment: - POSTGRES_USER - POSTGRES_PASSWORD - POSTGRES_ADMIN_USER - POSTGRES_ADMIN_PASSWORD - POSTGRES_SERVER - POSTGRES_PORT - IRIS_SECRET_KEY - IRIS_SECURITY_PASSWORD_SALT - IRIS_WORKER networks: - iris_backend - SOC_NET volumes: iris-downloads: user_templates: server_data: db_data: networks: iris_backend: name: iris_backend iris_frontend: name: iris_frontend SOC_NET: external: true name: SOC_NET
Ставил версию 2.4.7, т.к. в более новой версии был баг, который не позволял открывать карточки с результатами анализаторов
Установка Cortex, MISP
В документации The Hive 5 есть docker-compose, который позволяет из одного файла установить сразу The Hive, Cortex, MISP. Так как нам Hive не нужен, смело удаляет блоки связанные с ним - это сам thehive и cassandra, minio
services: elasticsearch: container_name: elasticsearch image: elasticsearch:7.17.9 restart: always mem_limit: 2048m environment: - discovery.type=single-node - xpack.security.enabled=false - cluster.name=hive - http.host=0.0.0.0 - ES_JAVA_OPTS=-Xms512m -Xmx512m - ingest.geoip.downloader.enabled=false ulimits: memlock: soft: -1 hard: -1 volumes: - elasticsearchdata:/usr/share/elasticsearch/data networks: - SOC_NET cortex: container_name: cortex image: thehiveproject/cortex:3.2.0 restart: unless-stopped environment: - job_directory=/tmp/cortex-jobs - docker_job_directory=/tmp/cortex-jobs - CORTEX_DOCKER_NETWORK=thehive5_SOC_NET volumes: - /var/run/docker.sock:/var/run/docker.sock - /tmp/cortex-jobs:/tmp/cortex-jobs - ./cortex/logs:/var/log/cortex - ./cortex/application.conf:/cortex/application.conf depends_on: elasticsearch: condition: service_started networks: - SOC_NET healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9001/api/status"] interval: 30s timeout: 10s retries: 3 misp: container_name: misp image: coolacid/misp-docker:core-v2.4.177 restart: unless-stopped depends_on: misp_mysql: condition: service_healthy redis: condition: service_started volumes: - ./server-configs/:/var/www/MISP/app/Config/ - ./logs/:/var/www/MISP/app/tmp/logs/ - ./files/:/var/www/MISP/app/files - ./certs/:/etc/nginx/certs environment: - MYSQL_HOST=misp_mysql - MYSQL_DATABASE=mispdb - MYSQL_USER=mispuser - MYSQL_PASSWORD=misppass - TIMEZONE=Europe/Samara - INIT=true - CRON_USER_ID=1 - REDIS_FQDN=redis - MISP_BASEURL=https://misp.${DOMAIN\\_NAME} - HOSTNAME=https://misp.${DOMAIN\\_NAME} # Для работы с самоподписными сертификатам - HTTPS_VERIFY_SSL=false networks: - SOC_NET misp_mysql: container_name: misp_mysql image: mysql/mysql-server:8.0 restart: unless-stopped volumes: - mispsqldata:/var/lib/mysql environment: - MYSQL_DATABASE=mispdb - MYSQL_USER=mispuser - MYSQL_PASSWORD=misppass - MYSQL_ROOT_PASSWORD=mispass - MYSQL_INNODB_BUFFER_POOL_SIZE=12 - MYSQL_INNODB_LOG_FILE_SIZE=1G - MYSQL_INNODB_READ_IO_THREADS=16 - MYSQL_INNODB_WRITE_IO_THREADS=16 - MYSQL_INNODB_FLUSH_METHOD=O_DIRECT # Прямой I/O - MYSQL_INNODB_FLUSH_LOG_AT_TRX_COMMIT=2 # Баланс скорости/безопасности - MYSQL_QUERY_CACHE_SIZE=0 # Выключить (устарело) - MYSQL_MAX_CONNECTIONS=500 networks: - SOC_NET healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"] interval: 30s timeout: 10s retries: 5 redis: container_name: redis image: redis:8.4.0 restart: unless-stopped command: redis-server --appendonly yes volumes: - redisdata:/data networks: - SOC_NET healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 30s timeout: 10s retries: 3 misp-modules: container_name: misp_modules image: coolacid/misp-docker:modules-v2.4.177 environment: - REDIS_BACKEND=redis - HTTPS_VERIFY_SSL=false depends_on: redis: condition: service_healthy misp_mysql: condition: service_healthy networks: - SOC_NET volumes: elasticsearchdata: mispsqldata: redisdata: networks: SOC_NET: external: true name: SOC_NET attachable: true
Установка n8n, traefik
При установке n8n с github через docker-compose, автоматически устанавливается и traefik, поэтому я решил и остальные сервисы завести за traefik
traefik: container_name: traefik image: "traefik:windowsservercore-ltsc2022" restart: always command: - "--configFile=/etc/traefik/traefik.yml" ports: - "80:80" - "443:443" networks: - SOC_NET volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./certs:/certs:ro # Сертификаты - ./traefik-config/traefik.yml:/etc/traefik/traefik.yml:ro - ./traefik-config/dynamic:/etc/traefik/dynamic:ro labels: - "traefik.enable=true" - "traefik.http.routers.traefik-api.rule=Host(`traefik.${DOMAIN_NAME}`)" #&& PathPrefix(`/api`)" - "traefik.http.routers.traefik-api.service=api@internal" - "traefik.http.routers.traefik-api.entrypoints=websecure" - "traefik.http.services.traefik-service.loadbalancer.server.port=8080" # Статический TLS сертификат через Docker labels - "traefik.tls.stores.default.defaultCertificate.certFile=/certs/cert.crt" - "traefik.tls.stores.default.defaultCertificate.keyFile=/certs/key.pem" n8n: container_name: n8n image: docker.n8n.io/n8nio/n8n:2.1.14 restart: always networks: - SOC_NET labels: - "traefik.enable=true" - "traefik.http.routers.n8n.rule=Host(`n8n.${DOMAIN_NAME}`)" - "traefik.http.routers.n8n.entrypoints=websecure" - "traefik.http.routers.n8n.tls=true" - "traefik.http.services.n8n.loadbalancer.server.port=5678" environment: - N8N_EMAIL_MODE=smtp - N8N_SMTP_HOST=smtp@domain.ru - N8N_SMTP_SENDER=smtp@domain.ru - N8N_SMTP_USER=mail - N8N_SMTP_PASS=${N8N_SMTP_PASS} - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true - N8N_HOST=localhost:5678 - N8N_PROTOCOL=https - N8N_RUNNERS_ENABLED=true - NODE_ENV=production - WEBHOOK_URL=localhost:5678 - GENERIC_TIMEZONE=${GENERIC_TIMEZONE} - TZ=${GENERIC_TIMEZONE} - NODE_TLS_REJECT_UNAUTHORIZED=0 # Для работы с самоподписными сертификатами volumes: - n8n_data:/home/node/.n8n - ./local-files:/files - ./n8n-config:/config volumes: n8n_data: traefik_data:
Итоговый docker-compose.yml выглядит вот так
services: traefik: container_name: traefik image: "traefik:windowsservercore-ltsc2022" restart: always command: - "--configFile=/etc/traefik/traefik.yml" ports: - "80:80" - "443:443" networks: - SOC_NET volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./certs:/certs:ro # Сертификаты - ./traefik-config/traefik.yml:/etc/traefik/traefik.yml:ro - ./traefik-config/dynamic:/etc/traefik/dynamic:ro labels: - "traefik.enable=true" - "traefik.http.routers.traefik-api.rule=Host(`traefik.${DOMAIN_NAME}`)" #&& PathPrefix(`/api`)" - "traefik.http.routers.traefik-api.service=api@internal" - "traefik.http.routers.traefik-api.entrypoints=websecure" - "traefik.http.services.traefik-service.loadbalancer.server.port=8080" # Статический TLS сертификат через Docker labels - "traefik.tls.stores.default.defaultCertificate.certFile=/certs/cert.crt" - "traefik.tls.stores.default.defaultCertificate.keyFile=/certs/key.pem" elasticsearch: container_name: elasticsearch image: elasticsearch:7.17.9 restart: always mem_limit: 2048m environment: - discovery.type=single-node - xpack.security.enabled=false - cluster.name=hive - http.host=0.0.0.0 - ES_JAVA_OPTS=-Xms512m -Xmx512m - ingest.geoip.downloader.enabled=false ulimits: memlock: soft: -1 hard: -1 volumes: - elasticsearchdata:/usr/share/elasticsearch/data networks: - SOC_NET cortex: container_name: cortex image: thehiveproject/cortex:3.2.0 restart: unless-stopped environment: - job_directory=/tmp/cortex-jobs - docker_job_directory=/tmp/cortex-jobs - CORTEX_DOCKER_NETWORK=thehive5_SOC_NET volumes: - /var/run/docker.sock:/var/run/docker.sock - /tmp/cortex-jobs:/tmp/cortex-jobs - ./cortex/logs:/var/log/cortex - ./cortex/application.conf:/cortex/application.conf depends_on: elasticsearch: condition: service_started networks: - SOC_NET labels: - "traefik.http.routers.cortex.tls=true" - "traefik.enable=true" - "traefik.http.routers.cortex.rule=Host(`cortex.${DOMAIN_NAME}`)" - "traefik.http.routers.cortex.entrypoints=websecure" - "traefik.http.services.cortex.loadbalancer.server.port=9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9001/api/status"] interval: 30s timeout: 10s retries: 3 misp: container_name: misp image: coolacid/misp-docker:core-v2.4.177 restart: unless-stopped depends_on: misp_mysql: condition: service_healthy redis: condition: service_started volumes: - ./server-configs/:/var/www/MISP/app/Config/ - ./logs/:/var/www/MISP/app/tmp/logs/ - ./files/:/var/www/MISP/app/files - ./certs/:/etc/nginx/certs environment: - MYSQL_HOST=misp_mysql - MYSQL_DATABASE=mispdb - MYSQL_USER=mispuser - MYSQL_PASSWORD=misppass - TIMEZONE=Europe/Samara - INIT=true - CRON_USER_ID=1 - REDIS_FQDN=redis - MISP_BASEURL=https://misp.${DOMAIN\\_NAME} - HOSTNAME=https://misp.${DOMAIN\\_NAME} # Для работы с самоподписными сертификатам - HTTPS_VERIFY_SSL=false labels: - "traefik.enable=true" - "traefik.http.routers.misp.rule=Host(`misp.${DOMAIN_NAME}`)" - "traefik.http.routers.misp.entrypoints=websecure" - "traefik.http.services.misp.loadbalancer.server.port=443" - "traefik.http.services.misp.loadbalancer.server.scheme=https" - "traefik.http.services.misp.loadbalancer.serversTransport=misp@file" networks: - SOC_NET misp_mysql: container_name: misp_mysql image: mysql/mysql-server:8.0 restart: unless-stopped volumes: - mispsqldata:/var/lib/mysql environment: - MYSQL_DATABASE=mispdb - MYSQL_USER=mispuser - MYSQL_PASSWORD=misppass - MYSQL_ROOT_PASSWORD=mispass - MYSQL_INNODB_BUFFER_POOL_SIZE=12 - MYSQL_INNODB_LOG_FILE_SIZE=1G - MYSQL_INNODB_READ_IO_THREADS=16 - MYSQL_INNODB_WRITE_IO_THREADS=16 - MYSQL_INNODB_FLUSH_METHOD=O_DIRECT # Прямой I/O - MYSQL_INNODB_FLUSH_LOG_AT_TRX_COMMIT=2 # Баланс скорости/безопасности - MYSQL_QUERY_CACHE_SIZE=0 # Выключить (устарело) - MYSQL_MAX_CONNECTIONS=500 networks: - SOC_NET healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"] interval: 30s timeout: 10s retries: 5 redis: container_name: redis image: redis:8.4.0 restart: unless-stopped command: redis-server --appendonly yes volumes: - redisdata:/data networks: - SOC_NET healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 30s timeout: 10s retries: 3 misp-modules: container_name: misp_modules image: coolacid/misp-docker:modules-v2.4.177 environment: - REDIS_BACKEND=redis - HTTPS_VERIFY_SSL=false depends_on: redis: condition: service_healthy misp_mysql: condition: service_healthy networks: - SOC_NET n8n: container_name: n8n image: docker.n8n.io/n8nio/n8n # Укажите версию restart: always networks: - SOC_NET labels: - "traefik.enable=true" - "traefik.http.routers.n8n.rule=Host(`n8n.${DOMAIN_NAME}`)" - "traefik.http.routers.n8n.entrypoints=websecure" - "traefik.http.routers.n8n.tls=true" - "traefik.http.services.n8n.loadbalancer.server.port=5678" environment: - N8N_EMAIL_MODE=smtp - N8N_SMTP_HOST=smtp@domain.ru - N8N_SMTP_SENDER=smtp@domain.ru - N8N_SMTP_USER=mail - N8N_SMTP_PASS=${N8N_SMTP_PASS} - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true - N8N_HOST=localhost:5678 - N8N_PROTOCOL=https - N8N_RUNNERS_ENABLED=true - NODE_ENV=production - WEBHOOK_URL=localhost:5678 - GENERIC_TIMEZONE=${GENERIC_TIMEZONE} - TZ=${GENERIC_TIMEZONE} - NODE_TLS_REJECT_UNAUTHORIZED=0 # Для работы с самоподписными сертификатами volumes: - n8n_data:/home/node/.n8n - ./local-files:/files - ./n8n-config:/config volumes: n8n_data: traefik_data: elasticsearchdata: mispsqldata: redisdata: networks: SOC_NET: external: true name: SOC_NET attachable: true
.env
# -- N8N -- DOMAIN_NAME=domain.com SUBDOMAIN=n8n N8N_SMTP_PASS=password SSL_EMAIL=admin@local.admin # -- IRIS -- LOG_LEVEL=info SERVER_NAME=iris.domain.com KEY_FILENAME=key.pem CERT_FILENAME=cert.pem # -- DATABASE DB_IMAGE_NAME=ghcr.io/dfir-iris/iriswebapp_db DB_IMAGE_TAG=2.4.24 POSTGRES_USER=postgres POSTGRES_PASSWORD=pass POSTGRES_ADMIN_USER=raptor POSTGRES_ADMIN_PASSWORD=pass POSTGRES_DB=iris_db POSTGRES_SERVER=db POSTGRES_PORT=5432 # -- IRIS APP_IMAGE_NAME=ghcr.io/dfir-iris/iriswebapp_app APP_IMAGE_TAG=2.4.24 DOCKERIZED=1 IRIS_SECRET_KEY=pass IRIS_SECURITY_PASSWORD_SALT=pass IRIS_UPSTREAM_SERVER=app IRIS_UPSTREAM_PORT=8000 IRIS_FRONTEND_SERVER=frontend IRIS_FRONTEND_PORT=5173 IRIS_SVELTEKIT_FRONTEND_DIR=../iris-frontend # -- WORKER CELERY_BROKER=amqp://rabbitmq # -- AUTH IRIS_AUTHENTICATION_TYPE=local # -- LISTENING PORT INTERFACE_HTTPS_PORT=4443
Интеграция SIEM и n8n
Последнее, что хотелось бы рассмотреть в этой части - это то, как подружить SIEM и n8n.
В n8n есть нода "вебхук", которая прослушивает определенный порт, который можно задать в файле .env при развертывании n8n и отправляет полученные данные дальше по конвейеру для дальнейшей обработки.

На стороне SIEM есть сущность "Конечная точка", с помощью которой можно отправлять сработки правил корреляции во внешнюю систему. В нашем случая внешняя система представлена в виде вебхука, поэтому отлично подойдет HTTP(s) c методом "POST". Указываем нужный адрес, выбираем HTTPS, так как по дефолту n8n работает только с HTTPS. Для указания TLS сертификата нужна задать "Секрет", делается это во вкладке Секрет -> Создать секрет -> Сертификат
Следом в конечной точке выбираем нужный сертификат и применяем конфигурацию конвейера.

Генерируем тестовую сработку для правила корреляции и смотрим в n8n на ноду вебхука. После генерации тестовой сработки данные успешно поступили в n8n, что подтвердило корректность интеграции и возможность дальнейшей автоматизации обработки событий.

На этом этапе инфраструктура для автоматизации была развернута, а SIEM успешно передает сработки правил корреляции во внешний конвейер обработки.
В следующей части я подробно разберу настройку автоматического обогащения алертов и создание кейсов в IRIS с использованием n8n.
