mirror of
https://github.com/admindroid-community/powershell-scripts.git
synced 2025-12-17 16:35:19 +00:00
349 lines
12 KiB
PowerShell
349 lines
12 KiB
PowerShell
<#
|
|
======================================================================================================
|
|
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
|
|
}
|
|
} |