In enterprise environments, Windows Event Collection (WEC) serves as a critical component for centralized event log management. However, one common challenge administrators face is efficiently managing permissions across numerous servers to ensure proper event collection and forwarding.
The problem arises when you try and give roles based on only the access required, If you run the WEC as an domain administrator then this bypasses the requirement for this article, I want to use a service account with least administrative privilege rather than blasting it with far too much access, hence, the reason for this article.
The Challenge
Recently, I worked on implementing a solution to add a specific service account to the "Event Log Readers" group across multiple servers registered in a Windows Event Collection subscription. This seemingly straightforward task revealed several complexities and nuances of remote administration in Windows environments.
The goal was simple: identify all computers in a specific WEC subscription and ensure a dedicated service account had appropriate permissions to read event logs on each machine. This was the journey, highlighting key challenges and solutions.
Registry-Based Discovery
The first step involved identifying which servers were part of a specific WEC subscription. Windows stores this information in the registry under:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\EventCollector\Subscriptions
Each subscription is stored as a subkey, with the computers listed under an "EventSources" subkey. Reading this information programmatically allows administrators to dynamically identify target servers without maintaining separate server lists.
Remote Administration Challenges
Once the target servers were identified, the next challenge was modifying the local group membership remotely. This revealed several interesting technical hurdles:
Domain Trust Relationships
In complex Active Directory environments with multiple domains or forests, trust relationships play a crucial role. We encountered scenarios where traditional PowerShell remoting failed due to broken trust relationships between domains. This necessitated a more resilient approach.
PowerShell Version Discrepancies
Different servers often run different PowerShell versions, especially in organizations with a mix of legacy and modern systems. Commands and parameters available in newer PowerShell versions may not exist in older ones, requiring a solution compatible across all versions.
Connectivity and Authentication
Not all servers in an enterprise are accessible through the same protocols. Some might block WinRM (Windows Remote Management), while others might restrict SMB access or ICMP (ping) packets. A robust solution needs to attempt multiple connection methods.
NOTE : If you block all methods of remote access, this cannot be completed remotely, so you will need to manually do it on the servers where this extra security is applied
Accuracy in Verification
Perhaps the most interesting challenge was verifying group membership changes. We discovered that even when commands executed successfully and returned "success" codes, the actual group membership changes didn't always take effect. This highlighted the importance of verification after making changes.
Working Approach to the solution
The most effective approach combined several techniques:
- Local Execution: Creating a batch file and executing it locally on each server proved more reliable than trying to make changes remotely.
- Pre-checks: Verifying whether the account was already a member of the group before attempting to add it eliminated unnecessary errors.
- Multiple Connection Methods: Implementing fallback methods when primary connection methods failed increased the success rate significantly.
- Timeout Handling: Setting appropriate timeouts prevented the process from hanging when servers were unresponsive.
- Comprehensive Reporting: Generating detailed reports helped identify which servers were successfully updated and which needed further attention.
Key Learnings
This project reinforced several important principles for enterprise systems administration:
- Trust But Verify: Always verify changes were actually applied, don't just trust success messages.
- Backwards Compatibility Matters: In heterogeneous environments, solutions must work with the oldest systems, not just the newest ones.
- Error Handling Is Critical: Robust error handling and reporting are essential for troubleshooting and follow-up actions.
- Security Considerations: Understanding how different security contexts affect remote administration capabilities is crucial for successful implementation.
- Simplicity Wins: Often, the most effective solutions are the simplest ones—in this case, executing a local batch file proved more reliable than complex remote PowerShell commands.
Results and Impact
After implementing these solutions, we successfully ensured the service account had appropriate permissions across all servers in our WEC infrastructure. This improved the reliability of our event collection system, reduced manual administration overhead, and provided better security monitoring capabilities.
For organizations managing Windows event collection at scale, addressing these permission challenges systematically is essential for maintaining a robust security monitoring infrastructure.
Script : WECPermissions.ps1
Note : Remember to update the bold sections in the code for your environment
# Legacy PowerShell Compatible WEC Script
# Compatible with older PowerShell versions
# Clear screen
Clear-Host
Write-Host "Legacy WEC Group Management Script" -ForegroundColor Cyan
Write-Host "--------------------------------" -ForegroundColor Cyan
# Step 1: Read registry for subscriptions
Write-Host "Reading registry for subscriptions..." -ForegroundColor Yellow
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\EventCollector\Subscriptions"
$subscriptions = @()
if (Test-Path $regPath) {
$keys = Get-ChildItem -Path $regPath
foreach ($key in $keys) {
$subscriptions += $key.PSChildName
}
}
if ($subscriptions.Count -eq 0) {
Write-Host "No subscriptions found." -ForegroundColor Red
exit
}
# Display subscriptions
Write-Host "`nAvailable subscriptions:" -ForegroundColor Green
for ($i = 0; $i -lt $subscriptions.Count; $i++) {
Write-Host "[$($i+1)] $($subscriptions[$i])"
}
# Let user select a subscription
$selection = 0
do {
$input = Read-Host "`nSelect a subscription (1-$($subscriptions.Count))"
[int]::TryParse($input, [ref]$selection)
} while ($selection -lt 1 -or $selection -gt $subscriptions.Count)
$selectedSubscription = $subscriptions[$selection-1]
Write-Host "Selected: $selectedSubscription" -ForegroundColor Yellow
# Step 2: Read computers for the selected subscription
Write-Host "`nReading computers in the subscription..." -ForegroundColor Yellow
$computersPath = "$regPath\$selectedSubscription\EventSources"
$computers = @()
if (Test-Path $computersPath) {
$keys = Get-ChildItem -Path $computersPath
foreach ($key in $keys) {
$computers += $key.PSChildName
}
}
if ($computers.Count -eq 0) {
Write-Host "No computers found in subscription." -ForegroundColor Red
exit
}
# Display computers
Write-Host "`nComputers in '$selectedSubscription' subscription:" -ForegroundColor Green
foreach ($computer in $computers) {
Write-Host "- $computer"
}
Write-Host "`nTotal computers: $($computers.Count)" -ForegroundColor Cyan
# Ask for confirmation
$account = "<service_Account>"
$confirmation = Read-Host "`nDo you want to add '$account' to Event Log Readers group on these computers? (yes/no)"
if ($confirmation.ToLower() -eq "yes" -or $confirmation.ToLower() -eq "y") {
Write-Host "`nProcessing computers..." -ForegroundColor Yellow
$results = @()
$successCount = 0
$failCount = 0
foreach ($computer in $computers) {
Write-Host "Working on $computer..." -ForegroundColor Gray
$success = $false
$message = ""
try {
# Try to check SMB connectivity instead of ping
$smbAccessible = $false
try {
# Try to access admin$ share to check connectivity
$adminShare = "\\$computer\admin$"
if (Test-Path $adminShare -ErrorAction Stop) {
$smbAccessible = $true
}
} catch {
# Failed to access admin$ share, but let's try anyway
# Sometimes WMI still works even when admin$ isn't accessible
}
# Proceed with the action regardless of SMB check result
# This ensures we attempt the operation even if the SMB check fails
try {
# Create a batch file locally that will be executed on the remote system
$batchContent = @"
@echo off
REM Check if account is already a member
net localgroup "Event Log Readers" | find /i "<service_account>" > nul
if %ERRORLEVEL% EQU 0 (
echo ALREADY_MEMBER: Account is already a member of the group
exit 0
)
REM Add the account to the group
net localgroup "Event Log Readers" <service_Account_to_add> /add
if %ERRORLEVEL% EQU 0 (
echo SUCCESS: Account added successfully
exit 0
) else (
if %ERRORLEVEL% EQU 2 (
echo ALREADY_MEMBER: Account is already a member of the group
exit 0
) else (
echo FAILED: Error code %ERRORLEVEL%
exit %ERRORLEVEL%
)
)
"@
# Create a temporary file
$tempBatchFile = [System.IO.Path]::GetTempFileName() + ".bat"
Set-Content -Path $tempBatchFile -Value $batchContent
# Copy the batch file to the remote machine
$destFile = "\\$computer\admin$\temp\add_wec_agent.bat"
try {
# Ensure the remote temp directory exists
if (-not (Test-Path "\\$computer\admin$\temp" -ErrorAction SilentlyContinue)) {
New-Item -Path "\\$computer\admin$\temp" -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
}
# Copy the file
Copy-Item -Path $tempBatchFile -Destination $destFile -Force -ErrorAction Stop
# Execute the batch file on the remote computer
$cmd = "cmd.exe /c `"C:\Windows\temp\add_wec_agent.bat`""
$result = Invoke-WmiMethod -ComputerName $computer -Class Win32_Process -Name Create -ArgumentList $cmd -ErrorAction Stop
if ($result.ReturnValue -eq 0) {
Start-Sleep -Seconds 3 # Give time for command to complete
# Try to verify the account was added
try {
$verifyCmd = "cmd.exe /c `"net localgroup `"Event Log Readers`" | find /i `"<service_Account>`"`""
$verifyResult = Invoke-WmiMethod -ComputerName $computer -Class Win32_Process -Name Create -ArgumentList $verifyCmd -ErrorAction Stop
Start-Sleep -Seconds 2
# Attempt to read the results
$processStillExists = Get-WmiObject -ComputerName $computer -Class Win32_Process -Filter "ProcessId = $($verifyResult.ProcessId)" -ErrorAction SilentlyContinue
if ($processStillExists) {
$processStillExists | Remove-WmiObject -ErrorAction SilentlyContinue
# If process still exists, it may mean the command is still running
# Let's assume it worked since the batch file execution succeeded
$success = $true
$message = "Command successful, account likely added"
} else {
# Process completed - this could mean either:
# 1. The account was found and the process completed normally (success)
# 2. The account wasn't found and the process exited with non-zero code (failure)
# Since we can't easily capture the output, let's be optimistic
# if the initial command succeeded
$success = $true
$message = "Command executed successfully - account either added or was already a member"
}
} catch {
# If verification fails, assume success based on the command
$success = $true
$message = "Command ran successfully, but verification failed. The account may have been added."
}
} else {
$message = "Failed to execute batch file (code: $($result.ReturnValue))"
}
# Clean up - try to delete the temp file
Remove-Item -Path $destFile -Force -ErrorAction SilentlyContinue
} catch {
$message = "Failed to copy batch file: $_"
}
# Delete the local temp file
Remove-Item -Path $tempBatchFile -Force -ErrorAction SilentlyContinue
} catch {
$message = "WMI connection failed: $_"
}
# No else block here - we try the WMI operation regardless of SMB check
} catch {
$message = "Error: $_"
}
# Add to results
$result = New-Object PSObject
$result | Add-Member -MemberType NoteProperty -Name "Computer" -Value $computer
$result | Add-Member -MemberType NoteProperty -Name "Success" -Value $success
$result | Add-Member -MemberType NoteProperty -Name "Message" -Value $message
$results += $result
# Update counters
if ($success) {
$successCount++
Write-Host " SUCCESS: $message" -ForegroundColor Green
} else {
$failCount++
Write-Host " FAILED: $message" -ForegroundColor Red
}
}
# Display summary
Write-Host "`nOperation completed:" -ForegroundColor Cyan
Write-Host " Successfully processed: $successCount computers" -ForegroundColor Green
Write-Host " Failed: $failCount computers" -ForegroundColor Red
# Export results
$exportCsv = Read-Host "`nExport results to CSV file? (yes/no)"
if ($exportCsv.ToLower() -eq "yes" -or $exportCsv.ToLower() -eq "y") {
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$csvPath = "EventLogReaders_$timestamp.csv"
$results | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host "Results exported to: $csvPath" -ForegroundColor Green
}
} else {
Write-Host "`nOperation cancelled." -ForegroundColor Yellow
}
Write-Host "`nScript completed." -ForegroundColor Cyan