DnsBlockList.psm1

# initialize global variable for ini config
$Global:Ini = @{}

# initialize global variable for README
[string]$Global:DBLReadme = ''

# initialize global variable for DefaultConfig INI
[string]$Global:DBLDefaultConfig = ''

# populated with domains currently being blocked using the RuleNamePrefix
[System.Collections.Generic.HashSet[string]]$Global:CurrentBlockList = @()

# populated with domains from updated blocklists
[System.Collections.Generic.HashSet[string]]$Global:NewBlockList = @()

# stores configuration loaded from CacheConfig.xml
$Global:CacheConfig = @{}

# stores data from recent http requests and will be written to CacheConfig.xml
$Global:NewCache = @{}

# Configure https protocol.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Function Clear-DnsBlockListQRPManually
{
    <#
    .SYNOPSIS

    Removes all DnsBlockList QRPs from the registry.

    Warning: This cmdlet will stop the DNS service until QRPs are removed from the registry.

    .DESCRIPTION

    Removes all DnsBlockList QRPs using the following steps. This process is fast since the loaded QRPs do not need to be reindexed.

    Only QRPs using the DnsBlockList prefix will be removed.

        1) Stops the DNS service: Stop-Service DNS

        2) Removes policies in the following registry location using the DnsBlockList Prefix

            "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\DNS Server\Policies"

        3) Starts the DNS service: Start-Service DNS

    .OUTPUTS

    System.String[]
    #>


    [CmdletBinding()]
    param(
        [parameter(Mandatory=$true)]
        [switch]
        # Required parameter to proceed with removing all DnsBlockList QRPs.
        $Confirm
        ,
        [parameter(Mandatory=$false)]
        [string]
        # Required parameter if configuration file has not been loaded.
        $QrpNamePrefix
    )

    [string]$RemoveQrpPrefix = ''

    if($Confirm){

        if([System.String]::IsNullOrEmpty($QrpNamePrefix) -eq $false){

            $RemoveQrpPrefix = $QrpNamePrefix

        } elseif([System.String]::IsNullOrEmpty($Global:Ini.Script.RuleNamePrefix) -eq $false) {

            $RemoveQrpPrefix = $Global:Ini.Script.RuleNamePrefix

        } else {

            Write-Output '[Error] No QRP rule name prefix specified.'
        }

        if([System.String]::IsNullOrEmpty($RemoveQrpPrefix) -eq $false){

            if(Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\DNS Server\Policies'){

                try {

                        Stop-Service DNS -ErrorAction Stop
                        Push-Location -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\DNS Server\Policies' -ErrorAction Stop
                        Remove-Item -Path ".\$($RemoveQrpPrefix)*" -Recurse -Force -ErrorAction Stop
                        Pop-Location -ErrorAction Stop
                        Start-Service DNS -ErrorAction Stop

                } catch {

                    $e = $_
                    Write-Output $('[Error] Manually removing QRP with prefix {0}' -f $RemoveQrpPrefix)
                    Write-Output $('[Error] Exception: {0}' -f $e.Exception.Message)
                }

            } else {

                Write-Output '[Error] Registry location not available'
                Write-Output "[Error] Command: Test-Path `'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\DNS Server\Policies`'"
            }
        }
    }

} # End Function Clear-DnsBlockListQRPManually

Function Clear-DnsBlockListQRP
{
    <#
    .SYNOPSIS

    Removes all DnsBlockList QRPs using Remove-DnsServerQueryResolutionPolicy cmdlet.

    .DESCRIPTION

    Removes all DnsBlockList QRPs using Remove-DnsServerQueryResolutionPolicy cmdlet.

    Does NOT Start/Stop the DNS service.

    NOTE: if a large number of QRPs are being removed, this process will take some time since the QRP list must be re-indexed after each deletion.

    .OUTPUTS

    System.String[]
    #>


    [CmdletBinding()]
    param(
        [parameter(Mandatory=$true)]
        [switch]
        # Required parameter to proceed with removing all DnsBlockList QRPs.
        $Confirm
                ,
        [parameter(Mandatory=$false)]
        [string]
        # Required parameter if configuration file has not been loaded.
        $QrpNamePrefix
    )

    [string]$RemoveQrpPrefix = ''

    if($Confirm){

        if([System.String]::IsNullOrEmpty($QrpNamePrefix) -eq $false){

            $RemoveQrpPrefix = $QrpNamePrefix

        } elseif([System.String]::IsNullOrEmpty($Global:Ini.Script.RuleNamePrefix) -eq $false) {

            $RemoveQrpPrefix = $Global:Ini.Script.RuleNamePrefix

        } else {

            Write-Output '[Error] No QRP rule name prefix specified.'
        }

        if([System.String]::IsNullOrEmpty($RemoveQrpPrefix) -eq $false){

            $BlockListQRPs = Get-DnsServerQueryResolutionPolicy | Where-Object{ $_.Name.StartsWith($RemoveQrpPrefix) } | Sort-Object ProcessingOrder -Descending

            if($null -ne $BlockListQRPs){

                foreach($QRP in $BlockListQRPs){

                    try {

                        Remove-DnsServerQueryResolutionPolicy -Name $QRP.Name -Force -ErrorAction Stop

                    } catch {

                        $e = $_
                        Write-Output '[Error] Removing QRP {0}' -f $QRP.Name
                        Write-Output '[Error] Exception: {0}' -f $e.Exception.Message
                    }
                }

            } else {

                Write-Output '[Info] No QRP using the prefix: {0}' -f $RemoveQrpPrefix
            }
        }
    }

} # End Function Clear-DnsBlockListQRP

Function Get-BlockListDomainsFromFile
{
    <#
    .SYNOPSIS

    Parses the downloaded dns blocklist files.

    .DESCRIPTION

    Parses all of the downloaded dns blocklist files cached in the working directory. If a domain isn't whitelisted it will be added to the NewBlockList global variable.

    A valid line to be parsed must meet the following criteria:
        1) must not begin with a hash (#) sign
        2) must contain a period (.)
        3) must be longer than 2 characters in length

    .OUTPUTS

    Object
    #>


    [CmdletBinding()]
    param( )

    [string[]]$ErrorMsg = @()

    foreach($key in $Global:NewCache.Keys){

        [int]$ParseMethod = 0

        if($Global:Ini.ParseMethod.ContainsKey($key)){

            [int]$ParseMethod = $Global:Ini.ParseMethod[$key]
        }

        if([System.String]::IsNullOrEmpty($Global:NewCache[$key]['File']) -eq $false){

            if(Test-Path -LiteralPath $Global:NewCache[$key]['File']){

                try{

                    $ErrorActionPreference = 'Stop'

                    $FileContents = [System.IO.File]::ReadAllLines($Global:NewCache[$key]['File'])

                    if($FileContents.Count -gt 0){

                        Write-Verbose -Message "ParseMethod=$($ParseMethod) Key=$($key)"

                        [System.Collections.Generic.HashSet[string]]$FileBlockList = @{}

                        foreach($line in $FileContents){

                            if([System.String]::IsNullOrEmpty($line) -eq $false){

                                if($line.Trim().StartsWith('#') -eq $false -and `
                                   $line.Contains('.') -eq $true -and `
                                   $line.Length -gt 2){

                                        [string]$LineDomain = Get-DomainFromLine -ParseMethod $ParseMethod -ParseLine $line

                                        if($Global:Ini.AllowDomainsHash.ContainsKey($LineDomain) -eq $false){

                                            $FileBlockList.Add($LineDomain) | Out-Null
                                        }
                                }
                            }
                        }

                        if($FileBlockList.Count -gt 0){

                            $Global:NewBlockList.UnionWith($FileBlockList) | Out-Null
                        }
                    }

                } catch {

                    $e = $_
                    $ErrorMsg += $('[Error][Script] Key: {0}' -f $key)
                    $ErrorMsg += $('[Error][Script] File: {0}' -f $Global:NewCache[$key]['File'])
                    $ErrorMsg += $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
                }
            }
        }
    }

    [string[]]$BlockDomains = $Global:Ini.BlockDomainsHash.Keys

    $Global:NewBlockList.UnionWith($BlockDomains)

    return $ErrorMsg

} # End Function Get-BlockListDomainsFromFile

Function Write-DnsBlockListNewCache
{
    <#
    .SYNOPSIS

    Exports values in Global:NewCache to CacheConfig.xml

    .DESCRIPTION

    The Global:NewCache variable stores Last-Modified and ETag values from DnsBlockList requests. This xml is imported and it's values are used in future requests to only download updated files.
    #>


    [CmdletBinding()]
    param( )

    [string[]]$ErrorMsg = @()

    $CacheConfigPath = Join-Path -Path $Global:Ini.Script.WorkingDirectory -ChildPath 'CacheConfig.xml'

    try {

        Export-Clixml -LiteralPath $CacheConfigPath -InputObject $Global:NewCache -Force

    } catch {

        $e = $_
        $ErrorMsg += '[Error] Write-DnsBlockListRunningConfig {0}' -f $ActiveConfigPath
        $ErrorMsg += '[Error] Exception: {0}' -f $e.Exception.Message
    }

    return $ErrorMsg

} # End Function Write-DnsBlockListNewCache

Function Get-DnsBlockListChangeList
{
    <#
    .SYNOPSIS

    Compares the NewBlockList with CurrentBlockList to determine what domains to add/remove.

    .OUTPUTS

    System.Collections.Hashtable
    #>


    [CmdletBinding()]
    param( )

    $ChangeList = @{}
    $ChangeList.Add('Add',@())
    $ChangeList.Add('Remove',@())
    $ChangeList.Add('HasChanges',$false)

    [System.Collections.Generic.HashSet[string]]$FullList = @()

    $FullList.UnionWith($Global:CurrentBlockList)
    $FullList.UnionWith($Global:NewBlockList)

    if($Global:Ini.ContainsKey('BlockDomainsHash') -eq $true){

        if($Global:Ini.BlockDomainsHash.Keys.Count -gt 0){

            [string[]]$k = $Global:Ini.BlockDomainsHash.Keys

            $FullList.UnionWith($k) | Out-Null
        }
    }

    foreach($domain in $FullList){

        $OnCurrentList = $Global:CurrentBlockList.Contains($domain)
        $OnNewList = $Global:NewBlockList.Contains($domain)

        if($OnNewList -eq $true -and $OnCurrentList -eq $false){

            if($Global:Ini.AllowDomainsHash.ContainsKey($domain) -eq $false){

                $ChangeList['Add'] += $domain
            }
        }

        if($OnNewList -eq $false -and $OnCurrentList -eq $true){

            $ChangeList['Remove'] += $domain

        }

        if($OnNewList -eq $false -and $OnCurrentList -eq $false){

            if($Global:Ini.AllowDomainsHash.ContainsKey($domain) -eq $false){

                $ChangeList['Add'] += $domain
            }
        }
    }

    if($ChangeList['Add'].Count -gt 0 -or $ChangeList['Remove'].Count -gt 0){

        $ChangeList['HasChanges'] = $true
    }

    return $ChangeList

} # End Function Get-DnsBlockListChangeList

Function Update-DnsBlockListGit
{
    <#
    .SYNOPSIS

    Updates git initiated working directory.

    .DESCRIPTION

    Runs the following GIT commands in the working directory:

        git add -A
        git commit -m "DnsBlockList-PowerShell-Module"
        git push origin master

    .OUTPUTS

    System.Boolean
    #>


    [CmdletBinding()]
    param( )

    [string[]]$ErrorMsg = @()

    try{

        Push-Location -Path $Global:Ini.Script.WorkingDirectory

        $null = & git add -A
        $null = & git commit -m "DnsBlockList-PowerShell-Module"
        $null = & git push origin master

        Pop-Location

    } catch {

        $e = $_
        $ErrorMsg =+ '[Error][Script] Failed to update git.'
        $ErrorMsg =+ '[Error][Script] Exception: {0}' -f $e.Exception.Message
    }

    return $ErrorMsg

} # End Function Update-DnsBlockListGit

Function Get-CurrentQRPDomainBlockList
{
    <#
    .SYNOPSIS

    Parses all Query Resolution Policies (QRP) using the RuleNamePrefix and populates the current DomainBlockList global variable.

    .OUTPUTS

    System.String[]
    #>


    [CmdletBinding()]
    param( )

    [string[]]$ErrorMsg = @()

    try{

        $CurrentQRPs = Get-DnsServerQueryResolutionPolicy | Where-Object{ $_.Name.ToString().StartsWith($Global:Ini.Script.RuleNamePrefix) }

        if($null -ne $CurrentQRPs){

            foreach($QRP in $CurrentQRPs){

                $QrpFqdn = Get-DomainFromCriteria -QRPCriteria $QRP.Criteria.Criteria

                $Global:CurrentBlockList.Add($QrpFqdn) | Out-Null
            }
        }

    } catch {

        $e = $_
        $ErrorMsg += $('[Error][Script] Failed to get current list of QRPs using: RuleNamePrefix = {0}' -f $Global:Ini.Script.RuleNamePrefix)
        $ErrorMsg += $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
    }

    return $ErrorMsg

} # End Function Get-CurrentQRPDomainBlockList

Function Get-DomainFromCriteria
{
    <#
    .SYNOPSIS

    Extracts the domain from the QRP criteria.

    .INPUTS

    System.String

    .OUTPUTS

    System.String
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [string]
            # QRP criteria
            $QRPCriteria
    )

    $entry = $QRPCriteria.Split(',')[-1]

    if($entry.StartsWith('*')){ $entry = $entry.Substring(1,$entry.Length - 1) }

    if($entry.StartsWith('.')){ $entry = $entry.Substring(1,$entry.Length - 1) }

    if($entry.EndsWith('.')){ $entry = $entry.Substring(0,$entry.Length - 1) }

    return $entry.Trim().ToLower()

} # End Function Get-DomainFromCriteria

Function Get-DnsBlockListQrpName
{
    <#
    .SYNOPSIS

    Returns the QRP name for a DnsBlockList domain.

    .INPUTS

    System.String

    .OUTPUTS

    System.String
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [string]
            # domain name to build QRP from
            $DomainName
    )

    $QRPName = '{0}{1}' -f $Global:Ini.Script.RuleNamePrefix,$DomainName

    if($QRPName.Length -gt 255){

        $QRPName = $QRPName.Substring(0,255)
    }

    Return $QRPName

} # End Function Get-DnsBlockListQrpName

Function Get-QrpNamesToRemove
{
    <#
    .SYNOPSIS

    Gets the QRP names to be removed ordered by descending ProcessingOrder. Removing QRPs in this order optimizes performance since QRPs with a higher ProcessingOrder must be reindexed.

    .INPUTS

    System.String[]

    .OUTPUTS

    System.String[]
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [string[]]
            # List of domains with QRPs to remove
            $DomainNameList
    )

    [string[]]$QrpNameList = $DomainNameList | ForEach-Object{ Get-DnsBlockListQrpName -DomainName $_ }

    [System.Collections.Generic.HashSet[string]]$RemoveHashSet = @()

    $RemoveHashSet.UnionWith($QrpNameList)

    [string[]]$RemoveQRPNames = Get-DnsServerQueryResolutionPolicy | Where-Object{ $RemoveHashSet.Contains($_.Name) } | Sort-Object ProcessingOrder -Descending | ForEach-Object{ $_.Name }

    return $RemoveQRPNames

} # End Function Get-QrpNamesToRemove

Function Get-DnsBlockListQRPCommands
{
    <#
    .SYNOPSIS

    Processes the ChangeList hashset to produce QRP Add/Remove commands. If ReadOnly=False, the command will be launched.

    .INPUTS

    System.Collections.Hashtable

    .OUTPUTS

    System.Collections.Hashtable
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [System.Collections.Hashtable]
            # Hashset containing Add/Remove domains
            $ChangeList
    )

    $ReturnMsg = @{}
    $ReturnMsg.Add('ChangeCommands',@())
    $ReturnMsg.Add('ErrorMsg',@())
    $ReturnMsg.Add('HasError',$false)

    [string[]]$QrpChangeCommands = @('Import-Module DnsServer')

    if($ChangeList['Remove'].Count -gt 0){

        [string[]]$QrpNamesRemove = Get-QrpNamesToRemove -DomainNameList $ChangeList['Remove']

        foreach($QRP in $QrpNamesRemove){

            $QrpChangeCommands += "Remove-DnsServerQueryResolutionPolicy -Name `'$QRP`' -Force"

            if($Global:Ini.Script.ReadOnly -eq $false){

                try {

                    Remove-DnsServerQueryResolutionPolicy -Name $QRP -Force

                } catch {

                    $e = $_
                    $ReturnMsg.HasError = $true
                    $ReturnMsg.ErrorMsg += '[Error][Script] Remove-DnsServerQueryResolutionPolicy -Name {0} -Force' -f $QRP
                    $ReturnMsg.ErrorMsg += '[Error][Script] Exception: {0}' -f $e.Exception.Message
                }
            }
        }
    }

    if($ChangeList['Add'].Count -gt 0){

        foreach($Domain in $ChangeList['Add']){

            $QrpName = Get-DnsBlockListQrpName -DomainName $Domain

            $QrpChangeCommands += "Add-DnsServerQueryResolutionPolicy -Name `'$QrpName`' -Action $($Global:Ini.Script.DnsResponse) -Fqdn `'EQ,$($Domain)`'"

            if($Global:Ini.Script.ReadOnly -eq $false){

                try{

                    Add-DnsServerQueryResolutionPolicy -Name $QrpName -Action $Global:Ini.Script.DnsResponse -Fqdn "EQ,$($Domain)"

                } catch {

                    $e = $_
                    $ReturnMsg.HasError = $true
                    $ReturnMsg.ErrorMsg += '[Error][Script] Add-DnsServerQueryResolutionPolicy -Name {0} -Action {1} -Fqdn "EQ,{2}"' -f $QrpName,$Global:Ini.Script.DnsResponse,$Domain
                    $ReturnMsg.ErrorMsg += '[Error][Script] Exception: {0}' -f $e.Exception.Message
                }
            }
        }
    }

    $ReturnMsg.ChangeCommands = $QrpChangeCommands

    return $ReturnMsg

} # End Function Get-DnsBlockListQRPCommands

Function Write-DnsBlockListQRPCommands
{
    <#
    .SYNOPSIS

    Writes the QRP Add/Remove commands to Update-DnsServerQRP.ps1

    .INPUTS

    System.String[]

    .OUTPUTS

    System.String[]
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [System.String[]]
            # String array containing QRP change commands.
            $ChangeCommands
    )

    [string[]]$ErrorMsg = @()

    if($ChangeCommands.Count -gt 1){

        try{

            [System.IO.File]::WriteAllLines($Global:Ini.Script.ChangeCmdFile, $ChangeCommands, [System.Text.Encoding]::ASCII) | Out-Null

        } catch {

            $e = $_
            $ErrorMsg += '[Error][Script] Failed writing change commands to {0}' -f $Global:Ini.Script.ChangeCmdFile
            $ErrorMsg += '[Error][Script] Exception: {0}' -f $e.Exception.Message
        }
    }

    return $ErrorMsg

} # End Function Write-DnsBlockListQRPCommands

Function Invoke-DnsBlockList
{
    <#
    .SYNOPSIS

    Launches DnsBlockList PowerShell module which was created to automate the aggregation of DNS blocklists for the creation of Query Resolution Policies (QRP) on a Microsoft DNS server.

    .DESCRIPTION

    The DnsBlockList module first reads and validates the provided configuration file. Then the Query Resolution Policies (QRP) are processed to get domains currently loaded. Next, each [BlockListUrl] is queried for changes and updated files are downloaded to the [WorkingDirectory] as a .DnsBlockListCache file. All files are processed and the aggregated list of domains get compared with those currently loaded to produce the add/remove commands in Update-DnsServerQRP.ps1.

    .INPUTS

    System.String

    .OUTPUTS

    System.String[]

    #>


    [CmdletBinding()]
    [OutputType('System.String[]')]
    param(
            [parameter(Mandatory=$true)]
            [ValidateScript({Test-Path -LiteralPath $_})]
            [string]
            # Path to configuration file.
            $Configuration
    )

    # Reset variables
    [System.Collections.Generic.HashSet[string]]$Global:CurrentBlockList = @()
    [System.Collections.Generic.HashSet[string]]$Global:NewBlockList = @()
    $Global:CacheConfig = @{}
    $Global:NewCache = @{}

    $Global:Ini = Get-IniConfig -Path $Configuration

    [bool]$ConfigFileError = Confirm-DnsBlockListSettings

    if($ConfigFileError -eq $false){

        $n = 0

        [string[]]$ReturnMsg = @()

        do{

            $n++

            if($ReturnMsg.Count -gt 0){

                $n = 1000
            }

            Write-Verbose -Message "Invoke-DnsBlockList: n=$($n)"

            switch($n)
            {
                1 {
                        $ReturnMsg = Get-CurrentQRPDomainBlockList
                }
                2 {
                        $ReturnMsg = Get-DnsBlockListWebFiles
                }
                3 {
                        $ReturnMsg = Get-BlockListDomainsFromFile
                }
                4 {
                        $ChangeObj = Get-DnsBlockListChangeList

                        Publish-QRPStats -Changes $ChangeObj

                        if($ChangeObj.HasChanges){

                            $CmdObj = Get-DnsBlockListQRPCommands -ChangeList $ChangeObj

                            if($CmdObj.HasError -eq $false){

                                if($CmdObj.ChangeCommands.Count -gt 1){

                                    $ReturnMsg += Write-DnsBlockListQRPCommands -ChangeCommands $CmdObj.ChangeCommands
                                }

                            } else {

                                $ReturnMsg += $CmdObj.ErrorMsg
                            }
                        }
                }
                5 {
                        $ReturnMsg = Write-DnsBlockListNewCache
                }
                6 {
                        if($Global:Ini.Script.UpdateGit){

                            $ReturnMsg = Update-DnsBlockListGit
                        }
                }
                default {

                        if($ReturnMsg.Count -gt 0){

                            $Global:Ini['Script']['HasError'] = $true

                            [string[]]$ReturnMsg = @('[Error][Script] Terminating error.') + $ReturnMsg

                            $Global:Ini['Script'].Add('ErrorMsg',$ReturnMsg)

                            $ReturnMsg | ForEach-Object{ Write-Output $_ }

                            Submit-Alert -Message $ReturnMsg -HasError

                        } else {

                            $Global:Ini['Script']['HasError'] = $false

                            $Global:Ini.Script.Stats | ForEach-Object{ Write-Output $_ }

                            Submit-Alert -Message $Global:Ini.Script.Stats
                        }

                        $n = 0
                }
            }

        } while($n -gt 0)

    } else {

        $Global:Ini.Script.ErrorMsg | ForEach-Object{ Write-Output $_ }

        Submit-Alert -Message $Global:Ini.Script.ErrorMsg -HasError
    }

} # End Function Invoke-DnsBlockList

Function Submit-Alert
{
    <#
    .SYNOPSIS

    Submits alert to Event Log or via SMTP.
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [System.String[]]
            # Message body.
            $Message
            ,
            [parameter(Mandatory=$false)]
            [switch]
            # Signifies an error.
            $HasError
    )

    if($Global:Ini.Alert.Method -match "^(?i)(.*)WinEvent(.*)$"){

        if($HasError){

            Write-EventAlert -Message $Message -HasError

        } else {

            Write-EventAlert -Message $Message
        }
    }

    if($Global:Ini.Alert.Method -match "^(?i)(.*)Smtp(.*)$"){

        if($HasError){

            Send-SmtpAlert -Message $Message -HasError

        } else {

            Send-SmtpAlert -Message $Message
        }
    }

} # End Function Submit-Alert

Function Write-EventAlert
{
    <#
    .SYNOPSIS

    Writes alert to windows event log.
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [System.String[]]
            # Message for alert.
            $Message
            ,
            [parameter(Mandatory=$false)]
            [switch]
            # Signifies an error.
            $HasError
    )

    $EventEntryType = 'Information'
    $EventId = $Global:Ini.WinEvent.InfoEventId

    if($HasError){

        $EventEntryType = 'Error'
        $EventId = $Global:Ini.WinEvent.ErrorEventId
    }

    try {

            Write-EventLog -LogName $Global:Ini.WinEvent.Logname `
                           -Source $Global:Ini.WinEvent.Source `
                           -EntryType $EventEntryType `
                           -EventId $EventId `
                           -Message $($Message -Join [System.Environment]::NewLine) `
                           -ErrorAction Stop

    } catch {

        $e = $_
        Write-Output '[Error][Script] Event log write failed.'
        Write-Output $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
    }


} # End Function Write-EventAlert

Function Send-SmtpAlert
{
    <#
    .SYNOPSIS

    Sends alert via SMTP.
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [System.String[]]
            # Message body.
            $Message
            ,
            [parameter(Mandatory=$false)]
            [switch]
            # Signifies an error
            $HasError
    )

    $EmailSubject = $Global:Ini.Smtp.Subject

    if($HasError){

        $EmailSubject = '[Error] {0}' -f $EmailSubject
    }

    try {

        if([System.String]::IsNullOrEmpty($Global:Ini.Smtp.CredentialXml)){

            Send-MailMessage -To $Global:Ini.Smtp.To `
                             -From $Global:Ini.Smtp.From `
                             -Subject $EmailSubject `
                             -SmtpServer $Global:Ini.Smtp.Server `
                             -Port $Global:Ini.Smtp.Port `
                             -Body $($Message -Join [System.Environment]::NewLine) `
                             -UseSsl `
                             -ErrorAction Stop

        } else {

            $creds = Import-Clixml -LiteralPath $Global:Ini.Smtp.CredentialXml

            Send-MailMessage -To $Global:Ini.Smtp.To `
                             -From $Global:Ini.Smtp.From `
                             -Subject $EmailSubject `
                             -SmtpServer $Global:Ini.Smtp.Server `
                             -Port $Global:Ini.Smtp.Port `
                             -Body $($Message -Join [System.Environment]::NewLine) `
                             -Credential $creds `
                             -UseSsl `
                             -ErrorAction Stop

            Remove-Variable -Name creds
        }

    } catch {

        $e = $_
        Write-Output '[Error][Script] Smtp send failed.'
        Write-Output $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
    }

} # End Function Send-SmtpAlert

Function Get-IniConfig
{
    <#
    .SYNOPSIS

    Parses the configuration file into a hashtable.

    .INPUTS

    System.String

    .OUTPUTS

    System.Collections.Hashtable

    .LINK

    https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to-work-with-any-ini-file/
    #>


    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]
    param(
            [parameter(Mandatory=$true)]
            [ValidateScript({Test-Path -LiteralPath $_})]
            [string]
            # Path to configuration file.
            $Path
    )

    $config = @{}

    switch -regex -file $Path
    {
        "^.*\[(.+)\].*$" # Section
        {
            $section = $matches[1]

            if($section.ToString().Trim().StartsWith('#') -eq $false){

                $config.Add($section.Trim(),@{})
            }
        }

        "(.+?)\s*=(.*)" # Key
        {
            $name,$value = $matches[1..2]

            if($name.ToString().Trim().StartsWith('#') -eq $false){

                $config[$section].Add($name.Trim(), $value.Trim())
            }
        }
    }

    if([System.String]::IsNullOrEmpty($config['Script'])){

        $config.Add('Script',@{})
    }

    $config['Script'].Add('ConfigPath',(Get-Item $Path).FullName)

    $config['Script'].Add('StartTS', (Get-Date).ToString('yyyy-MM-dd HH:mm:ss'))

    return $config

} # End Function Get-IniConfig

Function Confirm-DnsBlockListSettings
{
    <#
    .SYNOPSIS

    Validates DnsBlockList settings.

    .OUTPUTS

    System.Boolean
    #>


    [CmdletBinding()]
    param( )

    $i = 0

    [string[]]$ErrorMsg = @()

    do{

        $i++

        if($ErrorMsg.Count -gt 0){

            $i = 1000
        }

        Write-Verbose -Message "Confirm-DnsBlockListSettings: i=$($i)"

        switch($i)
        {
            1 {     # BEGIN validate [INI]

                    try {

                        $foo = Get-Variable -Name Ini -Scope Global

                    } catch {

                        $ErrorMsg += '[Error] No configuration file.'
                    }

                    if($ErrorMsg.Count -eq 0){

                        [string[]]$ConfigSections = 'Script','BlockListUrl','AllowDomains','BlockDomains','ParseMethod','Alert','Smtp','WinEvent'

                        foreach($section in $ConfigSections){

                            if($Global:Ini.ContainsKey($section) -eq $false){

                                $Global:Ini.Add($section,@{})

                            } else {

                                if($Global:Ini[$section].GetType().Name -ne 'Hashtable'){

                                    $ErrorMsg += $('[Error][Script] unknown type: {0}' -f $section)
                                }
                            }
                        }
                    }

            }       # END validate [INI]

            2 {     # BEGIN validate [Script] RuleNamePrefix

                    if($Global:Ini.Script.ContainsKey('RuleNamePrefix') -eq $true){

                        if([System.String]::IsNullOrEmpty($Global:Ini.Script.RuleNamePrefix)){

                            $ErrorMsg += '[Error][Script] RuleNamePrefix not specified.'
                        }

                    } else {

                        $ErrorMsg += '[Error][Script] RuleNamePrefix not specified.'
                    }

            }       # END validate [Script] RuleNamePrefix

            3 {     # BEGIN validate [Script] DnsResponse

                    if($Global:Ini.Script.ContainsKey('DnsResponse') -eq $true){

                        if([System.String]::IsNullOrEmpty($Global:Ini.Script.DnsResponse) -eq $false){

                            if($Global:Ini.Script.DnsResponse -notmatch "^(?i)(Allow|Deny|Ignore)$"){

                                $ErrorMsg += '[Error][Script] DnsResponse not valid.'
                            }

                        } else {

                            $ErrorMsg += '[Error][Script] DnsResponse not specified.'
                        }

                    } else {

                        $ErrorMsg += '[Error][Script] DnsResponse not specified.'
                    }

            }       # END validate [Script] DnsResponse

            4 {     # BEGIN validate [Script] WorkingDirectory

                    if($Global:Ini.Script.ContainsKey('WorkingDirectory') -eq $true){

                        if([System.String]::IsNullOrEmpty($Global:Ini.Script.WorkingDirectory) -eq $false){

                            try {

                                $WDItem = Get-Item -LiteralPath $Global:Ini.Script.WorkingDirectory -ErrorAction Stop

                                if($WDItem.PSIsContainer -eq $false){

                                    $ErrorMsg += '[Error][Script] WorkingDirectory not valid.'
                                }

                            } catch {

                                $e = $_
                                $ErrorMsg += '[Error][Script] WorkingDirectory not valid.'
                                $ErrorMsg += $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
                            }

                            if($ErrorMsg.Count -eq 0){

                                try {

                                    $TestFile = New-Item -Path $Global:Ini.Script.WorkingDirectory -Name "$(([System.Guid]::NewGuid()).Guid).test" -Type File
                                    Remove-Item $TestFile

                                } catch {

                                    $e = $_
                                    $ErrorMsg += '[Error][Script] WorkingDirectory failed to create/delete test file.'
                                    $ErrorMsg += $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
                                }
                            }

                        } else {

                            $ErrorMsg += '[Error][Script] WorkingDirectory not specified.'
                        }

                    } else {

                        $ErrorMsg += '[Error][Script] WorkingDirectory not specified.'
                    }

            }       # END validate [Script] WorkingDirectory

            5 {     # BEGIN validate [Script] ReadOnly

                    if($Global:Ini.Script.ContainsKey('ReadOnly') -eq $true){

                        if([System.String]::IsNullOrEmpty($Global:Ini.Script.ReadOnly) -eq $false){

                            if($Global:Ini.Script.ReadOnly -match "^(?i)(false)$"){

                                $Global:Ini.Script.ReadOnly = $false

                            } else {

                                $Global:Ini.Script.ReadOnly = $true
                            }

                        } else {

                            $Global:Ini.Script.ReadOnly = $true
                        }

                    } else {

                        $Global:Ini.Script.Add('ReadOnly',$true)
                    }

            }       # END validate [Script] ReadOnly

            6 {     # BEGIN validate [Script] UpdateGit

                    if($Global:Ini.Script.ContainsKey('UpdateGit') -eq $true){

                        if([System.String]::IsNullOrEmpty($Global:Ini.Script.UpdateGit) -eq $false){

                            if($Global:Ini.Script.UpdateGit -match "^(?i)(true)$"){

                                $Global:Ini.Script.UpdateGit = $true

                            } else {

                                $Global:Ini.Script.UpdateGit = $false
                            }

                        } else {

                            $Global:Ini.Script.UpdateGit = $false
                        }

                    } else {

                        $Global:Ini.Script.Add('UpdateGit',$false)
                    }

            }       # END validate [Script] UpdateGit

            7 {     # BEGIN validate [BlockListUrl]

                    if($Global:Ini.BlockListUrl.Count -gt 0){

                        $Keys = $Global:Ini.BlockListUrl.Keys | Sort-Object

                        foreach($key in $Keys){

                            if($ErrorMsg.Count -eq 0){

                                try{

                                    [System.Uri]$URI = [System.Uri]$($Global:Ini.BlockListUrl[$key])

                                } catch {

                                    $e = $_

                                    $ErrorMsg += $('[Error][BlockListUrl] {0} not valid.' -f $Global:Ini.BlockListUrl[$key])
                                    $ErrorMsg += $('[Error][BlockListUrl] {0} = {1}' -f $key,$Global:Ini.BlockListUrl[$key])
                                    $ErrorMsg += $('[Error][BlockListUrl] Exception: {0}' -f $e.Exception.Message)
                                }
                            }
                        }
                    }

            }       # END validate [BlockListUrl]

            8 {     # BEGIN validate [AllowDomains]

                    if($Global:Ini.AllowDomains.Count -gt 0){

                        $Keys = $Global:Ini.AllowDomains.Keys | Sort-Object

                        $AllowDomainsHash = @{}

                        foreach($key in $Keys){

                            if($ErrorMsg.Count -eq 0){

                                try{

                                    [System.Uri]$URI = [System.Uri]$('http://{0}/' -f $Global:Ini.AllowDomains[$key])

                                    if($AllowDomainsHash.ContainsKey($Global:Ini.AllowDomains[$key]) -eq $false){

                                        $AllowDomainsHash.Add($Global:Ini.AllowDomains[$key],1)
                                    }

                                } catch {

                                    $e = $_
                                    $ErrorMsg += $('[Error][AllowDomains] {0} not valid.' -f $Global:Ini.AllowDomains[$key])
                                    $ErrorMsg += $('[Error][AllowDomains] {0} = {1}' -f $key,$Global:Ini.AllowDomains[$key])
                                    $ErrorMsg += $('[Error][AllowDomains] Exception: {0}' -f $e.Exception.Message)
                                }
                            }
                        }

                        if($ErrorMsg.Count -eq 0){

                            $Global:Ini.Add('AllowDomainsHash',$AllowDomainsHash)
                        }

                    } else {

                        $Global:Ini.Add('AllowDomainsHash',@{})
                    }

            }       # END validate [AllowDomains]

            9 {     # BEGIN validate [BlockDomains]

                    if($Global:Ini.BlockDomains.Count -gt 0){

                        $Keys = $Global:Ini.BlockDomains.Keys | Sort-Object

                        $BlockDomainsHash = @{}

                        foreach($key in $Keys){

                            if($ErrorMsg.Count -eq 0){

                                try{

                                    [System.Uri]$URI = [System.Uri]$('http://{0}/' -f $Global:Ini.BlockDomains[$key])

                                    if($BlockDomainsHash.ContainsKey($Global:Ini.BlockDomains[$key]) -eq $false){

                                        if($Global:Ini.AllowDomainsHash.ContainsKey($Global:Ini.BlockDomains[$key]) -eq $false){

                                            $BlockDomainsHash.Add($Global:Ini.BlockDomains[$key],1)
                                        }
                                    }

                                } catch {

                                    $e = $_
                                    $ErrorMsg += $('[Error][BlockDomains] {0} not valid.' -f $Global:Ini.BlockDomains[$key])
                                    $ErrorMsg += $('[Error][BlockDomains] {0} = {1}' -f $key,$Global:Ini.BlockDomains[$key])
                                    $ErrorMsg += $('[Error][BlockDomains] {0}' -f $e.Exception.Message)
                                }
                            }
                        }

                        if($ErrorMsg.Count -eq 0){

                            $Global:Ini.Add('BlockDomainsHash',$BlockDomainsHash)
                        }

                    } else {

                        $Global:Ini.Add('BlockDomainsHash',@{})
                    }

            }       # END validate [BlockDomains]

            10 {    # BEGIN validate [ParseMethod]

                    if($Global:Ini.ParseMethod.Count -gt 0){

                        foreach($key in $Global:Ini.ParseMethod.Keys){

                            [int]$DefaultParse = 0

                            if([System.Int32]::TryParse($Global:Ini.ParseMethod[$key], [ref]$DefaultParse)){

                                if($DefaultParse -le 0){

                                    $ErrorMsg += $('[Error][ParseMethod] {0} Value must be a number greater than zero.' -f $Global:Ini.ParseMethod[$key])
                                }
                            }

                            if($Global:Ini.BlockListUrl.ContainsKey($key) -eq $false){

                                $ErrorMsg += $('[Error][ParseMethod] {0} must correspond to a BlockListUrl key.' -f $key)
                            }
                        }
                    }

            }        # END validate [ParseMethod]

            11 {    # BEGIN validate Eventlog source

                    try{

                        if([System.Diagnostics.EventLog]::SourceExists('DnsBlockList') -eq $false){

                            $ErrorMsg += '[Error][Script] DnsBlockList event source does not exist.'
                            $ErrorMsg += '[Error][Script] Run the following command as Administrator:'
                            $ErrorMsg += ' New-EventLog -LogName Application -Source DnsBlockList'
                        }

                    } catch {

                        $e = $_
                        $ErrorMsg += '[Error][WinEvent] Source does not exist.'
                        $ErrorMsg += '[Error][WinEvent] Exception: {0}' -f $e.Exception.Message
                    }

            }        # END validate Eventlog source

            12 {    # BEGIN validate DNS service

                    if((Get-Service -Name "DNS").Status -ne 'Running'){

                        $ErrorMsg += '[Error][Script] DNS Server Service is not Running or DNS role is not installed.'
                    }

            }        # END validate DNS service

            13 {    # BEGIN DNS Server Management Module

                    if($null -eq $(Get-Module -Name DnsServer)){

                        $ErrorMsg += '[Error][Script] Failed to load DNS Server Management Module (DnsServer).'
                    }

            }        # END DNS Server Management Module

            14 {    # BEGIN validate Cache config import

                    $CacheConfigPath = Join-Path -Path $Global:Ini.Script.WorkingDirectory -ChildPath 'CacheConfig.xml'

                    if(Test-Path -LiteralPath $CacheConfigPath){

                        try {

                            $Global:CacheConfig = Import-Clixml -LiteralPath $CacheConfigPath

                        } catch {

                            $ErrorMsg += '[Error][Script] Failed to import {0}' -f $CacheConfigPath
                        }
                    }

            }        # END validate Cache config import

            15 {    # BEGIN validate Cache Config variable

                    foreach($key in $Global:Ini.BlockListUrl.Keys){

                        if($Global:CacheConfig.ContainsKey($key) -eq $true -and $Global:Ini.BlockListUrl.ContainsKey($key) -eq $true){

                            if($Global:CacheConfig[$key].Url -ne $Global:Ini.BlockListUrl[$key]){

                                if(Test-Path -LiteralPath $Global:CacheConfig[$key].File){

                                    Remove-Item -LiteralPath $Global:CacheConfig[$key].File
                                }

                                $Global:CacheConfig[$key].File = ''
                                $Global:CacheConfig[$key].Url = ''
                                $Global:CacheConfig[$key].etag = ''
                                $Global:CacheConfig[$key].LastModified = ''
                            }
                        }
                    }

            }        # END validate Cache Config variable

            16 {    # BEGIN validate Git

                    if($Global:Ini.Script.UpdateGit){

                        try {

                            Push-Location -LiteralPath $Global:Ini.Script.WorkingDirectory

                            & git --version | Out-Null

                            Pop-Location

                        } catch {

                            $e = $_
                            $ErrorMsg += '[Error][Script] git install verification failed.'
                            $ErrorMsg += $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
                        }
                    }

            }        # END validate Git

            17 {    # BEGIN validate change cmd file

                    $ChangeCmdFile = Join-Path -Path $Global:Ini.Script.WorkingDirectory -ChildPath 'Update-DnsServerQRP.ps1'

                    if(Test-Path -LiteralPath $ChangeCmdFile){

                        Clear-Content -LiteralPath $ChangeCmdFile

                    } else {

                        New-Item -Path $ChangeCmdFile -Type File | Out-Null
                    }

                    if($Global:Ini.Script.ContainsKey('ChangeCmdFile') -eq $false){

                        $Global:Ini.Script.Add('ChangeCmdFile',$ChangeCmdFile)

                    } else {

                        $Global:Ini.Script.ChangeCmdFile = $ChangeCmdFile
                    }

            }        # END validate change cmd file

            18 {    # BEGIN validate [Alert] Method

                    if($Global:Ini.Alert.ContainsKey('Method') -eq $true){

                        if([System.String]::IsNullOrEmpty($Global:Ini.Alert.Method) -eq $false){

                            if($Global:Ini.Alert.Method -notmatch "^(?i)Smtp|WinEvent|stdout$"){

                                $ErrorMsg += '[Error][Alert] Method not valid.'
                            }

                        } else {

                            $Global:Ini.Alert.Method = 'stdout'
                        }

                    } else {

                        $Global:Ini.Alert.Add('Method','stdout')
                    }

            }       # END validate [Alert] Method

            19 {    # BEGIN validate [SMTP] To

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)Smtp(.*)$"){

                        if($Global:Ini.Smtp.ContainsKey('To') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.Smtp.To) -eq $false){

                                try {

                                    $smtpTo = [System.Net.Mail.MailAddress]::New($Global:Ini.Smtp.To)

                                } catch {

                                    $ErrorMsg += '[Error][SMTP] TO not valid.'
                                }

                            } else {

                                $ErrorMsg += '[Error][SMTP] TO not specified.'
                            }

                        } else {

                            $ErrorMsg += '[Error][SMTP] TO not specified.'
                        }
                    }

            }       # END validate [SMTP] To

            20 {    # BEGIN validate [SMTP] From

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)Smtp(.*)$"){

                        if($Global:Ini.Smtp.ContainsKey('From') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.Smtp.From) -eq $false){

                                try {

                                    $smtpFrom = [System.Net.Mail.MailAddress]::new($Global:Ini.Smtp.From)

                                } catch {

                                    $ErrorMsg += '[Error][SMTP] FROM not valid.'
                                }

                            } else {

                                $ErrorMsg += '[Error][SMTP] FROM not specified.'
                            }

                        } else {

                            $ErrorMsg += '[Error][SMTP] FROM not specified.'
                        }
                    }

            }       # END validate [SMTP] From

            21 {    # BEGIN validate [SMTP] Subject

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)Smtp(.*)$"){

                        if($Global:Ini.Smtp.ContainsKey('Subject') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.Smtp.Subject) -eq $false){

                                try {

                                    $msg = [System.Net.Mail.MailMessage]::new()
                                    $msg.Subject = $Global:Ini.Smtp.Subject
                                    $msg.Dispose()
                                    Remove-Variable -Name msg

                                } catch {

                                    $ErrorMsg += '[Error][SMTP] SUBJECT not valid.'
                                }

                            } else {

                                $ErrorMsg += '[Error][SMTP] SUBJECT not specified.'
                            }

                        } else {

                            $ErrorMsg += '[Error][SMTP] SUBJECT not specified.'
                        }
                    }

            }       # END validate [SMTP] Subject

            22 {    # BEGIN validate [SMTP] Port

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)Smtp(.*)$"){

                        if($Global:Ini.Smtp.ContainsKey('Port') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.Smtp.Port) -eq $false){

                                [int]$intPort = 0

                                if([System.Int32]::TryParse($Global:Ini.Smtp.Port, [ref]$intPort)){

                                    if($intPort -gt 0){

                                        [int]$Global:Ini.Smtp.Port = $intPort

                                    } else {

                                        $ErrorMsg += '[Error][SMTP] PORT must be a positive number.'
                                    }

                                } else {

                                    $ErrorMsg += '[Error][SMTP] PORT must be a positive number.'
                                }

                            } else {

                                $ErrorMsg += '[Error][SMTP] PORT not specified.'
                            }

                        } else {

                            $ErrorMsg += '[Error][SMTP] PORT not specified.'
                        }
                    }

            }       # END validate [SMTP] Port

            23 {    # BEGIN validate [SMTP] Server

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)Smtp(.*)$"){

                        if($Global:Ini.Smtp.ContainsKey('Server') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.Smtp.Server) -eq $false){

                                try {

                                    $smtpServer = New-Object System.Net.Sockets.TcpClient($Global:Ini.Smtp.Server, $Global:Ini.Smtp.Port)

                                    if($smtpServer.Connected -eq $false){

                                        $ErrorMsg += '[Error][SMTP] TCP connection failed to {0}:{1}' -f $Global:Ini.Smtp.Server,$Global:Ini.Smtp.Port
                                    }

                                    $smtpServer.Dispose()

                                    Remove-Variable -Name smtpServer

                                } catch {

                                    $e = $_
                                    $ErrorMsg += '[Error][SMTP] TCP connection failed to {0}:{1}' -f $Global:Ini.Smtp.Server,$Global:Ini.Smtp.Port
                                    $ErrorMsg += $('[Error][SMTP] Exception: {0}' -f $e.Exception.Message)
                                }

                            } else {

                                $ErrorMsg += '[Error][SMTP] SERVER not specified.'
                            }

                        } else {

                            $ErrorMsg += '[Error][SMTP] SERVER not specified.'
                        }
                    }

            }       # END validate [SMTP] Server

            24 {    # BEGIN validate [SMTP] CredentialXml

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)Smtp(.*)$"){

                        if($Global:Ini.Smtp.ContainsKey('CredentialXml') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.Smtp.CredentialXml) -eq $false){

                                try {

                                    $credFile = Get-Item -LiteralPath $Global:Ini.Smtp.CredentialXml -ErrorAction Stop

                                    $credCheck = Import-Clixml -LiteralPath $credFile.FullName

                                    if($credCheck.GetType().Name -ne 'PSCredential'){

                                        $ErrorMsg += '[Error][SMTP] CredentialXml import failed.'
                                    }

                                    Remove-Variable -Name credCheck,credFile

                                } catch {

                                    $ErrorMsg += '[Error][SMTP] CredentialXml import failed.'
                                }
                            }
                        }
                    }

            }       # END validate [SMTP] CredentialXml

            25 {    # BEGIN validate [WinEvent] Logname

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)WinEvent(.*)$"){

                        if($Global:Ini.WinEvent.ContainsKey('Logname') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.WinEvent.Logname) -eq $false){

                                try {

                                    Get-WinEvent -LogName $Global:Ini.WinEvent.Logname -MaxEvents 1 -ErrorAction Stop | Out-Null

                                } catch {

                                    $ErrorMsg += '[Error][WinEvent] no Logname {0}' -f $Global:Ini.WinEvent.Logname
                                }

                            } else {

                                $ErrorMsg += '[Error][WinEvent] Logname not specified.'
                            }

                        } else {

                            $ErrorMsg += '[Error][WinEvent] Logname not specified.'
                        }
                    }

            }       # END validate [WinEvent] Logname

            26 {    # BEGIN validate [WinEvent] Source

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)WinEvent(.*)$"){

                        if($Global:Ini.WinEvent.ContainsKey('Source') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.WinEvent.Source) -eq $false){

                                try{

                                    $result = [System.Diagnostics.EventLog]::SourceExists($Global:Ini.WinEvent.Source)

                                    if ($result -eq $false){

                                        $ErrorMsg += '[Error][WinEvent] Source does not exist.'
                                    }

                                } catch {

                                    $e = $_
                                    $ErrorMsg += '[Error][WinEvent] Source does not exist.'
                                    $ErrorMsg += '[Error][WinEvent] Exception: {0}' -f $e.Exception.Message
                                }

                            } else {

                                $ErrorMsg += '[Error][WinEvent] Source not specified.'
                            }

                        } else {

                            $ErrorMsg += '[Error][WinEvent] Source not specified.'
                        }
                    }

            }       # END validate [WinEvent] Source

            27 {    # BEGIN validate [WinEvent] InfoEventId

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)WinEvent(.*)$"){

                        if($Global:Ini.WinEvent.ContainsKey('InfoEventId') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.WinEvent.InfoEventId) -eq $false){

                                [int]$intInfoEventId = 0

                                if([System.Int32]::TryParse($Global:Ini.WinEvent.InfoEventId, [ref]$intInfoEventId)){

                                    if($intInfoEventId -gt 0){

                                        [int]$Global:Ini.WinEvent.InfoEventId = $intInfoEventId

                                    } else {

                                        $ErrorMsg += '[Error][WinEvent] InfoEventId must be a positive number.'
                                    }

                                } else {

                                    $ErrorMsg += '[Error][WinEvent] InfoEventId must be a positive number.'
                                }

                            } else {

                                $ErrorMsg += '[Error][WinEvent] InfoEventId not specified.'
                            }

                        } else {

                            $ErrorMsg += '[Error][WinEvent] InfoEventId not specified.'
                        }
                    }

            }       # END validate [WinEvent] InfoEventId

            28 {    # BEGIN validate [WinEvent] ErrorEventId

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)WinEvent(.*)$"){

                        if($Global:Ini.WinEvent.ContainsKey('ErrorEventId') -eq $true){

                            if([System.String]::IsNullOrEmpty($Global:Ini.WinEvent.ErrorEventId) -eq $false){

                                [int]$intErrorEventId = 0

                                if([System.Int32]::TryParse($Global:Ini.WinEvent.ErrorEventId, [ref]$intErrorEventId)){

                                    if($intErrorEventId -gt 0){

                                        [int]$Global:Ini.WinEvent.ErrorEventId = $intErrorEventId

                                    } else {

                                        $ErrorMsg += '[Error][WinEvent] ErrorEventId must be a positive number.'
                                    }

                                } else {

                                    $ErrorMsg += '[Error][WinEvent] ErrorEventId must be a positive number.'
                                }

                            } else {

                                $ErrorMsg += '[Error][WinEvent] ErrorEventId not specified.'
                            }

                        } else {

                            $ErrorMsg += '[Error][WinEvent] ErrorEventId not specified.'
                        }
                    }

            }       # END validate [WinEvent] ErrorEventId

            29 {    # BEGIN validate [WinEvent] InfoEventId and ErrorEventId different

                    if($Global:Ini.Alert.Method -match "^(?i)(.*)WinEvent(.*)$"){

                        if($Global:Ini.WinEvent.InfoEventId -eq $Global:Ini.WinEvent.ErrorEventId){

                            $ErrorMsg += '[Error][WinEvent] InfoEventId and ErrorEventId must different.'
                        }
                    }

            }       # END validate [WinEvent] InfoEventId and ErrorEventId different

            30 {    # BEGIN cleanup DnsBlockListCache files

                    $CacheFiles = Get-ChildItem -Path $Global:Ini.Script.WorkingDirectory | Where-Object{ $_.Extension -eq '.DnsBlockListCache' }

                    if($null -ne $CacheFiles){

                        foreach($file in $CacheFiles){

                            Write-Verbose -Message "CachedFile=$($file.FullName)"

                            if($Global:Ini.BlockListUrl.ContainsKey($file.BaseName) -eq $false){

                                Remove-Item -LiteralPath $file.FullName

                                Write-Verbose -Message " Removed=$($file.FullName)"

                                if($Global:CacheConfig.ContainsKey($key) -eq $true){

                                    $Global:CacheConfig[$key].File = ''
                                    $Global:CacheConfig[$key].Url = ''
                                    $Global:CacheConfig[$key].etag = ''
                                    $Global:CacheConfig[$key].LastModified = ''
                                }
                            }
                        }
                    }

            }       # END cleanup DnsBlockListCache files

            default {

                if($Global:Ini['Script'].ContainsKey('HasError') -eq $false){

                    $Global:Ini['Script'].Add('HasError',$false)
                }

                if($ErrorMsg.Count -gt 0){

                    $Global:Ini['Script']['HasError'] = $true

                    $ErrorMsg = @('[Error][Script] Terminating error.') + $ErrorMsg

                    if($Global:Ini['Script'].ContainsKey('ErrorMsg') -eq $false){

                        $Global:Ini['Script'].Add('ErrorMsg',$ErrorMsg)

                    } else {

                        $Global:Ini['Script']['ErrorMsg'] = $ErrorMsg
                    }

                } else {

                    $Global:Ini['Script']['HasError'] = $false
                }

                $i = 0
            }
        }

    } while($i -gt 0)

    return $Global:Ini['Script']['HasError']

} # End Function Confirm-DnsBlockListSettings

Function Publish-QRPStats
{
    <#
    .SYNOPSIS

    Gets the QRP stats for alerts.

    .INPUTS

    System.Collections.Hashtable
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [System.Collections.Hashtable]
            # Change object
            $Changes
    )

    [string[]]$StatMsg = @()
    [string[]]$AllQrpNames = @()
    [string[]]$DblQrp = @()

    [string[]]$AllQrpNames = Get-DnsServerQueryResolutionPolicy | ForEach-Object{ $_.Name }

    if($AllQrpNames.Count -gt 0){

        [string[]]$DblQrpRules = $AllQrpNames | Where-Object{ $_.StartsWith($Global:Ini.Script.RuleNamePrefix) }

        if($null -ne $DblQrpRules){

            if($DblQrpRules.Count -gt 0){

                [string[]]$DblQrp = $DblQrpRules
            }
        }
    }

    $StatMsg += 'QRP-Total = {0}' -f $AllQrpNames.Count
    $StatMsg += 'QRP-DnsBlockList = {0}' -f $DblQrp.count

    if($Changes.HasChanges -eq $true){

        $StatMsg += 'HasChanges = True'
        $StatMsg += 'Removed = {0}' -f $Changes.Remove.Count
        $StatMsg += 'Added = {0}' -f $Changes.Add.Count

    } else {

        $StatMsg += 'HasChanges = False'
        $StatMsg += 'Removed = 0'
        $StatMsg += 'Added = 0'
    }

    $StatMsg += 'ReadOnly = {0}' -f $Global:Ini.Script.ReadOnly
    $StatMsg += 'UpdateGit = {0}' -f $Global:Ini.Script.UpdateGit

    if($Global:Ini.Script.ContainsKey('Stats')){

        $Global:Ini.Script.Stats = $StatMsg

    } else {

        $Global:Ini.Script.Add('Stats',$StatMsg)
    }

} # End Function Publish-QRPStats

Function Get-DnsBlockListWebHeaders
{
    <#
    .SYNOPSIS

    Gets the HTTP request headers for the BlockListUrl.

    .DESCRIPTION

    Builds an HTTP request header hashset using Last-Modified and ETag values from the prior request so only updated content is downloaded.

    .INPUTS

    System.String

    .OUTPUTS

    System.Collections.Hashtable
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [string]
            # Key used to specify the target DNS blocklist.
            $BlockListKey
    )

    $RequestHeaders = @{ 'User-Agent' = 'Mozilla/5.0 (Windows NT; Windows NT 10.0;)' }

    if($Global:CacheConfig.ContainsKey($BlockListKey) -eq $true){

        if([System.String]::IsNullOrEmpty($Global:CacheConfig[$BlockListKey]['LastModified']) -eq $false){

            $RequestHeaders.Add('If-Modified-Since',$Global:CacheConfig[$BlockListKey]['LastModified'])
        }

        if([System.String]::IsNullOrEmpty($Global:CacheConfig[$BlockListKey]['etag']) -eq $false){

            $RequestHeaders.Add('If-None-Match',$Global:CacheConfig[$BlockListKey]['etag'])
        }
    }

    return $RequestHeaders

} # End Function Get-DnsBlockListWebHeaders

Function Get-DnsBlockListWebFiles
{
    <#
    .SYNOPSIS

    Sends HTTP GET using custom headers to BlockListUrl.

    .DESCRIPTION

    Only downloads files that have changed by using Last-Modified timestamps and ETag values.

    .OUTPUTS

    System.Boolean
    #>


    [CmdletBinding()]
    param( )

    [string[]]$ErrorMsg = @()

    foreach($key in $Global:Ini.BlockListUrl.Keys){

        $SaveFilePath = Join-Path -Path $Global:Ini.Script.WorkingDirectory -ChildPath "$($key).DnsBlockListCache"

        $ClientRequestHeaders = Get-DnsBlockListWebHeaders -BlockList $key

        $Global:NewCache.Add($key,@{})
        $Global:NewCache[$key].Add('Url',$Global:Ini.BlockListUrl[$key])
        $Global:NewCache[$key].Add('etag','')
        $Global:NewCache[$key].Add('LastModified','')
        $Global:NewCache[$key].Add('File','')
        $Global:NewCache[$key].Add('ResponseCode','ERROR')

        try{

            $HttpResponse = Invoke-WebRequest -Uri $Global:Ini.BlockListUrl[$key] -Headers $ClientRequestHeaders -UseBasicParsing -ErrorAction Stop

            $Global:NewCache[$key]['ResponseCode'] = [int]$HttpResponse.StatusCode

            $Global:NewCache[$key]['File'] = $SaveFilePath

            Out-File -LiteralPath $Global:NewCache[$key]['File'] -InputObject $HttpResponse.Content -ErrorAction Stop

            Write-Verbose -Message "HTTP_Response=200 Key=$($key)"

            if($HttpResponse.Headers.ContainsKey('ETag')){

                if([System.String]::IsNullOrEmpty($HttpResponse.Headers['ETag']) -eq $false){

                    $Global:NewCache[$key]['etag'] = $HttpResponse.Headers['ETag']
                }
            }

            if($HttpResponse.Headers.ContainsKey('Last-Modified')){

                if([System.String]::IsNullOrEmpty($HttpResponse.Headers['Last-Modified']) -eq $false){

                    $Global:NewCache[$key]['LastModified'] = $HttpResponse.Headers['Last-Modified']
                }
            }

        } catch [System.Net.WebException] {

            $e = $_

            if($e.Exception.Message -match "^.*\([0-9]{3}\).*$"){

                $Global:NewCache[$key]['ResponseCode'] = [int]([regex]::Match($e.Exception.Message,'[0-9]{3}')).Value

                switch($Global:NewCache[$key]['ResponseCode']){

                    304 {   # (304) Not Modified.

                            Write-Verbose -Message "HTTP_Response=304 Key=$($key)"

                            $Global:NewCache[$key]['File'] = $SaveFilePath

                            if($ClientRequestHeaders.ContainsKey('If-Modified-Since')){

                                if([System.String]::IsNullOrEmpty($ClientRequestHeaders['If-Modified-Since']) -eq $false){

                                    $Global:NewCache[$key]['LastModified'] = $ClientRequestHeaders['If-Modified-Since']
                                }
                            }

                            if($ClientRequestHeaders.ContainsKey('If-None-Match')){

                                if([System.String]::IsNullOrEmpty($ClientRequestHeaders['If-None-Match']) -eq $false){

                                    $Global:NewCache[$key]['etag'] = $ClientRequestHeaders['If-None-Match']
                                }
                            }

                    }       # (304) Not Modified.

                    default {

                            $ErrorMsg += $('[Error][Http] Processing: {0} = {1}' -f $key,$Global:Ini.BlockListUrl[$key])
                            $ErrorMsg += $('[Error][Http] Exception: {0}' -f $e.Exception.Message)
                    }
                }

            } else {

                $ErrorMsg += $('[Error][Http] Processing: {0} = {1}' -f $key,$Global:Ini.BlockListUrl[$key])
                $ErrorMsg += $('[Error][Http] Exception: {0}' -f $e.Exception.Message)
            }

        } catch {

            $e = $_
            $ErrorMsg += $('[Error][Http] Processing: {0} = {1}' -f $key,$Global:Ini.BlockListUrl[$key])
            $ErrorMsg += $('[Error][Http] Exception: {0}' -f $e.Exception.Message)
        }
    }

    return $ErrorMsg

} # End Function Get-DnsBlockListWebFiles

Function Set-ReadMeAndIniPath
{
    <#
    .SYNOPSIS

    Set path for README and default configuration file in global variables.
    #>


    [CmdletBinding()]
    param( )

    if([System.String]::IsNullOrEmpty($Global:Ini['Script'])){

        $Global:Ini.Add('Script',@{})
    }

    $ReadMePath = Join-Path -Path $PSScriptRoot -ChildPath 'README.md'

    $Global:DBLReadme = $ReadMePath

    $DefaultConfigPath = Join-Path -Path $PSScriptRoot -ChildPath 'DnsBlockList.ini'

    $Global:DBLDefaultConfig = $DefaultConfigPath

} # End Function Set-ReadMeAndIniPath

Function Get-DnsBlockListREADME
{
    <#
    .SYNOPSIS

    Returns the DnsBlockList README file (README.md) to standard out.
    #>


    [CmdletBinding()]
    param( )

    try {

        $ReadMeFile = Get-Item -LiteralPath $Global:DBLReadme -ErrorAction Stop

        Get-Content -LiteralPath $ReadMeFile.FullName | ForEach-Object{ Write-Output $_ }

    } catch {

        $e = $_
        Write-Output $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
    }

} # End Function Get-DnsBlockListREADME

Function Copy-DnsBlockListREADME
{
    <#
    .SYNOPSIS

    Copies the DnsBlockList README file (README.md) to the destination folder.
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [ValidateScript({Test-Path -LiteralPath $_ -PathType Container})]
            [string]
            # Destination folder to copy README.md
            $DestinationFolder
    )

    try {

        $ReadMeFile = Get-Item -LiteralPath $Global:DBLReadme -ErrorAction Stop

        Copy-Item -Path $ReadMeFile.FullName -Destination $DestinationFolder

    } catch {

        $e = $_
        Write-Output $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
    }

} # End Function Copy-DnsBlockListReadme

Function Get-DnsBlockListDefaultConfiguration
{
    <#
    .SYNOPSIS

    Returns the DnsBlockList default configuration file to standard out.
    #>


    [CmdletBinding()]
    param( )

    try {

        $ConfigFile = Get-Item -LiteralPath $Global:DBLDefaultConfig -ErrorAction Stop

        Get-Content -LiteralPath $ConfigFile.FullName | ForEach-Object{ Write-Output $_ }

    } catch {

        $e = $_
        Write-Output $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
    }

} # End Function Get-DnsBlockListDefaultConfiguration

Function Copy-DnsBlockListDefaultConfiguration
{
    <#
    .SYNOPSIS

    Copies the DnsBlockList default configuration file to the destination folder.
    #>


    [CmdletBinding()]
    param(
            [parameter(Mandatory=$true)]
            [ValidateScript({Test-Path -LiteralPath $_ -PathType Container})]
            [string]
            # Destination folder to copy DnsBlockList.ini
            $DestinationFolder
    )

    try {

        $ConfigFile = Get-Item -LiteralPath $Global:DBLDefaultConfig -ErrorAction Stop

        Copy-Item -Path $ConfigFile.FullName -Destination $DestinationFolder

    } catch {

        $e = $_
        Write-Output $('[Error][Script] Exception: {0}' -f $e.Exception.Message)
    }

} # End Function Copy-DnsBlockListDefaultConfiguration

Function Get-DomainFromLine
{
    <#
    .SYNOPSIS

    Extracts the domain name from the provided line of text.

    .DESCRIPTION

    This function will parse the provided line of text according to the ParseMethod defined in the configuration file.

    When a file needs an alternate parsing method, add the appropriate logic to the switch statement in this function. Then update the configuration file ParseMethod to reflect the switch value.

    .OUTPUTS

    System.String
    #>


    [CmdletBinding()]
    param(
        [parameter(Mandatory=$true)]
        [int]
        # ParseMethod value for DnsBlockList
        $ParseMethod
        ,
        [parameter(Mandatory=$true)]
        [string]
        # Line of text to parse.
        $ParseLine
    )

    switch ($ParseMethod) {

        1 { # malwaredomains

            return ($ParseLine.Trim().Split("`t")[0]).Trim().ToLower()
        }

        Default { # Default Parse is one domain per line

            return $ParseLine.Trim().ToLower()
        }
    }

} # End Function Get-DomainFromLine

Set-ReadMeAndIniPath