From b1ee9f9d024dbf9d711393c8014f9d6135aad27e Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Wed, 30 Oct 2024 18:34:02 +0530 Subject: [PATCH] Disable or Delete Inactive Users in M365 --- .../DeleteInactiveUsers.ps1 | 349 ++++++++++++++++++ 1 file changed, 349 insertions(+) create mode 100644 Find and Remove Inactive Users/DeleteInactiveUsers.ps1 diff --git a/Find and Remove Inactive Users/DeleteInactiveUsers.ps1 b/Find and Remove Inactive Users/DeleteInactiveUsers.ps1 new file mode 100644 index 0000000..4d5998c --- /dev/null +++ b/Find and Remove Inactive Users/DeleteInactiveUsers.ps1 @@ -0,0 +1,349 @@ +<# +====================================================================================================== +Name: Identify and Remove Inactive Users in Microsoft 365 +Version: 1.0 +Website: admindroid.com + +Script Highlights: +1. The script automatically verifies and installs the Microsoft Graph PowerShell SDK module (if not installed already) upon your confirmation. +2. Generates and exports all the inactive Microsoft 365 users into a CSV file. +3. Identifies sign-in enabled inactive users and disable their account. +4. Retrieves all the licensed inactive users and deletes them to reuse licenses. +5. Finds all the sign-in disabled users and deletes their accounts. +6. Identifies the external inactive users and removes them from the organization. +7. Allows to use the previously generated inactive users report and take actions later (i.e disable or delete). +8. The script is scheduler-friendly. +9. Supports certificate-based authentication (CBA) too. + +For detailed Script execution: https://blog.admindroid.com/identify-and-remove-inactive-users-in-microsoft-365/ + +============================================================================================================ +#> + + + + + + +Param +( + [int]$InactiveDays, + [int]$InactiveDays_NonInteractive, + [ValidateSet('Delete','Disable')] + [string]$Action, + [switch]$GenerateReportOnly, + [switch]$ExcludeNeverLoggedInUsers, + [switch]$EnabledUsersOnly, + [switch]$DisabledUsersOnly, + [switch]$LicensedUsersOnly, + [switch]$ExternalUsersOnly, + [switch]$Force, + [switch]$CreateSession, + [string]$ImportCsv, + [string]$TenantId, + [string]$ClientId, + [string]$CertificateThumbprint +) + +# Function to connect to Microsoft Graph +Function Connect_MgGraph { + # Check if Microsoft Graph module is installed + $MsGraphModule = Get-Module Microsoft.Graph -ListAvailable + if ($MsGraphModule -eq $null) { + Write-Host "`nImportant: Microsoft Graph module is unavailable. It is mandatory to have this module installed in the system to run the script successfully." + $confirm = Read-Host "Are you sure you want to install Microsoft Graph module? [Y] Yes [N] No" + if ($confirm -match "[yY]") { + Write-Host "Installing Microsoft Graph module..." + Install-Module Microsoft.Graph -Scope CurrentUser -AllowClobber + Write-Host "Microsoft Graph module is installed in the machine successfully" -ForegroundColor Magenta + } else { + Write-Host "Exiting. `nNote: Microsoft Graph module must be available in your system to run the script" -ForegroundColor Red + Exit + } + } + + # Disconnects existing connection + if($CreateSession.IsPresent) + { + Disconnect-MgGraph -WarningAction SilentlyContinue | Out-Null + } + + Write-Host "`nConnecting to Microsoft Graph..." + if (($TenantId -ne "") -and ($ClientId -ne "") -and ($CertificateThumbprint -ne "")) { + # Use certificate-based authentication if TenantId, ClientId, and CertificateThumbprint are provided + Connect-MgGraph -TenantId $TenantId -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome + } else { + # Use delegated permissions (Scopes) if credentials are not provided + Connect-MgGraph -Scopes "User.EnableDisableAccount.All","User.DeleteRestore.All" -NoWelcome + } + + # Verify connection + if ((Get-MgContext) -ne $null) { + Write-Host "Connected to Microsoft Graph PowerShell using account: $((Get-MgContext).Account)`n" + } else { + Write-Host "Failed to connect to Microsoft Graph." -ForegroundColor Red + Exit + } +} + +Connect_MgGraph + +# Function to validate mandatory parameters (inactive days & action) +Function mandatory-parameter-validation{ + # Inline validation for InactiveDays + if ((!$InactiveDays -and !$InactiveDays_NonInteractive) -and ($ImportCsv -eq "")) { + [int]$Script:InactiveDays = Read-Host "`n Enter InactiveDays" + if ([string]::IsNullOrWhiteSpace($InactiveDays) -or $InactiveDays -le 0) { + Write-Host "`n Invalid InactiveDays provided. Exiting script." -ForegroundColor Red + Exit + } + } + + # Inline validation for Action + if (!$GenerateReportOnly -and ($Action -eq "" )) { + $Script:Action = Read-Host "`n Enter the Action (Delete or Disable)" + if ($Action -ne 'Delete' -and $Action -ne 'Disable') { + Write-Host "`n Invalid Action provided. Exiting script." -ForegroundColor Red + Exit + } + } +} + +# Function to handle deletion/disabling +Function Delete_Inactive_Users { + param($UserRecord) + + $LogEntry = @() + $User_Id = $UserRecord.UPN + $Acc_Status = $UserRecord.'Account Status' + Try{ + switch ($Action) { + 'Delete' { + Remove-MgUser -UserId $User_Id -ErrorAction Stop + $LogEntry += "[INFO] - '$User_Id' is deleted." + break + } + 'Disable' { + if($Acc_Status -eq 'Disabled'){ + $LogEntry += "[INFO] - '$User_Id' is disabled already." + }else{ + Update-MgUser -UserId $User_Id -AccountEnabled:$false -ErrorAction Stop + $LogEntry += "[INFO] - '$User_Id' is disabled." + } + break + } + } + }catch{ + $LogEntry += "[ERROR] - Failed to $Action the user '$User_Id': $($_.Exception.Message)`n" + } + + + $LogEntry | Out-File -FilePath $LogFilePath -Append + +} + +# Fuction to handle user confirmation to perform deletion operation +Function Confirm_User_Deletion_Action{ + param($CsvFile) + + # Automatically performs action without confirmation + if ($Force) { + Import-Csv -Path $CsvFile | ForEach-Object{ Delete_Inactive_Users -UserRecord $_ } + Write-Host "`n The log file is available in: " -NoNewline -ForegroundColor Yellow + Write-Host " $LogFilePath" + } + # Asks confirmation to perform operation + else { + $Confirm = $(Write-Host "`n Are you sure you want to $Action $PrintedUser inactive users in the CSV file? [Y/N]: " -ForegroundColor Yellow -NoNewline; Read-Host) + if ($Confirm -match "[yY]") { + Import-Csv -Path $CsvFile | ForEach-Object { Delete_Inactive_Users -UserRecord $_ } + Write-Host "`n The log file is available in: " -NoNewline -ForegroundColor Yellow + Write-Host " $LogFilePath" + $Prompt = New-Object -ComObject wscript.shell + $UserInput = $Prompt.popup("Do you want to open the log file?", 0, "Open Output File", 4) + if ($UserInput -eq 6) { + Invoke-Item "$LogFilePath" + } + }else{ + Write-Host "`n No action performed." + } + } +} + +# Function to filter inactive users from all the available users +Function Filter_Inactive_Users{ + + # Process each user + $RequiredProperties=@('UserPrincipalName','EmployeeId','CreatedDateTime','AccountEnabled','Department','JobTitle','RefreshTokensValidFromDateTime','SigninActivity') + Get-MgUser -All -Property $RequiredProperties | Select $RequiredProperties | ForEach-Object { + $Count++ + $UPN = $_.UserPrincipalName + Write-Progress -Activity "`nProcessing user: $Count - $UPN" + $EmployeeId = $_.EmployeeId + $LastInteractiveSignIn = $_.SignInActivity.LastSignInDateTime + $LastNon_InteractiveSignIn = $_.SignInActivity.LastNonInteractiveSignInDateTime + $CreatedDate = $_.CreatedDateTime + $AccountEnabled = $_.AccountEnabled + $Department = $_.Department + $JobTitle = $_.JobTitle + + # Calculate inactive days for interactive sign-ins + if($LastInteractiveSignIn -eq $null) + { + $LastInteractiveSignIn = "Never Logged In" + $InactiveDays_InteractiveSignIn = "-" + } + else + { + $InactiveDays_InteractiveSignIn = (New-TimeSpan -Start $LastInteractiveSignIn).Days + } + if($LastNon_InteractiveSignIn -eq $null) + { + $LastNon_InteractiveSignIn = "Never Logged In" + $InactiveDays_NonInteractiveSignIn = "-" + } + else + { + $InactiveDays_NonInteractiveSignIn = (New-TimeSpan -Start $LastNon_InteractiveSignIn).Days + } + + # Get user account status + if($AccountEnabled -eq $true) + { + $AccountStatus='Enabled' + } + else + { + $AccountStatus='Disabled' + } + + #Get licenses assigned to mailboxes + $Subscriptions = Get-MgUserLicenseDetail -UserId $UPN | Select SkuId, SkuPartNumber + $Licenses = $Subscriptions.SkuPartNumber + $AssignedLicense = @() + + #Convert license plan to friendly name + if($Licenses.count -eq 0) + { + $LicenseDetails = "No License Assigned" + } + else + { + foreach($License in $Licenses) + { + $EasyName = $FriendlyNameHash[$License] + if(!($EasyName)) + {$NamePrint = $License} + else + {$NamePrint = $EasyName} + $AssignedLicense += $NamePrint + } + $LicenseDetails = $AssignedLicense -join ", " + } + $Print = 1 + + #Inactive days based on interactive signins filter + if($InactiveDays_InteractiveSignIn -ne "-"){ + if(($InactiveDays -ne "") -and ($InactiveDays -gt $InactiveDays_InteractiveSignIn)) + { + $Print=0 + } + } + + #Inactive days based on non-interactive signins filter + if($InactiveDays_NonInteractiveSignIn -ne "-"){ + if(($InactiveDays_NonInteractive -ne "") -and ($InactiveDays_NonInteractive -gt $InactiveDays_NonInteractiveSignIn)) + { + $Print=0 + } + } + + # Exclude never logged-in users + if ($ExcludeNeverLoggedInUsers -and ($LastInteractiveSignIn -eq "Never Logged In")) { + $Print = 0 + } + + # Filter for external users + if ($ExternalUsersOnly -and $UPN -notmatch '#EXT#') { + $Print = 0 + } + + #Signin Allowed Users + if($EnabledUsersOnly.IsPresent -and $AccountStatus -eq 'Disabled'){ + $Print=0 + } + + #Signin disabled users + if($DisabledUsersOnly.IsPresent -and $AccountStatus -eq 'Enabled'){ + $Print=0 + } + + # Licensed users only filter + if ($LicensedUsersOnly -and $Licenses.Count -eq 0){ + $Print = 0 + } + + # Generate report only + if ($Print -eq 1) { + $Script:PrintedUser++ + $ExportResult = [PSCustomObject]@{'UPN'=$UPN;'Last Interactive SignIn Date'=$LastInteractiveSignIn;'Last Non Interactive SignIn Date'=$LastNon_InteractiveSignIn;'Inactive Days(Interactive SignIn)'=$InactiveDays_InteractiveSignIn;'Inactive Days(Non-Interactive Signin)'=$InactiveDays_NonInteractiveSignIn;'License Details'=$LicenseDetails;'Account Status'=$AccountStatus;'Creation Date'=$CreatedDate;'Emp id'=$EmployeeId;'Department'=$Department;'Job Title'=$JobTitle} + $ExportResult | Export-Csv -Path $ExportCSV -NoTypeInformation -Append + } + } +} + +$Location = Get-Location + +$ExportCSV = "$Location\Inactive_M365_User_Report_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm-ss` tt).ToString()).csv" +$LogFilePath = "$Location\User_$($Action)_Log_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm-ss` tt).ToString()).txt" + +$ExportResult = "" +$ExportResults = @() + +$FriendlyNameHash=Get-Content -Raw -Path .\LicenseFriendlyName.txt -ErrorAction Stop | ConvertFrom-StringData + +$Count=0 +$PrintedUser=0 + +mandatory-parameter-validation + +if ($ImportCsv -ne "") { + $PrintedUser = Import-Csv -Path $ImportCsv | measure | Select -ExpandProperty Count + if($PrintedUser -eq 0){ + Write-Host "`n No users found in $ImportCsv." + } + else{ + Confirm_User_Deletion_Action -CsvFile $ImportCsv + } +}else { + Filter_Inactive_Users + if($PrintedUser -eq 0){ + Write-Host "`n Inactive users not found." + } + else{ + if(!$GenerateReportOnly){ + Write-Host "`n Detailed inactive users report available in: " -ForegroundColor Yellow + Write-Host "`n $ExportCSV" + Confirm_User_Deletion_Action -CsvFile $ExportCSV + } + } +} + +Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green +Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n `n + +if($GenerateReportOnly.IsPresent){ + if((Test-Path -Path $ExportCSV) -eq "True") + { + Write-Host "`n Detailed inactive users report available in: " -ForegroundColor Yellow + Write-Host "`n $ExportCSV" + $Prompt = New-Object -ComObject wscript.shell + $UserInput = $Prompt.popup("Do you want to open the output file?", 0, "Open Output File", 4) + if ($UserInput -eq 6) { + Invoke-Item "$ExportCSV" + } + } + else{ + Write-Host "`n No user found for the specific criteria" -ForegroundColor Red + } +} \ No newline at end of file