mirror of
https://github.com/admindroid-community/powershell-scripts.git
synced 2025-12-17 08:25:20 +00:00
Disable or Delete Inactive Users in M365
This commit is contained in:
parent
175aa3b34d
commit
b1ee9f9d02
349
Find and Remove Inactive Users/DeleteInactiveUsers.ps1
Normal file
349
Find and Remove Inactive Users/DeleteInactiveUsers.ps1
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user