You should always ensure your domain controllers remain properly patched is a critical to the security of the organisation, what started as a seemingly straightforward PowerShell project turned into a winding journey through unexpected errors, syntax quirks, and HTML/CSS challenges. Today, I'm sharing my complete experience—with all its frustrations and victories—building an interactive patch status dashboard.
The Dashboard Visuals
This dashboard uses the red, amber, green status that outlines the patches that are installed and compares them to the current system date, I have chosen the "Needs Update" option for this view:
You will also notice you see the "timeframe" with the status (underneath the number) which gives you a quick view of the patching status that can be run frequently.
Challenge #1: The Data Source Transition
My first hurdle came when I realized that switching from a log file to a CSV file format would give me more structured data to work with. This seemingly simple change caused quite a few headaches:
# Original approach - parsing log files
$servers = Get-Content "DCUpdates.log" | Where-Object { $_ -match "DC\d+" } |
ForEach-Object {
# Complex regex parsing to extract information
}
function Import-ServerDataFromCsv {
param(
[Parameter(Mandatory=$true)]
[string]$CsvFilePath
)
try {
$serverData = Import-Csv -Path $CsvFilePath -ErrorAction Stop
}
catch {
# Had to implement fallback parsing for malformed CSVs
$csvContent = Get-Content -Path $CsvFilePath -Raw
$lines = $csvContent -split "`n"
$headers = ($lines[0] -split ',').Trim()
$serverData = @()
for ($i = 1; $i -lt $lines.Count; $i++) {
# Custom CSV parsing logic
}
}
}
The CSV approach offered better structure but came with its own challenges—handling different CSV formats, inconsistent headers, and varying field names. I had to build in extra resilience to handle these variations.
My initial PowerShell script lacked clear thresholds for categorizing servers:
function Determine-ServerStatus {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server
)
# No clear definition of what constitutes "Up to Date" vs "Needs Update"
}
After several iterations, I realized I needed configurable thresholds that matched our organization's patching policies:
# Define thresholds for update status
$GreenThreshold = 30 # Up to Date: within 30 days
$AmberThreshold = 60 # Update Soon: between 31-60 days
# Needs Update: over 60 days
But when I tried to add this to my parameter block, I ran into a frustrating syntax error:
param(
# Other parameters...
[int]$AmberThreshold = 60 # Update Soon: 31-60 days
# Error: Missing ')' in function parameter list.
)
I discovered PowerShell doesn't like comments on the same line as parameter declarations in certain contexts. The solution was to move the thresholds outside the parameter block entirely:
param(
# Basic parameters only
)
# Define thresholds for update status as script variables instead
$GreenThreshold = 30 # Up to Date: within 30 days
$AmberThreshold = 60 # Update Soon: between 31-60 days
Challenge #3: The Disappearing Health Status Cards
One of the most frustrating issues I faced was that the status summary at the top of the dashboard wasn't displaying properly. The counts for green, amber, and red servers remained blank:
It also showed no valid data here, if you checked the html code there data was there but annoyingly hidden due to a coding error:
This also occurred on the professional report as well:
After much debugging, I discovered the issue was in how PowerShell handles variable expansion within here-strings (multi-line strings):
Wrong Method: This syntax tries to use curly braces for variable expansion, which works differently in PowerShell:
$html = @" <div class="status-count">${statusCounts.green}</div> "@Correct Method: Proper PowerShell subexpression syntax for accessing hashtable values
$html = @" <div class="status-count">$($statusCounts.green)</div> "@
The difference is subtle but crucial:
- ${statusCounts.green} - This syntax doesn't correctly access the hashtable's "green" property in PowerShell within a here-string
- $($statusCounts.green) - This uses PowerShell's subexpression operator $() to properly evaluate the expression and expand the hashtable value
Challenge #4: The Missing Server Icons
Another visually obvious issue was that the server icons weren't displaying with the correct color-coding:
The colored circles that should indicate server status were empty or incorrectly styled. Again, the culprit was PowerShell variable expansion, this time they are colour coded, red is the wrong version and green is the correct version:
<div class="icon ${server.Status}">DC</div>
<div class="icon $($server.Status)">DC</div>
This subtle syntax difference was causing all my status indicators to be blank!
Challenge #5: The Filtering That Didn't Filter
With all the server cards displaying, I expected the filter buttons to work properly, but clicking on "Up to Date" or "Needs Update" did nothing:
//
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.filter-button');
const serverCards = document.querySelectorAll('.server-card');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
const filter = this.getAttribute('data-filter');
serverCards.forEach(card => {
if (filter === 'all' || card.getAttribute('data-status') === filter) {
card.style.display = 'block';
} else {
card.style.display = 'none';
}
});
});
});
});
The JavaScript looked correct, but it wasn't working because the data-status
attributes on the server cards weren't set correctly due to the PowerShell variable expansion issue. Once I fixed the variable expansion, the filtering began working perfectly.
Challenge #6: Getting the Latest Patch Information
I wanted the dashboard to show not just the current patch on each server but also the latest available patch. This meant querying Microsoft's Update Catalog or using another method to get the latest KB numbers.
I created a function to handle this, but encountered corporate proxy issues:
function Get-LatestPatchInfo {
param(
[Parameter(Mandatory=$true)]
[hashtable]$Server,
[Parameter(Mandatory=$true)]
[string]$ProxyServer
)
# Configure proxy settings
if (-not [string]::IsNullOrEmpty($ProxyServer)) {
[System.Net.WebRequest]::DefaultWebProxy = New-Object
System.Net.WebProxy("http://$ProxyServer")
[System.Net.WebRequest]::DefaultWebProxy.Credentials =
[System.Net.CredentialCache]::DefaultNetworkCredentials
}
# Attempt to query Microsoft Update Catalog
try {
# Web query logic...
}
catch {
# Fallback to hardcoded values if the web query fails
}
}
Given our complex network environment, I implemented a fallback system that would use predefined patch information if the web query failed, you can see this working in the console output here:
Key Lessons Learned
This project taught me several valuable lessons:
- PowerShell variable expansion in here-strings: The syntax
$($variable)
is required for proper expansion, especially within HTML attributes and when accessing hashtable properties. - Parameter block limitations: Be careful with comments in parameter blocks. Sometimes it's better to define configuration variables outside the parameter block entirely.
- HTML generation in PowerShell: Testing the generated HTML is essential; what looks correct in the code might not render properly in the browser.
- Defensive coding: Always implement fallbacks for web queries and data processing to handle unexpected formats or connectivity issues.
- Incremental testing: Test each component separately before trying to integrate everything. I could have saved hours by testing my HTML generation independently from the data processing.
Conclusion
Building this domain controller patch status dashboard was far more challenging than I initially expected, but the end result has transformed how we monitor and manage our patching process. Now, at a glance, we can see exactly which servers need attention and prioritize patching accordingly.