If you manage quite a few macOS devices, it would be nice to have a corporate dashboard that tells you if those devices are running the latest version of macOS.
Manual checks - not really what this blog is about!
Obviously, you can quite easily go around, checking the version yourself, by using the Apple icon on the menu then clicking “About my Mac” - but that is quite a manual process if you have a few of these devices.
Scripting software version detection and HTML report
We therefore need a script to compare the minor and major version that’s installed on the macOS device and compare that with the latest version available, I tried to use the Apple developer website to complete this action, but found the data to be very unreliable where you would get the version and you then have to click into that version to get all the subversions - this was not particularly effective so instead, I’ve switched to IPSW - this is a website that lets you download the macOS image files, but we won’t be using it for that purpose more for the version and the Build ID, this is what the dashboard will look like:
If I choose a currently supported device, this website will then give me the version number with the subversion and the build number, this is exactly the information I need to produce a dashboard that gives you an easy to read overview of your macOS estate.
I would also like to point out if you choose an unsupported device, you would obviously not get the latest version, you will only get the latest version that is applicable to that device.
In this particular example, we are using current generation models which do get the latest version however, if you are using unsupported device devices, then there is probably no use for you to have the script anyway.
Extract Current macOS Version
This will completed with the putty suite and mainly the "plink" executable which is the CLI version of the PuTTY GUI what we need to do is automate the execution of the command "sw_vers" which when run returns the following information which shows the current MacOS version:
ProductName: macOS
ProductVersion: 15.3
BuildVersion: 24D60
$Credential = Get-Credential
$Credential | Export-Clixml -Path "credentials.xml"
When running the above commands the username and password will be the password you log into the MacOS device with, not your Windows credentials.
$PuttyPath = "C:\Program Files\PuTTY\plink.exe"
$CredentialFile = "credentials.xml"
$OutputFile = "macosversion.txt"
# Import credentials from XML
$Credential = Import-Clixml -Path $CredentialFile
$Username = $Credential.UserName
$Password = $Credential.GetNetworkCredential().Password
# Define list of remote hosts and their corresponding names
$Servers = @{
"10.78.299.1" = "hq_”
"10.76.299.1" = "nyc_"
"10.77.299.1" = "sfo_"
"10.78.299.1" = "lax_"
"10.79.299.1" = "atl_"
"10.80.299.1" = "mco_"
}
# Loop through each server and run sw_vers
foreach ($RemoteHost in $Servers.Keys) {
$ServerName = $Servers[$RemoteHost]
Write-Host "Checking macOS version on $ServerName ($RemoteHost)..."
try {
# Execute sw_vers command remotely
$Result = & $PuttyPath -ssh -batch -pw $Password "$Username@$RemoteHost" "sw_vers"
if ($LASTEXITCODE -eq 0) {
# Add server identifier and append to file
$Output = "`n=== $ServerName ($RemoteHost) ===`n$Result`n"
Add-Content -Path $OutputFile -Value $Output
Write-Host "Successfully retrieved version info from $ServerName"
} else {
$ErrorMessage = "Failed to execute command on $ServerName ($RemoteHost)"
Write-Host "ERROR: $ErrorMessage"
Add-Content -Path $OutputFile -Value "`n=== $ServerName ($RemoteHost) ===`nERROR: Command execution failed`n"
}
}
catch {
$ErrorMessage = "Connection failed to $ServerName ($RemoteHost): $_"
Write-Host "ERROR: $ErrorMessage"
Add-Content -Path $OutputFile -Value "`n=== $ServerName ($RemoteHost) ===`nERROR: Connection failed`n"
}
}
Write-Host "`nAll version checks completed. Results saved to: $OutputFile"
This will then create a file called macosversion.txt which will then contain the production version (MacOS version) and the Build Version, as you can see from below we are running on MacOS 15.3 or Sequoia:
Create the HTML Dashboard
We now have the data we require the txt file created earlier so we need to parse that file then compare it to the list on IPSW of the latest versions and then compare that version from online to the one installed on the MacOS device.
$InputFile = "macosversion.txt"
$OutputFile = "macos_health_dashboard.html"
# Function to get latest macOS version from ipsw.me
function Get-LatestMacOSVersion {
try {
# Add TLS 1.2 support
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Query ipsw.me
$url = "https://ipsw.me/Mac16,10"
$headers = @{
"User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
"Accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
}
Write-Host "Fetching data from ipsw.me..."
$response = Invoke-WebRequest -Uri $url -Headers $headers -UseBasicParsing
Write-Host "Response received, status code: $($response.StatusCode)"
# Look for version pattern in the content
$pattern = 'macOS\s+(\d+\.\d+\.?\d*)\s*\((\w+)\)'
if ($response.Content -match $pattern) {
$version = $Matches[1]
$build = $Matches[2]
Write-Host "Successfully parsed: Version=$version, Build=$build"
return @{
Version = $version
Build = $build
}
}
Write-Warning "Could not parse version information from ipsw.me"
throw "Version pattern not found"
}
catch {
Write-Warning "Error getting latest version: $_"
Write-Host "Using fallback version detection..."
# Fallback to using highest version from input file
$content = Get-Content $InputFile -Raw
$serverBlocks = $content -split '=== ' | Where-Object { $_ -ne '' }
$highestVersion = "0.0.0"
$highestBuild = ""
foreach ($block in $serverBlocks) {
if ($block -match 'ProductVersion:\s*(\d+\.\d+(?:\.\d+)?)\s+BuildVersion:\s*(\w+)') {
$version = $Matches[1]
$build = $Matches[2]
if ([version]$version -gt [version]$highestVersion) {
$highestVersion = $version
$highestBuild = $build
}
}
}
Write-Host "Using highest detected version from input file: $highestVersion ($highestBuild)"
return @{
Version = $highestVersion
Build = $highestBuild
}
}
}
# Enhanced function to parse macOS version string into comparable version object
function ConvertTo-VersionObject {
param([string]$versionString)
if ($versionString -match '(\d+)\.(\d+)(?:\.(\d+))?') {
return [PSCustomObject]@{
Major = [int]$Matches[1]
Minor = [int]$Matches[2]
Patch = if ($Matches[3]) { [int]$Matches[3] } else { 0 }
FullVersion = $versionString
}
}
}
# Function to compare versions
function Compare-MacOSVersions {
param(
[PSCustomObject]$version1,
[PSCustomObject]$version2
)
if ($version1.Major -ne $version2.Major) {
return $version1.Major - $version2.Major
}
if ($version1.Minor -ne $version2.Minor) {
return $version1.Minor - $version2.Minor
}
return $version1.Patch - $version2.Patch
}
# Get the latest version info
$latestInfo = Get-LatestMacOSVersion
Write-Host "Latest macOS version: $($latestInfo.Version) (Build: $($latestInfo.Build))"
# Read and parse the input file
$content = Get-Content $InputFile -Raw
$serverBlocks = $content -split '=== ' | Where-Object { $_ -ne '' }
$serverData = @()
foreach ($block in $serverBlocks) {
if ($block -match '([^\s]+)\s+\(([^\)]+)\)[^\n]*\n.*ProductVersion:\s*(\d+\.\d+(?:\.\d+)?)\s+BuildVersion:\s*(\w+)') {
$serverData += [PSCustomObject]@{
Name = $Matches[1]
IP = $Matches[2]
ProductVersion = $Matches[3].Trim()
BuildVersion = $Matches[4].Trim()
VersionObj = ConvertTo-VersionObject($Matches[3].Trim())
}
}
}
# Use the fetched latest version
$latestVersion = ConvertTo-VersionObject($latestInfo.Version)
# Generate HTML
$html = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MacOS Version Health Dashboard</title>
<style>
:root {
--health-good: #22c55e;
--health-warning: #f59e0b;
--health-critical: #ef4444;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
margin: 0;
padding: 2rem;
background: #f8fafc;
color: #1e293b;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 1.875rem;
font-weight: 600;
color: #0f172a;
margin-bottom: 0.5rem;
}
.summary {
background: white;
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.card {
background: white;
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.card h2 {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 1rem 0;
color: #0f172a;
}
.status {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-weight: 500;
font-size: 0.875rem;
}
.status-good {
background: color-mix(in srgb, var(--health-good) 15%, transparent);
color: var(--health-good);
}
.status-warning {
background: color-mix(in srgb, var(--health-warning) 15%, transparent);
color: var(--health-warning);
}
.status-critical {
background: color-mix(in srgb, var(--health-critical) 15%, transparent);
color: var(--health-critical);
}
.details {
margin-top: 1rem;
font-size: 0.875rem;
color: #64748b;
}
.details p {
margin: 0.25rem 0;
}
.timestamp {
margin-top: 2rem;
text-align: right;
font-size: 0.875rem;
color: #64748b;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>MacOS Version Health Dashboard</h1>
<p>Current macOS version: $($latestInfo.Version) (Build: $($latestInfo.Build))</p>
</div>
<div class="summary">
<h2>System Overview</h2>
<p>Total systems: $($serverData.Count)</p>
<p>Systems on latest version: $($serverData.Where({ Compare-MacOSVersions $_.VersionObj $latestVersion -eq 0 }).Count)</p>
<p>Systems requiring updates: $($serverData.Where({ Compare-MacOSVersions $_.VersionObj $latestVersion -lt 0 }).Count)</p>
</div>
<div class="grid">
"@
foreach ($server in $serverData) {
$versionDiff = Compare-MacOSVersions $latestVersion $server.VersionObj
$status = if ($versionDiff -eq 0) {
@{ class = "status-good"; text = "Current" }
} elseif ($versionDiff -le 2) {
@{ class = "status-warning"; text = "Update Available" }
} else {
@{ class = "status-critical"; text = "Multiple Versions Behind" }
}
$html += @"
<div class="card">
<h2>$($server.Name)</h2>
<span class="status $($status.class)">$($status.text)</span>
<div class="details">
<p>IP Address: $($server.IP)</p>
<p>macOS Version: $($server.ProductVersion)</p>
<p>Build: $($server.BuildVersion)</p>
</div>
</div>
"@
}
$html += @"
</div>
<div class="timestamp">
Last updated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
</div>
</div>
</body>
</html>
"@
# Save the HTML file
$html | Out-File -FilePath $OutputFile -Encoding UTF8
Write-Host "Dashboard has been generated at: $OutputFile"