Skip to content

Instantly share code, notes, and snippets.

@Ax-jguarracino
Created November 1, 2023 13:59
Show Gist options
  • Select an option

  • Save Ax-jguarracino/86698ada8befa3cdd72fc075849d198f to your computer and use it in GitHub Desktop.

Select an option

Save Ax-jguarracino/86698ada8befa3cdd72fc075849d198f to your computer and use it in GitHub Desktop.
Windows - Maintenance Tasks - Remove Old User Profiles
<#
.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