-
-
Save Ax-jguarracino/86698ada8befa3cdd72fc075849d198f to your computer and use it in GitHub Desktop.
Windows - Maintenance Tasks - Remove Old User Profiles
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Removes user profiles older than the provided age threshold. | |
| .PARAMETER ageLimit | |
| [ System.Int32 ] : Mandatory : EVALUATION & REMEDIATION | |
| The amount of time, in days, that should elapse before a user profile is considered "inactive" and removed. | |
| .PARAMETER whitelistedUsers | |
| [ System.String[] ] : Optional : EVALUATION & REMEDIATION | |
| Usernames that should be excluded from profile removal, even if they're older than our defined ageLimit. | |
| .HISTORY | |
| Name : Anthony Maxwell | |
| Date : 08/14/2023 | |
| Version: 1.0.0 | |
| - Initial Release. | |
| Name: John Guarracino | |
| Date: 10/03/2023 | |
| Version: 1.0.2 | |
| - Rewrote worklet to use LocalProfileUnloadTime registry values as profile detection method | |
| Name: John Guarracino | |
| Date: 11/01/2023 | |
| Version: 1.0.3 | |
| - Updated script to incorporate new evaluation logic. | |
| #> | |
| ######################################### | |
| # PARAMETERS | |
| # Define the time delta to prune profiles older than | |
| $ageLimit = 30 # For example, profiles older than 30 days | |
| # Define whitelisted usernames | |
| # Users entered here will be ignored during processing | |
| $whitelistedUsers = @('Administrator', 'defaultuser0', 'NetworkService', 'LocalService', 'systemprofile') | |
| ######################################### | |
| # Calculate the oldest allowable profile date | |
| $oldestDate = [System.DateTime]::Now.AddDays(-$ageLimit) | |
| # Get the profile list from the registry | |
| $regKeyPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" | |
| $regKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($regKeyPath) | |
| $profileList = $regKey.GetSubKeyNames() | |
| # Empty array for storing user names needing remediation | |
| $remediationList = @() | |
| Foreach ($sid in $profileList) | |
| { | |
| Try | |
| { | |
| # Attempt to get user profile info from Win32_UserProfile | |
| $userProfile = Get-CimInstance -ClassName Win32_UserProfile | Where-Object { $_.SID -eq $sid } | |
| If ($null -ne $userProfile) | |
| { | |
| # Extract username from the local path | |
| $userName = Split-Path -Path $userProfile.LocalPath -Leaf | |
| } | |
| Else | |
| { | |
| Write-Output "SID: $($sid) not found in local user profiles" | |
| } | |
| } | |
| Catch | |
| { | |
| Write-Output "Error querying Win32_UserProfile for SID $($sid): $_.Exception.Message" | |
| Continue | |
| } | |
| # Skip the whitelisted users | |
| If ($whitelistedUsers -contains $userName) | |
| { | |
| Continue | |
| } | |
| # Unload Time registry values to check | |
| $profileKey = $regKey.OpenSubKey($sid) | |
| # Retrieve decimal value and format to 8-digit hexadecimal | |
| $localProfileUnloadTimeHigh = '{0:X8}' -f $profileKey.GetValue("LocalProfileUnloadTimeHigh", $null) | |
| $localProfileUnloadTimeLow = '{0:X8}' -f $profileKey.GetValue("LocalProfileUnloadTimeLow", $null) | |
| # Check for High and Low load Unload Time values | |
| $unloadTime = If ($localProfileUnloadTimeHigh -and $localProfileUnloadTimeLow) | |
| { | |
| # Concatenate and convert Unload times to a DateTime object | |
| [datetime]::FromFileTime("0x$localProfileUnloadTimeHigh$localProfileUnloadTimeLow") | |
| } | |
| # Unload Time is null or undefined $null safety net. | |
| Else | |
| { | |
| $null | |
| } | |
| # Resolve the time delta | |
| $lastUsed = $null | |
| If ($unloadTime) | |
| { | |
| $delta = [System.DateTime]::Now - $unloadTime | |
| If ($delta -is [TimeSpan]) | |
| { | |
| $lastUsed = $delta.Days | |
| } | |
| } | |
| # Evaluate if the profile is older than our allowed range | |
| If ($unloadTime -and $unloadTime -lt $oldestDate) | |
| { | |
| Write-Output "The profile for user $userName has not been logged into for $lastUsed days. `nSID: $sid `nProfile Path: $($userProfile.LocalPath) `nProfile was last unloaded on $unloadTime" | |
| Write-Output "" | |
| $remediationList += $userName | |
| } | |
| } | |
| # Close registry | |
| $regKey.Close() | |
| ######################################### | |
| # VARS | |
| # Keep track of remove profile count | |
| $removedCount = 0 | |
| # Track freed up disk space from profile removals | |
| $freedStorage = 0 | |
| ######################################### | |
| # FUNCTIONS | |
| Function Get-DirectorySize | |
| { | |
| [OutputType([System.Int64])] | |
| Param ( | |
| [Parameter(Mandatory, Position = 0)] | |
| [ValidateScript({ [System.IO.Directory]::Exists($_) })] | |
| [System.String]$Path | |
| ) | |
| # Define an empty 64-bit integer to hold the directory size info | |
| [System.Int64] $size = 0 | |
| # Get the directoryinfo for the specified path | |
| $dirInfo = [System.IO.DirectoryInfo]::new($Path) | |
| Try | |
| { | |
| # Iterate files in the current directory | |
| Foreach ($file in $dirInfo.GetFiles()) | |
| { | |
| $size += $file.Length | |
| } | |
| } | |
| Catch | |
| { | |
| Write-Error "Failed to retrieve files at path `"$($dirInfo.FullName)`"." | |
| } | |
| Try | |
| { | |
| # Iterate directories in the current directory | |
| Foreach ($dir in $dirInfo.GetDirectories()) | |
| { | |
| $size += Get-DirectorySize -Path $dir.FullName | |
| } | |
| } | |
| Catch | |
| { | |
| Write-Error "Failed to retrieve directories at path `"$($dirInfo.FullName)`"." | |
| } | |
| Return $size | |
| } | |
| Function ConvertTo-LowestDenomination | |
| { | |
| Param ( | |
| [Parameter(Mandatory, Position = 0)] | |
| [System.Int64]$Number | |
| ) | |
| Foreach ($unit in (1TB, 'TB'), (1GB, 'GB'), (1MB, 'MB'), (1KB, 'KB')) | |
| { | |
| # Unpack the unit of measure and string representation | |
| $divisor, $unitStr = $unit | |
| # Evaluate if our profile size, divided by our unit of measure is 1 or greater ( for clean visual formatting ) | |
| If ($Number / $divisor -ge 1) | |
| { | |
| # Convert bytes to our current unit of measure, round and string format | |
| Return [System.Math]::Round($Number / $divisor, 2).ToString() + $unitStr | |
| } | |
| } | |
| } | |
| ######################################### | |
| # REMEDIATION | |
| # Calculate the oldest allowable profile date | |
| $oldestDate = [System.DateTime]::Now.AddDays(-$ageLimit) | |
| # --! Evaluate and Remediate Non-Compliance !-- | |
| If ($remediationList.Count -gt 0) | |
| { | |
| # Display users to be remediated | |
| Write-Output "Profiles flagged for remediation: $($remediationList -join ', ')" | |
| Foreach ($user in $remediationList) | |
| { | |
| Try | |
| { | |
| # Translate username to SID | |
| $sid = (New-Object System.Security.Principal.NTAccount $user).Translate([System.Security.Principal.SecurityIdentifier]).Value | |
| # Find the user profile via CIM (WMI) and SID | |
| $userProfile = Get-CimInstance -ClassName Win32_UserProfile | Where-Object { $_.Special -eq $false -and $_.SID -eq $sid } | |
| # Check if a profile was found | |
| If ($null -ne $userProfile) | |
| { | |
| # Get the size of the user's profile directory before deletion | |
| $profileSize = Get-DirectorySize -Path $userProfile.LocalPath | |
| # Attempt to remove the user profile | |
| Write-Output "`nAttempting to remove the profile for user: $user" | |
| $userProfile | Remove-CimInstance -ErrorAction Stop | |
| Write-Output "Successfully removed the profile for user: $user" | |
| # Increment our counters with the size of the deleted profile | |
| $removedCount++ | |
| $freedStorage += $profileSize | |
| } | |
| Else | |
| { | |
| Write-Output "No local profile found for user: $user" | |
| } | |
| } | |
| Catch | |
| { | |
| Write-Error "Failed to remove the profile for user: $user. Error: $($_.Exception.Message)" | |
| } | |
| } | |
| # Provide summary of freed storage and removed profile count | |
| Write-Output "`n====== Profile Removal Summary: ======" | |
| Write-Output "Profiles Removed: $removedCount" | |
| Write-Output "Space Freed: $(ConvertTo-LowestDenomination -Number $freedStorage)" | |
| Write-Output "`n====== Summary End ======" | |
| } | |
| Else | |
| { | |
| Write-Output "All user profiles on this device are compliant. Now exiting." | |
| Exit 0 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment