Public/NC.Security.ps1
|
#Requires -Version 5.0 using namespace System.Management.Automation # Nebula.Core: Security helpers ===================================================================================================================== function Disable-UserDevices { <# .SYNOPSIS Disables all registered devices for specified users. .DESCRIPTION Ensures Microsoft Graph connectivity, resolves the target users, and sets AccountEnabled = $false on each registered device. Skips missing users and reports progress. .PARAMETER UserPrincipalName One or more user principal names. Accepts pipeline input. .PARAMETER PassThru Emit the impacted devices as objects. .EXAMPLE Disable-UserDevices -UserPrincipalName user1@contoso.com,user2@contoso.com -WhatIf #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('User', 'UPN', 'Identity')] [string[]]$UserPrincipalName, [switch]$PassThru ) begin { Set-ProgressAndInfoPreferences $targets = [System.Collections.Generic.List[string]]::new() } process { foreach ($upn in $UserPrincipalName) { if (-not [string]::IsNullOrWhiteSpace($upn)) { $targets.Add($upn.Trim()) | Out-Null } } } end { try { if ($targets.Count -eq 0) { Write-NCMessage "No user principal names provided." -Level WARNING return } $scopes = @('Directory.ReadWrite.All', 'Device.ReadWrite.All') if (-not (Test-MgGraphConnection -Scopes $scopes -EnsureExchangeOnline:$false)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Graph modules. Please check logs." -Level ERROR return } $results = [System.Collections.Generic.List[object]]::new() $dedup = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $queue = foreach ($entry in $targets) { if ($dedup.Add($entry)) { $entry } } $counter = 0 foreach ($upn in $queue) { $counter++ $Percentage = Get-NCProgressPercent -Current $counter -Total $queue.Count Write-Progress -Activity "Resolving user $upn" -Status "$counter of $($queue.Count) - $Percentage%" -PercentComplete $Percentage try { $resolvedUpn = Find-UserRecipient -UserPrincipalName $upn if (-not $resolvedUpn) { continue } $user = Get-MgUser -UserId $resolvedUpn -ErrorAction Stop } catch { Write-NCMessage "Can't find Azure AD account for user $upn. $($_.Exception.Message)" -Level ERROR continue } if (-not $user) { Write-NCMessage "Can't find Azure AD account for user $upn." -Level ERROR continue } try { $devices = Get-MgUserRegisteredDevice -UserId $user.Id -All } catch { Write-NCMessage "Unable to retrieve registered devices for $($user.UserPrincipalName). $($_.Exception.Message)" -Level ERROR continue } if (-not $devices -or $devices.Count -eq 0) { Write-NCMessage ("No registered devices found for {0}." -f $user.UserPrincipalName) -Level WARNING continue } foreach ($device in $devices) { $deviceLabel = if ($device.DisplayName) { $device.DisplayName } else { $device.Id } if (-not $PSCmdlet.ShouldProcess($deviceLabel, "Disable device for user $($user.UserPrincipalName)")) { continue } try { Update-MgDevice -DeviceId $device.Id -AccountEnabled:$false -ErrorAction Stop | Out-Null $results.Add([pscustomobject]@{ UserPrincipalName = $user.UserPrincipalName UserDisplayName = $user.DisplayName DeviceId = $device.Id DeviceDisplayName = $device.DisplayName Action = 'Disabled' }) | Out-Null } catch { Write-NCMessage "Failed to disable device $deviceLabel for $($user.UserPrincipalName). $($_.Exception.Message)" -Level ERROR } } } if ($PassThru.IsPresent) { $results } elseif ($results.Count -gt 0) { $deviceLabel = if ($results.Count -eq 1) { 'device' } else { 'devices' } Write-NCMessage ("Disabled {0} {1}." -f $results.Count, $deviceLabel) -Level SUCCESS } } finally { Write-Progress -Activity "Resolving user" -Completed Restore-ProgressAndInfoPreferences } } } function Disable-UserSignIn { <# .SYNOPSIS Blocks sign-in for specified users. .DESCRIPTION Ensures Microsoft Graph connectivity, resolves the target users, and sets AccountEnabled = $false. Skips missing users and supports WhatIf/Confirm. .PARAMETER UserPrincipalName One or more user principal names. Accepts pipeline input. .PARAMETER PassThru Emit the impacted users as objects. .EXAMPLE Disable-UserSignIn -UserPrincipalName user1@contoso.com,user2@contoso.com -Confirm:$false #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('User', 'UPN', 'Identity')] [string[]]$UserPrincipalName, [switch]$PassThru ) begin { Set-ProgressAndInfoPreferences $targets = [System.Collections.Generic.List[string]]::new() } process { foreach ($upn in $UserPrincipalName) { if (-not [string]::IsNullOrWhiteSpace($upn)) { $targets.Add($upn.Trim()) | Out-Null } } } end { try { if ($targets.Count -eq 0) { Write-NCMessage "No user principal names provided." -Level WARNING return } $scopes = @('Directory.ReadWrite.All') if (-not (Test-MgGraphConnection -Scopes $scopes -EnsureExchangeOnline:$false)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Graph modules. Please check logs." -Level ERROR return } $results = [System.Collections.Generic.List[object]]::new() $dedup = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $queue = foreach ($entry in $targets) { if ($dedup.Add($entry)) { $entry } } $counter = 0 foreach ($upn in $queue) { $counter++ $Percentage = Get-NCProgressPercent -Current $counter -Total $queue.Count Write-Progress -Activity "Processing $upn" -Status "$counter of $($queue.Count) - $Percentage%" -PercentComplete $Percentage try { $resolvedUpn = Find-UserRecipient -UserPrincipalName $upn if (-not $resolvedUpn) { continue } $user = Get-MgUser -UserId $resolvedUpn -ErrorAction Stop } catch { Write-NCMessage "Can't find Azure AD account for user $upn. $($_.Exception.Message)" -Level ERROR continue } if (-not $user) { Write-NCMessage "Can't find Azure AD account for user $upn." -Level ERROR continue } if (-not $PSCmdlet.ShouldProcess($user.UserPrincipalName, "Disable sign-in")) { continue } try { Update-MgUser -UserId $user.Id -AccountEnabled:$false -ErrorAction Stop | Out-Null $results.Add([pscustomobject]@{ UserPrincipalName = $user.UserPrincipalName DisplayName = $user.DisplayName Action = 'SignInDisabled' }) | Out-Null } catch { Write-NCMessage "Failed to disable sign-in for $($user.UserPrincipalName). $($_.Exception.Message)" -Level ERROR } } if ($PassThru.IsPresent) { $results } elseif ($results.Count -gt 0) { $userLabel = if ($results.Count -eq 1) { 'user' } else { 'users' } Write-NCMessage ("Sign-in disabled for {0} {1}." -f $results.Count, $userLabel) -Level SUCCESS } } finally { Write-Progress -Activity "Processing users" -Completed Restore-ProgressAndInfoPreferences } } } function Get-ContentFilterPolicy { <# .SYNOPSIS Reads hosted content filter policy configuration. .DESCRIPTION Connects to Exchange Online and returns one or more hosted content filter policies. If no policy name is provided, the function lists all available policies. The output includes the current allow/block lists so you can inspect how each policy is configured before editing it. .PARAMETER Identity Hosted content filter policy name. If omitted, all policies are returned. .PARAMETER Detailed Include the resolved allow/block lists in the output. .EXAMPLE Get-ContentFilterPolicy .EXAMPLE Get-ContentFilterPolicy -Identity Contoso .EXAMPLE Get-ContentFilterPolicy -Detailed #> [CmdletBinding()] param( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('SpamFilter', 'PolicyName')] [string[]]$Identity, [switch]$Detailed ) begin { Set-ProgressAndInfoPreferences $targets = [System.Collections.Generic.List[string]]::new() } process { foreach ($entry in $Identity) { if (-not [string]::IsNullOrWhiteSpace($entry)) { $targets.Add($entry.Trim()) | Out-Null } } } end { try { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } $policyObjects = [System.Collections.Generic.List[object]]::new() if ($targets.Count -eq 0) { try { $policyObjects.AddRange(@(Get-HostedContentFilterPolicy -ErrorAction Stop)) } catch { Write-NCMessage "Unable to retrieve hosted content filter policies. $($_.Exception.Message)" -Level ERROR return } if ($policyObjects.Count -eq 0) { Write-NCMessage "No hosted content filter policies were found." -Level WARNING return } } else { $dedup = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($policyName in $targets) { if (-not $dedup.Add($policyName)) { continue } try { $policyObjects.Add((Get-HostedContentFilterPolicy -Identity $policyName -ErrorAction Stop)) | Out-Null } catch { Write-NCMessage "Unable to read hosted content filter policy '$policyName'. $($_.Exception.Message)" -Level ERROR } } if ($policyObjects.Count -eq 0) { Write-NCMessage "No hosted content filter policies matched the requested identity or identities." -Level WARNING return } } $policyObjects | Sort-Object Name | ForEach-Object { $blockedSenders = @(Get-NCContentFilterPolicyValues -PolicyObject $_ -PropertyName 'BlockedSenders' -PreferredValueProperty 'Sender') $blockedSenderDomains = @(Get-NCContentFilterPolicyValues -PolicyObject $_ -PropertyName 'BlockedSenderDomains' -PreferredValueProperty 'Domain') $allowedSenders = @(Get-NCContentFilterPolicyValues -PolicyObject $_ -PropertyName 'AllowedSenders' -PreferredValueProperty 'Sender') $allowedSenderDomains = @(Get-NCContentFilterPolicyValues -PolicyObject $_ -PropertyName 'AllowedSenderDomains' -PreferredValueProperty 'Domain') $summary = [ordered]@{ Identity = $_.Identity Name = $_.Name Enabled = $_.Enabled Priority = $_.Priority BlockedSenderCount = $blockedSenders.Count BlockedDomainCount = $blockedSenderDomains.Count AllowedSenderCount = $allowedSenders.Count AllowedDomainCount = $allowedSenderDomains.Count } if ($Detailed.IsPresent) { $summary.BlockedSenders = $blockedSenders $summary.BlockedSenderDomains = $blockedSenderDomains $summary.AllowedSenders = $allowedSenders $summary.AllowedSenderDomains = $allowedSenderDomains } [pscustomobject]$summary } } finally { Restore-ProgressAndInfoPreferences } } } function Edit-ContentFilterPolicy { <# .SYNOPSIS Updates a hosted content filter policy allow/block list. .DESCRIPTION Connects to Exchange Online, loads a hosted content filter policy, and adds or removes blocked senders, blocked domains, allowed senders, or allowed domains. When allowed senders are updated, the helper also keeps the configured allowed-senders group in sync and creates missing mail contacts. When allowed domains are updated, the helper also synchronizes the sender-domain exceptions on the configured transport rules. .PARAMETER Identity Hosted content filter policy name. Accepts the legacy SpamFilter alias. .PARAMETER BlockedSender Sender address to add or remove from the blocked senders list. .PARAMETER BlockedDomain Domain to add or remove from the blocked sender domains list. .PARAMETER AllowedSender Sender address to add or remove from the allowed senders list. .PARAMETER AllowedDomain Domain to add or remove from the allowed sender domains list. .PARAMETER AllowedSendersGroup Distribution group used to keep the allowed senders group in sync. .PARAMETER TransportRuleNames Transport rules that should mirror allowed-domain exceptions. .PARAMETER Remove Remove the provided values instead of adding them. .EXAMPLE Edit-ContentFilterPolicy -Identity Contoso -BlockedSender user@contoso.com .EXAMPLE Edit-ContentFilterPolicy -Identity Contoso -AllowedDomain contoso.com -Remove #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('SpamFilter', 'PolicyName')] [string]$Identity, [string[]]$BlockedSender, [string[]]$BlockedDomain, [string[]]$AllowedSender, [string[]]$AllowedDomain, [string]$AllowedSendersGroup, [string[]]$TransportRuleNames, [switch]$Remove ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } $blockedSendersQueue = @( foreach ($entry in @($BlockedSender)) { if (-not [string]::IsNullOrWhiteSpace($entry)) { $entry.Trim() } } ) | Sort-Object -Unique $blockedDomainsQueue = @( foreach ($entry in @($BlockedDomain)) { if (-not [string]::IsNullOrWhiteSpace($entry)) { $entry.Trim() } } ) | Sort-Object -Unique $allowedSendersQueue = @( foreach ($entry in @($AllowedSender)) { if (-not [string]::IsNullOrWhiteSpace($entry)) { $entry.Trim() } } ) | Sort-Object -Unique $allowedDomainsQueue = @( foreach ($entry in @($AllowedDomain)) { if (-not [string]::IsNullOrWhiteSpace($entry)) { $entry.Trim() } } ) | Sort-Object -Unique $modeLabel = if ($Remove.IsPresent) { 'Remove' } else { 'Add' } if ($blockedSendersQueue.Count -eq 0 -and $blockedDomainsQueue.Count -eq 0 -and $allowedSendersQueue.Count -eq 0 -and $allowedDomainsQueue.Count -eq 0) { Write-NCMessage "No changes requested. Returning the current policy state." -Level INFO } try { $policy = Get-HostedContentFilterPolicy -Identity $Identity -ErrorAction Stop } catch { Write-NCMessage "Unable to read hosted content filter policy '$Identity'. $($_.Exception.Message)" -Level ERROR return } $transportRules = [System.Collections.Generic.List[object]]::new() foreach ($ruleName in @($TransportRuleNames)) { if ([string]::IsNullOrWhiteSpace($ruleName)) { continue } try { $rule = Get-TransportRule -Identity $ruleName -ErrorAction Stop $transportRules.Add($rule) | Out-Null } catch { Write-NCMessage "Transport rule '$ruleName' was not found or could not be read. $($_.Exception.Message)" -Level WARNING } } $contactGroup = $null if (-not [string]::IsNullOrWhiteSpace($AllowedSendersGroup)) { try { $contactGroup = Get-DistributionGroup -Identity $AllowedSendersGroup -ErrorAction Stop } catch { Write-NCMessage "Allowed senders group '$AllowedSendersGroup' was not found or could not be read. $($_.Exception.Message)" -Level WARNING } } if ($allowedSendersQueue.Count -gt 0 -and -not $contactGroup) { Write-NCMessage "Allowed sender updates will skip group synchronization because -AllowedSendersGroup was not provided or could not be resolved." -Level INFO } if ($allowedDomainsQueue.Count -gt 0 -and $transportRules.Count -eq 0) { Write-NCMessage "Allowed domain updates will skip transport-rule synchronization because -TransportRuleNames was not provided or no rules could be resolved." -Level INFO } $changes = [System.Collections.Generic.List[object]]::new() $updateDomainList = { param( [object]$CurrentValues, [string]$Entry, [switch]$Remove ) $set = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($value in @($CurrentValues)) { if (-not [string]::IsNullOrWhiteSpace([string]$value)) { [void]$set.Add(([string]$value).Trim()) } } if ($Remove) { [void]$set.Remove($Entry) } else { [void]$set.Add($Entry) } return @($set) } Write-NCMessage "Policy: $Identity" -Level INFO Write-NCMessage ("Mode: {0}" -f $modeLabel) -Level INFO foreach ($entry in $blockedSendersQueue) { $actionText = if ($Remove.IsPresent) { "remove blocked sender '$entry'" } else { "add blocked sender '$entry'" } if (-not $PSCmdlet.ShouldProcess($Identity, $actionText)) { continue } try { if ($Remove.IsPresent) { Set-HostedContentFilterPolicy -Identity $Identity -BlockedSenders @{ remove = $entry } -ErrorAction Stop | Out-Null } else { Set-HostedContentFilterPolicy -Identity $Identity -BlockedSenders @{ add = $entry } -ErrorAction Stop | Out-Null } $changes.Add([pscustomobject]@{ Scope = 'BlockedSenders' Value = $entry Action = $(if ($Remove.IsPresent) { 'Removed' } else { 'Added' }) }) | Out-Null } catch { Write-NCMessage "Unable to update blocked sender '$entry' on '$Identity'. $($_.Exception.Message)" -Level ERROR } } foreach ($entry in $blockedDomainsQueue) { $actionText = if ($Remove.IsPresent) { "remove blocked domain '$entry'" } else { "add blocked domain '$entry'" } if (-not $PSCmdlet.ShouldProcess($Identity, $actionText)) { continue } try { if ($Remove.IsPresent) { Set-HostedContentFilterPolicy -Identity $Identity -BlockedSenderDomains @{ remove = $entry } -ErrorAction Stop | Out-Null } else { Set-HostedContentFilterPolicy -Identity $Identity -BlockedSenderDomains @{ add = $entry } -ErrorAction Stop | Out-Null } $changes.Add([pscustomobject]@{ Scope = 'BlockedSenderDomains' Value = $entry Action = $(if ($Remove.IsPresent) { 'Removed' } else { 'Added' }) }) | Out-Null } catch { Write-NCMessage "Unable to update blocked sender domain '$entry' on '$Identity'. $($_.Exception.Message)" -Level ERROR } } foreach ($entry in $allowedSendersQueue) { $actionText = if ($Remove.IsPresent) { "remove allowed sender '$entry'" } else { "add allowed sender '$entry'" } if (-not $PSCmdlet.ShouldProcess($Identity, $actionText)) { continue } try { if (-not $Remove.IsPresent) { $contact = Get-MailContact -Identity $entry -ErrorAction SilentlyContinue if (-not $contact) { New-MailContact -DisplayName $entry -Name $entry -ExternalEmailAddress $entry -ErrorAction Stop | Out-Null Write-NCMessage "Created mail contact for '$entry'." -Level SUCCESS } Set-MailContact -Identity $entry -HiddenFromAddressListsEnabled $true -ErrorAction Stop | Out-Null if ($contactGroup) { try { Add-DistributionGroupMember -Identity $contactGroup.Identity -Member $entry -ErrorAction Stop | Out-Null } catch { Write-NCMessage "Unable to add '$entry' to allowed senders group '$AllowedSendersGroup'. $($_.Exception.Message)" -Level WARNING } } } else { if ($contactGroup) { try { Remove-DistributionGroupMember -Identity $contactGroup.Identity -Member $entry -Confirm:$false -ErrorAction Stop | Out-Null } catch { Write-NCMessage "Unable to remove '$entry' from allowed senders group '$AllowedSendersGroup'. $($_.Exception.Message)" -Level WARNING } } } if ($Remove.IsPresent) { Set-HostedContentFilterPolicy -Identity $Identity -AllowedSenders @{ remove = $entry } -ErrorAction Stop | Out-Null } else { Set-HostedContentFilterPolicy -Identity $Identity -AllowedSenders @{ add = $entry } -ErrorAction Stop | Out-Null } $changes.Add([pscustomobject]@{ Scope = 'AllowedSenders' Value = $entry Action = $(if ($Remove.IsPresent) { 'Removed' } else { 'Added' }) }) | Out-Null } catch { Write-NCMessage "Unable to update allowed sender '$entry' on '$Identity'. $($_.Exception.Message)" -Level ERROR } } foreach ($entry in $allowedDomainsQueue) { $actionText = if ($Remove.IsPresent) { "remove allowed domain '$entry'" } else { "add allowed domain '$entry'" } if (-not $PSCmdlet.ShouldProcess($Identity, $actionText)) { continue } try { if ($Remove.IsPresent) { Set-HostedContentFilterPolicy -Identity $Identity -AllowedSenderDomains @{ remove = $entry } -ErrorAction Stop | Out-Null } else { Set-HostedContentFilterPolicy -Identity $Identity -AllowedSenderDomains @{ add = $entry } -ErrorAction Stop | Out-Null } foreach ($rule in $transportRules) { $currentDomains = @($rule.ExceptIfSenderDomainIs) $updatedDomains = & $updateDomainList $currentDomains $entry -Remove:$Remove.IsPresent $ruleActionText = if ($Remove.IsPresent) { "remove sender-domain exception '$entry' from transport rule '$($rule.Name)'" } else { "add sender-domain exception '$entry' to transport rule '$($rule.Name)'" } if (-not $PSCmdlet.ShouldProcess($rule.Name, $ruleActionText)) { continue } try { if ($updatedDomains.Count -gt 0) { Set-TransportRule -Identity $rule.Identity -ExceptIfSenderDomainIs $updatedDomains -ErrorAction Stop | Out-Null } else { Set-TransportRule -Identity $rule.Identity -ExceptIfSenderDomainIs $null -ErrorAction Stop | Out-Null } } catch { Write-NCMessage "Unable to update transport rule '$($rule.Name)' for domain '$entry'. $($_.Exception.Message)" -Level WARNING } } $changes.Add([pscustomobject]@{ Scope = 'AllowedSenderDomains' Value = $entry Action = $(if ($Remove.IsPresent) { 'Removed' } else { 'Added' }) }) | Out-Null } catch { Write-NCMessage "Unable to update allowed sender domain '$entry' on '$Identity'. $($_.Exception.Message)" -Level ERROR } } try { $updatedPolicy = Get-HostedContentFilterPolicy -Identity $Identity -ErrorAction Stop } catch { Write-NCMessage "Policy '$Identity' was updated, but the refreshed policy could not be read back. $($_.Exception.Message)" -Level WARNING $updatedPolicy = $policy } $summary = [pscustomobject]@{ Identity = $updatedPolicy.Identity DisplayName = $updatedPolicy.Name AllowedSenders = @(Get-NCContentFilterPolicyValues -PolicyObject $updatedPolicy -PropertyName 'AllowedSenders' -PreferredValueProperty 'Sender') AllowedSenderDomains = @(Get-NCContentFilterPolicyValues -PolicyObject $updatedPolicy -PropertyName 'AllowedSenderDomains' -PreferredValueProperty 'Domain') BlockedSenders = @(Get-NCContentFilterPolicyValues -PolicyObject $updatedPolicy -PropertyName 'BlockedSenders' -PreferredValueProperty 'Sender') BlockedSenderDomains = @(Get-NCContentFilterPolicyValues -PolicyObject $updatedPolicy -PropertyName 'BlockedSenderDomains' -PreferredValueProperty 'Domain') AllowedSendersGroup = $AllowedSendersGroup TransportRuleNames = @($transportRules | Select-Object -ExpandProperty Name) Changes = @($changes) } if ($changes.Count -gt 0) { Write-NCMessage ("Updated content filter policy '{0}' with {1} change(s)." -f $Identity, $changes.Count) -Level SUCCESS } else { Write-NCMessage "No policy changes were applied." -Level INFO } return $summary } end { Restore-ProgressAndInfoPreferences } } function Revoke-UserSessions { <# .SYNOPSIS Forces sign-out by revoking refresh tokens for users. .DESCRIPTION Ensures Microsoft Graph connectivity, targets all users or a selection (with optional exclusions), and calls Revoke-MgUserSignInSession. Supports WhatIf/Confirm. .PARAMETER All Target every user in the tenant. .PARAMETER UserPrincipalName Specific users to target. Accepts pipeline input. .PARAMETER Exclude Users to skip when using -All or a list. .PARAMETER PassThru Emit the impacted users as objects. .EXAMPLE Revoke-UserSessions -UserPrincipalName user1@contoso.com,user2@contoso.com .EXAMPLE Revoke-UserSessions -All -Exclude user@contoso.com -Confirm:$false #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [switch]$All, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('User', 'UPN', 'Identity')] [string[]]$UserPrincipalName, [string[]]$Exclude, [switch]$PassThru ) begin { Set-ProgressAndInfoPreferences $targets = [System.Collections.Generic.List[string]]::new() $exclusions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) } process { foreach ($upn in $UserPrincipalName) { if (-not [string]::IsNullOrWhiteSpace($upn)) { $targets.Add($upn.Trim()) | Out-Null } } } end { try { if (-not $All.IsPresent -and $targets.Count -eq 0) { Write-NCMessage "No target specified. Use -All or provide user principal names." -Level WARNING return } foreach ($ex in $Exclude) { if (-not [string]::IsNullOrWhiteSpace($ex)) { $exclusions.Add($ex.Trim()) | Out-Null } } $scopes = @('Directory.ReadWrite.All') if (-not (Test-MgGraphConnection -Scopes $scopes -EnsureExchangeOnline:$false)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Graph modules. Please check logs." -Level ERROR return } $queue = [System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.IMicrosoftGraphUser]]::new() if ($All.IsPresent) { try { $allUsers = Get-MgUser -All -ConsistencyLevel eventual -ErrorAction Stop foreach ($u in $allUsers) { $queue.Add($u) | Out-Null } } catch { Write-NCMessage "Unable to retrieve all users. $($_.Exception.Message)" -Level ERROR return } } else { $dedup = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $uniqueTargets = foreach ($entry in $targets) { if ($dedup.Add($entry)) { $entry } } foreach ($upn in $uniqueTargets) { try { $resolvedUpn = Find-UserRecipient -UserPrincipalName $upn if (-not $resolvedUpn) { continue } $user = Get-MgUser -UserId $resolvedUpn -ErrorAction Stop if ($user) { $queue.Add($user) | Out-Null } else { Write-NCMessage "Can't find Azure AD account for user $upn." -Level ERROR } } catch { Write-NCMessage "Can't find Azure AD account for user $upn. $($_.Exception.Message)" -Level ERROR } } } if ($queue.Count -eq 0) { Write-NCMessage "No users to process." -Level WARNING return } $results = [System.Collections.Generic.List[object]]::new() $counter = 0 foreach ($user in $queue) { $counter++ $Percentage = Get-NCProgressPercent -Current $counter -Total $queue.Count Write-Progress -Activity "Revoking sessions" -Status "$counter of $($queue.Count) - $Percentage%" -PercentComplete $Percentage if ($exclusions.Contains($user.UserPrincipalName)) { Write-NCMessage ("Skipping user {0}" -f $user.UserPrincipalName) -Level INFO continue } if (-not $PSCmdlet.ShouldProcess($user.UserPrincipalName, "Revoke sign-in sessions")) { continue } try { Revoke-MgUserSignInSession -UserId $user.Id -ErrorAction Stop | Out-Null $results.Add([pscustomobject]@{ UserPrincipalName = $user.UserPrincipalName DisplayName = $user.DisplayName Action = 'SessionsRevoked' }) | Out-Null } catch { Write-NCMessage "Failed to revoke sessions for $($user.UserPrincipalName). $($_.Exception.Message)" -Level ERROR } } if ($PassThru.IsPresent) { $results } elseif ($results.Count -gt 0) { $userLabel = if ($results.Count -eq 1) { 'user' } else { 'users' } Write-NCMessage ("Revoked sessions for {0} {1}." -f $results.Count, $userLabel) -Level SUCCESS } } finally { Write-Progress -Activity "Revoking sessions" -Completed Restore-ProgressAndInfoPreferences } } } |