Src/Private/Get-AbrADSecurityAssessment.ps1

function Get-AbrADSecurityAssessment {
    <#
    .SYNOPSIS
    Used by As Built Report to retrieve Microsoft AD Account Security Assessment information.
    .DESCRIPTION
 
    .NOTES
        Version: 0.9.11
        Author: Jonathan Colon
        Twitter: @jcolonfzenpr
        Github: rebelinux
    .EXAMPLE
 
    .LINK
 
    #>

    [CmdletBinding()]
    param (
        $Domain
    )

    begin {
        Write-PScriboMessage -Message "Collecting Account Security Assessment information on $($Domain.DNSRoot)."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'AD Account Security Assessment'
    }

    process {
        if ($HealthCheck.Domain.Security) {
            try {
                $LastLoggedOnDate = $(Get-Date) - $(New-TimeSpan -Days 180)
                $PasswordStaleDate = $(Get-Date) - $(New-TimeSpan -Days 180)
                $DomainUsers = $Users
                $DomainEnabledUsers = $DomainUsers | Where-Object { $_.Enabled -eq $True } | Measure-Object
                $DomainDisabledUsers = $DomainUsers | Where-Object { $_.Enabled -eq $false } | Measure-Object
                $DomainEnabledInactiveUsers = $DomainEnabledUsers | Where-Object { ($_.LastLogonDate -le $LastLoggedOnDate) -and ($_.PasswordLastSet -le $PasswordStaleDate) } | Measure-Object
                $DomainUsersWithReversibleEncryptionPasswordArray = $DomainUsers | Where-Object { $_.UserAccountControl -band 0x0080 } | Measure-Object
                $DomainUserPasswordNotRequiredArray = $DomainUsers | Where-Object { $_.PasswordNotRequired -eq $True } | Measure-Object
                $DomainUserPasswordNeverExpiresArray = $DomainUsers | Where-Object { $_.PasswordNeverExpires -eq $True } | Measure-Object
                $DomainKerberosDESUsersArray = $DomainUsers | Where-Object { $_.UserAccountControl -band 0x200000 } | Measure-Object
                $DomainUserDoesNotRequirePreAuthArray = $DomainUsers | Where-Object { $_.DoesNotRequirePreAuth -eq $True } | Measure-Object
                $DomainUsersWithSIDHistoryArray = $DomainUsers | Where-Object { $_.SIDHistory -like '*' } | Measure-Object
                if ($DomainUsers) {
                    $OutObj = [System.Collections.ArrayList]::new()
                    try {
                        $inObj = [ordered] @{
                            'Total' = $DomainUsers.Count
                            'Enabled' = $DomainEnabledUsers.Count
                            'Disabled' = $DomainDisabledUsers.Count
                            'Enabled Inactive' = $DomainEnabledInactiveUsers.Count
                            'Reversible Encryption Password' = $DomainUsersWithReversibleEncryptionPasswordArray.Count
                            'Password Not Required' = $DomainUserPasswordNotRequiredArray.Count
                            'Password Never Expires' = $DomainUserPasswordNeverExpiresArray.Count
                            'Kerberos DES' = $DomainKerberosDESUsersArray.Count
                            'Does Not Require Pre Auth' = $DomainUserDoesNotRequirePreAuthArray.Count
                            'SID History' = $DomainUsersWithSIDHistoryArray.Count
                        }
                        $OutObj.Add([pscustomobject](ConvertTo-HashToYN $inObj)) | Out-Null
                    } catch {
                        Write-PScriboMessage -IsWarning -Message "$($_.Exception.Message) (Account Security Assessment Item)"
                    }

                    if ($HealthCheck.Domain.Security) {
                        $OutObj | Where-Object { $_.'Enabled Inactive' -gt 0 } | Set-Style -Style Warning -Property 'Enabled Inactive'
                        $OutObj | Where-Object { $_.'Reversible Encryption Password' -gt 0 } | Set-Style -Style Warning -Property 'Reversible Encryption Password'
                        $OutObj | Where-Object { $_.'Password Not Required' -gt 0 } | Set-Style -Style Warning -Property 'Password Not Required'
                        $OutObj | Where-Object { $_.'Password Never Expires' -gt 0 } | Set-Style -Style Warning -Property 'Password Never Expires'
                        $OutObj | Where-Object { $_.'Kerberos DES' -gt 0 } | Set-Style -Style Warning -Property 'Kerberos DES'
                        $OutObj | Where-Object { $_.'Does Not Require Pre Auth' -gt 0 } | Set-Style -Style Warning -Property 'Does Not Require Pre Auth'
                        $OutObj | Where-Object { $_.'SID History' -gt 0 } | Set-Style -Style Warning -Property 'SID History'
                    }

                    $TableParams = @{
                        Name = "User Account Security Assessment - $($Domain.DNSRoot.ToString().ToUpper())"
                        List = $true
                        ColumnWidths = 40, 60
                    }

                    if ($Report.ShowTableCaptions) {
                        $TableParams['Caption'] = "- $($TableParams.Name)"
                    }

                    try {
                        $sampleData = $inObj.GetEnumerator() | Select-Object @{ Name = 'Category'; Expression = { $_.key } }, @{ Name = 'Value'; Expression = { $_.value } }
                        $Chart = New-PieChart -Values $sampleData.Value -Labels $sampleData.Category -Title 'User Account Security Assessment' -EnableLegend -LegendOrientation Horizontal -LegendAlignment UpperCenter -Width 600 -Height 600 -Format base64 -TitleFontSize 20 -TitleFontBold -EnableCustomColorPalette -CustomColorPalette $AbrCustomPalette -EnableChartBorder -ChartBorderStyle DenselyDashed -ChartBorderColor DarkBlue
                    } catch {
                        Write-PScriboMessage -IsWarning -Message "$($_.Exception.Message) (User Account Security Assessment Chart)"
                    }
                    if ($OutObj) {
                        Section -ExcludeFromTOC -Style NOTOCHeading4 'User Account Security Assessment' {
                            Paragraph "The following section provides a comprehensive summary of account security posture and potential vulnerabilities within the domain $($Domain.DNSRoot.ToString().ToUpper())."
                            BlankLine
                            if ($Chart) {
                                Image -Text 'User Account Security Assessment - Diagram' -Align 'Center' -Percent 100 -Base64 $Chart
                            }
                            $OutObj | Table @TableParams
                            Paragraph 'Health Check:' -Bold -Underline
                            BlankLine
                            Paragraph {
                                Text 'Corrective Actions:' -Bold
                                Text 'Ensure there are no accounts with a weak security posture.' }
                        }
                    }
                } else {
                    Write-PScriboMessage -Message "No Domain users information found in $($Domain.DNSRoot), Disabling this section."
                }
            } catch {
                Write-PScriboMessage -IsWarning -Message "$($_.Exception.Message) (Account Security Assessment Table)"
            }
            if ($InfoLevel.Domain -ge 2) {
                try {
                    if ($PrivilegedUsers) {
                        Section -ExcludeFromTOC -Style NOTOCHeading4 'Privileged Users Assessment' {
                            Paragraph "The following section provides a detailed assessment of privileged administrative accounts (user accounts with AdminCount attribute set to 1) within the domain $($Domain.DNSRoot.ToString().ToUpper())."
                            BlankLine
                            $OutObj = [System.Collections.ArrayList]::new()
                            $AccountNotDelegated = $PrivilegedUsers | Where-Object { -not $_.AccountNotDelegated -and $_.objectClass -eq 'user' }
                            foreach ($PrivilegedUser in $PrivilegedUsers) {
                                try {
                                    $inObj = [ordered] @{
                                        'Username' = $PrivilegedUser.SamAccountName
                                        'Password Last Set' = switch ($PrivilegedUser.PasswordLastSet) {
                                            $Null { '--' }
                                            default { $PrivilegedUser.PasswordLastSet.ToShortDateString() }
                                        }
                                        'Last Logon Date' = switch ($PrivilegedUser.LastLogonDate) {
                                            $Null { '--' }
                                            default { $PrivilegedUser.LastLogonDate.ToShortDateString() }
                                        }
                                        'Email Enabled?' = switch ([string]::IsNullOrEmpty($PrivilegedUser.EmailAddress)) {
                                            $true { 'No' }
                                            $false { 'Yes' }
                                            default { 'Unknown' }
                                        }
                                        'Trusted for Delegation' = switch ([string]::IsNullOrEmpty(($AccountNotDelegated | Where-Object { $_.SamAccountName -eq $PrivilegedUser.SamAccountName }))) {
                                            $true { 'No' }
                                            $false { 'Yes' }
                                            default { 'Unknown' }
                                        }
                                    }
                                    $OutObj.Add([pscustomobject](ConvertTo-HashToYN $inObj)) | Out-Null
                                } catch {
                                    Write-PScriboMessage -IsWarning -Message "$($_.Exception.Message) (Privileged Users Assessment Item)"
                                }
                            }

                            if ($HealthCheck.Domain.Security) {
                                foreach ( $OBJ in ($OutObj | Where-Object { $_.'Email Enabled?' -eq 'Yes' })) {
                                    $OBJ.'Email Enabled?' = "* $($OBJ.'Email Enabled?')"
                                }
                                $OutObj | Where-Object { $_.'Email Enabled?' -eq '* Yes' } | Set-Style -Style Warning -Property 'Email Enabled?'

                                foreach ( $OBJ in ($OutObj | Where-Object { $_.'Trusted for Delegation' -eq 'Yes' })) {
                                    $OBJ.'Trusted for Delegation' = "** $($OBJ.'Trusted for Delegation')"
                                }
                                $OutObj | Where-Object { $_.'Trusted for Delegation' -eq '** Yes' } | Set-Style -Style Warning -Property 'Trusted for Delegation'
                            }

                            $TableParams = @{
                                Name = "Privileged User Assessment - $($Domain.DNSRoot.ToString().ToUpper())"
                                List = $false
                                ColumnWidths = 40, 15, 15, 15, 15
                            }

                            if ($Report.ShowTableCaptions) {
                                $TableParams['Caption'] = "- $($TableParams.Name)"
                            }
                            $OutObj | Table @TableParams
                            if (($OutObj | Where-Object { $_.'Trusted for Delegation' -eq '** Yes' }) -or ($OutObj | Where-Object { $_.'Email Enabled?' -eq '* Yes' })) {
                                Paragraph 'Health Check:' -Bold -Underline
                                BlankLine
                                Paragraph 'Security Best Practice:' -Bold
                                BlankLine
                                if ($OutObj | Where-Object { $_.'Email Enabled?' -eq '* Yes' }) {
                                    Paragraph {
                                        Text '* Privileged accounts such as those belonging to any of the Administrators groups must not have configured email.'
                                    }
                                    BlankLine
                                }
                                if ($OutObj | Where-Object { $_.'Trusted for Delegation' -eq '** Yes' }) {
                                    Paragraph {
                                        Text '** Privileged accounts such as those belonging to any of the administrator groups must not be trusted for delegation. Allowing privileged accounts to be trusted for delegation provides a means for privilege escalation from a compromised system. Delegation of privileged accounts must be prohibited.'
                                        Text 'Reference: '
                                        Text 'https://www.stigviewer.com/stig/active_directory_domain/2017-12-15/finding/V-36435' -Color blue
                                    }
                                }
                            }
                        }
                    } else {
                        Write-PScriboMessage -Message "No Privileged User Assessment information found in $($Domain.DNSRoot), Disabling this section."
                    }
                } catch {
                    Write-PScriboMessage -IsWarning -Message "$($_.Exception.Message) (Privileged Users Table)"
                }
                try {
                    $InactivePrivilegedUsers = $PrivilegedUsers | Where-Object { ($_.LastLogonDate -le (Get-Date).AddDays(-30)) -and ($_.PasswordLastSet -le (Get-Date).AddDays(-365)) -and ($_.SamAccountName -ne 'krbtgt') -and ($_.SamAccountName -ne 'Administrator') }
                    if ($InactivePrivilegedUsers) {
                        Section -ExcludeFromTOC -Style NOTOCHeading4 'Inactive Privileged Accounts' {
                            Paragraph "The following section identifies privileged accounts in domain $($Domain.DNSRoot.ToString().ToUpper()) that have remained inactive for over 30 days and have not had their passwords changed in at least 365 days."
                            BlankLine
                            $OutObj = [System.Collections.ArrayList]::new()
                            foreach ($InactivePrivilegedUser in $InactivePrivilegedUsers) {
                                try {
                                    $inObj = [ordered] @{
                                        'Username' = $InactivePrivilegedUser.SamAccountName
                                        'Created' = switch ($InactivePrivilegedUser.Created) {
                                            $Null { '--' }
                                            default { $InactivePrivilegedUser.Created.ToShortDateString() }
                                        }
                                        'Password Last Set' = switch ($InactivePrivilegedUser.PasswordLastSet) {
                                            $Null { '--' }
                                            default { $InactivePrivilegedUser.PasswordLastSet.ToShortDateString() }
                                        }
                                        'Last Logon Date' = switch ($InactivePrivilegedUser.LastLogonDate) {
                                            $Null { '--' }
                                            default { $InactivePrivilegedUser.LastLogonDate.ToShortDateString() }
                                        }
                                    }
                                    $OutObj.Add([pscustomobject](ConvertTo-HashToYN $inObj)) | Out-Null
                                } catch {
                                    Write-PScriboMessage -IsWarning -Message "$($_.Exception.Message) (Inactive Privileged Accounts Item)"
                                }
                            }

                            if ($HealthCheck.Domain.Security) {
                                $OutObj | Set-Style -Style Warning
                            }

                            $TableParams = @{
                                Name = "Inactive Privileged Accounts - $($Domain.DNSRoot.ToString().ToUpper())"
                                List = $false
                                ColumnWidths = 40, 20, 20, 20
                            }

                            if ($Report.ShowTableCaptions) {
                                $TableParams['Caption'] = "- $($TableParams.Name)"
                            }
                            $OutObj | Table @TableParams
                            Paragraph 'Health Check:' -Bold -Underline
                            BlankLine
                            Paragraph {
                                Text 'Corrective Actions:' -Bold
                                Text 'Unused or underutilized accounts in highly privileged groups, outside of any break-glass emergency accounts like the default Administrator account, should have their AD Admin privileges removed.'
                            }
                        }
                    } else {
                        Write-PScriboMessage -Message "No Inactive Privileged Accounts information found in $($Domain.DNSRoot), Disabling this section."
                    }
                } catch {
                    Write-PScriboMessage -IsWarning -Message "$($_.Exception.Message) (Inactive Privileged Accounts Table)"
                }
                try {
                    $UserSPNs = Invoke-CommandWithTimeout -Session $TempPssSession -ScriptBlock { Get-ADUser -ResultPageSize 1000 -Server ($using:Domain).DNSRoot -Filter { ServicePrincipalName -like '*' } -Properties AdminCount, PasswordLastSet, LastLogonDate, ServicePrincipalName, TrustedForDelegation, TrustedtoAuthForDelegation }
                    if ($UserSPNs) {
                        Section -ExcludeFromTOC -Style NOTOCHeading4 'Service Accounts Assessment (Kerberoastable)' {
                            Paragraph "The following section provides an overview of service accounts (user accounts with Service Principal Names) that are potentially vulnerable to Kerberoasting attacks in domain $($Domain.DNSRoot.ToString().ToUpper())."
                            BlankLine
                            $OutObj = [System.Collections.ArrayList]::new()
                            $AdminCount = ($UserSPNs | Where-Object { $_.AdminCount -eq 1 -and $_.SamAccountName -ne 'krbtgt' }).Name
                            foreach ($UserSPN in $UserSPNs) {
                                try {
                                    $inObj = [ordered] @{
                                        'Username' = $UserSPN.SamAccountName
                                        'Enabled' = $UserSPN.Enabled
                                        'Password Last Set' = switch ($UserSPN.PasswordLastSet) {
                                            $Null { '--' }
                                            default { $UserSPN.PasswordLastSet.ToShortDateString() }
                                        }
                                        'Last Logon Date' = switch ($UserSPN.LastLogonDate) {
                                            $Null { '--' }
                                            default { $UserSPN.LastLogonDate.ToShortDateString() }
                                        }
                                        'Service Principal Name' = $UserSPN.ServicePrincipalName
                                    }
                                    $OutObj.Add([pscustomobject](ConvertTo-HashToYN $inObj)) | Out-Null
                                } catch {
                                    Write-PScriboMessage -IsWarning -Message "$($_.Exception.Message) (Service Accounts Assessment Item)"
                                }
                            }

                            if ($HealthCheck.Domain.Security) {
                                $OutObj | Where-Object { $_.'Username' -in $AdminCount } | Set-Style -Style Critical

                                foreach ( $OBJ in ($OutObj | Where-Object { $_.'Username' -in $AdminCount })) {
                                    $OBJ.Username = "** $($OBJ.Username)"
                                }
                            }

                            $TableParams = @{
                                Name = "Service Accounts Assessment - $($Domain.DNSRoot.ToString().ToUpper())"
                                List = $false
                                ColumnWidths = 30, 12, 14, 14, 30
                            }

                            if ($Report.ShowTableCaptions) {
                                $TableParams['Caption'] = "- $($TableParams.Name)"
                            }
                            $OutObj | Table @TableParams
                            Paragraph 'Health Check:' -Bold -Underline
                            BlankLine
                            if ($OutObj | Where-Object { $_.'Username' -match '\*' }) {
                                Paragraph {
                                    Text 'Security Best Practice:' -Bold

                                    Text '** Attackers are most interested in Service Accounts that are members of highly privileged groups like Domain Admins. A quick way to check for this is to enumerate all user accounts with the attribute AdminCount equal to 1. This means an attacker may just ask Active Directory for all user accounts with an SPN and with AdminCount=1. Ensure that there are no privileged accounts that have SPNs assigned to them.'
                                }
                            }
                        }
                    } else {
                        Write-PScriboMessage -Message "No Service Accounts Assessment information found in $($Domain.DNSRoot), Disabling this section."
                    }
                } catch {
                    Write-PScriboMessage -IsWarning -Message "$($_.Exception.Message) (Service Accounts Assessment Table)"
                }
            }
        }
    }
    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'AD Account Security Assessment'
    }
}