WebsiteFailedLogins.psm1

# Require TLS1.2 for all communications
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Function Invoke-WebsiteFailedLogins
{
    <#
        .SYNOPSIS

            Launches WebsiteFailedLogins.

        .DESCRIPTION

            Generates an alert for:

                - Each IP address meeting or exceeding the threshold FailedLoginsPerIP

                - When the total failed logins threshold (TotalFailedLogins) is met or exceeded

            See wiki for details.

        .LINK

            https://github.com/phbits/WebsiteFailedLogins/wiki

        .EXAMPLE

            $results = Invoke-WebsiteFailedLogins -Configuration D:\WFL\W3SVC1.ini

        .EXAMPLE

            $results = Invoke-WebsiteFailedLogins -Configuration D:\WFL\W3SVC1.ini -RunningConfig

        .INPUTS

            System.String

        .OUTPUTS

            System.Collections.Hashtable
    #>

    [CmdletBinding()]
    [OutputType('System.Collections.Hashtable')]
    param(
            [Parameter(Mandatory=$true)]
            [ValidateScript({Test-Path -LiteralPath $_})]
            [string]
            # Path to configuration file.
            $Configuration
            ,
            [Parameter(Mandatory=$false)]
            [switch]
            # Performs the minimum validation checks against the configuration file. Use this switch after all configuration errors have been resolved.
            $RunningConfig
    )

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

    $iniConfig = Get-IniConfig -Path $Configuration

    $configTestResult = Assert-ValidIniConfig -IniConfig $iniConfig -RunningConfig:$($RunningConfig)

    $returnValue.Configuration = $configTestResult.Configuration

    if ($configTestResult.HasError)
    {
        $returnValue.HasError = $true
        $returnValue.ErrorMessages = $configTestResult.ErrorMessages

        $alertData = $returnValue
        $alertData.Remove('FailedLoginsPerIP')
        $alertData.Remove('TotalFailedLogins')

        Submit-Alert -IniConfig $returnValue.Configuration -AlertData $alertData -TerminatingError

    } else {

        $lpQuery = Get-LogparserQuery -IniConfig $returnValue.Configuration

        $returnValue.Configuration.Logparser.Add('WebsiteFailedLoginsQuery', $lpQuery)

        $allFailedLogins = Get-WebsiteFailedLogins -IniConfig $returnValue.Configuration

        $returnValue.Configuration.Script.Add('EndTimeTSZ', (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))

        if ([System.String]::IsNullOrEmpty($allFailedLogins) -eq $false)
        {
            [Int] $totalFailedLogins = 0
            [Int] $bottom20Percent = [System.Math]::Round($(.2 * $returnValue.Configuration.Website.TotalFailedLogins))

            [Hashtable] $totalFailedLoginClientIpList = @{}

            foreach ($entry in $allFailedLogins)
            {
                if ([Int] $entry.FailedLoginCount -ge [Int] $returnValue.Configuration.Website.FailedLoginsPerIP)
                {
                    $clientIpResult = Get-FailedLoginsPerIpResult -IniConfig $returnValue.Configuration `
                                                                  -ClientIP $entry.ClientIP `
                                                                  -FailedLogins $entry.FailedLoginCount

                    $returnValue.FailedLoginsPerIP.Add($entry.ClientIP, $clientIpResult)

                    $returnValue.HasResults = $true
                }

                if ([Int] $entry.FailedLoginCount -gt $bottom20Percent)
                {
                    $totalFailedLoginClientIpList.Add($entry.ClientIP, [Int] $entry.FailedLoginCount)
                }

                $totalFailedLogins += [Int] $entry.FailedLoginCount
            }

            if ($totalFailedLogins -ge [Int] $returnValue.Configuration.Website.TotalFailedLogins)
            {
                $returnValue.TotalFailedLogins = Get-TotalFailedLoginsResult -IniConfig $returnValue.Configuration `
                                                                             -TotalFailedLogins $totalFailedLogins `
                                                                             -ClientIpList $totalFailedLoginClientIpList

                $returnValue.HasResults = $true
            }
        }

        # send alerts
        if ($returnValue.HasResults -eq $true -and $returnValue.Configuration.Alert.Method -imatch "(Smtp|WinEvent)")
        {
            if ($returnValue.FailedLoginsPerIP.Count -gt 0)
            {
                foreach ($key in $returnValue.FailedLoginsPerIP.Keys)
                {
                    Submit-Alert -IniConfig $returnValue.Configuration `
                                 -AlertData $($returnValue.FailedLoginsPerIP[$key])
                }
            }

            if ($returnValue.TotalFailedLogins.Count -gt 0)
            {
                Submit-Alert -IniConfig $returnValue.Configuration `
                             -AlertData $returnValue.TotalFailedLogins
            }
        }
    }

    return $returnValue

} # End Function Invoke-WebsiteFailedLogins

Function Get-WebsiteFailedLoginsREADME
{
    <#
        .SYNOPSIS

            Gets the WebsiteFailedLogins README file.
    #>

    [OutputType('System.String[]')]
    [CmdletBinding()]
    param(
            [Parameter(Mandatory=$false)]
            [System.String]
            # Section to return.
            $SectionKeyword
    )

    try {

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

        $readMeFile = Get-Item -LiteralPath $readMePath -ErrorAction Stop

        $readMeContent = Get-Content -LiteralPath $readMeFile.FullName

        if ([System.String]::IsNullOrEmpty($SectionKeyword))
        {
            $readMeContent | foreach-Object{ Write-Output $_ }

        } else {

            $printLine = $false

            $sectionKeywordLower = $SectionKeyword.ToLower().Trim()

            for ($i=0; $i -lt $readMeContent.Length; $i++)
            {
                $line = $readMeContent[$i]

                if ($line.Trim().StartsWith('#'))
                {
                    $printLine = $false
                }

                if ([System.String]::IsNullOrEmpty($line) -eq $false)
                {
                    if ($line.ToLower().Contains($sectionKeywordLower))
                    {
                        if ($printLine -eq $false)
                        {
                            Write-Output $readMeContent[$i - 1]
                        }

                        $printLine = $true
                    }
                }

                if ($printLine)
                {
                    Write-Output $line
                }
            }
        }

    } catch {

        $e = $_
        Write-Error -Message "$('[ERROR][Exception] {0}' -f $e.Exception.Message)"
    }

} # End Function Get-WebsiteFailedLoginsReadme

Function Copy-WebsiteFailedLoginsREADME
{
    <#
        .SYNOPSIS

            Copy the WebsiteFailedLogins README file (README.md) to the destination folder.
    #>

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

    try {

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

        $readMeFile = Get-Item -LiteralPath $readMePath -ErrorAction Stop

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

    } catch {

        $e = $_
        Write-Error -Message "$('[ERROR][Exception] {0}' -f $e.Exception.Message)"
    }

} # End Function Copy-WebsiteFailedLoginsReadme

Function Get-WebsiteFailedLoginsDefaultConfiguration
{
    <#
        .SYNOPSIS

            Gets the WebsiteFailedLogins default configuration file.
    #>

    [OutputType('System.String[]')]
    [CmdletBinding()]
    param( )

    try {

        $defaultConfigPathFolder = Join-Path -Path $PSScriptRoot -ChildPath 'Resources'

        $defaultConfigPath = Join-Path -Path $defaultConfigPathFolder -ChildPath 'WebsiteFailedLogins_default.ini'

        $configFile = Get-Item -LiteralPath $defaultConfigPath -ErrorAction Stop

        [string[]] $configContents = Get-Content -LiteralPath $configFile.FullName

        return $configContents

    } catch {

        $e = $_
        Write-Error -Message "$('[ERROR][Exception] {0}' -f $e.Exception.Message)"
    }

} # End Function Get-WebsiteFailedLoginsDefaultConfiguration

Function Copy-WebsiteFailedLoginsDefaultConfiguration
{
    <#
        .SYNOPSIS

            Copy the WebsiteFailedLogins default configuration file to the destination folder.
    #>

    [CmdletBinding()]
    param(
            [Parameter(Mandatory=$false)]
            [ValidateScript({Test-Path -LiteralPath $_ -PathType Container})]
            [string]
            # Destination folder to copy WebsiteFailedLogins.ini
            $DestinationFolder = (Get-Location).Path
    )

    try {

        $defaultConfigPathFolder = Join-Path -Path $PSScriptRoot -ChildPath 'Resources'

        $defaultConfigPath = Join-Path -Path $defaultConfigPathFolder -ChildPath 'WebsiteFailedLogins_default.ini'

        $configFile = Get-Item -LiteralPath $defaultConfigPath -ErrorAction Stop

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

    } catch {

        $e = $_
        Write-Error -Message "$('[ERROR][Exception] {0}' -f $e.Exception.Message)"
    }

} # End Function Copy-WebsiteFailedLoginsDefaultConfiguration