Public/Test-SecurityState.ps1
|
#Requires -Version 5.1 #Requires -PSEdition Core, Desktop using namespace System.Security.Cryptography.X509Certificates class SecurityInfoCheck { [int] $Id [string] $Status [string] $Category [string] $Test [Object] $Actual [Object] $Expected SecurityInfoCheck([int]$Id, [string]$Status, [string]$Category, [string]$Test, [Object]$Actual, [Object]$Expected) { $this.Id = $Id $this.Status = $Status $this.Category = $Category $this.Test = $Test $this.Actual = $Actual $this.Expected = $Expected } } function Test-CertificateExportability { param([X509Certificate2]$Cert) try { $Cert.Export(([X509ContentType]::Pfx)) | Out-Null return $true } catch { return $false } } <# .SYNOPSIS Analyzes the current system for PowerShell-related security signals. .OUTPUTS Returns a `[SecurityInfoCheck]` object. Additional context is available through verbose output. .EXAMPLE Test-SecurityState .EXAMPLE Test-SecurityState -Verbose .EXAMPLE Test-SecurityState -SkipModuleVersionTest -Verbose .EXAMPLE Test-SecurityState -SkipAlternateDataStreamTest -Verbose .EXAMPLE Test-SecurityState -SkipModuleVersionTest -SkipAlternateDataStreamTest -Verbose #> function Test-SecurityState { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock','')] [CmdletBinding()] param( # Skips the time-consuming module version checks that require an internet connection. [switch]$SkipModuleVersionTest, # Skips the time-consuming alternate data stream scan. [switch]$SkipAlternateDataStreamTest ) if (-not $IsWindows) { Write-Error 'Test-SecurityState is currently supported on Windows only.' return } $My = [HashTable]::Synchronized(@{}) $My.ESC = [char]0x1b $My.Status = $null $My.Result = $null $My.TargetResult = $null $My.Id = 1 $My.Neutral = 'Neutral' $My.Passed = "$($My.ESC)[92mPassed$($My.ESC)[0m" $My.Failed = "$($My.ESC)[91mFailed$($My.ESC)[0m" $My.Skip = "$($My.ESC)[95mSkipped$($My.ESC)[0m" $my.IsAdminRights = (New-Object -TypeName Security.Principal.WindowsPrincipal -ArgumentLIST ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::AdminISTrator) #region General [SecurityInfoCheck]::new($My.Id++, $My.Neutral, 'General', 'PSEdition', $PSVersionTable.PSEdition, '') "Check $($My.Id - 1): See about_PowerShell_Editions for more details." | Write-Verbose if ($PSVersionTable.PSVersion -le [Version]"2.0.0.0") { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++ , $My.Status, 'General', 'PSVersion', $PSVersionTable.PSVersion, '-gt 2.0') "Check $($My.Id - 1): Windows PowerShell 2.0 can bypass modern protections. Disable it through Windows features if it is still available." | Write-Verbose [SecurityInfoCheck]::new($My.Id++, $My.Neutral, 'General', 'Platform', $PSVersionTable.Platform, '') [SecurityInfoCheck]::new($My.Id++, $My.Neutral, 'General', 'CurrentPSHost', (Get-Host | Select-Object -ExpandProperty Name), '') "Check $($My.Id - 1): Other common PowerShell hosts include ConsoleHost, Visual Studio Code Host, and Windows PowerShell ISE Host." | Write-Verbose if ($ExecutionContext.SessionState.LanguageMode -ieq "FullLanguage") { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'General', 'PSSessionLanguageMode', ($ExecutionContext.SessionState.LanguageMode), 'ConstrainedLanguage') "Check $($My.Id - 1): See about_Language_Modes for more details." | Write-Verbose if ((Get-ExecutionPolicy) -ine "AllSigned") { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'General', 'ExecutionPolicy', (Get-ExecutionPolicy), 'AllSigned') "Check $($My.Id - 1): A strict baseline would use AllSigned. For development work, RemoteSigned is often the more practical choice." | Write-Verbose if ($my.IsAdminRights) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'General', 'Elevated admin rights', $my.IsAdminRights, $false) #endregion #region PROFILE $my.Result = Test-Path -Path $PROFILE.AllUsersAllHosts -PathType Leaf if ($My.Result) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'Profile file present', 'AllUsersAllHosts', $My.Result, $false) $my.Result = Test-Path -Path $PROFILE.AllUsersCurrentHost -PathType Leaf if ($My.Result) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'Profile file present', 'AllUsersCurrentHost', $My.Result, $false) "Check $($My.Id - 1): Also review profile paths used by other PowerShell hosts." | Write-Verbose $my.Result = Test-Path -Path $PROFILE.CurrentUserAllHosts -PathType Leaf if ($My.Result) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'Profile file present', 'CurrentUserAllHosts', $My.Result, $false) $my.Result = Test-Path -Path $PROFILE.CurrentUserCurrentHost -PathType Leaf if ($My.Result) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'Profile file present', 'CurrentUserCurrentHost', $My.Result, $false) "Check $($My.Id - 1): Also review profile paths used by other PowerShell hosts." | Write-Verbose #endregion #region Microsoft Defender Antivirus $my.Result = Get-MpPreference | Select-Object -ExpandProperty ExclusionPath if ($null -ne $My.Result) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++, $My.Status , 'Defender', 'ExclusionPath', $My.Result, 'No Exclusion Paths') #endregion #region Module Version Check $My.Status = $my.Skip ; $My.Result = 'Check skipped' ; $My.TargetResult = 'Check skipped' if (-not $SkipModuleVersionTest) { $My.Status = $My.Passed ; $My.Result = 'Not required in PowerShell 7' ; $My.TargetResult = 'Not required in PowerShell 7' if ($PSVersionTable.PSVersion -lt [Version]'6.0') { $My.Result = (Get-PackageProvider -Name NuGet).Version $My.TargetResult = (Find-PackageProvider -Name NuGet).Version if ($My.Result -le $My.TargetResult) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } } } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'PackageProvider', 'NuGet', $My.Result, $My.TargetResult) "Check $($My.Id - 1): This check is only relevant for Windows PowerShell 5.1." | Write-Verbose #endregion #region Module Version Check $My.Status = $my.Skip ; $My.Result = 'Check skipped' ; $My.TargetResult = 'Check skipped' if (-not $SkipModuleVersionTest) { $My.Result = (Get-Module -Name PackageManagement -LISTAvailable -Verbose:$false).Version | Sort-Object -Descending | Select-Object -First 1 $My.TargetResult = [Version](Find-Module -Name PackageManagement).Version if ($My.Result -lt $My.TargetResult) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'Module', 'PackageManagement', $My.Result, $My.TargetResult) $My.Status = $my.Skip ; $My.Result = 'Check skipped' ; $My.TargetResult = 'Check skipped' if (-not $SkipModuleVersionTest) { $My.Result = (Get-Module -Name PowerShellGet -LISTAvailable -Verbose:$false).Version | Sort-Object -Descending | Select-Object -First 1 $My.TargetResult = [Version](Find-Module -Name PowerShellGet).Version if ($My.Result -lt $My.TargetResult) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'Module', 'PowerShellGet', $My.Result, $My.TargetResult) $My.Status = $my.Skip ; $My.Result = 'Check skipped' ; $My.TargetResult = 'Check skipped' if (-not $SkipModuleVersionTest) { $My.Result = (Get-Module -Name PSScriptAnalyzer -LISTAvailable -Verbose:$false).Version | Sort-Object -Descending | Select-Object -First 1 $My.TargetResult = [Version](Find-Module -Name PSScriptAnalyzer).Version if ($My.Result -lt $My.TargetResult) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'Module', 'PSScriptAnalyzer', $My.Result, $My.TargetResult) $My.Status = $my.Skip ; $My.Result = 'Check skipped' ; $My.TargetResult = 'Check skipped' if (-not $SkipModuleVersionTest) { $My.Result = (Get-Module -Name Pester -LISTAvailable -Verbose:$false).Version | Sort-Object -Descending | Select-Object -First 1 $My.TargetResult = [Version](Find-Module -Name Pester).Version if ($My.Result -lt $My.TargetResult) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'Module', 'Pester', $My.Result, $My.TargetResult) #endregion #region ScriptBlockLogging $My.Result = Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'EnableScriptBlockLogging' -ErrorAction SilentlyContinue if ($My.Result -eq 1) { $My.Status = $My.Passed } else { $My.Status = $My.Failed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'ScriptBlockLogging', 'Windows PowerShell 64bit', $My.Result, 1) "Check $($My.Id - 1): Script block logging should be enabled, ideally with protected logging. See about_Logging_Windows for more details." | Write-Verbose $My.Result = Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'EnableScriptBlockLogging' -ErrorAction SilentlyContinue if ($My.Result -eq 1) { $My.Status = $My.Passed } else { $My.Status = $My.Failed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'ScriptBlockLogging', 'Windows PowerShell 32bit', $My.Result, 1) "Check $($My.Id - 1): Script block logging should be enabled, ideally with protected logging. See about_Logging_Windows for more details." | Write-Verbose $My.Result = Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\PowerShellCore\ScriptBlockLogging' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty 'EnableScriptBlockLogging' -ErrorAction SilentlyContinue if ($My.Result -eq 1) { $My.Status = $My.Passed } else { $My.Status = $My.Failed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'ScriptBlockLogging', 'PowerShell 6+ 64bit', $My.Result, 1) "Check $($My.Id - 1): Script block logging should be enabled, ideally with protected logging. See about_Logging_Windows for more details." | Write-Verbose #endregion #region Just Enough Administration if($my.IsAdminRights) { $My.Result = Get-PSSessionConfiguration -Verbose:$false | Where-Object -Property Name -NotMatch -Value 'PowerShell' | Select-Object -ExpandProperty Name } else { "Check $($My.Id - 1): Warning: no elevated admin rights available. Run this check again from an elevated session." | Write-Warning $My.Result = "n/v" } [SecurityInfoCheck]::new($My.Id++, $My.Neutral, 'JEA', 'SessionConfigurations', $my.Result, '') "Check $($My.Id - 1): Consider using JEA for non-administrators or accounts with administrative tasks." | Write-Verbose #endregion #region Alternate Data Stream $My.Status = $My.Skip; $My.Result = 'Check skipped' if (-not $SkipAlternateDataStreamTest) { $My.CurrentCounter = 0 $My.Status = $My.Neutral $My.Result = Get-ChildItem -Path c:\ -File -Force -Recurse -ErrorAction Ignore | ForEach-Object -Process { $My.CurrentCounter++ if (($My.CurrentCounter % 250) -eq 0) { Write-Progress -Activity 'Scanning files for alternate data streams' -Status "$($My.CurrentCounter) files scanned" } try { Get-Item -Path $_.FullName -Stream * -ErrorAction Ignore | Where-Object -Property Stream -ne ':$DATA' } catch { } } | Group-Object -Property Stream | Sort-Object -Descending Count | Select-Object -Property Count, Name, @{Name = 'Sum'; Expression = { ($_.Group | Measure-Object -Property Length -Sum | Select-Object -ExpandProperty Sum) / 1KB } }, Group Write-Progress -Activity 'Scanning files for alternate data streams' -Completed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'Alternate Data Stream', 'Grouped actual result', $My.Result, 'Zero ADS strategy') "Check $($My.Id - 1): Review suspicious or large ADS entries manually. See the grouped actual result for details." | Write-Verbose #endregion #region PKI $My.Result = Get-ChildItem -Path 'Cert:\' -Recurse -Force | Where-Object -Property 'HasPrivateKey' | ForEach-Object -Process { if ((Test-CertificateExportability $_)) { return "SUBJECT: $($_.Subject) THUMBPRINT: $($_.Thumbprint)" } } if ($null -ne $My.Result) { $My.Status = $My.Failed } else { $My.Status = $My.Passed } [SecurityInfoCheck]::new($My.Id++, $My.Status, 'PKI', 'ExportablePrivateCertificate', $My.Result, 'Zero strategy') "Check $($My.Id - 1): Certificates with private keys should not be exportable together with that key material." | Write-Verbose #endregion Remove-Variable -Name My -Force -ErrorAction Ignore } <# KOMPONENTEN TEST Update-FormatData -PrependPath '.\Modules\PowerShellBuddy\Public\Test-SecurityState.Format.ps1xml' Test-SecurityState Test-SecurityState -Verbose Test-SecurityState -SkipModuleVersionTest -Verbose Test-SecurityState -SkipAlternateDataStreamTest -Verbose Test-SecurityState -SkipModuleVersionTest -SkipAlternateDataStreamTest -Verbose #> |