Аудит печати
07 Sep 2017Предлагаемая мной система аудита печати состоит из скрипта сбора событий службы "Диспетчер печати", базы данных MySQL и отчетов на PHP фреймворке KoolReport.
Включение ведения журнала "Диспетчера печати"
- Убедитесь, что установлена ".NET Framework 3.5" или более поздняя версия:
- добавьте ".NET Framework 3.5.1" в разделе "Возможности .NET Framework 3.5.1" с помощью опции "Добавить функции" диспетчера сервера или
- Запустите PowerShell от имени администратора:
Import-Module ServerManager
Add-WindowsFeature NET-Framework-Core
- Включите и настройте журнал событий задания печати на нужном сервере печати:
- запустите "Устройства и принтеры" -> (выделите любой принтер) -> Свойства сервера печати -> Дополнительные параметры
- "Показывать уведомления для локальных принтеров"
- "Показывать уведомления для сетевых принтеров"
- OK
- Запустите "Просмотр событий" -> Журналы приложений и служб -> Microsoft -> Windows -> PrintService
- Открыйте свойтва журнала "Работает" (Operational), включите ведение журнала
- Укажите максимальный размер журнала (KB): 65536 (по умолчанию 1028 КБ)
- ОК
- Скрипт зависит от содержимого журнала событий, выбираются события с ID 307, 805 и был протестирован только на Windows Server 2008 R2 с SP1 и Windows Server 2012.
Сбор логов и отравка в базу данных
Скрипт сбора логов заданий печати:
```powershell
<#
icinga2scripts
Version 1.0
Description: Скрипт для Icinga 2 - Сбор логов заданий печати
Pavel Satin (c) 2017
pslater.ru@gmail.com
#>
#[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$returnStateOK = 0
$returnStateWarning = 1
$returnStateCritical = 2
$returnStateUnknown = 3
#$ErrorActionPreference = "SilentlyContinue"
$connString = "Server=192.168.0.209;Uid=dbuser;Pwd=password;database=printaudit;charset=utf8"
$connString0 = "Server=192.168.0.209;Uid=dbuser;Pwd=password;database=inventory;charset=utf8"
$recordCount = 0
#Проверка аргументов
# 0 - Сервер печати
if ( $args[0] -ne $Null) {
$ComputerName = $args[0]
} else {
$ComputerName = "localhost"
}
# 1 - За сколько последних часов сделать выборку (с начала часа до конца часа периода)
if ( $args[1] -ne $Null) {
$QrHours = $args[1]
} else {
$QrHours = 1
}
#Функция выполнения MySQL запроса
function run-MySQLQuery {
Param(
[Parameter(
Mandatory = $true,
ParameterSetName = '',
ValueFromPipeline = $true)]
[string]$query,
[Parameter(
Mandatory = $true,
ParameterSetName = '',
ValueFromPipeline = $true)]
[string]$connectionString
)
try {
# load MySQL driver and create connection
Write-Verbose "Create Database Connection"
# You could also could use a direct Link to the DLL File
$mySQLDataDLL = "C:\ProgramData\icinga2\Scripts\icinga2\MySQL.Data.dll"
[void][system.reflection.Assembly]::LoadFrom($mySQLDataDLL)
#[void][System.Reflection.Assembly]::LoadWithPartialName("MySql.Data")
$connection = New-Object MySql.Data.MySqlClient.MySqlConnection
$connection.ConnectionString = $ConnectionString
Write-Verbose "Open Database Connection"
$connection.Open()
# Run MySQL Querys
Write-Verbose "Run MySQL Querys"
$command = New-Object MySql.Data.MySqlClient.MySqlCommand($query, $connection)
$dataAdapter = New-Object MySql.Data.MySqlClient.MySqlDataAdapter($command)
$dataSet = New-Object System.Data.DataSet
$recordCount = $dataAdapter.Fill($dataSet, "data")
$dataSet.Tables["data"] # | Format-Table
}
catch {
Write-Host "Could not run MySQL Query" $Error[0]
}
Finally {
Write-Verbose "Close Connection"
$connection.Close()
}
} #Конец функции Run-MySQLQuery
#########################################################################################
#Замер времени исполнения скрипта
$watch = [System.Diagnostics.Stopwatch]::StartNew()
$watch.Start() #Запуск таймера
$StartDate_hour = [datetime]::Now.AddHours(-$QrHours)
$StartDate_hour = $StartDate_hour.AddMinutes(-$StartDate_hour.Minute)
$StartDate_hour = $StartDate_hour.AddSeconds(-$StartDate_hour.Second)
$StartDate = $StartDate_hour
$EndDate = $StartDate_hour.AddHours($QrHours)
# the main print job entries are event ID 307 (use "-ErrorAction SilentlyContinue" to handle the case where no event log messages are found)
$PrintEntries = Get-WinEvent -ErrorAction SilentlyContinue -ComputerName $ComputerName -FilterHashTable @{ProviderName="Microsoft-Windows-PrintService"; StartTime=$StartDate; EndTime=$EndDate; ID=307}
# the by-job number of copies are in event ID 805 (use "-ErrorAction SilentlyContinue" to handle the case where no event log messages are found)
$PrintEntriesNumberofCopies = Get-WinEvent -ErrorAction SilentlyContinue -ComputerName $ComputerName -FilterHashTable @{ProviderName="Microsoft-Windows-PrintService"; StartTime=$StartDate; EndTime=$EndDate; ID=805}
#####
# loop to parse ID 307 event log entries
ForEach ($PrintEntry in $PrintEntries) {
# retreive the remaining fields from the event log contents
$entry = [xml]$PrintEntry.ToXml()
$ComputerTargetName = $entry.Event.UserData.DocumentPrinted.Param4
#Write-Host $ComputerTargetName
if ($ComputerTargetName.StartsWith("\\")) {
$ComputerTargetName = $ComputerTargetName.Substring(2)
$ComputerTargetName = $ComputerTargetName.ToLower()
$ComputerTargetName = $ComputerTargetName + "." + (Get-WmiObject win32_computersystem).Domain
} else {
$ComputerTargetName = $ComputerTargetName.ToLower()
$ComputerTargetName = $ComputerTargetName + "." + (Get-WmiObject win32_computersystem).Domain
}
#Write-Host $ComputerTargetName
$sQLquery = "SELECT Name, ComputerTargetId FROM tbComputerTarget WHERE Name='$ComputerTargetName'"
$rQLquery = run-MySQLQuery -connectionString $connString0 -query $sQLquery
# get date and time of printjob from TimeCreated
$StartDate_Time = $PrintEntry.TimeCreated
## retreive the remaining fields from the event log contents
#$entry = [xml]$PrintEntry.ToXml()
$PrintJobId = $entry.Event.UserData.DocumentPrinted.Param1
$DocumentName = $entry.Event.UserData.DocumentPrinted.Param2
$UserName = $entry.Event.UserData.DocumentPrinted.Param3
$ClientPCName = $rQLquery.ComputerTargetId
$PrinterName = $entry.Event.UserData.DocumentPrinted.Param5
$PrinterPort = $entry.Event.UserData.DocumentPrinted.Param6
$PrintSizeBytes = $entry.Event.UserData.DocumentPrinted.Param7
$PrintPagesOneCopy = $entry.Event.UserData.DocumentPrinted.Param8
# get the user's full name from Active Directory
if ($UserName -gt "") {
$DirectorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$LdapFilter = "(&(objectClass=user)(samAccountName=${UserName}))"
$DirectorySearcher.Filter = $LdapFilter
$UserEntry = [adsi]"$($DirectorySearcher.FindOne().Path)"
$ADName = $UserEntry.displayName
}
# get the print job number of copies by correlating with event ID 805 records
# the ID 805 record always is logged immediately before (that is, earlier in time) its related 307 record
# the print job ID number restarts after a certain interval, so we need to check both for a matching job ID and a very close logging time (within 5 seconds) to its related event ID 307 record
$PrintEntryNumberofCopies = $PrintEntriesNumberofCopies | Where-Object {$_.TimeCreated -le $StartDate_Time -and $_.TimeCreated -ge ($StartDate_Time - (New-Timespan -second 5)) -and $_.Message -eq "Подготовка задания $PrintJobId."}
# check for found data and extract the number of copies if a matching event log ID 805 record was found
if ($PrintEntryNumberofCopies) {
# retrieve the remaining fields from the event log contents
$entry = [xml]$PrintEntryNumberofCopies.ToXml()
$NumberOfCopies = $entry.Event.UserData.RenderJobDiag.Copies
# some flawed printer drivers always report 0 copies for every print job; output a warning so this can be investigated further and set copies to be 1 in this case as a guess of what the actual number of copies was
if ($NumberOfCopies -eq 0) {
$NumberOfCopies = 1
$Message = "WARNING: Printer $PrinterName recorded that print job ID $PrintJobId was printed with 0 copies. This is probably a bug in the print driver. Upgrading or otherwise changing the print driver may help. Guessing that 1 copy of the job was printed and continuing on..."
Write-Host $Message
#Write-Output $Message | Out-File -FilePath $OutputFilenameByPrintJob -Encoding UTF8 -Append
}
}
# if no matching event log ID 805 record was found, exit with errorlevel 1 -- a matching record should always be present
else {
$Message = "ERROR: There were no print job event ID 805 entries found in the specified time range from $StartDate to $EndDate that matched print job id $PrintJobId. A matching record should always be present. Exiting script."
Write-Host $Message
#Write-Output $Message | Out-File -FilePath $OutputFilenameByPrintJob -Encoding UTF8 -Append
Exit 1
}
# calculate the total number of pages for the whole print job
$TotalPages = [int]$PrintPagesOneCopy * [int]$NumberOfCopies
$PrintEntryDateTime = ($PrintEntry.TimeCreated).ToString("yyyy-MM-dd HH:mm:ss")
$sQLquery = "INSERT INTO tbPrintJob ( PrintJobDateTime, PrintJobId, UserLogin, UserName, ComputerTargetId, PrinterName, PrinterPort, DocumentName, PrintJobSize, PageCountForOneCopy, NumberOfCopies, TotalPages ) VALUES ( '$PrintEntryDateTime', '$PrintJobId', '$UserName', '$ADName', '$ClientPCName', '$PrinterName', '$PrinterPort', '$DocumentName', '$PrintSizeBytes', '$PrintPagesOneCopy', '$NumberOfCopies', '$TotalPages' )"
#Write-Host $sQLquery
$rQLquery = run-MySQLQuery -connectionString $connString -query $sQLquery
$recordCount++
}
Write-Host "Inserted $recordCount entries in the database"
Write-Host "Request completed from $StartDate to $EndDate."
$watch.Stop() #Остановка таймера
Write-Host $watch.Elapsed #Время выполнения
Write-Host (Get-Date)
#[System.Environment]::Exit($returnStateOK)
```
Скрипт создания таблицы MySQL:
```sql
SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
DROP TABLE IF EXISTS `tbPrintJob`;
CREATE TABLE `tbPrintJob` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`PrintJobDateTime` datetime NOT NULL,
`PrintJobId` int(5) NOT NULL,
`UserLogin` varchar(256) NOT NULL,
`UserName` varchar(256) NOT NULL,
`ComputerTargetId` varchar(256) NOT NULL,
`PrinterName` varchar(256) NOT NULL,
`PrinterPort` varchar(256) NOT NULL,
`DocumentName` varchar(256) NOT NULL,
`PrintJobSize` int(20) NOT NULL,
`PageCountForOneCopy` int(5) NOT NULL,
`NumberOfCopies` int(5) NOT NULL,
`TotalPages` int(5) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
В MySQL 5.7.8 необходимо добавить права SELECT пользователю на следующие таблицы:
`performance_schema`.`session_status`
`performance_schema`.`session_variables`
`performance_schema`.`global_variables`
Мониторинг
Можно конечно просто повесить скрипт в "Планировщик задач" Windows, но мы не ищем легких путей, сделаем через систему мониторинга Icinga2. Каждому серверу печати необходимо добавить сервис:
apply Service "print-job-audit" {
max_check_attempts = 2
check_interval = 60m
retry_interval = 3m
enable_perfdata = true
assign where host.name == "ts3.mkucou.local"
enable_active_checks = false
check_command = "powershell"
vars.ps_command = "c:\\ProgramData\\icinga2\\Scripts\\icinga2\\check_print_audit.ps1"
vars.ps_args = [ "localhost", "1" ]
command_endpoint = host.vars.client_endpoint
}
Скрипт для cron`а:
#!/bin/sh
### Check print-job-audit ###
/bin/echo "[`date +%s`] SCHEDULE_FORCED_SVC_CHECK;ts3.mkucou.local;print-job-audit;`date +%s`" >> /var/run/icinga2/cmd/icinga2.cmd
exit 0
Задание в cron`е:
### Check print-job-audit ###
5 * * * * /etc/icinga2/scripts/cron-print-job-audit.sh
Дополнение (13.09.2018). В итоге все таки повешан скрипт в "Планировщик задач", но это скрипт отправляет результат по мимо базы данных еще и в пассивный сервис Icinga2. Наверное вообще не стоило упоминать о взаимодействии с системой мониторинга.
Теперь сервис Icinga2 выглядит так:
apply Service "print-job-audit" {
#display_name = "PrintJobAccountingReports"
check_command = "dummy"
enable_notifications = true
enable_active_checks = false
enable_passive_checks = true
enable_flapping = false
volatile = true
max_check_attempts = true
check_interval = 60m
enable_perfdata = true
vars.sla = "24x7"
vars.dummy_state = 2
vars.dummy_text = "No passive check result received."
action_url = "https://ts3.mkucou.local/printaudit2/"
# assign where host.vars.print_job_audit
assign where host.name == "ts3.mkucou.local"
}
Отчеты
Теперь осталось формировать отчеты из базы данных. Можно воспользоваться PHP Framework KoolReport, но об этом в другой раз.