Отчёт о том, что запускает пользователь на своём компьютере, крайне важен. С многих точек зрения. Особенно с точки зрения информационной безопасности.
Информация о запуске программ на компьютерах пользователей храниться в журнале безопасности. Конечно, рассматривается среда Windows. В Инете готового решения не нашёл, поэтому сделал свою реализацию.
Скрипт запускается на сервере. На выходе имеем набор файлов с отчётами о запуске программ.
Картинка для привлечения внимания:
![](http://habr.habrastorage.org/post_images/155/a2c/8a1/155a2c8a1d1b836b72d51769a2c1191d.png)
Основная идея такая. Текущие события журнала безопасности сохраняются в evt-файле на клиентском компьютере. Файл копируется на сервер, где информация из него загружается на SQL Server. Затем SQL-запросом формируется отчёт и сохраняется в файл.
Теперь, как это реализовано.
Необходимо создать папки Log, Logs, CheckComps, Logi_ForReports и Computer. У меня папки на диске F. В папке Log создать файл list.txt со списком компьютеров, которые необходимо проверить. Каждое имя компьютера с новой строки. Я создал 2 файла list.txt и list7.txt для XP и семёрок соответственно. В папке Computer создать файл is_computer_online_listComps.vbs
on error resume next dim gsFileName dim gsRunCmd dim gix dim giy dim giz if Wscript.Arguments.Count = 1 then gsFileName = Wscript.Arguments(0) gsOS = "XP" elseif Wscript.Arguments.Count = 2 then gsFileName = Wscript.Arguments(0) gsOS = Wscript.Arguments(1) else gsFileName = InputBox("Файл со списком компьютеров", "Ввод", "F:\Log\list.txt") gsOS = InputBox("Тип операционной системы:" & VBNewLine & "'XP' - для Windows 2000/XP" & VBNewLine & "'7' - для Windows 7", "Ввод", "XP") end if gsOS = uCase(gsOS) wscript.echo "gsOS: " & gsOS if inStr(gsOS, "XP") = 0 and inStr(gsOS, "7") = 0 then MsgBox "Некорректно указан тип операционной системы!", vbInformation, "Внимание" Wscript.Quit end if WScript.Echo "Файл со списком компьютеров: " & gsFileName Set objFSO = CreateObject("Scripting.FileSystemObject") Set objTextFileOpen = objFSO.OpenTextFile(gsFileName, 1) gix = 0 giy = 0 Set WshShell = CreateObject("WScript.Shell") do until objTextFileOpen.AtEndOfStream gsComputerName = objTextFileOpen.Readline giy = giy + 1 loop objTextFileOpen.Close wscript.echo "Найдено компьютеров: " & giy Set objTextFileOpen = objFSO.OpenTextFile(gsFileName, 1) do until objTextFileOpen.AtEndOfStream gsComputerName = objTextFileOpen.Readline gix = gix + 1 giz = gix * 100 giz = giz / giy giz = Round(giz, 1) giOst = giy - gix if fuPing(gsComputerName) then wscript.echo gsComputerName & VBTab & " осталось: " & giOst & ", готово: " & giz & "%" if inStr(gsOS, "XP") then gsRunCmd = "f:\Computer\is_computer_online.bat " & gsComputerName & " y" elseif inStr(gsOS, "7") then gsRunCmd = "f:\Computer\is_computer_online7.bat " & gsComputerName & " y" end if WshShell.Run gsRunCmd if giOst <> 0 then WScript.Sleep 180000 ' Внимание! Вот это задержка в 180 секунд между компьютерами. end if else wscript.echo gsComputerName & VBTab & " осталось: " & giOst & ", готово: " & giz & "%. Выключен." end if loop objTextFileOpen.Close WScript.Echo "Операция завершена!" function fuPing(NetworkDevice) lBoo = false set objPING = GetObject("winmgmts:{impersonationLevel=impersonate}")._ ExecQuery ("select * from Win32_PingStatus where address ='" & NetworkDevice & "'") For Each PING In objPing if PING.StatusCode = 0 then 'WScript.Echo "* Компьютер " & NetworkDevice & " в сети!" lBoo = true else 'WScript.Echo "* Компьютера нет в сети." end if next fuPing = lBoo end function
Запускается процедура проверки bat-файлом. Ссылку на него можно сделать, например, на рабочем столе.
cscript //nologo "f:\Computer\is_computer_online_listComps.vbs" %1 %2
Основной скрипт is_computer_online_listComps.vbs читает список компьютеров из текстового файла и для каждого запускает bat-файл формирования отчёта. Для XP — это файл is_computer_online.bat, для 7 — is_computer_online7.bat.
Примечание.
На сервере нужно установить logparser.
Всё описанное должно заработать и на компьютере администратора. Только надо установить Microsoft SQL SERVER 2008 NATIVE CLIENT и Microsoft SQL Server 2008 Command Line Utilities. Но я не проверял.
Блок работы с компьютерами XP
Bat-файл:
cscript //nologo "f:\Computer\is_computer_online.vbs" %1 %2
Bat-файл запускает скрипт. Скрипт выполняет сохранение событий журнала безопасности в evt-файл и запускает основной батник mo2csv.bat.
on error resume next dim gsComputerName dim gsUseLogFile dim gsLogFilename dim gbFlag dim gsTableName dim gsCompName dim gsRunCmd if Wscript.Arguments.Count = 1 then gsComputerName = Wscript.Arguments(0) gsUseLogFile = "n" elseif Wscript.Arguments.Count = 2 then gsComputerName = Wscript.Arguments(0) gsUseLogFile = Wscript.Arguments(1) else gsComputerName = InputBox("Имя компьютера", "Введите", "") gsUseLogFile = InputBox("Использовать log-файл для проверки?" & VBNewline & "[y/n]", "Введите", "y") end if WScript.Echo "* Имя компьютера " & gsComputerName gsLogFilename = "f:\Log\" & gsComputerName & ".log" if lCase(gsUseLogFile) = "y" then gbFlag = false WScript.Echo "* Файл журнала " & gsLogFilename set objFSO = CreateObject("Scripting.FileSystemObject") if not objFSO.FileExists(gsLogFilename) then WScript.Echo "* Файла журнала нет. Создается..." set objTextFileWriteLog = objFSO.OpenTextFile(gsLogFilename, 8, True) objTextFileWriteLog.writeLine "n" objTextFileWriteLog.close WScript.Echo "* Создан успешно." end if set objTextFileOpen = objFSO.OpenTextFile(gsLogFilename, 1) do until objTextFileOpen.AtEndOfStream record = trim(objTextFileOpen.Readline) if record = "n" then WScript.Echo "* Компьютер не проверялся ранее." if fuPing(gsComputerName) then gbFlag = true if fuBackup(gsComputerName) then WScript.Sleep 15000 ' <- 15 секунд задержки для бекапа fuUploadEvents gsComputerName wscript.sleep 10000 end if end if elseif record = "y" then WScript.Echo "* Информация с компьютера " & gsComputerName & " уже закачана на сервер." else WScript.Echo "* Некорректная информация о компьютере " & gsComputerName & " в log-файле." end if loop objTextFileOpen.close if gbFlag then set objTextFileWriteLog = objFSO.OpenTextFile(gsLogFilename, 2, True) objTextFileWriteLog.writeLine "y" objTextFileWriteLog.close WScript.Echo "* Информация записана в журнал." end if else 'if fuPing(gsComputerName) then if fuBackup(gsComputerName) then WScript.Sleep 15000 ' <- 15 секунд задержки для бекапа fuUploadEvents gsComputerName wscript.sleep 10000 end if 'end if end if wscript.sleep 1000 function fuPing(NetworkDevice) lBoo = false set objPING = GetObject("winmgmts:{impersonationLevel=impersonate}")._ ExecQuery ("select * from Win32_PingStatus where address ='" & NetworkDevice & "'") For Each PING In objPing if PING.StatusCode = 0 then WScript.Echo "* Компьютер " & NetworkDevice & " в сети!" lBoo = true else WScript.Echo "* Компьютера нет в сети." end if next fuPing = lBoo end function function fuBackup(lsComputername) lsEvtBackupFilename = "c:\" & lsComputername & ".evt" lsEvtBackupFilenameRemote = "\\" & lsComputername & "\c$\" & lsComputername & ".evt" lbFlag = false set lObjFSO = CreateObject("Scripting.FileSystemObject") if lObjFSO.FileExists(lsEvtBackupFilenameRemote) then WScript.Echo "* Файл журнала уже есть. Используем существующий..." lbFlag = true else Wscript.Echo "* Выполняется резервное копирование..." Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate,(Backup)}!\\" & lsComputername & "\root\cimv2") Set colLogFiles = objWMIService.ExecQuery ("Select * from Win32_NTEventLogFile where LogFileName='Security'") For Each objLogfile in colLogFiles errBackupLog = objLogFile.BackupEventLog(lsEvtBackupFilename) If errBackupLog = 0 Then Wscript.Echo "* Резервное копирование выполнено успешно." lbFlag = true Else Wscript.Echo "* Резервное копирование не выполнено." End If Next end if fuBackup = lbFlag end function function fuUploadEvents(lsComputername) WScript.Echo "* Запущена закачка на сервер..." gsCompName = lCase(lsComputername) gsTableName = fuGetTableName(gsCompName) gsTableName = uCase(gsTableName) gsOutputFilename = "f:\Computer\" & gsCompName & ".csv" gsOutputFilenameSQL = "f:\Computer\" & gsCompName & "_sql.csv" Set WshShell = CreateObject("WScript.Shell") gsRunCmd = "f:\Computer\mo2csv.bat " & gsCompName & " " & gsOutputFilename & " " & gsOutputFilenameSQL & " " & gsTableName WScript.Echo "* Выполняется команда: '" & gsRunCmd & "'" WshShell.Run gsRunCmd end function function fuGetTableName(lsCompName) lsTmp = lsCompName if InStr(lsTmp, "-") then lsTmp = Replace(lsTmp, "-", "_") end if fuGetTableName = lsTmp end function
mo2csv.bat делает следующее:
- Забирает evt-файл с удалённого компьютера на сервер.
- Преобразовывает evt-файл в evtx.
- Выгружает только события запуска/остановки программ из evtx-файла в текстовый файл csv.
- Информацию из текстового файла закачивает на SQL Server.
- Бекапит оригинальный evt-файл в папку Logi_ForReports (вдруг пользователь сотрёт свой журнал, а у нас копия есть).
- Удаляет временный evtx-файл.
- Формирует и выполняет sql-запрос к SQL Server’у.
- Удаляет временные файлы (в случае отладки или для изучения работы скрипта, этот раздел можно закомментировать).
- Перемещает отчёты в папку CheckComps.
@echo off @set WDate=%date:~-10% @echo * Журнал безопасности перемещается с удаленного компьютера %1 (Windows XP)... move \\%1\c$\%1.evt f:\Logs\ @echo * Перемещение завершено. @echo * Выполняется конвертация evt журнала в evtx... wevtutil epl f:\Logs\%1.evt f:\Logs\%1.evtx /lf:true @echo * Конвертация завершена. @echo * Информация из журнала безопасности выгружается в текстовый файл. Источник: f:\Logs\%1.evtx, назначение: %2 LogParser.exe file:"f:\Computer\get_info_from_log.sql"?source=f:\Logs\%1.evtx+output_file=%2 -i:EVT -o:TSV -headers:ON -oSeparator:tab -oTsFormat:"dd.MM.yyyy hh:mm:ss" -fileMode:1 @echo * Выгрузка завершена. @echo * Исправляю текстовый файл %2. Получаю %3... cscript F:\Computer\update_csvFile_forSQLCheck.vbs %2 %3 //NoLogo @echo * Исправление завершено. @echo * Информация из текстового файла закачивается на SQL Server. Источник: %3, таблица %4... LogParser.exe file:"f:\Computer\get_info_from_log_2SQL.sql"?source=%3+output_file=%4 -i:TSV -headerRow:ON -iSeparator:tab -iTsFormat:"dd.MM.yyyy hh:mm:ss" -o:SQL -server:"SQL-SRV\SEC" -database:quickly -driver:"SQL Server" -createTable:ON @echo * Процесс завершен. @echo * Журнал безопасности перемещается в архив... move f:\Logs\%1.evt f:\Logi_ForReports\%1_%WDate%_sec.evt @echo * Перемещение завершено. Имя архивного файла 'f:\Logi_ForReports\%1_%WDate%_sec.evt' @echo * Удаление временного evtx журнала... del f:\Logs\%1.evtx @echo * Удаление завершено. @echo * Создание sql-запроса... cscript "F:\Computer\create_SQL_full.vbs" %1 1 //nologo @echo * Создание завершено. @echo * Выполнение sql-запроса... SQLCMD.EXE -S SQL-SRV\SEC -d quickly -E -i f:\Computer\%1-1.sql -o "f:\Computer\%1. Запуск программ.csv" -W -R -s ";" -w 4000 @echo * Выполнение завершено. @echo * Исправляю результирующие файлы отчетов... cscript F:\Computer\update_result_file.vbs "f:\Computer\%1. Запуск программ.csv" //nologo @echo * Исправление завершено. @echo * Удаление временных файлов... del f:\Computer\%1-1.sql del %2 del %3 del f:\Computer\%1_dbg.txt del "f:\Computer\%1. Запуск программ.csv" @echo * Удаление завершено. @echo * Перемещение файлов-отчетов... move "f:\Computer\%1. Запуск программ.xls" "f:\CheckComps\%1. Запуск программ.xls" @echo * Перемещение завершено. @echo on
Примечание
Возможно, в батнике нужно будет SQLCMD.EXE заменить на «c:\Program Files\Microsoft SQL Server\100\Tools\Binn\SQLCMD.EXE», а LogParser.exe на «c:\Program Files (x86)\Log Parser 2.2\LogParser.exe» (или «c:\Program Files\Log Parser 2.2\LogParser.exe»).
Имя сервера с SQL Server’ом SQL-SRV, имя экземпляра SEC и имя базы quickly. Заменить на свои.
SELECT RecordNumber as id, eventid as eId, TimeGenerated as Tg, resolve_sid(sid) as UserName, computername as Computer, EXTRACT_TOKEN(Strings, 0, '|') as image_unique_id, EXTRACT_TOKEN(Strings, 1, '|') as image into %output_file% FROM %source% where ((EventID in (592; 593)) and (TO_UPPERCASE(resolve_sid(sid)) <> 'NT AUTHORITY\NETWORK SERVICE') and (TO_UPPERCASE(resolve_sid(sid)) <> 'NT AUTHORITY\SYSTEM')) and TimeGenerated >= TO_TIMESTAMP('01.03.2011 00:00:00','dd.MM.yyyy hh:mm:ss') order by recordnumber asc
SELECT * into %output_file% FROM %source%
On Error Resume Next dim gsSimbolSplitFields dim sgSimbolSplitAdmin dim gbInsideBlock dim gIx dim gbDebug dim gbWriteString Dim gArrBlock_admin gsSimbolSplitFields = vbTab sgSimbolSplitAdmin = ";" gbInsideBlock = false gbIERuning = false gbIE = false giBlockPlus = 0 giIEPlus = 0 gsDateBlock = "01.01.2011 00:00:00" TgBlockStop = "01.01.2011 00:00:00" idBlockStop = "" gIx = 0 gArrBlock_admin = Array (sgSimbolSplitAdmin & sgSimbolSplitAdmin, _ sgSimbolSplitAdmin & sgSimbolSplitAdmin, _ sgSimbolSplitAdmin & sgSimbolSplitAdmin, _ sgSimbolSplitAdmin & sgSimbolSplitAdmin) gbDebug = true 'gbDebug = false if Wscript.Arguments.Count = 1 then sgFilename = Wscript.Arguments(0) sgFilenameOut = fuRemoveExtention(sgFilename) & "_sql.csv" gsLogFilename = fuRemoveExtention(sgFilename) & "_dbg.txt" elseif Wscript.Arguments.Count = 2 then sgFilename = Wscript.Arguments(0) sgFilenameOut = Wscript.Arguments(1) gsLogFilename = fuRemoveExtention(sgFilename) & "_dbg.txt" elseif Wscript.Arguments.Count = 3 then sgFilename = Wscript.Arguments(0) sgFilenameOut = Wscript.Arguments(1) gsLogFilename = Wscript.Arguments(2) else sgFilename = InputBox("Имя исходного файла", "Введите", "f:\comp-6475.csv") sgFilenameOut = InputBox("Имя результирующего файла", "Введите", fuRemoveExtention(sgFilename) & "_sql.csv") gsLogFilename = InputBox("Имя файла журнала", "Введите", fuRemoveExtention(sgFilename) & "_dbg.txt") end if Set objFSO = CreateObject("Scripting.FileSystemObject") if not objFSO.FileExists(sgFilename) then wscript.echo "Исходного файла для обновления нет, выхожу!" Wscript.Quit end if Set objTextFileOpen = objFSO.OpenTextFile(sgFilename, 1) Set objTextFileWrite = objFSO.CreateTextFile(sgFilenameOut, True) if gbDebug then if not objFSO.FileExists(gsLogFilename) then set objTextFileWriteLog = objFSO.OpenTextFile(gsLogFilename, 8, True) else set objTextFileWriteLog = objFSO.CreateTextFile(gsLogFilename, True) end if end if Do Until objTextFileOpen.AtEndOfStream record = trim(objTextFileOpen.Readline) gIx = gIx + 1 gbWriteString = true fuPrint gIx & ". '" & record & "'" if InStr(record, gsSimbolSplitFields) then arr = Split(record, gsSimbolSplitFields) id = arr(0) eId = arr(1) Tg = arr(2) UserName = arr(3) Computer = arr(4) image_unique_id = arr(5) image = arr(6) if InStr(lCase(image), "explorer.exe") then if eId = "592" then gbBlockBegin = true gbBlockEnd = false giBlockPlus = giBlockPlus + 1 fuPrint "explorer.exe старт" else gbBlockBegin = false gbBlockEnd = true giBlockPlus = giBlockPlus - 1 if giBlockPlus < 0 then giBlockPlus = 0 end if fuPrint "explorer.exe стоп" end if else gbBlockBegin = false gbBlockEnd = false end if if InStr(lCase(image), "iexplore.exe") then gbIE = true fuPrint "Строка с iexplore.exe" if eId = "592" then fuPrint "iexplore.exe старт" giIEPlus = giIEPlus + 1 gbIERuning = true if giIEPlus = 1 then image_unique_idIEStart = image_unique_id end if else fuPrint "iexplore.exe стоп" giIEPlus = giIEPlus - 1 gbIERuning = false end if else gbIE = false fuPrint "Строка без iexplore.exe" end if if gIx = 1 then objTextFileWrite.WriteLine record & gsSimbolSplitFields & "CompStart" fuPrint "Первая строка, записываем" elseif gIx = 2 then fuPrint "вторая строка" if gbBlockBegin then fuPrint "Начало блока, записываем" gbInsideBlock = true gsDateBlock = Tg gsUserNameBlockStart = UserName image_unique_idBlockStart = image_unique_id objTextFileWrite.WriteLine record & gsSimbolSplitFields & gsDateBlock end if idPrev = id eIdPrev = eId TgPrev = Tg UserNamePrev = UserName ComputerPrev = Computer image_unique_idPrev = image_unique_id imagePrev = image else 'fuPrint "остальные строки" '-- Запуск explorer.exe if gbBlockBegin then fuPrint "Запуск explorer.exe (экземпляр № " & giBlockPlus & ")" if giBlockPlus = 1 then giDiff = DateDiff("s", CDate(TgBlockStop), CDate(Tg)) if giDiff > 9 then if Len(idBlockStop) > 0 then fuPrint "Это не перезапуск explorer.exe. Записываем остановку предыдущего блока" record_convert_prev = idBlockStop & gsSimbolSplitFields & _ eIdBlockStop & gsSimbolSplitFields & _ TgBlockStop & gsSimbolSplitFields & _ UserNameBlockStop & gsSimbolSplitFields & _ ComputerBlockStop & gsSimbolSplitFields & _ image_unique_idBlockStart & gsSimbolSplitFields & _ imageBlockStop & gsSimbolSplitFields & _ gsDateBlock fuPrint record_convert_prev objTextFileWrite.WriteLine record_convert_prev end if gsDateBlock = Tg fuPrint "Новая дата блока: '" & gsDateBlock & "'" image_unique_idBlockStart = image_unique_id fuPrint "Новый код блока: '" & image_unique_idBlockStart & "'" gsUserNameBlockStart = UserName fuPrint "Новый пользователь блока: '" & gsUserNameBlockStart & "'" else fuPrint "Это перезапуск explorer.exe! Не записываем остановку предыдущего блока и не записываем запуск этого." gbWriteString = false end if gbInsideBlock = true else if lCase(gsUserNameBlockStart) = lCase(UserName) then fuPrint "gsUserNameBlockStart: '" & gsUserNameBlockStart & "', UserName: '" & UserName & "'" fuPrint "Начало блока. Возможно, компьютер был аварийно выключен. Необходимо записать окончание предыдущего блока и сохранить новые параметры блока" record_convert_prev = "999" & gsSimbolSplitFields & _ "593" & gsSimbolSplitFields & _ Tg & gsSimbolSplitFields & _ UserName & gsSimbolSplitFields & _ Computer & gsSimbolSplitFields & _ image_unique_idBlockStart & gsSimbolSplitFields & _ "C:\WINDOWS\explorer.exe" & gsSimbolSplitFields & _ gsDateBlock fuPrint record_convert_prev objTextFileWrite.WriteLine record_convert_prev giBlockPlus = 1 gsDateBlock = Tg fuPrint "Новая дата блока: '" & gsDateBlock & "'" image_unique_idBlockStart = image_unique_id fuPrint "Новый код блока: '" & image_unique_idBlockStart & "'" else 'fuPrint "gsUserNameBlockStart: '" & gsUserNameBlockStart & "', UserName: '" & UserName & "'" fuPrint "Не начало блока. Возможно, администратор запустил explorer. Записать текущую строку и сохранить параметры" gArrBlock_admin(giBlockPlus-2) = image_unique_id & sgSimbolSplitAdmin & UserName & sgSimbolSplitAdmin & Tg fuPrint gArrBlock_admin(giBlockPlus-2) 'gsDateBlock_admin = Tg objTextFileWrite.WriteLine record & gsSimbolSplitFields & Tg gbWriteString = false end if end if end if '-- Остановка explorer.exe if gbBlockEnd then fuPrint "Остановлен explorer.exe (осталось экземпляров " & giBlockPlus & ")" if giBlockPlus = 0 then fuPrint "Остановлен последний экземпляр, сохраняем его значения" idBlockStop = id eIdBlockStop = eId TgBlockStop = Tg UserNameBlockStop = UserName ComputerBlockStop = Computer image_unique_idBlockStop = image_unique_id imageBlockStop = image gbInsideBlock = false giIEPlus = 0 ' <-- Добавил для обнуления количества копий IE else fuPrint "Остановлен не последний экземпляр, его значения не сохраняем, только записываем текущую строку" for giY = 0 to UBound(gArrBlock_admin) arrA = Split(gArrBlock_admin(giY), sgSimbolSplitAdmin) gsImage_unique_id_A = arrA(0) gsUserName_A = arrA(1) gsTg_A = arrA(2) if gsImage_unique_id_A = image_unique_id then gsDateBlock_admin = gsTg_A end if next objTextFileWrite.WriteLine record & gsSimbolSplitFields & gsDateBlock_admin gbWriteString = false end if end if '-- Записать текущую строку if gbInsideBlock then if gbIE then if (((gbIERuning) and (giIEPlus = 1)) or ((not gbIERuning) and (giIEPlus = 0))) then fuPrint "Записать IE строку" record_convert_prev = id & gsSimbolSplitFields & _ eId & gsSimbolSplitFields & _ Tg & gsSimbolSplitFields & _ UserName & gsSimbolSplitFields & _ Computer & gsSimbolSplitFields & _ image_unique_idIEStart & gsSimbolSplitFields & _ image & gsSimbolSplitFields & _ gsDateBlock fuPrint record_convert_prev objTextFileWrite.WriteLine record_convert_prev else fuPrint "Вот хрень с IE! gbIERuning: " & gbIERuning & ", giIEPlus: " & giIEPlus record_convert_prev = id & gsSimbolSplitFields & _ eId & gsSimbolSplitFields & _ Tg & gsSimbolSplitFields & _ UserName & gsSimbolSplitFields & _ Computer & gsSimbolSplitFields & _ image_unique_idIEStart & gsSimbolSplitFields & _ image & gsSimbolSplitFields & _ gsDateBlock fuPrint record_convert_prev end if else if gbWriteString then fuPrint "Текущая строка в блоке и ее нужно записать" fuPrint record & gsSimbolSplitFields & gsDateBlock objTextFileWrite.WriteLine record & gsSimbolSplitFields & gsDateBlock else fuPrint "Текущая строка в блоке, но ее записывать не нужно" end if end if else fuPrint "Текущая строка не в блоке. Не записываем" end if '-- Сохранить текущие значения параметров для следующего прохода idPrev = id eIdPrev = eId TgPrev = Tg UserNamePrev = UserName ComputerPrev = Computer image_unique_idPrev = image_unique_id imagePrev = image end if end if fuPrint "----------------------------------------------" Loop objTextFileWrite.Close objTextFileOpen.Close if gbDebug then objTextFileWriteLog.close end if WScript.Echo "" WScript.Echo "* Операция успешно завершена." function fuRemoveExtention(lsFilename) lRes = lsFilename if InStr(lsFilename, ".") then lRes = Left(lsFilename, Len(lsFilename)-4) end if fuRemoveExtention = lRes end function function fuGetDateFromFullDate(lsFullDate) lRes = lsFullDate if InStr(lsFullDate, " ") then lArr = Split(lsFullDate, " ") lsDate = lArr(0) lsTime = lArr(1) lRes = lsDate end if fuGetDateFromFullDate = lRes end function function fuGetTimeFromFullDate(lsFullDate) lRes = lsFullDate if InStr(lsFullDate, " ") then lArr = Split(lsFullDate, " ") lsDate = lArr(0) lsTime = lArr(1) lRes = lsTime end if fuGetDateFromFullDate = lRes end function function fuPrint(lsStr) 'if gbDebug then ' wscript.echo lsStr 'end if if gbDebug then objTextFileWriteLog.writeLine lsStr end if fuPrint = true end function
if Wscript.Arguments.Count = 1 then gsComputerName = Wscript.Arguments(0) gsSQLtype = "1" elseif Wscript.Arguments.Count = 2 then gsComputerName = Wscript.Arguments(0) gsSQLtype = Wscript.Arguments(1) else gsComputerName = InputBox("Имя компьютера", "Введите", "") gsSQLtype = InputBox("Тип sql-запроса?" & VBNewline & "[1 - короткий, 2 - полный, 3 - оба]", "Введите", "1") end if set objFSO = CreateObject("Scripting.FileSystemObject") if gsSQLtype = "1" then fuCreateSQLFile gsComputerName, "1" elseif gsSQLtype = "2" then fuCreateSQLFile gsComputerName, "2" elseif gsSQLtype = "3" then fuCreateSQLFile gsComputerName, "1" fuCreateSQLFile gsComputerName, "2" end if function fuGetTableName(lsCompName) lsTmp = lsCompName if InStr(lsTmp, "-") then lsTmp = Replace(lsTmp, "-", "_") end if fuGetTableName = lsTmp end function sub fuCreateSQLFile(lsComputerName, lsSQLtype) if lsSQLtype = "1" then lsTemplateFilename = "f:\Computer\template-short.sql" elseif gsSQLtype = "2" then lsTemplateFilename = "f:\Computer\template-full.sql" end if lsLogFilename = "f:\Computer\" & lsComputerName & "-" & lsSQLtype & ".sql" lsTableName = fuGetTableName(lsComputerName) if not objFSO.FileExists(lsLogFilename) then set objTextFileWriteLog = objFSO.OpenTextFile(lsLogFilename, 8, True) else set objTextFileWriteLog = objFSO.OpenTextFile(lsLogFilename, 2, True) end if Set objTextFileOpen = objFSO.OpenTextFile(lsTemplateFilename, 1) do until objTextFileOpen.AtEndOfStream record = objTextFileOpen.Readline if InStr(record, "WARNING__TABLE_NAME_FOR_CHANGE") then record = Replace(record, "WARNING__TABLE_NAME_FOR_CHANGE", lsTablename) end if objTextFileWriteLog.writeLine record loop objTextFileOpen.Close objTextFileWriteLog.Close end sub
SELECT TOP (100) PERCENT Computer AS [Имя компьютера], UserName AS [Учетная запись], image AS Программа, start_time AS [Время запуска], stop_time AS [Время завершения], dbo.FU_GET_FULL_QTY_TEST(CONVERT(VARCHAR, DATEDIFF(SECOND, start_time, stop_time) / 3600)) + ':' + dbo.FU_GET_FULL_QTY_TEST(CONVERT(VARCHAR, DATEDIFF(SECOND, start_time, stop_time) % 3600 / 60)) + ':' + dbo.FU_GET_FULL_QTY_TEST(CONVERT(VARCHAR, DATEDIFF(SECOND, start_time, stop_time) % 3600 % 60)) AS Длительность FROM (SELECT r.Computer, r.UserName, r.image_unique_id, r.image, r.Tg AS start_time, MIN(s.Tg) AS stop_time FROM (SELECT id, eId, Tg, UserName, Computer, image_unique_id, image FROM dbo.WARNING__TABLE_NAME_FOR_CHANGE WHERE (eId = 592) AND (Tg > CONVERT(DATETIME, '2013-01-01 00:00:00.000', 102)) ) AS r INNER JOIN (SELECT id, eId, Tg, UserName, Computer, image_unique_id, image FROM dbo.WARNING__TABLE_NAME_FOR_CHANGE WHERE (eId = 593)) AS s ON r.image_unique_id = s.image_unique_id AND r.image = s.image AND r.id < s.id AND r.Tg <= s.Tg GROUP BY r.UserName, r.Computer, r.image_unique_id, r.image, r.Tg) AS DERIVEDTBL ORDER BY 'Время запуска' DESC
if Wscript.Arguments.Count = 1 then gsFileName = Wscript.Arguments(0) gsFileNameRes = fuRemoveExtention(gsFileName) & ".xls" elseif Wscript.Arguments.Count = 2 then gsFileName = Wscript.Arguments(0) gsFileNameRes = Wscript.Arguments(1) else gsFileName = InputBox("Файл для обновления", "Ввод", "") gsFileNameRes = InputBox("Файл результата", "Ввод", fuRemoveExtention(gsFileName) & ".xls") end if sgSimbolSplit = ";" gsSimbolSplitFields = vbTab Set objFSO = CreateObject("Scripting.FileSystemObject") Set objTextFileOpen = objFSO.OpenTextFile(gsFileName, 1) if not objFSO.FileExists(gsFileName) then wscript.echo "Исходного файла для обновления нет, выхожу!" objTextFileOpen.Close Wscript.Quit end if if not objFSO.FileExists(gsFileNameRes) then set objTextFileWriteRes = objFSO.OpenTextFile(gsFileNameRes, 8, True) else set objTextFileWriteRes = objFSO.CreateTextFile(gsFileNameRes, True) end if do until objTextFileOpen.AtEndOfStream record = objTextFileOpen.Readline if ((InStr(record, "--------")) or (Len(record) = 0) or (InStr(record, "обработано строк")) or (InStr(record, "rows affected"))) then 'wscript.echo "пропускаю строку: '" & record & "'" else if InStr(record, sgSimbolSplit) then recordRes = Replace(record, sgSimbolSplit, gsSimbolSplitFields) else recordRes = record end if objTextFileWriteRes.writeLine recordRes end if loop objTextFileWriteRes.Close objTextFileOpen.Close WScript.Echo "Обновление завершено! Результирующий файл " & gsFileNameRes function fuRemoveExtention(lsFilename) lRes = lsFilename if InStr(lsFilename, ".") then lRes = Left(lsFilename, Len(lsFilename)-4) end if fuRemoveExtention = lRes end function
Блок работы с компьютерами с семёркой
Bat-файл:
cscript //nologo "f:\Computer\is_computer_online7.vbs" %1 %2
Bat-файл запускает скрипт. Скрипт выполняет сохранение событий журнала безопасности в evt-файл и запускает основной батник mo7.bat.
on error resume next dim gsComputerName dim gsUseLogFile dim gsLogFilename dim gbFlag dim gsTableName dim gsCompName dim gsRunCmd if Wscript.Arguments.Count = 1 then gsComputerName = Wscript.Arguments(0) gsUseLogFile = "n" elseif Wscript.Arguments.Count = 2 then gsComputerName = Wscript.Arguments(0) gsUseLogFile = Wscript.Arguments(1) else gsComputerName = InputBox("Имя компьютера", "Введите", "") gsUseLogFile = InputBox("Использовать log-файл для проверки?" & VBNewline & "[y/n]", "Введите", "y") end if WScript.Echo "* Имя компьютера " & gsComputerName if lCase(gsUseLogFile) = "y" then gbFlag = false gsLogFilename = "f:\Log\" & gsComputerName & ".log" WScript.Echo "* Файл журнала " & gsLogFilename set objFSO = CreateObject("Scripting.FileSystemObject") if not objFSO.FileExists(gsLogFilename) then WScript.Echo "* Файла журнала нет. Создается..." set objTextFileWriteLog = objFSO.OpenTextFile(gsLogFilename, 8, True) objTextFileWriteLog.writeLine "n" objTextFileWriteLog.close WScript.Echo "* Создан успешно." end if set objTextFileOpen = objFSO.OpenTextFile(gsLogFilename, 1) do until objTextFileOpen.AtEndOfStream record = trim(objTextFileOpen.Readline) if record = "n" then WScript.Echo "* Компьютер не проверялся ранее." if fuPing(gsComputerName) then 'fuListInstalledSoftware gsComputerName gbFlag = true if fuBackup(gsComputerName) then WScript.Sleep 15000 ' <- 15 секунд задержки для бекапа fuUploadEvents gsComputerName wscript.sleep 10000 end if end if elseif record = "y" then WScript.Echo "* Информация с компьютера " & gsComputerName & " уже закачана на сервер." else WScript.Echo "* Некорректная информация о компьютере " & gsComputerName & " в log-файле." end if loop objTextFileOpen.close if gbFlag then set objTextFileWriteLog = objFSO.OpenTextFile(gsLogFilename, 2, True) objTextFileWriteLog.writeLine "y" objTextFileWriteLog.close WScript.Echo "* Информация записана в журнал." 'MsgBox "Компьютер " & gsComputerName & " в сети!", vbInformation, "Внимание" end if else 'if fuPing(gsComputerName) then if fuBackup(gsComputerName) then WScript.Sleep 15000 ' <- 15 секунд задержки для бекапа fuUploadEvents gsComputerName wscript.sleep 60000 end if 'end if end if wscript.sleep 1000 function fuPing(NetworkDevice) lBoo = false set objPING = GetObject("winmgmts:{impersonationLevel=impersonate}")._ ExecQuery ("select * from Win32_PingStatus where address ='" & NetworkDevice & "'") For Each PING In objPing if PING.StatusCode = 0 then WScript.Echo "* Компьютер " & NetworkDevice & " в сети!" lBoo = true else WScript.Echo "* Компьютера нет в сети." end if next fuPing = lBoo end function function fuBackup(lsComputername) lsEvtBackupFilename = "c:\" & lsComputername & ".evt" lsEvtBackupFilenameRemote = "\\" & lsComputername & "\c$\" & lsComputername & ".evt" lbFlag = false 'WScript.Echo "* lsEvtBackupFilename: " & lsEvtBackupFilename 'WScript.Echo "* lsEvtBackupFilenameRemote: " & lsEvtBackupFilenameRemote set lObjFSO = CreateObject("Scripting.FileSystemObject") if lObjFSO.FileExists(lsEvtBackupFilenameRemote) then WScript.Echo "* Файл журнала уже есть. Используем существующий..." lbFlag = true else Wscript.Echo "* Выполняется резервное копирование..." Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate,(Backup)}!\\" & lsComputername & "\root\cimv2") Set colLogFiles = objWMIService.ExecQuery ("Select * from Win32_NTEventLogFile where LogFileName='Security'") For Each objLogfile in colLogFiles errBackupLog = objLogFile.BackupEventLog(lsEvtBackupFilename) If errBackupLog = 0 Then Wscript.Echo "* Резервное копирование выполнено успешно." lbFlag = true Else Wscript.Echo "* Резервное копирование не выполнено." End If Next end if fuBackup = lbFlag end function function fuUploadEvents(lsComputername) WScript.Echo "* Запущена закачка на сервер..." gsCompName = lCase(lsComputername) gsTableName = fuGetTableName(gsCompName) gsTableName = uCase(gsTableName) Set WshShell = CreateObject("WScript.Shell") gsRunCmd = "f:\Computer\mo7.bat " & gsCompName & " " & gsTableName WScript.Echo "* Выполняется команда: '" & gsRunCmd & "'" WshShell.Run gsRunCmd end function function fuGetTableName(lsCompName) lsTmp = lsCompName if InStr(lsTmp, "-") then lsTmp = Replace(lsTmp, "-", "_") end if fuGetTableName = lsTmp end function
mo7.bat делает следующее:
- Забирает evt-файл с удалённого компьютера на сервер.
- Преобразовывает evt-файл в evtx.
- Информацию из evtx-файла закачивает на SQL Server.
- Бекапит оригинальный evt-файл в папку Logi_ForReports (вдруг пользователь сотрёт свой журнал, а у нас копия есть).
- Удаляет временный evtx-файл.
- Формирует и выполняет sql-запрос к SQL Server’у.
- Удаляет временные файлы (в случае отладки или для изучения работы скрипта, этот раздел можно закомментировать).
- Перемещает отчёты в папку CheckComps.
@echo off @set WDate=%date:~-10% @echo * Журнал безопасности перемещается с удаленного компьютера %1 (Windows 7)... move \\%1\c$\%1.evt f:\Logs\ @echo * Перемещение завершено. @echo * Выполняется конвертация evt журнала в evtx... wevtutil epl f:\Logs\%1.evt f:\Logs\%1.evtx /lf:true @echo * Конвертация завершена. @echo * Информация из журнала безопасности закачивается на сервер. Источник: f:\Logs\%1.evtx LogParser.exe file:"f:\Computer\get_info_from_log7.sql"?source=f:\Logs\%1.evtx+output_file=%2 -i:EVT -o:SQL -server:"SQL-SRV\SEC" -database:quickly -driver:"SQL Server" -createTable:ON @echo * Процесс завершен. @echo * Журнал безопасности перемещается в архив... move f:\Logs\%1.evt f:\Logi_ForReports\%1_%WDate%_sec.evt @echo * Перемещение завершено. Имя архивного файла 'f:\Logi_ForReports\%1_%WDate%_sec.evtx' @echo * Создание sql-запроса... cscript "F:\Computer\create_SQL_full7.vbs" %1 1 //nologo @echo * Создание завершено. @echo * Выполнение sql-запроса... SQLCMD.EXE -S SQL-SRV\SEC -d quickly -E -i f:\Computer\%1-1.sql -o "f:\Computer\%1. Запуск программ.csv" -W -R -s ";" -w 4000 @echo * Выполнение завершено. @echo * Исправляю результирующие файлы отчетов... cscript F:\Computer\update_result_file7.vbs "f:\Computer\%1. Запуск программ.csv" //nologo @echo * Исправление завершено. @echo * Удаление временных файлов... del f:\Logs\%1.evtx del f:\Computer\%1-1.sql del "f:\Computer\%1. Запуск программ.csv" @echo * Удаление временных файлов завершено. @echo * Перемещение файлов-отчетов... move "f:\Computer\%1. Запуск программ.xls" "f:\CheckComps\%1. Запуск программ.xls" @echo * Перемещение завершено. @echo on
Примечание
Возможно, в батнике нужно будет SQLCMD.EXE заменить на «c:\Program Files\Microsoft SQL Server\100\Tools\Binn\SQLCMD.EXE», а LogParser.exe на «c:\Program Files (x86)\Log Parser 2.2\LogParser.exe» (или «c:\Program Files\Log Parser 2.2\LogParser.exe»).
Имя сервера с SQL Server’ом SQL-SRV, имя экземпляра SEC и имя базы quickly. Заменить на свои.
SELECT RecordNumber as id, eventid as eId, TimeGenerated as Tg, --resolve_sid(sid) as UserName, EXTRACT_TOKEN(Strings, 1, '|') as UserName, computername as Computer, EXTRACT_TOKEN(Strings, 4, '|') as image_id, EXTRACT_TOKEN(Strings, 5, '|') as image, EXTRACT_TOKEN(Strings, 6, '|') as name into %output_file% FROM %source% where (EventID in (4688;4689)) and ( (TO_UPPERCASE(resolve_sid(sid)) <> 'NT AUTHORITY\NETWORK SERVICE') and (TO_UPPERCASE(resolve_sid(sid)) <> 'NT AUTHORITY\SYSTEM')) and TimeGenerated >= TO_TIMESTAMP('01.01.2013 00:00:00','dd.MM.yyyy hh:mm:ss') order by recordnumber desc
'on error resume next if Wscript.Arguments.Count = 1 then gsComputerName = Wscript.Arguments(0) gsSQLtype = "1" elseif Wscript.Arguments.Count = 2 then gsComputerName = Wscript.Arguments(0) gsSQLtype = Wscript.Arguments(1) else gsComputerName = InputBox("Имя компьютера", "Введите", "") gsSQLtype = InputBox("Тип sql-запроса?" & VBNewline & "[1 - короткий, 2 - полный, 3 - оба]", "Введите", "1") end if set objFSO = CreateObject("Scripting.FileSystemObject") if gsSQLtype = "1" then fuCreateSQLFile gsComputerName, "1" elseif gsSQLtype = "2" then fuCreateSQLFile gsComputerName, "2" elseif gsSQLtype = "3" then fuCreateSQLFile gsComputerName, "1" fuCreateSQLFile gsComputerName, "2" end if function fuGetTableName(lsCompName) lsTmp = lsCompName if InStr(lsTmp, "-") then lsTmp = Replace(lsTmp, "-", "_") end if fuGetTableName = lsTmp end function sub fuCreateSQLFile(lsComputerName, lsSQLtype) if lsSQLtype = "1" then lsTemplateFilename = "f:\Computer\template-short7.sql" elseif gsSQLtype = "2" then lsTemplateFilename = "f:\Computer\template-full7.sql" end if lsLogFilename = "f:\Computer\" & lsComputerName & "-" & lsSQLtype & ".sql" lsTableName = fuGetTableName(lsComputerName) 'WScript.Echo "* Имя компьютера " & lsComputerName 'WScript.Echo "* Имя таблицы " & lsTableName 'WScript.Echo "* Имя файла sql-запроса " & lsLogFilename if not objFSO.FileExists(lsLogFilename) then set objTextFileWriteLog = objFSO.OpenTextFile(lsLogFilename, 8, True) else set objTextFileWriteLog = objFSO.OpenTextFile(lsLogFilename, 2, True) end if Set objTextFileOpen = objFSO.OpenTextFile(lsTemplateFilename, 1) do until objTextFileOpen.AtEndOfStream record = objTextFileOpen.Readline if InStr(record, "WARNING__TABLE_NAME_FOR_CHANGE") then record = Replace(record, "WARNING__TABLE_NAME_FOR_CHANGE", lsTablename) end if objTextFileWriteLog.writeLine record loop objTextFileOpen.Close objTextFileWriteLog.Close end sub
SELECT TOP (100) PERCENT Computer AS [Имя компьютера], UserName AS [Учетная запись], program AS Программа, start_time AS [Время запуска], stop_time AS [Время завершения], dbo.FU_GET_FULL_QTY_TEST(CONVERT(VARCHAR, DATEDIFF(SECOND, start_time, stop_time) / 3600)) + ':' + dbo.FU_GET_FULL_QTY_TEST(CONVERT(VARCHAR, DATEDIFF(SECOND, start_time, stop_time) % 3600 / 60)) + ':' + dbo.FU_GET_FULL_QTY_TEST(CONVERT(VARCHAR, DATEDIFF(SECOND, start_time, stop_time) % 3600 % 60)) AS Длительность FROM (SELECT r.Computer, s.UserName, r.programID, r.id AS R_ID, MIN(s.id) AS S_ID, r.program, r.Tg AS start_time, MIN(s.Tg) AS stop_time FROM (SELECT id, eId, Tg, UserName, Computer, image_id AS programID, image AS program FROM dbo.WARNING__TABLE_NAME_FOR_CHANGE WHERE (eId = 4688) AND (Tg > CONVERT(DATETIME, '2013-01-01 00:00:00.000', 102)) AND image not like '%.scr') AS r INNER JOIN (SELECT id, eId, Tg, UserName, Computer, image AS programID, name AS program FROM dbo.WARNING__TABLE_NAME_FOR_CHANGE WHERE (eId = 4689) AND name not like '%.scr') AS s ON r.programID = s.programID AND r.program = s.program AND r.UserName = s.UserName AND r.id <= s.id GROUP BY r.Computer, s.UserName, r.programID, r.id, r.program, r.Tg) AS DERIVEDTBL UNION ALL SELECT TOP (100) PERCENT Computer AS [Имя компьютера], UserName AS [Учетная запись], program AS Программа, start_time AS [Время запуска], stop_time AS [Время завершения], dbo.FU_GET_FULL_QTY_TEST(CONVERT(VARCHAR, DATEDIFF(SECOND, start_time, stop_time) / 3600)) + ':' + dbo.FU_GET_FULL_QTY_TEST(CONVERT(VARCHAR, DATEDIFF(SECOND, start_time, stop_time) % 3600 / 60)) + ':' + dbo.FU_GET_FULL_QTY_TEST(CONVERT(VARCHAR, DATEDIFF(SECOND, start_time, stop_time) % 3600 % 60)) AS Длительность FROM (SELECT r.Computer, s.UserName, r.programID, r.id AS R_ID, MIN(s.id) AS S_ID, r.program, r.Tg AS start_time, MIN(s.Tg) AS stop_time FROM (SELECT id, eId, Tg, UserName, Computer, image_id AS programID, image AS program FROM dbo.WARNING__TABLE_NAME_FOR_CHANGE WHERE (eId = 4688) AND (Tg > CONVERT(DATETIME, '2013-01-01 00:00:00.000', 102)) AND image like '%.scr') AS r INNER JOIN (SELECT id, eId, Tg, UserName, Computer, image AS programID, name AS program FROM dbo.WARNING__TABLE_NAME_FOR_CHANGE WHERE (eId = 4689) AND name like '%.scr') AS s ON r.programID = s.programID AND r.program = s.program AND r.id <= s.id GROUP BY r.Computer, s.UserName, r.programID, r.id, r.program, r.Tg) AS DERIVEDTBL2 ORDER BY 'Время запуска' DESC
'on error resume next if Wscript.Arguments.Count = 1 then gsFileName = Wscript.Arguments(0) gsFileNameRes = fuRemoveExtention(gsFileName) & ".xls" elseif Wscript.Arguments.Count = 2 then gsFileName = Wscript.Arguments(0) gsFileNameRes = Wscript.Arguments(1) else gsFileName = InputBox("Файл для обновления", "Ввод", "") gsFileNameRes = InputBox("Файл результата", "Ввод", fuRemoveExtention(gsFileName) & ".xls") end if sgSimbolSplit = ";" gsSimbolSplitFields = vbTab Set objFSO = CreateObject("Scripting.FileSystemObject") Set objTextFileOpen = objFSO.OpenTextFile(gsFileName, 1) if not objFSO.FileExists(gsFileName) then wscript.echo "Исходного файла для обновления нет, выхожу!" objTextFileOpen.Close Wscript.Quit end if if not objFSO.FileExists(gsFileNameRes) then set objTextFileWriteRes = objFSO.OpenTextFile(gsFileNameRes, 8, True) else set objTextFileWriteRes = objFSO.CreateTextFile(gsFileNameRes, True) end if do until objTextFileOpen.AtEndOfStream record = objTextFileOpen.Readline if ((InStr(record, "--------")) or (Len(record) = 0) or (InStr(record, "обработано строк")) or (InStr(record, "rows affected"))) then 'wscript.echo "пропускаю строку: '" & record & "'" else if InStr(record, sgSimbolSplit) then recordRes = Replace(record, sgSimbolSplit, gsSimbolSplitFields) else recordRes = record end if objTextFileWriteRes.writeLine recordRes end if loop objTextFileWriteRes.Close objTextFileOpen.Close WScript.Echo "Обновление завершено! Результирующий файл " & gsFileNameRes function fuRemoveExtention(lsFilename) lRes = lsFilename if InStr(lsFilename, ".") then lRes = Left(lsFilename, Len(lsFilename)-4) end if fuRemoveExtention = lRes end function
На SQL Server’е надо создать функцию FU_GET_FULL_QTY_TEST:
USE [quickly] GO /****** Object: UserDefinedFunction [dbo].[FU_GET_FULL_QTY_TEST] Script Date: 12/03/2013 13:03:43 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE FUNCTION [dbo].[FU_GET_FULL_QTY_TEST] (@short_qty varchar(255)) RETURNS varchar(255) AS BEGIN DECLARE @retMsg varchar(255) set @retMsg = @short_qty if len(@short_qty) <= 1 set @retMsg = '0' + @retMsg RETURN (@retMsg) END
Архив со скриптами можно скачать тут.
Знаю, кажется много батников и скриптов. Но достаточно один раз настроить и пользоваться потом.
И кто как делает отчёт по запуску программ на компьютерах пользователей? Поделитесь.
ссылка на оригинал статьи http://habrahabr.ru/post/202914/
Добавить комментарий