Resources/WebsiteFailedLogins.config.psm1

Import-Module $(Join-Path -Path $PSScriptRoot -ChildPath 'WebsiteFailedLogins.lp.psm1')

Function Assert-ValidIniConfig
{
    <#
        .SYNOPSIS
 
            Validates settings in the configuration file.
    #>

    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]
    param(
            [Parameter(Mandatory=$true)]
            [System.Collections.Hashtable]
            # INI Configuration.
            $IniConfig
            ,
            [Parameter(Mandatory=$false)]
            [switch]
            # Perform basic checks.
            $RunningConfig
    )

    Write-Verbose -Message '[Assert-ValidIniConfig] Validating configuration file settings.'
    $i = 0

    $returnValue = @{
                        'ErrorMessages' = @()
                        'HasError'      = $false
                        'Configuration' = @{}
                    }

    [int[]] $minimumChecks = 1,7,10,11

    do {

        $i++

        if ($returnValue.ErrorMessages.Count -gt 0)
        {
            Write-Verbose -Message "[Assert-ValidIniConfig] Error at #$($i - 1)"
            $i = 1000
        }

        if ($RunningConfig)
        {
            if ($minimumChecks.Contains($i) -eq $false)
            {
                do {

                    $i++

                } until($minimumChecks.Contains($i) -eq $true -or $i -gt 19)
            }
        }

        Write-Verbose -Message "[Assert-ValidIniConfig] Check #$($i)"

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

                    if ($IniConfig.Count -le 1)
                    {
                        $returnValue.ErrorMessages += '[Error][Config] No configuration file.'
                    }

            }       # END validate [INI]

            2 {     # BEGIN validate [Website] FriendlyName

                    if ([System.String]::IsNullOrEmpty($IniConfig.Website.FriendlyName) -eq $false)
                    {
                        if ($IniConfig.Website.FriendlyName -notmatch "^[a-zA-Z0-9-_\. ]{1,50}$")
                        {
                            $returnValue.ErrorMessages += '[Error][Config][Website] FriendlyName not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][Website] FriendlyName not specified.'
                    }

            }       # END validate [Website] FriendlyName

            3 {     # BEGIN validate [Website] Sitename

                    if ([System.String]::IsNullOrEmpty($IniConfig.Website.Sitename) -eq $false)
                    {
                        if ($IniConfig.Website.Sitename -notmatch "^(?i)(w3svc)[0-9]{1,6}$")
                        {
                            $returnValue.ErrorMessages += '[Error][Config][Website] Sitename not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][Website] Sitename not specified.'
                    }

            }       # END validate [Website] Sitename

            4 {     # BEGIN validate [Website] Authentication

                    if ([System.String]::IsNullOrEmpty($IniConfig.Website.Authentication) -eq $false)
                    {
                        if ($IniConfig.Website.Authentication -notmatch "^(?i)(Forms|Basic|Windows)$")
                        {
                            $returnValue.ErrorMessages += '[Error][Config][Website] Authentication not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][Website] Authentication not specified.'
                    }

            }       # END validate [Website] Authentication

            5 {     # BEGIN validate [Website] HttpResponse

                    if ([System.String]::IsNullOrEmpty($IniConfig.Website.HttpResponse) -eq $false)
                    {
                        if ($IniConfig.Website.HttpResponse -notmatch "^[0-9]{3}$")
                        {
                            $returnValue.ErrorMessages += '[Error][Config][Website] HttpResponse not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][Website] HttpResponse not specified.'
                    }

            }       # END validate [Website] HttpResponse

            6 {     # BEGIN validate [Website] UrlPath

                    if ($IniConfig.Website.Authentication -imatch 'Forms')
                    {
                        if ([System.String]::IsNullOrEmpty($IniConfig.Website.UrlPath) -eq $false)
                        {
                            try {

                                $null = [System.Uri]$('https://www.domain.com{0}' -f $IniConfig.Website.UrlPath)

                            } catch {

                                $returnValue.ErrorMessages += '[Error][Config][Website] UrlPath not valid.'
                            }

                        } else {

                            $returnValue.ErrorMessages += '[Error][Config][Website] UrlPath must be set when Authentication=Forms.'
                        }
                    }

            }       # END validate [Website] UrlPath

            7 {     # BEGIN validate [Website] LogPath

                    if ([System.String]::IsNullOrEmpty($IniConfig.Website.LogPath) -eq $false)
                    {
                        try {

                            $logPathRef = Get-Item -LiteralPath $IniConfig.Website.LogPath -ErrorAction Stop

                            if ($logPathRef.PSIsContainer)
                            {
                                if ($logPathRef.FullName.EndsWith('\'))
                                {
                                    $IniConfig.Logparser.Add('LogPath',$('{0}*' -f $logPathRef.FullName))

                                } else {

                                    $IniConfig.Logparser.Add('LogPath',$('{0}\*' -f $logPathRef.FullName))
                                }

                            } else {

                                $IniConfig.Logparser.Add('LogPath',$($logPathRef.FullName))
                            }

                        } catch {

                            $returnValue.ErrorMessages += '[Error][Config][Website] LogPath not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][Website] LogPath not specified.'
                    }

            }       # END validate [Website] LogPath

            8 {     # BEGIN validate [Website] FailedLoginsPerIP

                    if ([System.String]::IsNullOrEmpty($IniConfig.Website.FailedLoginsPerIP) -eq $false)
                    {
                        [Int] $intPerIP = 0

                        if ([System.Int32]::TryParse($IniConfig.Website.FailedLoginsPerIP, [ref] $intPerIP))
                        {
                            if ($intPerIP -le 0)
                            {
                                $returnValue.ErrorMessages += '[Error][Config][Website] FailedLoginsPerIP must be a positive number.'
                            }

                        } else {

                            $returnValue.ErrorMessages += '[Error][Config][Website] FailedLoginsPerIP must be a positive number.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][Website] FailedLoginsPerIP not specified.'
                    }

            }       # END validate [Website] FailedLoginsPerIP

            9 {     # BEGIN validate [Website] TotalFailedLogins

                    if ([System.String]::IsNullOrEmpty($IniConfig.Website.TotalFailedLogins) -eq $false)
                    {
                        [Int] $intTotal = 0

                        if ([System.Int32]::TryParse($IniConfig.Website.TotalFailedLogins, [ref] $intTotal))
                        {
                            if ($intTotal -le 0)
                            {
                                $returnValue.ErrorMessages += '[Error][Config][Website] TotalFailedLogins must be a positive number.'
                            }

                        } else {

                            $returnValue.ErrorMessages += '[Error][Config][Website] TotalFailedLogins must be a positive number.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][Website] TotalFailedLogins not specified.'
                    }

            }       # END validate [Website] TotalFailedLogins

            10 {    # BEGIN validate [Website] StartTime

                    if ([System.String]::IsNullOrEmpty($IniConfig.Website.StartTime) -eq $false)
                    {
                        [Int] $intSeconds = 0

                        if ([System.Int32]::TryParse($IniConfig.Website.StartTime, [ref] $intSeconds))
                        {
                            if ($intSeconds -gt 0)
                            {
                                [int] $IniConfig.Website.StartTime = $intSeconds

                                $startTS = (Get-Date).ToUniversalTime().AddSeconds($($intSeconds * -1))

                                $IniConfig.Script.Add('StartTimeTS', $($startTS.ToString('yyyy-MM-dd HH:mm:ss')))
                                $IniConfig.Script.Add('StartTimeTSZ', $($startTS.ToString('yyyy-MM-ddTHH:mm:ssZ')))

                            } else {

                                $returnValue.ErrorMessages += '[Error][Config][Website] StartTime must be a positive number.'
                            }

                        } else {

                            $returnValue.ErrorMessages += '[Error][Config][Website] StartTime must be a positive number.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][Website] StartTime not specified.'
                    }

            }       # END validate [Website] StartTime

            11 {    # BEGIN validate [Logparser] Path

                    if ([System.String]::IsNullOrEmpty($IniConfig.Logparser.Path) -eq $false)
                    {
                        if (Test-Path -LiteralPath $IniConfig.Logparser.Path)
                        {
                            $IniConfig.Logparser.Add('ExePath', $IniConfig.Logparser.Path)

                            $lp = Get-Item -LiteralPath $IniConfig.Logparser.ExePath

                            if ($lp.PSIsContainer)
                            {
                                $lpExePath = Join-Path -Path $IniConfig.Logparser.ExePath -ChildPath 'LogParser.exe'

                                if (Test-Path -LiteralPath $lpExePath)
                                {
                                    $IniConfig.Logparser.ExePath = Join-Path -Path $IniConfig.Logparser.ExePath -ChildPath 'LogParser.exe'

                                } else {

                                    $returnValue.ErrorMessages += '[Error][Config][Logparser] Path not valid.'
                                }

                            } elseif ($lp.Name -eq 'Logparser.exe') {

                                $IniConfig.Logparser.ExePath = $lp.FullName

                            } else {

                                $returnValue.ErrorMessages += '[Error][Config][Logparser] Path not valid.'
                            }

                        } else {

                            $returnValue.ErrorMessages += '[Error][Config][Logparser] Path not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][Logparser] Path not specified.'
                    }

            }       # END validate [Logparser] Path

            12 {    # BEGIN validate [Logparser] Exe

                    try {

                        $minVer = [System.Version]::Parse('2.2.10.0')

                        $lpExe = Get-Item -LiteralPath $IniConfig.Logparser.ExePath -ErrorAction Stop

                        $lpVer = [System.Version]::Parse($lpExe.VersionInfo.FileVersion)

                        if ($minVer -lt $lpVer)
                        {
                            $returnValue.ErrorMessages += $('[Error][Config][Logparser] Current Microsoft (R) Log Parser Version {0}' -f $lpExe.VersionInfo.FileVersion)

                            $returnValue.ErrorMessages += '[Error][Config][Logparser] Must be Microsoft (R) Log Parser Version 2.2.10'
                        }

                    } catch {

                        $e = $_
                        $returnValue.ErrorMessages += '[Error][Config][Logparser] Logparser.exe validation error.'
                        $returnValue.ErrorMessages += $('[Error][Config][Logparser] Exception: {0}' -f $e.Exception.Message)
                    }

            }       # END validate [Logparser] Exe

            13 {    # BEGIN validate [Logparser] dll

                    $minVer = [System.Version]::Parse('2.2.10.0')

                    $lp = Get-Item -LiteralPath $IniConfig.Logparser.ExePath

                    $lpDllPath = Join-Path -Path $lp.Directory -ChildPath 'logparser.dll'

                    try {

                        $lpDll = Get-Item -LiteralPath $lpDllPath

                        $lpVer = [System.Version]::Parse($lpExe.VersionInfo.FileVersion)

                        if ($lpVer -lt $minVer)
                        {
                            $returnValue.ErrorMessages += $('[Error][Config][Logparser] Current Microsoft (R) Log Parser DLL Version {0}' -f $lpDll.VersionInfo.FileVersion)
                            $returnValue.ErrorMessages += '[Error][Config][Logparser] Must be Microsoft (R) Log Parser DLL Version 2.2.10'
                        }

                    } catch {

                        $e = $_
                        $returnValue.ErrorMessages += '[Error][Config][Logparser] Logparser.dll validation error.'
                        $returnValue.ErrorMessages += $('[Error][Config][Logparser] Exception: {0}' -f $e.Exception.Message)
                    }

            }       # END validate [Logparser] dll

            14 {    # BEGIN validate [Logparser] run test query

                    $minVer = [System.Version]::Parse('2.2.10.0')

                    $lpQuery = "`"SELECT FileVersion FROM '{0}'`"" -f $IniConfig.Logparser.ExePath

                    $logparserArgs = @('-e:-1','-iw:ON','-headers:OFF','-q:ON','-i:FS','-o:CSV','-preserveLastAccTime:ON')

                    try {

                        [string] $lpFileVersion = Invoke-Logparser -Path $IniConfig.Logparser.ExePath `
                                                                    -Query $lpQuery `
                                                                    -Switches $logparserArgs

                        if ([System.String]::IsNullOrEmpty($lpFileVersion) -eq $false)
                        {
                            if ($lpFileVersion.Trim() -eq 'Task aborted.')
                            {
                                $returnValue.ErrorMessages += '[Error][Config][Logparser] Error testing launch of Logparser.exe'
                                $returnValue.ErrorMessages += '[Error][Config][Logparser] Task aborted.'

                            } elseif ($lpFileVersion.Trim().StartsWith('2.2.10') -eq $false) {

                                $returnValue.ErrorMessages += $('[Error][Config][Logparser] Current Microsoft (R) Log Parser Version {0}' -f $lpFileVersion)
                                $returnValue.ErrorMessages += '[Error][Config][Logparser] Must be Microsoft (R) Log Parser Version 2.2.10 or newer.'
                            }

                        } else {

                            $returnValue.ErrorMessages += '[Error][Config][Logparser] Error testing launch of Logparser.exe'
                            $returnValue.ErrorMessages += '[Error][Config][Logparser] No value returned.'
                        }

                    } catch {

                        $e = $_
                        $returnValue.ErrorMessages += '[Error][Config][Logparser] Error testing launch of Logparser.exe'
                        $returnValue.ErrorMessages += $('[Error][Config][Logparser] Exception: {0}' -f $e.Exception.Message)
                    }

            }       # END validate [Logparser] run test query

            15 {    # BEGIN validate [Alert] Method

                    if ([System.String]::IsNullOrEmpty($IniConfig.Alert.Method) -eq $false)
                    {
                        if ($IniConfig.Alert.Method -notmatch "^(?i)Smtp|WinEvent|None$")
                        {
                            $returnValue.ErrorMessages += '[Error][Alert] Method not valid.'
                        }

                    } else {

                        $IniConfig.Alert.Method = 'None'
                    }

            }       # END validate [Alert] Method

            16 {    # BEGIN validate [Alert] DataType

                    if ([System.String]::IsNullOrEmpty($IniConfig.Alert.DataType) -eq $false)
                    {
                        if ($IniConfig.Alert.DataType -notmatch "^(?i)(text|xml|json)$")
                        {
                            $returnValue.ErrorMessages += '[Error][Alert] DataType not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Alert] DataType not specified.'
                    }

            }       # END validate [Alert] DataType

            17 {    # BEGIN validate [SMTP]

                    if ([System.String]::IsNullOrEmpty($IniConfig.Alert.Method) -eq $false)
                    {
                        if ($IniConfig.Alert.Method -imatch 'Smtp')
                        {
                            $smtpResult = Assert-ValidSmtpSettings -IniConfig $IniConfig

                            if ($smtpResult.HasError -eq $true)
                            {
                                $returnValue.ErrorMessages = $smtpResult.ErrorMessages
                            }
                        }
                    }

            }       # END validate [SMTP]

            18 {    # BEGIN validate [WinEvent]

                    if ([System.String]::IsNullOrEmpty($IniConfig.Alert.Method) -eq $false)
                    {
                        if ($IniConfig.Alert.Method -imatch 'WinEvent')
                        {
                            $winEventResult = Assert-ValidWinEventSettings -IniConfig $IniConfig

                            if ($winEventResult.HasError -eq $true)
                            {
                                $returnValue.ErrorMessages = $winEventResult.ErrorMessages
                            }
                        }
                    }

            }       # END validate [WinEvent]

            19 {    # BEGIN validate IIS Log Access & verify logging fields

                    $lpQuery = "`"SELECT TOP 1 * FROM '$($IniConfig.Logparser.LogPath)' " + `
                               "WHERE s-sitename LIKE '$($IniConfig.Website.Sitename)' " + `
                               "ORDER BY date, time DESC`""

                    $logparserArgs = @('-recurse:-1','-headers:ON','-iw:ON','-q:ON','-i:IISW3C','-o:CSV')

                    $fullLpCmd = "$($IniConfig.Logparser.ExePath) $($logparserArgs -join ' ') $($lpQuery)"

                    $lpError = @(
                                    '[Error][Config][Script] Full Logparser command:',
                                    $('[Error][Config][Script] {0}' -f $fullLpCmd)
                                )

                    $lpOutput = Invoke-Logparser -Path $IniConfig.Logparser.ExePath `
                                                -Query $lpQuery `
                                                -Switches $logparserArgs

                    if ([System.String]::IsNullOrEmpty($lpOutput) -eq $false)
                    {
                        $lpOutputCsv = $lpOutput | ConvertFrom-Csv

                        # validate IIS logging field
                        $iisLogFields = @( 'date','time','c-ip','s-sitename','cs-method','cs-uri-stem','sc-status' )

                        $iisLogFieldError = $false

                        foreach ($logField in $iisLogFields)
                        {
                            # check if field exists
                            if ($null -eq $(Get-Member -InputObject $lpOutputCsv -Name $logField -MemberType NoteProperty))
                            {
                                $iisLogFieldError = $true
                                $returnValue.ErrorMessages += "[Error][Config][Script] IIS log field '$logField' not being logged."

                            } else {

                                # is a value being logged
                                $propertyValue = $lpOutputCsv | Select-Object -ExpandProperty $logField

                                if ([System.String]::IsNullOrEmpty($propertyValue) -eq $true)
                                {
                                    $iisLogFieldError = $true
                                    $returnValue.ErrorMessages += "[Error][Config][Script] IIS log field '$logField' not being logged."
                                }
                            }
                        }

                        if ($iisLogFieldError -eq $true)
                        {
                            $returnValue.ErrorMessages = $lpError + $returnValue.ErrorMessages
                            $returnValue.ErrorMessages += '[Error][Config][Script] https://github.com/phbits/WebsiteFailedLogins/wiki/Prerequisites'
                        }

                    } else {

                        $returnValue.ErrorMessages += $lpError
                        $returnValue.ErrorMessages += '[Error][Config][Script] Failed to get an IIS log record.'
                    }

            }       # END validate IIS Log Access & verify logging fields

            default {

                if ($returnValue.ErrorMessages.Count -gt 0)
                {
                    $returnValue.HasError = $true

                    $returnValue.ErrorMessages += '[Error][Config][Script] Terminating script.'

                    Write-Error -Message $($returnValue.ErrorMessages -join [System.Environment]::NewLine)
                }

                $i = 0
            }
        }

    } while ($i -gt 0)

    $returnValue.Configuration = $IniConfig

    return $returnValue

} # End Function Assert-ValidIniConfig

Function Assert-ValidWinEventSettings
{
    <#
        .SYNOPSIS
 
            Validates WinEvent settings in the configuration file.
    #>

    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]
    param(
            [Parameter(Mandatory=$true)]
            [System.Collections.Hashtable]
            # INI Configuration.
            $IniConfig
    )

    Write-Verbose -Message '[Assert-ValidWinEventSettings] Validating WinEvent configuration file settings.'

    $n = 0

    $returnValue = @{
                        'ErrorMessages' = @()
                        'HasError'      = $false
                        'Configuration' = @{}
                    }

    do {

        $n++

        if ($returnValue.ErrorMessages.Count -gt 0)
        {
            $n = 100
        }

        Write-Verbose -Message "[Assert-ValidWinEventSettings] Check #$($n)"

        switch ($n)
        {
            1 {     # BEGIN validate [WinEvent] Logname

                    if ([System.String]::IsNullOrEmpty($IniConfig.WinEvent.Logname) -eq $false)
                    {
                        if ([System.Diagnostics.EventLog]::Exists($IniConfig.WinEvent.Logname) -eq $false)
                        {
                            $returnValue.ErrorMessages += '[Error][Config][WinEvent] Logname not valid.'
                            $returnValue.ErrorMessages += $('[Error][Config][WinEvent] {0} does not exist.' -f $IniConfig.WinEvent.Logname)
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][WinEvent] Logname not specified.'
                    }

            }       # END validate [WinEvent] Logname

            2 {     # BEGIN validate [WinEvent] Source

                    if ([System.String]::IsNullOrEmpty($IniConfig.WinEvent.Source) -eq $false)
                    {
                        $result = [System.Diagnostics.EventLog]::SourceExists($IniConfig.WinEvent.Source)

                        if ($result -eq $false)
                        {
                            $returnValue.ErrorMessages += '[Error][Config][WinEvent] Source does not exist.'
                            $returnValue.ErrorMessages += '[Error][Config][WinEvent] Run the following command in an elevated prompt:'
                            $returnValue.ErrorMessages += $('[Error][Config][WinEvent] New-EventLog -LogName Application -Source {0}' -f $IniConfig.WinEvent.Source)
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][WinEvent] Source not specified.'
                    }

            }       # END validate [WinEvent] Source

            3 {     # BEGIN validate [WinEvent] EntryType

                    if ([System.String]::IsNullOrEmpty($IniConfig.WinEvent.EntryType) -eq $false)
                    {
                        if ($IniConfig.WinEvent.EntryType -notmatch "^(?i)(Error|FailureAudit|Information|SuccessAudit|Warning)$")
                        {
                            $returnValue.ErrorMessages += '[Error][Config][WinEvent] EntryType not valid.'
                            $returnValue.ErrorMessages += '[Error][Config][WinEvent] Choose one: Error,FailureAudit,Information,SuccessAudit,Warning'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][WinEvent] EntryType not specified.'
                        $returnValue.ErrorMessages += '[Error][Config][WinEvent] Choose one: Error,FailureAudit,Information,SuccessAudit,Warning'
                    }

            }       # END validate [WinEvent] EntryType

            4 {     # BEGIN validate [WinEvent] FailedLoginsPerIPEventId

                    if ([System.String]::IsNullOrEmpty($IniConfig.WinEvent.FailedLoginsPerIPEventId) -eq $false)
                    {
                        [Int] $intFailedLoginsPerIPEventId = 0

                        if ([System.Int32]::TryParse($IniConfig.WinEvent.FailedLoginsPerIPEventId, [ref] $intFailedLoginsPerIPEventId))
                        {
                            $winEventIdResult = Assert-WinEventId -EventName 'FailedLoginsPerIPEventId' `
                                                                  -EventId $intFailedLoginsPerIPEventId

                            if ($winEventIdResult.HasError -eq $true)
                            {
                                $returnValue.ErrorMessages = $winEventIdResult.ErrorMessages
                            }

                        } else {

                            $returnValue.ErrorMessages += '[Error][Config][WinEvent] FailedLoginsPerIPEventId not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][WinEvent] FailedLoginsPerIPEventId not specified.'
                    }

            }       # END validate [WinEvent] FailedLoginsPerIPEventId

            5 {     # BEGIN validate [WinEvent] TotalFailedLoginsEventId

                    if ([System.String]::IsNullOrEmpty($IniConfig.WinEvent.TotalFailedLoginsEventId) -eq $false)
                    {
                        [Int] $intTotalFailedLoginsEventId = 0

                        if ([System.Int32]::TryParse($IniConfig.WinEvent.TotalFailedLoginsEventId, [ref] $intTotalFailedLoginsEventId))
                        {
                            $winEventIdResult = Assert-WinEventId -EventName 'TotalFailedLoginsEventId' `
                                                                -EventId $intTotalFailedLoginsEventId

                            if ($winEventIdResult.HasError -eq $true)
                            {
                                $returnValue.ErrorMessages = $winEventIdResult.ErrorMessages
                            }

                        } else {

                            $returnValue.ErrorMessages += '[Error][Config][WinEvent] TotalFailedLoginsEventId not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][WinEvent] TotalFailedLoginsEventId not specified.'
                    }

            }       # END validate [WinEvent] TotalFailedLoginsEventId

            6 {     # BEGIN validate [WinEvent] Unique TotalFailedLoginsEventId & FailedLoginsPerIPEventId

                    if ($IniConfig.WinEvent.TotalFailedLoginsEventId -eq $IniConfig.WinEvent.FailedLoginsPerIPEventId)
                    {
                        $returnValue.ErrorMessages += '[Error][Config][WinEvent] TotalFailedLoginsEventId and FailedLoginsPerIPEventId must be different.'
                    }

            }       # END validate [WinEvent] Unique TotalFailedLoginsEventId & FailedLoginsPerIPEventId

            7 {     # BEGIN validate [WinEvent] Write Start

                    try {

                        Write-EventLog -LogName $IniConfig.WinEvent.Logname `
                                        -Source $IniConfig.WinEvent.Source `
                                        -EntryType $IniConfig.WinEvent.EntryType `
                                        -EventId 100 `
                                        -ErrorAction Stop `
                                        -Message 'Write-EventLog success.'

                    } catch {

                        $e = $_

                        $returnValue.ErrorMessages += '[Error][Config][Script] Event log write failed.'
                        $returnValue.ErrorMessages += $('[Error][Config][Script] Exception: {0}' -f $e.Exception.Message)
                    }

            }       # END validate [WinEvent] Write Start

            default {

                    if ($returnValue.ErrorMessages.Count -gt 0)
                    {
                        $returnValue.HasError = $true
                    }

                    $n = 0
            }
        }

    } while ($n -gt 0)

    return $returnValue

} # End Function Assert-ValidWinEventSettings

Function Assert-WinEventId
{
    <#
        .SYNOPSIS
 
            Validates WinEvent Event IDs
    #>

    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]
    param(
            [Parameter(Mandatory=$true)]
            [String]
            # Event Name
            $EventName
            ,
            [Parameter(Mandatory=$true)]
            [Int]
            # Event ID
            $EventId
    )

    $returnValue = @{
                        'ErrorMessages' = @()
                        'HasError'      = $false
                    }

    switch ($EventId)
    {
        0 {
            $returnValue.ErrorMessages += $('[Error][Config][WinEvent] {0} cannot be zero.' -f $EventName)
        }
        100 {
            $returnValue.ErrorMessages += $('[Error][Config][WinEvent] {0} cannot be {1}.' -f $EventName,$EventId)
        }
        200 {
            $returnValue.ErrorMessages += $('[Error][Config][WinEvent] {0} cannot be {1}.' -f $EventName,$EventId)
        }
        default {

            if (-not $($EventId -gt 0 -and $EventId -le 999))
            {
                $returnValue.ErrorMessages += $('[Error][Config][WinEvent] {0} not valid.' -f $EventName)
            }
        }
    }

    if ($returnValue.ErrorMessages.Count -gt 0)
    {
        $returnValue.ErrorMessages += $('[Error][Config][WinEvent] {0} must be between 1-999.' -f $EventName)
        $returnValue.HasError = $true
    }

    return $returnValue

} # End Function Assert-WinEventId

Function Assert-ValidSmtpSettings
{
    <#
        .SYNOPSIS
 
            Validates SMTP settings in the configuration file.
    #>

    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]
    param(
            [Parameter(Mandatory=$true)]
            [System.Collections.Hashtable]
            # INI Configuration.
            $IniConfig
    )

    Write-Verbose -Message '[Assert-ValidSmtpSettings] Validating SMTP configuration file settings.'

    $n = 0

    $returnValue = @{
                        'ErrorMessages' = @()
                        'HasError'      = $false
                        'Configuration' = @{}
                    }

    do {

        $n++

        if ($returnValue.ErrorMessages.Count -gt 0)
        {
            $n = 100
        }

        Write-Verbose -Message "[Assert-ValidSmtpSettings] Check #$($n)"

        switch ($n)
        {
            1 {    # BEGIN validate [SMTP] To

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

                            $null = [System.Net.Mail.MailAddress]::New($IniConfig.Smtp.To)

                        } catch {

                            $returnValue.ErrorMessages += '[Error][Config][SMTP] TO not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][SMTP] TO not specified.'
                    }

            }       # END validate [SMTP] To

            2 {     # BEGIN validate [SMTP] From

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

                            $null = [System.Net.Mail.MailAddress]::new($IniConfig.Smtp.From)

                        } catch {

                            $returnValue.ErrorMessages += '[Error][Config][SMTP] FROM not valid.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][SMTP] FROM not specified.'
                    }

            }       # END validate [SMTP] From

            3 {     # BEGIN validate [SMTP] Subject

                    if ([System.String]::IsNullOrEmpty($IniConfig.Smtp.Subject))
                    {
                        $returnValue.ErrorMessages += '[Error][SMTP] SUBJECT not specified.'

                    } else {

                        try {

                            $msg = [System.Net.Mail.MailMessage]::new()

                            $msg.Subject = $IniConfig.Smtp.Subject

                            $msg.Dispose()

                            Remove-Variable -Name msg

                        } catch {

                            $returnValue.ErrorMessages += '[Error][Config][SMTP] SUBJECT not valid.'
                        }
                    }

            }       # END validate [SMTP] Subject

            4 {     # BEGIN validate [SMTP] Port

                    if ([System.String]::IsNullOrEmpty($IniConfig.Smtp.Port) -eq $false)
                    {
                        [Int] $intPort = 0

                        if ([System.Int32]::TryParse($IniConfig.Smtp.Port, [ref] $intPort))
                        {
                            if (-not $($intPort -gt 0 -and $intPort -le 65535))
                            {
                                $returnValue.ErrorMessages += '[Error][Config][SMTP] PORT must be 1-65535.'
                            }

                        } else {

                            $returnValue.ErrorMessages += '[Error][Config][SMTP] PORT must be 1-65535.'
                        }

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][SMTP] PORT not specified.'
                    }

            }       # END validate [SMTP] Port

            5 {     # BEGIN validate [SMTP] Server

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

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

                            if ($smtpServer.Connected -eq $false)
                            {
                                $returnValue.ErrorMessages += '[Error][Config][SMTP] TCP connection failed to {0}:{1}' -f $IniConfig.Smtp.Server,$IniConfig.Smtp.Port
                            }

                            $smtpServer.Dispose()

                            Remove-Variable -Name smtpServer

                        } catch {

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

                    } else {

                        $returnValue.ErrorMessages += '[Error][Config][SMTP] SERVER not specified.'
                    }

            }       # END validate [SMTP] Server

            6 {     # BEGIN validate [SMTP] CredentialXml

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

                            $credCheck = Import-Clixml -LiteralPath $IniConfig.Smtp.CredentialXml -ErrorAction Stop

                            if ($credCheck.GetType().Name -ne 'PSCredential')
                            {
                                $returnValue.ErrorMessages += '[Error][Config][SMTP] CredentialXml import failed.'
                            }

                            Remove-Variable -Name credCheck

                        } catch {

                            $e = $_
                            $returnValue.ErrorMessages += '[Error][Config][SMTP] CredentialXml import failed.'
                            $returnValue.ErrorMessages += $('[Error][Config][SMTP] Exception: {0}' -f $e.Exception.Message)
                        }
                    }

            }       # END validate [SMTP] CredentialXml

            7 {     # BEGIN validate [SMTP] Send test alert

                    $emailSplat = @{
                        'To'          = $IniConfig.Smtp.To
                        'From'        = $IniConfig.Smtp.From
                        'Subject'     = $('[TEST] {0}' -f $IniConfig.Smtp.Subject)
                        'SmtpServer'  = $IniConfig.Smtp.Server
                        'Port'        = $IniConfig.Smtp.Port
                        'Body'        = "Test message to validate configuration.`n`nUse '-RunningConfig' switch once all errors have been fixed."
                        'UseSsl'      = $true
                        'ErrorAction' = 'Stop'
                    }

                    if ([System.String]::IsNullOrEmpty($IniConfig.Smtp.CredentialXml) -eq $false)
                    {
                        $emailSplat.Add('Credential', $(Import-Clixml -LiteralPath $IniConfig.Smtp.CredentialXml))
                    }

                    try {

                        Send-MailMessage @emailSplat | Out-Null

                        Remove-Variable -Name emailSplat

                    } catch {

                        $e = $_
                        $returnValue.ErrorMessages += '[Error][Config][SMTP] Send test message failed.'
                        $returnValue.ErrorMessages += $('[Error][Config][SMTP] Exception: {0}' -f $e.Exception.Message)
                    }

            }       # END validate [SMTP] Send test alert

            default {

                if ($returnValue.ErrorMessages.Count -gt 0)
                {
                    $returnValue.HasError = $true
                }

                $n = 0
            }
        }

    } while ($n -gt 0)

    return $returnValue

} # End Function Assert-ValidSmtpSettings

Function Get-IniConfig
{
    <#
        .SYNOPSIS
 
            Parses the configuration file into a hashtable.
 
        .INPUTS
 
            System.String
 
        .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 $_})]
            [string]
            # Path to configuration file.
            $Path
    )

    Write-Verbose -Message '[Get-IniConfig] Reading configuration file.'

    $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',@{})
    }

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

        $config['Script'].Add('StartTS', (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))
    }

    return $config

} # End Function Get-IniConfig

Export-ModuleMember -Function 'Assert-ValidIniConfig','Get-IniConfig'