ShouldProcess failing in PowerShell7 - powershell

Environment: Windows Server 2022 21H2, Powershell 7.2 (running as administrator)
I have a script that implements ShouldProcess, which works fine in Windows PowerShell 5. However, in PowerShell 7, the script invariably throws the error Cannot find an overload for "ShouldProcess" and the argument count: "1". ShouldProcess at MSDoc says that the one-argument overload for $PSCmdlet.ShouldProcess() exists and should work.
It's failing, as above. Why?
The script in question is pasted below; it's in a script module:
function Remove-DomainUserProfile {
<#
#Comment-based help removed for space considerations
#>
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")]
param(
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[Parameter(ParameterSetName='SpecificProfile')]
[Parameter(ParameterSetName='ByAge')]
[Parameter(ParameterSetName='AllProfiles')]
[String[]]$ComputerName = $env:ComputerName,
[Parameter(Mandatory=$true,ParameterSetName='SpecificProfile')]
[Parameter(ParameterSetName='ByAge')]
[Alias("UserName","sAMAccountName")]
[String]$Identity,
[Parameter(ParameterSetName='ByAge')]
[Parameter(ParameterSetName='AllProfiles')]
[Switch]$DomainOnly,
[Parameter(ParameterSetName='SpecificProfile')]
[Parameter(ParameterSetName='ByAge')]
[Int]$Age,
[Parameter(Mandatory=$true,ParameterSetName='AllProfiles')]
[Switch]$All
)
BEGIN {
if (-NOT (Test-IsAdmin)) {
Write-Output "This function requires being run in an Administrator session! Please start a PowerShell
session with Run As Administrator and try running this command again."
return
}
$NoSystemAccounts = "SID!='S-1-5-18' AND SID!='S-1-5-19' AND SID!='S-1-5-20' AND NOT SID LIKE 'S-1-5-%-500' "
# Don't even bother with the system or administrator accounts.
if ($DomainOnly) {
$SIDQuery = "SID LIKE '$((Get-ADDomain).DomainSID)%' " # All domain account SIDs begin
with the domain SID
} elseif ($Identity.Length -ne 0) {
$SIDQuery = "SID LIKE '$(Get-UserSID -AccountName $Identity)' "
}
$CutoffDate = (Get-Date).AddDays(-$Age)
$Query = "SELECT * FROM Win32_UserProfile "
}
PROCESS{
ForEach ($Computer in $ComputerName) {
Write-Verbose "Processing Computer $Computer..."
if ($SIDQuery) {
$Query += "WHERE " + $SIDQuery
} else {
$Query += "WHERE " + $NoSystemAccounts
}
if ($All) {
Write-Verbose "Querying WMI using '$Query'"
$UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query
} else {
Write-Verbose "Querying WMI using '$Query' and filtering for profiles last used before $CutoffDate ..."
$UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query | Where-Object {
[Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime) -lt $CutoffDate }
}
ForEach ($UserProfile in $UserProfiles) {
if ($PSCmdlet.ShouldProcess($UserProfile)) {
Write-Verbose "Deleting profile object $UserProfile ($(Get-SIDUser $UserProfile.SID))..."
$UserProfile.Delete()
}
}
}
}
END {}
}

To complement Santiago Squarzon's excellent analysis:
The behavior, present up to at least PowerShell 7.2.1, should be considered a bug, because any object should be auto-convertible to a string in a .NET method call.
There is no reason for [pscustomobject] a.k.a [psobject] instances to act differently than instances of any other type (irrespective of whether implicit stringification makes sense in a given situation); to give a simple example:
If (42).ToString((Get-Item /)) works, ...
... there's no reason why (42).ToString(([pscustomobject] #{ foo=1 })) shouldn't.
Note that implicit stringification in the context of cmdlets / functions / script is not affected; e.g., Get-Date -Format ([pscustomobject] #{ foo=1 }) doesn't cause an error.
See GitHub issue #16988.
The reason that the serialization infrastructure is involved at all is that the obsolete WMI cmdlets such as Get-WmiObject aren't natively available in PowerShell (Core) v6+ anymore, and using them implicitly makes use of the Windows PowerShell Compatibility feature:
This entails using a hidden powershell.exe child process, communication with which requires use of serialization, during which most non-primitive types lose their type identity and are emulated with method-less [psobject] instances that contain copies of the original object's properties.
In PowerShell v3 and above, and especially in PowerShell (Core) v6+, use the CIM cmdlets instead, such as Get-CimInstance, instead:
While similar to the WMI cmdlets in many respects, an important difference is that objects returned from CIM cmdlets have no methods; instead, methods must be called via Invoke-CimMethod.
See this answer for more information.

For reference, this error can be reproduced on both PowerShell versions 5.1 and Core. The steps to reproduce is passing a System.Management.Automation.PSObject as argument to the .ShouldProcess(String) overload. It makes sense, by looking at your comment mentioning a serialized object. In below example, if the System.Diagnostics.Process object is not serialized it works properly on both versions.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")]
param()
$obj = [System.Management.Automation.PSSerializer]::Deserialize(
[System.Management.Automation.PSSerializer]::Serialize((Get-Process)[0])
)
# will throw
if ($PSCmdlet.ShouldProcess($obj)) { 'hello' }
}
Test-ShouldProcess

Related

How can I use async or parallelism in Powershell class constructors?

I have a Powershell class (a couple, actually; it's nested) that I'm creating instances of in a loop. The constructor has a bunch of tasks that populate the object (including those nested classes, which also populate themselves). However, those tasks are sometimes a bit slow, and I'd like to be able to execute multiple tasks and instantiate multiple objects concurrently. How can I do that within Powershell?
Example class:
Class Server {
Server([string] $ServerName) {
$this.ServerName = $ServerName
$this.ComputerSystem = Get-CimInstance Win32_ComputerSystem -ComputerName $ServerName
$this.OperatingSystem = Get-CimInstance Win32_OperatingSystem -ComputerName $ServerName
$this.Bios = Get-CimInstance -ClassName Win32_BIOS -ComputerName $ServerName
$this.NetworkAdapter = Get-CimInstance Win32_NetworkAdapterConfiguration -ComputerName $ServerName
}
}
Unfortunately, there is no convenient solution as of PowerShell 7.3.0, because properties in PowerShell custom classes cannot be implemented by way of accessor methods.
Adding this ability in a future version has been proposed in GitHub issue #2219 - years ago.
The following:
uses methods (e.g., .Bios() in lieu of properties (e.g. .Bios), so that the "property value" can be retrieved dynamically; the backing instance property, which is hidden with the hidden attribute, is a hashtable that maps "property names" to their values.
uses Start-ThreadJob, which ships with PowerShell (Core) 7+, and can be installed in Windows PowerShell with Install-Module ThreadJob, to asynchronously launch thread-based jobs on construction, which perform the Get-CimInstance calls in the background.
Class Server {
# Instance variables:
[string] $ServerName
# A hidden map (hashtable) that maps what are conceptually properties to the
# commands that retrieve their values, via thread jobs.
# Note that the static values defined here are *script blocks*, which are replaced
# with what their *invocation* evaluates to in the constructor.
# Note: By default, up to 5 thread jobs are permitted to run at a time.
# You can modify this limit with -ThrottleLimit on the first Start-ThreadJob call,
# which then applies to all subsequent Start-ThreadJob calls that do not themselves
# use -ThrottleLimit in the same session.
hidden [hashtable] $_jobsMap = #{
ComputerSystem = { Start-ThreadJob { Get-CimInstance Win32_ComputerSystem -ComputerName $using:ServerName } }
OperatingSystem = { Start-ThreadJob { Get-CimInstance Win32_OperatingSystem -ComputerName $using:ServerName } }
Bios = { Start-ThreadJob { Get-CimInstance -ClassName Win32_BIOS -ComputerName $using:ServerName } }
NetworkAdapter = { Start-ThreadJob { Get-CimInstance Win32_NetworkAdapterConfiguration -ComputerName $using:ServerName } }
}
# Constructor
Server([string] $ServerName) {
$this.ServerName = $ServerName
# Asynchronously start the thread jobs that populate the "property"
# values, i.e. the entries of the $this._jobsMap hashtable.
foreach ($key in #($this._jobsMap.Keys)) {
# Replace each predefined script block with the result of its
# *invocation*, i.e. with an object describing each launched thread job.
$this._jobsMap[$key] = & $this._jobsMap[$key]
}
}
# Methods that act like property accessors.
[object] ComputerSystem() {
return $this.get('ComputerSystem')
}
[object] OperatingSystem() {
return $this.get('OperatingSystem')
}
[object] Bios() {
return $this.get('Bios')
}
[object] NetworkAdapter() {
return $this.get('NetworkAdapter')
}
# Hidden helper method that returns the value of interest,
# making sure that a value has been received from the relevant
# thread job first.
hidden [object] get($propName) {
if ($this._jobsMap[$propName] -is [System.Management.Automation.Job]) {
# Note that any error-stream output from the jobs
# is *not* automatically passed through; -ErrorVariable is used
# to collect any error(s), which are translated into a script-terminating
# error with `throw`
$e = $null
$this._jobsMap[$propName] = $this._jobsMap[$propName] |
Receive-Job -Wait -AutoRemoveJob -ErrorVariable e
if ($e) {
throw $e[0]
}
}
return $this._jobsMap[$propName]
}
# Method that indicates whether *all* "properties" have finished
# initializing.
[bool] IsInitialized() {
$pendingJobs = $this._jobsMap.Values.Where({ $_ -is [System.Management.Automation.Job] })
return $pendingJobs.Count -eq 0 -or $pendingJobs.Where({ $_.State -in 'NotStarted', 'Running' }, 'First').Count -eq 0
}
}
Example use:
# Instantiate [Server], which asynchronously starts the thread jobs
# that will populate the properties.
$server = [Server]::new('.')
# Access a "property" by way of its helper method, which
# waits for the job to complete first, if necessary.
$server.Bios()
# Test if all property values have been retrieved yet.
$server.IsInitialized()

While Loop with Break Statement in PowerShell [duplicate]

I am trying to build my own script to check some Windows services (status and start mode) and I am facing an issue on the IF ...
For example even if the service is "Running", it will never run the code inside the IF...
let me share my code below (I am a newbie on powershell so be gentle xD)
For info, I will do more actions inside the IF and ELSE, it is just for the example.
# import computers list, 1 by line
$Computers = get-content .\computers.txt
# define variable of services we want to check
$ServiceNetbios = "netbt"
# define variable to ask credentials
$Cred = Get-Credential
# declare Function to open a session a remote computer
Function EndPSS { Get-PSSession | Remove-PSSession }
EndPSS
########################################################
# BEGINNING OF SCRIPT #
# by xxx #
# 2022-02-03 #
########################################################
# loop for each computer imported from the file
foreach ($computer in $computers) {
# show name of computer in progress
$computer
# connect remotely to the computer
$session = New-PSSession -ComputerName $computer -Credential $Cred
# check Netbios service
$StatusServiceNetbios = Invoke-Command -Session $session -ScriptBlock { Get-Service -Name $Using:ServiceNetbios | select -property * }
# Check Netbios service started or not
write-host $StatusServiceNetbios.Status
if ($StatusServiceNetbios.Status -eq 'Running')
{
Write-host "IF Running"
}
else
{
write-host "IF NOT Running"
}
EndPSS
}
and what return my script :
computername
Running (<= the variable $StatusServiceNetbios.Status )
IF NOT Running (<= the ELSE action)
Thanks you in advance for your help,
this drive me crazy and maybe this is very simple...
To complement Cpt.Whale's helpful answer, this is likely to be caused by the serialization and deserialization done by Invoke-Command:
using namespace System.Management.Automation
$service = Get-Service netbt
$afterInvokeCmd = [PSSerializer]::Deserialize(([PSSerializer]::Serialize($service)))
$service.Status -eq 'Running' # => True
$afterInvokeCmd.Status -eq 'Running' # => False
$afterInvokeCmd.Status.Value -eq 'Running' # => True
$afterInvokeCmd.Status.ToString() -eq 'Running' # => True
To put some context to my answer, this is a nice quote from about_Remote_Output that can better explain why and what is happening:
Because most live Microsoft .NET Framework objects (such as the objects that PowerShell cmdlets return) cannot be transmitted over the network, the live objects are "serialized". In other words, the live objects are converted into XML representations of the object and its properties. Then, the XML-based serialized object is transmitted across the network.
On the local computer, PowerShell receives the XML-based serialized object and "deserializes" it by converting the XML-based object into a standard .NET Framework object.
However, the deserialized object is not a live object. It is a snapshot of the object at the time that it was serialized, and it includes properties but no methods.
This is probably because of the way powershell creates service objects - (Get-Service netbt).Status has a child property named Value:
$StatusServiceNetbios.Status
Value
-----
Running
# so Status is never -eq to 'Running':
$StatusServiceNetbios.Status -eq 'Running'
False
# use the Value property in your If statement instead:
$StatusServiceNetbios.Status.Value -eq 'Running'
True

Missing AD module and can't get it, need something similar or something to simulate it

So I'm trying to output a complete KB list for all computers on a server (which works on one computer) but it doesn't recognize Get-ADcomputer as a cmdlet. When checking various sources, it appears that the AD module isn't included. As I'm doing this on a work computer/server I'm hesitant to download anything or anything of that nature.
Is there any way I can achieve the following without using the AD module or someway I might be missing how to import the module (if it exists, which I don't think it does on this system)?
# 1. Define credentials
$cred = Get-Credential
# 2. Define a scriptblock
$sb = {
$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$HistoryCount = $Searcher.GetTotalHistoryCount()
$Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {
$Title = $null
if ($_.Title -match "\(KB\d{6,7}\)") {
# Split returns an array of strings
$Title = ($_.Title -split '.*\((?<KB>KB\d{6,7})\)')[1]
} else {
$Title = $_.Title
}
$Result = $null
switch ($_.ResultCode) {
0 { $Result = 'NotStarted'}
1 { $Result = 'InProgress' }
2 { $Result = 'Succeeded' }
3 { $Result = 'SucceededWithErrors' }
4 { $Result = 'Failed' }
5 { $Result = 'Aborted' }
default { $Result = $_ }
}
New-Object -TypeName PSObject -Property #{
InstalledOn = Get-Date -Date $_.Date;
Title = $Title;
Name = $_.Title;
Status = $Result
}
} | Sort-Object -Descending:$false -Property InstalledOn | Where {
$_.Title -notmatch "^Definition\sUpdate"
}
}
#Get all servers in your AD (if less than 10000)
Get-ADComputer -ResultPageSize 10000 -SearchScope Subtree -Filter {
(OperatingSystem -like "Windows*Server*")
} | ForEach-Object {
# Get the computername from the AD object
$computer = $_.Name
# Create a hash table for splatting
$HT = #{
ComputerName = $computer ;
ScriptBlock = $sb ;
Credential = $cred;
ErrorAction = "Stop";
}
# Execute the code on remote computers
try {
Invoke-Command #HT
} catch {
Write-Warning -Message "Failed to execute on $computer because $($_.Exception.Message)"
}
} | Format-Table PSComputerName,Title,Status,InstalledOn,Name -AutoSize
You've got 3 options:
First is to just install the RSAT feature for AD which will include the AD module. This is probably the best option unless there is something specific preventing it. If you're running your script from a client operating systems you need to install the RSAT first, though.
Option 2 (which should only be used if adding the Windows feature is somehow an issue) is to download and use the Quest AD tools, which give very similar functionality, but it looks like Dell is doing their best to hide these now so that may be difficult to locate...
Option 3 is to use the .NET ADSI classes to access AD directly, which will work without any additional downloads on any system capable of running PowerShell. If you'd like to go this route you should check out the documentation for the interface Here and for the System.DirectoryServices namespace Here.
Edit
Just noticed the last part of your question, what do you mean by "a complete KB list"? Not just Windows updates or things updated manually or whatever? What else would be in a list of Windows updates that was not a Windows update?
You have not mentioned the OSes you are using but in general if you have a server 2008 R2 or above, all you have to do it activate the RSAT feature AD PowerShell Module and you will have the cmdlet you are looking for.
On a client machine, you 'have to' install RSAT, and then activate the features. You can take a look at the technet article for more info: https://technet.microsoft.com/en-us/library/ee449483(v=ws.10).aspx
If you don't want to use that option, then you will have to use .NET ADSI classes. There are tons of examples on how to do this, it basically boils down to a couple of lines really. Technet has examples on this as well: https://technet.microsoft.com/en-us/library/ff730967.aspx

Powershell Encoding passing value to WmiMethod

so, I have been scripting some tasks for HP Bios's. There is a Wmi Namespace called HPBIOS_BIOSSettingInterface. It essentially has one methond SetBIOSSetting("Property","Value","CurrentSetupPassword"). The older HP systems only support KBD Encoding, which I wrote a function to encode a string with KDB values, can be found here: http://thomas-malkewitz.webnode.com/news/convert-tokbdstring/ and when setting the setup password value with this encoding, it works, but with new HPs, it only supports UTF-16 (I cannot find if its Big or Little, but have tried both). I keep getting errors when doing this. So, my question, is how can I encoded a string value and pass it to this method. I cannot figure this out for the life of me. here is my function:
<#
.Synopsis
Sets the Setup Password on an HP Bios.
.DESCRIPTION
This function can be used to set a password on the Bios, it can also be used to clear the password, the current password is needed to change the value.
If a new value is being set, and not cleared, it must be between 8 and 30 characters.
.EXAMPLE
Set-HpSetupPassword -NewSetupPassword "MyNewPassword"
.EXAMPLE
Set-HpSetupPassword -ComputerName "mycomputer.mydomain.org" -NewSetupPassword " " -CurrentSetupPassword "MyCurrentPassword"
.EXAMPLE
Set-HpSetupPassword -NewSetupPassword "MyNewSetupPassword" -CurrentSetupPassword "MyCurrentPassword"
.LINKS
https://github.com/necromorph1024/HPTpmAndBitLocker
#>
function Set-HpSetupPassword
{
[CmdletBinding()]
[OutputType([void])]
Param
(
# ComputerName, Type string, System to set Bios Setup Password.
[Parameter(Mandatory=$false,
Position=0)]
[string]
$ComputerName=$env:COMPUTERNAME,
# NewSetupPassword, Type string, The value of the password to be set. The password can be cleared by using a space surrounded by double quotes, IE: " ".
[Parameter(Mandatory=$true,
Position=1)]
[string]
$NewSetupPassword,
# CurrentSetupPassword, Type string, The value of the current setup password.
[Parameter(Mandatory=$false,
Position=2)]
[AllowEmptyString()]
[string]
$CurrentSetupPassword
)
Begin
{
if (!(Test-Connection -ComputerName $ComputerName -Quiet -Count 2))
{
throw "Unable to connect to $ComputerName. Please ensure the system is available."
}
try
{
$manufacturer = Get-WmiObject -Class Win32_ComputerSystem -Namespace "root\CIMV2" -Property "Manufacturer" -ComputerName $ComputerName -ErrorAction Stop
if ($manufacturer.Manufacturer -ne "Hewlett-Packard")
{
throw "Computer Manufacturer is not of type Hewlett-Packard. This cmdlet can only be used on Hewlett-Packard systems."
}
}
catch
{
throw "Unable to connect to the Win32_ComputerSystem WMI Namespace, verify the system is avaialbe and you have the permissions to access the namespace."
}
}
Process
{
if (-not([String]::IsNullOrWhiteSpace($NewSetupPassword)))
{
if (($NewSetupPassword.Length -lt 8) -or ($NewSetupPassword.Length -gt 30))
{
throw "The Password Values must be be between 8 and 30 characters if not clearing the password."
}
}
$hpBios = Get-WmiObject -Class HP_BiosSetting -Namespace "root\HP\InstrumentedBIOS" -ComputerName $ComputerName -ErrorAction Stop
$hpBiosSettings = Get-WmiObject -Class HPBIOS_BIOSSettingInterface -Namespace "root\HP\InstrumentedBIOS" -ComputerName $ComputerName -ErrorAction stop
if (($hpBios | Where-Object { $_.Name -eq "Setup Password"}).SupportedEncoding -eq "kbd")
{
if (-not([String]::IsNullOrEmpty($NewSetupPassword)))
{
$NewSetupPassword="<kbd/>"+(Convert-ToKbdString -UTF16String $NewSetupPassword)
}
if (-not([String]::IsNullOrEmpty($CurrentSetupPassword)))
{
$CurrentSetupPassword="<kbd/>"+(Convert-ToKbdString -UTF16String $CurrentSetupPassword)
}
}
$hpBiosSettings.SetBIOSSetting("Setup Password",$NewSetupPassword,$CurrentSetupPassword)
}
}
It keeps return 6 which is access denied, which is what I was getting with the older bios's until I created that Convert-KbdString method. I know the password im using is right. But I don't know what encoding is being sent to the WmiMethod. Thanks for any help
here is the GitHub Repo: https://github.com/necromorph1024/HpTpmAndBitLocker
.NET Strings are in the Unicode encoding, according to the documentation for the String class.
http://msdn.microsoft.com/en-us/library/system.string(v=vs.110).aspx
I feel so stupid for even asking this question now. I reviewed my own method, and the string needs to be tagged with the encoding type. and i KNEW this, thats why i append the <kbd/> to the string if it supports that encoding type. So, the <utf-16/> tag just needed to be appending to the start of the string. OMG, sorry for any time i may have wasted!
how would you get this to run with a Foreach($computer in $computername) loop? Where would the foureach be placed and would there be any special syntax?

Checking several Hyper-V Hosts for a specific VM in Powershell

I am writing a script to administer Hyper-VMs using the PowerShell Management Library for Hyper-V.
Since we are using several Hyper-V Hosts and our VMs can change their host for performance reasons or other reasons I need a script that finds out which Host a VM runs on for the following functions.
This was my try at accomplishing this:
function IdentifyHost
{
param
(
[parameter(Position=0, Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$VM
)
[Array]$hosts=Get-VMHost
if ($hosts.count -eq 0)
{
Write-Warning "No valid hosts found."
}
for ([int]$i=0; $i -lt $hosts.count; $i++ )
{
try
{
$out = Get-VM -Name $VM -Server $hosts[$i] -ErrorAction Stop
}
catch [UnauthorizedAccessException]
{
Write-Warning "Access to $hosts[$i] denied."
}
if ($VM -is [String])
{
if ($out.VMElementName -eq $VM )
{
return $out.__SERVER
}
}
elseif ($VM.ElementName -ne $null)
{
if ($out.VMElementName -eq $VM.VMElementName)
{
return $out.__SERVER
}
}
}
Write-Warning "No Host found for $VM"
}
Get-VMHost returns an array of all available Hyper-V hosts in the local area network.
My problem is that my function always returns the first element of the $hosts array whenever there is an UnauthorizedAccessException for the first element.
The plan is as following:
If the VM exists on the Host he will return a WMI Object representing that VM whose VMElementName property is equal to the VMs name given as parameter.
If the VM is given a WMI Object representing a VM the VMElementName properties of the two objects are equal.
If the VM does not exist on the Host he returns nothing.
If there's an access issue it should be catched.
But somehow it doesn't work out.
My question is this: What am I doing wrong in the code? And how can I fix it?
EDIT: The output of the function is the access problem warning for the first element of the $hosts array and then the first element of $hosts itself.
EDIT2: I fixed this myself by changing the return from the fragile $hosts[$i] to $out.__Server
Okay so I found a possible way of solving this issue:
Instead of returning the $hosts[$i] which yields unfavorable results I return the __Server property of $out, assuming there is a valid $out that matches the conditions.
If any of you guys knows a better or cleaner way of doing this, please by my guest.