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."
}
}
}