# hyperVServersReport.ps1
# Version: 0.0.1
# Description:
#   This script will scan for Hyper-V Hosts in the default domain,
#   and create a report in HTML format of general server settings:
#     - BIOS
#     - OS
#     - Security
#     - Networking

$reportName="Hyper-V Hosts Report for Domain $env:USERDNSDOMAIN"
$reportFilePath='C:\hyperVServersReport.html'
function getAllHyperVHosts{
    function getHyperVHostsInForest{
    function includeRSAT{
        $ErrorActionPreference='stop'
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
        #$rsatWindows7x32='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x86-RefreshPkg.msu'
        $rsatWindows7x64='https://download.microsoft.com/download/4/F/7/4F71806A-1C56-4EF2-9B4F-9870C4CFD2EE/Windows6.1-KB958830-x64-RefreshPkg.msu'
        $rsatWindows81='https://download.microsoft.com/download/1/8/E/18EA4843-C596-4542-9236-DE46F780806E/Windows8.1-KB2693643-x64.msu'
        $rsat1709 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1709-x64.msu"
        $rsat1803 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS_1803-x64.msu"
        $rsatWs2016 = "https://download.microsoft.com/download/1/D/8/1D8B5022-5477-4B9A-8104-6A71FF9D98AB/WindowsTH-RSAT_WS2016-x64.msu"
   
        # This command does not work on Windows 2012R2
        #$releaseId=(Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ReleaseId).ReleaseId
        #Get-ItemProperty : Property ReleaseId does not exist at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
        #NT\CurrentVersion.
        #At line:1 char:2
        #+ (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Na ...
        #+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        #    + CategoryInfo          : InvalidArgument: (ReleaseId:String) [Get-ItemProperty], PSArgumentException
        #    + FullyQualifiedErrorId : System.Management.Automation.PSArgumentException,Microsoft.PowerShell.Commands.GetItemPropertyCommand
   
        $releaseId=(Get-Item "HKLM:SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue('ReleaseID')
        $osVersion=[System.Environment]::OSVersion.Version
        [double]$osVersionMajorMinor="$($osVersion.Major).$($osVersion.Minor)" 
        $osName=(Get-WmiObject Win32_OperatingSystem).Name
        #$osType=switch ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType){
        #    1 {'client'}
        #    2 {'domaincontroller'}
        #    3 {'memberserver'}
        #    }
   
        $windowsVersion=(Get-CimInstance Win32_OperatingSystem).Version
   
        switch ($releaseId){
            1607{write-host 'Windows Server 2016 Release 1607 detected';$link=$rsatWs2016;break}
            1709{write-host 'Windows Server 2016 Release 1709 detected';$link=$rsat1709;break}
            1803{write-host 'Windows Server 2016 Release 1803 detected';$link=$rsat1803}
        }
       
        switch ($osVersionMajorMinor){
            {$_ -eq 6.0}{write-host 'Windows Server 2008 or Windows Vista detected';$link=$rsat1709;break}
            {$_ -eq 6.1}{write-host 'Windows Server 2008 R2 or Windows 7 detected';$link=$rsatWindows7x64;break}
            {$_ -eq 6.2}{write-host 'Windows Server 2012 or Windows 8.1 detected';$link=$rsatWindows81;break}
            {$_ -eq 6.3}{write-host 'Windows Server 2012 R2 detected';$link=$rsatWindows81}
        }
  
        if (!(Get-Module -ListAvailable -Name ActiveDirectory -EA SilentlyContinue)){
            Write-host "Prerequisite checks: module ActiveDirectory NOT currently available on this system. Please wait while the program adds that plugin..."
            try{
                # If OS is Windows Server, then install RSAT using a different method
                if ($osName -match "^Microsoft Windows Server") {
                    # This sequence has confirmed to be valid on Windows Server 2008 R2 and above
                    Write-Verbose "Importing Windows Feature: RSAT-AD-PowerShell"
                    Import-Module ServerManager
                    Add-WindowsFeature RSAT-AD-PowerShell
                    }
                else{
                    Write-Verbose "This sequence targets Windows Client versions"
                    $destinationFile= ($ENV:USERPROFILE) + "\Downloads\" + (split-path $link -leaf)
                    Write-Host "Downloading RSAT from $link..."
                    Start-BitsTransfer -Source $link -Destination $destinationFile
                    $fileCheck=Get-AuthenticodeSignature $destinationFile
                    if($fileCheck.status -ne "valid") {write-host "$destinationFile is not valid. Please try again...";break}
                    $wusaCommand = $destinationFile + " /quiet"
                    Write-host "Installing RSAT - please wait..."
                    Start-Process -FilePath "C:\Windows\System32\wusa.exe" -ArgumentList $wusaCommand -Wait
                    }
                return $true
                }
            catch{
                write-warning "$($error[0].Exception)"
                return $false
                }
        }else{
            Write-host "Prerequisite checks: module ActiveDirectory IS currently available on this system." -ForegroundColor Green
            return $true
            }
    }
    function listAllHyperVNodes($verbose=$true){
        try{
            $timer=[System.Diagnostics.Stopwatch]::StartNew()
            $domains=(Get-ADForest).Name|%{(Get-ADForest -Identity $_).Name}
            foreach ($domain in $domains){
                #[string]$dc=(get-addomaincontroller -DomainName "$domain" -Discover -NextClosestSite).HostName
                write-host "Collecting all Hyper-V Clusters in $domain. This may take a while, depending on cluster sizes."
                $allClusters=(get-cluster -domain $domain).Name
                # $allClusters=(get-cluster -domain $env:USERDNSDOMAIN).Name
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: cluster names collected"
                    }

                $allHyperVNodes=@()
                foreach ($cluster in $allClusters){
                    $nodes=.{$x=Get-ClusterNode -Cluster $cluster -ea SilentlyContinue
                            if($x){
                                $x|Where-Object{$_.State -eq 'Up'}|Select-Object Name,@{name='Cluster';e={$cluster}}
                            }else{
                                $false
                            }
                            }
                    if($nodes){$allHyperVNodes+=$nodes}
                }
                if($verbose){
                    $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
                    write-host "Minutes elapsed $elapsed`: Hyper Node names collected..."
                    }
                }                
            return $allHyperVNodes
        }catch{
            Write-Error $_
            return $false
            }
    }
 
    try{
        $null=includeRSAT;
        $hyperVHosts=listAllHyperVNodes
        $hyperVHostNames=$hyperVHosts|sort -property Cluster
        return $hyperVHostNames
    }catch{
        Write-Error $_
        return $false
        }
    }
    function sortArrayStringAsNumbers([string[]]$names){
        $hashTable=@{}
        foreach ($name in $names){
            #[int]$x=.{[void]($name -match '(?:.(\d+))+$');$matches[1]}
            #$x=.{[void]($name -match '(?:.(\d+)+)$');@($name.substring(0,$name.length-$matches[1].length),$matches[1])}
            $x=.{[void]($name -match '(?:.(\d+)+)$');($name.substring(0,$name.length-$matches[1].length))+$matches[1].PadLeft(8,'0')}
            $hashTable.Add($name,$x)
            }
        $sorted=foreach($item in $hashTable.GetEnumerator() | Sort Value){$item.Name}
        return $sorted
    }
    write-host "Obtaining cluster names and associated hosts..."
    $hyperVHostsInForest=getHyperVHostsInForest
    $hyperVHosts=sortArrayStringAsNumbers $hyperVHostsInForest.Name
    return $hyperVHosts
}
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 = $mitigationStatus
        }
    }
    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){
    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{
        #$vmSwitch=get-vmswitch -Computername $computer
        $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
        $virtualMachineMigrationPerformanceOption=(Get-VMHost).VirtualMachineMigrationPerformanceOption
        $cpuCompatiblityformigrationDisabled=(Get-VMProcessor -VMName *|?{$_.CompatibilityForMigrationEnabled -eq $false}).VMName
        $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
            virtualMachineMigrationPerformanceOption=$virtualMachineMigrationPerformanceOption
            volumes=($volumes|Sort-Object|ft -HideTableHeaders|out-string).trim() -replace '\s{2}',''
            cpuCompatiblityformigrationDisabled=($cpuCompatiblityformigrationDisabled|out-string).trim()
        }       
    }
    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 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 checkSystem($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
        $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)
    }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}
        }
    }
}

function getHyperVServersReport{
    param(
        $hyperVHosts,   
        $reportName="Hyper-V Hosts Report for Domain $env:USERDNSDOMAIN",
        $reportFilePath='C:\hyperVServersReport.html'        
    )
    $timer=[System.Diagnostics.Stopwatch]::StartNew()
    if(!$hyperVHosts){
        $hyperVHosts=getAllHyperVHosts
    }
    if($hyperVHosts){    
        $results=@()
        $hostCount=$hyperVHosts.Count
        for ($i=0;$i -lt $hostCount;$i++){
            write-host "Scanning $($i+1) of $hostCount`: $($hyperVHosts[$i])..."
            $results+=checkSystem $hyperVHosts[$i]
            $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
            write-host "Minutes elapsed: $elapsed"
        }
        # Source code adapted from https://learn.microsoft.com/en-us/archive/blogs/brandev/powershell-quick-html-reports-using-css-stylesheet
        $cssTest = "
        <style>
        h1, h3, h5, th { text-align: center; font-family: Segoe UI; }
        table {margin: auto; font-family: Segoe UI; box-shadow: 10px 10px 5px #888; border: thin ridge grey;
            #table-layout: fixed;
            }
        th { background: #0046c3; color: #fff; padding: 5px 10px; }
        td { font-size: 11px; padding: 5px 20px; color: #000;
                width: 1px;
            #overflow: hidden;
            white-space: pre;
            }
        tr { background: #b8d1f3;display: }
        tr:nth-child(even) { background: #dae5f4;}
        tr:nth-child(odd) { background: #b8d1f3; }
        </style>
        "
        $css="
        <style>
        h1, h3, h5, th { text-align: center; font-family: Segoe UI; }
        table {
  		border-collapse: separate;
  		border-spacing: 0;
  		border-top: 1px solid grey;
  		margin: auto;
  		box-shadow: 10px 10px 5px #888;
		}
		
		td, th {
  		margin: 0;
  		border: 1px solid grey;
  		white-space: pre;
  		border-top-width: 0px;
		}		
		div {
  		width: 500px;
  		overflow-x: scroll;
  		margin-left: 5em;
  		overflow-y: visible;
  		padding: 0;
		}
		
		.headcol {
  		position: absolute;
  		width: 5em;
  		left: 0;
  		top: auto;
  		border-top-width: 1px;
  		/*only relevant for first row*/
  		margin-top: -1px;
  		/*compensate for top border*/
		}		
		.headcol:before {
  		content: 'Row ';
		}		
		.long {
  		background: yellow;
  		letter-spacing: 1em;
		}
		th { background: #0046c3; color: #fff; padding: 5px 10px; }
		tr:nth-child(even) { background: white;}
        tr:nth-child(odd) { background: #dae5f4; }
        </style>
        "
        $results|convertTo-Html -Head $css -Body "<h1>$reportName</h1>`n<h5>Generated on $(Get-Date)</h5>"|Out-File $reportFilePath
        $elapsed=[math]::round($timer.Elapsed.TotalMinutes,2)
        write-host "Total run time: $elapsed"
        write-host "Report path: $reportFilePath"
    }else{
        write-warning "Hyper-V host names are required to run this program."
    }
}
getHyperVServersReport -reportName $reportName -reportFilePath $reportFilePath