Deploy-SignedWDACConfig.psm1

#Requires -RunAsAdministrator
function Deploy-SignedWDACConfig {
    [CmdletBinding(
        SupportsShouldProcess = $true,
        PositionalBinding = $false,
        ConfirmImpact = 'High'
    )]
    Param(
        [ValidatePattern('\.xml$')]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' }, ErrorMessage = 'The path you selected is not a file path.')]
        [parameter(Mandatory = $true)][System.String[]]$PolicyPaths,

        [Parameter(Mandatory = $false)][Switch]$Deploy,
    
        [ValidatePattern('\.cer$')]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' }, ErrorMessage = 'The path you selected is not a file path.')]
        [parameter(Mandatory = $false)][System.String]$CertPath,

        [ValidateScript({
                $certs = foreach ($cert in (Get-ChildItem 'Cert:\CurrentUser\my')) {
                    (($cert.Subject -split ',' | Select-Object -First 1) -replace 'CN=', '').Trim()
                } 
                $certs -contains $_
            }, ErrorMessage = "A certificate with the provided common name doesn't exist in the personal store of the user certificates." )]
        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)][System.String]$CertCN,

        [parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [System.String]$SignToolPath,
        
        [Parameter(Mandatory = $false)][Switch]$SkipVersionCheck
    )

    begin {
        # Importing resources such as functions by dot-sourcing so that they will run in the same scope and their variables will be usable
        . "$psscriptroot\Resources.ps1"
        
        # Stop operation as soon as there is an error anywhere, unless explicitly specified otherwise
        $ErrorActionPreference = 'Stop'        
        if (-NOT $SkipVersionCheck) { . Update-self }

        # Detecting if Debug switch is used, will do debugging actions based on that
        $Debug = $PSBoundParameters.Debug.IsPresent

        # Fetch User account directory path
        [string]$global:UserAccountDirectoryPath = (Get-CimInstance Win32_UserProfile -Filter "SID = '$([System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value)'").LocalPath

        #region User-Configurations-Processing-Validation
        # If any of these parameters, that are mandatory for all of the position 0 parameters, isn't supplied by user
        if (!$SignToolPath -or !$CertPath -or !$CertCN) {
            # Read User configuration file if it exists
            $UserConfig = Get-Content -Path "$global:UserAccountDirectoryPath\.WDACConfig\UserConfigurations.json" -ErrorAction SilentlyContinue   
            if ($UserConfig) {
                # Validate the Json file and read its content to make sure it's not corrupted
                try { $UserConfig = $UserConfig | ConvertFrom-Json }
                catch {            
                    Write-Error 'User Configuration Json file is corrupted, deleting it...' -ErrorAction Continue
                    # Calling this function with this parameter automatically does its job and breaks/stops the operation
                    Set-CommonWDACConfig -DeleteUserConfig         
                }                
            }
        }

        # Get SignToolPath from user parameter or user config file or auto-detect it
        if ($SignToolPath) {
            $SignToolPathFinal = Get-SignTool -SignToolExePath $SignToolPath
        } # If it is null, then Get-SignTool will behave the same as if it was called without any arguments.
        else {
            $SignToolPathFinal = Get-SignTool -SignToolExePath ($UserConfig.SignToolCustomPath ?? $null)
        }    
                
        # If CertPath parameter wasn't provided by user
        if (!$CertPath) {
            if ($UserConfig.CertificatePath) {
                # validate user config values for Certificate Path
                if (Test-Path $($UserConfig.CertificatePath)) {
                    # If the user config values are correct then use them
                    $CertPath = $UserConfig.CertificatePath
                }            
                else {
                    throw 'The currently saved value for CertPath in user configurations is invalid.'
                }
            }
            else {
                throw "CertPath parameter can't be empty and no valid configuration was found for it."
            }
        }
                        
        # If CertCN was not provided by user
        if (!$CertCN) {
            if ($UserConfig.CertificateCommonName) {
                # Check if the value in the User configuration file exists and is valid
                if (Confirm-CertCN $($UserConfig.CertificateCommonName)) {
                    # if it's valid then use it
                    $CertCN = $UserConfig.CertificateCommonName
                }
                else {
                    throw 'The currently saved value for CertCN in user configurations is invalid.'
                }
            }
            else {
                throw "CertCN parameter can't be empty and no valid configuration was found for it."
            }
        }
        #endregion User-Configurations-Processing-Validation
    }

    process {        
        foreach ($PolicyPath in $PolicyPaths) {          
            
            # Gather policy details
            $xml = [xml](Get-Content $PolicyPath)
            [System.String]$PolicyType = $xml.SiPolicy.PolicyType
            [System.String]$PolicyID = $xml.SiPolicy.PolicyID
            [System.String]$PolicyName = ($xml.SiPolicy.Settings.Setting | Where-Object { $_.provider -eq 'PolicyInfo' -and $_.valuename -eq 'Name' -and $_.key -eq 'Information' }).value.string
            [System.String[]]$PolicyRuleOptions = $xml.SiPolicy.Rules.Rule.Option

            # Remove the .CIP file of the same policy being signed and deployed if any in the current working directory
            Remove-Item -Path ".\$PolicyID.cip" -ErrorAction SilentlyContinue
            
            # Ensure -Supplemental is not used when the policy type is supplemental
            if ($PolicyType -eq 'Supplemental Policy') {
                # Make sure -User is not added if the UMCI policy rule option doesn't exist in the policy, typically for Strict kernel mode policies
                if ('Enabled:UMCI' -in $PolicyRuleOptions) {          
                    Add-SignerRule -FilePath $PolicyPath -CertificatePath $CertPath -Update -User -Kernel
                }
                else {
                    Add-SignerRule -FilePath $PolicyPath -CertificatePath $CertPath -Update -Kernel
                }
            }
            else {
                # Make sure -User is not added if the UMCI policy rule option doesn't exist in the policy, typically for Strict kernel mode policies
                if ('Enabled:UMCI' -in $PolicyRuleOptions) {            
                    Add-SignerRule -FilePath $PolicyPath -CertificatePath $CertPath -Update -User -Kernel -Supplemental
                }
                else {
                    Add-SignerRule -FilePath $PolicyPath -CertificatePath $CertPath -Update -Kernel -Supplemental
                }
            }
            Set-HVCIOptions -Strict -FilePath $PolicyPath
            Set-RuleOption -FilePath $PolicyPath -Option 6 -Delete
            ConvertFrom-CIPolicy $PolicyPath "$PolicyID.cip" | Out-Null            

            # Configure the parameter splat
            $ProcessParams = @{
                'ArgumentList' = 'sign', '/v' , '/n', "`"$CertCN`"", '/p7', '.', '/p7co', '1.3.6.1.4.1.311.79.1', '/fd', 'certHash', ".\$PolicyID.cip"
                'FilePath'     = $SignToolPathFinal         
                'NoNewWindow'  = $true
                'Wait'         = $true
                'ErrorAction'  = 'Stop'
            }
            # Hide the SignTool.exe's normal output unless -Debug parameter was used
            if (!$Debug) { $ProcessParams['RedirectStandardOutput'] = 'NUL' } 
            # Sign the files with the specified cert
            Start-Process @ProcessParams

            Remove-Item ".\$PolicyID.cip" -Force            
            Rename-Item "$PolicyID.cip.p7" -NewName "$PolicyID.cip" -Force

            if ($Deploy) {

                CiTool --update-policy ".\$PolicyID.cip" -json | Out-Null
                Write-Host "`npolicy with the following details has been Signed and Deployed in Enforced Mode:" -ForegroundColor Green        
                Write-Output "PolicyName = $PolicyName"
                Write-Output "PolicyGUID = $PolicyID`n"
                Remove-Item -Path ".\$PolicyID.cip" -Force

                #region Detecting Strict Kernel mode policy and removing it from User Configs
                if ('Enabled:UMCI' -notin $PolicyRuleOptions) {

                    [System.String]$StrictKernelPolicyGUID = Get-CommonWDACConfig -StrictKernelPolicyGUID
                    [System.String]$StrictKernelNoFlightRootsPolicyGUID = Get-CommonWDACConfig -StrictKernelNoFlightRootsPolicyGUID
                    
                    if (($PolicyName -like '*Strict Kernel mode policy Enforced*')) {
                        if ($StrictKernelPolicyGUID) {                            
                            if ($($PolicyID.TrimStart('{').TrimEnd('}')) -eq $StrictKernelPolicyGUID) {
                                Remove-CommonWDACConfig -StrictKernelPolicyGUID | Out-Null
                            }                            
                        }
                    }
                    
                    elseif (($PolicyName -like '*Strict Kernel No Flights mode policy Enforced*')) {
                        if ($StrictKernelNoFlightRootsPolicyGUID) {                           
                            if ($($PolicyID.TrimStart('{').TrimEnd('}')) -eq $StrictKernelNoFlightRootsPolicyGUID) {
                                Remove-CommonWDACConfig -StrictKernelNoFlightRootsPolicyGUID | Out-Null
                            }                            
                        }
                    }
                }
                #endregion Detecting Strict Kernel mode policy and removing it from User Configs
           
                # Show the question only for base policies. Don't show it for Strict kernel mode policies
                if (($PolicyType -ne 'Supplemental Policy') -and ($PolicyName -notlike '*Strict Kernel*')) {

                    # Ask user question about whether or not to add the Signed policy xml file to the User Config Json for easier usage later
                    $userInput = ''
                    while ($userInput -notin 1, 2) {
                        $userInput = $(Write-Host 'Add the Signed policy xml file path just created to the User Configurations? Please enter 1 to Confirm or 2 to Skip.' -ForegroundColor Cyan ; Read-Host) 
                        if ($userInput -eq 1) {
                            Set-CommonWDACConfig -SignedPolicyPath $PolicyPath
                            &$WriteHotPink "Added $PolicyPath to the User Configuration file."             
                        }
                        elseif ($userInput -eq 2) {                    
                            &$WritePink 'Skipping...'                  
                        }
                        else {
                            Write-Warning 'Invalid input. Please enter 1 or 2 only.'
                        }               
                    }
                }
            }

            else {            
                Write-Host "`npolicy with the following details has been Signed and is ready for deployment:" -ForegroundColor Green
                Write-Output "PolicyName = $PolicyName"
                Write-Output "PolicyGUID = $PolicyID`n"
            }
        }
    }

    <#
.SYNOPSIS
Signs and Deploys WDAC policies, accepts signed or unsigned policies and deploys them
 
.LINK
https://github.com/HotCakeX/Harden-Windows-Security/wiki/Deploy-SignedWDACConfig
 
.DESCRIPTION
Using official Microsoft methods, Signs and Deploys WDAC policies, accepts signed or unsigned policies and deploys them (Windows Defender Application Control)
 
.COMPONENT
Windows Defender Application Control, ConfigCI PowerShell module
 
.FUNCTIONALITY
Using official Microsoft methods, Signs and Deploys WDAC policies, accepts signed or unsigned policies and deploys them (Windows Defender Application Control)
 
.PARAMETER CertPath
Path to the certificate .cer file
 
.PARAMETER PolicyPaths
Path to the policy xml files that are going to be signed
 
.PARAMETER CertCN
Certificate common name
 
.PARAMETER SignToolPath
Path to the SignTool.exe - optional parameter
 
.PARAMETER Deploy
Indicates that the cmdlet will deploy the signed policy on the current system
 
.PARAMETER SkipVersionCheck
Can be used with any parameter to bypass the online version check - only to be used in rare cases
 
#>

}

# Importing argument completer ScriptBlocks
. "$psscriptroot\ArgumentCompleters.ps1"
# Set PSReadline tab completion to complete menu for easier access to available parameters - Only for the current session
Set-PSReadLineKeyHandler -Key Tab -Function MenuComplete
Register-ArgumentCompleter -CommandName 'Deploy-SignedWDACConfig' -ParameterName 'CertCN' -ScriptBlock $ArgumentCompleterCertificateCN
Register-ArgumentCompleter -CommandName 'Deploy-SignedWDACConfig' -ParameterName 'PolicyPaths' -ScriptBlock $ArgumentCompleterPolicyPaths
Register-ArgumentCompleter -CommandName 'Deploy-SignedWDACConfig' -ParameterName 'CertPath' -ScriptBlock $ArgumentCompleterCerFilePathsPicker
Register-ArgumentCompleter -CommandName 'Deploy-SignedWDACConfig' -ParameterName 'SignToolPath' -ScriptBlock $ArgumentCompleterExeFilePathsPicker