349 lines
12 KiB
PowerShell
Raw Permalink Normal View History

<#
======================================================================================================
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
}
}