Last active
January 9, 2026 15:01
-
-
Save tcartwright/639aefbee39aa30044fec669c60082ce to your computer and use it in GitHub Desktop.
AZURE YML: Windows On Premise Build Server Agent Report
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| trigger: none | |
| variables: | |
| cleanPoolName: ${{ replace(replace(replace(parameters.agentPool, '/', '_'), '\', '_'), ':', '_') }} | |
| cleanAgentName: ${{ replace(replace(replace(replace(parameters.agentName, '*', 'ANY'), '/', '_'), '\', '_'), ':', '_') }} | |
| reportFile: $(Build.ArtifactStagingDirectory)\BuildServerReport.txt | |
| name: 'BuildServerReport_${{ variables.cleanPoolName }}_${{ variables.cleanAgentName }}' | |
| parameters: | |
| - name: agentPool | |
| displayName: 'Select Agent Pool' | |
| type: string | |
| default: 'Default' | |
| values: | |
| # Add your on-premise pools below | |
| - 'Default' | |
| - name: agentName | |
| displayName: 'Agent Name (optional, use star "*" for any)' | |
| type: string | |
| default: '*' | |
| stages: | |
| - stage: BuildServerReport | |
| displayName: 'Report: ${{ parameters.agentPool }} / ${{ parameters.agentName }}' | |
| jobs: | |
| - job: CollectInfo | |
| displayName: 'Collect Server Information' | |
| pool: | |
| name: ${{ parameters.agentPool }} | |
| ${{ if ne(parameters.agentName, '*') }}: | |
| demands: | |
| - Agent.Name -equals ${{ parameters.agentName }} | |
| steps: | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| & { | |
| $divider | |
| "BUILD SERVER INFORMATION REPORT" | |
| $divider | |
| "{0,-25} {1}" -f "Generated:", (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | |
| "{0,-25} {1}" -f "Pool:", '${{ parameters.agentPool }}' | |
| "{0,-25} {1}" -f "Agent:", $env:AGENT_NAME | |
| "" | |
| $subDivider | |
| "REPORT PARAMETERS" | |
| $subDivider | |
| "{0,-25} {1}" -f "Pool:", '${{ parameters.agentPool }}' | |
| "{0,-25} {1}" -f "Agent:", '${{ parameters.agentName }}' | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" | |
| displayName: 'Report Header' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| & { | |
| $divider | |
| "OPERATING SYSTEM" | |
| $divider | |
| $os = Get-CimInstance Win32_OperatingSystem | |
| $cs = Get-CimInstance Win32_ComputerSystem | |
| $procs = Get-CimInstance Win32_Processor | |
| "{0,-25} {1}" -f "Computer Name:", $os.CSName | |
| "{0,-25} {1}" -f "OS Name:", $os.Caption | |
| "{0,-25} {1}" -f "OS Version:", $os.Version | |
| "{0,-25} {1}" -f "OS Build:", $os.BuildNumber | |
| "{0,-25} {1}" -f "OS Architecture:", $os.OSArchitecture | |
| "{0,-25} {1}" -f "Install Date:", $os.InstallDate | |
| "" | |
| $subDivider | |
| "HARDWARE" | |
| $subDivider | |
| "{0,-25} {1}" -f "Manufacturer:", $cs.Manufacturer | |
| "{0,-25} {1}" -f "Model:", $cs.Model | |
| "{0,-25} {1}" -f "Total RAM:", "$([math]::Round($cs.TotalPhysicalMemory / 1GB, 2)) GB" | |
| "{0,-25} {1}" -f "Free RAM:", "$([math]::Round($os.FreePhysicalMemory / 1MB, 2)) GB" | |
| "{0,-25} {1}" -f "Logical Processors:", $cs.NumberOfLogicalProcessors | |
| "" | |
| $subDivider | |
| "PROCESSORS" | |
| $subDivider | |
| $i = 1 | |
| foreach ($proc in $procs) { | |
| "{0,-25} {1}" -f "Processor $i`:", $proc.Name | |
| "{0,-25} {1}" -f " Cores:", $proc.NumberOfCores | |
| "{0,-25} {1}" -f " Logical Processors:", $proc.NumberOfLogicalProcessors | |
| "{0,-25} {1}" -f " Max Clock Speed:", "$($proc.MaxClockSpeed) MHz" | |
| $i++ | |
| } | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: 'OS Information' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| $output = & { | |
| $divider | |
| "SYSTEM UPTIME" | |
| $divider | |
| $os = Get-CimInstance Win32_OperatingSystem | |
| $lastBoot = $os.LastBootUpTime | |
| $uptime = (Get-Date) - $lastBoot | |
| "{0,-25} {1:yyyy-MM-dd HH:mm:ss}" -f "Last Reboot:", $lastBoot | |
| "{0,-25} {1} days, {2} hours, {3} minutes" -f "Uptime:", $uptime.Days, $uptime.Hours, $uptime.Minutes | |
| "" | |
| if ($uptime.Days -gt 30) { | |
| "WARNING: Server has not been rebooted in over 30 days ($($uptime.Days) days)" | |
| } | |
| "" | |
| } | |
| $output | Tee-Object -FilePath "$(reportFile)" -Append | |
| # Azure warning separately so it shows in pipeline UI | |
| $os = Get-CimInstance Win32_OperatingSystem | |
| $uptime = (Get-Date) - $os.LastBootUpTime | |
| if ($uptime.Days -gt 30) { | |
| Write-Host "##[warning]Server has not been rebooted in over 30 days ($($uptime.Days) days)" | |
| } | |
| displayName: 'System Uptime' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| & { | |
| $divider | |
| "SERVICE PACKS & HOTFIXES" | |
| $divider | |
| $os = Get-CimInstance Win32_OperatingSystem | |
| "{0,-25} {1}" -f "Service Pack:", $(if ($os.ServicePackMajorVersion -gt 0) { "SP$($os.ServicePackMajorVersion)" } else { "None" }) | |
| "{0,-25} {1}" -f "OS Build:", $os.BuildNumber | |
| "{0,-25} {1}" -f "OS Version:", $os.Version | |
| "" | |
| $ubr = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).UBR | |
| if ($ubr) { | |
| "{0,-25} {1}.{2}" -f "Full Build:", $os.BuildNumber, $ubr | |
| } | |
| $releaseId = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).ReleaseId | |
| $displayVersion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).DisplayVersion | |
| if ($displayVersion) { | |
| "{0,-25} {1}" -f "Display Version:", $displayVersion | |
| } elseif ($releaseId) { | |
| "{0,-25} {1}" -f "Release ID:", $releaseId | |
| } | |
| "" | |
| $subDivider | |
| "INSTALLED HOTFIXES" | |
| $subDivider | |
| $hotfixes = Get-HotFix -ErrorAction SilentlyContinue | | |
| Sort-Object { if ($_.InstalledOn) { $_.InstalledOn } else { [DateTime]::MinValue } } -Descending | |
| "{0,-25} {1}" -f "Total Hotfixes:", $hotfixes.Count | |
| "" | |
| "{0,-15} {1,-40} {2}" -f "HotFixID", "Description", "Installed On" | |
| "{0,-15} {1,-40} {2}" -f "--------", "-----------", "------------" | |
| foreach ($hf in $hotfixes | Select-Object -First 25) { | |
| $installedDate = if ($hf.InstalledOn) { $hf.InstalledOn.ToString("yyyy-MM-dd") } else { "Unknown" } | |
| "{0,-15} {1,-40} {2}" -f $hf.HotFixID, $hf.Description, $installedDate | |
| } | |
| if ($hotfixes.Count -gt 25) { | |
| "" | |
| "... and $($hotfixes.Count - 25) more (showing most recent 25)" | |
| } | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: 'Service Packs & Hotfixes' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| & { | |
| $divider | |
| "NETWORK / IP INFORMATION" | |
| $divider | |
| "{0,-25} {1}" -f "Hostname:", $env:COMPUTERNAME | |
| "{0,-25} {1}" -f "FQDN:", ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName) | |
| "" | |
| $subDivider | |
| "NETWORK ADAPTERS" | |
| $subDivider | |
| $adapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | |
| foreach ($adapter in $adapters) { | |
| "" | |
| "{0,-25} {1}" -f "Adapter:", $adapter.Name | |
| "{0,-25} {1}" -f " Description:", $adapter.InterfaceDescription | |
| "{0,-25} {1}" -f " MAC Address:", $adapter.MacAddress | |
| "{0,-25} {1}" -f " Link Speed:", $adapter.LinkSpeed | |
| "{0,-25} {1}" -f " Status:", $adapter.Status | |
| $ipConfig = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -ErrorAction SilentlyContinue | |
| $ipv4 = $ipConfig | Where-Object { $_.AddressFamily -eq 'IPv4' } | |
| $ipv6 = $ipConfig | Where-Object { $_.AddressFamily -eq 'IPv6' } | |
| foreach ($ip in $ipv4) { | |
| "{0,-25} {1}/{2}" -f " IPv4 Address:", $ip.IPAddress, $ip.PrefixLength | |
| } | |
| foreach ($ip in $ipv6) { | |
| if ($ip.IPAddress -notlike 'fe80*') { | |
| "{0,-25} {1}/{2}" -f " IPv6 Address:", $ip.IPAddress, $ip.PrefixLength | |
| } | |
| } | |
| $gateway = Get-NetRoute -InterfaceIndex $adapter.ifIndex -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | |
| if ($gateway) { | |
| "{0,-25} {1}" -f " Default Gateway:", $gateway.NextHop | |
| } | |
| } | |
| "" | |
| $subDivider | |
| "DNS CONFIGURATION" | |
| $subDivider | |
| $dnsServers = Get-DnsClientServerAddress | Where-Object { $_.ServerAddresses -and $_.AddressFamily -eq 2 } | |
| $uniqueDns = $dnsServers.ServerAddresses | Select-Object -Unique | |
| $i = 1 | |
| foreach ($dns in $uniqueDns) { | |
| "{0,-25} {1}" -f "DNS Server $i`:", $dns | |
| $i++ | |
| } | |
| "" | |
| $subDivider | |
| "IP ADDRESS SUMMARY" | |
| $subDivider | |
| "{0,-20} {1,-18} {2,-10} {3}" -f "Adapter", "IP Address", "Type", "Prefix" | |
| "{0,-20} {1,-18} {2,-10} {3}" -f "-------", "----------", "----", "------" | |
| Get-NetIPAddress | Where-Object { | |
| $_.AddressState -ieq 'Preferred' -and | |
| $_.IPAddress -notlike '127.*' -and | |
| $_.IPAddress -inotlike 'fe80*' -and | |
| $_.IPAddress -ne '::1' | |
| } | ForEach-Object { | |
| $adapterName = (Get-NetAdapter -InterfaceIndex $_.InterfaceIndex -ErrorAction SilentlyContinue).Name | |
| if (-not $adapterName) { $adapterName = "Unknown" } | |
| $adapterName = if ($adapterName.Length -gt 18) { $adapterName.Substring(0,15) + "..." } else { $adapterName } | |
| "{0,-20} {1,-18} {2,-10} {3}" -f $adapterName, $_.IPAddress, $_.AddressFamily, "/$($_.PrefixLength)" | |
| } | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: 'Network / IP Information' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | |
| $warnings = @() | |
| $output = & { | |
| $divider | |
| "AZURE & DEVOPS CONNECTIVITY" | |
| $divider | |
| $endpoints = @( | |
| @{Name="Azure DevOps"; Url="https://dev.azure.com"; Port=443}, | |
| @{Name="Azure DevOps (vsrm)"; Url="https://vsrm.dev.azure.com"; Port=443}, | |
| @{Name="Azure Artifacts"; Url="https://pkgs.dev.azure.com"; Port=443}, | |
| @{Name="Azure Portal"; Url="https://portal.azure.com"; Port=443}, | |
| @{Name="Azure Management API"; Url="https://management.azure.com"; Port=443}, | |
| @{Name="Azure AD / Entra"; Url="https://login.microsoftonline.com"; Port=443}, | |
| @{Name="Azure Blob Storage"; Url="https://blob.core.windows.net"; Port=443}, | |
| @{Name="Azure Key Vault"; Url="https://vault.azure.net"; Port=443}, | |
| @{Name="NuGet Gallery"; Url="https://api.nuget.org"; Port=443}, | |
| @{Name="GitHub"; Url="https://github.com"; Port=443}, | |
| @{Name="VS Marketplace"; Url="https://marketplace.visualstudio.com"; Port=443} | |
| ) | |
| "{0,-30} {1,-12} {2,-10} {3}" -f "Endpoint", "Status", "Time (ms)", "Details" | |
| "{0,-30} {1,-12} {2,-10} {3}" -f "--------", "------", "---------", "-------" | |
| foreach ($ep in $endpoints) { | |
| $uri = [System.Uri]$ep.Url | |
| $host_ = $uri.Host | |
| try { | |
| $sw = [System.Diagnostics.Stopwatch]::StartNew() | |
| $tcp = New-Object System.Net.Sockets.TcpClient | |
| $connect = $tcp.BeginConnect($host_, $ep.Port, $null, $null) | |
| $wait = $connect.AsyncWaitHandle.WaitOne(5000, $false) | |
| $sw.Stop() | |
| if ($wait -and $tcp.Connected) { | |
| $tcp.EndConnect($connect) | |
| $tcp.Close() | |
| "{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "OK", $sw.ElapsedMilliseconds, $host_ | |
| } else { | |
| $tcp.Close() | |
| "{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "TIMEOUT", "-", $host_ | |
| $script:warnings += "Connection timeout to $($ep.Name) ($host_)" | |
| } | |
| } catch { | |
| "{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "FAILED", "-", $_.Exception.Message | |
| $script:warnings += "Failed to connect to $($ep.Name) ($host_`:$($ep.Port)): $($_.Exception.Message)" | |
| } | |
| } | |
| "" | |
| $subDivider | |
| "HTTPS ENDPOINT VALIDATION" | |
| $subDivider | |
| "{0,-30} {1,-12} {2,-10} {3}" -f "Endpoint", "Status", "HTTP Code", "Details" | |
| "{0,-30} {1,-12} {2,-10} {3}" -f "--------", "------", "---------", "-------" | |
| $httpEndpoints = @( | |
| @{Name="Azure DevOps API"; Url="https://dev.azure.com"}, | |
| @{Name="Azure Status"; Url="https://status.azure.com/en-us/status"}, | |
| @{Name="NuGet API"; Url="https://api.nuget.org/v3/index.json"}, | |
| @{Name="Azure Pipelines Agent"; Url="https://download.agent.dev.azure.com"} | |
| ) | |
| foreach ($ep in $httpEndpoints) { | |
| try { | |
| $sw = [System.Diagnostics.Stopwatch]::StartNew() | |
| $response = Invoke-WebRequest -Uri $ep.Url -Method Get -UseBasicParsing -TimeoutSec 10 -MaximumRedirection 5 -ErrorAction Stop | |
| $sw.Stop() | |
| "{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "OK", $response.StatusCode, "$($sw.ElapsedMilliseconds)ms" | |
| } catch { | |
| $statusCode = $null | |
| if ($_.Exception.Response) { | |
| $statusCode = [int]$_.Exception.Response.StatusCode | |
| } | |
| if ($statusCode) { | |
| if ($statusCode -in 400, 401, 403, 404) { | |
| "{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "Reachable", $statusCode, "$($statusCode) (auth/not found - expected)" | |
| } else { | |
| "{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "HTTP Error", $statusCode, $_.Exception.Message | |
| $script:warnings += "HTTP request failed for $($ep.Name) ($($ep.Url)) - Status: $statusCode" | |
| } | |
| } else { | |
| "{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "FAILED", "-", $_.Exception.Message | |
| $script:warnings += "Request failed for $($ep.Name) ($($ep.Url)): $($_.Exception.Message)" | |
| } | |
| } | |
| } | |
| "" | |
| $subDivider | |
| "DNS RESOLUTION TEST" | |
| $subDivider | |
| "{0,-35} {1,-12} {2}" -f "Hostname", "Status", "Resolved IP(s)" | |
| "{0,-35} {1,-12} {2}" -f "--------", "------", "--------------" | |
| $dnsHosts = @( | |
| "dev.azure.com", | |
| "management.azure.com", | |
| "login.microsoftonline.com", | |
| "api.nuget.org", | |
| "github.com" | |
| ) | |
| foreach ($h in $dnsHosts) { | |
| try { | |
| $resolved = [System.Net.Dns]::GetHostAddresses($h) | |
| $ips = ($resolved | Select-Object -First 3 | ForEach-Object { $_.IPAddressToString }) -join ", " | |
| if ($resolved.Count -gt 3) { $ips += " ..." } | |
| "{0,-35} {1,-12} {2}" -f $h, "OK", $ips | |
| } catch { | |
| "{0,-35} {1,-12} {2}" -f $h, "FAILED", $_.Exception.Message | |
| $script:warnings += "DNS resolution failed for $h" | |
| } | |
| } | |
| "" | |
| } | |
| $output | Tee-Object -FilePath "$(reportFile)" -Append | |
| # Emit Azure warnings separately | |
| foreach ($w in $warnings) { | |
| Write-Host "##[warning]$w" | |
| } | |
| displayName: 'Azure & DevOps Connectivity' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $warnings = @() | |
| $output = & { | |
| $divider | |
| "DISK SPACE" | |
| $divider | |
| "{0,-10} {1,15} {2,15} {3,15} {4,10}" -f "Drive", "Total (GB)", "Used (GB)", "Free (GB)", "Free %" | |
| "{0,-10} {1,15} {2,15} {3,15} {4,10}" -f "-----", "----------", "---------", "---------", "------" | |
| Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object { | |
| $total = [math]::Round($_.Size / 1GB, 2) | |
| $free = [math]::Round($_.FreeSpace / 1GB, 2) | |
| $used = [math]::Round($total - $free, 2) | |
| $pct = [math]::Round(($free / $total) * 100, 1) | |
| "{0,-10} {1,15} {2,15} {3,15} {4,10}" -f $_.DeviceID, $total, $used, $free, "$pct%" | |
| if ($pct -lt 10) { | |
| $script:warnings += "Low disk space on $($_.DeviceID) - only $pct% free" | |
| } | |
| } | |
| "" | |
| } | |
| $output | Tee-Object -FilePath "$(reportFile)" -Append | |
| foreach ($w in $warnings) { | |
| Write-Host "##[warning]$w" | |
| } | |
| displayName: 'Disk Space' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| & { | |
| $divider | |
| ".NET FRAMEWORK" | |
| $divider | |
| "{0,-30} {1,-20} {2}" -f "Component", "Version", "Release" | |
| "{0,-30} {1,-20} {2}" -f "---------", "-------", "-------" | |
| $netFwPath = 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' | |
| if (Test-Path $netFwPath) { | |
| Get-ChildItem $netFwPath -Recurse | Get-ItemProperty -Name Version, Release -ErrorAction SilentlyContinue | | |
| Where-Object { $_.PSChildName -match '^(?!S)\p{L}'} | | |
| Select-Object @{n='Component';e={$_.PSChildName}}, Version, Release | | |
| Sort-Object Component -Unique | | |
| ForEach-Object { | |
| $rel = if ($_.Release) { $_.Release } else { "-" } | |
| "{0,-30} {1,-20} {2}" -f $_.Component, $_.Version, $rel | |
| } | |
| } | |
| "" | |
| $subDivider | |
| ".NET FRAMEWORK 4.5+ DETECTION" | |
| $subDivider | |
| $release = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -ErrorAction SilentlyContinue).Release | |
| if ($release) { | |
| $version = switch ($release) { | |
| { $_ -ge 533320 } { "4.8.1 or later"; break } | |
| { $_ -ge 528040 } { "4.8"; break } | |
| { $_ -ge 461808 } { "4.7.2"; break } | |
| { $_ -ge 461308 } { "4.7.1"; break } | |
| { $_ -ge 460798 } { "4.7"; break } | |
| { $_ -ge 394802 } { "4.6.2"; break } | |
| { $_ -ge 394254 } { "4.6.1"; break } | |
| { $_ -ge 393295 } { "4.6"; break } | |
| { $_ -ge 379893 } { "4.5.2"; break } | |
| { $_ -ge 378675 } { "4.5.1"; break } | |
| { $_ -ge 378389 } { "4.5"; break } | |
| default { "Unknown" } | |
| } | |
| "{0,-25} {1}" -f "Detected Version:", $version | |
| "{0,-25} {1}" -f "Release Number:", $release | |
| } | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: '.NET Framework Versions' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| & { | |
| $divider | |
| ".NET CORE / .NET SDKs" | |
| $divider | |
| $dotnetPath = Get-Command dotnet -ErrorAction SilentlyContinue | |
| if ($dotnetPath) { | |
| "{0,-25} {1}" -f "CLI Location:", $dotnetPath.Source | |
| "" | |
| $subDivider | |
| "INSTALLED SDKs" | |
| $subDivider | |
| "{0,-20} {1}" -f "Version", "Location" | |
| "{0,-20} {1}" -f "-------", "--------" | |
| dotnet --list-sdks | ForEach-Object { | |
| if ($_ -match '^(\S+)\s+\[(.+)\]$') { | |
| "{0,-20} {1}" -f $matches[1], $matches[2] | |
| } | |
| } | |
| "" | |
| $subDivider | |
| "INSTALLED RUNTIMES" | |
| $subDivider | |
| "{0,-35} {1,-15} {2}" -f "Runtime", "Version", "Location" | |
| "{0,-35} {1,-15} {2}" -f "-------", "-------", "--------" | |
| dotnet --list-runtimes | ForEach-Object { | |
| if ($_ -match '^(\S+)\s+(\S+)\s+\[(.+)\]$') { | |
| "{0,-35} {1,-15} {2}" -f $matches[1], $matches[2], $matches[3] | |
| } | |
| } | |
| } else { | |
| "dotnet CLI not found in PATH" | |
| } | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: '.NET Core/SDK Information' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| & { | |
| $divider | |
| "INSTALLED APPLICATIONS" | |
| $divider | |
| "{0,-50} {1,-20} {2}" -f "Name", "Version", "Publisher" | |
| "{0,-50} {1,-20} {2}" -f "----", "-------", "---------" | |
| $apps = @() | |
| $regPaths = @( | |
| 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', | |
| 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' | |
| ) | |
| foreach ($path in $regPaths) { | |
| if (Test-Path $path) { | |
| $apps += Get-ItemProperty $path -ErrorAction SilentlyContinue | | |
| Where-Object { $_.DisplayName -and $_.DisplayName.Trim() } | | |
| Select-Object DisplayName, DisplayVersion, Publisher | |
| } | |
| } | |
| $apps | Sort-Object DisplayName -Unique | ForEach-Object { | |
| $name = if ($_.DisplayName.Length -gt 48) { $_.DisplayName.Substring(0,45) + "..." } else { $_.DisplayName } | |
| $ver = if ($_.DisplayVersion) { $_.DisplayVersion } else { "-" } | |
| $ver = if ($ver.Length -gt 18) { $ver.Substring(0,15) + "..." } else { $ver } | |
| $pub = if ($_.Publisher) { $_.Publisher } else { "-" } | |
| $pub = if ($pub.Length -gt 30) { $pub.Substring(0,27) + "..." } else { $pub } | |
| "{0,-50} {1,-20} {2}" -f $name, $ver, $pub | |
| } | |
| "" | |
| "{0,-25} {1}" -f "Total Applications:", $apps.Count | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: 'Installed Applications' | |
| - powershell: | | |
| $ErrorActionPreference = 'Continue' | |
| $divider = "=" * 80 | |
| & { | |
| $divider | |
| "DEVELOPMENT TOOLS" | |
| $divider | |
| "{0,-20} {1,-15} {2}" -f "Tool", "Status", "Version" | |
| "{0,-20} {1,-15} {2}" -f "----", "------", "-------" | |
| $tools = @( | |
| @{Name="Git"; Cmd="git"; Args=@("--version")}, | |
| @{Name="Node.js"; Cmd="node"; Args=@("--version")}, | |
| @{Name="npm"; Cmd="npm"; Args=@("--version")}, | |
| @{Name="Python"; Cmd="python"; Args=@("--version")}, | |
| @{Name="Java"; Cmd="java"; Args=@("-version")}, | |
| @{Name="Maven"; Cmd="mvn"; Args=@("--version")}, | |
| @{Name="NuGet"; Cmd="nuget"; Args=@("help")}, | |
| @{Name="Azure CLI"; Cmd="az"; Args=@("--version")}, | |
| @{Name="PowerShell"; Cmd="powershell"; Args=@("-Command", "`$PSVersionTable.PSVersion.ToString()")} | |
| @{Name="PowerShell Core"; Cmd="pwsh"; Args=@("--version")}, | |
| @{Name="Docker"; Cmd="docker"; Args=@("--version")} | |
| ) | |
| foreach ($tool in $tools) { | |
| $cmds = @(Get-Command $tool.Cmd -ErrorAction SilentlyContinue) | |
| $versions = @() | |
| foreach ($cmd in $cmds) { | |
| try { | |
| $output = & $cmd.Source $tool.Args 2>&1 | |
| $outStr = $output | Out-String | |
| if ($LASTEXITCODE -eq 0 -and $outStr -inotmatch "was not found|Microsoft Store") { | |
| $ver = ($outStr.Split("`n") | Where-Object { $_.Trim() } | Select-Object -First 1).Trim() | |
| $versions += @{Version=$ver; Path=$cmd.Source} | |
| } | |
| } catch { | |
| continue | |
| } | |
| } | |
| if ($versions.Count -eq 0) { | |
| "{0,-20} {1,-15} {2}" -f $tool.Name, "Not Installed", "-" | |
| } elseif ($versions.Count -eq 1) { | |
| "{0,-20} {1,-15} {2}" -f $tool.Name, "Installed", $versions[0].Version | |
| } else { | |
| "{0,-20} {1,-15} {2}" -f $tool.Name, "Installed", "$($versions.Count) versions" | |
| foreach ($v in $versions) { | |
| "{0,-20} {1,-15} {2}" -f "", "", $v.Version | |
| "{0,-20} {1,-15} -> {2}" -f "", "", $v.Path | |
| } | |
| } | |
| } | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: 'Development Tools' | |
| continueOnError: true | |
| - powershell: | | |
| $divider = "=" * 80 | |
| & { | |
| $divider | |
| "VISUAL STUDIO INSTALLATIONS" | |
| $divider | |
| $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" | |
| if (Test-Path $vsWhere) { | |
| "{0,-25} {1,-20} {2,-15} {3}" -f "Product", "Version", "Channel", "Path" | |
| "{0,-25} {1,-20} {2,-15} {3}" -f "-------", "-------", "-------", "----" | |
| $instances = & $vsWhere -all -format json | ConvertFrom-Json | |
| foreach ($vs in $instances) { | |
| $channel = ($vs.channelId -split '\.')[-1] | |
| "{0,-25} {1,-20} {2,-15} {3}" -f $vs.displayName, $vs.installationVersion, $channel, $vs.installationPath | |
| } | |
| } else { | |
| "vswhere.exe not found - Visual Studio may not be installed" | |
| } | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: 'Visual Studio Info' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| & { | |
| $divider | |
| "MSBUILD INSTALLATIONS" | |
| $divider | |
| "{0,-15} {1,-25} {2}" -f "Version", "VS Product", "Path" | |
| "{0,-15} {1,-25} {2}" -f "-------", "----------", "----" | |
| $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" | |
| if (Test-Path $vsWhere) { | |
| $msbuildPaths = & $vsWhere -all -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe | |
| foreach ($msbuildPath in $msbuildPaths) { | |
| if (Test-Path $msbuildPath) { | |
| $version = (Get-Item $msbuildPath).VersionInfo.ProductVersion | |
| $vsProduct = if ($msbuildPath -match 'Visual Studio\\(\d{4})\\(\w+)\\') { "$($matches[1]) $($matches[2])" } else { "-" } | |
| "{0,-15} {1,-25} {2}" -f $version, $vsProduct, $msbuildPath | |
| } | |
| } | |
| } | |
| $buildToolsPaths = @( | |
| "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe", | |
| "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe", | |
| "${env:ProgramFiles(x86)}\MSBuild\14.0\Bin\MSBuild.exe", | |
| "${env:ProgramFiles(x86)}\MSBuild\12.0\Bin\MSBuild.exe" | |
| ) | |
| "" | |
| $subDivider | |
| "STANDALONE / LEGACY MSBUILD" | |
| $subDivider | |
| "{0,-15} {1,-25} {2}" -f "Version", "Type", "Path" | |
| "{0,-15} {1,-25} {2}" -f "-------", "----", "----" | |
| foreach ($path in $buildToolsPaths) { | |
| if (Test-Path $path) { | |
| $version = (Get-Item $path).VersionInfo.ProductVersion | |
| $type = if ($path -match 'BuildTools') { "Build Tools" } else { "Standalone" } | |
| "{0,-15} {1,-25} {2}" -f $version, $type, $path | |
| } | |
| } | |
| "" | |
| $subDivider | |
| "MSBUILD IN PATH" | |
| $subDivider | |
| $msbuildInPath = Get-Command msbuild -ErrorAction SilentlyContinue | |
| if ($msbuildInPath) { | |
| $version = & msbuild -version | Select-Object -Last 1 | |
| "{0,-25} {1}" -f "Version:", $version | |
| "{0,-25} {1}" -f "Location:", $msbuildInPath.Source | |
| } else { | |
| "MSBuild not found in PATH" | |
| } | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: 'MSBuild Information' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| $subDivider = "-" * 60 | |
| & { | |
| $divider | |
| "BUILD AGENT IDENTITY" | |
| $divider | |
| $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() | |
| $principal = New-Object System.Security.Principal.WindowsPrincipal($identity) | |
| "{0,-25} {1}" -f "Username:", $identity.Name | |
| "{0,-25} {1}" -f "User SID:", $identity.User.Value | |
| "{0,-25} {1}" -f "Auth Type:", $identity.AuthenticationType | |
| "{0,-25} {1}" -f "Is System:", $identity.IsSystem | |
| "{0,-25} {1}" -f "Is Admin:", $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) | |
| "" | |
| $subDivider | |
| "GROUP MEMBERSHIPS" | |
| $subDivider | |
| $identity.Groups | ForEach-Object { | |
| try { | |
| $group = $_.Translate([System.Security.Principal.NTAccount]) | |
| " - {0}" -f $group.Value | |
| } catch { | |
| " - {0} (unresolved)" -f $_.Value | |
| } | |
| } | |
| "" | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: 'Build Agent Identity' | |
| - powershell: | | |
| $divider = "=" * 80 | |
| & { | |
| $divider | |
| "AZURE DEVOPS AGENT VARIABLES" | |
| $divider | |
| "{0,-40} {1}" -f "Variable", "Value" | |
| "{0,-40} {1}" -f "--------", "-----" | |
| Get-ChildItem env: | Where-Object { | |
| $_.Name -like 'AGENT_*' | |
| } | Sort-Object Name | ForEach-Object { | |
| "{0,-40} {1}" -f $_.Name, $_.Value | |
| } | |
| "" | |
| $divider | |
| "BUILD VARIABLES" | |
| $divider | |
| "{0,-40} {1}" -f "Variable", "Value" | |
| "{0,-40} {1}" -f "--------", "-----" | |
| Get-ChildItem env: | Where-Object { | |
| $_.Name -like 'BUILD_*' | |
| } | Sort-Object Name | ForEach-Object { | |
| "{0,-40} {1}" -f $_.Name, $_.Value | |
| } | |
| "" | |
| $divider | |
| "END OF REPORT" | |
| $divider | |
| } | Tee-Object -FilePath "$(reportFile)" -Append | |
| displayName: 'Environment Variables' | |
| - task: PublishPipelineArtifact@1 | |
| inputs: | |
| targetPath: '$(reportFile)' | |
| artifact: 'BuildServerReport_${{ variables.cleanPoolName }}_${{ variables.cleanAgentName }}' | |
| publishLocation: 'pipeline' | |
| displayName: 'Publish Report Artifact' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #Requires -Version 5.1 | |
| <# | |
| .SYNOPSIS | |
| Generates a comprehensive server information report in JSON format. | |
| .DESCRIPTION | |
| Collects system information including OS, hardware, network, installed software, | |
| and development tools. Outputs JSON to console and optionally to a file. | |
| .PARAMETER OutputPath | |
| Path to save the JSON report file. If not specified, outputs to console only. | |
| .PARAMETER ComputerName | |
| Remote computer to run against. If not specified, runs locally. | |
| .PARAMETER Credential | |
| Credentials for remote computer. Only used with -ComputerName. | |
| .PARAMETER Compress | |
| Output minified JSON instead of formatted. | |
| .EXAMPLE | |
| .\ServerReport-Json.ps1 | |
| .\ServerReport-Json.ps1 -OutputPath "C:\Reports\ServerReport.json" | |
| .\ServerReport-Json.ps1 -ComputerName "SERVER01" -Credential (Get-Credential) | |
| #> | |
| param( | |
| [string]$OutputPath = "$($env:TEMP)\ServerReport.txt", | |
| [string]$ComputerName, | |
| [PSCredential]$Credential, | |
| [switch]$Compress | |
| ) | |
| Clear-Host | |
| $ErrorActionPreference = 'Continue' | |
| # Build remote session params if needed | |
| $remoteParams = @{} | |
| if ($ComputerName) { | |
| $remoteParams['ComputerName'] = $ComputerName | |
| if ($Credential) { | |
| $remoteParams['Credential'] = $Credential | |
| } | |
| Write-Host "Connecting to remote computer: $ComputerName" -ForegroundColor Cyan | |
| } | |
| # Scriptblock containing all the report logic | |
| $reportScript = { | |
| $report = [ordered]@{} | |
| #region Report Metadata | |
| $report.Metadata = [ordered]@{ | |
| GeneratedAt = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ss') | |
| ServerName = $env:COMPUTERNAME | |
| ReportVersion = "1.0" | |
| } | |
| #endregion | |
| #region OS Information | |
| $os = Get-CimInstance Win32_OperatingSystem | |
| $cs = Get-CimInstance Win32_ComputerSystem | |
| $report.OperatingSystem = [ordered]@{ | |
| ComputerName = $os.CSName | |
| Name = $os.Caption | |
| Version = $os.Version | |
| BuildNumber = $os.BuildNumber | |
| Architecture = $os.OSArchitecture | |
| InstallDate = $os.InstallDate.ToString('yyyy-MM-ddTHH:mm:ss') | |
| } | |
| # Add UBR and display version | |
| $ubr = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).UBR | |
| if ($ubr) { | |
| $report.OperatingSystem.FullBuild = "$($os.BuildNumber).$ubr" | |
| } | |
| $displayVersion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).DisplayVersion | |
| if ($displayVersion) { | |
| $report.OperatingSystem.DisplayVersion = $displayVersion | |
| } | |
| #endregion | |
| #region Hardware | |
| $procs = Get-CimInstance Win32_Processor | |
| $report.Hardware = [ordered]@{ | |
| Manufacturer = $cs.Manufacturer | |
| Model = $cs.Model | |
| TotalRamGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 2) | |
| FreeRamGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2) | |
| LogicalProcessors = $cs.NumberOfLogicalProcessors | |
| Processors = @($procs | ForEach-Object { | |
| [ordered]@{ | |
| Name = $_.Name | |
| Cores = $_.NumberOfCores | |
| LogicalProcessors = $_.NumberOfLogicalProcessors | |
| MaxClockSpeedMHz = $_.MaxClockSpeed | |
| } | |
| }) | |
| } | |
| #endregion | |
| #region System Uptime | |
| $lastBoot = $os.LastBootUpTime | |
| $uptime = (Get-Date) - $lastBoot | |
| $report.Uptime = [ordered]@{ | |
| LastReboot = $lastBoot.ToString('yyyy-MM-ddTHH:mm:ss') | |
| UptimeDays = $uptime.Days | |
| UptimeHours = $uptime.Hours | |
| UptimeMinutes = $uptime.Minutes | |
| UptimeTotalHours = [math]::Round($uptime.TotalHours, 2) | |
| Warning = if ($uptime.Days -gt 30) { "Server has not been rebooted in over 30 days" } else { $null } | |
| } | |
| #endregion | |
| #region Hotfixes | |
| $hotfixes = Get-HotFix -ErrorAction SilentlyContinue | | |
| Sort-Object { if ($_.InstalledOn) { $_.InstalledOn } else { [DateTime]::MinValue } } -Descending | |
| $report.Hotfixes = [ordered]@{ | |
| TotalCount = $hotfixes.Count | |
| Items = @($hotfixes | Select-Object -First 50 | ForEach-Object { | |
| [ordered]@{ | |
| HotFixID = $_.HotFixID | |
| Description = $_.Description | |
| InstalledOn = if ($_.InstalledOn) { $_.InstalledOn.ToString('yyyy-MM-dd') } else { $null } | |
| InstalledBy = $_.InstalledBy | |
| } | |
| }) | |
| } | |
| #endregion | |
| #region Network | |
| $adapters = Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'Up' } | |
| $networkAdapters = @($adapters | ForEach-Object { | |
| $adapter = $_ | |
| $ipConfig = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -ErrorAction SilentlyContinue | |
| $gateway = Get-NetRoute -InterfaceIndex $adapter.ifIndex -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | |
| [ordered]@{ | |
| Name = $adapter.Name | |
| Description = $adapter.InterfaceDescription | |
| MacAddress = $adapter.MacAddress | |
| LinkSpeed = $adapter.LinkSpeed | |
| Status = $adapter.Status | |
| IPv4Addresses = @($ipConfig | Where-Object { $_.AddressFamily -eq 'IPv4' } | ForEach-Object { | |
| "$($_.IPAddress)/$($_.PrefixLength)" | |
| }) | |
| IPv6Addresses = @($ipConfig | Where-Object { $_.AddressFamily -eq 'IPv6' -and $_.IPAddress -notlike 'fe80*' } | ForEach-Object { | |
| "$($_.IPAddress)/$($_.PrefixLength)" | |
| }) | |
| DefaultGateway = if ($gateway) { $gateway.NextHop } else { $null } | |
| } | |
| }) | |
| $dnsServers = Get-DnsClientServerAddress -ErrorAction SilentlyContinue | | |
| Where-Object { $_.ServerAddresses -and $_.AddressFamily -eq 2 } | |
| $uniqueDns = @($dnsServers.ServerAddresses | Select-Object -Unique) | |
| $fqdn = try { ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName) } catch { $null } | |
| $report.Network = [ordered]@{ | |
| Hostname = $env:COMPUTERNAME | |
| FQDN = $fqdn | |
| DnsServers = $uniqueDns | |
| Adapters = $networkAdapters | |
| } | |
| #endregion | |
| #region Disk Space | |
| $disks = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | |
| $report.Disks = @($disks | ForEach-Object { | |
| $total = [math]::Round($_.Size / 1GB, 2) | |
| $free = [math]::Round($_.FreeSpace / 1GB, 2) | |
| $used = [math]::Round($total - $free, 2) | |
| $pct = [math]::Round(($free / $total) * 100, 1) | |
| [ordered]@{ | |
| Drive = $_.DeviceID | |
| VolumeName = $_.VolumeName | |
| TotalGB = $total | |
| UsedGB = $used | |
| FreeGB = $free | |
| FreePercent = $pct | |
| Warning = if ($pct -lt 10) { "Low disk space" } else { $null } | |
| } | |
| }) | |
| #endregion | |
| #region .NET Framework | |
| $netFwPath = 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' | |
| $frameworks = @() | |
| if (Test-Path $netFwPath) { | |
| $frameworks = @(Get-ChildItem $netFwPath -Recurse | | |
| Get-ItemProperty -Name Version, Release -ErrorAction SilentlyContinue | | |
| Where-Object { $_.PSChildName -match '^(?!S)\p{L}'} | | |
| Select-Object @{n='Component';e={$_.PSChildName}}, Version, Release | | |
| Sort-Object Component -Unique | | |
| ForEach-Object { | |
| [ordered]@{ | |
| Component = $_.Component | |
| Version = $_.Version | |
| Release = $_.Release | |
| } | |
| }) | |
| } | |
| $net4Release = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -ErrorAction SilentlyContinue).Release | |
| $net4Version = if ($net4Release) { | |
| switch ($net4Release) { | |
| { $_ -ge 533320 } { "4.8.1 or later"; break } | |
| { $_ -ge 528040 } { "4.8"; break } | |
| { $_ -ge 461808 } { "4.7.2"; break } | |
| { $_ -ge 461308 } { "4.7.1"; break } | |
| { $_ -ge 460798 } { "4.7"; break } | |
| { $_ -ge 394802 } { "4.6.2"; break } | |
| { $_ -ge 394254 } { "4.6.1"; break } | |
| { $_ -ge 393295 } { "4.6"; break } | |
| { $_ -ge 379893 } { "4.5.2"; break } | |
| { $_ -ge 378675 } { "4.5.1"; break } | |
| { $_ -ge 378389 } { "4.5"; break } | |
| default { "Unknown" } | |
| } | |
| } else { $null } | |
| $report.DotNetFramework = [ordered]@{ | |
| Net45PlusVersion = $net4Version | |
| Net45PlusRelease = $net4Release | |
| Components = $frameworks | |
| } | |
| #endregion | |
| #region .NET Core / SDK | |
| $dotnetPath = Get-Command dotnet -ErrorAction SilentlyContinue | |
| if ($dotnetPath) { | |
| $sdks = @(dotnet --list-sdks 2>$null | ForEach-Object { | |
| if ($_ -match '^(\S+)\s+\[(.+)\]$') { | |
| [ordered]@{ | |
| Version = $matches[1] | |
| Location = $matches[2] | |
| } | |
| } | |
| }) | |
| $runtimes = @(dotnet --list-runtimes 2>$null | ForEach-Object { | |
| if ($_ -match '^(\S+)\s+(\S+)\s+\[(.+)\]$') { | |
| [ordered]@{ | |
| Runtime = $matches[1] | |
| Version = $matches[2] | |
| Location = $matches[3] | |
| } | |
| } | |
| }) | |
| $report.DotNetCore = [ordered]@{ | |
| CliLocation = $dotnetPath.Source | |
| SDKs = $sdks | |
| Runtimes = $runtimes | |
| } | |
| } else { | |
| $report.DotNetCore = $null | |
| } | |
| #endregion | |
| #region Installed Applications | |
| $apps = @() | |
| $regPaths = @( | |
| 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', | |
| 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' | |
| ) | |
| foreach ($path in $regPaths) { | |
| if (Test-Path $path) { | |
| $apps += Get-ItemProperty $path -ErrorAction SilentlyContinue | | |
| Where-Object { $_.DisplayName -and $_.DisplayName.Trim() } | | |
| Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | |
| } | |
| } | |
| $report.InstalledApplications = [ordered]@{ | |
| TotalCount = $apps.Count | |
| Items = @($apps | Sort-Object DisplayName -Unique | ForEach-Object { | |
| [ordered]@{ | |
| Name = $_.DisplayName | |
| Version = $_.DisplayVersion | |
| Publisher = $_.Publisher | |
| InstallDate = $_.InstallDate | |
| } | |
| }) | |
| } | |
| #endregion | |
| #region Development Tools | |
| $tools = @( | |
| @{Name="Git"; Cmd="git"; Args=@("--version")}, | |
| @{Name="Node.js"; Cmd="node"; Args=@("--version")}, | |
| @{Name="npm"; Cmd="npm"; Args=@("--version")}, | |
| @{Name="Python"; Cmd="python"; Args=@("--version")}, | |
| @{Name="Java"; Cmd="java"; Args=@("-version")}, | |
| @{Name="Maven"; Cmd="mvn"; Args=@("--version")}, | |
| @{Name="NuGet"; Cmd="nuget"; Args=@("help")}, | |
| @{Name="PowerShell"; Cmd="powershell"; Args=@("-Command", "`$PSVersionTable.PSVersion.ToString()")} | |
| @{Name="PowerShell Core"; Cmd="pwsh"; Args=@("--version")}, | |
| @{Name="Docker"; Cmd="docker"; Args=@("--version")} | |
| ) | |
| $devTools = @($tools | ForEach-Object { | |
| $tool = $_ | |
| $cmd = Get-Command $tool.Cmd -ErrorAction SilentlyContinue | |
| $result = [ordered]@{ | |
| Name = $tool.Name | |
| Installed = $false | |
| Version = $null | |
| Path = $null | |
| } | |
| if ($cmd) { | |
| try { | |
| $output = & $cmd.Source $tool.Args 2>&1 | Out-String | |
| if ($LASTEXITCODE -eq 0 -and $output -inotmatch "was not found|Microsoft Store") { | |
| $ver = ($output.Split("`n") | Where-Object { $_.Trim() } | Select-Object -First 1).Trim() | |
| $result.Installed = $true | |
| $result.Version = $ver | |
| $result.Path = $cmd.Source | |
| } | |
| } catch {} | |
| } | |
| $result | |
| }) | |
| $report.DevelopmentTools = $devTools | |
| #endregion | |
| #region Visual Studio | |
| $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" | |
| if (Test-Path $vsWhere) { | |
| $vsInstances = & $vsWhere -all -format json | ConvertFrom-Json | |
| $report.VisualStudio = @($vsInstances | ForEach-Object { | |
| [ordered]@{ | |
| DisplayName = $_.displayName | |
| Version = $_.installationVersion | |
| Channel = ($_.channelId -split '\.')[-1] | |
| InstallationPath = $_.installationPath | |
| ProductId = $_.productId | |
| } | |
| }) | |
| } else { | |
| $report.VisualStudio = @() | |
| } | |
| #endregion | |
| #region MSBuild | |
| $msbuildInstalls = @() | |
| if (Test-Path $vsWhere) { | |
| $msbuildPaths = & $vsWhere -all -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe | |
| foreach ($msbuildPath in $msbuildPaths) { | |
| if (Test-Path $msbuildPath) { | |
| $version = (Get-Item $msbuildPath).VersionInfo.ProductVersion | |
| $vsProduct = if ($msbuildPath -match 'Visual Studio\\(\d{4})\\(\w+)\\') { "$($matches[1]) $($matches[2])" } else { $null } | |
| $msbuildInstalls += [ordered]@{ | |
| Version = $version | |
| Type = "Visual Studio" | |
| VsProduct = $vsProduct | |
| Path = $msbuildPath | |
| } | |
| } | |
| } | |
| } | |
| $buildToolsPaths = @( | |
| "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe", | |
| "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe", | |
| "${env:ProgramFiles(x86)}\MSBuild\14.0\Bin\MSBuild.exe", | |
| "${env:ProgramFiles(x86)}\MSBuild\12.0\Bin\MSBuild.exe" | |
| ) | |
| foreach ($path in $buildToolsPaths) { | |
| if (Test-Path $path) { | |
| $version = (Get-Item $path).VersionInfo.ProductVersion | |
| $type = if ($path -match 'BuildTools') { "Build Tools" } else { "Standalone" } | |
| $msbuildInstalls += [ordered]@{ | |
| Version = $version | |
| Type = $type | |
| VsProduct = $null | |
| Path = $path | |
| } | |
| } | |
| } | |
| $msbuildInPath = Get-Command msbuild -ErrorAction SilentlyContinue | |
| $report.MSBuild = [ordered]@{ | |
| InPath = if ($msbuildInPath) { $msbuildInPath.Source } else { $null } | |
| Installations = $msbuildInstalls | |
| } | |
| #endregion | |
| #region Current User Identity | |
| $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() | |
| $principal = New-Object System.Security.Principal.WindowsPrincipal($identity) | |
| $groups = @($identity.Groups | ForEach-Object { | |
| try { | |
| $_.Translate([System.Security.Principal.NTAccount]).Value | |
| } catch { | |
| "$($_.Value) (unresolved)" | |
| } | |
| }) | |
| $report.CurrentUser = [ordered]@{ | |
| Username = $identity.Name | |
| SID = $identity.User.Value | |
| AuthenticationType = $identity.AuthenticationType | |
| IsSystem = $identity.IsSystem | |
| IsAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) | |
| Groups = $groups | Where-Object { $_ -inotmatch '\(unresolved\)' } | |
| } | |
| #endregion | |
| #region Key Windows Services | |
| $keyServices = @( | |
| 'W3SVC', 'MSSQLSERVER', 'SQLSERVERAGENT', 'WinRM', 'Spooler', | |
| 'WSearch', 'wuauserv', 'EventLog', 'Schedule', 'LanmanServer', 'LanmanWorkstation' | |
| ) | |
| $report.Services = @($keyServices | ForEach-Object { | |
| $svcName = $_ | |
| $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue | |
| if ($svc) { | |
| $wmiSvc = Get-CimInstance Win32_Service -Filter "Name='$svcName'" -ErrorAction SilentlyContinue | |
| [ordered]@{ | |
| Name = $svc.Name | |
| DisplayName = $svc.DisplayName | |
| Status = $svc.Status.ToString() | |
| StartType = if ($wmiSvc) { $wmiSvc.StartMode } else { $null } | |
| StartName = if ($wmiSvc) { $wmiSvc.StartName } else { $null } | |
| } | |
| } | |
| } | Where-Object { $_ }) | |
| #endregion | |
| return $report | |
| } | |
| # Execute locally or remotely | |
| if ($ComputerName) { | |
| $report = Invoke-Command @remoteParams -ScriptBlock $reportScript | |
| } else { | |
| $report = & $reportScript | |
| } | |
| # Convert to JSON | |
| $jsonParams = @{ Depth = 100 } | |
| # ConvertTo-Json doesn't have -Indent in PS 5.1, but it formats by default when Depth is set | |
| if ($Compress) { | |
| $jsonParams['Compress'] = $true | |
| } | |
| $json = $report | ConvertTo-Json @jsonParams | |
| # Output to console | |
| Write-Host $json | |
| # Write to file if specified | |
| if ($OutputPath) { | |
| [System.IO.File]::WriteAllText($OutputPath, $json) | |
| Write-Host "" | |
| Write-Host "Report saved to: $OutputPath" -ForegroundColor Green | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment