Public/Get-NSPConditionalAccessReportOnlyImpact.ps1
|
function Get-NSPConditionalAccessReportOnlyImpact { <# .SYNOPSIS Read-only: summarizes what each report-only policy would have done, from recent sign-in logs. .DESCRIPTION Reads interactive sign-ins from the last -Days days and counts, per policy, the report-only results Entra recorded: - WouldBlock: reportOnlyFailure (the policy would have blocked the sign-in). - WouldInterrupt: reportOnlyInterrupted (the user would have been prompted, e.g. for MFA). - Satisfied: reportOnlySuccess (the sign-in already met the policy). UsersAffected counts distinct users with a WouldBlock or WouldInterrupt result. Reading stops after -MaxSignIns sign-ins (newest first); Truncated says so. Sign-in logs need Entra ID P1. Without it, or without AuditLog.Read.All, Available is false and Reason says why; nothing throws, so promotion planning can still show the other gates. -Save writes the result under .nsp-conditionalaccess\impact (Git-ignored). .EXAMPLE $impact = Get-NSPConditionalAccessReportOnlyImpact -Days 14 -Save $impact.Policies | Format-Table DisplayName, WouldBlock, WouldInterrupt, UsersAffected #> [CmdletBinding(SupportsShouldProcess)] param( [ValidateRange(1, 30)][int]$Days = 7, [ValidateRange(100, 500000)][int]$MaxSignIns = 20000, [switch]$Save, [string]$ConfigRoot ) Import-NSPM365Graph $context = Assert-NSPConditionalAccessSession -ConfigRoot $ConfigRoot function Get-Value { param($Object, [string]$Key) Get-NSPConditionalAccessMapValue $Object $Key } $since = (Get-Date).ToUniversalTime().AddDays(-$Days).ToString('yyyy-MM-ddTHH:mm:ssZ') $result = [pscustomobject][ordered]@{ SchemaVersion = 1 TenantId = $context.TenantId CapturedAtUtc = (Get-Date).ToUniversalTime().ToString('o') Days = $Days SinceUtc = $since Available = $true Reason = $null SignInsRead = 0 Truncated = $false Policies = @() Path = $null } $stats = @{} $next = "auditLogs/signIns?`$filter=$([uri]::EscapeDataString("createdDateTime ge $since"))&`$top=999" try { while ($next -and $result.SignInsRead -lt $MaxSignIns) { Write-NSPConditionalAccessProgress -Activity Impact -Status "Last $Days day(s): $($result.SignInsRead) sign-ins read" -Step $result.SignInsRead -Of $MaxSignIns $page = Invoke-NSPGraphRequest -Uri $next foreach ($signIn in @(Get-Value $page 'value')) { if ($null -eq $signIn) { continue } if ($result.SignInsRead -ge $MaxSignIns) { $result.Truncated = $true; break } $result.SignInsRead++ $userId = Get-Value $signIn 'userId' foreach ($applied in @(Get-Value $signIn 'appliedConditionalAccessPolicies')) { $outcome = [string](Get-Value $applied 'result') if ($outcome -notlike 'reportOnly*') { continue } $policyId = Get-Value $applied 'id' if (-not $stats.ContainsKey($policyId)) { $stats[$policyId] = @{ DisplayName = Get-Value $applied 'displayName'; WouldBlock = 0; WouldInterrupt = 0; Satisfied = 0; NotApplied = 0; Users = @{} } } $entry = $stats[$policyId] switch ($outcome) { 'reportOnlyFailure' { $entry.WouldBlock++; if ($userId) { $entry.Users[$userId] = $true } } 'reportOnlyInterrupted' { $entry.WouldInterrupt++; if ($userId) { $entry.Users[$userId] = $true } } 'reportOnlySuccess' { $entry.Satisfied++ } default { $entry.NotApplied++ } } } } $next = Get-Value $page '@odata.nextLink' } if ($next -and $result.SignInsRead -ge $MaxSignIns) { $result.Truncated = $true } } catch { $message = "$_" $result.Available = $false $result.Reason = if ($message -match 'RequestFromNonPremiumTenant|Premium') { 'Sign-in logs need Entra ID P1 in this tenant.' } elseif ($message -match 'Forbidden|403|Authorization_RequestDenied') { 'The session cannot read sign-in logs (AuditLog.Read.All, plus a Reports Reader, Security Reader, or Global Reader role).' } else { "Sign-in logs could not be read: $message" } } Write-NSPConditionalAccessProgress -Activity Impact -Completed $result.Policies = @($stats.Keys | ForEach-Object { $entry = $stats[$_] [pscustomobject][ordered]@{ PolicyId = $_ DisplayName = $entry.DisplayName WouldBlock = $entry.WouldBlock WouldInterrupt = $entry.WouldInterrupt Satisfied = $entry.Satisfied NotApplied = $entry.NotApplied UsersAffected = $entry.Users.Count } } | Sort-Object DisplayName) if ($Save -and $PSCmdlet.ShouldProcess($result.TenantId, 'Save report-only impact')) { $folder = Join-Path (Get-NSPConditionalAccessLocalPath -Kind Impact -ConfigRoot $ConfigRoot) $result.TenantId $result.Path = Join-Path $folder ((Get-Date).ToUniversalTime().ToString('yyyyMMdd-HHmmss') + '.json') Save-NSPConditionalAccessJson -InputObject $result -Path $result.Path } $result } |