Почтовый сервер может годами принимать письма, пока антивирус ещё не подключён, базы сигнатур устарели или интеграция молча сломалась. В итоге во входящих уже лежат .eml с эксплойтами и троянами, а штатный сканер hMailServer смотрит только на новую почту. Набор hmailserver-kaspersky-cleanup закрывает именно этот разрыв: Kaspersky avp.com ищет заражения в режиме «только отчёт», человек проверяет план, а удаление идёт через COM API hMailServer, а не через сам антивирус.
Репозиторий написан на PowerShell 7.2+ для Windows, лицензия MIT, автор — Михаил Дейнекин (@paulmann). Скрипты обкатывались на живом сервере с Kaspersky 21.25, а не на синтетической схеме «вызвал SCAN и удалил файл».
В чём настоящая проблема
hMailServer умеет отдавать входящую почту внешнему антивирусу. Это защита периметра, а не аудит архива. Сообщения, которые уже лежат в Data, антивирусный хук больше не видит. На старом сервере типичны три источника «наследия»:
- письма приняты до включения сканера;
- сканер работал со старыми базами и пропустил объект, который сегодня детектится как
HEUR:Exploit.MSOffice.*; - интеграция падала тихо, очередь жила, пользователь открывал вложение.
Наивный ответ звучит просто: avp.com SCAN C:\mail\Data /i2. Для файлового каталога это ещё можно обсуждать. Для почтового хранилища это почти гарантированный инцидент целостности.
.eml — контейнер. Вердикт Касперского относится к объекту внутри письма: вложению, RTF, Equation Editor, архивному слою. Безопасно убрать можно только сообщение целиком. Если антивирус вырежет или удалит файл за спиной hMailServer, база продолжит указывать на путь, которого уже нет. Клиенты начнут показывать «битые» письма, IMAP-папки разъедутся с диском, а восстановить соответствие «запись ↔ файл» без бэкапа будет дорого.
Вторая ловушка — код возврата avp.com. Консольный клиент возвращает ненулевые коды на вполне успешных прогонах. Код 3 на продуктивном хранилище — норма: объекты найдены и оставлены необработанными, потому что выбран report-only. Наивная обёртка трактует это как сбой, выбрасывает как раз те пакеты, где что-то нашлось, и рапортует «чисто» на ящике, в котором лежит эксплойт.
Третья — сам продукт. После незавершённого обновления рядом живут две папки Kaspersky. Мёртвый avp.com принимает вызов, отвечает пустотой и кодом 3. Если брать «самую новую папку», можно неделю сканировать мёртвым клиентом.
Отсюда жёсткое разделение ролей: антивирус только смотрит, человек только подтверждает, почтовый сервер только удаляет.
Как устроено решение
В репозитории три рабочих скрипта и документация.
| Файл | Роль |
|---|---|
Invoke-HMailKasperskyCleanup.ps1 | Основной конвейер: Preflight, Scan, Plan, Delete |
Remove-HMailInfectedFromPlan.ps1 | Точечное удаление по плану: открываются только затронутые ящики |
Test-AvpCli.ps1 | Лабораторный матричный прогон avp.com и локатор живого клиента |
Логика основного скрипта укладывается в четыре режима.
Preflight ничего не трогает в хранилище. Он находит кандидатов avp.com (явный путь, корень Kaspersky Lab, типичные каталоги), каждому задаёт HELP и отбрасывает сборки без ответа. Затем гоняет матрицу способов вызова: cmd.exe с перенаправлением против прямых .NET-каналов, списки ANSI/OEM против аргументов в командной строке, с /RA и без. Результат пишется в preflight-matrix.json. Без этого шага легко выбрать форму вызова, которую конкретная сборка молча игнорирует.
Scan и Plan — аудит. Скрипт находит каталог Data по цепочке COM → hMailServer.INI → реестр → путь установки, собирает *.eml, при необходимости режет выборку -SinceDays, считает SHA-256 (хеширование пропускается, если файлов больше 50 000), затем сканирует пакетами с /i0. Из отчёта разбираются семейства вердиктов: EICAR, HEUR:, UDS:, VHO:, PDM:, Trojan, Exploit, Worm и соседние. Строки ok и промежуточные слои архива отбрасываются. Путь до .eml склеивается с сообщением через COM: домен → ящик → INBOX и IMAP-папки. На выходе — remediation-plan.json схемы 5 и плоский infected-messages.csv для ревью.
Delete в основном скрипте потребляет уже просмотренный план. Схема должна быть 5, DataDirectory обязан совпасть с текущим хранилищем, любой путь вне Data отвергается. Перед удалением запись снова ищется по MessageId и файлу. Если включён карантин и копия не удалась, это письмо не трогают. Удаление идёт через DeleteByDBID. Есть -WhatIf и -Confirm (ConfirmImpact = High). Опция -DeleteOrphan снимает только *.eml внутри стора, на которые нет сообщения.
Для большого сервера полный COM-обход на этапе удаления слишком дорог: сотни тысяч вызовов ради пары писем. Поэтому появился Remove-HMailInfectedFromPlan.ps1. Он выводит домен и ящик из раскладки хранилища
<DataDirectory>\<domain>\<mailbox>\<два hex-символа>\<GUID>.eml
и открывает Domains.ItemByName / Accounts.ItemByAddress, а если хелперов нет — перечисляет только этот домен. Обрабатываются и MatchedMessages, и Orphans. Сироты — штатный случай, когда на этапе скана COM-аутентификация не удалась и идентификатор письма так и не был сопоставлен: точечный скрипт резолвит его сам, уже открыв нужный ящик.
Режим Com удаляет через API. FileOnly — крайняя мера: файл снимается с диска, запись в базе остаётся. После такого прогона клиенты могут показывать нечитаемые письма, пока хранилище не согласуют. Оба режима по умолчанию копируют .eml в карантин с суффиксом .bad.
Параллельный запуск режется глобальными мьютексами Global\Invoke-HMailKasperskyCleanup и Global\RemoveHMailInfectedFromPlan. Каталоги отчётов и карантина обязаны лежать вне Data: скрипт отказывается стартовать, если их подсунули внутрь стора.
Рядом живёт близнец на ClamAV: hmailserver-clamav-cleanup. Тот же конвейер «скан → план → ревью → COM-удаление», другой движок детекта.
Практический конвейер
Нужны Windows x64, pwsh 7.2+, повышенные права и интерактивная сессия, в которой работает резидентный avp.exe. Без живого процесса консольный клиент задачи не выполняет. Пароль — это Administrator password из hMailServer.INI, не пароль почтового ящика. Перед Delete снимают бэкап базы и каталога Data. Карантин — удобство, не замена резервной копии.
# 0. Доказать, что avp.com на этой машине живой, и увидеть рабочие формы вызова
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Preflight
# 1. По желанию: полная матрица поведения сборки на одноразовом EICAR-корпусе
.\Test-AvpCli.ps1 -Runner Both
# 2. Аудит за сутки, только отчёт, хранилище не меняется
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Scan -SinceDays 1 -BatchSize 40
# 3. Полный стор, обновление баз, сохранение артефактов каждого пакета
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Scan -UpdateDatabases -KeepBatchArtifacts
После прогона в %SystemDrive%\mail\reports\kaspersky\run-yyyyMMdd-HHmmss\ лежат run.log, манифест, пакетные .lst/.report/.out, склейка kaspersky-aggregated.txt, план и CSV. Имеет смысл сначала открыть CSV: ящик, папка, id, дата, from, subject, размер, вердикт, внутренний объект, файл. Вердикт вида HEUR:Exploit.MSOffice.Generic на //attachment.rtf//equation означает, что эксплойт дошёл до ящика. Удаление не заменяет разбор: открывали ли вложение.
Сухой прогон удаления, затем боевой:
$plan = 'C:\mail\reports\kaspersky\run-20260811-221500\remediation-plan.json'
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Delete -PlanPath $plan -WhatIf
.\Invoke-HMailKasperskyCleanup.ps1 -Mode Delete -PlanPath $plan
На большом сторе после ревью плана обычно выгоднее точечный скрипт:
.\Remove-HMailInfectedFromPlan.ps1 `
-PlanPath $plan `
-QuarantineDirectory 'C:\mail\quarantine' `
-WhatIf
.\Remove-HMailInfectedFromPlan.ps1 `
-PlanPath $plan `
-QuarantineDirectory 'C:\mail\quarantine'
Одно письмо можно выцелить -OnlyPath. После удаления папки в клиентах стоит сжать, затем снова прогнать Scan. Письмо, которое «ещё видно», часто уже нет на диске: клиент держит кэш.
Полный ночной скан всего стора через Task Scheduler не всегда реален: avp.com хочет интерактивную сессию с живым avp.exe. Рабочий компромисс — ежедневный audit-only за последние сутки-двое и ручное Delete по CSV.
$action = New-ScheduledTaskAction -Execute 'pwsh.exe' `
-Argument '-NoProfile -File "C:\tools\Invoke-HMailKasperskyCleanup.ps1" -Mode Plan -SinceDays 2'
$trigger = New-ScheduledTaskTrigger -Daily -At 03:30
Register-ScheduledTask -TaskName 'hMail Kaspersky audit' -Action $action -Trigger $trigger -RunLevel Highest
Test-AvpCli.ps1 полезен не только как лаборатория. Его можно вызывать как локатор из чужих скриптов:
$avp = .\Test-AvpCli.ps1 -ResolvePathOnly -Quiet
& $avp.Path SCAN 'C:\mail\spool' /i0
Корпус EICAR собирается на лету: простой файл, кириллический каталог, .eml с base64-вложением, mbox, ZIP. Резидентная защита имеет право снести EICAR прямо во время матрицы. Харнесс не просит отключать защиту и не делает вид, что исчезнувший файл «просканирован»: такой кейс помечается как потеря корпуса. Деструктивные /i1, /i2, /i8 включаются только явно и только на одноразовых копиях.
Что сломалось бы без полевых замеров
Несколько решений в коде выглядят странно, пока не увидеть живой avp.com.
Вердикт берётся из тела отчёта и блока ; Total detected:, а не из %ERRORLEVEL%. Нефатальными считаются 0, 3, 101, 102. Фатальны 1 (неверный параметр), 2, 4 (отмена), -10 на форме /@: и -1, если процесс не стартовал. Ранняя ревизия как раз отбрасывала пакеты с кодом 3 и рисовала ноль детектов на сторе, где уже лежало письмо с эксплойтом.
Списки обхода в UTF-16 эта линейка отвергает и тратит на это минуты. В работу идут ANSI и OEM, плюс прямые аргументы с бюджетом командной строки (по умолчанию 7000 символов, размер пакета при необходимости режется). Типичный -BatchSize — 40 сообщений на вызов, таймаут — 60 минут.
SCAN и UPDATE не принимают /login и /password. Если дописать учётные данные «на всякий случай», вызов ломается. Харнесс добавляет секреты только к задокументированно защищённым командам (ACTIVATE, EXPORT, STOP и соседние). Неверный пароль специально не зондируется: серия ошибок блокирует Password-protection.
Разбор отчёта двуязычный. Английские и русские маркеры (suspicion / detected / skipped и кириллические аналоги) равноправны. Файл отчёта читается с BOM, как UTF-16 или в OEM-кодовой странице. Иерархия контейнера выглядит так:
C:\mail\Data\example.com\user\a1\{ID}.eml//attachment.rtf//equation → suspicion → HEUR:Exploit.MSOffice...
Базовый .eml — ключ к сообщению hMailServer. Хвост после // — объект внутри. Без этой нарезки легко «удалить вложение», которого как файла на диске не существует.
Покрытие считается отдельно: сколько файлов хотели, сколько реально прошли, сколько исчезли, сколько пакетов упало. Частичное покрытие — не повод считать аудит успешным. Непросканированные пути пишутся в unscanned-files.json; уже найденные детекты при этом остаются валидными. -AllowPartialCoverage лишь глушит громкое предупреждение.
Типичные отказы сходятся к короткому чеклисту. No avp.com build answered the HELP command — нет резидентного процесса, сессия не интерактивная или не elevated, либо все кандидаты из мёртвой установки. 0x80040154 на hMailServer.Application — COM не зарегистрирован или не совпала разрядность pwsh. Пустая статистика при ненулевом Total detected — другая вёрстка отчёта у этой сборки, смотреть kaspersky-aggregated.txt.
Модель безопасности и граница ответственности
Инструмент привилегированный: он умеет вычищать чужую почту. Поэтому в коде намеренно много отказов.
Kaspersky всегда зовут с /i0. Антивирус не имеет права менять стор. Удаление возможно только из просмотренного плана, только после повторной сверки пути с текущим Data, только через COM — либо, в крайнем FileOnly, с явной пометкой, что база останется грязной. Неудачный карантин отменяет удаление этого письма. Пароль живёт как SecureString; в харнессе секреты маскируются в логе, CSV, JSON и консоли, а в Cmd-раннере уходят через переменную окружения процесса, которую подставляет cmd.exe.
Этого недостаточно, если относиться к CSV как к кнопке «удалить всё». HEUR: — эвристика. Ложное срабатывание на легитимном вложении бухгалтерии хуже, чем лишний день карантина. Практичное правило: удалять автоматически можно разве что однозначные семейства вроде EICAR и классических Trojan/Worm на входящих; эвристики по Office и Equation — только после взгляда на subject, отправителя и внутренний объект.
Второй контур — расследование, а не гигиена диска. Если эксплойт дошёл до INBOX, вопрос не «файл ещё на месте?», а «открывали ли его с Outlook / через веб-клиент, есть ли запуск макроса, ушла ли копия в пересылку». Карантинный .bad стоит держать, пока этот вопрос не закрыт.
Третий — операционный. Отчёты лучше класть на том с аудитом. -DataDirectory нельзя направлять туда, что не бэкапилось сегодня. Планировщик в режиме Plan снижает риск «ночью кто-то нажал Delete», но не заменяет ревью человеком.
Набор не заменяет входящий антивирусный хук hMailServer, SPF/DKIM/DMARC, серые списки и обучение пользователей. Он закрывает слепое пятно: то, что уже лежит в сторе. Если это пятно один раз закрыть полным сканом, дальше достаточно узкого -SinceDays и привычки не оставлять CSV без разбора.
Репозиторий: github.com/paulmann/hmailserver-kaspersky-cleanup. Лицензия MIT. Полезны issue с живыми фрагментами отчётов других сборок Kaspersky: парсер вердиктов держится на полевой грамматике, а не на обещаниях справки avp.com HELP SCAN.