XMLOps/Merge-Signers_Semantic.psm1

Function Merge-Signers_Semantic {
    <#
    .SYNOPSIS
        Merges the FilePublisher and Publisher Signers in an XML file based on their TBS, Name, and CertPublisher values
        For each FilePublisher signer, if two signers are found with the same TBS, Name, and CertPublisher, only one of them will be kept, and their FileAttribRefs are merged
        For each Publisher signer, if two signers are found with the same TBS, Name, and CertPublisher, only one of them will be kept
 
        If two signers have the same TBS, Name, and CertPublisher but only one of them has FileAttribRefs, then they are not the same. This function makes the distinction between FilePublisher and Publisher signers.
        This function makes the distinction between the Signers that are part of Signing Scenario 131 and the ones that are part of Signing Scenario 12.
        So there are 4 different Signer types to consider.
 
        At the end, the XML file will have unique FilePublisher and Publisher signers for Signing Scenario 131 and 12, unique elements in the <AllowedSigners> and <CiSigners>
 
        Also, each Signer will have unique and valid FileAttribRef elements with IDs that point to an existing FileAttrib element in the <FileRules> node
    .PARAMETER XmlFilePath
        The path to the XML file to be modified
    .INPUTS
        System.IO.FileInfo
    .OUTPUTS
        System.Void
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    Param(
        [Parameter(Mandatory = $true)][System.IO.FileInfo]$XmlFilePath
    )
    Begin {
        [System.Xml.XmlDocument]$Xml = Get-Content -Path $XmlFilePath
    }

    Process {

        # Define the namespace manager
        [System.Xml.XmlNamespaceManager]$Ns = New-Object -TypeName System.Xml.XmlNamespaceManager -ArgumentList $Xml.NameTable
        $Ns.AddNamespace('ns', 'urn:schemas-microsoft-com:sipolicy')

        # Get the User Mode Signing Scenario node
        [System.Xml.XmlNodeList]$AllowedSigners12 = $Xml.SelectNodes('//ns:SigningScenarios/ns:SigningScenario[@Value="12"]/ns:ProductSigners/ns:AllowedSigners', $Ns)

        # Get the Kernel Mode Signing Scenario node
        [System.Xml.XmlNodeList]$AllowedSigners131 = $Xml.SelectNodes('//ns:SigningScenarios/ns:SigningScenario[@Value="131"]/ns:ProductSigners/ns:AllowedSigners', $Ns)

        # Get the CiSigners node
        [System.Xml.XmlNodeList]$CiSigners = $Xml.SelectNodes('//ns:CiSigners', $Ns)

        # Find all Signer nodes
        [System.Xml.XmlNodeList]$SignerNodes = $Xml.SelectNodes('//ns:Signers/ns:Signer', $Ns)

        # Create a hashtable to track unique FilePublisher signers for Signing Scenario 131 - Signers that have at least one FileAttribRef
        [System.Collections.Hashtable]$UniqueFilePublisherSigners131 = @{}

        # Create a hashtable to track unique Publisher signers for Signing Scenario 131 - Signers that have no FileAttribRef
        [System.Collections.Hashtable]$UniquePublisherSigners131 = @{}

        # Create a hashtable to track unique FilePublisher signers for Signing Scenario 12 - Signers that have at least one FileAttribRef
        [System.Collections.Hashtable]$UniqueFilePublisherSigners12 = @{}

        # Create a hashtable to track unique Publisher signers for Signing Scenario 12 - Signers that have no FileAttribRef
        [System.Collections.Hashtable]$UniquePublisherSigners12 = @{}

        # XPath expression to select the Signing Scenario with value 131
        [System.String]$SigningScenario131XPath = '//ns:SigningScenarios/ns:SigningScenario[@Value="131"]'

        # Select the Signing Scenario with value 131
        [System.Xml.XmlNode]$SigningScenario131Node = $Xml.SelectSingleNode($SigningScenario131XPath, $Ns)

        # Find all of the <FileAttrib> elements in the <FileRules> node
        [System.Xml.XmlNodeList]$FileRulesElements = $Xml.SelectNodes('//ns:FileRules/ns:FileAttrib', $Ns)

        $FileRulesValidID_HashSet = [System.Collections.Generic.HashSet[System.String]]@($FileRulesElements.ID)

        if ($SignerNodes.Count -eq 0) {
            [WDACConfig.Logger]::Write('Merge-Signers: No Signer nodes found in the XML file. Exiting the function.')
            Return
        }

        # Iterate over each Signer node
        foreach ($Signer in $SignerNodes) {

            # If the signer has FileAttribRefs, it is a FilePublisher signer
            if ($Signer.SelectNodes('ns:FileAttribRef', $Ns).Count -gt 0) {

                # Making sure that each FilePublisher Signer has valid and unique FileAttribRef elements with IDs that point to an existing FileAttrib element in the <FileRules> node
                $Collection1 = New-Object -TypeName 'System.Collections.Generic.List[System.Xml.XmlNode]'
                foreach ($Item in $Signer.FileAttribRef) {
                    if ($FileRulesValidID_HashSet.Contains($Item.RuleID)) {
                        $Collection1.Add($Item)
                    }
                }
                $ContentToReplaceWith = foreach ($Item in ($Collection1 | Group-Object -Property RuleID)) {
                    $Item.Group[0]
                }

                [System.Int64]$Before = $Signer.FileAttribRef.count

                # Remove all FileAttribRef elements from the Signer, whether they are valid or not
                foreach ($Item in $Signer.FileAttribRef) {
                    [System.Void]$Item.ParentNode.RemoveChild($Item)
                }

                # Add the valid FileAttribRef elements back to the Signer
                foreach ($Item in $ContentToReplaceWith) {
                    [System.Void]$Signer.AppendChild($Xml.ImportNode($Item, $true))
                }

                [System.Int64]$After = $Signer.FileAttribRef.count

                if ($Before -ne $After) {
                    [WDACConfig.Logger]::Write("Merge-Signers: Removed $($Before - $After) FileAttribRef elements from Signer with ID $($Signer.ID).")
                }

                # Determine the Signing Scenario based on the AllowedSigners
                $SigningScenario = $SigningScenario131Node.SelectSingleNode("./ns:ProductSigners/ns:AllowedSigners/ns:AllowedSigner[@SignerId='$($Signer.GetAttribute('ID'))']", $Ns)

                # If the signer is part of Signing Scenario 131
                if ($SigningScenario) {

                    # Create a unique key for each FilePublisher signer based on TBS, Name, and CertPublisher
                    [System.String]$FilePublisherKey = $Signer.SelectSingleNode('ns:CertRoot', $Ns).GetAttribute('Value') + '|' +
                    $Signer.GetAttribute('Name') + '|' +
                    ($Signer.SelectSingleNode('ns:CertPublisher', $Ns) ? $Signer.SelectSingleNode('ns:CertPublisher', $Ns).GetAttribute('Value') : $Null)

                    # If the signer is not in the hashtable, add it with its necessary details
                    if (-not $UniqueFilePublisherSigners131.ContainsKey($FilePublisherKey)) {

                        # Create a temp hashtable to store the signer and its details
                        [System.Collections.Hashtable]$FilePublisherKeyTemp = @{}
                        $FilePublisherKeyTemp['Signer'] = @($Signer.Clone())
                        $FilePublisherKeyTemp['AllowedSigner'] = $AllowedSigners131.SelectNodes("//ns:AllowedSigner[@SignerId='$($Signer.GetAttribute('ID'))']", $Ns)

                        # Add the temp signer hashtable to the main hashtable
                        $UniqueFilePublisherSigners131[$FilePublisherKey] = $FilePublisherKeyTemp
                    }

                    # If the signer is already in the hashtable
                    else {

                        # Get the FileAttribRefs of the existing signer
                        [System.Xml.XmlNodeList]$FileAttribRefs = $Signer.SelectNodes('ns:FileAttribRef', $Ns)

                        # add each of its FileAttribRefs to the existing signer
                        foreach ($FileAttribRef in $FileAttribRefs) {
                            [System.Void]$UniqueFilePublisherSigners131[$FilePublisherKey]['Signer'].AppendChild($Xml.ImportNode($FileAttribRef, $true))
                        }

                        [WDACConfig.Logger]::Write("Merge-Signers: Merged FilePublisher signer for Signing Scenario 131 with IDs: $($UniqueFilePublisherSigners131[$FilePublisherKey].ID) and $($Signer.ID). Their FileAttribRefs are merged.")
                    }
                }
                # If the signer is part of Signing Scenario 12
                else {

                    # Create a unique key for each FilePublisher signer based on TBS, Name, and CertPublisher
                    [System.String]$FilePublisherKey = $Signer.SelectSingleNode('ns:CertRoot', $Ns).GetAttribute('Value') + '|' +
                    $Signer.GetAttribute('Name') + '|' +
                    ($Signer.SelectSingleNode('ns:CertPublisher', $Ns) ? $Signer.SelectSingleNode('ns:CertPublisher', $Ns).GetAttribute('Value') : $Null)

                    # If the signer is not in the hashtable, add it with its FileAttribRefs
                    if (-not $UniqueFilePublisherSigners12.ContainsKey($FilePublisherKey)) {

                        # Create a temp hashtable to store the signer and its details
                        [System.Collections.Hashtable]$FilePublisherKeyTemp = @{}
                        $FilePublisherKeyTemp['Signer'] = @($Signer.Clone())
                        $FilePublisherKeyTemp['AllowedSigner'] = $AllowedSigners12.SelectNodes("//ns:AllowedSigner[@SignerId='$($Signer.GetAttribute('ID'))']", $Ns)
                        $FilePublisherKeyTemp['CiSigners'] = $CiSigners.SelectNodes("//ns:CiSigner[@SignerId='$($Signer.GetAttribute('ID'))']", $Ns)

                        # Add the temp signer hashtable to the main hashtable
                        $UniqueFilePublisherSigners12[$FilePublisherKey] = $FilePublisherKeyTemp
                    }

                    # If the signer is already in the hashtable
                    else {

                        # Get the FileAttribRefs of the existing signer
                        [System.Xml.XmlNodeList]$FileAttribRefs = $Signer.SelectNodes('ns:FileAttribRef', $Ns)

                        # add each of its FileAttribRefs to the existing signer
                        foreach ($FileAttribRef in $FileAttribRefs) {
                            [System.Void]$UniqueFilePublisherSigners12[$FilePublisherKey]['Signer'].AppendChild($Xml.ImportNode($FileAttribRef, $true))
                        }

                        [WDACConfig.Logger]::Write("Merge-Signers: Merged FilePublisher signer for Signing Scenario 12 with IDs: $($UniqueFilePublisherSigners12[$FilePublisherKey].ID) and $($Signer.ID). Their FileAttribRefs are merged.")
                    }
                }
            }

            # If the signer has no FileAttribRefs, it is a Publisher or PCA signer
            elseif ($Signer.SelectNodes('ns:FileAttribRef', $Ns).Count -eq 0) {

                # Determine the Signing Scenario based on the AllowedSigners
                $SigningScenario = $SigningScenario131Node.SelectSingleNode("./ns:ProductSigners/ns:AllowedSigners/ns:AllowedSigner[@SignerId='$($Signer.GetAttribute('ID'))']", $Ns)

                # If the signer is part of Signing Scenario 131
                if ($SigningScenario) {

                    # Create a unique key for each Publisher signer based on TBS, Name, and CertPublisher
                    [System.String]$PublisherKey = $Signer.SelectSingleNode('ns:CertRoot', $Ns).GetAttribute('Value') + '|' +
                    $Signer.GetAttribute('Name') + '|' +
                    ($Signer.SelectSingleNode('ns:CertPublisher', $Ns) ? $Signer.SelectSingleNode('ns:CertPublisher', $Ns).GetAttribute('Value') : $Null)

                    # If the signer is not in the hashtable, add it with its FileAttribRefs
                    if (-not $UniquePublisherSigners131.ContainsKey($PublisherKey)) {

                        # Create a temp hashtable to store the signer and its details
                        [System.Collections.Hashtable]$PublisherKeyTemp = @{}
                        $PublisherKeyTemp['Signer'] = @($Signer.Clone())
                        $PublisherKeyTemp['AllowedSigner'] = $AllowedSigners131.SelectNodes("//ns:AllowedSigner[@SignerId='$($Signer.GetAttribute('ID'))']", $Ns)

                        # Add the temp signer hashtable to the main hashtable
                        $UniquePublisherSigners131[$PublisherKey] = $PublisherKeyTemp
                    }
                    else {
                        [WDACConfig.Logger]::Write("Merge-Signers: Excluded Publisher signer for Signing Scenario 131 with ID: $($Signer.ID). Only one Publisher signer is allowed per TBS, Name, and CertPublisher.")
                    }
                }
                # If the signer is part of Signing Scenario 12
                else {

                    # Create a unique key for each Publisher signer based on TBS, Name, and CertPublisher
                    [System.String]$PublisherKey = $Signer.SelectSingleNode('ns:CertRoot', $Ns).GetAttribute('Value') + '|' +
                    $Signer.GetAttribute('Name') + '|' +
                    ($Signer.SelectSingleNode('ns:CertPublisher', $Ns) ? $Signer.SelectSingleNode('ns:CertPublisher', $Ns).GetAttribute('Value') : $Null)

                    # If the signer is not in the hashtable, add it with its FileAttribRefs
                    if (-not $UniquePublisherSigners12.ContainsKey($PublisherKey)) {

                        # Create a temp hashtable to store the signer and its details
                        [System.Collections.Hashtable]$PublisherKeyTemp = @{}
                        $PublisherKeyTemp['Signer'] = @($Signer.Clone())
                        $PublisherKeyTemp['AllowedSigner'] = $AllowedSigners12.SelectNodes("//ns:AllowedSigner[@SignerId='$($Signer.GetAttribute('ID'))']", $Ns)
                        $PublisherKeyTemp['CiSigners'] = $CiSigners.SelectNodes("//ns:CiSigner[@SignerId='$($Signer.GetAttribute('ID'))']", $Ns)

                        # Add the temp signer hashtable to the main hashtable
                        $UniquePublisherSigners12[$PublisherKey] = $PublisherKeyTemp
                    }
                    else {
                        [WDACConfig.Logger]::Write("Merge-Signers: Excluded Publisher signer for Signing Scenario 12 with ID: $($Signer.ID). Only one Publisher signer is allowed per TBS, Name, and CertPublisher.")
                    }
                }
            }
        }

        foreach ($Item in $UniqueFilePublisherSigners12.Values) {

            # Create a unique ID for each signer
            [System.String]$Guid = [System.Guid]::NewGuid().ToString().replace('-', '').ToUpper()
            $Guid = "ID_SIGNER_A_$Guid"

            # Set the ID attribute of the Signer node to the unique ID
            foreach ($Signer in $Item['Signer']) {
                $Signer.SetAttribute('ID', $Guid)
            }
            # Set the SignerId attribute of the AllowedSigner node to the unique ID
            foreach ($AllowedSigner in $Item['AllowedSigner']) {
                $AllowedSigner.SetAttribute('SignerId', $Guid)
            }
            # Set the SignerId attribute of the CiSigner node to the unique ID
            foreach ($CiSigner in $Item['CiSigners']) {
                $CiSigner.SetAttribute('SignerId', $Guid)
            }
        }

        foreach ($Item in $UniquePublisherSigners12.Values) {

            # Create a unique ID for each signer
            [System.String]$Guid = [System.Guid]::NewGuid().ToString().replace('-', '').ToUpper()
            $Guid = "ID_SIGNER_B_$Guid"

            # Set the ID attribute of the Signer node to the unique ID
            foreach ($Signer in $Item['Signer']) {
                $Signer.SetAttribute('ID', $Guid)
            }
            # Set the SignerId attribute of the AllowedSigner node to the unique ID
            foreach ($AllowedSigner in $Item['AllowedSigner']) {
                $AllowedSigner.SetAttribute('SignerId', $Guid)
            }
            # Set the SignerId attribute of the CiSigner node to the unique ID
            foreach ($CiSigner in $Item['CiSigners']) {
                $CiSigner.SetAttribute('SignerId', $Guid)
            }
        }

        foreach ($Item in $UniquePublisherSigners131.Values) {

            # Create a unique ID for each signer
            [System.String]$Guid = [System.Guid]::NewGuid().ToString().replace('-', '').ToUpper()
            $Guid = "ID_SIGNER_B_$Guid"

            # Set the ID attribute of the Signer node to the unique ID
            foreach ($Signer in $Item['Signer']) {
                $Signer.SetAttribute('ID', $Guid)
            }
            # Set the SignerId attribute of the AllowedSigner node to the unique ID
            foreach ($AllowedSigner in $Item['AllowedSigner']) {
                $AllowedSigner.SetAttribute('SignerId', $Guid)
            }
        }

        foreach ($Item in $UniqueFilePublisherSigners131.Values) {

            # Create a unique ID for each signer
            [System.String]$Guid = [System.Guid]::NewGuid().ToString().replace('-', '').ToUpper()
            $Guid = "ID_SIGNER_A_$Guid"

            # Set the ID attribute of the Signer node to the unique ID
            foreach ($Signer in $Item['Signer']) {
                $Signer.SetAttribute('ID', $Guid)
            }

            # Set the SignerId attribute of the AllowedSigner node to the unique ID
            foreach ($AllowedSigner in $Item['AllowedSigner']) {
                $AllowedSigner.SetAttribute('SignerId', $Guid)
            }
        }

        # Clear the existing Signers node from any type of Signer
        [System.Xml.XmlElement]$SignersNode = $Xml.SelectSingleNode('//ns:Signers', $Ns)

        if ($null -ne $SignersNode) {
            $SignersNode.RemoveAll()
        }

        # Clear the existing AllowedSigners and CiSigners nodes from any type of Signer
        [System.Xml.XmlElement]$AllowedSigners12ToClear = $Xml.SelectSingleNode('//ns:SigningScenarios/ns:SigningScenario[@Value="12"]/ns:ProductSigners/ns:AllowedSigners', $Ns)
        [System.Xml.XmlElement]$AllowedSigners131ToClear = $Xml.SelectSingleNode('//ns:SigningScenarios/ns:SigningScenario[@Value="131"]/ns:ProductSigners/ns:AllowedSigners', $Ns)
        [System.Xml.XmlElement]$CiSignersToClear = $Xml.SelectSingleNode('//ns:CiSigners', $Ns)

        # Making sure the nodes are not null meaning they are not empty, before attempting to remove all of their elements
        if ($null -ne $AllowedSigners12ToClear) {
            $AllowedSigners12ToClear.RemoveAll()
        }

        if ($null -ne $AllowedSigners131ToClear) {
            $AllowedSigners131ToClear.RemoveAll()
        }

        if ($null -ne $CiSignersToClear) {
            $CiSignersToClear.RemoveAll()
        }

        # Repopulate the Signers, AllowedSigners and CiSigners nodes with the unique values

        # Add the unique FilePublisher signers for Signing Scenario 131 back to the Signers node
        foreach ($Signer in $UniqueFilePublisherSigners131.Values) {

            # index 0 is used because otherwise it would throw an error about array not being able to be appended to the XML
            # first index makes sure it is XmlElement type
            [System.Void]$SignersNode.AppendChild($Signer['Signer'][0])

            # A warning message to catch any edge cases that shouldn't happen
            if (($Signer['AllowedSigner'].SignerId | Select-Object -Unique).count -gt 1) {
                Write-Warning -Message "Multiple AllowedSigners found for FilePublisher signer for Signing Scenario 131 with ID $($Signer['Signer'][0].ID)."
            }

            [System.Void]$AllowedSigners131.AppendChild($Signer['AllowedSigner'].Count -gt 1 ? $Signer['AllowedSigner'][0] : $Signer['AllowedSigner'])
        }

        # Add the unique Publisher signers for Signing Scenario 131 back to the Signers node
        foreach ($Signer in $UniquePublisherSigners131.Values) {

            # A warning message to catch any edge cases that shouldn't happen
            if (($Signer['AllowedSigner'].SignerId | Select-Object -Unique).count -gt 1) {
                Write-Warning -Message "Multiple AllowedSigners found for Publisher signer for Signing Scenario 131 with ID $($Signer['Signer'][0].ID)."
            }

            # Add the <Signer> element to the <Signers> node
            [System.Void]$SignersNode.AppendChild($Signer['Signer'][0])

            # Add the <AllowedSigner> element to the <AllowedSigners> node
            [System.Void]$AllowedSigners131.AppendChild($Signer['AllowedSigner'].Count -gt 1 ? $Signer['AllowedSigner'][0] : $Signer['AllowedSigner'])
        }

        # Add the unique FilePublisher signers for Signing Scenario 12 back to the Signers node
        foreach ($Signer in $UniqueFilePublisherSigners12.Values) {

            # A warning message to catch any edge cases that shouldn't happen
            if (($Signer['AllowedSigner'].SignerId | Select-Object -Unique).count -gt 1) {
                Write-Warning -Message "Multiple AllowedSigners found for FilePublisher signer for Signing Scenario 12 with ID $($Signer['Signer'][0].ID)."
            }

            # A warning message to catch any edge cases that shouldn't happen
            if (($Signer['CiSigners'].SignerId | Select-Object -Unique).count -gt 1) {
                Write-Warning -Message "Multiple CiSigners found for FilePublisher signer for Signing Scenario 12 with ID $($Signer['Signer'][0].ID)."
            }

            # Add the <Signer> element to the <Signers> node
            [System.Void]$SignersNode.AppendChild($Signer['Signer'][0])

            # Add the <AllowedSigner> element to the <AllowedSigners> node
            [System.Void]$AllowedSigners12.AppendChild($Signer['AllowedSigner'].Count -gt 1 ? $Signer['AllowedSigner'][0] : $Signer['AllowedSigner'])

            if ($Null -ne $Signer['CiSigners']) {
                # Add the <CiSigner> element to the <CiSigners> node
                [System.Void]$CiSigners.AppendChild($Signer['CiSigners'].Count -gt 1 ? $Signer['CiSigners'][0] : $Signer['CiSigners'])
            }
        }

        # Add the unique Publisher signers for Signing Scenario 12 back to the Signers node
        foreach ($Signer in $UniquePublisherSigners12.Values) {

            # A warning message to catch any edge cases that shouldn't happen
            if (($Signer['AllowedSigner'].SignerId | Select-Object -Unique).count -gt 1) {
                Write-Warning -Message "Multiple AllowedSigners found for Publisher signer for Signing Scenario 12 with ID $($Signer['Signer'][0].ID)."
            }

            # A warning message to catch any edge cases that shouldn't happen
            if (($Signer['CiSigners'].SignerId | Select-Object -Unique).count -gt 1) {
                Write-Warning -Message "Multiple CiSigners found for Publisher signer for Signing Scenario 12 with ID $($Signer['Signer'][0].ID)."
            }

            # Add the <Signer> element to the <Signers> node
            [System.Void]$SignersNode.AppendChild($Signer['Signer'][0])

            # Add the <AllowedSigner> element to the <AllowedSigners> node
            [System.Void]$AllowedSigners12.AppendChild($Signer['AllowedSigner'].Count -gt 1 ? $Signer['AllowedSigner'][0] : $Signer['AllowedSigner'])

            if ($Null -ne $Signer['CiSigners']) {
                # Add the <CiSigner> element to the <CiSigners> node
                [System.Void]$CiSigners.AppendChild($Signer['CiSigners'].Count -gt 1 ? $Signer['CiSigners'][0] : $Signer['CiSigners'])
            }
        }

    }

    End {
        # Save the changes back to the XML file
        $Xml.Save($XmlFilePath)
    }
}
Export-ModuleMember -Function 'Merge-Signers_Semantic'