
Привет! На связи Виктор Лысенко из Cloud4Y. Сегодня хочу рассказать о массовых рассылках и способах борьбы с ними.
Кейс исключительно практический: возникла необходимость предупреждать и отслеживать потенциальных спамеров (входящих и исходящих). И я решил вывести большие рассылки в Zabbix со следующими полями писем Exchange:
Domain
Timestamp
Sender
RecipientCount
Subject
InternalMessageId
Примерный план работ:
Подготовить скрипт, генерирующий .json и поместить его в планировщик задач
Добавить UserParameter в конфигурацию Zabbix (zabbix_agentd.conf)
Добавляем макрос для поля RecipientCount в шаблон
Создать главный элемент данных
Создание зависимого правила обнаружения
Создание прототипа элемента данных
Создание прототипа триггера
Применение шаблона на Exchange сервера
Подготовка скрипта
Подготовлен скрипт, который собирает все полученные и отправленные письма текущим сервером, с количеством получателей более двух (переменная $Limit) и выгружает в json‑файл. Дальнейшая фильтрация по количеству получателей будет производится в Zabbix через макрос {$BIG_MAIL_TRIGGER}.
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010
$Utf8NoBomEncoding = New-Object System.Text.Utf8Encoding
$RunServer = $env:COMPUTERNAME
$Limit = 2
$OutFile = "D:\Scripts\Zabbix\Results\zabbix_big_mailings.json"
[System.Collections.ArrayList]$Status = @()
$Nums = Get-MessageTrackingLog -Server $RunServer -EventId "EXPAND" -Resultsize Unlimited -Start (Get-Date).AddDays(-3) | ?{$_.RecipientCount -gt $Limit}
ForEach ($Num in $Nums) {
$Status += [PSCustomObject]@{
'Domain' = ($Num.sender).split("@")[1]
'Timestamp' = [string]$Num.Timestamp
'Sender' = [string]$Num.Sender
'RecipientCount' = [int]$Num.RecipientCount
'Subject' = [string]$Num.MessageSubject
'InternalMessageId' = [string]$Num.InternalMessageId}
}
#Генерируем заглушку, если нет данных после выборки
if ($Nums.count -eq 0){
$Status += ,[PSCustomObject]@{
'Domain' = "domain.com"
'Timestamp' = "01/01/1900 00:00:0"
'Sender' = "debug@domain.com"
'RecipientCount' = 0
'Subject' = "Only if $Nums = 0, stub for zabbix"
'InternalMessageId' = "0123456789ABCDE"}
}
if ($Nums.Count -eq 1) {[array]$Sorted = @($Status) | Sort Domain} else {$Sorted = $Status | Sort Domain}
$MyRawString = ,$Sorted | ConvertTo-Json -Depth 5
[System.IO.File]::WriteAllLines($OutFile, $MyRawString, $Utf8NoBomEncoding)
В моём примере команда «(Get‑Date).AddDays(-3)» получает выборку за последние три дня. Можно, например, установить выборку за последние 6 часов: «(Get‑Date).AddHours(-6)».
Пример JSON‑файла:

Размещаем выполнение скрипта в планировщике задач:

2. Добавить UserParameter в конфигурацию Zabbix
В файл конфигурации zabbix_agentd.conf добавляем пользовательский параметр с ключом exchange.big.mail.get, можно указать любой другой ключ, просто в следующем пункте нужно будет указать именно этот ключ.
По этому ключу zabbix будет получать .json, созданный в предыдущем пункте при выполнении скрипта:
UserParameter = exchange.big.mailings.get, type D:\Scripts\Zabbix\EX01\zabbix_big_mailings.json
После внесения изменений в zabbix_agentd.conf не забываем перезапустить службу «Zabbix Agent».
3. Добавление макроса в шаблон
Создаём новый шаблон или используем другой. Важно добавить макрос «{$BIG_MAIL_TRIGGER}». Этот макрос устанавливает пороговое значение для перехода на следующий этап, то есть срабатывания триггера.
В моём примере порог срабатывания 100 получателей — RecipientCount:

4. Создаём главный элемент данных Zabbix
Главный элемент данных (Master Item) — это обычный элемент данных (Item) в Zabbix, который хранит «сырые» данные (например, JSON), используемые другими зависимыми элементами (Dependent Items) или правилами обнаружения (LLD Rules).
Создаём новый элемент данных.. Здесь при создании важно указать:
тип — «Zabbix Agent»
ключ — указанный ранее в пункте 2, в «UserParameter». В моём случае — «exchange.big.mailings.get»

Далее нужно сделать предобработку, отфильтровать элементы массива, добавляем зависимость от макроса «{$BIG_MAIL_TRIGGER}» во вкладке «Предобработка». Я опишу два варианта:
А. Логичнее это реализовать с помощью JSONPath — «$.value[?(@.RecipientCount >= {$BIG_MAIL_TRIGGER})]»:

Но в случае, когда после фильтрации не останется элементов, Zabbix вернёт ошибку, не пустой массив: «cannot extract value from json by path "$..[?(@.RecipientCount >= {$BIG_MAIL_TRIGGER})]": no data matches the specified path». Что НЕ запустит процесс обновления элементов данных и триггеров.
На практике это означает, что старые триггеры будут висеть до тех пор, пока не выполнится условие в JSONPath и уже следующий массив «спамеров» не пойдёт в генерацию новых элементов данных и триггеров. В некоторых случаях это может пригодится — всегда наблюдать последних спамеров.
«$» — корневой элемент JSON-документа.
«value» — поиск в пределах массива value (нерекурсивный).
«[?(@.RecipientCount >= {$BIG_MAIL_TRIGGER})]» — это фильтр, который применяется к каждому элементу массива или объекту, найденному на предыдущем шаге. Разберём его подробнее:
«?» — обозначает начало фильтра.
«@» — текущий элемент, который проверяется.
«.RecipientCount» — обращение к полю «RecipientCount» текущего элемента.
«>= {$BIG_MAIL_TRIGGER}» — условие, при котором значение поля «RecipientCount» должно быть больше или равно значению макроса(переменной) «{$BIG_MAIL_TRIGGER}».
Б. Но я реализую с помощью JavaScript, с условием если нет элементов, то вернётся пустой массив:
try {
var threshold = Number("{$BIG_MAIL_TRIGGER}"); // Преобразуем макрос в число
var data = JSON.parse(value); // Разбираем JSON
if (!data.value || typeof data.value !== "object") {
return "[]"; // Если нет массива, возвращаем пустой массив
}
var result = [];
for (var i = 0; i < data.value.length; i++) {
if (data.value[i].RecipientCount >= threshold) {
result.push(data.value[i]); // Добавляем элемент в результат, если проходит фильтр
}
}
return JSON.stringify(result); // Возвращаем отфильтрованный JSON
} catch (e) {
return "[]"; // В случае ошибки возвращаем пустой массив
}

5. Создаём правило обнаружения
В Zabbix правило обнаружения (Discovery Rule) — это механизм, который позволяет автоматически обнаруживать и создавать объекты мониторинга (например, элементы данных, триггеры, графики) на основе динамически собираемых данных. Это особенно полезно для мониторинга систем, где количество объектов может меняться, например, сетевые интерфейсы, диски, сервисы, виртуальные машины и т. д.
В нашем случае правило обнаружения анализирует полученный JSON, и извлекает из него массив объектов рассылки. На основе обнаруженных объектов Zabbix автоматически создает элементы данных и триггеры.
Создаём правило обнаружения. Правило следует связать с главным элементом данных созданном в предыдущем пункте. Связь устанавливается указанием двух полей:
Тип — «Зависимый элемент данных»
Основной элемент данных — указываем имя главного элемента данных из п.4.
Имя и остальные параметры, устанавливаем на своё усмотрение

Правило обнаружения понадобилось, чтобы использовать функциональность LLD (Low Level Discovery) макросов в прототипах триггера и элемента данных. Добавляем LLD макросы:

LLD макросы:
Макрос | JSONPath |
{#BIGMAILINGSORG} | $.Domain |
{#BIGMAILINGSTIMESTAMP} | $.Timestamp |
{#ID} | $.InternalMessageId |
{#MAILSCOUNT} | $.RecipientCount |
{#SENDER} | $.Sender |
{#SUBJECT} | $.Subject |
Количество макросов можно расширить, для этого достаточно указать нужные поля, которые требуется добавить в выходной JSON, используя скрипт в п.1.
6. Создаём прототип элемента данных
Ограничение Zabbix: зависимый элемент данных (Dependent Item) нельзя привязать напрямую к правилу обнаружения (Discovery Rule). Это связано с архитектурой Zabbix — Зависимый элемент данных может зависеть только от другого элемента данных (Item), а не от LLD‑правила.
Если бы не это ограничение, элемент данных (п.4), можно было бы не создавать, а прототип привязать сразу к правилу обнаружения.
Создаём прототип элемента данных. Правило следует связать с элементом данных созданным в п.4. — поля «Тип» и «Основной элемент данных», заполняем и другие поля:
Тип — «Зависимый элемент данных»
Основной элемент данных — указываем имя главного элемента данных из п.4.
Имя — имя должно быть уникальным, не повторять даже макросы
Ключ — по ключу будет обращение триггера, важно указать макрос «{#ID}» в качестве параметра
Тип информации — Числовой, так как будет возвращаться «RecipientCount»
Остальные параметры устанавливаем на своё усмотрение

Далее во вкладке «Предобработка» добавляем условия возврата «RecipientCount» для рассылки с указанным «{#ID}».
Поле JSONPath - «$[?(@.InternalMessageId=="{#ID}")].RecipientCount»:

Так как значение возвращается в квадратных скобках, просто обрезаем их с помощью правил предобработки.
Проверяем с заранее подготовленным массивом. Результат тестирования правил предобработки для рассылки с {#ID} = 31 628 139 167 757, вернул количество получателей — 3:

7. Создаём прототип триггера
Вот здесь нам и потребуется указать LLD макросы, в полях:
Имя — «_BigMail: {#SENDER} send {#MAILSCOUNT} messages at {#BIGMAILINGSTIMESTAMP}, Subject: {#SUBJECT}»
Выражение — «last(/Template Microsoft Exchange PS Checks/exchange.big.mailings.getstatus[{#ID}])>{$BIG_MAIL_TRIGGER}»

Имя триггера (поле «Имя») при подстановке макросов будет выглядеть примерно так: «_BigMail: debug@domain.com send 200 messages at 01/01/1900 00:00:00, Subject: Stub For Zabbix»
А поле «Выражение» — это условие срабатывание/отображения триггера. Это выражение вызывает прототип элемента данных с макросом\переменной {#ID} письма, и полученный результат сравнивает с {$BIG_MAIL_TRIGGER} - «RecipientCount > {$BIG_MAIL_TRIGGER}». Если соблюдено условие, то срабатывает триггер.
8. Применяем подготовленный шаблон на сервер Exchange
В свойствах узла сети выбираем шаблон и нажимаем «Обновить». Нужно время, чтобы шаблон применился и создались элементы данных и триггеры.

Вот так можно поколдовать с почтой. Есть идеи, как можно сделать лучше? Я с интересом выслушаю.
Спасибо за внимание, ваш Cloud4Y. Читайте нас здесь или в Telegram‑канале!