598 lines
24 KiB
PowerShell
598 lines
24 KiB
PowerShell
#requires -Version 5.1
|
|
<#
|
|
______ __ __ __
|
|
/ ____/_ __/ /_ ___ _____/ /____ / /__
|
|
/ / / / / / __ \/ _ \/ ___/ __/ _ \/ //_/
|
|
/ /___/ /_/ / /_/ / __/ / / /_/ __/ ,<
|
|
\____/\__, /_.___/\___/_/ \__/\___/_/|_|
|
|
/____/
|
|
|
|
.SYNOPSIS
|
|
CYBERTEK Syncro monitor for Windows Security event log auditing.
|
|
|
|
.DESCRIPTION
|
|
Monitors high-value Windows Security audit events from
|
|
Microsoft-Windows-Security-Auditing and creates Syncro alerts when new
|
|
matching events are found. The script stores the last processed RecordId
|
|
locally so recurring scheduled runs do not keep alerting on the same event.
|
|
|
|
.NOTES
|
|
Script Name : event_log_monitor.ps1
|
|
Author : Cybertek
|
|
Maintainer : Cybertek / Codex-assisted implementation
|
|
Version : 2026-03-14
|
|
Run Context : SYSTEM via Syncro RMM
|
|
Purpose : Alert on new high-value Windows Security events with low noise.
|
|
#>
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
Import-Module $env:SyncroModule
|
|
|
|
$Config = [ordered]@{
|
|
LogName = 'Security'
|
|
ProviderName = 'Microsoft-Windows-Security-Auditing'
|
|
AlertCategory = 'security_event_monitor'
|
|
AuditPolicyAlertCategory = 'security_event_monitor_audit_policy'
|
|
ErrorAlertCategory = 'security_event_monitor_error'
|
|
LogEventName = 'Security Event Monitoring'
|
|
StatePath = 'C:\ProgramData\Cybertek\SecurityEventMonitor\state.json'
|
|
ReportPath = 'C:\ProgramData\Cybertek\SecurityEventMonitor\last_report.txt'
|
|
InitialLookbackMins = 90
|
|
MaxEventsPerAlert = 25
|
|
MaxAlertBodyLength = 7000
|
|
EnableAuditPolicyPreflight = $true
|
|
AutoEnableMissingAuditPolicy = $true
|
|
CloseAlertWhenClean = $false
|
|
CloseAuditPolicyAlertWhenClean = $true
|
|
}
|
|
|
|
$MonitoredEvents = @(
|
|
[PSCustomObject]@{ Key='interactive_logon'; EventId=4624; Title='Interactive Logon'; MatchField='LogonType'; MatchValue='2' }
|
|
[PSCustomObject]@{ Key='rdp_logon'; EventId=4624; Title='RDP Logon'; MatchField='LogonType'; MatchValue='10' }
|
|
[PSCustomObject]@{ Key='failed_logon'; EventId=4625; Title='Failed Logon'; MatchField=$null; MatchValue=$null }
|
|
[PSCustomObject]@{ Key='explicit_creds'; EventId=4648; Title='Explicit Credentials Used'; MatchField=$null; MatchValue=$null }
|
|
[PSCustomObject]@{ Key='user_created'; EventId=4720; Title='Local User Created'; MatchField=$null; MatchValue=$null }
|
|
[PSCustomObject]@{ Key='user_removed_grp'; EventId=4729; Title='User Removed From Group'; MatchField=$null; MatchValue=$null }
|
|
[PSCustomObject]@{ Key='user_added_grp'; EventId=4732; Title='User Added To Group'; MatchField=$null; MatchValue=$null }
|
|
[PSCustomObject]@{ Key='acct_lockout'; EventId=4740; Title='Account Locked Out'; MatchField=$null; MatchValue=$null }
|
|
[PSCustomObject]@{ Key='user_logoff'; EventId=4634; Title='User Logoff'; MatchField=$null; MatchValue=$null }
|
|
[PSCustomObject]@{ Key='system_logoff'; EventId=4647; Title='Logoff Initiated By User'; MatchField=$null; MatchValue=$null }
|
|
)
|
|
|
|
# Audit policy prerequisites are intentionally mapped at the event-family level.
|
|
# This keeps remediation understandable and avoids overfitting to individual IDs.
|
|
$RequiredAuditPolicies = @(
|
|
[PSCustomObject]@{ Subcategory='Logon'; RequireSuccess=$true; RequireFailure=$true; Purpose='4624 / 4625 / 4648 logon activity' }
|
|
[PSCustomObject]@{ Subcategory='Logoff'; RequireSuccess=$true; RequireFailure=$false; Purpose='4634 logoff activity' }
|
|
[PSCustomObject]@{ Subcategory='Other Logon/Logoff Events'; RequireSuccess=$true; RequireFailure=$false; Purpose='4647 user-initiated logoff activity' }
|
|
[PSCustomObject]@{ Subcategory='User Account Management'; RequireSuccess=$true; RequireFailure=$false; Purpose='4720 local user creation activity' }
|
|
[PSCustomObject]@{ Subcategory='Security Group Management'; RequireSuccess=$true; RequireFailure=$false; Purpose='4729 / 4732 group membership changes' }
|
|
[PSCustomObject]@{ Subcategory='Account Lockout'; RequireSuccess=$true; RequireFailure=$false; Purpose='4740 account lockout activity' }
|
|
)
|
|
|
|
function Write-Log {
|
|
param([string]$Message)
|
|
[Console]::WriteLine("[SECMON] $Message")
|
|
}
|
|
|
|
function Ensure-ParentDirectory {
|
|
param([string]$Path)
|
|
$parent = Split-Path -Path $Path -Parent
|
|
if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent)) {
|
|
New-Item -Path $parent -ItemType Directory -Force | Out-Null
|
|
}
|
|
}
|
|
|
|
function Get-SafeCount {
|
|
param([AllowNull()] [object]$Value)
|
|
if ($null -eq $Value) { return 0 }
|
|
$countProp = $Value.PSObject.Properties['Count']
|
|
if ($countProp) {
|
|
try { return [int]$countProp.Value } catch {}
|
|
}
|
|
if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {
|
|
return @($Value).Count
|
|
}
|
|
return 1
|
|
}
|
|
|
|
function ConvertTo-NormalizedString {
|
|
param([AllowNull()] [object]$Value)
|
|
if ($null -eq $Value) { return $null }
|
|
$s = [string]$Value
|
|
if ([string]::IsNullOrWhiteSpace($s)) { return $null }
|
|
return $s.Trim()
|
|
}
|
|
|
|
function Load-State {
|
|
param([Parameter(Mandatory)] [string]$Path)
|
|
if (-not (Test-Path -LiteralPath $Path -ErrorAction SilentlyContinue)) {
|
|
return [PSCustomObject]@{ LastRecordId = 0 }
|
|
}
|
|
|
|
try {
|
|
return (Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json)
|
|
}
|
|
catch {
|
|
Write-Warning "Could not load state file: $($_.Exception.Message)"
|
|
return [PSCustomObject]@{ LastRecordId = 0 }
|
|
}
|
|
}
|
|
|
|
function Save-State {
|
|
param(
|
|
[Parameter(Mandatory)] [string]$Path,
|
|
[Parameter(Mandatory)] [long]$LastRecordId
|
|
)
|
|
|
|
try {
|
|
Ensure-ParentDirectory -Path $Path
|
|
([PSCustomObject]@{
|
|
LastRecordId = $LastRecordId
|
|
UpdatedAt = (Get-Date -Format s)
|
|
} | ConvertTo-Json -Depth 4) | Set-Content -LiteralPath $Path -Encoding UTF8 -Force
|
|
}
|
|
catch {
|
|
Write-Warning "Could not save state file: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
function Close-AlertIfEnabled {
|
|
param(
|
|
[Parameter(Mandatory)] [string]$Category,
|
|
[Parameter(Mandatory)] [bool]$Enabled
|
|
)
|
|
|
|
if ($Enabled -and (Get-Command Close-Rmm-Alert -ErrorAction SilentlyContinue)) {
|
|
Close-Rmm-Alert -Category $Category -CloseAlertTicket $false
|
|
}
|
|
}
|
|
|
|
function Get-AuditPolicyMap {
|
|
$map = @{}
|
|
$lines = @()
|
|
|
|
try {
|
|
$lines = @(auditpol /get /category:* 2>&1)
|
|
}
|
|
catch {
|
|
throw "Could not run auditpol: $($_.Exception.Message)"
|
|
}
|
|
|
|
foreach ($line in $lines) {
|
|
$text = [string]$line
|
|
if ([string]::IsNullOrWhiteSpace($text)) { continue }
|
|
if ($text -match '^\s{2,}(.+?)\s{2,}(.+)$') {
|
|
$subcategory = $matches[1].Trim()
|
|
$setting = $matches[2].Trim()
|
|
if ($subcategory -and $setting) {
|
|
$map[$subcategory] = $setting
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($map.Count -eq 0) {
|
|
throw 'auditpol returned no parseable audit policy data.'
|
|
}
|
|
|
|
return $map
|
|
}
|
|
|
|
function Test-AuditPolicyRequirement {
|
|
param(
|
|
[Parameter(Mandatory)] [string]$Setting,
|
|
[Parameter(Mandatory)] [bool]$RequireSuccess,
|
|
[Parameter(Mandatory)] [bool]$RequireFailure
|
|
)
|
|
|
|
$normalized = $Setting.ToLowerInvariant()
|
|
$hasSuccess = $normalized.Contains('success')
|
|
$hasFailure = $normalized.Contains('failure')
|
|
|
|
if ($RequireSuccess -and -not $hasSuccess) { return $false }
|
|
if ($RequireFailure -and -not $hasFailure) { return $false }
|
|
return $true
|
|
}
|
|
|
|
function Get-AuditPolicyFindings {
|
|
param(
|
|
[Parameter(Mandatory)] [hashtable]$AuditPolicyMap,
|
|
[Parameter(Mandatory)] [object[]]$Requirements
|
|
)
|
|
|
|
$findings = New-Object System.Collections.Generic.List[object]
|
|
|
|
foreach ($requirement in $Requirements) {
|
|
$setting = $null
|
|
if ($AuditPolicyMap.ContainsKey($requirement.Subcategory)) {
|
|
$setting = [string]$AuditPolicyMap[$requirement.Subcategory]
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($setting)) {
|
|
$findings.Add([PSCustomObject]@{
|
|
Subcategory = $requirement.Subcategory
|
|
Current = 'Missing'
|
|
Expected = if ($requirement.RequireFailure) { 'Success and Failure' } elseif ($requirement.RequireSuccess) { 'Success' } else { 'Configured' }
|
|
Purpose = $requirement.Purpose
|
|
})
|
|
continue
|
|
}
|
|
|
|
if (-not (Test-AuditPolicyRequirement -Setting $setting -RequireSuccess $requirement.RequireSuccess -RequireFailure $requirement.RequireFailure)) {
|
|
$findings.Add([PSCustomObject]@{
|
|
Subcategory = $requirement.Subcategory
|
|
Current = $setting
|
|
Expected = if ($requirement.RequireFailure) { 'Success and Failure' } elseif ($requirement.RequireSuccess) { 'Success' } else { 'Configured' }
|
|
Purpose = $requirement.Purpose
|
|
})
|
|
}
|
|
}
|
|
|
|
return $findings
|
|
}
|
|
|
|
function Set-AuditPolicyRequirement {
|
|
param([Parameter(Mandatory)] [pscustomobject]$Requirement)
|
|
|
|
$successArg = if ($Requirement.RequireSuccess) { 'enable' } else { 'disable' }
|
|
$failureArg = if ($Requirement.RequireFailure) { 'enable' } else { 'disable' }
|
|
|
|
$output = @(auditpol /set /subcategory:"$($Requirement.Subcategory)" /success:$successArg /failure:$failureArg 2>&1)
|
|
$exitCode = $LASTEXITCODE
|
|
|
|
return [PSCustomObject]@{
|
|
Subcategory = $Requirement.Subcategory
|
|
Success = ($exitCode -eq 0)
|
|
ExitCode = $exitCode
|
|
Output = (($output | ForEach-Object { [string]$_ }) -join [Environment]::NewLine)
|
|
}
|
|
}
|
|
|
|
function Build-AuditPolicyRemediationBody {
|
|
param(
|
|
[Parameter(Mandatory)] [object[]]$Findings,
|
|
[Parameter(Mandatory)] [object[]]$RemediationResults
|
|
)
|
|
|
|
$lines = New-Object System.Collections.Generic.List[string]
|
|
$lines.Add('Windows Security auditing prerequisites were missing and remediation was attempted.')
|
|
$lines.Add("Host : $env:COMPUTERNAME")
|
|
$lines.Add("Timestamp : $(Get-Date -Format s)")
|
|
$lines.Add('')
|
|
|
|
foreach ($item in $Findings) {
|
|
$result = @($RemediationResults | Where-Object { $_.Subcategory -eq $item.Subcategory } | Select-Object -First 1)
|
|
$status = if ((Get-SafeCount $result) -gt 0 -and $result[0].Success) { 'Remediated' } else { 'Failed' }
|
|
$lines.Add("- Subcategory=$($item.Subcategory); Current=$($item.Current); Expected=$($item.Expected); Result=$status; Purpose=$($item.Purpose)")
|
|
}
|
|
|
|
return ($lines -join [Environment]::NewLine)
|
|
}
|
|
|
|
function Build-AuditPolicyAlertBody {
|
|
param([Parameter(Mandatory)] [object[]]$Findings)
|
|
|
|
$lines = New-Object System.Collections.Generic.List[string]
|
|
$lines.Add('Windows Security auditing may be incomplete for this monitor.')
|
|
$lines.Add("Host : $env:COMPUTERNAME")
|
|
$lines.Add("Timestamp : $(Get-Date -Format s)")
|
|
$lines.Add('')
|
|
|
|
foreach ($item in $Findings) {
|
|
$lines.Add("- Subcategory=$($item.Subcategory); Current=$($item.Current); Expected=$($item.Expected); Purpose=$($item.Purpose)")
|
|
}
|
|
|
|
return ($lines -join [Environment]::NewLine)
|
|
}
|
|
|
|
function Get-EventDataMap {
|
|
param([Parameter(Mandatory)] [System.Diagnostics.Eventing.Reader.EventRecord]$Event)
|
|
|
|
$map = @{}
|
|
try {
|
|
$xml = [xml]$Event.ToXml()
|
|
foreach ($node in @($xml.Event.EventData.Data)) {
|
|
$name = [string]$node.Name
|
|
if ([string]::IsNullOrWhiteSpace($name)) { continue }
|
|
$map[$name] = [string]$node.'#text'
|
|
}
|
|
}
|
|
catch {}
|
|
|
|
return $map
|
|
}
|
|
|
|
function Resolve-EventDefinition {
|
|
param(
|
|
[Parameter(Mandatory)] [System.Diagnostics.Eventing.Reader.EventRecord]$Event,
|
|
[Parameter(Mandatory)] [object[]]$Definitions
|
|
)
|
|
|
|
$eventData = Get-EventDataMap -Event $Event
|
|
|
|
foreach ($definition in $Definitions) {
|
|
if ($Event.Id -ne $definition.EventId) { continue }
|
|
if (-not $definition.MatchField) {
|
|
return [PSCustomObject]@{
|
|
Definition = $definition
|
|
EventData = $eventData
|
|
}
|
|
}
|
|
|
|
$value = $null
|
|
if ($eventData.ContainsKey($definition.MatchField)) {
|
|
$value = ConvertTo-NormalizedString $eventData[$definition.MatchField]
|
|
}
|
|
|
|
if ($value -eq $definition.MatchValue) {
|
|
return [PSCustomObject]@{
|
|
Definition = $definition
|
|
EventData = $eventData
|
|
}
|
|
}
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
function Get-PrimaryUser {
|
|
param([hashtable]$EventData)
|
|
foreach ($field in @('TargetUserName', 'SubjectUserName', 'TargetUser', 'AccountName')) {
|
|
if ($EventData.ContainsKey($field)) {
|
|
$value = ConvertTo-NormalizedString $EventData[$field]
|
|
if ($value -and $value -notin @('-', 'SYSTEM', 'ANONYMOUS LOGON')) {
|
|
return $value
|
|
}
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Get-RemoteSource {
|
|
param([hashtable]$EventData)
|
|
foreach ($field in @('IpAddress', 'WorkstationName', 'ProcessName', 'IpPort')) {
|
|
if ($EventData.ContainsKey($field)) {
|
|
$value = ConvertTo-NormalizedString $EventData[$field]
|
|
if ($value -and $value -ne '-') { return $value }
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function New-MonitoredEvent {
|
|
param(
|
|
[Parameter(Mandatory)] [System.Diagnostics.Eventing.Reader.EventRecord]$Event,
|
|
[Parameter(Mandatory)] [hashtable]$EventData,
|
|
[Parameter(Mandatory)] [pscustomobject]$Definition
|
|
)
|
|
|
|
[PSCustomObject]@{
|
|
RecordId = [long]$Event.RecordId
|
|
EventId = [int]$Event.Id
|
|
TimeCreated = $Event.TimeCreated
|
|
Title = $Definition.Title
|
|
Key = $Definition.Key
|
|
User = Get-PrimaryUser -EventData $EventData
|
|
Source = Get-RemoteSource -EventData $EventData
|
|
LogonType = if ($EventData.ContainsKey('LogonType')) { $EventData['LogonType'] } else { $null }
|
|
}
|
|
}
|
|
|
|
function Get-EventSummaryLine {
|
|
param([Parameter(Mandatory)] [object]$Event)
|
|
|
|
switch ($Event.Key) {
|
|
'interactive_logon' { return "User '$($Event.User)' signed in locally." }
|
|
'rdp_logon' { return "User '$($Event.User)' signed in through Remote Desktop." }
|
|
'failed_logon' { return "A sign-in attempt for '$($Event.User)' failed." }
|
|
'explicit_creds' { return "Explicit credentials were used for '$($Event.User)'." }
|
|
'user_created' { return "A local user account '$($Event.User)' was created." }
|
|
'user_removed_grp' { return "A user was removed from group '$($Event.User)'." }
|
|
'user_added_grp' { return "A user was added to group '$($Event.User)'." }
|
|
'acct_lockout' { return "The account '$($Event.User)' was locked out." }
|
|
'user_logoff' { return "User '$($Event.User)' signed out." }
|
|
'system_logoff' { return "A user-initiated logoff was recorded for '$($Event.User)'." }
|
|
default { return "$($Event.Title) detected for '$($Event.User)'." }
|
|
}
|
|
}
|
|
|
|
function Build-AlertBody {
|
|
param(
|
|
[Parameter(Mandatory)] [System.Collections.Generic.List[object]]$Events,
|
|
[Parameter(Mandatory)] [hashtable]$Config,
|
|
[switch]$Truncate
|
|
)
|
|
|
|
$lines = New-Object System.Collections.Generic.List[string]
|
|
$lines.Add("Windows Security audit events detected.")
|
|
$lines.Add("Host : $env:COMPUTERNAME")
|
|
$lines.Add("Timestamp : $(Get-Date -Format s)")
|
|
$lines.Add("Event Count : $(Get-SafeCount $Events)")
|
|
$lines.Add("")
|
|
|
|
$lines.Add("Summary")
|
|
foreach ($group in ($Events | Group-Object Title | Sort-Object Name)) {
|
|
$lines.Add("- $($group.Name): $(@($group.Group).Count)")
|
|
}
|
|
$lines.Add("")
|
|
|
|
$lines.Add("Details")
|
|
|
|
foreach ($item in ($Events | Select-Object -First $Config.MaxEventsPerAlert)) {
|
|
$lines.Add("- $($item.Title)")
|
|
$lines.Add(" What happened : $(Get-EventSummaryLine -Event $item)")
|
|
$lines.Add(" Time : $($item.TimeCreated)")
|
|
$lines.Add(" User : $($item.User)")
|
|
if ($item.Source) {
|
|
$lines.Add(" Source : $($item.Source)")
|
|
}
|
|
if ($item.LogonType) {
|
|
$lines.Add(" Logon Type : $($item.LogonType)")
|
|
}
|
|
$lines.Add(" Event ID : $($item.EventId)")
|
|
$lines.Add(" Record ID : $($item.RecordId)")
|
|
$lines.Add("")
|
|
}
|
|
|
|
if ((Get-SafeCount $Events) -gt $Config.MaxEventsPerAlert) {
|
|
$lines.Add("... $((Get-SafeCount $Events) - $Config.MaxEventsPerAlert) additional events truncated")
|
|
}
|
|
|
|
$body = ($lines -join [Environment]::NewLine)
|
|
if ($Truncate -and $body.Length -gt $Config.MaxAlertBodyLength) {
|
|
return $body.Substring(0, $Config.MaxAlertBodyLength) + "`r`n... truncated. Full report: $($Config.ReportPath)"
|
|
}
|
|
|
|
return $body
|
|
}
|
|
|
|
$exitCode = 0
|
|
try {
|
|
if ($Config.EnableAuditPolicyPreflight) {
|
|
try {
|
|
Write-Log 'Checking local audit policy prerequisites...'
|
|
$auditPolicyMap = Get-AuditPolicyMap
|
|
$auditFindings = @(Get-AuditPolicyFindings -AuditPolicyMap $auditPolicyMap -Requirements $RequiredAuditPolicies)
|
|
|
|
if ((Get-SafeCount $auditFindings) -gt 0) {
|
|
$auditMessage = "Audit policy preflight found $(Get-SafeCount $auditFindings) prerequisite issue(s)."
|
|
Write-Warning $auditMessage
|
|
|
|
if ($Config.AutoEnableMissingAuditPolicy) {
|
|
Write-Log 'Attempting to enable missing audit policy prerequisites...'
|
|
$remediationResults = New-Object System.Collections.Generic.List[object]
|
|
|
|
foreach ($finding in $auditFindings) {
|
|
$requirement = @($RequiredAuditPolicies | Where-Object { $_.Subcategory -eq $finding.Subcategory } | Select-Object -First 1)
|
|
if ((Get-SafeCount $requirement) -eq 0) { continue }
|
|
$remediationResults.Add((Set-AuditPolicyRequirement -Requirement $requirement[0]))
|
|
}
|
|
|
|
$failedRemediations = @($remediationResults | Where-Object { -not $_.Success })
|
|
|
|
if ((Get-SafeCount $failedRemediations) -gt 0) {
|
|
$auditBody = Build-AuditPolicyRemediationBody -Findings $auditFindings -RemediationResults $remediationResults
|
|
if (Get-Command Rmm-Alert -ErrorAction SilentlyContinue) {
|
|
Rmm-Alert -Category $Config.AuditPolicyAlertCategory -Body $auditBody
|
|
}
|
|
Write-Warning "Audit policy remediation failed for $(Get-SafeCount $failedRemediations) subcategory(s)."
|
|
foreach ($item in $failedRemediations) {
|
|
Write-Warning "Audit policy remediation failure: Subcategory=$($item.Subcategory); ExitCode=$($item.ExitCode); Output=$($item.Output)"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log "Audit policy remediation completed successfully for $(Get-SafeCount $remediationResults) subcategory(s)."
|
|
if (Get-Command Log-Activity -ErrorAction SilentlyContinue) {
|
|
Log-Activity -Message "Audit policy remediation completed successfully for $(Get-SafeCount $remediationResults) subcategory(s)." -EventName $Config.LogEventName
|
|
}
|
|
Close-AlertIfEnabled -Category $Config.AuditPolicyAlertCategory -Enabled $Config.CloseAuditPolicyAlertWhenClean
|
|
}
|
|
}
|
|
elseif (Get-Command Rmm-Alert -ErrorAction SilentlyContinue) {
|
|
Rmm-Alert -Category $Config.AuditPolicyAlertCategory -Body (Build-AuditPolicyAlertBody -Findings $auditFindings)
|
|
}
|
|
|
|
if (Get-Command Log-Activity -ErrorAction SilentlyContinue) {
|
|
Log-Activity -Message $auditMessage -EventName $Config.LogEventName
|
|
}
|
|
}
|
|
else {
|
|
Write-Log 'Audit policy preflight passed; required subcategories are already enabled.'
|
|
if (Get-Command Log-Activity -ErrorAction SilentlyContinue) {
|
|
Log-Activity -Message 'Audit policy preflight passed; required subcategories are already enabled.' -EventName $Config.LogEventName
|
|
}
|
|
Close-AlertIfEnabled -Category $Config.AuditPolicyAlertCategory -Enabled $Config.CloseAuditPolicyAlertWhenClean
|
|
}
|
|
}
|
|
catch {
|
|
Write-Warning "Audit policy preflight could not be completed: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
$state = Load-State -Path $Config.StatePath
|
|
$lastRecordId = 0
|
|
if ($state.PSObject.Properties['LastRecordId']) {
|
|
$lastRecordId = [long]$state.LastRecordId
|
|
}
|
|
|
|
$startTime = (Get-Date).AddMinutes(-1 * $Config.InitialLookbackMins)
|
|
$eventIds = @($MonitoredEvents | Select-Object -ExpandProperty EventId -Unique)
|
|
|
|
Write-Log "Querying Windows Security log..."
|
|
try {
|
|
$rawEvents = @(Get-WinEvent -FilterHashtable @{
|
|
LogName = $Config.LogName
|
|
ProviderName = $Config.ProviderName
|
|
Id = $eventIds
|
|
StartTime = $startTime
|
|
} -ErrorAction Stop | Sort-Object RecordId)
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message -like '*No events were found that match the specified selection criteria*') {
|
|
$rawEvents = @()
|
|
}
|
|
else {
|
|
throw
|
|
}
|
|
}
|
|
|
|
$matchedEvents = New-Object System.Collections.Generic.List[object]
|
|
$maxRecordId = $lastRecordId
|
|
|
|
foreach ($event in @($rawEvents)) {
|
|
if ([long]$event.RecordId -le $lastRecordId) { continue }
|
|
|
|
$resolved = Resolve-EventDefinition -Event $event -Definitions $MonitoredEvents
|
|
if ($null -eq $resolved) { continue }
|
|
|
|
$matchedEvents.Add((New-MonitoredEvent -Event $event -EventData $resolved.EventData -Definition $resolved.Definition))
|
|
if ([long]$event.RecordId -gt $maxRecordId) {
|
|
$maxRecordId = [long]$event.RecordId
|
|
}
|
|
}
|
|
|
|
if ((Get-SafeCount $matchedEvents) -gt 0) {
|
|
$fullReport = Build-AlertBody -Events $matchedEvents -Config $Config
|
|
$alertBody = Build-AlertBody -Events $matchedEvents -Config $Config -Truncate
|
|
|
|
try {
|
|
Ensure-ParentDirectory -Path $Config.ReportPath
|
|
Set-Content -LiteralPath $Config.ReportPath -Value $fullReport -Encoding UTF8 -Force
|
|
}
|
|
catch {
|
|
Write-Warning "Could not write report file: $($_.Exception.Message)"
|
|
}
|
|
|
|
if (Get-Command Rmm-Alert -ErrorAction SilentlyContinue) {
|
|
Rmm-Alert -Category $Config.AlertCategory -Body $alertBody
|
|
}
|
|
else {
|
|
Write-Output $alertBody
|
|
}
|
|
|
|
$summary = "Detected $(Get-SafeCount $matchedEvents) new Windows Security event(s)."
|
|
Write-Log $summary
|
|
if (Get-Command Log-Activity -ErrorAction SilentlyContinue) {
|
|
Log-Activity -Message $summary -EventName $Config.LogEventName
|
|
}
|
|
}
|
|
else {
|
|
Write-Log 'No new monitored Windows Security events detected.'
|
|
Close-AlertIfEnabled -Category $Config.AlertCategory -Enabled $Config.CloseAlertWhenClean
|
|
}
|
|
|
|
Save-State -Path $Config.StatePath -LastRecordId $maxRecordId
|
|
}
|
|
catch {
|
|
$message = "Security event monitor failed: $($_.Exception.Message)"
|
|
[Console]::Error.WriteLine($message)
|
|
if (Get-Command Rmm-Alert -ErrorAction SilentlyContinue) {
|
|
try { Rmm-Alert -Category $Config.ErrorAlertCategory -Body $message } catch {}
|
|
}
|
|
$exitCode = 2
|
|
}
|
|
|
|
exit $exitCode
|