Forum Discussion

ryedogg72's avatar
ryedogg72
Copper Contributor
Nov 06, 2024

UserProfile Management with PowerShell

We have an issue where quarterly Nessus scans enumerate vulnerability findings for every user profile on an endpoint. This started me on a path to remove obsolete user profiles to reduce the noise from Nessus. I need to accomplish three things in my final script: 1. set the execution policy to bypass; 2. reset the NTUser.dat to the last write time (if this is not done, the third criteria will not return any hits); 3. find all user profiles older than 60 days and delete them. I did try the GPO to delete profiles older than a certain number of days, but it never removes all of them.

I pieced together a script from a couple diff sources and came up with the below. My PowerShell-fu is not that advanced so I am looking for suggestions to make it more efficient. The script works, but I noticed that empty user profile folders at C:\Users\ were left behind. Please advise. Is this a candidate to be made into a function?

$ErrorActionPreference = "SilentlyContinue"
$Report = $Null
$Path = "C:\Users"
$UserFolders = $Path | GCI -Directory
$currentDate = Get-Date
$ageLimit = 60
$userProfiles = Get-ChildItem -Path $Path

Set-ExecutionPolicy Bypass -Force

ForEach ($UserFolder in $UserFolders) {

$UserName = $UserFolder.Name
If (Test-Path "$Path\$UserName\NTUSer.dat") {
    $Dat = Get-Item "$Path\$UserName\NTUSer.dat" -force
    $DatTime = $Dat.LastWriteTime
    If ($UserFolder.Name -ne "default") {
      $Dat.LastWriteTime = $UserFolder.LastWriteTime
    }
  }  
}
ForEach ($profile in $userProfiles) {

$lastWriteTime = $profile.LastWriteTime
$profileAge = ($currentDate - $lastWriteTime).Days
If ($profileAge -ge $ageLimit) {
        Remove-Item -Path $profile.FullName -Recurse -Force
   }
}

4 Replies

  • DarkStar's avatar
    DarkStar
    Copper Contributor

    Personally I would change to 90 days to have a better error on the side of caution. Here is a down and dirty script, 4 lines. Excludes, All Users, Default*, Admin* and Public. Deletes the User Folder if empty.  Use with much caution and test test test before deploying. 

    Set-ExecutionPolicy Bypass -Force
    $CleanUpFolder = "C:\Users"
    
    Get-ChildItem -Directory $CleanUpFolder -Recurse -Force | Where {$_.LastWriteTime -lt (Get-Date).AddDays(-90) -and $_.Name -notlike "Default*" -and $_.Name -ne "Public" -and $_.Name -ne "All Users" -and $_.Name -notlike "Admin*"} | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue
    Get-ChildItem -Directory $CleanUpFolder -Recurse -Force | Sort-Object -Property FullName -Descending | Where { $_.GetFiles().Count -eq 0 -and $_.GetDirectories().Count -eq 0 } | Remove-Item -Force -ErrorAction SilentlyContinue

     

  • $ErrorActionPreference = "SilentlyContinue"
    $Path = "C:\Users"
    $UserFolders = Get-ChildItem -Path $Path -Directory
    $currentDate = Get-Date
    $ageLimit = 60

    # Set the execution policy to bypass (use with caution in production)
    Set-ExecutionPolicy Bypass -Force

    # Loop through each user profile folder
    ForEach ($UserFolder in $UserFolders) {

        $UserName = $UserFolder.Name

        # Only process profiles that have an NTUser.dat file
        $NTUserPath = "$Path\$UserName\NTUSER.DAT"
        If (Test-Path $NTUserPath) {
            $Dat = Get-Item $NTUserPath -Force
            $DatTime = $Dat.LastWriteTime

            # Reset NTUSER.DAT's LastWriteTime to match the folder's LastWriteTime
            If ($UserFolder.Name -ne "default") {
                $Dat.LastWriteTime = $UserFolder.LastWriteTime
            }
        }

        # Calculate profile age and delete if older than $ageLimit
        $profileAge = ($currentDate - $UserFolder.LastWriteTime).Days
        If ($profileAge -ge $ageLimit) {
            # Remove user profile and any content inside it (use with caution)
            Try {
                Remove-Item -Path $UserFolder.FullName -Recurse -Force
                Write-Host "Deleted profile: $UserName"
            } Catch {
                Write-Host "Error deleting profile: $UserName - $_"
            }
        }
    }

    # Optional: Clean up empty folders (if necessary)
    $EmptyFolders = Get-ChildItem -Path $Path -Directory | Where-Object { $_.GetFileSystemInfos().Count -eq 0 }
    ForEach ($EmptyFolder in $EmptyFolders) {
        Try {
            Remove-Item -Path $EmptyFolder.FullName -Force
            Write-Host "Deleted empty folder: $($EmptyFolder.Name)"
        } Catch {
            Write-Host "Error deleting empty folder: $($EmptyFolder.Name) - $_"
        }
    }

    Write-Host "Profile cleanup complete."

    • ricardovalmada's avatar
      ricardovalmada
      Copper Contributor

      Hey! Thanks for that script and explanation! Is it possible (and how) to exclude profiles from being deleted, like system defaults and post-created admin accounts? Do the cleaning operation also delete profile registry data?

  • Your script is on the right track for deleting old user profiles, but there are a few improvements that can make it more efficient, avoid some pitfalls (such as leaving empty folders), and potentially clean up obsolete or unnecessary parts of the code. Here's an enhanced version of the script, with some optimizations and added checks:

    Suggestions & Improvements:

    1. Use -Directory when getting user profiles: You can directly filter for directories with -Directory in the Get-ChildItem cmdlet, reducing the need for additional checks.
    2. Ensure NTUser.dat exists: You don't need to call Get-Item multiple times in a loop if you can check for existence first. I’ve added that check with Test-Path.
    3. Handle empty profile folders: When a profile folder is removed, we should check if it’s empty before attempting to remove it. If it's empty, you can remove it with Remove-Item as well.
    4. Use -Force for visibility of hidden files: This ensures hidden files like NTUser.dat are also handled properly.
    5. Remove the report object: If you're not using $Report, it can be omitted.
    6. Ensure execution policy is set before script runs: It’s more logical to set the execution policy at the start, so it’s enforced before running any other script actions.

Resources