Src/Private/Get-AbrEntraIDIdentityProtection.ps1
|
function Get-AbrEntraIDIdentityProtection { [CmdletBinding()] param ([Parameter(Position=0,Mandatory)][string]$TenantId) begin { Write-PScriboMessage "Collecting Identity Protection data for tenant $TenantId." Show-AbrDebugExecutionTime -Start -TitleMessage 'Identity Protection' } process { Section -Style Heading2 'Identity Protection' { Paragraph "The following section documents the Microsoft Entra ID Identity Protection configuration for tenant $TenantId, including risky users, risk detections, and risk-based Conditional Access policy coverage." BlankLine try { #region Risk Policy Summary Write-Host " - Retrieving Identity Protection risk policies..." $CAPolicies = Get-MgIdentityConditionalAccessPolicy -All -ErrorAction SilentlyContinue # Detect sign-in risk CA policies $SignInRiskPolicies = @($CAPolicies | Where-Object { $_.Conditions.SignInRiskLevels.Count -gt 0 -and $_.State -eq 'enabled' }) # Detect user risk CA policies $UserRiskPolicies = @($CAPolicies | Where-Object { $_.Conditions.UserRiskLevels.Count -gt 0 -and $_.State -eq 'enabled' }) # Medium+high risk coverage (CIS 5.2.2.8) $SignInRiskBlocksMedHigh = $false $SignInRiskBlocksMedHighDetail = 'No CA policy found covering both medium and high sign-in risk levels. [WARN]' foreach ($P in $SignInRiskPolicies) { $levels = $P.Conditions.SignInRiskLevels if ($levels -contains 'medium' -and $levels -contains 'high') { $SignInRiskBlocksMedHigh = $true $SignInRiskBlocksMedHighDetail = "Policy '$($P.DisplayName)' covers medium and high sign-in risk. [OK]" break } } $RiskPolicySumObj = [System.Collections.ArrayList]::new() $riskSumInObj = [ordered] @{ 'Sign-In Risk CA Policies (Enforced)' = $SignInRiskPolicies.Count 'User Risk CA Policies (Enforced)' = $UserRiskPolicies.Count 'Sign-In Risk Levels Covered' = if ($SignInRiskPolicies.Count -gt 0) { ($SignInRiskPolicies.Conditions.SignInRiskLevels | Select-Object -Unique) -join ', ' } else { 'None' } 'User Risk Levels Covered' = if ($UserRiskPolicies.Count -gt 0) { ($UserRiskPolicies.Conditions.UserRiskLevels | Select-Object -Unique) -join ', ' } else { 'None' } } $RiskPolicySumObj.Add([pscustomobject]$riskSumInObj) | Out-Null $null = (& { if ($HealthCheck.EntraID.ConditionalAccess) { $null = ($RiskPolicySumObj | Where-Object { [int]$_.'Sign-In Risk CA Policies (Enforced)' -eq 0 } | Set-Style -Style Warning | Out-Null) $null = ($RiskPolicySumObj | Where-Object { [int]$_.'User Risk CA Policies (Enforced)' -eq 0 } | Set-Style -Style Warning | Out-Null) } }) $RiskPolTableParams = @{ Name = "Risk-Based CA Policy Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($Report.ShowTableCaptions) { $RiskPolTableParams['Caption'] = "- $($RiskPolTableParams.Name)" } $RiskPolicySumObj | Table @RiskPolTableParams #endregion #region Risky Users Write-Host " - Retrieving risky users..." try { $RiskyUsers = Invoke-MgGraphRequest -Method GET ` -Uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers?$top=999&$orderby=riskLastUpdatedDateTime desc' ` -ErrorAction Stop $AllRiskyUsers = $RiskyUsers.value while ($RiskyUsers.'@odata.nextLink') { $RiskyUsers = Invoke-MgGraphRequest -Method GET -Uri $RiskyUsers.'@odata.nextLink' -ErrorAction Stop $AllRiskyUsers += $RiskyUsers.value } $HighRisk = @($AllRiskyUsers | Where-Object { $_.riskLevel -eq 'high' }).Count $MedRisk = @($AllRiskyUsers | Where-Object { $_.riskLevel -eq 'medium' }).Count $LowRisk = @($AllRiskyUsers | Where-Object { $_.riskLevel -eq 'low' }).Count $AtRisk = @($AllRiskyUsers | Where-Object { $_.riskState -in @('atRisk','confirmedCompromised') }).Count $Dismissed = @($AllRiskyUsers | Where-Object { $_.riskState -eq 'dismissed' }).Count $Remediated = @($AllRiskyUsers | Where-Object { $_.riskState -eq 'remediated' }).Count BlankLine $RiskyUserSumObj = [System.Collections.ArrayList]::new() $riskyUserInObj = [ordered] @{ 'Total Risky Users' = @($AllRiskyUsers).Count 'High Risk' = $HighRisk 'Medium Risk' = $MedRisk 'Low Risk' = $LowRisk 'Currently At Risk' = $AtRisk 'Confirmed Compromised' = @($AllRiskyUsers | Where-Object { $_.riskState -eq 'confirmedCompromised' }).Count 'Remediated' = $Remediated 'Dismissed' = $Dismissed } $RiskyUserSumObj.Add([pscustomobject]$riskyUserInObj) | Out-Null $null = (& { if ($HealthCheck.EntraID.MFA) { $null = ($RiskyUserSumObj | Where-Object { [int]$_.'High Risk' -gt 0 } | Set-Style -Style Critical | Out-Null) $null = ($RiskyUserSumObj | Where-Object { [int]$_.'Confirmed Compromised' -gt 0 } | Set-Style -Style Critical | Out-Null) $null = ($RiskyUserSumObj | Where-Object { [int]$_.'Medium Risk' -gt 0 } | Set-Style -Style Warning | Out-Null) } }) $RiskyUserSumTableParams = @{ Name = "Risky User Summary - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($Report.ShowTableCaptions) { $RiskyUserSumTableParams['Caption'] = "- $($RiskyUserSumTableParams.Name)" } $RiskyUserSumObj | Table @RiskyUserSumTableParams # Per-user risky detail at InfoLevel 2 if ($InfoLevel.IdentityProtection -ge 2 -and $AtRisk -gt 0) { $ActiveRiskyUsers = @($AllRiskyUsers | Where-Object { $_.riskState -in @('atRisk','confirmedCompromised') } | Select-Object -First 50) if ($ActiveRiskyUsers.Count -gt 0) { Section -Style Heading3 'Active Risky Users' { Paragraph "The following $($ActiveRiskyUsers.Count) user(s) are currently at risk or confirmed compromised in tenant $TenantId. Investigate and remediate immediately." BlankLine $RiskyUserDetailObj = [System.Collections.ArrayList]::new() foreach ($RU in ($ActiveRiskyUsers | Sort-Object riskLevel)) { $ruInObj = [ordered] @{ 'Display Name' = $RU.userDisplayName 'UPN' = $RU.userPrincipalName 'Risk Level' = $RU.riskLevel 'Risk State' = $RU.riskState 'Risk Detail' = if ($RU.riskDetail) { $RU.riskDetail } else { '--' } 'Last Updated' = if ($RU.riskLastUpdatedDateTime) { ([datetime]$RU.riskLastUpdatedDateTime).ToString('yyyy-MM-dd HH:mm') } else { '--' } } $RiskyUserDetailObj.Add([pscustomobject]$ruInObj) | Out-Null } $null = (& { if ($HealthCheck.EntraID.MFA) { $null = ($RiskyUserDetailObj | Where-Object { $_.'Risk Level' -eq 'high' -or $_.'Risk State' -eq 'confirmedCompromised' } | Set-Style -Style Critical | Out-Null) $null = ($RiskyUserDetailObj | Where-Object { $_.'Risk Level' -eq 'medium' } | Set-Style -Style Warning | Out-Null) } }) $RiskyDetailTableParams = @{ Name = "Active Risky Users - $TenantId"; List = $false; ColumnWidths = 16, 22, 10, 14, 20, 18 } if ($Report.ShowTableCaptions) { $RiskyDetailTableParams['Caption'] = "- $($RiskyDetailTableParams.Name)" } $RiskyUserDetailObj | Table @RiskyDetailTableParams $null = ($script:ExcelSheets['Risky Users'] = $RiskyUserDetailObj) } } } $null = ($script:ExcelSheets['Risk Summary'] = $RiskyUserSumObj) } catch { Write-AbrSectionError -Section 'Risky Users' -Message "Identity Protection data requires IdentityRiskyUser.Read.All scope or Entra ID P2 licence. Error: $($_.Exception.Message)" } #endregion #region Risk-Based CA Policy Detail if ($SignInRiskPolicies.Count -gt 0 -or $UserRiskPolicies.Count -gt 0) { BlankLine $AllRiskPolicies = @($SignInRiskPolicies) + @($UserRiskPolicies) | Select-Object -Unique Id, DisplayName, State, Conditions, GrantControls $RiskCAObj = [System.Collections.ArrayList]::new() foreach ($P in ($AllRiskPolicies | Sort-Object DisplayName)) { $rcaInObj = [ordered] @{ 'Policy Name' = $P.DisplayName 'State' = $P.State 'Sign-In Risk Levels'= if ($P.Conditions.SignInRiskLevels) { $P.Conditions.SignInRiskLevels -join ', ' } else { '--' } 'User Risk Levels' = if ($P.Conditions.UserRiskLevels) { $P.Conditions.UserRiskLevels -join ', ' } else { '--' } 'Grant Controls' = if ($P.GrantControls.BuiltInControls) { $P.GrantControls.BuiltInControls -join ', ' } else { '--' } } $RiskCAObj.Add([pscustomobject]$rcaInObj) | Out-Null } $RiskCATableParams = @{ Name = "Risk-Based CA Policies - $TenantId"; List = $false; ColumnWidths = 26, 12, 18, 18, 26 } if ($Report.ShowTableCaptions) { $RiskCATableParams['Caption'] = "- $($RiskCATableParams.Name)" } $RiskCAObj | Table @RiskCATableParams $null = ($script:ExcelSheets['Risk CA Policies'] = $RiskCAObj) } #endregion #region ACSC E8 + CIS Identity Protection Assessment BlankLine if ($script:IncludeACSCe8) { Paragraph "ACSC Essential Eight Maturity Level Assessment -- Identity Protection:" BlankLine #region ACSC E8 Assessment (definitions from Src/Compliance/ACSC.E8.json) $_ComplianceVars = @{ 'SignInRiskPolicies' = $SignInRiskPolicies 'UserRiskPolicies' = $UserRiskPolicies 'AtRisk' = $AtRisk 'SignInRiskBlocksMedHigh' = $SignInRiskBlocksMedHigh 'SignInRiskBlocksMedHighDetail'= $SignInRiskBlocksMedHighDetail } $E8IPChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrE8Checks -Section 'IdentityProtection') ` -Framework E8 ` -CallerVariables $_ComplianceVars New-AbrE8AssessmentTable -Checks $E8IPChecks -Name 'Identity Protection' -TenantId $TenantId # Consolidated into ACSC E8 Assessment sheet if ($E8IPChecks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8IPChecks | Select-Object @{N='Section';E={'Identity Protection'}}, ML, Control, Status, Detail ))) } #endregion } if ($script:IncludeCISBaseline) { BlankLine Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Identity Protection:" BlankLine #region CIS Assessment (definitions from Src/Compliance/CIS.M365.json) $CISIPChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrCISChecks -Section 'IdentityProtection') ` -Framework CIS ` -CallerVariables $_ComplianceVars New-AbrCISAssessmentTable -Checks $CISIPChecks -Name 'Identity Protection' -TenantId $TenantId # Consolidated into CIS Assessment sheet if ($CISIPChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISIPChecks | Select-Object @{N='Section';E={'Identity Protection'}}, CISControl, Level, Status, Detail ))) } #endregion } #endregion } catch { Write-AbrSectionError -Section 'Identity Protection' -Message "$($_.Exception.Message)" } } # end Section Identity Protection } end { Show-AbrDebugExecutionTime -End -TitleMessage 'Identity Protection' } } |