Get All Windows XP Machines in an Active Directory Forest

Support for Windows XP ends on April 8, 2014. Some organizations might not be completely off Windows XP and need a good way to determine the status of those workstations. I combined some PowerShell that I wrote a while back to accomplish this:

Pinging a List of Machines in PowerShell

Get Counts of all Operating Systems in a Forest Per Domain

The purpose of this was to find all Windows XP machines in all domains in the forest and then ping each one to determine if it is online. It only looks at XP machines that have active computer accounts in AD and have changed their computer password within the last 31 days (This happens by default every 30 days). This script can also be used to look for any OS, just search the code for “Windows XP” and specify a new filter.

The script doesn’t take any arguments but you must have RSAT and Excel installed on the machine running the script:

Remote Server Administration Tools (RSAT)

  • For desktop OS’s download RSAT
  • For server OS’s use Add-WindowsFeature RSAT-AD-PowerShell. *Note, the script will try to install this feature if it isn’t already installed, but it only works for server OS’s.

The output of the script will display the XP count per domain but the rich data is in the Excel file that gets generated. There is a tab for each Domain in your environment:

image

Fields

  • DNSHostName: DNS Name from Active Directory
  • CN: CN from Active Directory
  • Name: Name from Active Directory
  • DistinguishedName: DN from Active Directory
  • Description: Description from Active Directory
  • OperationSystem: OS from Active Directory
  • OperatingSystemVersion: OS Version from Active Directory
  • NameFromDNS: DNS name from DNS
  • IPAddressFromDNS: IP retrieved from DNS
  • PingSuccessPercentage: It was pinged 4 times, this is how many were successful

Script

 #Functions
function ImportADModule
{
  Import-Module ActiveDirectory
  if (!($?))
  { 
    #Only works for Windows Server OS with PS running as admin, download RSAT if using desktop OS
    Add-WindowsFeature RSAT-AD-PowerShell
    Import-Module ActiveDirectory
  }
}

function GetScriptDirectory
{
  $invocation = (Get-Variable MyInvocation -Scope 1).Value
  Split-Path $invocation.MyCommand.Path
}

function GetDN
{
  param($domain)
  $names = $domain.Split(".")
  $bFirst = $true
  foreach ($name in $names)
  {
    if ($bFirst)
    {
      $dn += "DC=" + $name
      $bFirst = $false
    }
    else { $dn += ",DC=" + $name }
  }
  return $dn
}

function GetDNs
{
  param($domains)
  $dns = @{}
  foreach ($domain in $domains)
  {
    $dns.Add($domain, (GetDN -domain $domain))
  }
  return $dns
}

function GetStatusCodeString
{
  param ($code)

  switch ($code)
  {
    $null {$ret = "Ping Command Failed"}
        0 {$ret = "Success"}
    11001 {$ret = "Buffer Too Small"}
    11002 {$ret = "Destination Net Unreachable"}
    11003 {$ret = "Destination Host Unreachable"}
    11004 {$ret = "Destination Protocol Unreachable"}
    11005 {$ret = "Destination Port Unreachable"}
    11006 {$ret = "No Resources"}
    11007 {$ret = "Bad Option"}
    11008 {$ret = "Hardware Error"}
    11009 {$ret = "Packet Too Big"}
    11010 {$ret = "Request Timed Out"}
    11011 {$ret = "Bad Request"}
    11012 {$ret = "Bad Route"}
    11013 {$ret = "TimeToLive Expired Transit"}
    11014 {$ret = "TimeToLive Expired Reassembly"}
    11015 {$ret = "Parameter Problem"}
    11016 {$ret = "Source Quench"}
    11017 {$ret = "Option Too Big"}
    11018 {$ret = "Bad Destination"}
    11032 {$ret = "Negotiating IPSEC"}
    11050 {$ret = "General Error"}
    default {$ret = "Ping Failed"}
  }

  return $ret
}

function GetPingResultsFromHashTable
{
  param($ht, $maxConcurrent, $count, $timeout)

  $bDone = $false
  $i = 0
  $totalMachines = 0
  $htResults = @{}
  $dotTime = [System.DateTime]::Now
  if ($timeout -eq $null) {$timeout = 120}

  Write-Host ("Sending Ping Command to {0} Machines" -f $ht.Count) -NoNewline

  foreach ($name in $ht.GetEnumerator())
  {
    while ((Get-Job -State Running).Count -ge $maxConcurrent)
    {    
      Start-Sleep -Seconds 1
      if ($i -ge 50) { Write-Host "*"; $i = 0 }
      else { Write-Host "*" -NoNewline; $i++ }
    }

    $job = Test-Connection -ComputerName $name.Key.ToString() -Count $count -AsJob
    $job.name = "ping:{0}" -f $name.Key.ToString()

    if ([System.DateTime]::Now -gt $dotTime)
    {
      $dotTime = ([System.DateTime]::Now).AddSeconds(1)
      if ($i -ge 50) { Write-Host "."; $i = 0 }
      else { Write-Host "." -NoNewline; $i++ }
    }
  }

  #Start time now, exit in case of timeout
  $timeout = ([System.DateTime]::Now).AddSeconds($timeout)
  $dotTime = [System.DateTime]::Now
  $i = 0
  Write-Host
  Write-Host "Getting Ping Results" -NoNewline

  while(!($bDone))
  {
    $results = Get-Job -Name 'ping:*'
    $bRunning = $false

    foreach ($result in $results)
    {
      if ($result.State -ne 'Running')
      {
        if ($result.State -eq 'Failed')
        {
          #resubmit job
          if ($i -ge 50) { Write-Host "+"; $i = 0 }
          else { Write-Host "+" -NoNewline; $i++ }
          $job = Test-Connection -ComputerName $result.Name.ToString().Split(":")[1] -Count $count -AsJob
          $job.name = "ping:{0}" -f $result.Name.ToString().Split(":")[1]
        }
        else
        {
          try { $htResults.Add($result.Name.ToString().Split(":")[1], (Receive-Job $result)) } catch {}
          $totalMachines++
        }

        if ([System.DateTime]::Now -gt $dotTime)
        {
          $dotTime = ([System.DateTime]::Now).AddSeconds(1)
          if ($i -ge 50) { Write-Host "."; $i = 0 }
          else { Write-Host "." -NoNewline; $i++ }
        }
        
        try { Remove-Job $result } catch {}
      }
      else
      {
        $bRunning = $true
      }
    }

    #Check for timeout condition, clean up all jobs if true
    if ([System.DateTime]::Now -gt $timeout)
    {
      $bDone = $true
      Write-Host "Timeout reached, removing jobs"
      $results = Get-Job -Name 'ping:*'
      foreach ($result in $results)
      {
        Write-Host "RemoveJob:"$result.Name
        try
        {
          Stop-Job $result
          try { Remove-Job $result -Force } catch {}
        }
        catch {}
      }
    }

    #If the timeout hasn't been reached and jobs are still running, loop again
    if (!($bRunning)) { $bDone = $true }
  }

  Write-Host 
  Write-Host ("Received Ping Results From {0} Machines" -f $totalMachines)
  
  return $htResults
}

function ResolveNamesFromPingResults
{
  param($array, $maxConcurrent, $resolveNames, $timeout)

  try { if ($resolveNames -ne $null) { [bool]$resolveNames = [System.Convert]::ToBoolean($resolveNames) } } catch {}

  $htResults = @{}

  if ($resolveNames)
  {
    $dotTime = ([System.DateTime]::Now)
    if ($timeout -eq $null) {$timeout = 120}
    $i = 0
    $scriptBlock = 
    {
      param($s)
      try { $ret = [System.Net.DNS]::GetHostEntry($s) } catch {}
      return $ret
    }
    Write-Host ("Resolving DNS Names for {0} Machines" -f $array.Count) -NoNewline
    foreach ($name in $array)
    {
      while ((Get-Job -State Running).Count -ge $maxConcurrent)
      {    
        Start-Sleep -Seconds 1
        if ($i -ge 50) { Write-Host "*"; $i = 0 }
        else { Write-Host "*" -NoNewline; $i++ }
      }
      $job = Start-Job -ScriptBlock $scriptBlock -ArgumentList $name.NameInList
      $job.name = "resolve:{0}" -f $name.NameInList
      if ([System.DateTime]::Now -gt $dotTime)
      {
        $dotTime = ([System.DateTime]::Now).AddSeconds(1)
        if ($i -ge 50) { Write-Host "."; $i = 0 }
        else { Write-Host "." -NoNewline; $i++ }
      }
    }

    #Start time now, exit in case of timeout
    $timeout = ([System.DateTime]::Now).AddSeconds($timeout)
    $dotTime = ([System.DateTime]::Now)
    $i = 0
    $bDone = $false

    Write-Host
    Write-Host "Getting DNS Results" -NoNewline
    while(!($bDone))
    {
      $results = Get-Job -Name 'resolve:*'
      $bRunning = $false

      foreach ($result in $results)
      {
        if ($result.State -ne 'Running')
        {
          if ($result.State -eq 'Failed')
          {
            #resubmit job
            if ($i -ge 50) { Write-Host "+"; $i = 0 }
            else { Write-Host "+" -NoNewline; $i++ }
            $job = Start-Job -ScriptBlock $scriptBlock -ArgumentList $result.Name.ToString().Split(":")[1]
            $job.name = "resolve:{0}" -f $result.Name.ToString().Split(":")[1]
          }
          else
          {
            try { $htResults.Add($result.Name.ToString().Split(":")[1], (Receive-Job $result)) } catch {continue}
          }

          if ([System.DateTime]::Now -gt $dotTime)
          {
            $dotTime = ([System.DateTime]::Now).AddSeconds(1)
            if ($i -ge 50) { Write-Host "."; $i = 0 }
            else { Write-Host "." -NoNewline; $i++ }
          }
        
          try { Remove-Job $result -Force} catch {}
        }
        else
        {
          $bRunning = $true
        }
      }

      #Check for timeout condition, clean up all jobs if true
      if ([System.DateTime]::Now -gt $timeout)
      {
        $bDone = $true
        Write-Host "Timeout reached, removing jobs"
        $results = Get-Job -Name 'resolve:*'
        foreach ($result in $results)
        {
          Write-Host "RemoveJob:"$result.Name
          try
          {
            Stop-Job $result
            try { Remove-Job $result -Force } catch {}
          }
          catch {}
        }
      }

      #If the timeout hasn't been reached and jobs are still running, loop again
      if (!($bRunning)) { $bDone = $true }
    }
    Write-Host 
    Write-Host ("Received DNS Results From {0} Machines" -f $htResults.Count)
  }

  return $htResults
}

function GetFormattedPingResultsFromHashTable
{
  param($ht)

  $fResults = New-Object System.Collections.ArrayList
  $dotTime = ([System.DateTime]::Now)
  $i = 0
  Write-Host "Formatting Ping Results" -NoNewLine

  foreach ($result in $ht.GetEnumerator())
  {
    #There are multiple pings here if we ping more than once per computer
    $originalAddress = $result.Key.ToString()
    $pingCount = 0
    $successCount = 0
    $status = 'Ping Job Failed'
    $pingedFrom = 'Ping Job Failed'
    $successPercentage = 0
    
    try { $pings = $result.Value.Count } catch { $pings = 0 }
    if ($pings -gt 0) 
    {
      $status = GetStatusCodeString -code $result.Value[$pings-1].StatusCode
      $pingedFrom = $result.Value[$pings-1].PSComputerName
    }

    foreach ($ping in $result.Value)
    {
      $pingCount++
      if ($ping.StatusCode -eq 0) { $successCount++ }
      #If you try to get the IPv4Address or IPv6Address it slows down this loop significantly
    }
     
    #Calculate percentage
    if ($pingCount -ne 0) { $successPercentage = ($successCount / $pingCount) * 100 } 
    else { $successPercentage = 0 }

    #Add to array
    $o = New-Object PSObject -Property @{
      NameInList = $originalAddress
      PingedFrom = $pingedFrom
      SuccessPercentage = $successPercentage
      LastPingStatus = $status
    }
   
    [void]$fResults.Add($o)

    if ([System.DateTime]::Now -gt $dotTime)
    {
      $dotTime = ([System.DateTime]::Now).AddSeconds(1)
      if ($i -ge 50) { Write-Host "."; $i = 0 }
      else { Write-Host "." -NoNewline; $i++ }
    }
  }

  Write-Host 
  Write-Host ("Formatted Ping Results for {0} Machines" -f $fResults.Count)

  return $fResults
}

function GetFormattedPingAndDNSResults
{
  param($pingResults, $dnsResults)

  if ($dnsResults.Count -ne 0)
  {
    Write-Host "Formatting DNS Results" -NoNewLine
    $dotTime = ([System.DateTime]::Now)
    $i = 0
    foreach ($ping in $pingResults)
    {
      $dns = $dnsResults.Get_Item($ping.NameInList)
      if ($dns -ne $null)
      {
        $bFirst = $true
        foreach ($ip in $dns.AddressList)
        {
          if ($bFirst){ $ipList = $ip }
          else { $ipList += "|" + $ip }
        }

        $fqdn = $dns.HostName
      }
      else
      {
        $ipList = $null
        $fqdn = 'No DNS Entry Found'
      }

      $ping | Add-Member -MemberType NoteProperty -Name NameFromDNS -value $fqdn -Force
      $ping | Add-Member -MemberType NoteProperty -Name IPAddressListFromDNS -value $ipList -Force

      if ([System.DateTime]::Now -gt $dotTime)
      {
        $dotTime = ([System.DateTime]::Now).AddSeconds(1)
        if ($i -ge 50) { Write-Host "."; $i = 0 }
        else { Write-Host "." -NoNewline; $i++ }
      }
    }
    Write-Host 
    Write-Host ("Formatted DNS Results for {0} Machines" -f $pingResults.Count)
  }

  return $pingResults
}

function GetTimeSpanStringInMinutesAndSeconds
{
  param($startTime, $endTime)

  $time = $startTime.Subtract($endTime)
  $minutes = $time.ToString().Split(":")[1]
  $seconds = $time.ToString().Split(":")[2].Split(".")[0]
  $timeSpan = "{0} Minutes and {1} Seconds" -f $minutes, $seconds
  return $timeSpan
}

function GetSuccessPingCount
{
  param($results)

  $successCount = 0
  foreach ($result in $results)
  {
    if ($result.SuccessPercentage -gt 0) { $successCount++ }
  }

  return $successCount
}

function GetDNSNamesResolvedCount
{
  param($results)

  $namesResolved = 0
  foreach ($result in $results)
  {
    if ($result.IPAddressListFromDNS -ne $null) { $namesResolved++ }
  }

  return $namesResolved
}

function GetPercentageAsString
{
  param($n1, $n2)

  if ($n1 -ne 0) { $percentage = ($n1 / $n2) * 100 } 
  else { $percentage = 0 }

  $percentage = ("{0:N0}" -f $percentage) + "%"

  return $percentage
}

function GetOSCountsPerDomain
{
  param($dns, $enabled, $daysOld)
  $osCounts = @{}
  $cutOffDate = ((Get-Date).Adddays(-($daysOld))).ToFileTime()
  Write-Host "Getting Data" -NoNewline -ForegroundColor Yellow

  $filter = "(PwdLastSet -gt {0}) -and (Enabled -eq '{1}') -and (OperatingSystem -like '{2}')" -f $cutOffDate, $enabled, 'Windows XP*'
  foreach ($domain in $dns.GetEnumerator())
  {
    $domains = @{}
    $htComputers = @{}
    Write-Host "." -NoNewline -ForegroundColor Yellow
    $computers = Get-ADComputer -Filter $filter -SearchBase $domain.Value -Server $domain.Key -Properties CN, Description, DistinguishedName, DNSHostName, Name, OperatingSystem, OperatingSystemVersion
    foreach ($computer in $computers)
    {
      try { $htComputers.Add($computer.DNSHostName, $computer) } catch {}
      if ($computer.OperatingSystem -eq $null) { $os = 'NULL'}
      else { $os = $computer.OperatingSystem }
      if ($computer.OperatingSystemVersion -eq $null) { $osver = 'NULL'}
      else { $osver = $computer.OperatingSystemVersion }
      try { $domains.Add(($os + " - " + $osver), 1) }
      catch { $domains.Set_Item(($os + " - " + $osver), ($domains.Get_Item($os + " - " + $osver))+1) }
    }
    $osCounts.Add($domain.Key, $domains)

    #Ping Computers
    $results = GetPingResultsFromHashTable -ht $htComputers -maxConcurrent 100 -count 4 -timeout $TimeoutInSeconds 90

    #Format ping results into an array of objects
    $formattedPingResults = GetFormattedPingResultsFromHashTable -ht $results

    #Resolve DNS Names if specified
    $dnsResults = ResolveNamesFromPingResults -array $formattedPingResults -maxConcurrent 5 -resolveNames $true -timeout 90

    #Format DNS results by adding them to the ping results
    $formattedPingResults = GetFormattedPingAndDNSResults -pingResults $formattedPingResults -dnsResults $dnsResults

    #Update master list with ping data
    $fResults = New-Object System.Collections.ArrayList
    foreach($formattedPingResult in $formattedPingResults)
    {
      $o = $htComputers.Get_Item($formattedPingResult.NameInList)
      $nO = New-Object PSObject -Property @{
        DNSHostName = $o.DNSHostName;
        CN = $o.CN;
        Name = $o.Name;
        DistinguishedName = $o.DistinguishedName;
        Description = $o.Description;
        OperatingSystem = $o.OperatingSystem;
        OperatingSystemVersion = $o.OperatingSystemVersion;
        NameFromDNS = $formattedPingResult.NameFromDNS;
        IPAddressFromDNS = $formattedPingResult.IPAddressListFromDNS;
        PingSuccessPercentage = $formattedPingResult.SuccessPercentage;
        }
      [void]$fResults.Add($nO)
    }

    #Output data to csv
    $outputFile = "{0}.csv" -f $domain.Key
    $path = Join-Path ($Global:CurrentDirectory) $outputFile
    $Global:CSVs.Add($domain.Key,$path)

    $fResults | select DNSHostName, CN, Name, DistinguishedName, Description, OperatingSystem, OperatingSystemVersion, NameFromDNS, IPAddressFromDNS, PingSuccessPercentage  | sort DNSHostName | Export-Csv $path -NoTypeInformation
    Write-Host 
    Write-Host "Detailed Output File: $path"
  }
  Write-Host
  return $osCounts
}

function CreateExcelFile
{
  param ($osCounts)

  #Create the Excel file
  $oExcel = New-Object -ComObject Excel.Application
  $outputFile = "GetXPWorkstations.xlsx"
  $path = Join-Path ($Global:CurrentDirectory) $outputFile
  $oExcel.Visible = $true
  $oWorkbook = $oExcel.Workbooks.Add()

  #Create a tab for each domain
  foreach ($csv in $Global:CSVs.GetEnumerator())
  {
    $oCSV = $oExcel.Workbooks.Open($csv.Value)
    $oCSV.ActiveSheet.Move($oWorkBook.ActiveSheet)
  }
  $oWorkbook.SaveAs($path)
}

function DisplayOutput
{
  param($osCounts)
  Write-Host
  foreach ($osCount in $osCounts.GetEnumerator())
  {
    Write-Host $OSCount.Key -ForegroundColor Green
    $osCount.Value.GetEnumerator() | Sort-Object Value -Descending | Format-Table -AutoSize
  }
}

#Main
$Global:CurrentDirectory = (GetScriptDirectory)
$Global:CSVs = @{}

#Import AD Module for PowerShell
ImportADModule

#Get list of domains from current forest
$Domains = (Get-ADForest).domains

#Get hash table of domains and distinguished names from current forest
$DNs = GetDNs -domains $Domains

#Get OS counts per domain (specify age here)
$OSCounts = GetOSCountsPerDomain -dns $DNs -enabled $true -daysOld 31

#Convert output to Excel File
CreateExcelFile -osCounts $OSCounts

#Display Results
DisplayOutput -osCounts $OSCounts

GetXPWorkstations.renametops1