<# .SYNOPSIS Captures a technical baseline of a Windows server, before and after a migration. .DESCRIPTION Run it inside the guest once before the cutover and once after it, then compare the two JSON files with Compare-MigrationBaseline.ps1. The capture is read-only. Collected: platform (manufacturer/model), OS, network configuration (IP, gateway, DNS, MAC), ghost (non-present) network adapters, disks and volumes, listening ports, automatic services, installed virtualization tools, secure channel, activation, time source and recent System/Application errors. .PARAMETER Path Output JSON file. Defaults to .\baseline--.json .EXAMPLE .\Get-MigrationBaseline.ps1 -Path C:\Migration\before.json .NOTES Windows Server 2012 R2 or later, Windows PowerShell 5.1. Run as administrator. Source: ANYSYS ICT, https://www.anysys.be/articles/ #> [CmdletBinding()] param( [string]$Path = (Join-Path (Get-Location) ("baseline-{0}-{1:yyyyMMdd-HHmm}.json" -f $env:COMPUTERNAME, (Get-Date))) ) $ErrorActionPreference = 'Continue' $cs = Get-CimInstance Win32_ComputerSystem $os = Get-CimInstance Win32_OperatingSystem $cfg = Get-NetIPConfiguration -Detailed $network = foreach ($c in $cfg) { [pscustomobject]@{ Alias = $c.InterfaceAlias Mac = $c.NetAdapter.MacAddress Dhcp = [string]$c.NetIPv4Interface.Dhcp IPv4 = @($c.IPv4Address | ForEach-Object { '{0}/{1}' -f $_.IPAddress, $_.PrefixLength }) Gateway = @($c.IPv4DefaultGateway.NextHop) Dns = @($c.DNSServer | Where-Object AddressFamily -eq 2 | ForEach-Object ServerAddresses) Profile = [string]$c.NetProfile.NetworkCategory } } # Non-present adapters keep their static IP in the registry and block re-use of that IP. $ghostNics = @(Get-PnpDevice -Class Net -ErrorAction SilentlyContinue | Where-Object Status -eq 'Unknown' | Select-Object FriendlyName, InstanceId) $disks = @(Get-Disk | Select-Object Number, FriendlyName, PartitionStyle, IsOffline, IsReadOnly, @{ n = 'SizeGB'; e = { [math]::Round($_.Size / 1GB, 1) } }) $volumes = @(Get-Volume | Where-Object DriveLetter | Sort-Object DriveLetter | Select-Object DriveLetter, FileSystemLabel, FileSystem, HealthStatus, @{ n = 'SizeGB'; e = { [math]::Round($_.Size / 1GB, 1) } }, @{ n = 'FreeGB'; e = { [math]::Round($_.SizeRemaining / 1GB, 1) } }) $listening = @(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalAddress -notin '127.0.0.1', '::1' } | ForEach-Object { $p = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue '{0}/{1}' -f $_.LocalPort, $p.ProcessName } | Sort-Object -Unique) $autoServices = @(Get-CimInstance Win32_Service -Filter "StartMode='Auto'" | Select-Object Name, State | Sort-Object Name) $uninstallKeys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' $virtTools = @(Get-ItemProperty $uninstallKeys -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match 'VMware Tools|Nutanix|VirtIO|QEMU|Hyper-V' } | Select-Object DisplayName, DisplayVersion, PSChildName) $secureChannel = $null if ($cs.PartOfDomain) { try { $secureChannel = Test-ComputerSecureChannel -ErrorAction Stop } catch { $secureChannel = "error: $($_.Exception.Message)" } } $activation = @(Get-CimInstance SoftwareLicensingProduct -Filter "PartialProductKey IS NOT NULL AND ApplicationID='55c92734-d682-4d71-983e-d6ec3f16059f'" | Select-Object Name, LicenseStatus) $since = (Get-Date).AddHours(-24) $errors = foreach ($log in 'System', 'Application') { Get-WinEvent -FilterHashtable @{ LogName = $log; Level = 1, 2; StartTime = $since } -ErrorAction SilentlyContinue | Group-Object ProviderName, Id | ForEach-Object { [pscustomobject]@{ Log = $log; Source = $_.Group[0].ProviderName; Id = $_.Group[0].Id; Count = $_.Count } } } [pscustomobject]@{ CapturedAt = (Get-Date).ToString('s') ComputerName = $env:COMPUTERNAME Manufacturer = $cs.Manufacturer Model = $cs.Model OS = '{0} ({1})' -f $os.Caption, $os.Version LastBoot = $os.LastBootUpTime.ToString('s') Domain = $cs.Domain SecureChannel = $secureChannel TimeSource = (& w32tm.exe /query /source | Out-String).Trim() Network = @($network) GhostNics = $ghostNics Disks = $disks Volumes = $volumes ListeningPorts = $listening AutoServices = $autoServices VirtTools = $virtTools Activation = $activation RecentErrors = @($errors) } | ConvertTo-Json -Depth 5 | Set-Content -Path $Path -Encoding UTF8 Write-Host "Baseline written to $Path"