Forum Discussion
Issue with date modified for NTUSER.DAT
- Feb 23, 2018
Here is the code from the script:
#Purpose: Used to set the ntuser.dat last modified date to that of the last modified date on the user profile folder.
#This is needed because windows cumulative updates are altering the ntuser.dat last modified date which then defeats
#the ability for GPO to delete profiles based on date and USMT migrations based on date.$ErrorActionPreference = "SilentlyContinue"
$Report = $Null
$Path = "C:\Users"
$UserFolders = $Path | GCI -DirectoryForEach ($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
}
Write-Host $UserName $DatTime
Write-Host (Get-item $Path\$UserName -Force).LastWriteTime
$Report = $Report + "$UserName`t$DatTime`r`n"
$Dat = $Null
}
}
Ryan Pertusio
First off, thank you for taking the time to look at this and reply. What exactly is this value supposed to represent? I would assume the last time the profile was loaded?
I tested but all profiles are returning the value of "01/01/1601 00:00:00".
EDIT: After closer review, the workstation I tested on, NONE of the profiles had LocalProfileLoadTime values... strange. I tested on another workstation and it provided useful data. About 20 out of the 180 profiles did have erroneous values though.
So here's the next question: How accurate is this data? If it follows the same patterns as the other data, there can be some inconsistency, I could say, as I've demonstrated by pulling 180 profiles where 20 of them were invalid. Also, what about my other workstation that simply does not have LocalProfileLoadTime values at all?
When should this value be used over other values? If this value returns erroneously or not at all, what's nest? IconCache.db, then user profile directory?
Unfortunately, all I can say is that MS told me it's the value they use for the Group Policy to cleanup old profiles. Maybe there's more to their secret sauce that they're not sharing, OR, they're using inconsistent data.
- MB_99Dec 01, 2022Copper ContributorHad a thought, why not create a logon script that runs each time a user logs in, that appends their username and logon date to a CSV file. Then you can use a PowerShell script to go through the each record to determine which profiles can be removed or not based on the date. This would take any of the guess work out, and would take the reliance of modify dates to determine last logon. I don't have time right now but if I do in the near future I will write something and post back.
- midnight51Dec 02, 2022Copper Contributor
MB_99 , Ryan Pertusio , Ryan Pertusio
This is an OK idea, but I would not fully rely on it. The best you could hope for is to put this at the top of your IF statement to use IF the file exists. Because inevitably this will run on a workstation that does not have this file and your script will be looking for it and it won't be found--what happens then? A bunch of stuff that you did not want to be deleted is now gone.
Again, my Org does not use the Group Policy to delete profiles. We're using a script on a "on run" basis, or sometimes via Task Scheduler to do the cleanup. This is the operation I landed on. The $UserProfileThreshold is set outside of the scriptblock as a script parameter. The basis for deletion is determined one of two ways. If the User's LocalProfileLoadTime is able to be determined via the registry, this timestamp is used first. If these registry properties don't exist, I take the newest of the IconCache.db and the LastWriteTime of the user profile directory. These are the three values I trust the most, the LocalProfileLoadTime value being the most accurate out of the bunch.
OldUserProfiles { $DeleteProfile = (Get-Date).AddDays(-$UserProfileThreshold) $ExcludedUsers = @('Administrator','Public','Default',"$ComputerName") $UserFolders = "C:\Users" | Get-ChildItem -Directory -Exclude $ExcludedUsers $CimProfiles = Get-CimInstance -Class Win32_UserProfile $RegProfiles = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\ProfileList\*" $NA = [datetime] "11/11/1111 11:11:11 AM" $Valid = [datetime] "2/2/2000 22:22:22 PM" $ProfileData = ForEach ($UserFolder in $UserFolders) { $UserName = $UserFolder.Name $UserProfileTime = $UserFolder.LastWriteTime $CimProfile = $CimProfiles | Where-Object { $_.localpath -eq $UserFolder.FullName } if ($CimProfile) { $CimProfileTime = $CimProfile.LastUseTime } else { $CimProfileTime = $NA } #$NTUsr = Get-Item "$UserFolder\NTUSer.dat" -Force #if ($NTUsr) { $NTUsrTime = $NTUsr.LastWriteTime } else { $NTUsrTime = $NA } #$UsrClass = Get-Item "$UserFolder\AppData\Local\Microsoft\Windows\UsrClass.dat" -force #if ($UsrClass) { $UsrClassTime = $UsrClass.LastWriteTime } else { $UsrClassTime = $NA } $IconCache = Get-Item "$UserFolder\AppData\Local\IconCache.db" -Force if ($IconCache) { $IconCacheTime = $IconCache.LastWriteTime } else { $IconCacheTime = $NA} $RegProfile = $RegProfiles | Where-Object { $_.ProfileImagePath -eq $UserFolder.FullName } $ProfileLoadHighDec = $RegProfile.LocalProfileLoadTimeHigh $ProfileLoadLowDec = $RegProfile.LocalProfileLoadTimeLow # Convert Decimal to Hex string, Example: ProfileLoadHighHex = 01d8fd57 / ProfileLoadLowHex = 86b1d3bc $ProfileLoadHighHex = [System.Convert]::ToString($ProfileLoadHighDec,16) $ProfileLoadLowHex = [System.Convert]::ToString($ProfileLoadLowDec,16) # Concatenate hex strings, Example: 01d8fd5786b1d3bc $ProfileHexJoined = -join ($ProfileLoadHighHex,$ProfileLoadLowHex) # Convert to DateTime format, Example: 11/21/2022 03:15:37 $TimestampInt = [Convert]::ToInt64($ProfileHexJoined,16) $ProfileLoadTime = [DateTime]::FromFileTimeutc($TimestampInt) # If ProfileLoadTime exists, use that as the newest timestamp, if not use the newest of UserProfileTime and IconCacheTime if ($ProfileLoadTime -gt $Valid) { $LastUpdated = $ProfileLoadTime } else { $LastUpdated = $UserProfileTime,$IconCacheTime | Sort-Object | Select-Object -Last 1 } if ($LastUpdated -lt $DeleteProfile) { $Delete = $True } else { $Delete = $False } [PSCustomObject]@{ UserName = $UserName ProfileFullname = $UserFolder.FullName UserProfileTime = $UserProfileTime CimProfileTime = $CimProfileTime # NTUsrTime = $NTUsrTime # UsrClassTime = $UsrClassTime IconCacheTime = $IconCacheTime ProfileLoadTime = $ProfileLoadTime LastUpdated = $LastUpdated Delete = $Delete } # Remove all iteration data as to not corrupt datastream 'UserName','UserProfileTime','CimProfile','CimProfileTime','NTUsr','NTUsrTime','UsrClass','UsrClassTime','IconCache','IconCacheTime','LastUpdated','Delete' | ForEach-Object { Remove-Variable $_ } 'RegProfile','ProfileLoadHighDec','ProfileLoadLowDec','ProfileLoadHighHex','ProfileLoadLowHex','ProfileHexJoined','TimestampInt','ProfileLoadTime'| ForEach-Object { Remove-Variable $_ } } ForEach ($UserProfile in $ProfileData) { if ($UserProfile.Delete) { if ($UserProfile.CimProfileTime) { $CimProfile = $CimProfiles | Where-Object { $_.LocalPath -eq $UserProfile.ProfileFullName } Remove-CimInstance $CimProfile Write-Log "User OS profile permanently deleted: $($UserProfile.ProfileFullname)" $LogFile } else { # If the profile found in C:\Users does not exist in Win32_UserProfiles #Remove-Item $UserProfile.ProfileFullName -Recurse -Force -ErrorAction Stop #Write-Log "User OS profile permanently deleted: $($UserProfile.ProfileFullname)" $LogFile Write-Log "USER OS PROFILE DOES NOT EXIST IN THE REGISTRY: $($UserProfile.ProfileFullname)" $LogFile } } } Write-Log "END OldUserProfiles cleanup" $LogFile }- cameron1340Dec 19, 2022Copper Contributor
Your script shows accurately the issues we face in IT to support our business devices. I'm wondering if you would be willing to share a completed script you use to help us periodically run and free up disk space where profiles are not used after 90 days. I only ask because not all of us are fluent in PowerShell to achieve this outcome.