How to get remote or local user profiles using PowerShell
Much like my past posts on finding logged on users and logging them off, we are going to wrap an executable so that we can use it with PowerShell and the pipeline. This executable, however, is not available natively on Windows so we’ll have to grab it.
Delprof2
Delprof2 is a commandline tool written by Helge Klein that is, what they call, the ‘unofficial successor to Microsoft’s Delprof’. It’s main usage is cleaning up inactive profiles in Windows.
I’ve used this tool on and off over the years. After picking up PowerShell, it was easy for me to use traditional command line tools less because they don’t integrate with the pipeline. I missed this one enough that I decided to wrap it with PowerShell and make it work!
So to follow along and use the tool I’m walking through, make sure you download delprof2.
Syntax
I’m not going to run through everything you can do with delprof2 because that would be an entire blog post in and of itself, so instead lets focus on the ones that I’ve integrated with PowerShell.
delprof2 /l
This is the simplest usage and simply lists all of the profiles on a system. By default it will list out all profiles and what it would do with them by default:
DelProf2 by Helge Klein (http://helgeklein.com)
Listing inactive profiles on 'VHOST01'.
Ignoring profile '\\VHOST01\C$\Users\Default' (reason: special profile)
Ignoring profile '\\VHOST01\C$\Users\Public' (reason: special profile)
Ignoring profile '\\VHOST01\C$\Users\Administrator' (reason: in use)
The following user profiles match the deletion criteria:
\\VHOST01\C$\Users\D
\\VHOST01\C$\Users\L
You’ll notice that it even lists out the special profiles and safely ignores profiles in use.
If you ran delprof2 without any parameters, it would have deleted the D
and L
profiles in this case.
It does also include some filtering parameters. Specifically for what I thought was most useful, it can include certain profiles (which excludes all that don’t match):
delprof2 /l /id:a*
Exclude them (which includes those that don’t match):
delprof2 /l /ed:adm?nistrator
And find ones older than a certain number of days:
delprof2 /l /d:30
Each of this uses the /l
flag so that it only lists data, it doesn’t delete anything. Both the include and exclude parameters support *
as a wildcard and ?
as any single character.
Oh, and it does also work on remote systems:
delprof2 /l /c:ALPHAWOLF
And we can combine any of the above parameters to get a combination of includes, excludes, that are older than a certain number of days, and from a remote computer. Yeah, it is that awesome!
Wrapping delprof2
So now, if we want to wrap delprof2 and return objectized data for the pipeline to consume, we need to do some string parsing. The best tool for the job? Regex! Though, funny story, in writing this blog post I discovered that I’m much better at regex than I was when I originally wrote this function, so if you are just learning regex, have no fear! Better regex skillz do come with time.
Prereqs
Ok, so first things first, we need to determine how we are going to run delprof2. For my function, I decided that I’d have the .exe is a standard location and make that an optional parameter:
Param (
...
[string]$delprof2 = "c:\windows\system32\delprof2.exe"
)
I’ve selected system32 because that is the default start path for elevated PowerShell, but pick something that works for you.
To actually run delprof2, we now use the ampersand &
, which is the call operator, like this:
& $delprof2 '/l'
Dealing with the output
Then, to get the output, we need to assign that to a variable:
$output = & $delprof2 '/l'
Once we have the output, we’ll use a foreach
loop to parse through each string checking to see what each line actually is.
Ignored profiles
An ignored profile looks like:
Ignoring profile '\\VHOST01\C$\Users\Default' (reason: special profile)
We need to get several pieces of data from there. Besides the fact that it is an ignored profile, I’d like to collect the profile name and the reason. So a little bit of regex magic like:
if ($line -match "^Ignoring profile \'(?<profilePath>\\\\([^\\]+\\)+(?<profile>[^\']+))\' \(reason\: (?<reason>[^\)]+)\)")
Will deliver us some nice data:
Name Value
---- -----
profile Default
reason special profile
profilePath \\VHOST01\C$\Users\Default
1 Users\
0 Ignoring profile '\\VHOST01\C$\Users\Default' (reason: special profile)
So we can build a return object:
[pscustomobject]@{
Ignored = $true
Reason = $Matches.reason
ProfilePath = $Matches.profilePath
Profile = $Matches.profile
}
Included profiles
Included profiles are a little easier, there isn’t additional information to parse. So the regex is going to look like:
if ($line -match "^(?<profilePath>\\\\([^\\]+\\)+(?<profile>\S+))")
This gives us more useful data that we can use in a return object:
[pscustomobject]@{
Ignored = $false
Reason = $null
ProfilePath = $Matches.ProfilePath
Profile = $Matches.Profile
}
And also input!
Since we do need to be able to define parameters for this function, we do want to be able to also accept input with some parameters.
For the param
block, we can start with (I’m excluding the Parameter()
settings for brevity):
param (
[string]$Name
,
[string[]]$Include
,
[string[]]$Exclude
,
[int]$OlderThan
)
So what we’ll need is a couple of foreach loops to get all the switches in an array:
$switches = & {
"/l"
if ($PSBoundParameters.ContainsKey('Include')) {
foreach ($inc in $include) {
"/id:$inc"
}
}
if ($PSBoundParameters.ContainsKey('Exclude')) {
foreach ($exc in $exclude) {
"/ed:$exc"
}
}
if ($PSBoundParameters.ContainsKey('OlderThan')) {
"/d:$OlderThan"
}
}
Then, since we are running it against a remote computer, we should test connectivity:
if ($PSBoundParameters.ContainsKey('Name') -and ($Name -ne $env:COMPUTERNAME)) {
if (Test-Connection $Name -Count 1 -Quiet) {
$computer += "/c:$Name"
} else {
Throw "Cannot ping $name"
}
}
And now our command would look like:
$return = & $delprof2 $computer $switches
Functionitized
So the whole function would be:
Function Get-InactiveUserProfiles {
[cmdletbinding()]
param (
[Parameter(
Position = 1,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true
)]
[Alias('Computer','ComputerName','HostName')]
[ValidateNotNullOrEmpty()]
[string]$Name = $env:COMPUTERNAME
,
[Parameter()]
[string[]]$include # Supports ? and *
,
[Parameter()]
[string[]]$exclude # Supports ? and *
,
[Parameter()]
[int]$olderThan
,
[string]$delprof2 = "c:\windows\system32\delprof2.exe"
)
Begin {
$switches = & {
"/l"
if ($include) {
foreach ($inc in $include) {
"/id:$inc"
}
}
if ($exclude) {
foreach ($exc in $exclude) {
"/ed:$exc"
}
}
if ($olderThan) {
"/d:$olderThan"
}
}
}
Process {
if ($PSBoundParameters.ContainsKey('Name') -and ($Name -ne $env:COMPUTERNAME)) {
if (Test-Connection $Name -Count 1 -Quiet) {
$computer = "/c:$Name"
} else {
Throw "Cannot ping $Name"
}
}
Write-Verbose "CMD: $delprof2 $computer $switches"
$return = & $delprof2 $computer $switches
foreach ($line in $return) {
if ($line -match "^Ignoring profile \'(?<profilePath>\\\\([^\\]+\\)+(?<profile>[^\']+))\' \(reason\: (?<reason>[^\)]+)\)") {
# Ignored profile
[pscustomobject]@{
Ignored = $true
Reason = $Matches.reason
ProfilePath = $Matches.profilePath
Profile = $Matches.profile
ComputerName = $Name
}
} elseif ($line -match "^(?<profilePath>\\\\([^\\]+\\)+(?<profile>\S+))") {
# Included profile
[pscustomobject]@{
Ignored = $false
Reason = $null
ProfilePath = $Matches.ProfilePath
Profile = $Matches.Profile
ComputerName = $Name
}
} elseif ($line -match "^Access denied to profile \'(?<profilePath>\\\\([^\\]+\\)+(?<profile>[^\']+))\'") {
# Access denied
[pscustomobject]@{
Ignored = $true
Reason = 'Access denied'
ProfilePath = $Matches.ProfilePath
Profile = $Matches.Profile
ComputerName = $Name
}
}
}
}
End{}
}
This is also now in my Utilities Github repo
Get-InactiveUserProfiles
Now we can check for inactive user profiles locally or remotely! Lets try it out.
Simple usage
To check the local computer (piping to format table for nice output):
Get-InactiveUserProfiles | Format-Table
We get:
Ignored Reason ProfilePath Profile ComputerName
------- ------ ----------- ------- ------------
True special profile \\VHOST01\C$\Users\Default Default VHOST01
True special profile \\VHOST01\C$\Users\Public Public VHOST01
True in use \\VHOST01\C$\Users\Administrator Administrator VHOST01
False \\VHOST01\C$\Users\D D VHOST01
False \\VHOST01\C$\Users\L L VHOST01
Or if we wanted to get only profiles that start with D
and haven’t been used for longer than 20 days:
Get-InactiveUserProfiles -Include 'D*' -OlderThan 20 | Format-Table
We get:
Ignored Reason ProfilePath Profile ComputerName
------- ------ ----------- ------- ------------
True special profile \\VHOST01\C$\Users\Default Default VHOST01
True special profile \\VHOST01\C$\Users\Public Public VHOST01
True directory inclusion \\VHOST01\C$\Users\Administrator Administrator VHOST01
True directory inclusion \\VHOST01\C$\Users\L L VHOST01
False \\VHOST01\C$\Users\D D VHOST01
Advanced usage
Now, remember that we can both accept pipeline input and output objects? Yeah, so we can do some pretty cool things like, if you’ve got to do some profile cleanup on a bunch of computers:
Get-ADComputer -Filter {Name -like 'CallCenter-*'} | Get-InactiveUserProfiles -Exclude 'Administrator' -OlderThan 60 | Where-Object {-not $_.Ignored}
That will list out profiles from all the computers returned in that AD query that aren’t the Administrator
profile and haven’t been touched in 2 months. Notice I’m using the Where-Object cmdlet to filter for only the profiles that meet that criteria. So you could even use Export-Excel
and create a nice report for the call center manager if needed.
Oh, and I should mention that the -Name
parameter is passed via the pipeline for Get-InactiveUserProfiles
, no need to explicitly state it.
You could also have a text file list of computers and use that:
Get-Content C:\path\to\comps.txt | Get-InactiveUserProfiles | Where-Object {-not $_.ignored}
There’s a LOT that you can do with it now that it works in PowerShell!
Conclusion
Anyways, I hope you find this helpful! Be sure to check out the other functions in my Utilities Repo. If you find something broken, would like to see an additional feature, or maybe you’ve got a question about something, open an issue.
Or you can comment here, reach out to me on Twitter, by email, LinkedIn, whatever strikes your fancy.
Next week I’ll be writing about how you can:
Get-InactiveProfiles -Computer RemoteComputer | Remove-InactiveProfiles
Leave a Comment