Предисловие


Все началось с того, что купленное корпоративное решение для аудита систем не предоставляло нужную информацию по используемому антивирусному продукту, да и работала слишком уж долго, а панель управления, используемого компанией антивируса, оставляет желать лучшего. Решено было использовать «костыль» для сбора информации об антивирусах в домене.

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

Что же собственно нужно и как это сделать?


Первым делом было установлено, что же нужно получать от антивируса и как это сделать в короткий промежуток времени со всего домена.

Получать нужно было:

1) Наименование установленного антивируса
2) Активен ли антивирус
3) Обновлены ли на нем базы

Собственно способ был найден моментально — использовать WMI (Windows Management Instrumentation).

Пришлось погрузиться в изучение самой структуры WMI на ПК, в этом мне помог замечательный набор утилит WMI Tools.

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

Разбор полетов


Сценарий обрабатывает 3500 хостов примерно за 1 час. Собственно сам код сценария:

Код сценария
WScript.Interactive = true

compid = 0

On Error Resume Next
	
Set objDomain = GetObject("LDAP://DOMAIN/OU=Workstations,DC=DOMAIN") 

comps = Array() 

for each objDomainItem in objDomain 
	if objDomainItem.objectClass = "Computer" then
		idxLast = UBound (comps)
		ReDim Preserve comps(idxLast + 1)
		comps(idxLast + 1) = objDomainItem.dNSHostName
	end if
next

Set objFS = CreateObject("Scripting.FileSystemObject") 
Set objNewFile = objFS.CreateTextFile("C:\TEMP\AV_Check\Reports\AV_Status_Scan_is_Running.WAIT") 

a =    "<style>"
a = a& "@import ""TableFilter/filtergrid.css"";"
a = a& "BODY{background-color:Lavender ;}"
a = a& "TABLE{font-size: 10pt; font-family: arial;}"
a = a& "TH{background-color: buttonface; }"
a = a& "</style>"


objNewFile.WriteLine "<html>"
objNewFile.WriteLine "<head>"
objNewFile.WriteLine "<script language=""javascript"" type=""text/javascript"" src=""TableFilter/tablefilter.js""></script>"
objNewFile.WriteLine "<title>AntiVirus status information</title>"
objNewFile.WriteLine a & "</head><body>"
objNewFile.WriteLine "<h2>AVSI -- Date: " & Now() & "</h2>"

objNewFile.WriteLine "<script type=""text/javascript"">"
objNewFile.WriteLine "var tableToExcel = (function() {"
objNewFile.WriteLine "	var uri = 'data:application/vnd.ms-excel;base64,'"
objNewFile.WriteLine "	, template = '<html xmlns:o=""urn:schemas-microsoft-com:office:office"" xmlns:x=""urn:schemas-microsoft-com:office:excel""><head></head><body><table>{table}</table></body></html>'"
objNewFile.WriteLine "	, base64 = function(s) { return window.btoa(unescape(encodeURIComponent(s))) }"
objNewFile.WriteLine "	, format = function(s, c) { return s.replace(/{(\w+)}/g, function(m, p) { return c[p]; }) }"
objNewFile.WriteLine "return function(table, name) {"
objNewFile.WriteLine "	if (!table.nodeType) table = document.getElementById(table)"
objNewFile.WriteLine "	var ctx = {worksheet: name || 'Worksheet', table: table.innerHTML}"
objNewFile.WriteLine "	window.location.href = uri + base64(format(template, ctx))"
objNewFile.WriteLine "}"
objNewFile.WriteLine "})()"
objNewFile.WriteLine "</script>"
objNewFile.WriteLine "<input type=""button"" onclick=""tableToExcel('table1', 'Export data')"" value=""Export data to Excel"">"
	
objNewFile.WriteLine "<table id=""table1"" BORDER=""1"" width=""100%"">" 
objNewFile.WriteLine "<tr><th width=""2%"">id</th><th>Computer</th><th>AV Name</th><th>AV Status</th><th>AV Bases</th><th>Host Status</th></tr>"

for each comp in comps 
	compid = compid + 1 
		
	Set WshShell = WScript.CreateObject("WScript.Shell")
	Ping = WshShell.Run("ping -n 1 " & comp, 0, True)
	Select Case Ping
	Case 0
		On Error Resume next 
		Set oWMI = GetObject("winmgmts:\\" & comp & "\root\SecurityCenter2") 
		On Error Resume next 
		Set colAVItems = oWMI.ExecQuery("Select * from AntiVirusProduct") 
		If colAVItems.count = 0 Then 
			objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th><font color=""red"">No AntiViruses found</font></th><th><font color=""red"">Disabled</font></th><th><font color=""red"">NOT Up to Date</font></th><th><font color=""green"">Online</font></th></tr>"
		ElseIf colAVItems.count = 1 Then 
			For Each AntiVirus in colAVItems
				If (AntiVirus.displayName) <> "Windows Defender" Then 
					AVStatus = hex(AntiVirus.ProductState) 
					If (AVStatus = "61000")  _
					OR (AVStatus = "51000") _
					OR (AVStatus = "41000") Then 
						objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th>" & AntiVirus.displayName & "</th><th><font color=""green"">Active</font></th><th><font color=""green"">Up to Date</font></th><th><font color=""green"">Online</font></th></tr>"
					ElseIf (AVStatus = "41010") _
					OR (AVStatus = "61010") _
					OR (AVStatus = "51010") Then
						objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th>" & AntiVirus.displayName & "</th><th><font color=""green"">Active</font></th><th><font color=""red"">NOT Up to Date</font></th><th><font color=""green"">Online</font></th></tr>"
					ElseIf (AVStatus = "60000") _
					OR (AVStatus = "50000") _
					OR (AVStatus = "40000") Then
						objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th>" & AntiVirus.displayName & "</th><th><font color=""red"">On Access scanning disabled!</font></th><th><font color=""green"">Up to Date</font></th><th><font color=""green"">Online</font></th></tr>"
					Else 
						objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th>" & AntiVirus.displayName & "</th><th><font color=""purple"">Unknown</font></th><th><font color=""purple"">Unknown</font></th><th><font color=""green"">Online</font></th></tr>"
					End if
				End If
			Next
		End If
	Case 1
		objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th><font color=""purple"">Unknown</font></th><th><font color=""purple"">Unknown</font></th><th><font color=""purple"">Unknown</font></th><th><font color=""red"">Offline</font></th></tr>"
	End Select
Next
objNewFile.WriteLine "</table>"
	
objNewFile.WriteLine "<script language=""javascript"" type=""text/javascript"">"
objNewFile.WriteLine "//<![CDATA["
objNewFile.WriteLine "var tableFilters = {"
objNewFile.WriteLine "	sort_select: true,"
objNewFile.WriteLine "	loader: true,"
objNewFile.WriteLine "	rows_counter: true,"
objNewFile.WriteLine "	col_2: ""select"","
objNewFile.WriteLine "	col_3: ""select"","
objNewFile.WriteLine "	col_4: ""select"","
objNewFile.WriteLine "	col_5: ""select"","
objNewFile.WriteLine "	on_change: true,"
objNewFile.WriteLine "	display_all_text: "" [ Show all ] "","
objNewFile.WriteLine "	alternate_rows: true,"
objNewFile.WriteLine "}"
objNewFile.WriteLine "setFilterGrid( ""table1"",tableFilters);"
objNewFile.WriteLine "//]]>"
objNewFile.WriteLine "</script>"

objNewFile.WriteLine "<h2>End of scan: " & Now() & "</h2>"

objNewFile.WriteLine "</body>"
objNewFile.WriteLine "</html>"
objNewFile.Close 
objFS.MoveFile "C:\TEMP\AV_Check\Reports\AV_Status_Scan_is_Running.WAIT", "C:\TEMP\AV_Check\Reports\AV_Status_" & Date & "_" & Hour(Now()) & "." & Minute(Now()) & ".htm"


Что же собственно происходит? А происходит именно вот что:

1) Мы подключаемся к домену и заходим в каталог Workstations:

Подключение к домену посредством LDAP
Set objDomain = GetObject("LDAP://DOMAIN/OU=Workstations,DC=DOMAIN")


2) Создаем пустой массив, в который будут добавляться ПК пользователей с соответствующим классом Computer:

Создание и заполнение массива
for each objDomainItem in objDomain
	if objDomainItem.objectClass = "Computer" then
		idxLast = UBound (comps)
		ReDim Preserve comps(idxLast + 1)
		comps(idxLast + 1) = objDomainItem.dNSHostName
	end if
next


3) Создаем файл отчета. Отчет было решено формировать в .htm формате и соответственно формируем страницу в самом сценарии:

Формирование htm страницы
Set objFS = CreateObject("Scripting.FileSystemObject") 
Set objNewFile = objFS.CreateTextFile("C:\TEMP\AV_Check\Reports\AV_Status_Scan_is_Running.WAIT") 

a =    "<style>"
a = a& "@import ""TableFilter/filtergrid.css"";"
a = a& "BODY{background-color:Lavender ;}"
a = a& "TABLE{font-size: 10pt; font-family: arial;}"
a = a& "TH{background-color: buttonface; }"
a = a& "</style>"


objNewFile.WriteLine "<html>"
objNewFile.WriteLine "<head>"
objNewFile.WriteLine "<script language=""javascript"" type=""text/javascript"" src=""TableFilter/tablefilter.js""></script>"
objNewFile.WriteLine "<title>AntiVirus status information</title>"
objNewFile.WriteLine a & "</head><body>"
objNewFile.WriteLine "<h2>AVSI -- Date: " & Now() & "</h2>"

objNewFile.WriteLine "<script type=""text/javascript"">"
objNewFile.WriteLine "var tableToExcel = (function() {"
objNewFile.WriteLine "	var uri = 'data:application/vnd.ms-excel;base64,'"
objNewFile.WriteLine "	, template = '<html xmlns:o=""urn:schemas-microsoft-com:office:office"" xmlns:x=""urn:schemas-microsoft-com:office:excel""><head></head><body><table>{table}</table></body></html>'"
objNewFile.WriteLine "	, base64 = function(s) { return window.btoa(unescape(encodeURIComponent(s))) }"
objNewFile.WriteLine "	, format = function(s, c) { return s.replace(/{(\w+)}/g, function(m, p) { return c[p]; }) }"
objNewFile.WriteLine "return function(table, name) {"
objNewFile.WriteLine "	if (!table.nodeType) table = document.getElementById(table)"
objNewFile.WriteLine "	var ctx = {worksheet: name || 'Worksheet', table: table.innerHTML}"
objNewFile.WriteLine "	window.location.href = uri + base64(format(template, ctx))"
objNewFile.WriteLine "}"
objNewFile.WriteLine "})()"
objNewFile.WriteLine "</script>"
objNewFile.WriteLine "<input type=""button"" onclick=""tableToExcel('table1', 'Export data')"" value=""Export data to Excel"">"
	
objNewFile.WriteLine "<table id=""table1"" BORDER=""1"" width=""100%"">" 
objNewFile.WriteLine "<tr><th width=""2%"">id</th><th>Computer</th><th>AV Name</th><th>AV Status</th><th>AV Bases</th><th>Host Status</th></tr>"

{ ... }

objNewFile.WriteLine "</table>"
	
objNewFile.WriteLine "<script language=""javascript"" type=""text/javascript"">"
objNewFile.WriteLine "//<![CDATA["
objNewFile.WriteLine "var tableFilters = {"
objNewFile.WriteLine "	sort_select: true,"
objNewFile.WriteLine "	loader: true,"
objNewFile.WriteLine "	rows_counter: true,"
objNewFile.WriteLine "	col_2: ""select"","
objNewFile.WriteLine "	col_3: ""select"","
objNewFile.WriteLine "	col_4: ""select"","
objNewFile.WriteLine "	col_5: ""select"","
objNewFile.WriteLine "	on_change: true,"
objNewFile.WriteLine "	display_all_text: "" [ Show all ] "","
objNewFile.WriteLine "	alternate_rows: true,"
objNewFile.WriteLine "}"
objNewFile.WriteLine "setFilterGrid( ""table1"",tableFilters);"
objNewFile.WriteLine "//]]>"
objNewFile.WriteLine "</script>"

objNewFile.WriteLine "<h2>End of scan: " & Now() & "</h2>"

objNewFile.WriteLine "</body>"
objNewFile.WriteLine "</html>"
objNewFile.Close 


Для удобства был использован HTML Table Filter Generator, который формирует фильтры по завершению выполнения сценария по сформированной таблице с полями id, Computer, AV Name, AV Status, AV Bases.

Также добавлена кнопка выгрузки таблицы в Excel, которую я нашел на каком-то форуме (честно сказать с ней я даже не возился и оставил как есть, работает криво и, как показала практика, только под FireFox).

4) Массив у нас сформирован и хранится в памяти, теперь рассмотрим заполнение таблицы отчета. Первым делом мы ставим счетчик для присваивания id каждому хосту из массива. После этого вызываем Shell скрипт для проверки доступности хоста путем отправки на него 1 icmp пакета. Создаем условия на обработку отклика, Case 0 — хост доступен и выполняем сценарий сбора данных для заполнения таблицы, Case 1 — выводим в таблицу данные о том, что хост не доступен:

Проверка доступности хоста
for each comp in comps 
	compid = compid + 1
		
	Set WshShell = WScript.CreateObject("WScript.Shell")
	Ping = WshShell.Run("ping -n 1 " & comp, 0, True)
	Select Case Ping
	Case 0
		{ ... }
	Case 1
		objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th><font color=""purple"">Unknown</font></th><th><font color=""purple"">Unknown</font></th><th><font color=""purple"">Unknown</font></th><th><font color=""red"">Offline</font></th></tr>"
	End Select
Next


5) Проверяем есть ли у нас ошибки, а если есть, то просто переходим к следующему хосту. Информация по Антивирусам, Антишпионам и Межсетевым экранам в WMI хранится в каталоге \root\SecurityCenter2, по-этому подключаемся к нему и создаем WQL запрос, который собственно и вытаскивает нужную нам информацию по продукту. Проверяем есть ли вообще на хосте антивирус, а если есть, то проверяем не Windows Defender ли он, а если даже и он, то просто игнорируем его. Каждый антивирус имеет свой код состояния, об этом я узнал на форуме:

Проверка статуса Антивируса и заполнение таблицы
On Error Resume next 
Set oWMI = GetObject("winmgmts:\\" & comp & "\root\SecurityCenter2") 
On Error Resume next 
Set colAVItems = oWMI.ExecQuery("Select * from AntiVirusProduct") 
	If colAVItems.count = 0 Then 
		objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th><font color=""red"">No AntiViruses found</font></th><th><font color=""red"">Disabled</font></th><th><font color=""red"">NOT Up to Date</font></th><th><font color=""green"">Online</font></th></tr>"
	ElseIf colAVItems.count = 1 Then 
		For Each AntiVirus in colAVItems
			If (AntiVirus.displayName) <> "Windows Defender" Then 
				AVStatus = hex(AntiVirus.ProductState) 
				If (AVStatus = "61000")  _
				OR (AVStatus = "51000") _
				OR (AVStatus = "41000") Then 
					objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th>" & AntiVirus.displayName & "</th><th><font color=""green"">Active</font></th><th><font color=""green"">Up to Date</font></th><th><font color=""green"">Online</font></th></tr>"
				ElseIf (AVStatus = "41010") _
				OR (AVStatus = "61010") _
				OR (AVStatus = "51010") Then
					objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th>" & AntiVirus.displayName & "</th><th><font color=""green"">Active</font></th><th><font color=""red"">NOT Up to Date</font></th><th><font color=""green"">Online</font></th></tr>"
				ElseIf (AVStatus = "60000") _
				OR (AVStatus = "50000") _
				OR (AVStatus = "40000") Then
					objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th>" & AntiVirus.displayName & "</th><th><font color=""red"">On Access scanning disabled!</font></th><th><font color=""green"">Up to Date</font></th><th><font color=""green"">Online</font></th></tr>"
				Else 
					objNewFile.WriteLine "<tr><th>" & compid & "</th><th>" & comp & "</th><th>" & AntiVirus.displayName & "</th><th><font color=""purple"">Unknown</font></th><th><font color=""purple"">Unknown</font></th><th><font color=""green"">Online</font></th></tr>"
				End if
			End If
		Next
	End If


Бонус


Ну и как бонус код на Python. Он создает текстовый файл, который можно использовать для выгрузки в Excel (в виде разделителя колонок — табуляция). К сожалению посредством LDAP выгружать список хостов у меня не получилось, а так же при использовании большого списка сценарий падает, возможно кто-нибудь доработает его для своих нужд:

Сам код
import wmi
import codecs
import os

with open('Comp_list.txt','r') as list:

	file = codecs.open('text.txt', 'w', 'utf-8')

	file.write("Computer" + "	AV Name" + "	Host Status" + "	AV Status" + "	AV Bases\n")

	for comp in list:
		response = os.system("ping -n 1 " + comp)
		if response == 0:
			path = '//%s/root/SecurityCenter2' % comp
			c = wmi.WMI(moniker=path)
			wql = "Select * from AntiVirusProduct"
			wql = c.query(wql)
			if wql == []:
				file.write(comp + "	no AntiVirus found" + "	Online" + "	Unknown" + "	Unknown\n")
			else:
				for AntiVirus in wql:
					ProductState_in_hex = str(hex(AntiVirus.ProductState))
					check_install = ProductState_in_hex[0:3]
					check_state = ProductState_in_hex[3:5]
					check_updates = ProductState_in_hex[5:7]
					if check_state == "10" and check_updates == "00":
						file.write(comp + "	" + AntiVirus.displayName + "	Online" + "	Active" + "	Up to Date\n")
					elif check_state == "10" and check_updates == "10":
						file.write(comp + "	" + AntiVirus.displayName + "	Online" + "	Active" + "	NOT Up to Date\n")
					elif check_state == "00" and check_updates == "00":
						file.write(comp + "	" + AntiVirus.displayName + "	Online" + "	On Access scanning disabled!" + "	Up to Date\n")
					elif check_state == "00" and check_updates == "00":
						file.write(comp + "	" + AntiVirus.displayName + "	Online" + "	On Access scanning disabled!" + "	NOT Up to Date\n")
					else:
						file.write(comp + "	" + AntiVirus.displayName + "	Online" + "	Unknown state\n" + "	" + check_install)
		else:
			file.write(comp + "	Unknown" + "	Offline" + "	Unknown" + "	Unknown\n")
	file.close()


Комментарии (15)


  1. GoldenStar
    11.03.2016 21:13

    Возможно, что весь скрипт VBA можно запускать прямо в листе Excel и заполнять данными ячейки листа. Формировать файл *.txt не понадобится. Когда то давным давно в 2002 году пользовался аналогичными VBA решениями работая с БД MS Access. Создавал пользовательские формы для работы с БД и импортировал/экспортировал данные из других баз данных.


    1. Valsek
      12.03.2016 10:01
      -1

      Можно, но я решил сделать так, потому что мне так удобнее, на вкус и цвет как говорится… вариантов как и куда складировать данные уйма (есть вариант и с MySQL), так что это на любителя


  1. Sergey-S-Kovalev
    12.03.2016 07:18

    Я правильно понимаю, что на 3500+ ПК не используется нормальное корпоративное решение в котором видно все что Вы тут костылите, и даже гораздо больше?

    Не то что бы я против VBA, но даже такое решение принесло бы больше пользы в PowerShell реализации. Для сообщества системных администраторов по крайней мере.


  1. domix32
    12.03.2016 13:06
    +1

    А почему вы не воспользовались многострочной строкой для HTML кусков? Вместо этого гоняете операции записи


    1. Valsek
      12.03.2016 13:49

      Сначала написал представление на html, а потом скопировал код и добавил в начало каждой строки код записи, особо заниматься организацией красивого кода времени не было, т.к. нужно было это решение сделать в достаточно короткие сроки. Сам сценарий написать — час от силы со всеми проверками и условиями, но разбор работы с самим WMI и поиск информации о представлении в нем(wmi) сведений об антивирусе заняло много времени. Для меня WMI был темным лесом на тот момент.


      1. domix32
        12.03.2016 14:06
        +1

        То есть было что-то вроде

        fileWrite(
        "<html>
        <body>
        Hello, habr
        </body>
        </html>"
        )

        вы сделали

        fileWrite("<html>")
        fileWrite("<body>")
        fileWrite(var_habr)
        fileWrite("</body>")
        fileWrite("</html>")

        вместо

        fileWrite(
        "<html>
        <body>" +
        var_habr
        + "</body>
        </html>"
        )

        Но зачем? Удвоили работу, не?


        1. Valsek
          12.03.2016 14:33

          Да, я с Вами согласен, что можно было поступить проще и изящнее. Я сделал замену в notepad++ по регулярке и потому то, что вы считаете удвоением было произведено моментально. Сейчас код имеет совершенно иной вид и работает по-другому (идет запись в БД MySQL, а данные в свою очередь выгружаются на php страничку).


  1. Sleuthhound
    12.03.2016 20:38

    А где код TableFilter/tablefilter.js и TableFilter/filtergrid.css? Откройте для себя github.


    1. Valsek
      13.03.2016 09:49

      Ссылка по тексту


  1. edinorog
    13.03.2016 13:47
    -2

    3500 машин и костыль? жизнь не научила пользоваться вещами подобными этой? https://www.esetnod32.ru/business/products/era/


    1. teecat
      14.03.2016 10:24
      +1

      Не поверите — куча проектов по переходу на иной антивирус и компании совершенно не знают, что стоит в их сети и как это все удалить. Особенно на такие запросы был богат 2014й — у организаций тогда появились деньги и желание привести инфраструктуру в порядок.

      Кстати насколько я знаю рекорд закупки на защиту без централизованного управления помнится был на 10 тыс машин


  1. eevdokimov
    14.03.2016 12:26

    Подскажите, а как сделать сканирование если OUшек с рабочими станциями несколько:
    OU=Computers,OU=Центр,OU=НашаФирма,DC=usl,DC=local
    OU=Computers,OU=Регион1,OU=НашаФирма,DC=usl,DC=local
    OU=Computers,OU=Регион2,OU=НашаФирма,DC=usl,DC=local
    OU=Computers,OU=Регион...,OU=НашаФирма,DC=usl,DC=local
    OU=Computers,OU=РегионN,OU=НашаФирма,DC=usl,DC=local


    1. Valsek
      14.03.2016 13:05

      С ходу могу предложить 3 варианта (кому-то покажутся они грубоватыми), а может кто и лучше подскажет решение:

      Первый вариант
      Set objDomain1 = GetObject("LDAP://usl.local/OU=Computers,OU=Центр,OU=НашаФирма,DC=usl,DC=local")
      Set objDomain2 = GetObject("LDAP://usl.local/OU=Computers,OU=Регион1,OU=НашаФирма,DC=usl,DC=local")
      Set objDomain3 = GetObject("LDAP://usl.local/OU=Computers,OU=Регион2,OU=НашаФирма,DC=usl,DC=local")
      ...
      Set objDomainN = GetObject("LDAP://usl.local/OU=Computers,OU=РегионN,OU=НашаФирма,DC=usl,DC=local")


      1. eevdokimov
        14.03.2016 13:13
        +1

        Спасибо.


  1. whiplash
    14.03.2016 15:22

    KAV или ESET ???
    У обоих, насколько я в курсе — адекватное централизованное управление есть