Remove Profiles from a local or remote computer


A common need for a Remote Desktop Services (RDS) and/or Citrix farm admin, is to remove local profiles from a server.
Another example for this, is the question posted a few days ago in the PowerShell.org forum here.

Funny thing, about 6 years ago, I wrote a vbscript that does that. It just doesn’t filter by last used date.

Anyway, I decided to write the Remove-Profile PowerShell function:

#requires -version 3
function Remove-Profile {
param(
[string[]]$ComputerName = $env:ComputerName,
[pscredential]$Credential = $null,
[string[]]$Name,
[ValidateRange(0,365)][int]$DaysOld = 0,
[string[]]$Exclude,
[switch]$IgnoreLastUseTime,
[switch]$Remove
)
$ComputerName | ForEach-Object {
if(Test-Connection -ComputerName $_ -BufferSize 16 -Count 2 -Quiet) {
$params = @{
ComputerName = $_
Namespace = 'root\cimv2'
Class = 'Win32_UserProfile'
}
if($Credential -and (@($env:ComputerName,'localhost','127.0.0.1','::1','.') -notcontains $_)) {
$params.Add('Credential', $Credential)
}
if($null -ne $Name) {
if($Name.Count -gt 1) {
$params.Add('Filter', ($Name | % { "LocalPath = '{0}'" -f $_ }) -join ' OR ')
} else {
$params.Add('Filter', "LocalPath LIKE '%{0}'" -f ($Name -replace '\*', '%'))
}
}
Get-WmiObject @params | ForEach-Object {
$WouldBeRemoved = $false
if(($_.SID -notin @('S-1-5-18', 'S-1-5-19', 'S-1-5-20')) -and
((Split-Path -Path $_.LocalPath -Leaf) -notin $Exclude) -and (-not $_.Loaded) -and ($IgnoreLastUseTime -or (
($_.LastUseTime) -and (([WMI]'').ConvertToDateTime($_.LastUseTime)) -lt (Get-Date).AddDays(-1*$DaysOld)))) {
$WouldBeRemoved = $true
}
$prf = [pscustomobject]@{
PSComputerName = $_.PSComputerName
Account = (New-Object System.Security.Principal.SecurityIdentifier($_.Sid)).Translate([System.Security.Principal.NTAccount]).Value
LocalPath = $_.LocalPath
LastUseTime = if($_.LastUseTime) { ([WMI]'').ConvertToDateTime($_.LastUseTime) } else { $null }
Loaded = $_.Loaded
}
if(-not $Remove) {
$prf | Select-Object -Property *, @{N='WouldBeRemoved'; E={$WouldBeRemoved}}
}
if($Remove -and $WouldBeRemoved) {
try {
$_.Delete()
$Removed = $true
} catch {
$Removed = $false
Write-Error -Exception $_
}
finally {
$prf | Select-Object -Property *, @{N='Removed'; E={$Removed}}
}
}
}
} else {
Write-Warning -Message "Computer $_ is unavailable"
}
}
}

You can use it to report all the profiles in the local computer:

Remove-Profile

To report all the profiles, except a specific profile:

Remove-Profile -Exclude Administrator

To report profiles last used in the past 90 days that would be deleted:

Remove-Profile -DaysOld 90 | Where-Object { $_.WouldBeRemoved }

To report against a remote computer:

Remove-Profile -ComputerName myRemoteServer

To report against a collection of remote computers, authenticating different credentials:

Remove-Profile -ComputerName $Computers -Credential $cred -DaysOld 90

To ignore the LastUseTime value in case the account never logged-on locally (e.g. an IIS ApplicationPool), use the IgnoreLastUseTime switch:

Remove-Profile -ComputerName WebServer01 -DaysOld 30 -IgnoreLastUseTime 

To really remove the profiles, use the -Remove switch:

Remove-Profile -DaysOld 90 -Remove

HTH,
Martin.

Embed PowerShell code in a batch file


In a certain scenario, I needed a batch file (bat or cmd extension) that runs PowerShell code, and I could have only one file, so I couldn’t go with the easy way of a batch file calling PowerShell.exe with the -File parameter specifying the path to a ps1 file.

For this, I created a special batch file with a header that reads the contents of itself, excludes the lines that have the batch code (lines stat start with @@) and then runs the rest in PowerShell.

Here is the batch template, just replace the lines below the comment that says “POWERSHELL CODE STARTS HERE” with your PowerShell code.

@ECHO off
@setlocal EnableDelayedExpansion
@goto label1
@echo this lines should not be printed
@echo and this whole block (lines 4-8) can actually be removed
@echo I added these just for Charlie22911
@:label1
@set LF=^
@SET command=#
@FOR /F "tokens=*" %%i in ('findstr -bv @ "%~f0"') DO SET command=!command!!LF!%%i
@powershell -noprofile -noexit -command !command! & goto:eof
# *** POWERSHELL CODE STARTS HERE *** #
Write-Host 'This is PowerShell code being run from inside a batch file!' -Fore red
$PSVersionTable
Get-Process -Id $PID | Format-Table

Though not intended, it’s another way of bypassing the ExecutionPolicy even if it’s set in Group Policy, since the code is run as commands and not a script file.

HTH,
Martin.

Passing down the WhatIf and Confirm preferences to other cmdlets from an Advanced Function


A colleague of mine was writing a cool function that tidies up the GPOs, and he wanted to implement the -WhatIf common cmdlet parameter in his function.

There’s a simple way of doing that, just add the [cmdletBinding(SupportsShouldProcess)] and the param() blocks in the top of your function, add the if ($PSCmdlet.ShouldProcess($target)) {} in the body. Where the scriptblock is where you put all your function logic. For example:

function Do-Something {
    [cmdletbinding(SupportsShouldProcess)]
    Param()
    $target = Get-Something
    if ($PSCmdlet.ShouldProcess($target)) {
        Set-Something -Target $target
        Set-AnotherThing -Target $target
    }
}

Then, if you call it with the -WhatIf or -Confirm switches, the output would be:


PS> Do-Something -WhatIf
What if: Performing the operation "Do-Something" on target "'Target'".

PS> Do-Something -Confirm
Confirm
Are you sure you want to perform this action?
Performing the operation "Do-Something" on target "'Target'".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):

But he wanted the WhatIf message or confirmation for each of the operations he was doing inside his function, not just one.

So instead of using the “plain” if ($PSCmdlet.ShouldProcess($target)) {}, I suggested he used a hashtable to “splat” the values of -WhatIf and -Confirm “down” to the cmdlets in his function. For example:

function Do-Something {
    [cmdletbinding(SupportsShouldProcess)]
    Param()
    $target = Get-Something
    $commonParams = @{}
    if($WhatIfPreference.IsPresent) {$commonParams.Add('WhatIf', $true)}
    if($ConfirmPreference.IsPresent) {$commonParams.Add('Confirm', $true)}
    Set-Something -Target $target @commonParams
    Set-AnotherThing -Target $target @commonParams
}

Then, if you call it with the -WhatIf or -Confirm switches, the output would be:


PS> Do-Something -WhatIf
What if: Performing the operation "Set-Something" on target "'Target'".
What if: Performing the operation "Set-AnotherThing" on target "'Target'".

PS> Do-Something -Confirm
Confirm
Are you sure you want to perform this action?
Performing the operation "Set-Something" on target "'Target'".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):

Confirm
Are you sure you want to perform this action?
Performing the operation "Set-AnotherThing" on target "'Target'".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):

For more information about the common cmdlet parameters and the risk mitigation switches, run:

Get-Help -Name about_CommonParameters

HTH,
Martin.

Using VSCode and the PowerShell extension in an offline environment


The PowerShell ISE was first shipped with PowerShell 2.0 (November 2006), and greatly improved in PowerShell 3.0 (August 2012), with the PowerShell Tabs, the Show-Command Add-on and the snippets (CTRL+J). But since then, it pretty much stayed the same.

Fast forward to May 2017, David Wilson from the PowerShell team announced (amongst other things) that:

“The PowerShell ISE has been the official editor for PowerShell throughout most of the history of Windows PowerShell. Now with the advent of the cross-platform PowerShell Core, we need a new official editor that’s available across all supported OS platforms and versions. Visual Studio Code is now that editor and the majority of our effort will be focused there.”

This means that if haven’t already, it’s time to get acquainted with Visual Studio Code (aka VSCode).

There are lots of blogs and tutorials out there, describing the VSCode installation process and usage, with or without the PowerShell extension, but none are discussing the installation of both in an “offline” environment.
This post is an attempt to bridge that gap, and explain the steps required to prepare your PowerShell development environment in case your machine is disconnected from the internet and you can’t directly install everything.

Step 1 – Download VSCode:

The installation package can be downloaded from: https://code.visualstudio.com/Download

Direct link for Windows x64: https://go.microsoft.com/fwlink/?Linkid=852157

Step 2 – Download the PowerShell extension:

The PowerShell extension can be downloaded from the extensions repository:
https://marketplace.visualstudio.com

Direct link for version 1.5.1:
https://marketplace.visualstudio.com/_apis/public/gallery/publishers/ms-vscode/vsextensions/PowerShell/1.5.1/vspackage

Step 3 – Copy the files to the “offline” machine

Step 4 – Install:

The VSCode installation is pretty much straight forward. “Next, Next, Next… Install”.
The PowerShell extension (vsix) installation is done manually, from the VSCode itself:

vscode-vsix-install

  1. Open the extensions sidebar by clicking the last icon on the bottom left
  2. Click on the ellipsis (…) in the right upper corner
  3. Choose Install from vsix
  4. Browse to the vsix you previously downloaded, and that’s it. Maybe*

* There’s currently a problem installing the PowerShell extension version 1.5.1 on VSCode 1.19.x.
But don’t worry, there’s a workaround:
Since the vsix is actually a zip file, you just need to extract and copy the contents of the extension folder to $env:userprofile.vscodeextensions{Name_And_Version_Of_The_Extension}

To automate it:


$vsixFile = 'C:Tempms-vscode.PowerShell.1.5.1.vsix'
$tmp = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([guid]::NewGuid().GUID)
$zipFile = Rename-Item -Path $vsixFile -NewName ($vsixFile -replace '.vsix', '.zip') -PassThru | Select-Object -Expand FullName
Expand-Archive -Path $zipFile -DestinationPath $tmp
Copy-Item -Path "$tmpextension" -Destination ('{0}.vscodeextensions{1}' -f $env:USERPROFILE, (Get-Item -Path $zipFile).BaseName) -Recurse -Force
Remove-Item -Path $tmp -Recurse -Force

For more information and resources on writing PowerShell with VSCode, check out these:

Transitioning from PowerShell ISE to VS Code:
https://channel9.msdn.com/Blogs/MVP-Azure/Transitioning-from-PowerShell-ISE-to-VS-Code

Experimenting with VSCode instead of the ISE:
https://voiceofthedba.com/2017/09/18/experimenting-with-vscode-instead-of-the-ise/

Debugging PowerShell script in Visual Studio Code:
https://blogs.technet.microsoft.com/heyscriptingguy/2017/02/06/debugging-powershell-script-in-visual-studio-code-part-1/

HTH,
Martin.

Get the certificate selected in Get-Credential


Following Matt Bongiovi’s post at the Hey, Scripting Guy! Blog about PowerShell support for certificate credentials, I ported the main parts of the c# code he references in his post to PowerShell.

So here you have, a quick-and-dirty Get-CertificateFromCredential function you can use to get the certificate for the credentials the user selected from the drop down in the Get-Credential window:

function Get-CertificateFromCredential {
param([PSCredential]$Credential)
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class NativeMethods {
public enum CRED_MARSHAL_TYPE {
CertCredential = 1,
UsernameTargetCredential
}
[StructLayout(LayoutKind.Sequential)]
public struct CERT_CREDENTIAL_INFO {
public uint cbSize;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] rgbHashOfCert;
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CredUnmarshalCredential(
IntPtr MarshaledCredential,
out CRED_MARSHAL_TYPE CredType,
out IntPtr Credential
);
}
'@ -ReferencedAssemblies System.Runtime.InteropServices
$credData = [IntPtr]::Zero
$credInfo = [IntPtr]::Zero
$credType = [NativeMethods+CRED_MARSHAL_TYPE]::CertCredential
try {
$credData = [System.Runtime.InteropServices.Marshal]::StringToHGlobalUni($Credential.UserName);
$success = [NativeMethods]::CredUnmarshalCredential($credData, [ref] $credType, [ref] $credInfo)
if ($success) {
[NativeMethods+CERT_CREDENTIAL_INFO] $certStruct = [NativeMethods+CERT_CREDENTIAL_INFO][System.Runtime.InteropServices.Marshal]::PtrToStructure(
$credInfo, [System.Type][NativeMethods+CERT_CREDENTIAL_INFO])
[byte[]] $rgbHash = $certStruct.rgbHashOfCert
[string] $hex = [BitConverter]::ToString($rgbHash) -replace '-'
$certCredential = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store -ArgumentList @(
[System.Security.Cryptography.X509Certificates.StoreName]::My,
[System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser
)
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
$certsReturned = $store.Certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $hex, $false)
if($null -eq $certsReturned) {
throw ('Could not find a certificate with thumbprint {0}' -f $hex)
}
$certsReturned[0]
}
} catch {
throw ('An error occured: {0}' -f $_.Exception.Message)
}
finally {
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($credData)
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($credInfo)
if($null -ne $store) { $store.Close() }
}
}

Then, you can use the function:


$cred = Get-Credential -Message 'Select the SMARTCARD'
Get-CertificateFromCredential -Credential $cred

Important: Keep in mind that Get-Credential cmdlet doesn’t verify the credentials anywhere, it just opens the $Host.UI.PromptForCredential popup and returns a PSCredential object. The credentials themselves are verified only when used with another cmdlet.
This means that the user can select a certificate from the dropdown in the Get-Credential window, enter an incorrect PIN and this function will still return the certificate.

I’ve also been following issue #3048 in the PowerShell repository on github. Hopefully, native support for certificate authentication will be added in a future version (6.1.0?)

HTH,
Martin.