Загрузка данных
#requires -Version 5.1
<#
.SYNOPSIS
Отправляет email при получении Event ID 4741 в журнал ForwardedEvents на WEC.
.DESCRIPTION
Совместимо с Windows PowerShell 5.1 / .NET Framework.
- Читает локальный ForwardedEvents на WEC.
- Обрабатывает только Event ID 4741.
- После успешной отправки письма сохраняет EventBookmark.
- Если SMTP недоступен, bookmark не сдвигается и событие повторится на следующем запуске.
- Если checkpoint сохранить не удалось, скрипт прекращает обработку, чтобы не перескочить событие.
- Ведет подробный Transcript.
- Не допускает одновременный запуск двух экземпляров.
#>
Set-StrictMode -Version 2.0
$ErrorActionPreference = 'Stop'
# ============================================================
# CONFIGURATION
# ============================================================
$SmtpServer = 'mail.remind.ru'
$SmtpPort = 25
$SmtpEnableSsl = $false
$SmtpTimeoutMs = 15000
$MailFrom = 'ad-computer-report@remind.ru'
$MailTo = @(
'Pavel.semenov@remind.ru',
'Vladimir.zatulov@remind.ru',
'Stanislav.nikonov@remind.ru',
'imarchenko@remind.ru'
)
$EventLogName = 'ForwardedEvents'
$EventXPath = '*[System[(EventID=4741)]]'
# false = при первом запуске существующие 4741 не отправлять,
# а взять последнее существующее событие как baseline.
# true = при первом запуске обработать всю имеющуюся историю 4741.
$ProcessExistingEventsOnFirstRun = $false
$BaseDirectory = 'C:\ProgramData\AD-Computer-Report'
$LogDirectory = Join-Path $BaseDirectory 'Logs'
$StateFile = Join-Path $BaseDirectory 'State.json'
$StateBackupFile = Join-Path $BaseDirectory 'State.json.bak'
$LockFile = Join-Path $BaseDirectory 'run.lock'
$LogRetentionDays = 30
$StateVersion = 2
# ============================================================
# INITIAL DIRECTORIES
# ============================================================
foreach ($Directory in @($BaseDirectory, $LogDirectory)) {
if (-not (Test-Path -LiteralPath $Directory)) {
New-Item -Path $Directory -ItemType Directory -Force | Out-Null
}
}
# ============================================================
# SINGLE INSTANCE LOCK
# ============================================================
$LockStream = $null
try {
$LockStream = [System.IO.File]::Open(
$LockFile,
[System.IO.FileMode]::OpenOrCreate,
[System.IO.FileAccess]::ReadWrite,
[System.IO.FileShare]::None
)
$LockText = "PID=$PID`r`nStarted=$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`r`nComputer=$env:COMPUTERNAME`r`n"
$LockBytes = [System.Text.Encoding]::UTF8.GetBytes($LockText)
$LockStream.SetLength(0)
$LockStream.Write($LockBytes, 0, $LockBytes.Length)
$LockStream.Flush()
}
catch [System.IO.IOException] {
Write-Host 'Another instance of AD-Computer-Report is already running.'
exit 0
}
# ============================================================
# TRANSCRIPT
# ============================================================
$TranscriptFile = Join-Path $LogDirectory ("AD-Computer-Report_{0:yyyy-MM-dd_HH-mm-ss}.log" -f (Get-Date))
$TranscriptStarted = $false
$ExitCode = 0
# ============================================================
# FUNCTIONS
# ============================================================
function Write-Log {
param (
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[string]$Message,
[ValidateSet('INFO', 'SUCCESS', 'WARNING', 'ERROR', 'DEBUG')]
[string]$Level = 'INFO'
)
if ([string]::IsNullOrEmpty($Message)) {
Write-Host ''
return
}
$Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff'
Write-Host "[$Timestamp][$Level] $Message"
}
function Write-ExceptionLog {
param (
[Parameter(Mandatory = $true)]
[string]$Context,
[Parameter(Mandatory = $true)]
$ErrorRecord
)
Write-Log -Level 'ERROR' -Message $Context
Write-Log -Level 'ERROR' -Message "Exception: $($ErrorRecord.Exception.GetType().FullName)"
Write-Log -Level 'ERROR' -Message "Message: $($ErrorRecord.Exception.Message)"
if ($ErrorRecord.FullyQualifiedErrorId) {
Write-Log -Level 'DEBUG' -Message "FullyQualifiedErrorId: $($ErrorRecord.FullyQualifiedErrorId)"
}
if ($ErrorRecord.ScriptStackTrace) {
Write-Log -Level 'DEBUG' -Message "StackTrace: $($ErrorRecord.ScriptStackTrace)"
}
}
function Convert-ToHtml {
param (
[AllowNull()]
[object]$Value
)
if ($null -eq $Value) {
return ''
}
return [System.Net.WebUtility]::HtmlEncode([string]$Value)
}
function Get-StatePropertyValue {
param (
[AllowNull()]
$State,
[Parameter(Mandatory = $true)]
[string]$Name
)
if ($null -eq $State) {
return $null
}
$Property = $State.PSObject.Properties[$Name]
if ($null -eq $Property) {
return $null
}
return $Property.Value
}
function New-ForwardedEventQuery {
param (
[bool]$Reverse = $false
)
$Query = New-Object -TypeName 'System.Diagnostics.Eventing.Reader.EventLogQuery' -ArgumentList @(
$EventLogName,
[System.Diagnostics.Eventing.Reader.PathType]::LogName,
$EventXPath
)
$Query.ReverseDirection = $Reverse
return $Query
}
function New-EventLogReader {
param (
[Parameter(Mandatory = $true)]
[System.Diagnostics.Eventing.Reader.EventLogQuery]$Query,
[AllowNull()]
[System.Diagnostics.Eventing.Reader.EventBookmark]$Bookmark
)
if ($null -eq $Bookmark) {
return (New-Object -TypeName 'System.Diagnostics.Eventing.Reader.EventLogReader' -ArgumentList @($Query))
}
return (New-Object -TypeName 'System.Diagnostics.Eventing.Reader.EventLogReader' -ArgumentList @($Query, $Bookmark))
}
function Get-EventBookmarkString {
param (
[Parameter(Mandatory = $true)]
[System.Diagnostics.Eventing.Reader.EventBookmark]$Bookmark
)
# В новых реализациях .NET BookmarkXml может быть публичным.
$PublicProperty = $Bookmark.PSObject.Properties['BookmarkXml']
if ($null -ne $PublicProperty) {
$Value = [string]$PublicProperty.Value
if (-not [string]::IsNullOrWhiteSpace($Value)) {
return $Value
}
}
# Windows PowerShell 5.1 / .NET Framework использует внутренний BookmarkText.
$Flags = [System.Reflection.BindingFlags]::Instance -bor `
[System.Reflection.BindingFlags]::Public -bor `
[System.Reflection.BindingFlags]::NonPublic
$BookmarkType = $Bookmark.GetType()
$InternalProperty = $BookmarkType.GetProperty('BookmarkText', $Flags)
if ($null -ne $InternalProperty) {
$Value = [string]$InternalProperty.GetValue($Bookmark, $null)
if (-not [string]::IsNullOrWhiteSpace($Value)) {
return $Value
}
}
# Дополнительный fallback на случай отличия реализации .NET.
foreach ($FieldName in @('bookmarkText', '_bookmarkText')) {
$InternalField = $BookmarkType.GetField($FieldName, $Flags)
if ($null -ne $InternalField) {
$Value = [string]$InternalField.GetValue($Bookmark)
if (-not [string]::IsNullOrWhiteSpace($Value)) {
return $Value
}
}
}
throw 'Unable to extract bookmark XML/text from EventBookmark.'
}
function New-EventBookmarkFromString {
param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$BookmarkString
)
# В .NET Framework конструктор EventBookmark(String) существует.
# Используем reflection, чтобы одинаково работать в разных реализациях.
$BookmarkType = [System.Diagnostics.Eventing.Reader.EventBookmark]
$Flags = [System.Reflection.BindingFlags]::Instance -bor `
[System.Reflection.BindingFlags]::Public -bor `
[System.Reflection.BindingFlags]::NonPublic
$Constructor = $BookmarkType.GetConstructor(
$Flags,
$null,
[Type[]]@([string]),
$null
)
if ($null -eq $Constructor) {
throw 'EventBookmark(String) constructor was not found.'
}
$Bookmark = $Constructor.Invoke([object[]]@($BookmarkString))
if ($null -eq $Bookmark) {
throw 'EventBookmark could not be reconstructed from stored state.'
}
return $Bookmark
}
function Get-EventInformation {
param (
[Parameter(Mandatory = $true)]
[System.Diagnostics.Eventing.Reader.EventRecord]$Event
)
[xml]$Xml = $Event.ToXml()
$EventData = @{}
foreach ($Item in @($Xml.Event.EventData.Data)) {
if ($null -eq $Item) {
continue
}
$Name = [string]$Item.Name
if (-not [string]::IsNullOrWhiteSpace($Name)) {
$EventData[$Name] = [string]$Item.'#text'
}
}
$ComputerName = [string]$EventData['TargetUserName']
if ([string]::IsNullOrWhiteSpace($ComputerName)) {
$ComputerName = 'UNKNOWN'
}
$CreatedByDomain = [string]$EventData['SubjectDomainName']
$CreatedByUser = [string]$EventData['SubjectUserName']
if (-not [string]::IsNullOrWhiteSpace($CreatedByDomain) -and
-not [string]::IsNullOrWhiteSpace($CreatedByUser)) {
$CreatedBy = "$CreatedByDomain\$CreatedByUser"
}
elseif (-not [string]::IsNullOrWhiteSpace($CreatedByUser)) {
$CreatedBy = $CreatedByUser
}
else {
$CreatedBy = ''
}
[PSCustomObject]@{
ComputerName = $ComputerName
DNSHostName = [string]$EventData['DnsHostName']
TargetDomain = [string]$EventData['TargetDomainName']
TargetSID = [string]$EventData['TargetSid']
CreatedBy = $CreatedBy
CreatedBySID = [string]$EventData['SubjectUserSid']
SourceComputer = [string]$Xml.Event.System.Computer
SourceRecordId = [string]$Xml.Event.System.EventRecordID
EventTime = $Event.TimeCreated
SamAccountName = [string]$EventData['SamAccountName']
ServicePrincipalNames = [string]$EventData['ServicePrincipalNames']
}
}
function Save-State {
param (
[AllowNull()]
[string]$BookmarkXml,
[AllowNull()]
$EventInfo
)
$LastSourceComputer = $null
$LastSourceRecordId = $null
$LastComputerName = $null
$LastEventTime = $null
if ($null -ne $EventInfo) {
$LastSourceComputer = $EventInfo.SourceComputer
$LastSourceRecordId = $EventInfo.SourceRecordId
$LastComputerName = $EventInfo.ComputerName
if ($null -ne $EventInfo.EventTime) {
$LastEventTime = $EventInfo.EventTime.ToString('o')
}
}
$State = [ordered]@{
Version = $StateVersion
Initialized = $true
BookmarkXml = $BookmarkXml
UpdatedAt = (Get-Date).ToString('o')
LastSourceComputer = $LastSourceComputer
LastSourceRecordId = $LastSourceRecordId
LastComputerName = $LastComputerName
LastEventTime = $LastEventTime
}
$Json = $State | ConvertTo-Json -Depth 5
$TempFile = "$StateFile.tmp"
$Utf8NoBom = New-Object -TypeName 'System.Text.UTF8Encoding' -ArgumentList @($false)
try {
[System.IO.File]::WriteAllText($TempFile, $Json, $Utf8NoBom)
if (Test-Path -LiteralPath $StateFile) {
if (Test-Path -LiteralPath $StateBackupFile) {
Remove-Item -LiteralPath $StateBackupFile -Force -ErrorAction SilentlyContinue
}
[System.IO.File]::Replace($TempFile, $StateFile, $StateBackupFile, $true)
}
else {
[System.IO.File]::Move($TempFile, $StateFile)
}
Write-Log -Level 'DEBUG' -Message "State successfully saved: $StateFile"
}
catch {
if (Test-Path -LiteralPath $TempFile) {
Remove-Item -LiteralPath $TempFile -Force -ErrorAction SilentlyContinue
}
throw
}
}
function Load-State {
if (-not (Test-Path -LiteralPath $StateFile)) {
Write-Log -Message 'State file does not exist. This is the first run.'
return $null
}
try {
$RawState = Get-Content -LiteralPath $StateFile -Raw -ErrorAction Stop
$State = $RawState | ConvertFrom-Json -ErrorAction Stop
Write-Log -Level 'SUCCESS' -Message "State successfully loaded: $StateFile"
return $State
}
catch {
Write-ExceptionLog -Context 'Primary state file cannot be read.' -ErrorRecord $_
if (Test-Path -LiteralPath $StateBackupFile) {
Write-Log -Level 'WARNING' -Message "Attempting to load backup state: $StateBackupFile"
try {
$RawState = Get-Content -LiteralPath $StateBackupFile -Raw -ErrorAction Stop
$State = $RawState | ConvertFrom-Json -ErrorAction Stop
Write-Log -Level 'SUCCESS' -Message 'Backup state successfully loaded.'
return $State
}
catch {
Write-ExceptionLog -Context 'Backup state cannot be read.' -ErrorRecord $_
}
}
throw 'No valid state file is available.'
}
}
function Send-NewComputerMail {
param (
[Parameter(Mandatory = $true)]
$EventInfo
)
$DisplayComputerName = ([string]$EventInfo.ComputerName).TrimEnd('$')
if ([string]::IsNullOrWhiteSpace($DisplayComputerName)) {
$DisplayComputerName = 'UNKNOWN'
}
$Subject = "AD: новый компьютер $DisplayComputerName"
$EventTimeText = ''
if ($null -ne $EventInfo.EventTime) {
$EventTimeText = $EventInfo.EventTime.ToString('dd.MM.yyyy HH:mm:ss')
}
$ComputerHtml = Convert-ToHtml $DisplayComputerName
$SamHtml = Convert-ToHtml $EventInfo.ComputerName
$DnsHtml = Convert-ToHtml $EventInfo.DNSHostName
$DomainHtml = Convert-ToHtml $EventInfo.TargetDomain
$SidHtml = Convert-ToHtml $EventInfo.TargetSID
$CreatedByHtml = Convert-ToHtml $EventInfo.CreatedBy
$CreatedBySidHtml = Convert-ToHtml $EventInfo.CreatedBySID
$SourceHtml = Convert-ToHtml $EventInfo.SourceComputer
$RecordHtml = Convert-ToHtml $EventInfo.SourceRecordId
$TimeHtml = Convert-ToHtml $EventTimeText
$WecHtml = Convert-ToHtml $env:COMPUTERNAME
$Body = @"
<html>
<head>
<meta charset="utf-8">
</head>
<body style="font-family:Segoe UI,Arial,sans-serif;font-size:14px;color:#222222;">
<h2>В Active Directory создан новый компьютер</h2>
<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;">
<tr><td><b>Компьютер:</b></td><td>$ComputerHtml</td></tr>
<tr><td><b>SAM Account:</b></td><td>$SamHtml</td></tr>
<tr><td><b>DNS:</b></td><td>$DnsHtml</td></tr>
<tr><td><b>Домен:</b></td><td>$DomainHtml</td></tr>
<tr><td><b>Computer SID:</b></td><td>$SidHtml</td></tr>
<tr><td><b>Создал:</b></td><td>$CreatedByHtml</td></tr>
<tr><td><b>SID инициатора:</b></td><td>$CreatedBySidHtml</td></tr>
<tr><td><b>Контроллер домена:</b></td><td>$SourceHtml</td></tr>
<tr><td><b>Время события:</b></td><td>$TimeHtml</td></tr>
<tr><td><b>Event ID:</b></td><td>4741</td></tr>
<tr><td><b>EventRecordID на DC:</b></td><td>$RecordHtml</td></tr>
<tr><td><b>WEC:</b></td><td>$WecHtml</td></tr>
</table>
<br>
<div style="color:#666666;font-size:12px;">Автоматическое уведомление системы мониторинга Active Directory.</div>
</body>
</html>
"@
$MailMessage = New-Object -TypeName 'System.Net.Mail.MailMessage'
$Smtp = $null
try {
$MailMessage.From = New-Object -TypeName 'System.Net.Mail.MailAddress' -ArgumentList @($MailFrom)
foreach ($Recipient in $MailTo) {
[void]$MailMessage.To.Add((New-Object -TypeName 'System.Net.Mail.MailAddress' -ArgumentList @($Recipient)))
}
$MailMessage.Subject = $Subject
$MailMessage.Body = $Body
$MailMessage.IsBodyHtml = $true
$MailMessage.SubjectEncoding = [System.Text.Encoding]::UTF8
$MailMessage.BodyEncoding = [System.Text.Encoding]::UTF8
$Smtp = New-Object -TypeName 'System.Net.Mail.SmtpClient' -ArgumentList @($SmtpServer, $SmtpPort)
$Smtp.EnableSsl = $SmtpEnableSsl
$Smtp.UseDefaultCredentials = $false
$Smtp.DeliveryMethod = [System.Net.Mail.SmtpDeliveryMethod]::Network
$Smtp.Timeout = $SmtpTimeoutMs
Write-Log -Message "SMTP: sending '$Subject' via $SmtpServer`:$SmtpPort"
$Smtp.Send($MailMessage)
Write-Log -Level 'SUCCESS' -Message "Email successfully sent for computer '$DisplayComputerName'."
}
finally {
if ($null -ne $Smtp) {
$Smtp.Dispose()
}
if ($null -ne $MailMessage) {
$MailMessage.Dispose()
}
}
}
function Initialize-BookmarkFromCurrentEnd {
Write-Log -Message 'Looking for the newest existing Event ID 4741 to create baseline...'
$Query = New-ForwardedEventQuery -Reverse $true
$Reader = $null
$LastEvent = $null
try {
$Reader = New-EventLogReader -Query $Query -Bookmark $null
$LastEvent = $Reader.ReadEvent()
if ($null -ne $LastEvent) {
$Info = Get-EventInformation -Event $LastEvent
$BookmarkString = Get-EventBookmarkString -Bookmark $LastEvent.Bookmark
# Проверяем round-trip ДО сохранения state.
$TestBookmark = New-EventBookmarkFromString -BookmarkString $BookmarkString
if ($null -eq $TestBookmark) {
throw 'Bookmark round-trip validation failed.'
}
Write-Log -Level 'WARNING' -Message "Baseline event: Computer='$($Info.ComputerName)', DC='$($Info.SourceComputer)', RecordID='$($Info.SourceRecordId)'."
Save-State -BookmarkXml $BookmarkString -EventInfo $Info
Write-Log -Level 'SUCCESS' -Message 'Initial bookmark created. Existing events will NOT be emailed.'
}
else {
Save-State -BookmarkXml $null -EventInfo $null
Write-Log -Level 'SUCCESS' -Message 'No Event ID 4741 currently exists. Empty baseline state created.'
}
}
finally {
if ($null -ne $LastEvent) {
$LastEvent.Dispose()
}
if ($null -ne $Reader) {
$Reader.Dispose()
}
}
}
function Test-BookmarkSerialization {
Write-Log -Message 'Testing EventBookmark serialization compatibility...'
$Query = New-ForwardedEventQuery -Reverse $true
$Reader = $null
$Event = $null
try {
$Reader = New-EventLogReader -Query $Query -Bookmark $null
$Event = $Reader.ReadEvent()
if ($null -eq $Event) {
Write-Log -Level 'WARNING' -Message 'No 4741 event exists yet; bookmark self-test cannot be performed now.'
return
}
$BookmarkString = Get-EventBookmarkString -Bookmark $Event.Bookmark
$RestoredBookmark = New-EventBookmarkFromString -BookmarkString $BookmarkString
if ($null -eq $RestoredBookmark) {
throw 'Restored bookmark is null.'
}
Write-Log -Level 'SUCCESS' -Message 'EventBookmark serialization self-test passed.'
}
finally {
if ($null -ne $Event) {
$Event.Dispose()
}
if ($null -ne $Reader) {
$Reader.Dispose()
}
}
}
# ============================================================
# MAIN
# ============================================================
$RunStarted = Get-Date
$EventsProcessed = 0
$EmailErrors = 0
$StateErrors = 0
try {
Start-Transcript -Path $TranscriptFile -Force -ErrorAction Stop
$TranscriptStarted = $true
Write-Log -Message '============================================================'
Write-Log -Message 'AD Computer Report started'
Write-Log -Message '============================================================'
Write-Log -Message "WEC server : $env:COMPUTERNAME"
Write-Log -Message "User : $env:USERDOMAIN\$env:USERNAME"
Write-Log -Message "PowerShell : $($PSVersionTable.PSVersion)"
Write-Log -Message "Event log : $EventLogName"
Write-Log -Message 'Event ID : 4741'
Write-Log -Message "SMTP : $SmtpServer`:$SmtpPort"
Write-Log -Message "From : $MailFrom"
Write-Log -Message "Recipients : $($MailTo -join ', ')"
Write-Log -Message "Transcript : $TranscriptFile"
# --------------------------------------------------------
# Verify ForwardedEvents
# --------------------------------------------------------
Write-Log -Message 'Checking ForwardedEvents log...'
$LogInfo = Get-WinEvent -ListLog $EventLogName -ErrorAction Stop
Write-Log -Level 'SUCCESS' -Message "ForwardedEvents available. RecordCount=$($LogInfo.RecordCount)"
# Проверяем bookmark-механику до того, как отправлять письма.
Test-BookmarkSerialization
# --------------------------------------------------------
# Load state / migrate old state
# --------------------------------------------------------
$State = Load-State
$StateVersionFromFile = Get-StatePropertyValue -State $State -Name 'Version'
if ($null -ne $State -and ([int]$StateVersionFromFile -lt $StateVersion)) {
Write-Log -Level 'WARNING' -Message "Old/broken state format detected (Version=$StateVersionFromFile)."
Write-Log -Level 'WARNING' -Message 'Creating a new baseline at the newest current 4741 to prevent duplicate mail from the previous script version.'
Initialize-BookmarkFromCurrentEnd
$State = Load-State
}
# --------------------------------------------------------
# First run
# --------------------------------------------------------
if ($null -eq $State -and -not $ProcessExistingEventsOnFirstRun) {
Write-Log -Level 'WARNING' -Message 'First run detected.'
Initialize-BookmarkFromCurrentEnd
Write-Log -Message 'Initialization completed. No existing events were emailed.'
}
else {
# ----------------------------------------------------
# Build query and restore bookmark
# ----------------------------------------------------
$Query = New-ForwardedEventQuery -Reverse $false
$Bookmark = $null
$StoredBookmarkXml = Get-StatePropertyValue -State $State -Name 'BookmarkXml'
if (-not [string]::IsNullOrWhiteSpace([string]$StoredBookmarkXml)) {
try {
$Bookmark = New-EventBookmarkFromString -BookmarkString ([string]$StoredBookmarkXml)
Write-Log -Level 'SUCCESS' -Message 'Saved EventBookmark successfully restored.'
}
catch {
Write-ExceptionLog -Context 'Stored bookmark cannot be restored. ForwardedEvents may have been cleared/recreated or state is invalid.' -ErrorRecord $_
Write-Log -Level 'WARNING' -Message 'Creating a new baseline at the newest current 4741. Existing events will not be re-sent.'
Initialize-BookmarkFromCurrentEnd
$Bookmark = $null
$State = Load-State
$StoredBookmarkXml = Get-StatePropertyValue -State $State -Name 'BookmarkXml'
if (-not [string]::IsNullOrWhiteSpace([string]$StoredBookmarkXml)) {
$Bookmark = New-EventBookmarkFromString -BookmarkString ([string]$StoredBookmarkXml)
}
}
}
else {
Write-Log -Message 'No saved bookmark. Reading matching events from the current ForwardedEvents log.'
}
# ----------------------------------------------------
# Open reader and process events
# ----------------------------------------------------
$Reader = $null
try {
try {
$Reader = New-EventLogReader -Query $Query -Bookmark $Bookmark
}
catch {
if ($null -eq $Bookmark) {
throw
}
Write-ExceptionLog -Context 'EventLogReader cannot open ForwardedEvents from the saved bookmark.' -ErrorRecord $_
Write-Log -Level 'WARNING' -Message 'The log may have been cleared/recreated. Creating a new baseline at the newest current 4741.'
Initialize-BookmarkFromCurrentEnd
$State = Load-State
$StoredBookmarkXml = Get-StatePropertyValue -State $State -Name 'BookmarkXml'
$Bookmark = $null
if (-not [string]::IsNullOrWhiteSpace([string]$StoredBookmarkXml)) {
$Bookmark = New-EventBookmarkFromString -BookmarkString ([string]$StoredBookmarkXml)
}
$Query = New-ForwardedEventQuery -Reverse $false
$Reader = New-EventLogReader -Query $Query -Bookmark $Bookmark
}
while ($true) {
$Event = $Reader.ReadEvent()
if ($null -eq $Event) {
break
}
try {
$Info = Get-EventInformation -Event $Event
Write-Log -Message ''
Write-Log -Message '------------------------------------------------------------'
Write-Log -Message 'Processing Event ID 4741'
Write-Log -Message "Computer : $($Info.ComputerName)"
Write-Log -Message "DNS : $($Info.DNSHostName)"
Write-Log -Message "Created by : $($Info.CreatedBy)"
Write-Log -Message "Source DC : $($Info.SourceComputer)"
Write-Log -Message "Source RecordID: $($Info.SourceRecordId)"
Write-Log -Message "Event time : $($Info.EventTime)"
# ВАЖНО: сначала убеждаемся, что bookmark этого события можно сериализовать.
# Это не позволяет повторить предыдущую проблему: письмо ушло, а checkpoint сохранить невозможно.
try {
$BookmarkString = Get-EventBookmarkString -Bookmark $Event.Bookmark
$BookmarkRoundTrip = New-EventBookmarkFromString -BookmarkString $BookmarkString
if ($null -eq $BookmarkRoundTrip) {
throw 'Bookmark validation returned null.'
}
}
catch {
$StateErrors++
$ExitCode = 3
Write-ExceptionLog -Context "Bookmark validation FAILED for '$($Info.ComputerName)'. Email will NOT be sent." -ErrorRecord $_
Write-Log -Level 'WARNING' -Message 'Processing stops so no event can be skipped.'
break
}
# SMTP
try {
Send-NewComputerMail -EventInfo $Info
}
catch {
$EmailErrors++
$ExitCode = 2
Write-ExceptionLog -Context "EMAIL SEND FAILED for '$($Info.ComputerName)'." -ErrorRecord $_
Write-Log -Level 'WARNING' -Message 'Bookmark will NOT be advanced. The event will be retried on the next run.'
break
}
# Checkpoint only after successful SMTP.
try {
Save-State -BookmarkXml $BookmarkString -EventInfo $Info
$EventsProcessed++
Write-Log -Level 'SUCCESS' -Message 'Event successfully processed and checkpoint saved.'
}
catch {
$StateErrors++
$ExitCode = 3
Write-ExceptionLog -Context 'Email was sent, but state could NOT be saved.' -ErrorRecord $_
Write-Log -Level 'WARNING' -Message 'This event may be emailed again on the next run.'
Write-Log -Level 'WARNING' -Message 'Processing stops so later events cannot be skipped.'
break
}
}
finally {
if ($null -ne $Event) {
$Event.Dispose()
}
}
}
}
finally {
if ($null -ne $Reader) {
$Reader.Dispose()
}
}
}
# --------------------------------------------------------
# Cleanup old transcripts
# --------------------------------------------------------
try {
$CleanupBefore = (Get-Date).AddDays(-$LogRetentionDays)
$OldLogs = @(
Get-ChildItem -LiteralPath $LogDirectory -Filter '*.log' -File -ErrorAction Stop |
Where-Object { $_.LastWriteTime -lt $CleanupBefore }
)
foreach ($OldLog in $OldLogs) {
Write-Log -Level 'DEBUG' -Message "Removing old transcript: $($OldLog.Name)"
Remove-Item -LiteralPath $OldLog.FullName -Force -ErrorAction Stop
}
}
catch {
Write-ExceptionLog -Context 'Transcript cleanup failed.' -ErrorRecord $_
}
# --------------------------------------------------------
# Summary
# --------------------------------------------------------
$Duration = (Get-Date) - $RunStarted
Write-Log -Message ''
Write-Log -Message '============================================================'
Write-Log -Message 'RUN SUMMARY'
Write-Log -Message '============================================================'
Write-Log -Message "Events processed : $EventsProcessed"
Write-Log -Message "Email errors : $EmailErrors"
Write-Log -Message "State errors : $StateErrors"
Write-Log -Message "Exit code : $ExitCode"
Write-Log -Message "Duration : $($Duration.ToString())"
Write-Log -Message '============================================================'
}
catch {
$ExitCode = 1
Write-ExceptionLog -Context 'FATAL SCRIPT ERROR' -ErrorRecord $_
}
finally {
if ($TranscriptStarted) {
Write-Log -Message 'AD Computer Report finished.'
Stop-Transcript | Out-Null
}
if ($null -ne $LockStream) {
$LockStream.Dispose()
}
}
exit $ExitCode