Powershell Encoding passing value to WmiMethod - powershell

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?

Related

ShouldProcess failing in PowerShell7

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

Get-Counter not retrieving valid details

I am trying to retrieve I/O details from the task manager using PowerShell. Below is the script that I am trying to use as of now and have had partial success with the same.
$gc = get-counter -ComputerName $($service.ServerName) "\Process($Tservicename)\IO Other Bytes/sec" -ErrorAction SilentlyContinue
$OtherBytes=$gc.CounterSamples|Select cookedvalue
Here $serviceName & ServerName is looped through. Below are the issues that I am facing.
I am unable to retrieve IO Other bytes details for all the service, I am running the script in admin mode so the access shouldn't be an issue.
Will the above script give the cumulative result in case there is more than process is executing, for eg for the chrome.exe there would be multiple services running, will it provide an cumulative value. If not how I extract details for each process of the chrome.
-- Updated Question--
We are using Get-Counter cmdlet to retrieve the IO read and write bytes details. This returns 0 for most of the process, is this due to being unable to access system process or due to the access issue.
-- Answer--
After researching a bit realized that value retrieved by the cmdlet is based on that particular instance, which is why we need to sampling of the data by using the SampleInterval. However my requirement was sufficed by using the RawValue
parameter since I was looking for the value post the server startup as a cumulative value.
List item
Two important things to understand here:
There's not a 1-to-1 relationship between service names and process names
Performance counter object instance names don't use process ID's (by default at least)
That means you need to do two levels of instance translation to make sense of the counters:
Translate Service to Process ID - we can use Win32_Service WMI class for this
Translate Process ID to Process counter instances - we can use the Process(*)\ID Process counter value for this
function Get-ServiceCounter
{
param(
[Parameter(Mandatory = $true, Position = 0)]
[string]
$ServiceName,
[Parameter(Position = 1)]
[string]
$ValueName
[string]
$Computername = '.'
)
$ID = Get-CimInstance Win32_Service -Filter "Name = '${ServiceName}'" -ComputerName $ComputerName -ErrorAction SilentlyContinue |Select -Expand ProcessID
if(-not $ID){
Write-Error "Could not resolve process for service '${ServiceName}'"
return
}
$Instance = Get-Counter -ComputerName $ComputerName "\Process(*)\ID Process" |Where-Object CookedValue -eq $ID -ErrorAction SilentlyContinue
if(-not $Instance){
Write-Error "Could not performance counter instance for Process ID ${ID}"
return
}
Get-Counter ($InstanceName -replace '\\id process',"\${ValueName}") -ComputerName $ComputerName -ErrorAction SilentlyContinue
}

PowerShell booleans -- How to handle null differently from false?

I'm trying to write a server build script that includes Boolean parameters for various tasks, e.g. whether to install IIS. If the user does not specify this parameter one way or the other, I want the script to prompt for a decision, but for convenience and unattended execution I want the user to be able to explicitly choose to install IIS or NOT install IIS by setting the value to True or False on the command line and therefore avoid being prompted. My issue is that when I create a Boolean parameter, PowerShell automatically sets it to False, rather than leaving it null, if it wasn't specified on the command line. Here is the design that I THOUGHT would've worked:
param(
[bool]$IIS
)
if ($IIS -eq $null) {
$InstallIIS = Read-Host "Do you want to install IIS? (Y/N)"
if ($InstallIIS -eq "Y") {$IIS = $true}
}
if ($IIS) {Do stuff here}
Any suggestions for how to achieve my desired outcome would be most appreciated. Then if this changes anything, what I'd REALLY like to do is leverage PSRemoting to accept these build decision parameters on the user's system host and then pass them to the targets as an ArgumentList, and I'm wondering if that will affect how these Booleans are handled. For example:
param (
[string[]]$Computers
[bool]$IIS
)
$Computers | Foreach-Object {
Invoke-Command -ComputerName $_ -ArgumentList $IIS -ScriptBlock {
param(
[bool]$IIS
)
if ($IIS -eq $null) {
$InstallIIS = Read-Host "Do you want to install IIS? (Y/N)"
if ($InstallIIS -eq "Y") {$IIS = $true}
}
if ($IIS) {Do stuff here}
Ideas?
The way to accomplish this is with Parameter Sets:
[CmdletBinding()]
param (
[Parameter()]
[string[]]$Computers ,
[Parameter(ParameterSetName = 'DoSomethingWithIIS', Mandatory = $true)]
[bool]$IIS
)
$Computers | Foreach-Object {
Invoke-Command -ArgumentList $IIS -ScriptBlock {
param(
[bool]$IIS
)
if ($PSCmdlet.ParameterSetName -ne 'DoSomethingWithIIS') {
$InstallIIS = Read-Host "Do you want to install IIS? (Y/N)"
if ($InstallIIS -eq "Y") {$IIS = $true}
}
if ($IIS) {Do stuff here}
Well of course even though I Googled about this quite a bit before posting here, including discovering the [AllowNull()] parameter and finding that it did NOT help in my use case, I ended up finding the answer in the first Google search AFTER posting. This is what worked:
[nullable[bool]]$IIS
My only gripe with that syntax is that running Get-Help against the script now returns shows this for the IIS parameter:
-IIS <Nullable`1>
instead of:
-IIS <Boolean>
But unless there's a more elegant way to achieve what I need, I think I can live with that by adding a useful description for that parameter as well as Examples.
Even though boolean operators handle $null, $False, '', "", and 0 the same, you can do an equality comparison to see which is which.
If ($Value -eq $Null) {}
ElseIf ($Value -eq $False) {}
..etc..
In your situation, you want to use [Switch]$IIS. This will be $False by default, or $True if entered with the command a la Command -IIS, then you can handle it in your code like:
If ($IIS) {}
Which will only be $True if entered at the command line with -IIS
Instead of using an equality test that's going to try to coerce the value to make the test work:
if ($IIS -eq $Null)
Use -is to check the type directly:
PS C:\> $iis = $null
PS C:\> $iis -is [bool]
False

What's the fastest way to get online computers

I'm writing a function which returns all Online Computers in our network, so I can do stuff like this:
Get-OnlineComputers | % { get-process -computername $_ }
Now I basically got my function ready, but it's taking way too long.
I want to only return Computers which have WinRM active, but I also want to provide the option to get every computer even those which haven't got WinRM set up (switch parameter).
This is my function. first it creates a pssession to the domaincontroller, to get all computers in our LAN. then foreach computer, it will test if they have WinRM active or if they accept ping. if so, it gets returned.
$session = New-PSSession Domaincontroller
$computers = Invoke-Command -Session $session { Get-ADComputer -filter * } | select -ExpandProperty Name
$computers | % {
if ($IncludeNoWinRM.IsPresent)
{
$ErrorActionPreference = "SilentlyContinue"
$ping = Test-NetConnection $_
if ($ping.PingSucceeded -eq 'True')
{
$_
}
}
else
{
$ErrorActionPreference = "SilentlyContinue"
$WinRM = Test-WSMan $_
if ($WinRM)
{
$_
}
}
}
Is this the best way I can go to check my online computers? Does anyone have a faster and better idea?
Thanks!
Very Quick Solution is using the -Quiet Parameter of the Test-Connection cmdlet:
so for example:
$ping = Test-Connection "Computer" -Quiet -Count 1
if ($ping)
{
"Online"
}
else
{
"Offline"
}
if it's not enough fast for you, you can use the Send Method of the System.Net.NetworkInformation.Ping
here's a sample function:
Function Test-Ping
{
Param($computer = "127.0.0.1")
$ping = new-object System.Net.NetworkInformation.Ping
Try
{
[void]$ping.send($computer,1)
$Online = $true
}
Catch
{
$Online = $False
}
Return $Online
}
Regarding execute it on multiple computers, I suggest using RunSpaces, as it's the fastest Multithreading you can get with PowerShell,
For more information see:
Runspaces vs Jobs
Basic Runspaces implemenation
Boe Prox (master of runspaces) has written a function which is available from the Powershell Gallery. I've linked the script below.
He uses many of the answers already given to achieve the simultaneous examination of 100s of computers by name. The script gets WMI network information if test-connection succeeds. It should be fairly easy to adapt to get any other information you want, or just return the result of the test-connection.
The script actually uses runspace pools rather than straight runspaces to limit the amount of simultaneous threads that your loop can spawn.
Boe also wrote the PoSH-RSJob module already referenced. This script will achieve what you want in native PoSH without having to install his module.
https://gallery.technet.microsoft.com/scriptcenter/Speedy-Network-Information-5b1406fb

Need to check for the existence of an account if true skip if false create account

I am trying to create local user on all servers and I want to schedule this as a scheduled task so that it can run continually capturing all new servers that are created.
I want to be able to check for the existence of an account and if true, skip; if false, create account.
I have imported a module called getlocalAccount.psm1 which allows me to return all local accounts on the server and another function called Add-LocaluserAccount
which allows me to add local accounts these work with no problems
when I try and run the script I have created the script runs but does not add accounts
Import-Module "H:\powershell scripts\GetLocalAccount.psm1"
Function Add-LocalUserAccount{
[CmdletBinding()]
param (
[parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string[]]$ComputerName=$env:computername,
[parameter(Mandatory=$true)]
[string]$UserName,
[parameter(Mandatory=$true)]
[string]$Password,
[switch]$PasswordNeverExpires,
[string]$Description
)
foreach ($comp in $ComputerName){
[ADSI]$server="WinNT://$comp"
$user=$server.Create("User",$UserName)
$user.SetPassword($Password)
if ($Description){
$user.Put("Description",$Description)
}
if ($PasswordNeverExpires){
$flag=$User.UserFlags.value -bor 0x10000
$user.put("userflags",$flag)
}
$user.SetInfo()
}
}
$usr = "icec"
$rand = New-Object System.Random
$computers = "ServerA.","ServerB","Serverc","ServerD","ServerE"
Foreach ($Comp in $Computers){
if (Test-Connection -CN $comp -Count 1 -BufferSize 16 -Quiet){
$admin = $usr + [char]$rand.next(97,122) + [char]$rand.next(97,122) + [char]$rand.next(97,122) + [char]$rand.next(97,122)
Get-OSCLocalAccount -ComputerName $comp | select-Object {$_.name -like "icec*"}
if ($_.name -eq $false) {
Add-LocalUserAccount -ComputerName $comp -username $admin -Password "password" -PasswordNeverExpires
}
Write-Output "$comp online $admin"
} Else {
Write-Output "$comp Offline"
}
}
Why bother checking? You can't create an account that already exists; you will receive an error. And with the ubiquitous -ErrorAction parameter, you can determine how that ought to be dealt with, such as having the script Continue. Going beyond that, you can use a try-catch block to gracefully handle those exceptions and provide better output/logging options.
Regarding your specific script, please provide the actual error you receive. If it returns no error but performs no action check the following:
Event Logs on the target computer
Results of -Verbose or -Debug output from the cmdlets you employ in your script
ProcMon or so to see what system calls, if any, happen.
On a sidenote, please do not tag your post with v2 and v3. If you need a v2 compatible answer, then tag it with v2. Piling on all the tags with the word "powershell" in them will not get the question answered faster or more effectively.
You can do a quick check for a local account like so:
Get-WmiObject Win32_UserAccount -Filter "LocalAccount='true' and Name='Administrator'"
If they already exist, you can either output an error (Write-Error "User $UserName Already exists"), write a warning (Write-Warning "User $UserName Already exists"), or simply silently skip the option.
Please don't use -ErrorAction SilentlyContinue. Ever. It will hide future bugs and frustrate you when you go looking for them.
This can very easily be done in one line:
Get-LocalUser 'username'
Therefore, to do it as an if statement:
if((Get-LocalUser 'username').Enabled) { # do something }
If you're not sure what the local users are, you can list all of them:
Get-LocalUser *
If the user is not in that list, then the user is not a local user and you probably need to look somewhere else (e.g. Local Groups / AD Users / AD Groups
There are similar commands for looking those up, but I will not outline them here