[Из песочницы] Kaspersky Security Center — борьба за автоматизацию

Как это ни странно, я нашёл на Хабре всего одну статью по данной тематике — и ту в песочнице и сильно незаконченную фактически содержащую в себе маленький кусочек чуть переделанной справки по продукту. Да и Google по запросу klakaut молчит.

Я не собираюсь рассказывать, как администрировать иерархию Kaspersky Security Center (далее по тексту KSC) из командной строки — мне это пока не понадобилось ни разу. Просто хочу поделиться некоторыми соображениями по поводу средств автоматизации с теми, кому это может понадобиться, и разберу один кейс, с которым мне пришлось столкнуться. Если тебе, %habrauser%, эта тема будет интересной — добро пожаловать под кат.
Исторически сложилось так, что в качестве средства антивирусной защиты на работе я предпочитаю продукты Лаборатории Касперского (далее ЛК). Причины и прочие священные войны личных мнений, пожалуй, оставим за кадром.

Естественно, хотелось бы централизованно развернуть, защитить, оградить и не пущать рисовать красивые графики, интегрироваться в существующие системы мониторинга и заниматься прочим перекладыванием работы с больной головы на здоровый сервер. И если с развёртыванием и защитой тут всё более или менее в порядке (у ЛК даже есть какие-то онлайн-курсы по продуктам), то с интеграцией уже сильно грустнее: в последней на текущий момент версии KSC 10.2.434 появилась интеграция аж с двумя SIEM: Arcsight и Qradar. На этом всё.

Для интеграции в что-то своё KSC предоставляет аж 2 интерфейса:

  • klakdb: в БД KSC есть ряд представлений с именами, начинающимися на «v_akpub_», из которых можно достать какую-то информацию о состоянии антивирусной защиты.
  • klakaut: DCOM-объект, позволяющий скриптовать работу с KSC.


По обоим пунктам есть документация в составе KSC, как это и указано в статьях, на которые я дал ссылки. Правда, документация эта вызывает ряд вопросов, которые можно задать в службу поддержки корпоративных продуктов CompanyAccount и получить ответ вида «Уточнили информацию. К сожалению, поддержка по скриптам для klakaut не оказывается.».

Минусы klakdb очевидны: чтобы напрямую обратиться к БД, нужно иметь к этой БД доступ, что приводит к необходимости лишних телодвижений по созданию правил доступа в межсетевых экранах, настройке прав доступа на серверах СУБД и прочему крайне неувлекательному времяпрепровождению. Ну и плюс мониторинг актуальности всех этих правил, естественно. Особенно интересно становится, когда имеется 20+ серверов — и все в разных филиалах, в каждом из которых свои администраторы.

Ну и вишенки на этом торте: доступ исключительно Read Only и, мягко говоря, неполная информация о среде. Плюсы не столь очевидны, но тоже есть: можно очень быстро выгрузить по одному серверу статистическую информацию по количеству хостов, используемым версиям антивирусов и антивирусных баз, а главное — можно весьма удобно (удобство зависит от знания SQL) работать с зарегистрированными на KSC событиями антивирусной инфраструктуры.

klakaut в этом плане значительно более интересен: подключившись к корневому серверу иерархии, можно средствами самого KSC пройтись по оной иерархии и получить доступ ко всем нужным данным. Например, построить дерево серверов KSC с пометкой, кто из них живой, а кто нет, позапускать задачи, поперемещать компьютеры и вообще дать волю фантазии.

Минусы тоже есть, естественно: долго и сложно. Если нужно собрать какую-то статистику — нужно будет сначала долго писать скрипт, а потом долго ловить баги ждать, когда он отработает.

Естественно, никто не запрещает (по крайней мере, мне про это неизвестно) использовать оба механизма вместе: например, пройтись по иерархии серверов с помощью klakaut, получить полный список серверов KSC с информацией об используемых БД, а потом уже передать эту информацию в более другие средства автоматизации, которые удалят устаревшие правила из сетевых экранов, создадут новые, дадут разрешения на доступ и принесут кофе в постель отредактируют список источников данных в вашей системе мониторинга, которая, в свою очередь, опросит список и, обнаружив какие-нибудь девиации, с помощью klakaut сделает что-нибудь хорошее. Ну, или просто зарегистрирует инцидент в трекере. Тогда что-нибудь хорошее сделают администраторы в ручном режиме.

Воодушевлённый всеми этими соображениями, я написал свой первый скрипт:

Вот он
$Params = New-Object -ComObject 'klakaut.KlAkParams';
$Params.Add('Address', 'localhost:13000');
$Params.Add('UseSSL', $true);

$Proxy = New-Object -ComObject 'klakaut.KlAkProxy';
try {
    $Proxy.Connect($Params);
    $Proxy.Disconnect();
} catch {
    $_;
}

Remove-Variable -Name 'Params';
Remove-Variable -Name 'Proxy';



И запустил его на сервере:

Exception calling "Connect" with "1" argument(s): "Transport level error while connecting to http://localhost:13000: authentication failure"
At line:7 char:5
+     $Proxy.Connect($Params);
+     ~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : ComMethodTargetInvocation


Начало хорошее.

Хозяйке на заметку

Если использовать js, эта ошибка не возникает. Интересно, почему.


Открыв консоль KSC, я убедился, что с правами у меня всё в порядке.

К сожалению, KSC не логирует неудачные попытки входа. Переписка с вендором показала, что логирование неудачных попыток входа можно включить (это была отдельная увлекательная история, которая, кстати, ещё не закончилась), однако данная конкретная попытка в логи всё равно попадать отказалась.

Казалось бы, можно сделать вот так:

Неправильный скрипт
$Params = New-Object -ComObject 'klakaut.KlAkParams';
$Params.Add('Address', 'localhost:13000');
$Params.Add('UseSSL', $true);

#------------ Зададим в явном виде данные для входа --------------------------------
$Params.Add('User', 'kavadmin');
$Params.Add('Password', 'P@ssw0rd');
$Params.Add('Domain', 'test');
#-----------------------------------------------------------------------------------

$Proxy = New-Object -ComObject 'klakaut.KlAkProxy';
try {
    $Proxy.Connect($Params);
    $Proxy.Disconnect();
}

Remove-Variable -Name 'Params';
Remove-Variable -Name 'Proxy';



В этом случае никаких ошибок не будет, но указывать логин с паролем открытым текстом в скрипте мне не показалось замечательной идеей. Новая переписка с техподдержкой ЛК подарила мне следующую рекомендацию:

Необходимо выставить в настройках COM, на вкладке Default Properties:
Default Authentication Level: Packet
Default Impersonation Level: Delegate


Эта инструкция заставила работать исходный скрипт, но показалась мне сомнительной в плане безопасности, поэтому я решил покопать чуть глубже. После некоторого времени поисков нашёлся добрый человек, который подсказал мне, как задать указанные уровни проверки подлинности и олицетворения для конкретного объекта, а не разрешать всем и всё сразу:

Правильный скрипт
$Params = New-Object -ComObject 'klakaut.KlAkParams';
$Params.Add('Address', 'localhost:13000');
$Params.Add('UseSSL', $true);

$Proxy = New-Object -ComObject 'klakaut.KlAkProxy';

$code =  @"
using System;
using System.Runtime.InteropServices;

public class PowershellComSecurity
{
   [DllImport("Ole32.dll", CharSet = CharSet.Auto)]
   public static extern int CoSetProxyBlanket(IntPtr p0, uint p1, uint p2, uint p3, uint p4, uint p5, IntPtr p6, uint p7);

   public static int EnableImpersonation(object objDCOM) { return CoSetProxyBlanket(Marshal.GetIDispatchForObject(objDCOM), 10, 0, 0, 0, 3, IntPtr.Zero, 0); }
}
"@
Add-Type -TypeDefinition $code;
Remove-Variable -Name 'code';

[PowershellComSecurity]::EnableImpersonation($Proxy) | Out-Null;

try {
    $Proxy.Connect($Params);
    # <-- Вот сюда мы будем вставлять код
    $Proxy.Disconnect();
} catch {
    $_;
}

Remove-Variable -Name 'Params';
Remove-Variable -Name 'Proxy';


Вот так скрипт никаких ошибок выдавать не стал. Первый квест пройден.

Хозяйке на заметку

Вообще в боевой среде неплохо бы проверять при вызове EnableImpersonation возвращаемое значение на предмет ошибок, а не перенаправлять его в никуда, как это сделал я.


Следующая задача: получить от KSC данные об используемой БД.

А вот тут всё сложно: документация о том, как это сделать, молчит. Исследование класса KlAkProxy ничего интересного не выявило, кроме параметра KLADMSRV_SERVER_HOSTNAME, который оказался идентификатором компьютера, на котором установлен KSC.

Перейдём тогда к компьютеру, для этого есть специальный класс KlAkHosts2. Для сокращения количества кода приведу только содержимое блока try:

блок try
try {
    $Proxy.Connect($Params);
    $KSCHost = New-Object -ComObject 'klakaut.KlAkHosts2';
    $KSCHost.AdmServer = $Proxy;
    $HostParams = New-Object -ComObject 'klakaut.KlAkCollection';
    $HostParams.SetSize(1);
    $HostParams.SetAt(0, 'KLHST_WKS_DN');
    ($KSCHost.GetHostInfo($Proxy.GetProp('KLADMSRV_SERVER_HOSTNAME'), $HostParams)).Item('KLHST_WKS_DN');
    Remove-Variable -Name 'HostParams';
    Remove-Variable -Name 'KSCHost';
    $Proxy.Disconnect();
}


Обратите внимание: переменная $Params, которую я использовал при подключении к KSC — экземпляр класса KlAkParams. А переменная $HostParams при, на мой взгляд, аналогичной функциональности, является экземпляром класса KlAkCollection. Почему используются разные классы — боюсь даже представить. Видимо, то, что SetAt принимает первым аргументом только целочисленные значения — очень принципиальный момент.

Данный код вернул значение «KSC», а значит, я на верном пути.

Метод GetHostInfo класса KlAkHosts2 достаточно хорошо задокументирован, но — не содержит нужной мне информации. Увы и ах. Зато есть метод GetHostSettings. Всё описание для которого сводится к следующему:

Returns host’s settings as setting storage.


Давайте, заглянем внутрь:

try
try {
    $Proxy.Connect($Params);
    $KSCHost = New-Object -ComObject 'klakaut.KlAkHosts2';
    $KSCHost.AdmServer = $Proxy;
    $KSCSettings = $KSCHost.GetHostSettings($Proxy.GetProp('KLADMSRV_SERVER_HOSTNAME'), 'SS_SETTINGS');
    $KSCSettings.Enum() | % {
        '------------------------';
        $tmp = $_;
        $tmp | % {"$_ = $($tmp.Item($_))";};
        Remove-Variable -Name 'tmp';
    };
    Remove-Variable -Name 'KSCSettings';
    Remove-Variable -Name 'KSCHost';
    $Proxy.Disconnect();
}



Результат
------------------------
PRODUCT = .core
SECTION = SubscriptionData
VERSION = .independent
------------------------
PRODUCT = 1093
SECTION = 85
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = 87
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLEVP_NF_SECTION
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLNAG_SECTION_DPNS
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLSRV_CONSRVINIT
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLSRV_CONSRVUPGRADE
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLSRV_DEF_NAGENT_PACKAGE
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLSRV_MASTER_SRV
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLSRV_NETSIZE_SECTION
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLSRV_PKG_ANDROID_CERT_SECTION
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLSRV_PROXY_SECTION
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLSRV_SRVLIC_SECTION
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KLSRV_USER_ACCOUNTS_SECTION
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KSNPROXY_KEY_STORAGE
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = KSNPROXY_SETTINGS
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = Packages
VERSION = 1.0.0.0
------------------------
PRODUCT = 1093
SECTION = Updater
VERSION = 1.0.0.0
------------------------
PRODUCT = 1103
SECTION = 85
VERSION = 1.0.0.0
------------------------
PRODUCT = 1103
SECTION = 86
VERSION = 1.0.0.0
------------------------
PRODUCT = 1103
SECTION = FileTransfer
VERSION = 1.0.0.0
------------------------
PRODUCT = 1103
SECTION = KLEVP_NF_SECTION
VERSION = 1.0.0.0
------------------------
PRODUCT = 1103
SECTION = KLNAG_KLNLA_DATA
VERSION = 1.0.0.0
------------------------
PRODUCT = 1103
SECTION = KLNAG_SECTION_NETSCAN
VERSION = 1.0.0.0
------------------------
PRODUCT = 1103
SECTION = KLNAG_SECTION_SERVERDATA
VERSION = 1.0.0.0
------------------------
PRODUCT = 1103
SECTION = Updater
VERSION = 1.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = .KLNAG_SECTION_REBOOT_REQUEST
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = 85
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = Backup section
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = Business logic section
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = HSM system section
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = Internal product info
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = KLEVP_NF_SECTION
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = Notification section
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = Predefined tasks section
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = Quarantine section
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = Reporting section
VERSION = 8.0.0.0
------------------------
PRODUCT = KAVFSEE
SECTION = Trusted processes section
VERSION = 8.0.0.0



В klakaut.chm есть раздел «List of KLHST_WKS_PRODUCT_NAME and KLHST_WKS_PRODUCT_VERSION values for products», где можно подсмотреть, что поле PRODUCT для KSC должно быть 1093, соответственно, всё остальное можно смело проигнорировать. Пока что, по крайней мере.

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

try
try {
    $Proxy.Connect($Params);
    $KSCHost = New-Object -ComObject 'klakaut.KlAkHosts2';
    $KSCHost.AdmServer = $Proxy;
    $KSCSettings = $KSCHost.GetHostSettings($Proxy.GetProp('KLADMSRV_SERVER_HOSTNAME'), 'SS_SETTINGS');
    $KSCSettings.Read('1093', '1.0.0.0', '85');
    '-----------------';
    $KSCSettings.Read('1093', '1.0.0.0', '87');
    Remove-Variable -Name 'KSCSettings';
    Remove-Variable -Name 'KSCHost';
    $Proxy.Disconnect();
}



Результат
EventFolder
EventStoragePath
KLAG_WAIT_SCHED_FOR_START_EVENT
TaskStoragePath
-----------------
KLSRV_AD_SCAN_ENABLED
KLSRV_CONNECTION_DATA
KLSRV_DATABASENAME
KLSRV_NET_SCAN_ENABLED
KLSRV_SERVERINSTANCENAME
KLSRV_SP_DPNS_ENABLE
KLSRV_SP_FASTUPDATENET_PERIOD
KLSRV_SP_FULLUPDATENET_PERIOD
KLSRV_SP_INSTANCE_ID
KLSRV_SP_MAX_EVENTS_IN_DB
KLSRV_SP_OPEN_AKLWNGT_PORT
KLSRV_SP_SCAN_AD
KLSRV_SP_SERVERID
KLSRV_SP_SERVERID_DPE
KLSRV_SP_SERVER_AKLWNGT_PORTS_ARRAY
KLSRV_SP_SERVER_PORTS_ARRAY
KLSRV_SP_SERVER_SSL_PORTS_ARRAY
KLSRV_SP_SERVER_SSL_PORTS_ARRAY_GUI
KLSRV_SP_SYNC_LIFETIME
KLSRV_SP_SYNC_LOCKTIME
KLSRV_SP_SYNC_SEC_PACKET_SIZE
KLSRV_SSL_CERT_RSA_BIT_NUMBER



Секция 85, судя по всему, отвечает за события и ныне нам неинтересна. А вот в 87 есть что-то, на что стоит обратить внимание:

try
try {
    $Proxy.Connect($Params);
    $KSCHost = New-Object -ComObject 'klakaut.KlAkHosts2';
    $KSCHost.AdmServer = $Proxy;
    $KSCSettings = $KSCHost.GetHostSettings($Proxy.GetProp('KLADMSRV_SERVER_HOSTNAME'), 'SS_SETTINGS');
    $87 = $KSCSettings.Read('1093', '1.0.0.0', '87');
    "KLSRV_SERVERINSTANCENAME = $($87.Item('KLSRV_SERVERINSTANCENAME'))";
    "KLSRV_DATABASENAME = $($87.Item('KLSRV_DATABASENAME'))";
    "KLSRV_CONNECTION_DATA =`r`n$($87.Item('KLSRV_CONNECTION_DATA') | % {"`t$_ = $($87.Item('KLSRV_CONNECTION_DATA').Item($_))`r`n";})";
    Remove-Variable -Name '87';
    Remove-Variable -Name 'KSCSettings';
    Remove-Variable -Name 'KSCHost';
    $Proxy.Disconnect();
}



Результат
KLSRV_SERVERINSTANCENAME = .
KLSRV_DATABASENAME = KAV
KLSRV_CONNECTION_DATA =
        KLDBCON_DB = KAV
        KLDBCON_DBTYPE = MSSQLSRV
        KLDBCON_HOST = .



Тут я воспользовался одним из предыдущих кейсов, где упоминалось, что нужные данные следует брать именно из KLSRV_CONNECTION_DATA (тогда я ещё не знал, что это вообще такое, просто отложилось).

Ну, вот, в общем-то, и всё. Данные об используемой БД получены. Квест пройден.

Наверное, ещё неплохо бы набросать скрипт для прохождения по иерархии серверов. Здесь ничего загадочного не оказалось, всё было вполне по документации. Я написал скрипт, который выбирает UID родителя, UID самого сервера, экземпляр СУБД и имя БД и выводит их в stdout через разделитель.

Скрипт
$SrvAddr = 'localhost:13291'

function EnumSrv(
    $Pxy,
    [bool]$IsAlive = $true,
    [string]$ParentPxyId = 'Root'
)
{
    [string]$result = "$ParentPxyId";
    if ($IsAlive) {
        $result += "|$($Pxy.GetProp('KLADMSRV_SERVER_HOSTNAME'))";
        
        $Hosts = New-Object -ComObject 'klakaut.KlAkHosts2';
        $Hosts.AdmServer = $Pxy;
        
        $Settings = $Hosts.GetHostSettings($Pxy.GetProp('KLADMSRV_SERVER_HOSTNAME'), 'SS_SETTINGS').Read('1093', '1.0.0.0', '87').Item('KLSRV_CONNECTION_DATA');
        Remove-Variable -Name 'Hosts';
    
        #'-------->   DB  Info <--------';
        $result += "|$($Settings.Item('KLDBCON_HOST'))";
        $result += "|$($Settings.Item('KLDBCON_DB'))";
        #'-----------------------------';
        
        Remove-Variable -Name 'Settings';
        
        $SlaveSrvEnum = New-Object -ComObject 'klakaut.KlAkSlaveServers';
        $SlaveSrvEnum.AdmServer = $Pxy;
        $SlaveServers = $SlaveSrvEnum.GetServers(-1);
        
        $SlaveServers | % {
            $Child = $_;
            $TmpSrvId = $Child.Item('KLSRVH_SRV_ID');
            $HostActive = $true;
            try
            {
                $TmpSrv = $SlaveSrvEnum.Connect($TmpSrvId, -1);
            }
            catch
            {
                $HostActive = $false;
            };
            if ($HostActive) {$HostActive = ($TmpSrv.GetProp('IsAlive') -eq 1);};
            $result += "`r`n$(EnumSrv -Pxy $TmpSrv -IsAlive $HostActive -ParentPxyId $Pxy.GetProp('KLADMSRV_SERVER_HOSTNAME'))";
        };
        Remove-Variable -Name 'SlaveServers';
        Remove-Variable -Name 'SlaveSrvEnum';
    };
    return ("$result`r`n");
}

Clear-Host

$Params = New-Object -ComObject 'klakaut.KlAkParams'
$Params.Add('Address', $SrvAddr)
$Params.Add('UseSSL', $true)

$code =  @"
using System;
using System.Runtime.InteropServices;

public class PowershellComSecurity
{
   [DllImport("Ole32.dll", CharSet = CharSet.Auto)]
   public static extern int CoSetProxyBlanket(IntPtr p0, uint p1, uint p2, uint p3, uint p4, uint p5, IntPtr p6, uint p7);

   public static int EnableImpersonation(object objDCOM) { return CoSetProxyBlanket(Marshal.GetIDispatchForObject(objDCOM), 10, 0, 0, 0, 3, IntPtr.Zero, 0); }
}
"@
Add-Type -TypeDefinition $code

$Srv = New-Object -ComObject 'klakaut.KlAkProxy'
[PowershellComSecurity]::EnableImpersonation($Srv) | Out-Null
$Srv.Connect($Params)

"ParentPxyId|KLADMSRV_SERVER_HOSTNAME|KLDBCON_HOST|KLDBCON_DB`r`n" + (EnumSrv -Pxy $Srv);
Remove-Variable -Name 'Srv';
Remove-Variable -Name 'Params';



Стенд маленький, поэтому результат оказался не очень впечатляющим:

Результат
ParentPxyId|KLADMSRV_SERVER_HOSTNAME|KLDBCON_HOST|KLDBCON_DB
Root|9d476a75-1e36-4c0e-8145-56e5b888df67|.|KAV
9d476a75-1e36-4c0e-8145-56e5b888df67|ef4fc3be-3abd-4322-ae35-2c50afdce780|.\KAV_CS_ADMIN_KIT|KAV



Естественно, чтобы превратить точку в актуальное имя сервера, придётся поколдовать с KlAkHosts2.GetHostInfo (), но это уже не столь страшно, просто ещё сколько-то кода.

Техподдержка ЛК, естественно, пугала меня тем, что структура SS_SETTINGS в следующих релизах KSC может поменяться, поэтому так лучше не делать. К сожалению, даже мой внутренний перфекционист считает, что скрипт нельзя просто написать и забыть: при смене версии используемого ПО его по-любому придётся тестировать и отлаживать. Так что пока пользуемся тем, что есть, а проблемы будем решать по мере поступления, благо, техника уже отработана.

© Habrahabr.ru