Instantly share code, notes, and snippets.
Created
August 22, 2019 15:17
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save rcabr/5a11ae94cf5543635bcec8a06755f5cf to your computer and use it in GitHub Desktop.
Scans for Azure SQL servers using unauthorized IP addresses in their firewalls. Report. Optionally, remove.
This file contains 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
<# | |
.Synopsis | |
Scans for Azure SQL servers using unauthorized IP addresses in their firewalls, | |
groups them by user (by looking for a managedBy tag where the value is an e-mail address), | |
and sends an e-mail to each user. | |
.NOTES | |
AUTHOR: Reuben Cabrera | |
LASTEDIT: 2019-08-22 | |
#> | |
Param( | |
[Parameter(Mandatory = $true, HelpMessage = "Send to this address when we can't identify a user to notify.")] | |
[String] $DefaultEmailAddress, | |
[Parameter(Mandatory = $true, HelpMessage = "When true, sends separate reports to each suspect. When false, sends a single report to the DefaultEmailAddress.")] | |
[Boolean] $SendSeparateReports, | |
[Parameter(HelpMessage = "When true, violating firewall rules will be automatically removed.")] | |
[bool] $RemoveViolatingRules = $false, | |
[Parameter(HelpMessage = "ServerName of Azure Sql servers to ignore.")] | |
[String[]] $ExceptionList = @(), | |
[Parameter(HelpMessage = "A string array of subscription names to ignore")] | |
[string[]]$SubscriptionsToIgnore = "my-playground", | |
[Parameter(HelpMessage = "The NAME of the Azure Automation Account Credential for logging in to Office 365 to send e-mail.")] | |
[String] $O365CredentialName = "O365Credential", | |
[Parameter(HelpMessage = "When set, no e-mail will be sent to any other address but this one. Useful for debugging and testing.")] | |
[String] $OverrideEmailAddress = $null, | |
[Parameter(HelpMessage = "The e-mail subject line.")] | |
[String] $EmailSubject = "Azure Sql Server firewall scan" | |
) | |
# Constants | |
$attachmentFileName = "AzureSqlFirewallScan.csv"; # name of the e-mail's file attachment | |
$smtpServerName = "[your-org].mail.protection.outlook.com"; | |
$smtpServerPort = 25; | |
$fromEmailAddress = "[YourOrg] Azure Automation <no.reply@[your-domain]>"; | |
$emailBodyTitle = "<h2>Azure SQL Server firewall scan results 🔓🛡</h2>"; | |
$emailBodyInstructions = "<p>Verify that these Azure SQL Servers are configured correctly.</p>"; | |
$managedByTagName = "managedBy"; | |
$emailBodyNextSteps = ""; | |
$allowedIPAddresses = @( | |
"0.0.0.0", | |
"1.1.1.1" # change/add your IPs | |
); | |
Write-Output "Allowed IP addresses are: $allowedIPAddresses" | |
Write-Output "" | |
Write-Output "These servers will not be scanned: $ExceptionList" | |
Write-Output "" | |
if ($RemoveViolatingRules) { | |
Write-Output "Violating firewall rules will be removed automatically." | |
$emailBodyInstructions = "<p><strong>Violating firewall rules may have been removed automatically.</strong> Verify that these Azure SQL Servers are configured correctly.</p>"; | |
} | |
function Get-DestinationEmailAddress { | |
param($Users) | |
if (($null -ne $OverrideEmailAddress) -and ($OverrideEmailAddress -ne "")) { | |
Write-Warning "OVERRIDE: Sending results for user(s) '$($Users -join "","")' to '$OverrideEmailAddress' instead" | |
return $OverrideEmailAddress; | |
} | |
if (($null -eq $Users) -or ($Users -eq "")) | |
{ | |
Write-Warning "Sending results for unknown users to '$DefaultEmailAddress' instead" | |
return $DefaultEmailAddress; | |
} | |
return $Users; | |
} | |
# Try to find a managedBy tag on a resource, its resource group, or its subscription, | |
# and return the value (or $null). | |
# Throws an exception if the resource doesn't exist. | |
function Get-ResourceManagedBy { | |
param([string] $ResourceType, [string] $ResourceId) | |
$rt = $ResourceType; | |
$rid = $ResourceId; | |
# if not a resource group | |
if ($rt -ne "/Microsoft.Resources/subscriptions/resourceGroups") { | |
$r = Get-AzureRMResource -ResourceId $rid -ErrorAction Stop; | |
if ($null -ne $r.Tags -and $r.Tags.ContainsKey($managedByTagName) -eq $True) { | |
return $r.Tags[$managedByTagName]; | |
} | |
# move up to the resource group level | |
$rg = Get-AzureRMResourceGroup -Name $r.ResourceGroupName; | |
$rt = "/Microsoft.Resources/subscriptions/resourceGroups"; | |
} | |
# if it's a resource group | |
if ($rt -eq "/Microsoft.Resources/subscriptions/resourceGroups") { | |
if ($null -eq $rg) { | |
$rg = Get-AzureRMResourceGroup -Id $ResourceId -ErrorAction Stop; | |
} | |
if ($null -ne $rg.Tags -and $rg.Tags.ContainsKey($managedByTagName) -eq $True) { | |
return $rg.Tags[$managedByTagName]; | |
} | |
# move up to the subscription level | |
$sb = Get-AzureRmSubscription; | |
$rt = "/Microsoft.Resources/subscriptions"; | |
} | |
# if it's a subscription | |
if ($rt -eq "/Microsoft.Resources/subscriptions") { | |
if ($null -eq $sb) { | |
$sb = Get-AzureRmSubscription; | |
} | |
if ($null -ne $sb.Tags -and $sb.Tags.ContainsKey($managedByTagName) -eq $True) { | |
return $sb.Tags[$managedByTagName]; | |
} | |
} | |
return $null; | |
} | |
$connectionName = "AzureRunAsConnection" | |
try { | |
# Get the connection "AzureRunAsConnection " | |
$servicePrincipalConnection = Get-AutomationConnection -Name $connectionName | |
"Logging in to Azure..." | |
$login = Add-AzureRMAccount ` | |
-ServicePrincipal ` | |
-TenantId $servicePrincipalConnection.TenantId ` | |
-ApplicationId $servicePrincipalConnection.ApplicationId ` | |
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint | |
} | |
catch { | |
if (!$servicePrincipalConnection) { | |
$ErrorMessage = "Connection $connectionName not found." | |
throw $ErrorMessage | |
} | |
else { | |
Write-Error -Message $_.Exception | |
throw $_.Exception | |
} | |
} | |
# Collect violations to return | |
$violations = New-Object System.Collections.ArrayList | |
function CreateViolationRecord ($SubscriptionName, $ServerName, $ResourceGroupName, $ManagedBy, $FirewallRuleName, $StartIpAddress, $EndIpAddress, $ViolationMessage) { | |
return New-Object PSObject -Property @{ | |
Subscription = $SubscriptionName | |
ServerName = $ServerName | |
ResourceGroupName = $ResourceGroupName | |
ManagedBy = $ManagedBy | |
ViolationMessage = $ViolationMessage | |
FirewallRuleName = $FirewallRuleName | |
StartIpAddress = $StartIpAddress | |
EndIpAddress = $EndIpAddress | |
} | |
} | |
function CheckRulesForUnauthorizedIPAddr($FirewallRules, $ManagedBy) { | |
$foundByStart = $FirewallRules | Where-Object StartIpAddress -NotIn $allowedIPAddresses; | |
if ($null -ne $foundByStart) { | |
$subName = (Get-AzureRMContext).Subscription.Name | |
$v = CreateViolationRecord $subName $serverName $resourceGroupName $ManagedBy $foundByStart.FirewallRuleName $foundByStart.StartIpAddress $foundByStart.EndIpAddress ` | |
"Unauthorized IP address permitted by firewall rule $($foundByStart.FirewallRuleName) $($foundByStart.StartIpAddress)" | |
$index = $violations.Add($v) | |
Write-Warning "VIOLATION FOUND: $v" | |
} | |
} | |
$subscriptions = Get-AzureRMSubscription | |
foreach ($subscription in $subscriptions) { | |
if ($subscription.Name -in $SubscriptionsToIgnore) { | |
Write-Output "Skipping subscription '$($subscription.Name)'" | |
continue; | |
} | |
$ctx = Set-AzureRMContext -Subscription $subscription.Name; | |
$servers = Get-AzureRMSqlServer | Where-Object ServerName -NotIn $ExceptionList; | |
Write-Output "Examining $($servers.Count) Sql Servers in subscription '$($subscription.Name)'" | |
$ErrorActionPreference = "Continue"; | |
foreach ($server in $servers) { | |
$resourceGroupName = $server.ResourceGroupName; | |
$serverName = $server.ServerName; | |
$firewallRules = Get-AzureRMSqlServerFirewallRule -ResourceGroupName $resourceGroupName ` | |
-ServerName $serverName; | |
$managedBy = Get-ResourceManagedBy "Microsoft.Sql/servers" $server.ResourceId | |
CheckRulesForUnauthorizedIPAddr $firewallRules -ManagedBy $managedBy | |
if ($RemoveViolatingRules -ne $false) { | |
foreach ($violation in $violations) { | |
Write-Output ">> Removing firewall rule '$($violation.FirewallRuleName)' from server '$serverName' in resource group '$resourceGroupName'" | |
$removeResult = Remove-AzureRmSqlServerFirewallRule -FirewallRuleName $violation.FirewallRuleName ` | |
-ResourceGroupName $resourceGroupName -ServerName $serverName | |
} | |
} | |
} | |
} | |
# get O365 credentials to send e-mails with | |
$o365Cred = Get-AutomationPSCredential -Name $O365CredentialName; | |
if ($null -eq $o365Cred) { | |
Write-Error "Credential $O365CredentialName does not exist in this Automation Account. Please create one."; | |
exit | |
} | |
# send full report by e-mail | |
if ($SendSeparateReports -eq $false) { | |
$records = $violations | Sort-Object ManagedBy, Subscription, ServerName -Descending ` | |
| Select-Object ManagedBy, Subscription, ServerName, ResourceGroupName, ViolationMessage; | |
$toEmail = Get-DestinationEmailAddress $null | |
$records | Export-Csv -Path $attachmentFileName -NoTypeInformation; # export to CSV so we can attach the file to the e-mail | |
$count = 1 | |
if ($records.GetType().FullName -eq "System.Object[]") { | |
$count = $records.Length | |
} | |
$emailBody = $emailBodyTitle ` | |
+ "<p>$count unauthorized IP addresses are allowed by Azure Sql firewall rules. See the attached file and below.</p>" ` | |
+ ($records | ConvertTo-Html -Fragment) ` | |
+ $emailBodyInstructions ` | |
+ "<p><em>This message is intended for: $toEmail</em></p>"; | |
Send-MailMessage ` | |
-To $toEmail ` | |
-From $fromEmailAddress ` | |
-Subject "Report: $EmailSubject" ` | |
-Body $emailBody ` | |
-Attachments $attachmentFileName ` | |
-UseSsl ` | |
-Port $smtpServerPort ` | |
-BodyAsHtml ` | |
-SmtpServer $smtpServerName ` | |
-Credential $o365Cred; | |
Remove-Item -Path $attachmentFileName; # remove attachment file from filesystem | |
Write-Output "Sent full report e-mail to $toEmail with $count violation(s)." | |
} | |
else # send out the e-mails to each user | |
{ | |
# group violations by notifyees | |
$violationsByUser = $violations | Group-Object ManagedBy; | |
foreach ($violations in $violationsByUser | Where-Object -Property Count -NE -Value 0) { | |
#$user = $violations[0].NotifyWho; | |
$user = ($violations.Group | Select-Object ManagedBy)[0].ManagedBy; | |
$records = $violations.Group | Sort-Object Subscription, ServerName ` | |
| Select-Object ManagedBy, Subscription, ServerName, ResourceGroupName, ViolationMessage; | |
$toEmail = Get-DestinationEmailAddress $user | |
$records | Export-Csv -Path $attachmentFileName -NoTypeInformation; # export to CSV so we can attach the file to the e-mail | |
$count = 1 | |
if ($records.GetType().FullName -eq "System.Object[]") { | |
$count = $records.Length | |
} | |
$emailBody = $emailBodyTitle ` | |
+ "<p>$count unauthorized IP addresses are allowed by Azure Sql firewall rules. See the attached file and below.</p>" ` | |
+ ($records | ConvertTo-Html -Fragment) ` | |
+ $emailBodyInstructions ` | |
+ $emailBodyNextSteps ` | |
+ "<p><em>This message is intended for: $user </em></p>"; | |
Send-MailMessage ` | |
-To $toEmail ` | |
-From $fromEmailAddress ` | |
-Subject "Warning: $EmailSubject" ` | |
-Body $emailBody ` | |
-Attachments $attachmentFileName ` | |
-UseSsl ` | |
-Port $smtpServerPort ` | |
-BodyAsHtml ` | |
-SmtpServer $smtpServerName ` | |
-Credential $o365Cred; | |
Remove-Item -Path $attachmentFileName; # remove attachment file from filesystem | |
Write-Output "Sent e-mail to $toEmail with $count violation(s)." | |
} | |
} | |
exit |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment