Module/STIG/Functions.RuleQuery.ps1

using module ..\Rule\Rule.psm1

<#
    .SYNOPSIS
        Get the STIG Rule Details for a given rule supported by PowerSTIG.
 
    .DESCRIPTION
        Get the STIG Rule Details for a given rule supported by PowerSTIG.
 
    .PARAMETER VulnId
        VulnId within PowerSTIG is typically labled as the RuleId, which
        may not be consistent with DISA terminology.
 
    .PARAMETER LegacyId
        Specify the "previous" VulnId/RuleId, prior to DISA October 2020 Id
        updates.
 
    .PARAMETER ProcessedXmlPath
        Either the folder where the processed xml resides or a specific xml path.
        The default is .\StigData\Processed\*.xml
 
    .EXAMPLE
        PS> Get-StigRule -VulnId 'V-1114', 'V-1115'
 
        This example will return the rule details for V-1114 and V-1115 from the Windows Server
        2012 R2 Member Server and Domain Controller STIGs.
#>

function Get-StigRule
{
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'VulnId')]
        [ValidateScript({$_ -match '^V-\d{1,}(|\.[a-z])$'})]
        [Alias("RuleId")]
        [string[]]
        $VulnId,

        [Parameter(Mandatory = $true, ParameterSetName = 'LegacyId')]
        [ValidateScript({$_ -match '^V-\d{1,}(|\.[a-z])$'})]
        [string[]]
        $LegacyId,

        [Parameter()]
        [ValidateScript({Test-Path -Path $_})]
        [string]
        $ProcessedXmlPath = (Join-Path -Path $PSScriptRoot -ChildPath '..\..\StigData\Processed\*.xml'),

        [Parameter()]
        [switch]
        $Detailed
    )

    switch ($PSCmdlet.ParameterSetName)
    {
        'VulnId'
        {
            $vulnIdPattern = '<Rule\s+id\s*="{0}"' -f $VulnId
            $patternReplacement = '<Rule\s+id\s*="'
            $xmlXPathPattern = '//Rule[@id = "{0}"]'
        }

        'LegacyId'
        {
            $vulnIdPattern = '<LegacyId>{0}' -f $LegacyId
            $patternReplacement = '<LegacyId>'
            $xmlXPathPattern = '//Rule[LegacyId="{0}"]'
        }
    }

    $processedXml = Select-String -Path $ProcessedXmlPath -Pattern $vulnIdPattern -Exclude '*.org.default.xml' | Sort-Object -Property Pattern

    if ($null -eq $processedXml)
    {
        Write-Warning -Message "The VulnId(s) specified were not found in $ProcessedXmlPath"
        return
    }

    # hashtable to store rule property lookups when multiple rule types are specified
    $ruleTypeProperty = @{}

    foreach ($technologyXml in $processedXml)
    {
        # based on the VulnId specificed use XPath to search the xml object
        $vulnIdFromXml = $technologyXml.Pattern.Replace($patternReplacement, $null).Replace('"', $null)
        $ruleIdXPath = $xmlXPathPattern -f $vulnIdFromXml
        [xml] $xml = Get-Content -Path $technologyXml.Path
        $ruleData = $xml.DISASTIG.SelectNodes($ruleIdXPath)
        $ruleType = $ruleData.ParentNode.ToString()

        # if the current rule type is not stored in the hashtable, run Get-UniqueRuleTypeProperty and store the results for future use
        if (-not $ruleTypeProperty.ContainsKey($ruleType))
        {
            $uniqueRuleTypeProperty = Get-UniqueRuleTypeProperty -Rule $ruleData
            $ruleTypeProperty.Add($ruleType, $uniqueRuleTypeProperty)
        }

        # pulling the VulnDiscussion as the description out of the xml using a regex capture group
        $ruleDescriptionMatch = [regex]::Match($ruleData.description.Replace("`n", ' '), '<VulnDiscussion>(?<description>.*)<\/VulnDiscussion>')

        # address edge case where an out of place OS Control charactor [char]157 in the STIG's description, i.e. Adobe Reader / V-64919, removing it
        $ruleDescriptionValue = $ruleDescriptionMatch.Groups.Item('description').Value -replace '\u009D'

        # using PSv3 "ordered" to create an ordered hashtable for PSCustomObject property list display order
        if ($PSBoundParameters.ContainsKey('Detailed'))
        {
            $ruleDetail = [ordered] @{
                StigId                      = $xml.DISASTIG.stigid
                StigVersion                 = $xml.DISASTIG.fullversion
                VulnId                      = $ruleData.id
                LegacyId                    = $ruleData.LegacyId
                Severity                    = $ruleData.severity
                Title                       = $ruleData.title
                Description                 = $ruleDescriptionValue
                RuleType                    = $ruleType
                DscResource                 = $ruleData.dscresource
                DuplicateOf                 = $ruleData.DuplicateOf
                OrganizationValueRequired   = $ruleData.OrganizationValueRequired
                OrganizationValueTestString = $ruleData.OrganizationValueTestString
            }
        }
        else
        {
            $ruleDetail = [ordered] @{
                RuleType = $ruleType
                VulnId   = $ruleData.id
            }
        }

        # adding the rule specific properties to the ordered hashtable and then casting to PSCustomObject
        foreach ($value in $ruleTypeProperty[$ruleType])
        {
            $ruleDetail.Add($value, $ruleData.$value)
        }

        [PSCustomObject] $ruleDetail
    }
}

<#
    .SYNOPSIS
        Get the unique rule type properties given a specific rule type.
 
    .DESCRIPTION
        Get the unique rule type properties given a specific rule type.
 
    .PARAMETER Rule
        A rule by leveraging the selected XmlNodeList from a processed xml.
 
    .EXAMPLE
        PS> Get-UniqueRuleTypeProperty -Rule $xml.DISASTIG.RegistryRule.Rule[0]
 
        Returns the delta properties between the RegistryRule and Base Rule class
#>

function Get-UniqueRuleTypeProperty
{
    [CmdletBinding()]
    [OutputType([string[]])]
    param
    (
        [Parameter(Mandatory = $true)]
        [Object]
        $Rule
    )

    $blankRule = New-Object -TypeName Rule
    $commonProperties = ($blankRule | Get-Member -MemberType Property).Name
    $ruleProperty = ($Rule | Get-Member -MemberType 'NoteProperty', 'Property').Name
    $compareObjResult = Compare-Object -ReferenceObject $ruleProperty -DifferenceObject $commonProperties
    $filteredCompareResult = $compareObjResult | Where-Object -FilterScript {$PSItem.SideIndicator -eq '<=' -and $PSItem -notmatch 'Stig(Id|Version)|VulnId|RuleType'}
    return $filteredCompareResult.InputObject
}

<#
    .SYNOPSIS
        Returns a string of all properties of a given rule and structured for use within a PowerSTIG configuraiton.
 
    .DESCRIPTION
        Returns a string of all properties of a given rule and structured for use within a PowerSTIG configuraiton.
 
    .PARAMETER Rule
        A rule object which was created through Get-StigRule
 
    .PARAMETER Formatted
        By default the function will return a single line string which represents the rule exception, when Formatted
        is supplied, the funciton will return a formatted string, i.e.:
 
        V-1155 = @{
            Constant = 'SeDenyNetworkLogonRight'
            DisplayName = 'Deny access to this computer from the network'
            Force = 'False'
            Identity = ''
        }
 
    .EXAMPLE
        PS> $rule = Get-StigRule -RuleId V-1155 | Select-Object -First 1
        PS> Get-StigRuleExceptionString -Rule $rule
 
        Returns the following exception string:
        V-1155 = @{Constant = 'SeDenyNetworkLogonRight'; DisplayName = 'Deny access to this computer from the network'; Force = 'False'; Identity = ''}
 
    .EXAMPLE
        PS> $rule = Get-StigRule -RuleId V-1155 | Select-Object -First 1
        PS> Get-StigRuleExceptionString -Rule $rule -Formatted
 
        Returns the following exception string:
        V-1155 = @{
          Constant = 'SeDenyNetworkLogonRight'
          DisplayName = 'Deny access to this computer from the network'
          Force = 'False'
          Identity = ''
        }
#>

function Get-StigRuleExceptionString
{
    [CmdletBinding()]
    [OutputType([string])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSObject[]]
        $Rule,

        [Parameter()]
        [switch]
        $Formatted
    )

    begin
    {
        $ruleTypeProperty = @{}
    }

    process
    {
        foreach ($ruleData in $Rule)
        {
            if (-not $ruleTypeProperty.ContainsKey($ruleData.RuleType))
            {
                $uniqueRuleTypeProperty = Get-UniqueRuleTypeProperty -Rule $ruleData
                $ruleTypeProperty.Add($ruleData.RuleType, $uniqueRuleTypeProperty)
            }

            $ruleDetail = [ordered] @{}

            foreach ($value in $ruleTypeProperty[$ruleData.RuleType])
            {
                if ($value -ne $ruleData.RuleType)
                {
                    $ruleDetail.Add($value, $ruleData.$value)
                }
            }

            $exceptionString = New-Object -TypeName System.Text.StringBuilder
            [void] $exceptionString.Append("$($ruleData.VulnId) = @{")
            foreach ($key in $ruleDetail.Keys)
            {
                [void] $exceptionString.Append("$key = '$($ruleDetail[$key])'; ")
            }

            $exceptionString = $exceptionString.ToString() -replace ';\s$', '}'

            if ($Formatted)
            {
                $exceptionString -replace ';\s?', "`n " -replace '@{', "@{`n " -replace '}', "`n}"
            }
            else
            {
                $exceptionString
            }
        }
    }
}