Last active
October 22, 2024 14:57
-
-
Save ghotz/1b99470bce5caed245c1b024040ec263 to your computer and use it in GitHub Desktop.
Auto ban IP subnets by POST requests total size over a certain threshold in the last http log file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#region startup | |
# Warning: remember to whitelist who you want to allow to post by entering their IP addresses in the Azure Portal | |
#import-module Az; | |
$TenantID = 'YOURTENANTID'; | |
$SubscriptionID = 'YOURSUBID' | |
$ResourceGroupName = 'WebSite'; | |
$AppServiceName = 'YOURSERVICENAME'; | |
$MyIP = (Invoke-WebRequest -uri "http://ifconfig.me/ip").Content; #avoid self-banning | |
$MaxPostsBytes = 100KB; | |
$SubnetSize = 30; # adjust as needed | |
#endregion startup | |
#region functions | |
Function Test-IPInSubnet { | |
param ( | |
[string]$ipAddress, | |
[string]$subnet | |
) | |
# Split the subnet into network address and prefix length | |
$subnetParts = $subnet -split "/" | |
if ($subnetParts.Count -ne 2) { | |
throw "Invalid subnet format. Use CIDR notation, e.g., 192.168.1.0/24" | |
} | |
$networkAddress = $subnetParts[0] | |
$prefixLength = [int]$subnetParts[1] | |
# Function to convert IP address to UInt32 correctly handling endianness | |
Function IPAddressToUInt32($ipAddress) { | |
$ip = [System.Net.IPAddress]::Parse($ipAddress) | |
$bytes = $ip.GetAddressBytes() | |
if ([System.BitConverter]::IsLittleEndian) { | |
[Array]::Reverse($bytes) | |
} | |
return [System.BitConverter]::ToUInt32($bytes, 0) | |
} | |
# Convert IP addresses to UInt32 | |
$ipUInt32 = IPAddressToUInt32($ipAddress) | |
$networkUInt32 = IPAddressToUInt32($networkAddress) | |
# Calculate the subnet mask as a UInt32 | |
$maskBits = ('1' * $prefixLength).PadRight(32, '0') | |
$maskUInt32 = [Convert]::ToUInt32($maskBits, 2) | |
# Calculate the network address for both IPs | |
$ipNetwork = $ipUInt32 -band $maskUInt32 | |
$networkBase = $networkUInt32 -band $maskUInt32 | |
# Check if the IP is in the subnet | |
if ($ipNetwork -eq $networkBase) { | |
#Write-Host "$ipAddress is included in the IP range $subnet" | |
return $true; | |
} else { | |
#Write-Host "$ipAddress is NOT included in the IP range $subnet" | |
return $false; | |
} | |
}; | |
Function Test-Banned | |
{ | |
param ( | |
[string]$ipAddress, | |
[string[]]$subnets | |
) | |
$subnets | % { if (Test-IPInSubnet $ipAddress $_) { return $true; } } | |
return $false; | |
} | |
#endregion functions | |
#region main | |
cls; | |
# Authenticate to Azure if not already authenticated | |
if (!$Connection) { $Connection = Connect-AzAccount -Tenant $TenantID -Subscription $SubscriptionID }; | |
# Get Log files technique from https://hkarthik7.github.io/azure%20devops/powershell/azure/2021/01/16/Parse-http-logs-of-Azure-webapp-using-Azure-DevOps-CI-pipeline.html | |
$SCM = "https://$($AppServiceName).scm.azurewebsites.net/api/vfs/LogFiles/http/rawlogs/" # to get zip file $SCM.Replace("vfs", "zip") | |
# Get authentication details from the publishing profile | |
Write-Host "Getting the authentication details from the publishing profile..."; | |
if (!$WebApp) { $WebApp = Get-AzWebApp -ResourceGroupName $ResourceGroupName -Name $AppServiceName }; | |
if (!$PublishingProfile) { [xml]$PublishingProfile = Get-AzWebAppPublishingProfile -WebApp $WebApp }; | |
$UserName = $PublishingProfile.publishData.publishProfile[0].userName; | |
$Password = $PublishingProfile.publishData.publishProfile[0].userPWD; | |
$Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName,$Password))); | |
# Get the log files | |
Write-Host "Getting the log files..."; | |
$LogFiles = Invoke-RestMethod -Uri $SCM -Headers @{Authorization=("Basic {0}" -f $Base64AuthInfo)} -Method GET; | |
#$LogFiles = $LogFiles | Where-Object { (Get-Date $_.crtime).Date -eq (Get-Date).Date } | Select * -Last 1; # filter by date as needed | |
$LogFile = $LogFiles | Sort-Object -Property mtime -Descending | Select -First 1; | |
$RawLog = Invoke-RestMethod -Uri "$SCM/$($LogFile.name)" -Headers @{Authorization=("Basic {0}" -f $Base64AuthInfo)} -Method GET; # to export -OutFile "PATH" -ContentType "text/plain" or "multipart/form-data" if zip file | |
# Parse the log files and get the IP addresses to ban | |
Write-Host "Parsing the log files and getting the IP addresses to ban..."; | |
$ParsedLog = $RawLog.Split([Environment]::NewLine) | Select -Skip 2 | ConvertFrom-Csv -Delimiter " " -Header 'date','time','s-sitename','cs-method','cs-uri-stem','cs-uri-query','s-port,cs-username','c-ip','cs(User-Agent)','cs(Cookie)','cs(Referer)','cs-host','sc-status','sc-substatus','sc-win32-status','sc-bytes','cs-bytes','time-taken'; | |
$AggregatedLog = $ParsedLog | ? { $_.'cs(User-Agent)' -ne $MyIP } ` | |
| Select * ` | |
, @{Name='TimeStampHr';Expression={"{0}T{1}" -f $_.date,$_.time.SubString(0,2)}} ` | |
, @{Name='TimeStampMi';Expression={"{0}T{1}" -f $_.date,$_.time.SubString(0,5)}} ` | |
, @{Name='TimeStampSs';Expression={"{0}T{1}" -f $_.date,$_.time}} ` | |
| Group-object -Property 'cs(User-Agent)' ` | |
| % { | |
[pscustomobject]@{ | |
IPAddress=$_.Name; | |
RequestsCountTotal=$_.Count; | |
RequestsBytesTotal=($_.group | measure-object 'cs-bytes' -Sum).Sum; | |
RequestsCountAvgPerHour=($_.group | Group-object -Property 'TimeStampHr'| % { $_.Count } | Measure-Object -Average).Average; | |
#RequestsBytesAvgPerHour=($_.group | Group-object -Property 'TimeStampHr'| % { ($_.group | Measure-Object 'cs-bytes' -Sum).Sum } | Measure-Object -Average).Average | |
RequestsBytesAvgPerHour=($_.group | Group-object -Property 'TimeStampHr'| % { ($_.group | Measure-Object 'cs-bytes' -Average).Average } | Measure-Object -Average).Average | |
RequestsCountAvgPerMinute=($_.group | Group-object -Property 'TimeStampMi'| % { $_.Count } | Measure-Object -Average).Average; | |
#RequestsBytesAvgPerMinute=($_.group | Group-object -Property 'TimeStampMi'| % { ($_.group | Measure-Object 'cs-bytes' -Sum).Sum } | Measure-Object -Average).Average | |
RequestsBytesAvgPerMinute=($_.group | Group-object -Property 'TimeStampMi'| % { ($_.group | Measure-Object 'cs-bytes' -Average).Average } | Measure-Object -Average).Average | |
RequestsCountAvgPerSecond=($_.group | Group-object -Property 'TimeStampSs'| % { $_.Count } | Measure-Object -Average).Average; | |
#RequestsBytesAvgPerSecond=($_.group | Group-object -Property 'TimeStampSs'| % { ($_.group | Measure-Object 'cs-bytes' -Sum).Sum } | Measure-Object -Average).Average | |
RequestsBytesAvgPerSecond=($_.group | Group-object -Property 'TimeStampSs'| % { ($_.group | Measure-Object 'cs-bytes' -Average).Average } | Measure-Object -Average).Average | |
} | |
} | |
| Select * ` | |
, @{Name='BytesPerRequestTotal';Expression={ $_.RequestsBytesTotal / $_.RequestsCountTotal }} ` | |
, @{Name='BytesPerRequestAvgPerHour';Expression={ $_.RequestsBytesAvgPerHour / $_.RequestsCountAvgPerHour }} ` | |
, @{Name='BytesPerRequestAvgPerMinute';Expression={ $_.RequestsBytesAvgPerMinute / $_.RequestsCountAvgPerMinute }} ` | |
, @{Name='BytesPerRequestAvgPerSecond';Expression={ $_.RequestsBytesAvgPerSecond / $_.RequestsCountAvgPerSecond }} | |
$IPAdressesToBan = $ParsedLog | ? { $_.'cs(User-Agent)' -ne $MyIP } | Group-object -Property 'cs(User-Agent)' | % {[pscustomobject]@{IPAddress=$_.Name;PostsCount=$_.Count;PostsBytes = ($_.group | measure-object 'cs-bytes' -Sum).Sum}} | ? PostsBytes -gt $MaxPostsBytes | Select *; | |
Write-Host "Top 10 IP address L2 prefixes with the most POSTs and over $MaxPostsBytes bytes:"; | |
$IPAdressesToBan | % { | |
[pscustomobject]@{ | |
L1 = ($_ -split '\.')[0]; | |
L2 = ("{0}.{1}" -f ($_ -split '\.')[0],($_ -split '\.')[1]); | |
L3 = ("{0}.{1}.{2}" -f ($_ -split '\.')[0],($_ -split '\.')[1],($_ -split '\.')[2]); | |
L4 = $_; | |
} | |
} | | |
Group-Object -Property L2 | sort count -Descending | select -First 10 | ft; | |
Write-Host "Top 10 IP address L3 prefixes with the most POSTs and $MaxPostsBytes bytes:"; | |
$IPAdressesToBan | % { | |
[pscustomobject]@{ | |
L1 = ($_ -split '\.')[0]; | |
L2 = ("{0}.{1}" -f ($_ -split '\.')[0],($_ -split '\.')[1]); | |
L3 = ("{0}.{1}.{2}" -f ($_ -split '\.')[0],($_ -split '\.')[1],($_ -split '\.')[2]); | |
L4 = $_; | |
} | |
} | | |
Group-Object -Property L3 | sort count -Descending | select -First 10 | ft; | |
Write-Host "This is a good time to decide wich wider subnet to manually ban before proceeding."; | |
$input = Read-Host "Do you want to continue banning these IP addresses by subnet size `/$($SubnetSize)? (Y/N)"; | |
if ($input -ne 'Y') { exit; }; | |
# Get the IP addresses already banned | |
Write-Host "Getting IP addresses already banned..."; | |
$Restrictions = Get-AzWebAppAccessRestrictionConfig -ResourceGroupName $ResourceGroupName -Name $AppServiceName; | |
$IPAddressBanned = $Restrictions.MainSiteAccessRestrictions | ? { $_.Action -eq 'deny' -and $_.IpAddress -ne 'Any' } | % { $_.IpAddress }; | |
if (!$IPAddressBanned) { $IPAddressBanned = @(); }; | |
$NewIPBannedNumber = 0; | |
# Ban the new IP addresses | |
$IPAdressesToBan | ? { $_.IPAddress -notin $IPAddressBanned } | % { | |
if (Test-IPInSubnet $MyIP ("{0}/{1}" -f $_.IPAddress,$SubnetSize)) | |
{ | |
Write-Host; | |
Write-Host "Cannot ban $($_.IPAddress) because your IP address $MyIP is included in subnet $($_.IPAddress)/$SubnetSize" -ForegroundColor DarkRed; | |
} | |
elseif ($IPAddressBanned -and (Test-Banned $_.IPAddress $IPAddressBanned)) | |
{ | |
Write-Host "`r$($_.IPAddress) is already in a banned subnet " -NoNewline; | |
} | |
else { | |
Write-Host; | |
Write-Host "Banning $($_.IPAddress)..." -ForegroundColor Yellow; | |
Add-AzWebAppAccessRestrictionRule -ResourceGroupName $ResourceGroupName -WebAppName $AppServiceName -Name "BL $($_.IPAddress)" -Priority 90 -Action Deny -IpAddress ("{0}/{1}" -f $_.IPAddress,$SubnetSize) | |
$IPAddressBanned += ("{0}/{1}" -f $_.IPAddress,$SubnetSize); | |
$NewIPBannedNumber++; | |
} | |
}; | |
Write-Host; | |
Write-Host "Finished banning $NewIPBannedNumber new IP addresses."; | |
#endregion main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment