Can Ansible's win_powershell thoroughly process a boolean - powershell

win_powershell seems to choke when I attempt to pass a Boolean variable through it to register a tested discrete state on a remote guest/target (for later consumption). For numerous attempts to provide compliant statements, I get errors such as ...
was not followed by a valid variable name character. Consider using ${} to delimit the name.
Last close parenthesis is an invalid character
Cannot convert string to bool
Colon is unexpected token
$True is unexpected token
Unable to convert to dictionary
Must either be a JSON string or in the key=value form
My goal was to use the win_powershell module to execute a powershell script ...
$IsInstalled = ((gp HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Match "Microsoft SQL Server").Length -gt 0
... on a remote host and consume the IsInstalled variable later in the Playbook. (the p/s statement executes flawlessy when invoked directly).
My task ended up looking like the below with all of the various/unsuccessful attempts commented out. Notice that I resorted to the simplest of tests in the end which infers that processing a boolean via win_powershell is not do-able. I'm hoping somebody can prove me wrong and/or confirm that I need to get such resolve by processing a string variable instead.
BTW (for the innocent by-stander) there are no hits for the word bool in the Ansible documentation for win_powershell. However, the description for parameters includes the word dictionary and term key=value pairs. If these descriptions are restraints, it should be (I wish it were) stated as such (more emphatic).
Key points observed along the way, …
An = operator always produces a string; use json syntax for other variable types; a : (colon) for other types such as boolean.
Consider using ${} to delimit the variable name (but how?)
Enclose any boolean equates with single ‘
I found some other blogs/posts but nothing in layman (json newbie) terms (that I could understand).
Anybody have a solution to make a Boolean work as sought or should I (just) try to process the needed state as a string variable? Check out these blogs …
powershell - Variable reference is not valid. ':' was not followed by a valid variable name character
How can you use an object's property in a double-quoted string?
Here's my task efforts (with failed statements being commented) ...
- name: Test for installed SQL Server
ansible.windows.win_powershell:
script: |
[CmdletBinding()]
param (
[bool]$IsInstalled
)
# $IsInstalled = ((gp HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Match "Microsoft SQL Server").Length -gt 0
# $IsInstalled: ((gp HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Match "Microsoft SQL Server").Length -gt 0
# $IsInstalled: ${ ((gp HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Match "Microsoft SQL Server").Length -gt 0 }
# ${IsInstalled}: ((gp HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Match "Microsoft SQL Server").Length -gt 0
# if ( ((gp HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Match "Microsoft SQL Server").Length -gt 0 ) # last close paren is invalid character
# if ( '((gp HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Match "Microsoft SQL Server").Length -gt 0' ) # cannot convert string to bool
if ( $False ) # a sanity check ...
# { $IsInstalled: $True }
# { ${ $IsInstalled}: $True }
# { ${IsInstalled}: $True } # colon is unexpected token
# { ${IsInstalled} = $True } # equals outputs a string; cannot convert to boolean (i knew that but what the hey)
# { ${ $IsInstalled: } $True } # unexpected token $True
{ '$IsInstalled: $True' }
else
# { $IsInstalled: $False }
# { ${ $IsInstalled}: $False }
# { ${IsInstalled}: $False }
# { ${IsInstalled} = $False }
# { ${ $IsInstalled: } $False }
{ '$IsInstalled: $False' }
parameters:
# IsInstalled: $True # cannot convert string to bool
# 'IsInstalled: $True' # is of type System.String ... unable to convert to dict ... must either be a JSON string or in the key=value form"
}
register: SQLtest
I was expecting the first (commented) statement to work; done. That is, to register SQLtest.IsInstalled as a boolean state for later consumption.

Here's an exact solution that did work; i couldn't get the templating to work, ...
- name: Test for installed SQL Server
ansible.windows.win_powershell:
script: |
((gp HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*).DisplayName -Match "Microsoft SQL Server 2019 ").Length -gt 0
register: SQLtest
... as consumed later in a when conditional ...
when: not SQLtest.output
It's always simple (once you know how) ...

Related

Replace PSObj property value based on a list

I've the following PSObj with some properties stored in an $array :
ComputerName : MyComputer
Time : 08/11/2022 13:57:53
DetectionFile : MyBadFile.exe
ThreatName : WS.Reputation.1
Action : 12
I'm trying to replace the action ID number by it's corresponding description. I've a hashtable with the possibles reasons behind the Action ID
$ActionId = #{
0 = 'Unknown'
1 = 'Blocked'
2 = 'Allowed'
3 = 'No Action'
4 = 'Logged'
5 = 'Command Script Run'
6 = 'Corrected'
7 = 'Partially Corrected'
8 = 'Uncorrected'
10 = 'Delayed Requires reboot to finish the operation.'
11 = 'Deleted'
12 = 'Quarantined'
13 = 'Restored'
14 = 'Detected'
15 = 'Exonerated No longer suspicious (re-scored).'
16 = 'Tagged Marked with extended attributes.'
}
I'm trying to parse each item of this array, and each value of the reason ID to replace the ID by the reason string
# parse array
foreach ($Item in $array) {
# parse possible values
foreach ($value in $ActionId) {
if ($value -eq $item.Action) {
$Item.Action = $ActionId[$value]
$Item.Action
}
}
From my understanding, I'm missing the correct syntax here
$Item.Action = $ActionId[$value]
I do not get any errors, but from the debugger, I'm replacing the action property by $null with the above...
The immediate fix is to loop over the keys (.Keys) of your $ActionId hashtable:
foreach ($Item in $array) {
# parse possible values
foreach ($value in $ActionId.Keys) {
if ($value -eq $item.Action) {
$Item.Action = $ActionId[$value]
$Item.Action # diagnostic output
}
}
}
Note:
To avoid confusion, consider renaming $value to $key.
Generally, note that hashtables are not enumerated in the pipeline / in looping constructs in PowerShell.
That is, foreach ($value in $ActionId) ... doesn't actually loop over the hashtable's entries, and is the same as $value = $ActionID)
If you want to enumerate a hashtable's entries - as key-value pairs of type System.RuntimeType - you would need to use the .GetEnumerator() method; in your case, however, enumerating the keys is sufficient.
However, the simpler and more efficient solution is to test whether the $Item.Action value exists as a key in your hashtable, using the latter's .Contains() method:[1]
foreach ($Item in $array) {
if ($ActionId.Contains($Item.Action)) {
$Item.Action = $ActionId[$Item.Action]
$Item.Action # diagnostic output
}
}
You can further streamline this as follows, though it is conceptually a bit obscure:
foreach ($Item in $array) {
if ($null -ne ($value = $ActionId[$Item.Action])) {
$Item.Action = $value
$Item.Action # diagnostic output
}
}
= is only ever PowerShell's assignment operator; for equality / non-equality comparison, -eq / -ne is required.
Here, an assignment to $value is indeed being performed and the assigned value then acts as the RHS of the -ne operation; in other words: you can use assignment as expressions in PowerShell.
If hashtable $ActionId has no key with value $Item.Action, $ActionId[$Item.Action] quietly returns $null.
Finally - in PowerShell (Core) 7+ only - an even more concise (though not necessarily faster) solution is possible, using ??, the null-coalescing operator:
foreach ($Item in $array) {
$Item.Action = $ActionId[$Item.Action] ?? $Item.Action
$Item.Action # diagnostic output
}
That is, the value of $ActionId[$Item.Action] is only used if it isn't $null; otherwise, $Item.Action, i.e. the current value, is used (which is effectively a no-op).
[1] .ContainsKey() works too, and while this name is conceptually clearer than .Contains(), it is unfortunately not supported by PowerShell's [ordered] hashtables (System.Collections.Specialized.OrderedDictionary) and, generally speaking, not supported by other dictionary (hashtable-like types), given that the System.Collections.IDictionary interface only has .Contains()
In addition mklement0's helpful answer, I was just thinking outside the box (aka question):
This is typical situation where I would consider to use an enum except for the fact that that the keys do not (easily) accept spaces (as in your question).
Enum ActionTypes {
Unknown
Blocked
Allowed
NoAction
Logged
CommandScriptRun
Corrected
PartiallyCorrected
Uncorrected
Delayed
Deleted
Quarantined
Restored
Detected
Exonerated
Tagged
}
$PSObj = [PSCustomObject]#{
ComputerName = 'MyComputer'
Time = [DateTime]'08/11/2022 13:57:53'
DetectionFile = 'MyBadFile.exe'
ThreatName = 'WS.Reputation.1'
Action = 12
}
$PSObj.Action = [ActionTypes]$PSObj.Action
$PSObj
ComputerName : MyComputer
Time : 8/11/2022 1:57:53 PM
DetectionFile : MyBadFile.exe
ThreatName : WS.Reputation.1
Action : Restored
The advantage is that you won't lose the actual action id, meaning if you e.g. insert the object back into a database, it will automatically type cast to the original integer type:
$PSObj.Action
Restored
[int]$PSObj.Action
12

Powershell Understanding "continue"

So in the spirit of this short but sweet tutorial I'm trying to filter out disabled user and only work on the "Enabled" users with this code. (FYI Search-ADAccount needs elevated)
$EXPusers = (Search-ADAccount -AccountExpired -UsersOnly)
foreach($user in $EXPusers){
$UENB = $user.Enabled
$UENB # Sanity Check
if($UENB -eq "False"){
continue
}
# All of this is functioning
# disable user
# Logoff user
# Send email
}
In my lab $EXPusers just resolves to one user that is disabled or Enabled=False. So what happens is no matter what I set $UENB equal to it keeps sending mail. Seems to me that if it's "False" it should skip that iteration and not process the rest of the foreach statement and move to the next user, in this case do nothing.
What am I missing?
The reason why it's failing is because you're comparing a boolean (the Enabled Property of an ADAccount instance is a bool) with a string. It's important to note that, in PowerShell a string that is not empty will always be $true and, since in your comparison the string is in the right hand side (RHS) of the comparison, PowerShell attempts type coercion as the same type of the left hand side (LHS), so the string 'false' is converted to a boolean during the comparison which results being $true.
In about Comparison Operators documentation on the Equality operators section we can read the following:
The equality operator can compare objects of different types. It is important to understand that the value is on the right-hand side of the comparison can be converted to the type of the left-hand side value for comparison.
A simple demo:
[bool] 'false' # => $true
$false -eq 'false' # => $false
'false' -eq $false # => $true
The last comparison results in $true because the boolean $false in the RHS is converted to string and, in PowerShell, [string] $false results in the literal string false.
In conclusion, by simply changing your if condition your code would work properly:
if($false -eq $UENB) {
continue
}
The other alternative would be to use the logical -not operator:
if(-not $UENB) {
continue
}

PowerShell If statement not equating properly

What am I doing wrong here?
Why do the 2 variables not equal each other?
When I run this script
$temp1 = "#{Dhcp=Disabled}"
$temp2 = Get-NetIPInterface My_Ethernet | select Dhcp
write-host ""
write-host "1" $temp1
write-host "2" $temp2
write-host ""
if ($temp2 -eq $temp1){
write-host "IP address is Static "
}
Else {
write-host "IP address is Not Static"
}
I get this result
1 #{Dhcp=Disabled}
2 #{Dhcp=Disabled}
IP address is Not Static
With the helpful suggestion from Mathias this is now working as expected
$temp1 = "Disabled"
$temp2 = Get-NetIPInterface My_Ethernet | select Dhcp
write-host ""
write-host ""
write-host "1" $temp1
write-host "2" $temp2.dhcp
write-host ""
write-host ""
if ($temp2.dhcp -eq $temp1){
write-host "IP address is Static "
}
Else {
write-host "IP address is Not Static"
}
Just to complement your own effective solution:
Since your intent was to compare a property's value to another value, select -ExpandProperty Dhcp would have returned that value directly (see the docs for Select-Object):
if ((Get-NetIPInterface My_Ethernet | select -ExpandProperty Dhcp) -eq $temp1) { # ...
However, it would be much simpler to use direct property access, using ., the member-access operator:
if ((Get-NetIPInterface My_Ethernet).Dhcp -eq $temp1) { # ...
Note that .Dhcp would work even if your Get-NetIPInterface call returned multiple objects, in which case an array of values is returned, courtesy of PowerShell's member-access enumeration feature.[1]
Finally, note that Write-Host is typically the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, or redirect it to a file. To output a value, use it by itself; e.g. $value, instead of Write-Host $value (or use Write-Output $value); see this answer.
To explicitly print only to the display but with rich formatting, use Out-Host, given that the .ToString() stringification that Write-Host uses on its input is often unhelpful - see this post.
[1] Note that PowerShell's comparison operators such as -eq exhibit filtering behavior when their LHS is an array (collection); that is, instead of returning $true or $false, they then return the sub-array of matching elements - see about_Comparison_Operators.

PowerShell Ast for injection detection

I am generating a ScriptBlock based on DB input which I invoke later in the script. I now want to ensure that a malicious user is not injecting any PS code in the DB varchar field that then gets executed.
First, I filtered the String Script Block for forbidden chars such as $ or ;. But I want to take it one step further and use AST to check if there is any executable code in the DB field.
When I use $Ast.FindAll for a specific element such as ForEachStatementAst it works fine.
However, I also want to be able to detect cmdlets etc in the String.
Examples that should be recognised as being ok:
abc
123
'a','b'
true
Examples that should be recognised as being not ok:
Write-host or Remove-Item or any other get-command cmdlet.
`$(MySubExpression)
When using AST visualisation, I get the same tree for both examples. ('abc', 'Write-Host')
ScriptBlockAst-> NamedBlockAst -> PipelineAst -> CommandAst -> StringConstantExpressionAst
Is there any way I can use AST to determine whether the DB field (or any string) contains only allowed entries such as non PS keywords / cmdlets, numbers etc but nothing that could be used as a PS command and that could be invoked?
The following code works for the test cases but I wonder if this can be achieved in a better way. If Res.count > 0, the input was not ok, if =0, it was ok.
$DebugPreference = 'Continue'
[System.Collections.Generic.List[System.String]]$InputStringList = New-Object -TypeName "System.Collections.Generic.List[System.String]"
$InputStringList.Add("foreach (`$x in #('a','b')){;}")
$InputStringList.Add("New-item -Path 'C:\Test.txt' -ItemType File")
$InputStringList.Add("Write-host 'as'")
$InputStringList.Add("abc")
$InputStringList.Add("a,b,c")
$InputStringList.Add("123")
$InputStringList.Add("true")
[System.Collections.Generic.List[System.Type]]$TypeList = New-Object -TypeName "System.Collections.Generic.List[System.Type]"
$TypeList.Add([System.Management.Automation.Language.StringLiteralToken])
$TypeList.Add([System.Management.Automation.Language.ScriptBlockAst])
$TypeList.Add([System.Management.Automation.Language.NamedBlockAst])
$TypeList.Add([System.Management.Automation.Language.StringConstantExpressionAst])
$TypeList.Add([System.Management.Automation.Language.ConstantExpressionAst])
$TypeList.Add([System.Management.Automation.Language.CommandExpressionAst])
$TypeList.Add([System.Management.Automation.Language.CommandAst])
$TypeList.Add([System.Management.Automation.Language.PipelineAst])
$TypeList.Add([System.Management.Automation.Language.ArrayLiteralAst])
[String[]]$CommandArray = (Get-Command | Select-Object -ExpandProperty 'Name')
[System.Management.Automation.ScriptBlock]$Predicate =
{
param([System.Management.Automation.Language.Ast]$AstObject)
Write-Debug -Message $AstObject.GetType().FullName
if($AstObject -is [System.Management.Automation.Language.StringConstantExpressionAst])
{
if($AstObject.Value -in $CommandArray)
{
return $true
}
else
{
return $false
}
}
else
{
return (-not($AstObject.GetType() -in $TypeList))
}
}
$InputStringList.GetEnumerator() | ForEach-Object -Process `
{
Write-Debug -Message ("Processing string: "+$PsItem.ToString())
$ast = [System.Management.Automation.Language.Parser]::ParseInput($PsItem, [ref]$null, [ref]$null)
$res=$ast.FindAll($Predicate, $true)
Write-Debug -Message $res.count.ToString()
}
As commented, what you trying to do is creating your own restricted languagemode. Meaning that it would probably be easier to invoke the concerned scriptblock in an restricted runspace.
Derived from #mklement0 great answer for Automatically retrieve Allowed Types for Constrained Language mode:
Function Invoke-Restricted {
[CmdletBinding()]param([String]$Expression)
$Restricted = [powershell]::Create()
$Restricted.Runspace.SessionStateProxy.LanguageMode = 'Restricted'
Try { $Restricted.AddScript($expression).Invoke() }
Catch { $PSCmdlet.ThrowTerminatingError($_) }
}
Restricted expression
Invoke-Restricted #'
#{
string = 'abc'
int = 123
array = 'a','b'
hashtable = #{ a = 1; b = 2 }
boolean = $true
}
'#
Yields
Name Value
---- -----
array {a, b}
int 123
boolean True
string abc
hashtable {b, a}
Invalid expression
Invoke-Restricted #'
#{
TimeSpan = [TimeSpan]'12:34:45'
}
'#
Throws an error:
Invoke-Restricted: Exception calling "Invoke" with "0" argument(s): "At line:1 char:1
+ [TimeSpan]"12:34:45"
+ ~~~~~~~~~~
The type TimeSpan is not allowed in restricted language mode or a Data section."
Yet, it has some limitations as it does not prevent e.g. the use of cmdlets.
For an easy and secure way to retrieve a (structured) configuration file I would depend on a serialized format as JSON using the ConvertFrom-Json cmdlet
Related: #12377 Running partly trusted PowerShell code in a restricted security environment.

Verifying a string is not empty/null in an If condition

I'm writing a script that will accept user input via Read-Host (set to $String), and I want to avoid any issues that could be caused by having a blank value for the variables. Since I'll be using this a lot, I want to implement it into a function that verifies no invalid characters are being used.
I thought I could use an if statement with ![string]::IsNullOrEmpty($String) as one of the conditions:
Function Test-ValidCharacters ($String, $ValidCharacters) {
if (($String -match $ValidCharacters) -and (!([string]::IsNullOrEmpty($String)))) {
return $true
}
else {return $false}
}
I also tried this:
Function Test-ValidCharacters ($String, $ValidCharacters) {
if (($String -match $ValidCharacters) -and ($String -ceq "")) {
return $true
}
else {return $false}
}
In both of these cases, I can just hit enter when presented with the $String's Read-Host prompt and the script will behave as if the function returned $True (and then later encounter fatal errors). The other half works - if I include characters not specified by $ValidCharacters the function returns $False as expected.
I am sure I'm missing something here. I even tried doing a second nested if statement and got the same result.
Edit: Here's the code snippet where I call the function and notice the issue.
$ValidCharacters = '[^a-zA-Z0-9]'
$FirstN = Read-Host -Prompt "New user's first name"
While (Test-ValidCharacters $FirstN $ValidCharacters -eq $false) {
Write-Output "$FirstN contains illegal characters. A-Z, a-z, and 0-9 are accepted."
$FirstN = Read-Host -Prompt "New user's first name"
}
Assuming $ValidCharacters isn't itself an empty string and contains an anchored character-range regex (regular expression) that covers the entire input string, such as ^[a-z0-9./:]+$, given that the -match operator matches any substring by default (note that a better name for the parameter is therefore something like $ValidationRegex):[1]
In the first function definition, the RHS of your -and operation is redundant - it adds nothing to the conditional, because if $String -match $ValidCharacters is $true, then so is ! [string]::IsNullOrEmpty($String), by definition.
Conversely, in the second function definition your -and operation always returns $false, because $String -ceq "" is by definition $false, if the LHS returned $true.
Assuming that your intent is to prevent empty or all-whitespace input and to ensure that any string - trimmed of incidental leading and/or trailing whitespace - is composed only of expected characters, use the following:
Function Test-ValidCharacters ($String, $ValidCharacters) {
# Note: no strict need for `return`.
$String.Trim() -match $ValidCharacters
}
[1] Alternatively, stick with $ValidCharacters and pass a regex that describes only a single valid character, such as '[a-z0-9./:]', and construct the entire-string matching regex inside the function with '^' + $ValidCharacters + '+$'