Regardless what type of estate of Windows-devices, there always seems to be a need of clearing out unused profiles from a computer to save diskspace, increase performance and what not.
In Windows 7 there was an issue (resolved by a hotfix) that simply loading up a ntuser.dat file would change the timestamp of when it was last written to. It seems that this has now been the defacto default behaviour for Windows 10, and a long-running thread disusses different ways of adressing the issue – how can you identify if a profile was recently used on a device? Nirsoft tools (aren’t they great?) provide a great and easy to read overview if logon history based on security event logs.
That seems tedious. Using the written time for the folder doesn’t seem to be accurate – and the risk of removing active user profiles is high. However, if one could track the last-write time for the registry entry for the profile – we should be good, right? Unfortunately – last write time for the registry entry isn’t there out of the box using Powershell (or VBScript etc). Seems to be a few things posted on Technet Gallery (to be gone soon) that will provide the missing piecies.
Where are we looking? Right here;
Use the function Add-RegKeyMember, loop through all profiles and then filter any potential things you want to leave behind – and we should be able to clear out not so active profiles. A few dangerous lines commented out so you can copy and paste at will.
function Add-RegKeyMember { <# .SYNOPSIS Adds note properties containing the last modified time and class name of a registry key. .DESCRIPTION The Add-RegKeyMember function uses the unmanged RegQueryInfoKey Win32 function to get a key's last modified time and class name. It can take a RegistryKey object (which Get-Item and Get-ChildItem output) or a path to a registry key. .EXAMPLE PS> Get-Item HKLM:\SOFTWARE | Add-RegKeyMember | Select Name, LastWriteTime Show the name and last write time of HKLM:\SOFTWARE .EXAMPLE PS> Add-RegKeyMember HKLM:\SOFTWARE | Select Name, LastWriteTime Show the name and last write time of HKLM:\SOFTWARE .EXAMPLE PS> Get-ChildItem HKLM:\SOFTWARE | Add-RegKeyMember | Select Name, LastWriteTime Show the name and last write time of HKLM:\SOFTWARE's child keys .EXAMPLE PS> Get-ChildItem HKLM:\SYSTEM\CurrentControlSet\Control\Lsa | Add-RegKeyMember | where classname | select name, classname Show the name and class name of child keys under Lsa that have a class name defined. .EXAMPLE PS> Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall | Add-RegKeyMember | where lastwritetime -gt (Get-Date).AddDays(-30) | >> select PSChildName, @{ N="DisplayName"; E={gp $_.PSPath | select -exp DisplayName }}, @{ N="Version"; E={gp $_.PSPath | select -exp DisplayVersion }}, lastwritetime | >> sort lastwritetime Show applications that have had their registry key updated in the last 30 days (sorted by the last time the key was updated). NOTE: On a 64-bit machine, you will get different results depending on whether or not the command was executed from a 32-bit or 64-bit PowerShell prompt. #> [CmdletBinding()] param( [Parameter(Mandatory, ParameterSetName="ByKey", Position=0, ValueFromPipeline)] # Registry key object returned from Get-ChildItem or Get-Item [Microsoft.Win32.RegistryKey] $RegistryKey, [Parameter(Mandatory, ParameterSetName="ByPath", Position=0)] # Path to a registry key [string] $Path ) begin { # Define the namespace (string array creates nested namespace): $Namespace = "CustomNamespace", "SubNamespace" # Make sure type is loaded (this will only get loaded on first run): Add-Type @" using System; using System.Text; using System.Runtime.InteropServices; $($Namespace | ForEach-Object { "namespace $_ {" }) public class advapi32 { [DllImport("advapi32.dll", CharSet = CharSet.Auto)] public static extern Int32 RegQueryInfoKey( Microsoft.Win32.SafeHandles.SafeRegistryHandle hKey, StringBuilder lpClass, [In, Out] ref UInt32 lpcbClass, UInt32 lpReserved, out UInt32 lpcSubKeys, out UInt32 lpcbMaxSubKeyLen, out UInt32 lpcbMaxClassLen, out UInt32 lpcValues, out UInt32 lpcbMaxValueNameLen, out UInt32 lpcbMaxValueLen, out UInt32 lpcbSecurityDescriptor, out Int64 lpftLastWriteTime ); } $($Namespace | ForEach-Object { "}" }) "@ # Get a shortcut to the type: $RegTools = ("{0}.advapi32" -f ($Namespace -join ".")) -as [type] } process { switch ($PSCmdlet.ParameterSetName) { "ByKey" { # Already have the key, no more work to be done :) } "ByPath" { # We need a RegistryKey object (Get-Item should return that) $Item = Get-Item -Path $Path -ErrorAction Stop # Make sure this is of type [Microsoft.Win32.RegistryKey] if ($Item -isnot [Microsoft.Win32.RegistryKey]) { throw "'$Path' is not a path to a registry key!" } $RegistryKey = $Item } } # Initialize variables that will be populated: $ClassLength = 255 # Buffer size (class name is rarely used, and when it is, I've never seen # it more than 8 characters. Buffer can be increased here, though. $ClassName = New-Object System.Text.StringBuilder $ClassLength # Will hold the class name $LastWriteTime = $null switch ($RegTools::RegQueryInfoKey($RegistryKey.Handle, $ClassName, [ref] $ClassLength, $null, # Reserved [ref] $null, # SubKeyCount [ref] $null, # MaxSubKeyNameLength [ref] $null, # MaxClassLength [ref] $null, # ValueCount [ref] $null, # MaxValueNameLength [ref] $null, # MaxValueValueLength [ref] $null, # SecurityDescriptorSize [ref] $LastWriteTime )) { 0 { # Success $LastWriteTime = [datetime]::FromFileTime($LastWriteTime) # Add properties to object and output them to pipeline $RegistryKey | Add-Member -NotePropertyMembers @{ LastWriteTime = $LastWriteTime ClassName = $ClassName.ToString() } -PassThru -Force } 122 { # ERROR_INSUFFICIENT_BUFFER (0x7a) throw "Class name buffer too small" # function could be recalled with a larger buffer, but for # now, just exit } default { throw "Unknown error encountered (error code $_)" } } } } $profiles = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" | Add-RegKeyMember | Select Name, LastWriteTime foreach ($p in $profiles) { if ($p.lastwritetime -lt $(Get-Date).Date.AddDays(-90)) { $key = $($p.name) -replace "HKEY_LOCAL_MACHINE","HKLM:" $path = (Get-ItemProperty -Path $key -Name ProfileImagePath).ProfileImagePath $path.tolower() if ($path.ToLower() -notlike 'c:\windows\*' -and $path.ToLower() -notlike 'c:\users\adm*') { write-host "delete " $path #Get-CimInstance -Class Win32_UserProfile | Where-Object { $path.split('\')[-1] -eq $User } | Remove-CimInstance #-ErrorAction SilentlyContinue #Add-Content c:\windows\temp\DeleteProfiles.log -Value "$User was deleted from this computer." } } }