18 views +0 -0

Cleanup Stale Profiles

# Cleanup-StaleProfiles.ps1
# Removes user profiles inactive for more than N days, using User Profile Service events

$DaysInactive = 25
$CutoffDate   = (Get-Date).AddDays(-$DaysInactive)
$LogFile      = 'C:\ProfileCleanup.log'

# Paths that should never be touched
$ExcludedPaths = @(
    'C:\Windows\ServiceProfiles\NetworkService'
    'C:\Windows\ServiceProfiles\LocalService'
    'C:\Windows\system32\config\systemprofile'
    'C:\Windows\SysWOW64\config\systemprofile'
)

# Usernames that should never be touched
$ExcludedUsers = @(
    'Administrator'
    'Default'
    'Default User'
    'Public'
    'All Users'
    'WDAGUtilityAccount'
)

# Ensure log directory exists
$logDir = Split-Path $LogFile -Parent
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }

function Write-Log {
    param([string]$Message)
    $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $line = "$ts  $Message"
    Write-Output $line
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
}

function Get-FreeSpaceGB {
    $drive = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='C:'"
    [math]::Round($drive.FreeSpace / 1GB, 2)
}

$freeBefore = Get-FreeSpaceGB
Write-Log "=== Profile cleanup started (cutoff: $CutoffDate) ==="
Write-Log "Free space on C: before: $freeBefore GB"

# 1. Build last-activity table from User Profile Service events
$lastActivity = @{}
Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-User Profile Service/Operational'
    Id      = 1,2
} -ErrorAction SilentlyContinue | ForEach-Object {
    $sid = $_.UserId.Value
    if (-not $lastActivity.ContainsKey($sid) -or $lastActivity[$sid] -lt $_.TimeCreated) {
        $lastActivity[$sid] = $_.TimeCreated
    }
}

# 2. Currently logged-on users
$loggedOnUsers = @()
try {
    $loggedOnUsers = quser 2>$null | Select-Object -Skip 1 | ForEach-Object {
        ($_.Trim() -split '\s+')[0].TrimStart('>')
    }
} catch { }

# 3. Get candidate profiles
$profiles = Get-CimInstance -ClassName Win32_UserProfile | Where-Object {
    -not $_.Special -and
    -not $_.Loaded -and
    $_.LocalPath -like 'C:\Users\*' -and
    $ExcludedPaths -notcontains $_.LocalPath
}

# 4. Evaluate each profile
$removedCount = 0
foreach ($p in $profiles) {
    $username = Split-Path $p.LocalPath -Leaf
    $sid      = $p.SID

    if ($ExcludedUsers -contains $username) {
        Write-Log "SKIP (excluded user): $($p.LocalPath)"
        continue
    }
    if ($loggedOnUsers -contains $username) {
        Write-Log "SKIP (logged on): $($p.LocalPath)"
        continue
    }

    $lastSeen = $lastActivity[$sid]
    if ($lastSeen) {
        $source = 'UserProfileService event'
    } else {
        $lastSeen = $p.LastUseTime
        $source   = 'LastUseTime (no events)'
    }

    if (-not $lastSeen) {
        Write-Log "SKIP (no activity data): $($p.LocalPath)"
        continue
    }

    if ($lastSeen -lt $CutoffDate) {
        try {
            $p | Remove-CimInstance -Confirm:$false -ErrorAction Stop
            Write-Log "REMOVED: $($p.LocalPath)  | last activity $lastSeen ($source)"
            $removedCount++
        } catch {
            Write-Log "FAILED:  $($p.LocalPath)  | $($_.Exception.Message)"
        }
    } else {
        Write-Log "KEEP:    $($p.LocalPath)  | last activity $lastSeen ($source)"
    }
}

$freeAfter = Get-FreeSpaceGB
$reclaimed = [math]::Round($freeAfter - $freeBefore, 2)

Write-Log "Free space on C: after:  $freeAfter GB"
Write-Log "Profiles removed: $removedCount"
Write-Log "Space reclaimed:  $reclaimed GB"
Write-Log "=== Profile cleanup finished ==="