Как это ни странно, я нашёл на Хабре всего одну статью по данной тематике — и ту в песочнице и сильно незаконченную фактически содержащую в себе маленький кусочек чуть переделанной справки по продукту. Да и 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 может поменяться, поэтому так лучше не делать. К сожалению, даже мой внутренний перфекционист считает, что скрипт нельзя просто написать и забыть: при смене версии используемого ПО его по-любому придётся тестировать и отлаживать. Так что пока пользуемся тем, что есть, а проблемы будем решать по мере поступления, благо, техника уже отработана.