# windowsDiscovery.ps1

function checkSpectreVulnerability($computer=$env:computername){
  $command={
      $patchedVersion="10.0.14393.2842"
      $actualVersion=(Get-Item C:\Windows\system32\mcupdate_genuineintel.dll | select VersionInfo).VersionInfo.ProductVersion
      $intelDllPatched=[version]$actualVersion -ge [version]$patchedVersion
      <#
      if(!$intelDllPatched){
          write-warning "mcupdate_genuineintel.dll $actualVersion IS VULNERABLE to Spectre meltdown"
      }else{
          write-host "mcupdate_genuineintel.dll $actualVersion is NOT vulnerable to Spectre meltdown" -ForegroundColor Green
      }
      #>
      # Source: https://support.microsoft.com/en-us/topic/kb4073119-windows-client-guidance-for-it-pros-to-protect-against-silicon-based-microarchitectural-and-speculative-execution-side-channel-vulnerabilities-35820a8a-ae13-1299-88cc-357f104f5b11
      # If Hyper-V feature is enabled
      $virtualizationRegKey='REGISTRY::HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization'
      $virtualization=try{get-itemproperty $virtualizationRegKey}catch{$false}
      $virtualServerMitigated=if($virtualization){
              $virtualServerMitigated=[int]($virtualization.MinVmVersionForCpuBasedMitigations) -ge 1
              if($virtualServerMitigated){
                  #write-host "Hyper-V server $env:computername has enabled MinVmVersionForCpuBasedMitigations" -ForegroundColor Green
                  $true
              }else{
                  #write-warning "Hyper-V server $env:computername has NOT enabled MinVmVersionForCpuBasedMitigations"
                  $false
              }
              #reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization" /v MinVmVersionForCpuBasedMitigations /t REG_SZ /d "1.0" /f
          }else{
              'N/A'
          }

      # To enable mitigations for:
      # CVE-2017-5715 (Spectre Variant 2)
      # CVE-2017-5754 (Meltdown)
      # CVE-2018-3639 (Speculative Store Bypass)
      $spectreMeltDownRegKey='REGISTRY::HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management'
      #reg add $spectreMeltDownRegKey /v FeatureSettingsOverride /t REG_DWORD /d 8 /f #disable: 3
      #reg add $spectreMeltDownRegKey /v FeatureSettingsOverrideMask /t REG_DWORD /d 3 /f

      # To enable mitigations for: CVE-2017-5715 (Spectre Variant 2)
      #$branchTargetInjectionRegKey="REGISTRY::HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management"
      #reg add $branchTargetInjectionRegKey /v FeatureSettingsOverride /t REG_DWORD /d 0 /f #disable: 1
      #reg add $branchTargetInjectionRegKey /v FeatureSettingsOverrideMask /t REG_DWORD /d 3 /f

      $featureSettingsOverrideValue=(Get-ItemProperty $spectreMeltDownRegKey).FeatureSettingsOverride
      $mitigationStatus=switch($featureSettingsOverrideValue){
          0 {'ENABLED: default mitigations for CVE-2017-5715 (Spectre Variant 2) and CVE-2017-5754 (Meltdown)'}
          1 {'DISABLED: mitigations for CVE-2017-5715 (Spectre Variant 2)'}
          3 {'DISABLED: all Spectre mitigations'}
          8 {'ENABLED: mitigations for CVE-2018-3639 (Speculative Store Bypass), default mitigations for CVE-2017-5715 (Spectre Variant 2) and CVE-2017-5754 (Meltdown)'}
          64 {'ENABLED: user-to-kernel protection on AMD and ARM processors together with other protections for CVE 2017-5715'}
          72 {'ENABLED: user-to-kernel protection on AMD processors together with other protections for CVE 2017-5715 and protections for CVE-2018-3639 (Speculative Store Bypass)`r`nmitigations for Intel® Transactional Synchronization Extensions (Intel® TSX) Transaction Asynchronous Abort vulnerability (CVE-2019-11135) and Microarchitectural Data Sampling (CVE-2018-11091, CVE-2018-12126, CVE-2018-12127, CVE-2018-12130) along with Spectre (CVE-2017-5753 & CVE-2017-5715) and Meltdown (CVE-2017-5754) variants, including Speculative Store Bypass Disable (SSBD) (CVE-2018-3639) as well as L1 Terminal Fault (L1TF) (CVE-2018-3615, CVE-2018-3620, and CVE-2018-3646) without disabling Hyper-Threading'}
          8264 {'ENABLED: mitigations for Intel® Transactional Synchronization Extensions (Intel® TSX) Transaction Asynchronous Abort vulnerability (CVE-2019-11135) and Microarchitectural Data Sampling (CVE-2018-11091, CVE-2018-12126, CVE-2018-12127, CVE-2018-12130) along with Spectre (CVE-2017-5753 & CVE-2017-5715) and Meltdown (CVE-2017-5754) variants, including Speculative Store Bypass Disable (SSBD) (CVE-2018-3639) as well as L1 Terminal Fault (L1TF) (CVE-2018-3615, CVE-2018-3620, and CVE-2018-3646) with Hyper-Threading disabled'}
      }
      return [PSCustomObject][ordered]@{
          computerName=$env:computername
          intelDllPatched = $intelDllPatched
          vmSpectreMitigationEnabled = $virtualServerMitigated
          osMitigationSettings = if($mitigationStatus){$mitigationStatus}else{'None'}
      }
  }
  invoke-command -ComputerName $computer -ScriptBlock $command|select -Property * -ExcludeProperty RunspaceId
}

function checkSpeculationControls($computer=$env:computername){
  $command={
      # Interpretations of output: https://support.microsoft.com/en-us/topic/kb4074629-understanding-speculationcontrol-powershell-script-output-fd70a80a-a63f-e539-cda5-5be4c9e67c04
      #$originalExecutionPolicy = Get-ExecutionPolicy
      #Set-ExecutionPolicy RemoteSigned -Scope Currentuser
      $ErrorActionPreference='stop'
      if(!(Get-command Get-SpeculationControlSettings -ea SilentlyContinue)){
          [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
          if(!(Get-PackageProvider 'nuget' -ea SilentlyContinue)){
              try{
                  Install-PackageProvider -Name 'NuGet' -Force
              }catch{
                  write-warning "$env:computername is unable to reach go.microsoft.com to obtain NuGet."
                  #Set-ItemProperty -Path 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord
                  #Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord
                  #Install-PackageProvider -Name NuGet -Force
                  return $false
              }
              # Overcome this error:
              #WARNING: Unable to download from URI 'https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409' to ''.
              #WARNING: Unable to download the list of available providers. Check your internet connection.
              #Install-PackageProvider : No match was found for the specified search criteria for the provider 'NuGet'. The package
              #provider requires 'PackageManagement' and 'Provider' tags. Please check if the specified package has the tags.
              #    + CategoryInfo          : InvalidArgument: (Microsoft.Power...PackageProvider:InstallPackageProvider) [Install-PackageProvider], Exception
              #    + FullyQualifiedErrorId : NoMatchFoundForProvider,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackageProvider
          }
          try{
              Install-Module SpeculationControl -force
          }catch{
              write-warning $_
              return $false
          }            
      }
      #Set-ExecutionPolicy $originalExecutionPolicy -Scope Currentuser
      $speculationControlSettings=Get-SpeculationControlSettings -Quiet
      $speculationControlSettings|Add-Member -MemberType NoteProperty -name 'computerName' -value $env:computername
      return $speculationControlSettings
  }
  invoke-command -ComputerName $computer -ScriptBlock $command
}

function checkBios($computer=$env:computername){
  if($env:computername -ne $computer){
    $thisSession=new-pssession $computer
  }
  invoke-command -computername $computer -scriptblock{
      function displayArrayAsColumns($array){   
          function inflateString([string]$string="test",[int]$maxInflate=6,[string]$fillWith=' '){
              [int]$difference=$maxInflate-$string.tostring().Length
              if($difference -gt 0){        
                  [string]$fill=$fillWith*$difference
                  return $string+$fill
                  }    
          }
          $columnWidth=.{return ($array[$array.length-1]).tostring().Length+1}
          $numberOfElements=$array.Count
          $numberOfRows=[math]::Ceiling(($numberOfElements+$columnWidth)/4)
          $splittedArray=@()
          $fragment=[math]::Ceiling($numberOfElements/$numberOfRows)
          $array=$array|%{inflateString $_ $columnWidth}
          for ($i=0;$i -lt $numberOfRows; $i++){                       
              $endIndex=($fragment*($i+1))-1;
              $startIndex=$endIndex-$fragment+1;
              if($i -eq $columns-1){$endIndex=$elementsCount-1}
              #write-host "startIndex: $startIndex | endIndex: $endIndex";   
              $splittedArray+=,$array[$startIndex..$endIndex];
              }
          return ($splittedArray|%{$_ -join ''} | out-string).trim()
      }
      $biosRegKey="REGISTRY::HKEY_LOCAL_MACHINE\Hardware\Description\System\Bios"
      $bios=Get-ItemProperty $biosRegKey
      $computerInfo=Get-ComputerInfo
      $cpuVirtualizationEnabledInBios=if($computerInfo.HyperVRequirementVirtualizationFirmwareEnabled){$true}else{$false}
      $os=$computerInfo.OsName
      $osHotFixes=$computerInfo.OsHotFixes.HotFixID
      $cpuModel=($computerInfo.CsProcessors|Select-Object -Unique).Name
      $cpuCount=$computerInfo.CsNumberOfProcessors
      $cpuUtilization=(Get-WmiObject win32_processor|Measure-Object -property LoadPercentage -Average|Select @{Name="CurrentLoad";Expression={"{0:N2} %" -f ($_.Average)}}).CurrentLoad
      $logicalCpuCount=$computerInfo.CsNumberOfLogicalProcessors
      $memoryGb=[math]::round($computerInfo.OsTotalVisibleMemorySize/1048576)
      $osInfo=gwmi -Class win32_operatingsystem
      $memoryUtilization="{0:N2} %" -f ((($osInfo.TotalVisibleMemorySize - $osInfo.FreePhysicalMemory)*100)/ $osInfo.TotalVisibleMemorySize)
      return [PSCustomObject][ordered]@{
          computerName=$env:computername
          manufacturer=($bios.BaseBoardManufacturer).split(' ')[0]
          model=$bios.SystemProductName
          motherboard=$bios.BaseBoardProduct
          motherboardVersion=$bios.BaseBoardVersion
          biosVersion=$bios.BIOSVersion
          biosReleaseDate=$bios.BIOSReleaseDate
          cpuVirtualizationEnabledInBios=$cpuVirtualizationEnabledInBios
          os=$os
          osHotFixes=displayArrayAsColumns $osHotFixes
          cpuModel=$cpuModel
          cpuCount=$cpuCount            
          logicalCpuCount=$logicalCpuCount
          cpuUtilization=$cpuUtilization
          memoryGb=$memoryGb
          memoryUtilization=$memoryUtilization
      }
      #$manufacturer=$bios.BaseBoardManufacturer
      #$wmiBios=Get-WmiObject -computername $computer win32_bios
      #$systemInfoBios=systeminfo | findstr /I /c:bios #This gives us the date as well
      #$biosVersion=$wmiBios.SMBIOSBIOSVersion
  }
}
function checkNetwork($computer=$env:computername){
  invoke-command -ComputerName $computer -ScriptBlock{
      $adapters=get-netadapter|?{$_.Status -eq 'Up'}|select Name,InterfaceDescription,LinkSpeed
      $physicalAdapters=get-netadapter -physical|?{$_.Status -eq 'Up'}|select Name,InterfaceDescription,LinkSpeed,@{Name='vmq';e={$vmqEnabled=try{(Get-NetAdapterVmq $_.Name).Enabled}catch{$false};if($vmqEnabled){'VMQ Enabled'}else{'VMQ Disabled'}}}
      $virtualAdapters=($adapters|?{$_.Name -notin $physicalAdapters.Name}).Name
      $netOffloadSettings=Get-NetOffloadGlobalSetting
      return [pscustomobject][ordered]@{
          computerName=$env:computerName
          physicalAdapters=($physicalAdapters|Sort-Object|ft -HideTableHeaders|out-string).trim()
          virtualAdapters=if($virtualAdapters){($virtualAdapters|Sort-Object|out-string).trim()}else{'None'}
          netOffloadSettings=($netOffloadSettings|out-string).trim()
      }
  }
}
function checkHyperVConfigs($computer=$env:computername){
  $getHyperVConfigs={
      $offlineFileCache=(Get-WmiObject win32_offlinefilescache).Enabled
      $volumes=gwmi -Class win32_volume -Filter "DriveType!=5" -ea stop| ?{$_.DriveLetter -ne $isnull}|Select-object @{Name="Letter";Expression={$_.DriveLetter}},@{Name="Label";Expression={$_.Label}},@{Name="Capacity";Expression={"{0:N2}GB" -f ($_.Capacity/1073741824)}},@{Name = "Utilization"; Expression = {"{0:N2} %" -f  ((($_.Capacity-$_.FreeSpace) / $_.Capacity)*100)}}
      return [PSCustomObject][ordered]@{
          computerName=$env:computername
          offlineFileCache=$offlineFileCache
          volumes=($volumes|Sort-Object|ft -HideTableHeaders|out-string).trim() -replace '\s{2}',''
      }       
  }
  invoke-command -ComputerName $computer -ScriptBlock $getHyperVConfigs
}

function checkSecurity($computer=$env:computername){
  $securityCheckCommand={
      $remoteCodeExecutionScan=.{
          $passVersion='14.0'
          $visualCFiles=(Get-ItemProperty Registry::HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*  -ErrorAction SilentlyContinue|?{$_.displayname -like "Microsoft Visual C++*"}|select DisplayName,DisplayVersion)
          $results=@()
          foreach ($vc in $visualCFiles){
              $displayName=$vc.DisplayName
              [version]$displayVersion=$vc.DisplayVersion
              if($displayVersion -ge [version]$passVersion){
                  $results+="$displayName`: Pass"
              }else{
                  $results+="$displayName`: Fail"
              }
          }
          return $results
      }
      $unquotedServiceEnumerationPassed=.{
          $unquotedServicePathItems=(wmic service get name","displayname","pathname","startmode |findstr /i "auto" |findstr /i /v "c:\windows\\" |findstr /i /v "''").Trim()
          if ($unquotedServicePathItems){
              return $false
          }else{
              return $true
          }
      }
      $rdpSecurityPassed=.{
          $rdpAuth=(Get-WmiObject -class "Win32_TSGeneralSetting" -Namespace root\cimv2\terminalservices -Filter "TerminalName='RDP-tcp'").UserAuthenticationRequired
          #$encryptionLevel=(Get-WmiObject -class "Win32_TSGeneralSetting" -Namespace root\cimv2\terminalservices -Filter "TerminalName='RDP-tcp'").MinEncryptionLevel
          #$compliant=switch ($encryptionLevel){
          #    1 {"Low";}
          #    2 {"Client Compatible";}
          #    3 {"High";}
          #    4 {"FIPS Compliant";}
          #}
          if($rdpAuth){
              #write-host "RDP Network Authentication Requirement: passed!`nRDP Encryption Level: $compliant"
              return $true
          }else{
              #write-host "RDP Network Authentication Requirement: Fail";
              return $false
          }
      }
      $ieSecurityScan=.{
          $ieKeys=@(
              @("CVE-2017-829 (32-Bit)","HKLM:SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX"),
              @("CVE-2017-8529 (64-bit)","HKLM:SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ENABLE_PRINT_INFO_DISCLOSURE_FIX"),
              @("ASLR Hardening Setting for IE (32-Bit)","HKLM:SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING"),
              @("ASLR Hardening Setting for IE (64-Bit)","HKLM:SOFTWARE\WOW6432Node\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_ALLOW_USER32_EXCEPTION_HANDLER_HARDENING")
              )    
          foreach ($ieKey in $ieKeys){
                  try{
                      $value=(Get-ItemProperty -Path $ieKey[1] -Name "iexplore.exe" -ErrorAction SilentlyContinue).'iexplore.exe';
                  }catch{
                      $value=0;
                      continue;
                  }
                  $ieResult=if($value){"Pass"}else{"Fail";}
                  $ieKey[0] + ": " + $ieResult
              }
      }
      $memoryManagementScan=.{
          $memKeys=@(
              @("CVE-2017-5715","HKLM:SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management","FeatureSettingsOverride","0"),
              @("CVE-2017-5715","HKLM:SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management","FeatureSettingsOverrideMask","3"),
              @("CVE-2017-5753-54","HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization","MinVmVersionForCpuBasedMitigations","1.0")
              )
          foreach ($memKey in $memKeys){
              $value=(Get-ItemProperty -Path $memKey[1] -Name $memKey[2] -ErrorAction SilentlyContinue).[string]($memKey[2])
              $memResult=if($value -eq $memKey[3]){"Pass"}else{"Fail";}
              $memKey[0]+ ": " + $memResult;
          }
      }
      $localAdmins=.{
          net localgroup administrators | where {$_ -AND $_ -notmatch "command completed successfully"} | select -skip 4
      }
      $scheduledTasksRunasDomainAccount=.{
          $domain="$env:USERDOMAIN"
          $Results = @() #Initializes an empty array
          ForEach ($Computer in $ComputerNames){
              # Use the legacy schtasks command from localhost to query remote machine and format an output int CSV format              
              $tasksAsCSV = schtasks.exe /query /s $env:computername /V /FO CSV    
              # Process the CSV result into PowerShell. Filter entries that are not labeled as "TaskName" and by "Run as User" field
              $result = $tasksAsCSV | ConvertFrom-Csv | ? { $_.TaskName -ne "TaskName" -and $_."Run As User" -match $domain}
              $results += $result
          }            
          if ($Results){
              Return $results."Task To Run"
          }else{
              'None'
          }
      }
      $servicesRunasDomainAccount=.{
          $domain="$env:USERDOMAIN"
          $services=Get-Wmiobject win32_service|select-object Name,StartName|?{$_.StartName -match $domain}
          if($services){
              return $services
          }else{
              return 'None'
          }
      }
      return [pscustomobject][ordered]@{
          computerName=$env:computername
          rdpSecurityPassed=$rdpSecurityPassed
          ieSecurity=($ieSecurityScan|out-string).trim()
          remoteCodeExecution=($remoteCodeExecutionScan|out-string).trim()
          memoryManagement=($memoryManagementScan|out-string).trim()
          unquotedServiceEnumerationPassed=$unquotedServiceEnumerationPassed
          localAdmins=($localAdmins|out-string).trim()
          servicesRunasDomainAccount=$servicesRunasDomainAccount
          scheduledTasksRunasDomainAccount=$scheduledTasksRunasDomainAccount
      }
  }
  invoke-command -ComputerName $computer -ScriptBlock $securityCheckCommand
}

function checkInstalledApps($computer=$env:computername){
    $getInstalledApps={
    $installedApps=Get-CimInstance -computername $computer -ClassName win32_InstalledWin32Program -ErrorAction Stop|select Name,Version
    return [PSCustomObject][ordered]@{
        computerName=$env:computername
        installedApps=($installedApps|Sort-Object|ft -HideTableHeaders|out-string).trim() -replace '\s{2}',''
    }
  }
  invoke-command -ComputerName $computer -ScriptBlock $getInstalledApps
}

function checkIis($computer=$env:computername){
  $checkIis={
    $iis=Get-WindowsOptionalFeature -Online -FeatureName 'IIS-WebServer'
    $webSites=if($iis.State -eq 'Enabled'){
      get-website
    }else{
      'None'
    }
    $ftpInstalled=Get-WindowsFeature Web-Ftp-Server|Select -Expand Installed
    return [PSCustomObject][ordered]@{
      computerName=$env:computername
      iisStatus=$iis.State
      webSites=($webSites|ft|out-string).trim()
      ftpInstalled=$ftpInstalled
    }
  }
  invoke-command -ComputerName $computer -ScriptBlock $checkIis 
}

function checkDotNet($computer=$env:computername){
  $checkDotNet={
    $dotNet=Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -recurse| `
      Get-ItemProperty -name Version,Release -EA 0|?{$_.PSChildName -match '^(?!S)\p{L}'}| `
      Select-Object @{name='dotNetProduct';e={$_.PSChildName}}, Version, Release
      return [PSCustomObject][ordered]@{
        computerName=$env:computername
        iisStatus=$iis.State
        dotNetProducts=($dotNet|ft|out-string).trim()
      }
    }
    invoke-command -ComputerName $computer -ScriptBlock $checkDotNet
}

function checkConnections($computer=$env:computername){
  function invokeGetProcessPorts{
    [cmdletbinding()]
    Param(
        [string[]]$computerNames=$env:computername,
        [string[]]$processNames=$null,
        [string[]]$portNumbers=$null
        )
    function getProcessConnections{
        [cmdletbinding()]
        Param(
            [parameter(ValueFromPipeLine=$True)][AllowEmptyCollection()][string[]]$processNames=$null,
            [parameter(ValueFromPipeLine=$True)][AllowEmptyCollection()][string[]]$portNumbers=$null
            )
        # Initialize variables    
        $results=@()
        $psVersionFeasible=$PSVersionTable.PSVersion.Major -ge 4
        $processes=if($processNames){
                write-host "Checking connections on for process name(s): $processNames..."
                if($psVersionFeasible){
                        try{                
                            get-process $processNames -IncludeUserName # This is only available in PoSh 4.0+ 
                        }catch{
                            write-warning $_
                        }
                    }else{
                        get-process $processNames
                        }
            }else{
                if($psVersionFeasible){               
                    get-process -IncludeUserName
                }else{
                    get-process
                }
            }
         
        if($processes){
            write-host "Checking $($processes.count) processes..." -ForegroundColor Yellow
            # Collecting wmiobjects to be able to invoke .getowner() method
            $processObjects=if(!$psVersionFeasible){Get-WmiObject Win32_Process}else{$null}
            $netStat=if($portNumbers){
                    write-host "Checking connections on port(s) $portNumbers..."
                    netstat -ano|?{$_ -match "\:($($portNumbers -join '|'))\s"}
                }else{
                    write-host 'Port numbers were not defined. Scanning all ports...' -ForegroundColor Yellow
                    Netstat -ano|?{$_ -match "\d$"}
                }
            $previousPid=$previousOwner=$null
            write-verbose "Process ID:"
            foreach($process in $processes){            
                $processId=$process.ID
                write-verbose "$processId"
                $processName=$process.ProcessName
                $processOwner=if($processId -ne $previousPid){
                    $previousOwner=if($processObjects){
                            $processObjects|?{$_.ProcessId -eq $processId}|%{if($_.GetOwner().User){$_.GetOwner().Domain+"\"+$_.GetOwner().User}else{'unknown'}}|select -unique
                        }else{
                            $process.UserName
                        }
                    $previousOwner
                }else{
                    $previousOwner
                }
                $matchedLines = $netStat|findstr $processId
                write-host "Process Id $processId matched $($matchedLines.count) lines"
                foreach($matchedLine in $matchedLines){                    
                    $line = $matchedLine.Split('') | where{$_ -ne ""} # remove empty lines
                    $leftCount = $line[1].LastIndexOf(':')
                    $rightCount = $line[2].LastIndexOf(':')
                    $sourceEndpoint=$line[1].SubString(0,$leftCount)+':'+$line[1].SubString($leftCount+1,($line[1].Length-$leftCount-1))
                    $destinationEndpoint=$line[2].SubString(0,$rightCount)+':'+$line[2].SubString($rightCount+1,($line[2].Length-$rightCount-1))
                    $results += [PSCustomObject]@{              
                        ComputerName = $env:computername
                        ProcessName  = $processName
                        PID = $processId
                        Protocol = $line[0]
                        SourceEndPoint = $sourceEndpoint
                        DestinationEndpoint = $destinationEndpoint
                        #LocalAddress = $line[1].SubString(0,$leftCount)
                        #LocalPort = $line[1].SubString($leftCount+1,($line[1].Length-$leftCount-1))
                        #RemoteAddress = $line[2].SubString(0,$rightCount)
                        #RemotePort = $line[2].SubString($rightCount+1,($line[2].Length-$rightCount-1))
                        ConnectionStatus = $(if(!($line[3] -match '\d')){$line[3]}) # Checking if the connection contains any empty string.
                        processOwner=$processOwner
                    }
                }
                $previousPid=$processId
            }            
        if($results){
            return $results
            }elseif($processNames -and $portNumbers){
                write-host "$processNames not found on $portNumbers" -ForegroundColor Yellow
                return $null                
            }else{
                write-host "No processes matched." -ForegroundColor Yellow
                return $null
            }
        }else{
            write-host "No processes matched." -ForegroundColor Yellow
            return $null
        }
    }
 
    $results=@()
    foreach ($computerName in $computerNames){
        $session=New-PSSession -ComputerName $computerName
        if($session.state -eq 'Opened'){
            $result=invoke-command -ComputerName $computerName -scriptblock{
                param($importFunc,$x,$y)
                write-host "Executing function on $env:computername"
                [scriptblock]::create($importFunc).invoke($x,$y)
            } -args ${function:getProcessConnections},$processNames,$portNumbers
            if($result){
                $results+=$result
            }else{
                write-host "No matches on $computerName"
            }
            Remove-PSSession $session
        }else{
            write-warning "Unable to connect to $computername"
        }
    }
    return $results
  }
  $excludeProcesses=@(
    'Idle',
    'SecurityHealthService',
    'services',
    'System',
    'svchost'
  )
  $establishedConnections=invokeGetProcessPorts $computer|?{$_.ConnectionStatus -eq 'ESTABLISHED' -and $_.ProcessName -notin $excludeProcesses}

  $checkDynamicPorts={
    $tcpSettings=Get-NetTCPSetting -Setting Internet
    $ephemeralStartPort=$tcpSettings.DynamicPortRangeStartPort
    $ephemeralEndPort=$ephemeralStartPort+$tcpSettings.DynamicPortRangeNumberOfPorts
    return [PSCustomObject][ordered]@{
        computerName=$env:computername
        ephemeralStartPort=$ephemeralStartPort
        ephemeralEndPort=$ephemeralEndPort
      }
    }
  $dynamicPorts=invoke-command -ComputerName $computer -ScriptBlock $checkDynamicPorts

  $result=($establishedConnections|Group-Object 'ProcessName','SourceEndPoint' | `
  %{$_.Group|Select 'ProcessName','Protocol','ProcessOwner','SourceEndPoint','DestinationEndPoint' -First 1} | `
  ?{($_.SourceEndPoint -split ':'|select -last 1) -lt $dynamicPorts.ephemeralStartPort -or ($_.DestinationEndPoint -split ':'|select -last 1) -lt $dynamicPorts.ephemeralStartPort} | `
  Sort 'ProcessName','SourceEndPoint','DestinationEndPoint'|ft|out-string).trim()

  return [PSCustomObject][ordered]@{
    computerName=$env:computername
    connections=$result
  }
}

function checkUsers($computername=$env:computername){
    $excludeServiceNames='conhost|dllhost|ctfmon|audiodg|cytray|ApplicationFrameHost|SYSTEM|LOCAL SERVICE|NETWORK SERVICE|DWM-\d+|UMFD-\d+'
    $volumes = (gwmi -Class win32_volume -computername $computername -Filter "DriveType!=5" -ea stop| `
        ?{$_.DriveLetter -ne $isnull}|Select-object `
            @{Name="Letter";Expression={$_.DriveLetter}}, `
            @{Name="Label";Expression={$_.Label}}, `
            @{Name="Capacity";Expression={"{0:N2} GiB" -f ($_.Capacity/1073741824)}}, `
            @{Name="Utilization";Expression={"{0:N2} %" -f ((($_.Capacity-$_.FreeSpace)/$_.Capacity)*100)}} | `
            sort -Property Letter|Out-String).Trim()
    $topProcesses=(get-process -computername $computername|Group-Object -Property ProcessName | `
        Select-Object -First 10 | `
        Select Name, `
        @{N='Memory (MB)';E={[math]::Round((($_.Group|Measure-Object WorkingSet -Sum).Sum/1MB),2)}} | `
        Sort-Object -Property "Memory (MB)" -Descending| `
        ?{$_.Name -ne '' -and $_.Name -notmatch $excludeServiceNames}| `
        Out-String).trim()
    $memoryUsagePerUser=(get-wmiobject win32_process -computername $computername|select @{N='User';E={$_.getowner().user}}, WorkingSetSize  | group user|
        select Name, @{N='Memory (MB)';E={[math]::Round(($_.Group.WorkingSetSize | `
        Measure-Object -Sum).Sum / 1Mb,2)}}|sort-object -property "Memory (MB)" -Descending| `
        ?{$_.Name -ne '' -and $_.Name -notmatch $excludeServiceNames}| `
        Out-String).trim()
    
    function getSessionsInfo([string[]]$computername=$env:computername){
        $results=@()
        function getDisconnectedSessionInfo($thisLine,$computer){
            # convert multiple spaces into single space and split pieces into array
            $sessionArray = $thisLine.Trim() -Replace '\s+',' ' -Split '\s'
            $properties = @{
                UserName = $sessionArray[0]
                ComputerName = $computer
                }
            $properties.SessionName = $null
            $properties.SessionId = $sessionArray[1]
            $properties.State = $sessionArray[2]                        
            $properties.IdleMinutes=.{
                    [string]$x=$sessionArray[3]
                    switch -regex ($x){
                        '\.' {0;break}                   
                        '\+' {$dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                $dayMinutes+$hourMinutes
                                break;
                                }               
                        '\:' {try{
                                ([TimeSpan]::Parse($x)).totalMinutes
                                }catch{
                                    "Invalid value: $x"
                                    }
                                break
                                }
                        default {$x}
                    }                                  
                }
            $properties.LogonTime = $sessionArray[4..6] -join ' '
            $result=New-Object -TypeName PSCustomObject -Property $properties
            return $result
            }
                
        function getActiveSessionInfo($thisLine,$computer){
            $sessionArray = $thisLine.Trim() -Replace '\s+',' ' -Split '\s'
            $properties = @{
                UserName = $sessionArray[0]
                ComputerName = $computer
                }
            $properties.SessionName = $sessionArray[1]
            $properties.SessionId = $sessionArray[2]
            $properties.State = $sessionArray[3]
            $properties.IdleMinutes=.{
                    $x=$sessionArray[4]
                    switch -regex ($x){
                        '\.' {0;break}                   
                        '\+' {  $dayMinutes=.{[void]($x -match '^(\d+)\+');[int]($matches[1])*1440}
                                $hourMinutes=.{[void]($x -match '\+(.*)$');([TimeSpan]::Parse($matches[1])).totalMinutes}
                                $dayMinutes+$hourMinutes
                                break;
                                }               
                        '\:' {  $timeFragments=[regex]::match($x,'(\d+)\:(\d+)')
                                [Int16]$hours=$timeFragments.Groups[1].Value
                                [Int16]$minutes=$timeFragments.Groups[2].Value
                                $minutes + ($hours * 60)
                                break
                                }
                        default {$x}
                    }
                }
            $properties.LogonTime = $sessionArray[5..($sessionArray.GetUpperBound(0))] -join ' '
            $result=New-Object -TypeName PSCustomObject -Property $properties
            return $result
            }
    
        foreach ($computer in $computername){
            try {
                # Perusing legacy commandlets as there are no PowerShell equivalents at this time
                $sessions=quser /server:$computer 2>&1 | Select-Object -Skip 1
                ForEach ($line in $sessions) {               
                    $fragments = $line.Trim() -Replace '\s+',' ' -Split '\s'
                    $disconnectedSession=$fragments[2] -eq 'Disc' -or $fragments[3] -eq 'Disc'
                    if ($disconnectedSession){
                        $result=getDisconnectedSessionInfo $line $computer
                    }else{
                        $result=getActiveSessionInfo $line $computer
                    }
                    if($result){$results+=$result}
                }
            }catch{
                $results+=New-Object -TypeName PSCustomObject -Property @{
                    ComputerName=$computer
                    Error=$_.Exception.Message
                } | Select-Object -Property UserName,ComputerName,SessionName,SessionId,State,IdleMinutes,LogonTime,Error|sort -Property UserName
            }
        }
        return $results
    }
    $activeUsers=((getSessionsInfo $computername|?{$_.State -eq 'Active'}).UserName|out-string).trim()
    return [PSCustomObject][ordered]@{
        computername=$computername
        memoryUsagePerUser=$memoryUsagePerUser
        activeUsers=$activeUsers
        volumes=$volumes
        topProcesses=$topProcesses
    }
}
function combineObjects($objects,$verbose=$false){  
  $combinedMembers=@()
  foreach($object in $objects){
      if($object.gettype().BaseType.Name -eq 'Object'){
          $combinedMembers+=get-member -InputObject $object -MemberType NoteProperty
      }else{
          write-warning "This item is NOT an object:`r`n$object"
      }
  }    
  $newObject=New-Object -TypeName PSObject
  foreach($property in $combinedMembers){
      $propertyName=$property.Name
      $value=$property.Definition -replace '^.*='
      $propertyExists=$propertyName -in ($newobject|get-member).Name
      if(!$propertyExists){
          Add-Member -InputObject $newObject -MemberType NoteProperty -Name $propertyName -Value $value -Force
      }elseif($verbose){
          $previousValue=($newobject|get-member|?{$_.Name -eq $propertyName}).Definition -replace '^.*='
          write-warning "$propertyName value of previous value of $previousValue has NOT been replaced with $value"
      }
  }
  return $newObject
}
function windowsDiscovery($computer=$env:computername){ 
  $session=try{New-PSSession $computer}catch{$false}
  if($session){
      Remove-PSSession $session
      $bios=checkBios $computer
      $spectreMitigation=checkSpectreVulnerability $computer
      $network=checkNetwork $computer        
      $security=checkSecurity $computer
      $hyperV=checkHyperVConfigs $computer
      $installedApps=checkInstalledApps $computer
      $iisInfo=checkIis $computer
      $dotNetInfo=checkDotNet $computer
      $connections=checkConnections $computer
      $users=checkUsers $computer
      $getIpCommand={
          $regexIpv4 = '\b(?:(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b'
          #[Net.DNS]::GetHostEntry($env:computername).AddressList.IPAddressToString|?{$_ -match $regexIpv4}
          $defaultInterface=Get-NetRoute -DestinationPrefix "0.0.0.0/0"
          return (Get-NetIPAddress -InterfaceIndex $defaultInterface.ifIndex).IPAddress -match $regexIpv4
      }
      #$getAntivirus={((get-wmiobject -class "Win32_Process" -namespace "root\cimv2"|where-object {$_.Name -match "antivirus|endpoint|protection|security|defender|msmpeng"}).Name | Out-String).Trim()}
      $computerObject=[pscustomobject][ordered]@{
          computerName=$computer
          ipv4Address=(invoke-command -computername $computer -scriptblock $getIpCommand|out-string).trim()
          lastOsUpdate=Invoke-command -ComputerName $computer -ScriptBlock {(Get-HotFix | Measure-Object InstalledOn -Maximum).Maximum.ToString().Trim()}
          #antivirus=Invoke-command -ComputerName $computer -ScriptBlock $getAntivirus
      }
      #$speculationControlSettings=checkSpeculationControls $computer
      $thisSystem=combineObjects @($computerObject,$bios,$spectreMitigation,$hyperV,$network,$security,$installedApps,$iisInfo,$dotNetInfo,$connections,$users)
  }else{
      Write-Warning "$computer is not Reachable from $env:computername via WinRM"
      $thisSystem=$false        
  }
  if($thisSystem){
      return $thisSystem|Select-Object -Property * -ExcludeProperty PSComputerName,RunspaceId,PSShowComputerName
  }else{
      return [PSCustomObject]@{
          computerName = $computer+' Unreachable'
          ipv4Addresses=[Net.DNS]::GetHostEntry($computer).AddressList.IPAddressToString|?{$_ -match $regexIpv4}
      }
  }
}

$thisSystem=windowsDiscovery