Module/Common/Function.RangeConversion.ps1
#region Header # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. #endregion #region Main Functions <# .SYNOPSIS This function is a selection function that looks at text containing conditional language and tries to identify the correct specialized function to set it to for conversion. The conversion functions called by this function do the English to PowerShell conversion. .Parameter String The STIG text contains conditional text to try and convert to a PowerShell expression. .Notes General Notes #> function Get-OrganizationValueTestString { [CmdletBinding()] [OutputType([string])] param ( [parameter(Mandatory = $true)] [string] $String ) switch ($String) { {Test-StringIsNegativeOr -String $PSItem} { ConvertTo-OrTestString -String $PSItem -Operator NotMatch continue } {Test-StringIsPositiveOr -String $PSItem} { ConvertTo-OrTestString -String $PSItem -Operator Match continue } { (Test-StringIsLessThan -String $PSItem) -or (Test-StringIsLessThanOrEqual -String $PSItem) -or (Test-StringIsLessThanButNot -String $PSItem) -or (Test-StringIsLessThanOrEqualButNot -String $PSItem) -or (Test-StringIsGreaterThan -String $PSItem) -or (Test-StringIsGreaterThanOrEqual -String $PSItem) -or (Test-StringIsGreaterThanButNot -String $PSItem) -or (Test-StringIsGreaterThanOrEqualButNot -String $PSItem) } { ConvertTo-TestString -String $PSItem continue } {Test-StringIsMultipleValue -String $PSItem} { ConvertTo-MultipleValue -String $PSItem continue } } } function Get-TestStringTokenNumbers { [CmdletBinding()] [OutputType([string[]])] param ( [parameter(Mandatory = $true)] [string] $String ) $tokens = [System.Management.Automation.PSParser]::Tokenize($String, [ref]$null) $number = $tokens.Where({$PSItem.type -eq 'Number'}).Content <# There is an edge case where the hex and decimal values are provided inline, so pick the hex code out and convert it to an int. #> $match = $number | Select-String -Pattern "\b(0x[A-Fa-f0-9]{8}){1}\b" if ($match) { [convert]::ToInt32($match,16) } else { $number } } <# .SYNOPSIS Uses the PowerShell parser to tokenize the English sentences into individual words that are regrouped and complied into PS representations that can be applied and measured automatically. #> function Get-TestStringTokenList { [CmdletBinding(DefaultParameterSetName = 'CommandTokens')] [OutputType([string])] param ( [parameter(Mandatory = $true)] [string] $String, [parameter(ParameterSetName = 'CommandTokens')] [switch] $CommandTokens, [parameter(ParameterSetName = 'StringTokens')] [switch] $StringTokens ) $tokens = [System.Management.Automation.PSParser]::Tokenize($String, [ref]$null) if($PSCmdlet.ParameterSetName -eq 'StringTokens') { return $tokens.Where({ $PSItem.type -eq 'String' }).Content } $commands = $tokens.Where({ $PSItem.type -eq 'CommandArgument' -or $PSItem.type -eq 'Command' }).Content return ( $commands -join " " ) } function ConvertTo-TestString { [CmdletBinding()] [OutputType([string])] param ( [parameter(Mandatory = $true)] [string] $String ) $number = Get-TestStringTokenNumbers -String $String $operators = Get-TestStringTokenList -String $String -CommandTokens switch ($operators) { 'greater than' { return "{0} -gt '$number'" } 'or greater' { return "{0} -ge '$number'" } 'greater than but not' { return "{0} -gt '$($number[0])' -and {0} -lt '$($number[1])'" } 'or greater but not' { return "{0} -ge '$($number[0])' -and {0} -lt '$($number[1])'" } 'less than' { return "{0} -lt '$number'" } 'or less' { return "{0} -le '$number'" } 'less than but not' { return "{0} -lt '$($number[0])' -and {0} -gt '$($number[1])'" } 'or less but not' { return "{0} -le '$($number[0])' -and {0} -gt '$($number[1])'" } } } <# .SYNOPSIS Converts a Rule to a hashtable so it can be splatted to other functions .PARAMETER InputObject The object being converted .NOTES There are multiple rules in the DNS STIG that enforce the same setting. If a duplicate rule is found it is converted to a documentRule #> function ConvertTo-HashTable { [CmdletBinding()] [OutputType([hashtable])] param ( [object] $InputObject ) $hashTable = @{ Id = $InputObject.id Severity = $InputObject.Severity Title = $InputObject.title } return $hashTable } #endregion #region Or <# .SYNOPSIS Checks if a string is asking for a negative or evaluation. Applies a reagular expression against the string to look for a known pattern asking for a value to not be equal to one of 2 values. .PARAMETER String The string data to evaluate. .EXAMPLE This example returns $true Test-StringIsNegativeOr -String "1 or 2 = a Finding" .EXAMPLE This example returns $false Test-StringIsNegativeOr -String "1 or 2 = is not a Finding" .NOTES Tests if a string such as '1 or 2 = a Finding' is a negative or test. #> function Test-StringIsNegativeOr { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) # if ($string -match "^(\s*)(\d{1,})(\s*)or(\s*)(\d{1,})(\s*)=(\s*)a(\s*)Finding(\s*)$") { $true } else { $false } } <# .SYNOPSIS .PARAMETER string The string data to evaluate. .EXAMPLE An example .NOTES # This regex looks for patterns such as "1 (Lock Workstation) or 2 (Force Logoff)" #> function Test-StringIsPositiveOr { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $string ) <# Optional characters were seperated from the rest of the RegEx because it is a repeating pattern. If new characters are discovered in the future, they can be added here and in the tests. #> $optionalCharacter = "(\(|'|"")?" $regex = "^(\s*)(\d{1,})(\s*)$optionalCharacter.*$optionalCharacter" + "(\s*)or(\s*)(\d{1,})(\s*)$optionalCharacter.*$optionalCharacter(\s*)$" if ($string -match $regex) { $true } else { $false } } <# .SYNOPSIS Converts English textual representation of a comparison to a PowerShell code representation. .DESCRIPTION Using the Abstract Syntax Tree capability of PowerShell, the provided string is broken into individual AST Tokens. Those tokens are then combined to form the PowerShell version of the English text. The output of this function is intended to be added to any STIG rule that is ambiguous due to a range of possibilities be valid. The test string is used to determine if a local organizational setting is within a valid range according to the STIG. .PARAMETER String The string to convert .EXAMPLE This example returns the following comparison test -ne '1|2' ConvertTo-OrTestString -String '1 or 2 = a Finding' -Operator NotEqual .EXAMPLE This example returns the following comparison test -eq '1|2' ConvertTo-OrTestString -String '1 (Lock Workstation) or 2 (Force Logoff)' -Operator Equal .NOTES General notes #> function ConvertTo-OrTestString { [CmdletBinding()] [OutputType([string])] param ( [parameter(Mandatory = $true)] [string] $String, [Parameter(Mandatory = $true)] [ValidateSet('Match', 'NotMatch')] [String] $Operator ) $operatorString = @{ 'Match' = '-match' 'NotMatch' = '-notmatch' } try { $tokens = [System.Management.Automation.PSParser]::Tokenize($string, [ref]$null) $numbers = $tokens.Where( {$PSItem.type -eq 'Number'}).Content "{0} $($operatorString[$Operator]) '$($numbers -join "|")'" } catch { Throw "Unable to convert $string into test string." } } #endregion #region Greater Than <# .SYNOPSIS Converts English textual representation of numeric ranges into PowerShell equivalent comparison statements. .PARAMETER string The String to test. .EXAMPLE This example returns $true Test-StringIsGreaterThan -String '14 (or greater)' .NOTES Sample STIG data would convert #> function Test-StringIsGreaterThan { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) if ($string -match "^(\s*)Greater(\s*)than(\s*)(\d{1,})(\s*)$") { $true } else { $false } } <# .SYNOPSIS Converts English textual representation of numeric ranges into PowerShell equivalent comparison statements. .PARAMETER string The String to test. .EXAMPLE This example returns $true Test-StringIsGreaterThanOrEqual -String '0x00000032 (50) (or greater)' .NOTES Sample STIG data would convert 0x00000032 (50) (or greater) into '-ge 50'" #> function Test-StringIsGreaterThanOrEqual { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) if ($string -match "^(\s*)((0x[A-Fa-f0-9]{8}){1})|(\d{1,})(\s*)(\()?or(\s*)greater(\s*)(\))?(\s*)$") { $true } else { $false } } <# .SYNOPSIS Converts English textual representation of numeric ranges into PowerShell equivalent comparison statements. .PARAMETER string The String to test. .EXAMPLE This example returns $true Test-StringIsGreaterThanButNot -String 'Greater than 30' .NOTES Sample STIG data would convert 30 (or greater, but not 100) #> function Test-StringIsGreaterThanButNot { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) if ($string -match "^(\s*)greater(\s*)than(\s*)(\d{1,})(\s*)(\()?(\s*)but(\s*)not(\s*)(\d{1,})(\))?(\s*)$") { $true } else { $false } } <# .SYNOPSIS Converts English textual representation of numeric ranges into PowerShell equivalent comparison statements. .PARAMETER string The String to test. .EXAMPLE This example returns $true Test-StringIsGreaterThanOrEqualToButNot -String '0x00000032 (50) (or greater)' .NOTES Sample STIG data #> function Test-StringIsGreaterThanOrEqualButNot { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) if ($string -match "^(\s*)(\d{1,})(\s*)(\()?(\s*)or(\s*)greater(\s*),(\s*)but(\s*)not(\s*)(\d{1,})(\))?(\s*)$") { $true } else { $false } } #endregion #region Less Than <# .SYNOPSIS Converts English textual representation of numeric ranges into PowerShell equivalent comparison statements. .PARAMETER string The String to test. .EXAMPLE This example returns $true Test-StringIsLessThan -String 'is less than "14"' #> function Test-StringIsLessThan { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) if ($String -match "^(\s*)less(\s*)than(\s*)(\d{1,})(\))?(\s*)$") { $true } else { $false } } <# .SYNOPSIS Converts English textual representation of numeric ranges into PowerShell equivalent comparison statements. .PARAMETER string The String to test. .EXAMPLE This example returns $true Test-StringIsLessThanOrEqual -String '"4" logons or less' #> function Test-StringIsLessThanOrEqual { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) # Turn 0x00000384 (900) (or less) into '-le 900' if ($String -match "^((\s*)((0x[A-Fa-f0-9]{8}){1}))?(\s*)(\()?(\d{1,})(\))?(\s*)(\()?or(\s*)less(\))?(\s*)$") { $true } else { $false } } <# .SYNOPSIS Converts English textual representation of numeric ranges into PowerShell equivalent comparison statements. .PARAMETER string The String to test. .EXAMPLE This example returns $true Test-StringIsLessThanButNot -String 'Less than 30 (but not 0)' .NOTES Sample STIG data would convert "Less than 30 (but not 0)" into '$i -lt "30" -and $i -gt 0' #> function Test-StringIsLessThanButNot { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) #"$i -lt $value -and -ne $x" if ($string -match "^(\s*)less(\s*)than(\s*)(\d{1,})(\s*)(\()?but(\s*)not(\s*)(\d{1,})(\))?(\s*)$") { $true } else { $false } } <# .SYNOPSIS Converts English textual representation of numeric ranges into PowerShell equivalent comparison statements. .PARAMETER string The String to test. .EXAMPLE This example returns $true Test-StringIsLessThanOrEqualToButNot -String '30 (or less, but not 0)' .NOTES Sample STIG data would convert 30 (or less, but not 0) into '$i -le "30" -and $i -gt 0' #> function Test-StringIsLessThanOrEqualButNot { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) if ($string -match "^(\s*)(\d{1,})(\s*)(\()?or(\s*)less(\s*),(\s*)but(\s*)not(\s*)(\d{1,})(\))?(\s*)$") { $true } else { $false } } #endregion #region Multiple Values <# .SYNOPSIS Test if the string may contain multiple setting values .PARAMETER String The string to test .EXAMPLE This example returns $true Test-StringIsMultipleValue -String 'Possible values are orange, lemon, cherry' #> function Test-StringIsMultipleValue { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [string] $String ) if ($string -match "(?<=Possible values are ).*") { $true } else { $false } } <# .SYNOPSIS Returns the possible setting values .PARAMETER String The string to test .EXAMPLE This example returns "{0} -match 'orange|lemon|cherry'"" ConvertTo-MultipleValue -String 'Possible values are orange, lemon, cherry' #> function ConvertTo-MultipleValue { [CmdletBinding()] [OutputType([string])] Param ( [parameter(Mandatory)] [string[]] $String ) $values = [regex]::match( $string, "(?<=Possible values are ).*" ).groups.Value $options = $values.replace(', ', '|') Write-Verbose "[$($MyInvocation.MyCommand.Name)] Possible Values : $options " return $( "'{0}' -match '^($options)$'" ) } #endregion #region Security Policy <# .SYNOPSIS Selects the string that contains the policy setting and value(s) #> function Get-SecurityPolicyString { [CmdletBinding()] [OutputType([string])] param ( [parameter(Mandatory = $true)] [AllowEmptyString()] [string[]] $CheckContent ) Write-Verbose "[$($MyInvocation.MyCommand.Name)]" $stringMatch = 'If the (value for (the)?)?|(value\s)' $result = ( $CheckContent | Select-String -Pattern $stringMatch ) -replace $stringMatch, '' # 'V-63427' (Win10) returns multiple matches. This is ensure the only the correct one is returned. $result = $result | Where-Object -FilterScript {$PSItem -notmatch 'site is using a password filter'} # V-73317 (WinSvr 2016) returns multiple matches, but we want both joined to calculate the range. $result = ($result -join " or ").Trim() return $result } <# .SYNOPSIS Checks the string for text that indicates a range of acceptable acceptable values are allowed by the STIG. #> function Test-SecurityPolicyContainsRange { [CmdletBinding()] [OutputType([bool])] param ( [parameter(Mandatory = $true)] [AllowEmptyString()] [string[]] $CheckContent ) Write-Verbose "[$($MyInvocation.MyCommand.Name)]" $string = Get-SecurityPolicyString -CheckContent $CheckContent $string = Get-TestStringTokenList -String $string if ( $string -match '(?:is not set to )(?!(?:(a )other than)).*(?:this is a finding\.)' ) { return $false } return $true } <# .SYNOPSIS Converts the Check-Content string into a PowerShell comparison string that can validate user input to organizational values. #> function Get-SecurityPolicyOrganizationValueTestString { [CmdletBinding()] param ( [parameter(Mandatory = $true)] [AllowEmptyString()] [string[]] $CheckContent ) Write-Verbose "[$($MyInvocation.MyCommand.Name)]" $stringBase = Get-SecurityPolicyString -CheckContent $CheckContent $string = Get-TestStringTokenList -String $stringBase -CommandTokens $settings = Get-TestStringTokenList -String $stringBase -StringTokens $reverse = @{ 'lt' = 'ge'; 'le' = 'gt'; 'gt' = 'le'; 'ge' = 'lt'; 'eq' = 'ne'; 'ne' = 'eq' } # The index string to add to the comparison for use in composite formatting. $indexString = "'{0}'" # The variable needs to be strongly typed for the indexing to work properly, when a single operator is found. # If not strongly typed, a single operator will return indexed characters. [string[]] $operators = @() # Some of the sentence structure is inverted, so this flag will realign the sentence structure so that that range operator # is always before the eq|ne operators. $invertAdjective = $false # Some of the ranges have exclusions, so the comparison operator should not be inverted and this flag controls that. $excludeSecondAdjective = $false switch ($string) { {$string -match '^is set to'} { $operators = 'eq' } {$string -match '^is less than(?!.*excluding)'} { $operators = 'lt'; continue } {$string -match '^is less than(?=.*excluding)'} { $operators = 'lt', 'or', 'eq'; $excludeSecondAdjective = $true; continue } {$string -match '^is greater than(?!.*(excluding|is set))'} { $operators = 'gt'; continue } {$string -match '^is greater than.*(?=is set)'} { $operators = 'gt', 'and', 'eq'; continue } # The InvertAdjective changes the string to read 'is more than or' to move the equal to the second position like everything else. {$string -match '^is or more than'} { $operators = 'gt', 'and', 'eq'; $invertAdjective = $true; continue } {$string -match '^is not set to a other than'} { $operators = 'eq' } } # Since the sentence was inverted, the value positions need to be inverted as well. if ($invertAdjective) { $firstValue = $settings[2] $secondValue = $settings[1] } else { $firstValue = $settings[1] $secondValue = $settings[2] } # Some settings are negated with the string 'this is a finding, so invert the comparison operators if the check is negated. if ($string -match 'this is a finding') { # if a string contains and/or build that into the test string operators if ($operators.count -gt '1') { # Some settings have values that need to be excluded from a range, so do not invert that operator if ($excludeSecondAdjective) { "$indexString -$($reverse[$operators[0]]) '$firstValue' -$($operators[1]) $indexString -$($operators[2]) '$secondValue'" } else { "$indexString -$($reverse[$operators[0]]) '$firstValue' -$($operators[1]) $indexString -$($reverse[$operators[2]]) '$secondValue'" } } else { "$indexString -$($reverse[$operators[0]]) '$firstValue'" } } else { "$indexString -$operators '$firstValue'" } } #endregion |