Skip to content

Instantly share code, notes, and snippets.

@exactmike
Last active July 13, 2022 15:07
Show Gist options
  • Select an option

  • Save exactmike/7b214fd86c252ba4f7f74d447a2e9493 to your computer and use it in GitHub Desktop.

Select an option

Save exactmike/7b214fd86c252ba4f7f74d447a2e9493 to your computer and use it in GitHub Desktop.
StaleUserManagement

Initial Tenant Setup

Register an Enterprise App

Attribute Value
Name Stale Account Management
Supported Account Types Accounts in this organizational directory only

Note the application (client) ID and the Directory (tenant) ID

Add a client credential to the Enterprise App

See Client Certificate Management or Client Secret Management

Add required permissions to the Enterprise App

The Stale User Accounts application requires the following permissions:

  • Sign in and read user profile (allows app sign in)
  • Read and write all users' full profiles (allows app to disable a user account)
  • Read all groups (allows app to view groups)
  • Read and write all group memberships (allows app to add and remove members to the application groups)
  • Read all audit log data (allows app to view last logon audit information)

Add required security groups to Azure AD

  • A group to hold objects that have been disabled and are pending deletion for the DeleteAfterDays period
  • A group to hold objects granted an exception to automated deletion due to stale login activity
  • The objectID of each of these groups will be required for running to application function

Client Certificate Management

Requirements

  • Windows PowerShell 5.1
  • Microsoft's built-in PKI PowerShell Module
  • Elevated PowerShell 5.1 instance on the automation server
  • Access to Azure AD and permissions to manage the StaleCloudAccountsProcessing application Azure AD

Steps

Create certificate

Suggested Values

$ValidYears = 1
$ThroughDate = $(Get-Date).AddYears($ValidYears)
$DNSName = '#####.onmicrosoft.com' # replace #### with your tenant's subdomain
$Certificate = New-SelfSignedCertificate -DnsName $DNSName -CertStoreLocation "cert:\CurrentUser\My" -NotAfter $ThroughDate -KeySpec KeyExchange
#OR
"cert:\LocalMachine\My" -NotAfter $ThroughDate -KeySpec KeyExchange

Export certificate for upload to Azure AD

$Certificate | Export-Certificate -FilePath PublicCert.cer

Upload certificate to the StaleCloudAccountsProcessing application in Azure AD

  • Navigate to the application in Azure AD Portal and access Certificates & Secrets.
  • Click, 'Upload Certificate' and select and add the exported certificate file.
  • Verify the certificate thumbprint, start date, and expiration date

Update the run script with the new certificate thumbprint

  • Update the value provided in the script for the CertificateThumbprint parameter.

Backup the Certificate (if required)

Export certificate to .pfx file with private key

$CertificateFullFilePath = 'c:\MyCert.cer'
$Password = $(Get-Credential).Password
$Certificate | Export-PfxCertificate -FilePath $CertificateFullFilePath -Password $Password

Client Secret Management

Create Client Secret

  • Navigate to the application in Azure AD Portal and access Certificates & Secrets.
  • Click, 'Client secrets', then 'New client secret'.
  • Provide a description and select an expiration period.
  • Copy the value from the new client secret (the full value won't be available later).
  • Securely Record the value for later use in the schedule task script

Usage Of StaleUserManagemetnScriptViaMSGraph.ps1

The script file contains the definitions of multiple support functions as well as the main function Invoke-StaleUserAccountProcessing. A call to Invoke-StaleUserAccountProcessing can be added, with appropriate parameters, to the end of the script file. Alternatively, the script file can be "dot-sourced" into memory and then Invoke-StaleUserAccountProcessing can be called separately from the script file.

Required Parameters

Invoke-StaleUserAccountProcessing has the following required parameters. [guid] type parameters will accept a string value which is convertible to [guid]

[string]$CertificateThumbprint OR [securestring]$ClientSecret
[guid]$TenantID
[guid]$ClientID
[guid]$StaleGroupID
[guid]$ExceptionGroupID

Default Value Parameters

If not specified the following parameters have a default value as follows:

[int]$StaleDisablePolicyDays = 30
[int]$StaleDeletePolicyDays = 60
[string]$ReportFolderPath = $env:TEMP

Optional Parameters

The following parameters are optional. If not specified the function will not perform the disable and delete actions but will update the membershhip of the StaleGroup and the ExceptionGroup specified by their respective parameters.

[switch]$DisableStaleAccounts
[switch]$DeleteAgedStaleAccounts

Usage examples:

$myCertparams = @{
    CertificateThumbprint  = 'D05F9E3850A3A085B10407AA848864A90A1E4DCC'
    TenantID               = 'redacted'
    ClientID               = '3a3be3ed-db98-42dd-ad1b-0e606db8572c'
    StaleGroupID           = '9583d831-3267-4b87-a1d8-336793672a76'
    ExceptionGroupID       = 'fe7e80da-45ae-4642-87ce-87a5b930c536'
    StaleDisablePolicyDays = 15
    StaleDeletePolicyDays  = 30
    ReportFolderPath       = 'C:\StaleUserProcessingReports'
}

Invoke-StaleUserAccountProcessing @myCertparams

$myCSparams = @{
    ClientSecret           = 'redacted' | ConvertTo-SecureString -AsPlainText -Force
    TenantID               = 'redacted'
    ClientID               = '3a3be3ed-db98-42dd-ad1b-0e606db8572c'
    StaleGroupID           = '9583d831-3267-4b87-a1d8-336793672a76'
    ExceptionGroupID       = 'fe7e80da-45ae-4642-87ce-87a5b930c536'
    StaleDisablePolicyDays = 15
    StaleDeletePolicyDays  = 30
    ReportFolderPath       = 'C:\StaleUserProcessingReports'
}

Invoke-StaleUserAccountProcessing @myCSparams
Function New-SplitArrayRange
{
<#
MIT License
Copyright (c) 2021 Mike Campbell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#>
<#
.SYNOPSIS
Provides Start and End Ranges to Split an array into a specified number of parts (new arrays) or parts (new arrays) with a specified number (size) of elements
.PARAMETER inArray
A one dimensional array you want to split
.EXAMPLE
Split-array -inArray @(1,2,3,4,5,6,7,8,9,10) -parts 3
.EXAMPLE
Split-array -inArray @(1,2,3,4,5,6,7,8,9,10) -size 3
.NOTE
Derived from https://gallery.technet.microsoft.com/scriptcenter/Split-an-array-into-parts-4357dcc1#content
#>
[cmdletbinding()]
param(
[parameter(Mandatory)]
[array]$inputArray
,
[parameter(Mandatory, ParameterSetName = 'Parts')]
[int]$parts
,
[parameter(Mandatory, ParameterSetName = 'Size')]
[int]$size
)
switch ($PSCmdlet.ParameterSetName)
{
'Parts'
{
$PartSize = [Math]::Ceiling($inputArray.count / $parts)
}#Parts
'Size'
{
$PartSize = $size
$parts = [Math]::Ceiling($inputArray.count / $size)
}#Size
}#switch
for ($i = 1; $i -le $parts; $i++)
{
$start = (($i - 1) * $PartSize)
$end = (($i) * $PartSize) - 1
if ($end -ge $inputArray.count) { $end = $inputArray.count }
$SplitArrayRange = [pscustomobject]@{
Part = $i
Start = $start
End = $end
}
$SplitArrayRange
}#for
}
Function Get-RandomFileName
{
<#
MIT License
Copyright (c) 2021 Mike Campbell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#>
[cmdletbinding(DefaultParameterSetName = 'WithExtension')]
param (
[parameter(Mandatory, ParameterSetName = 'SpecifiedExtension')]
[string]$extension
)
switch ($PSCmdlet.ParameterSetName)
{
'WithExtension'
{
([IO.Path]::GetRandomFileName())
}
'SpecifiedExtension'
{
"$([io.path]::GetFileNameWithoutExtension([IO.Path]::GetRandomFileName())).$extension"
}
}
}
Function Connect-SUMGraph
{
[CmdletBinding(DefaultParameterSetName = 'Certificate')]
param(
[parameter(ParameterSetName = 'Certificate')]
[string]$CertificateThumbprint
,
[parameter(ParameterSetName = 'ClientSecret')]
[securestring]$ClientSecret
,
[parameter()]
[guid]$TenantID
,
[parameter()]
[guid]$ClientID
)
$Token = $(
switch ($PSCmdlet.ParameterSetName)
{
'Certificate'
{
$cert = Get-ChildItem -Path cert:\ -Recurse -Include $CertificateThumbprint -ErrorAction Stop | Select-Object -First 1 -ErrorAction Stop
Write-Information -MessageData "Certificate found: $($cert.Subject; $cert.thumbprint)"
Get-MsalToken -TenantId $TenantID.guid -ClientCertificate $cert -ClientId $ClientID.guid -ErrorAction Stop
}
'ClientSecret'
{
Get-MsalToken -TenantId $TenantID.guid -ClientSecret $ClientSecret -ClientId $ClientID.guid -ErrorAction Stop
}
}
)
Write-Information -MessageData "Token Received for Tenant: $($Token.TenantId)"
$Global:ssToken = $Token.AccessToken | ConvertTo-SecureString -AsPlainText -Force -ErrorAction Stop
Write-Information -MessageData 'Token converted for use with MS Graph'
$Global:IRMParams = @{
Authentication = 'OAuth'
Token = $ssToken
ErrorAction = 'stop'
}
}
Function Get-GraphResultAll
{
param(
[hashtable]$OperationParams
)
do
{
$Result = Invoke-RestMethod @IRMParams @OperationParams
$Result.value #output current results
switch ($null -ne $Result.'@odata.nextLink')
{
$true
{
$OperationParams.URI = $Result.'@odata.nextLink'
}
$false
{
$OperationParams.URI = $null
}
}
}
until ($null -eq $OperationParams.URI)
}
Function Get-StaleUser
{
[CmdletBinding()]
param(
[psobject]$User
)
$GetStaleUsersParams = @{
Method = 'get'
Headers = @{'ConsistencyLevel' = 'eventual'}
URI = "https://graph.microsoft.com/beta/users?`$filter=signInActivity/lastSignInDateTime le $strStaleDateString&`$select=displayName,userPrincipalName,ID,signInActivity,onPremisesSyncEnabled&`$count=true"
}
$StaleUsers = @(Get-GraphResultAll -OperationParams $GetStaleUsersParams)
}
function Get-NonSyncedUser
{
[CmdletBinding()]
param(
[hashtable]
$IRMParams
,
[string[]]$IncludeAttribute = @('displayName', 'userPrincipalName', 'ID', 'signInActivity', 'userType', 'externalUserState', 'onPremisesSyncEnabled', 'employeeId', 'employeeType', 'createdDateTime', 'accountEnabled')
)
$IncludeAttributeString = $IncludeAttribute -join ','
$OperationParams = @{
Method = 'get'
Headers = @{'ConsistencyLevel' = 'eventual'}
URI = "https://graph.microsoft.com/beta/users?`$filter=onPremisesSyncEnabled ne true &`$select=$IncludeAttributeString&`$count=true"
}
Write-Information -MessageData $($OperationParams | ConvertTo-Json -Compress)
$Users = @(Get-GraphResultAll -OperationParams $OperationParams)
$Users | Select-Object -Property id, displayName, userPrincipalName, onPremisesSyncEnabled, userType, externalUserState, employeeId, employeeType, @{n='LastLoginDate'; e={$_.signInActivity.lastSignInDateTime}}, createdDateTime, accountEnabled
}
function Select-StaleNonSyncedUser
{
[CmdletBinding()]
param(
[psobject]
$NonSyncedUser
,
[int]$StaleDisablePolicyDays
,
[int]$StaleDeletePolicyDays
)
$today = Get-Date
$NonSyncedUser.where({
($null -eq $_.LastLoginDate -and $_.createdDateTime -le $today.AddDays(-$StaleDisablePolicyDays)) -or
($null -ne $_.LastLoginDate -and $_.LastLoginDate -le $today.AddDays(-$StaleDisablePolicyDays))
}) |
Select-Object -Property *,
@{n='StaleDisablePolicyDays'; e={$StaleDisablePolicyDays}},
@{n='StaleDeletePolicyDays'; e={$StaleDeletePolicyDays}},
@{n='StaleAgeDays'; e = {
if ($null -eq $_.LastLoginDate)
{
$($today - $($_.createdDateTime)).Days
}
else
{
$($today - $($_.LastLoginDate)).Days
}
}
}
}
function Resolve-StaleNonSyncedUserException
{
[CmdletBinding()]
param(
[parameter(Mandatory)]
[psobject[]]
$StaleNonSyncedUser,
[parameter(Mandatory)]
[psobject[]]
$Exception
)
if ($Exception[0].psobject.TypeNames -notcontains 'StaleUserException')
{
throw ('This function accepts exceptions only from New-StaleUserException')
}
if ($null -eq $StaleNonSyncedUser[0].StaleAgeDays)
{
throw ('This function accepts only StaleNonSyncedUsers from Select-StaleNonSyncedUser')
}
$expandedException = @(
switch ($Exception)
{
{$_.ExceptionType -eq 'Group'}
{
[pscustomobject]@{
ExceptionType = 'Group'
GroupID = $_.value
Members = @(Get-GroupMember -GroupID $_.value)
}
}
{$_.ExceptionType -eq 'AttributeComparison'}
{
[pscustomobject]@{
ExceptionType = 'AttributeComparison'
Attribute = $_.Attribute
ComparisonOperator = $_.ComparisonOperator
ComparisonValue = $_.ComparisonValue
}
#$StaleNonSyncedUser.where({$_.employeeID -like $($exception.employeeID) + '*'})
}
}
)
$StaleNonSyncedUser.foreach({
$user = $_
$userException = $false
switch ($expandedException)
{
{$_.ExceptionType -eq 'Group'}
{
if ($user.id -in $_.Members.id)
{
$exceptionDescription = "group $($_.GroupID)"
$user | Select-Object -Property *, @{n='ExceptionType'; e = {$exceptionDescription}}
$userException = $true
break
}
}
{$_.ExceptionType -eq 'AttributeComparison'}
{
if ($user.$($_.Attribute) -like $($_.ComparisonValue))
{
$exceptionDescription = "$($_.Attribute) -like $($_.ComparisonValue)"
$user | Select-Object -Property *, @{n='ExceptionType'; e = {$exceptionDescription}}
$userException = $true
break
}
}
}
if ($false -eq $userException)
{
$user | Select-Object -Property *, @{n='ExceptionType'; e = {$null}}
}
})
}
function New-StaleUserException
{
[CmdletBinding(DefaultParameterSetName = 'Group')]
param (
[parameter(Mandatory)]
[validateSet('Group', 'AttributeComparison')]
[string]$ExceptionType
,
[parameter(Mandatory, ParameterSetName = 'Group')]
[string]$GroupID
,
[parameter(Mandatory, ParameterSetName = 'AttributeComparison')]
[string]$Attribute
,
[parameter(Mandatory, ParameterSetName = 'AttributeComparison')]
[string]$ComparisonValue
)
begin
{
}
process
{
switch ($ExceptionType)
{
'Group'
{
[pscustomobject]@{
PSTypeName = 'StaleUserException'
ExceptionType = $ExceptionType
Value = $GroupID
}
}
'AttributeComparison'
{
[pscustomobject]@{
PSTypeName = 'StaleUserException'
ExceptionType = $ExceptionType
Attribute = $Attribute
ComparisonValue = $ComparisonValue
}
}
}
}
end
{
}
}
function Get-GroupMember
{
[CmdletBinding()]
param(
[guid]$GroupID
)
$GetGroupMembersParams = @{
Method = 'get'
URI = "https://graph.microsoft.com/v1.0/groups/{$($GroupID.guid)}/members"
}
Get-GraphResultAll -OperationParams $GetGroupMembersParams
}
function Remove-GroupMember
{
[cmdletbinding()]
param(
[parameter(Mandatory, ValueFromPipelineByPropertyName)]
[guid]$GroupID
,
[parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[guid[]]$MemberID
)
process
{
foreach ($m in $MemberID)
{
$RemoveMemberParams = @{
method = 'delete'
URI = "https://graph.microsoft.com/v1.0/groups/{$($GroupID.guid)}/members/{$($m.guid)}/`$ref"
}
Invoke-RestMethod @IRMParams @RemoveMemberParams
}
}
}
function Add-GroupMember
{
[CmdletBinding()]
param(
[parameter(Mandatory, ValueFromPipelineByPropertyName)]
[guid]$GroupID
,
[parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[guid[]]$MemberID
)
process
{
switch ($MemberID.count) #the limit for group member adds in Microsoft Graph is 20
{
#create an array of ranges in either case so we can use the same code below to process member adds either way
{ $_ -gt 20 }
{
#create a range for each 20 users to add
$ranges = @(New-SplitArrayRange -inputArray $MemberID -size 20 -ErrorAction Stop)
}
{ $_ -le 20 -and $_ -gt 0 } #count is greater than 0 and less than or equal to 20
{
#creat a range for all users to add
$ranges = @(New-SplitArrayRange -inputArray $MemberID -size $MemberID.count -ErrorAction Stop)
}
{ $_ -eq 0 }
{ } #do nothing if the count is zero
}
if ($null -ne $ranges)
{
foreach ($r in $ranges)
{
$AddMemberObject = [PSCustomObject]@{
'members@odata.bind' = @($MemberID[$r.Start..$r.end].foreach(
{
"https://graph.microsoft.com/v1.0/directoryObjects/{$($_.guid)}"
}
)
)
}
$AddMemberJsonContent = $AddMemberObject | ConvertTo-Json
$AddMembersParams = @{
method = 'Patch'
URI = "https://graph.microsoft.com/v1.0/groups/{$($GroupID.guid)}"
ContentType = 'application/json'
Body = $AddMemberJsonContent
}
try
{
Invoke-RestMethod @IRMParams @AddMembersParams
}
catch
{
throw ($_)
}
}
}
}
}
function Disable-StaleUser
{
[cmdletbinding()]
param(
[parameter(Mandatory)]
[psobject[]]$StaleUser
)
process
{
foreach ($su in $StaleUser)
{
switch ($su.accountEnabled)
{
$true
{
$DisableUserParams = @{
method = 'Patch'
URI = "https://graph.microsoft.com/v1.0/users/{$($su.id)}"
ContentType = 'application/json'
Body = '{"accountEnabled":false}'
}
try
{
Invoke-RestMethod @IRMParams @DisableUserParams
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
$ScriptErrorStrings.Add("Error disabling UserID $($su.id)")
}
}
$false
{
Write-Warning "User $($su.id) is already disabled."
}
}
}
}
}
function Remove-StaleUser
{
[cmdletbinding()]
param(
[parameter(Mandatory)]
[psobject[]]$StaleUser
)
process
{
foreach ($su in $StaleUser)
{
$DeleteUserParams = @{
method = 'Delete'
URI = "https://graph.microsoft.com/v1.0/users/{$($su.id)}"
}
try
{
Invoke-RestMethod @IRMParams @DeleteUserParams
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
$ScriptErrorStrings.Add("Error deleting UserID $($su.id)")
}
}
}
}
function Invoke-StaleUserAccountProcessing
{
#Requires -PSEdition Core -Modules MSAL.PS, ImportExcel
[cmdletbinding(DefaultParameterSetName = 'Certificate')]
param(
[parameter(ParameterSetName = 'Certificate', Mandatory)]
[string]$CertificateThumbprint
,
[parameter(ParameterSetName = 'ClientSecret', Mandatory)]
[securestring]$ClientSecret
,
[parameter(Mandatory)]
[guid]$TenantID
,
[parameter(Mandatory)]
[guid]$ClientID
,
[parameter(Mandatory)]
[guid]$StaleGroupID
,
[parameter(Mandatory)]
[guid]$ExceptionGroupID
,
[parameter()]
[int]$StaleDisablePolicyDays = 30
,
[parameter()]
[int]$StaleDeletePolicyDays = 60
,
[switch]$DisableStaleAccounts
,
[switch]$DeleteAgedStaleAccounts
,
[parameter()]
[string]$ReportFolderPath = $env:TEMP
)
# Assumptions for Process outside of this script:
# Permanent exception accounts should be placed in the exceptions group (service accounts that are used only periodically)
# Accounts that need to be re-enabled but not be a permanant exception should be re-enabled and removed from the Stale Users Group (guest accounts or admin accounts that should be regularly used or given up)
$exceptions = @(
New-StaleUserException -ExceptionType Group -GroupID $ExceptionGroupID
New-StaleUserException -ExceptionType AttributeComparison -Attribute employeeID -ComparisonValue '#*'
)
$ScriptErrorStrings = [System.Collections.Generic.List[string]]::new()
$CompletedOperations = [System.Collections.Generic.List[string]]::new()
[datetime]$today = (Get-Date)
$staleDisableDate = $today.AddDays( - $StaleDisablePolicyDays)
#$strStaleDateString = $staleDate.ToString('yyyy-MM-dd') + 'T00:00:00Z'
$staleDeleteDate = $today.AddDays( - $StaleDisablePolicyDays)
#$strDeleteDateString = $DeleteAfterDate.ToString( 'yyyy-M -dd') + 'T00:00:00Z'
#properties/attributes to include when getting group membership info
$groupMemberProperties = @('id', 'displayName', 'userPrincipalName')
$ReportObject = [PSCustomObject]@{
OperationDetails = [PSCustomObject]@{
AuthenticationMethod = $($PSCmdlet.ParameterSetName + ' ' + $CertificateThumbprint)
DisableStaleAccounts = $DisableStaleAccounts
DeleteAgedStaleAccounts = $DeleteAgedStaleAccounts
OperationDateUTC = $today.ToUniversalTime()
StaleDisablePolicyDays = $StaleDisablePolicyDays
StaleDeletePolicyDays = $StaleDeletePolicyDays
StaleDisableDate = $staleDisableDate.ToUniversalTime()
StaleDeleteDate = $staleDeleteDate.ToUniversalTime()
StaleUsersDetectedCount = $null
StartingStaleGroupMembersCount = $null
ExceptionGroupMembersCount = $null
RemovedFromStaleGroupCount = $null
AddedStaleGroupMembersCount = $null
DisabledStaleUsersCount = $null
RemovedStaleUsersCount = $null
CompletedOperations = $null
ErrorCount = $null
}
StaleUsersDetected = $null
StartingStaleGroupMembers = $null
ExceptionGroupMembers = $null
RemovedFromStaleGroup = $null
AddedToStaleGroup = $null
DisabledStaleUsers = $null
RemovedStaleUsers = $null
}
Try
{
try # Connect to Microsoft Graph
{
$Operation = 'Authentication'
Write-Information -MessageData "Operation: $Operation"
switch ($PSCmdlet.ParameterSetName)
{
'Certificate'
{
Connect-SUMGraph -CertificateThumbprint $CertificateThumbprint -TenantID $TenantID -ClientID $ClientID -ErrorAction stop
}
'ClientSecret'
{
Connect-SUMGraph -ClientSecret $ClientSecret -TenantID $TenantID -ClientID $ClientID -ErrorAction stop
}
}
$CompletedOperations.Add($Operation)
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
try # Get All Non AD Synced Users with necessary attributes for evaluating staleness
{
$Operation = 'GetNonSyncedUser'
Write-Information -MessageData "Operation: $Operation"
$NonSyncedUser = Get-NonSyncedUser -IRMParams $irmParams -ErrorAction Stop
$CompletedOperations.Add($Operation)
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
try # Select the stale users from the Non AD Synced Users
{
$Operation = 'SelectStaleNonSyncedUser'
Write-Information -MessageData "Operation: $Operation"
$StaleNonSyncedUser = @(Select-StaleNonSyncedUser -NonSyncedUser $NonSyncedUser -StaleDisablePolicyDays $StaleDisablePolicyDays -StaleDeletePolicyDays $StaleDeletePolicyDays -ErrorAction Stop)
$CompletedOperations.Add($Operation)
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
try # resolve any exceptions
{
$Operation = 'ResolveStaleUserException'
Write-Information -MessageData "Operation: $Operation"
$StaleNonSyncedUserER = @(Resolve-StaleNonSyncedUserException -StaleNonSyncedUser $StaleNonSyncedUser -Exception $exceptions -ErrorAction Stop)
$CompletedOperations.Add($Operation)
$ReportObject.StaleUsersDetected = $StaleNonSyncedUserER
$ReportObject.OperationDetails.StaleUsersDetectedCount = $StaleNonSyncedUserER.count
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
try # Get Exception Group members for the report (actual exception processed in previous step)
{
$Operation = 'GetExceptionGroupMember'
Write-Information -MessageData "Operation: $Operation"
$ExceptionGroupMembers = @(Get-GroupMember -GroupID $ExceptionGroupID -ErrorAction Stop | Select-Object -Property $groupMemberProperties)
$CompletedOperations.Add($Operation)
$ReportObject.ExceptionGroupMembers = $ExceptionGroupMembers
$ReportObject.OperationDetails.ExceptionGroupMembersCount = $ExceptionGroupMembers.count
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
try # Get Stale Group Members Current
{
$Operation = 'GetStaleGroupMember'
Write-Information -MessageData "Operation: $Operation"
$StaleGroupMembersCurrent = @(Get-GroupMember -GroupID $StaleGroupID -ErrorAction Stop | Select-Object -Property $groupMemberProperties)
$CompletedOperations.Add($Operation)
$ReportObject.StartingStaleGroupMembers = $StaleGroupMembersCurrent
$ReportObject.OperationDetails.StartingStaleGroupMembersCount = $StaleGroupMembersCurrent.count
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
try # Remove No longer stale users (due to exception detected)
{
$Operation = 'RemoveStaleGroupMember'
Write-Information -MessageData "Operation: $Operation"
$StaleGroupMembersToRemove = @($StaleGroupMembersCurrent.where({
$_.id -in @($StaleNonSyncedUserER.where({$null -ne $_.ExceptionType}).id) -or $_.id -notin @($StaleNonSyncedUserER.id)
}))
if ($StaleGroupMembersToRemove.count -ge 1)
{
Remove-GroupMember -GroupID $StaleGroupID -MemberID $StaleGroupMembersToRemove.id
}
$CompletedOperations.Add($Operation)
$ReportObject.RemovedFromStaleGroup = $StaleGroupMembersToRemove
$ReportObject.OperationDetails.RemovedFromStaleGroupCount = $StaleGroupMembersToRemove.count
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
try # Add newly detected stale users to the Stale Group used for observability into pending removals
{
$Operation = 'AddStaleGroupMember'
Write-Information -MessageData "Operation: $Operation"
$StaleGroupMembersToAdd = @($StaleNonSyncedUserER.where({
$_.id -notin $StaleGroupMembersCurrent.id -and $null -eq $_.ExceptionType
}))
if ($StaleGroupMembersToAdd.count -ge 1)
{
Add-GroupMember -GroupID $StaleGroupID -MemberID $StaleGroupMembersToAdd.id
}
$CompletedOperations.Add($Operation)
$ReportObject.AddedToStaleGroup = $StaleGroupMembersToAdd
$ReportObject.OperationDetails.AddedStaleGroupMembersCount = $StaleGroupMembersToAdd.count
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
try # Calculate and report on stale users to be disabled and disable them if the parameter was set
{
$Operation = 'DisableStaleUser'
Write-Information -MessageData "Operation: $Operation"
# calculate users to disable
$StaleNonSyncedUserToDisable = @($StaleNonSyncedUserER.where({$null -eq $_.ExceptionType -and $_.accountEnabled}))
$ReportObject.DisabledStaleUsers = $StaleNonSyncedUserToDisable
$ReportObject.OperationDetails.DisabledStaleUsersCount = $StaleNonSyncedUserToDisable.count
# Perform Disable Actions if required by parameter
if ($DisableStaleAccounts)
{
if ($StaleNonSyncedUserToDisable.count -ge 1)
{
Disable-StaleUser -StaleUser $StaleNonSyncedUserToDisable -ErrorAction Stop
}
$CompletedOperations.Add($Operation)
}
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
try # Calculate and report on stale users to be removed and remove them if the parameter was set
{
$Operation = 'RemoveStaleUser'
Write-Information -MessageData "Operation: $Operation"
# calculate users to remove
$StaleNonSyncedUserToRemove = @($StaleNonSyncedUserER.where({$null -eq $_.ExceptionType -and $_.StaleAgeDays -ge $_.StaleDeletePolicyDays}))
$ReportObject.RemovedStaleUsers = $StaleNonSyncedUserToRemove
$ReportObject.OperationDetails.RemovedStaleUsersCount = $StaleNonSyncedUserToRemove.count
# Perform Delete Actions if required by parameter
if ($DeleteAgedStaleAccounts)
{
if ($StaleNonSyncedUserToRemove.count -ge 1)
{
Remove-StaleUser -StaleUser $StaleNonSyncedUserToRemove -ErrorAction Stop
}
$CompletedOperations.Add($Operation)
}
}
catch
{
$ScriptErrorStrings.Add($_.tostring())
throw($Operation)
}
}
Catch
{
$ScriptErrorStrings.Add($_.tostring())
}
$ReportObject.OperationDetails.ErrorCount = $ScriptErrorStrings.count
$ReportObject.OperationDetails.CompletedOperations = $CompletedOperations -join ';'
$fileName = Get-RandomFileName -Extension 'xlsx'
$FullFilePath = Join-Path -Path $ReportFolderPath -ChildPath $fileName
$ExportExcelParams = @{
path = $FullFilePath
AutoSize = $true
}
$ReportObject.OperationDetails | Export-Excel @ExportExcelParams -WorksheetName 'OperationDetails'
$ScriptErrorStrings | Export-Excel @ExportExcelParams -WorksheetName 'Errors'
$ReportArrays = @('StaleUsersDetected', 'StartingStaleGroupMembers', 'ExceptionGroupMembers', 'RemovedFromStaleGroup', 'AddedToStaleGroup', 'DisabledStaleUsers', 'RemovedStaleUsers')
$ReportArrays.foreach( {
$ReportObject.$_ | Export-Excel @ExportExcelParams -WorksheetName $_
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment