Framework/Core/SubscriptionSecurity/Alerts.ps1
using namespace System.Management.Automation Set-StrictMode -Version Latest # Class to implement Subscription alert controls class Alerts: CommandBase { hidden [PSObject[]] $Policy = $null; hidden [PSObject[]] $ApplicableAlerts = $null; hidden [string] $TargetResourceGroup; hidden [string] $ResourceGroup = "AzSDKAlertsRG"; hidden [string] $ResourceGroupLocation = "East US"; Alerts([string] $subscriptionId, [InvocationInfo] $invocationContext, [string] $tags): Base($subscriptionId, $invocationContext) { $this.Policy = [array] $this.LoadServerConfigFile("Subscription.InsAlerts.json"); $this.FilterTags = $this.ConvertToStringArray($tags); } hidden [PSObject[]] GetApplicableAlerts([string[]] $alertNames) { if($null -eq $this.ApplicableAlerts) { $this.ApplicableAlerts = @(); if($alertNames -and $alertNames.Count -ne 0) { $this.ApplicableAlerts += $this.Policy | Where-Object { $alertNames -Contains $_.Name }; } elseif(($this.FilterTags | Measure-Object).Count -ne 0) { $this.Policy | ForEach-Object { $currentItem = $_; if(($currentItem.Tags | Where-Object { $this.FilterTags -Contains $_ } | Measure-Object).Count -ne 0) { $this.ApplicableAlerts += $currentItem; } } } } return $this.ApplicableAlerts; } hidden [PSObject[]] GetApplicableAlerts() { return $this.GetApplicableAlerts(@()); } [MessageData[]] SetAlerts([string] $targetResourceGroup, [string] $securityContactEmails, [string] $alertResourceGroupLocation) { # Parameter validation if([string]::IsNullOrWhiteSpace($securityContactEmails)) { throw [System.ArgumentException] ("The argument 'securityContactEmails' is null or empty"); } $allEmails = @(); $allEmails += $this.ConvertToStringArray($securityContactEmails); if($allEmails.Count -eq 0) { throw [System.ArgumentException] ("Please provide valid email address(es)"); } if(-not [string]::IsNullOrWhiteSpace($alertResourceGroupLocation)) { $this.ResourceGroupLocation = $alertResourceGroupLocation; } [MessageData[]] $messages = @(); if(($this.Policy | Measure-Object).Count -ne 0) { if($this.GetApplicableAlerts() -ne 0) { $startMessage = [MessageData]::new("Processing AzSDK alerts. Total alerts: $($this.GetApplicableAlerts().Count)"); $messages += $startMessage; $this.PublishCustomMessage($startMessage); $this.PublishCustomMessage("Note: Configuring alerts can take about 10-12 minutes depending on number of alerts to be processed...", [MessageType]::Warning); $disabledAlerts = $this.GetApplicableAlerts() | Where-Object { -not $_.Enabled }; if(($disabledAlerts | Measure-Object).Count -ne 0) { $disabledMessage = "Found alerts which are disabled. This is intentional. Total disabled alerts: $($disabledAlerts.Count)"; $messages += [MessageData]::new($disabledMessage, $disabledAlerts); } $enabledAlerts = @(); $enabledAlerts += $this.GetApplicableAlerts() | Where-Object { $_.Enabled }; if($enabledAlerts.Count -ne 0) { $messages += [MessageData]::new([Constants]::SingleDashLine + "`r`nAdding following alerts to the subscription. Total alerts: $($enabledAlerts.Count)", $enabledAlerts); # Check if Resource Group exists $existingRG = Get-AzureRmResourceGroup -Name $this.ResourceGroup -ErrorAction SilentlyContinue if(-not $existingRG) { New-AzureRmResourceGroup -Name $this.ResourceGroup -Location $this.ResourceGroupLocation -Tag @{ "AzSDKVersion" = $this.GetCurrentModuleVersion(); "CreationTime" = $this.RunIdentifier; "AzSDKFeature" = "Alerts" } -ErrorAction Stop | Out-Null } $messages += [MessageData]::new("All the alerts registered by this script will be placed in a resource group named: $($this.ResourceGroup)"); #Remove Resource Lock on Resource Group if any $messages += $this.RemoveAllResourceGroupLocks(); #Add-OutputLogEvent -OutputLogFilePath $outputLogFilePath -EventData ("A resource lock will be deployed on this resource group to protect from accidental deletion.") -RawOutput $isTargetResourceGroup = -not [string]::IsNullOrWhiteSpace($targetResourceGroup); [Helpers]::RegisterResourceProviderIfNotRegistered("microsoft.insights"); # Accepting only first email address because of Azure issue in creating Alert rule with multiple emails $emailAction = New-AzureRmAlertRuleEmail -CustomEmails $allEmails[0] -WarningAction SilentlyContinue $errorCount = 0; $currentCount = 0; $enabledAlerts | ForEach-Object { $alertName = $_.Name; $currentCount += 1; # Add alert try { if($isTargetResourceGroup) { Add-AzureRmLogAlertRule -Name $_.Name -Location $this.ResourceGroupLocation -ResourceGroup $this.ResourceGroup ` -TargetResourceGroup $targetResourceGroup -OperationName $_.OperationName ` -Actions $emailAction -Description $_.Description -WarningAction Ignore } else { Add-AzureRmLogAlertRule -Name $_.Name -Location $this.ResourceGroupLocation -ResourceGroup $this.ResourceGroup ` -OperationName $_.OperationName ` -Actions $emailAction -Description $_.Description -WarningAction Ignore } } catch { $messages += [MessageData]::new("Error while adding alert [$alertName] to the subscription", $_, [MessageType]::Error); $errorCount += 1; } $this.CommandProgress($enabledAlerts.Count, $currentCount, 20); }; [MessageData[]] $resultMessages = @(); #Logic to validate if Alerts are configured. $configuredAlerts = @(); $configuredAlerts = Get-AzureRmAlertRule -ResourceGroup $this.ResourceGroup -WarningAction SilentlyContinue $actualConfiguredAlertsCount = ($configuredAlerts | Measure-Object).Count $notConfiguredAlertsCount = $enabledAlerts.Count - $actualConfiguredAlertsCount if($errorCount -eq 0 -and $actualConfiguredAlertsCount -eq $enabledAlerts.Count) { $resultMessages += [MessageData]::new("All AzSDK alerts have been configured successfully.`r`n" + [Constants]::SingleDashLine, [MessageType]::Update); } elseif($errorCount -eq $enabledAlerts.Count) { $resultMessages += [MessageData]::new("No alerts have been added to the subscription due to error occurred. Please add the alerts manually.`r`n" + [Constants]::SingleDashLine, [MessageType]::Error); } else { $resultMessages += [MessageData]::new("$notConfiguredAlertsCount/$($enabledAlerts.Count) alert(s) have not been added to the subscription. Please add the alerts manually.", [MessageType]::Error); $resultMessages += [MessageData]::new("$actualConfiguredAlertsCount/$($enabledAlerts.Count) alert(s) have been added to the subscription successfully`r`n" + [Constants]::SingleDashLine, [MessageType]::Update); } $messages += $resultMessages; $this.PublishCustomMessage($resultMessages); # Create the lock $messages += $this.AddResourceGroupLock(); } } else { $this.PublishCustomMessage("No alerts have been found that matches the specified tags. Tags:[$([string]::Join(",", $this.FilterTags))].", [MessageType]::Warning); } } else { $this.PublishCustomMessage("No alerts found in the alert policy file", [MessageType]::Warning); } return $messages; } [MessageData[]] RemoveAlerts([bool] $deleteResourceGroup, [string] $alertNames) { [MessageData[]] $messages = @(); # Check for existence of resource group $existingRG = Get-AzureRmResourceGroup -Name $this.ResourceGroup -ErrorAction SilentlyContinue if($existingRG) { $startMessage = [MessageData]::new("Found AzSDK alerts resource group: $($this.ResourceGroup)"); $messages += $startMessage; $this.PublishCustomMessage($startMessage); # Remove all locks $messages += $this.RemoveAllResourceGroupLocks(); # Check if user wants to remove resource group if($deleteResourceGroup) { $messages += [MessageData]::new("Removing all AzSDK configured alerts by removing resource group: $($this.ResourceGroup)"); # Remove entire RG (containing all alerts). Remove-AzureRmResourceGroup -Name $this.ResourceGroup -Force $resultMessage = [MessageData]::new("All AzSDK configured alerts removed successfully"); $messages += $resultMessage; $this.PublishCustomMessage($resultMessage); } else { $alertNameArray = @(); if(-not [string]::IsNullOrWhiteSpace($alertNames)) { $alertNameArray += $this.ConvertToStringArray($alertNames); if($alertNameArray.Count -eq 0) { throw [System.ArgumentException] ("The argument 'alertNames' is null or empty"); } } # User wants to remove only specific alerts if(($this.Policy | Measure-Object).Count -ne 0) { if($this.GetApplicableAlerts($alertNameArray) -ne 0) { $startMessage = [MessageData]::new("Removing alerts. Tags:[$([string]::Join(",", $this.FilterTags))]. Total alerts: $($this.GetApplicableAlerts($alertNameArray).Count)"); $messages += $startMessage; $this.PublishCustomMessage($startMessage); $this.PublishCustomMessage("Note: Removing alerts can take few minutes depending on number of alerts to be processed...", [MessageType]::Warning); $disabledAlerts = $this.GetApplicableAlerts($alertNameArray) | Where-Object { -not $_.Enabled }; if(($disabledAlerts | Measure-Object).Count -ne 0) { $disabledMessage = "Found alerts which are disabled and will not be removed. This is intentional. Total disabled alerts: $($disabledAlerts.Count)"; $messages += [MessageData]::new($disabledMessage, $disabledAlerts); #$this.PublishCustomMessage($disabledMessage, [MessageType]::Warning); } $enabledAlerts = @(); $enabledAlerts += $this.GetApplicableAlerts($alertNameArray) | Where-Object { $_.Enabled }; if($enabledAlerts.Count -ne 0) { $messages += [MessageData]::new([Constants]::SingleDashLine + "`r`nRemoving following alerts from the subscription. Total alerts: $($enabledAlerts.Count)", $enabledAlerts); $errorCount = 0; $currentCount = 0; $enabledAlerts | ForEach-Object { $alertName = $_.Name; $currentCount += 1; # Remove alert try { Remove-AzureRmAlertRule -ResourceGroup $this.ResourceGroup -Name $alertName -WarningAction SilentlyContinue } catch { $messages += [MessageData]::new("Error while removing alert [$alertName] from the subscription", $_, [MessageType]::Error); $errorCount += 1; } $this.CommandProgress($enabledAlerts.Count, $currentCount, 20); }; [MessageData[]] $resultMessages = @(); if($errorCount -eq 0) { $resultMessages += [MessageData]::new("All alerts have been removed from the subscription successfully`r`n" + [Constants]::SingleDashLine, [MessageType]::Update); } elseif($errorCount -eq $enabledAlerts.Count) { $resultMessages += [MessageData]::new("No alerts have been removed from the subscription due to error occurred. Please add the alerts manually.`r`n" + [Constants]::SingleDashLine, [MessageType]::Error); } else { $resultMessages += [MessageData]::new("$errorCount/$($enabledAlerts.Count) alert(s) have not been removed from the subscription. Please remove the alerts manually.", [MessageType]::Error); $resultMessages += [MessageData]::new("$($enabledAlerts.Count - $errorCount)/$($enabledAlerts.Count) alert(s) have been removed from the subscription successfully`r`n" + [Constants]::SingleDashLine, [MessageType]::Update); } $messages += $resultMessages; $this.PublishCustomMessage($resultMessages); # Create the lock $messages += $this.AddResourceGroupLock(); } } else { $this.PublishCustomMessage("No alerts have been found that matches the specified tags. Tags:[$([string]::Join(",", $this.FilterTags))].", [MessageType]::Warning); } } else { $this.PublishCustomMessage("No alerts found in the alert policy file", [MessageType]::Warning); } } } else { $this.PublishCustomMessage("No AzSDK configured alerts found in the subscription. Resource group not found: $($this.ResourceGroup)", [MessageType]::Warning); } return $messages; } hidden [MessageData[]] RemoveAllResourceGroupLocks() { $messages = @(); #Remove Resource Lock on Resource Group if any $locks = @(); $locks += Get-AzureRmResourceLock -ResourceGroupName $this.ResourceGroup if($locks.Count -ne 0) { $messages += [MessageData]::new("Removing following existing resource group locks so that alerts can be processed. Total: $($locks.Count)", $locks); $locks | ForEach-Object { Remove-AzureRmResourceLock -LockId $_.LockId -Force | Out-Null } Start-Sleep -Seconds 60 } return $messages; } hidden [MessageData[]] AddResourceGroupLock() { $messages = @(); # Create the lock $lockName = "AzSDKAlertLock"; $lockObject = New-AzureRmResourceLock -ResourceGroupName $this.ResourceGroup -LockName $lockName -LockLevel ReadOnly -LockNotes "Lock created by AzSDK to protect alert objects" -Force $messages += [MessageData]::new("Created ResourceGroup lock to protect the alert objects. Lock name: $lockName", $lockObject); return $messages; } [MessageData[]] SetAlertsPreview([string] $targetResourceGroup, [string] $securityContactEmails, [string] $alertResourceGroupLocation) { # Parameter validation if([string]::IsNullOrWhiteSpace($securityContactEmails)) { throw [System.ArgumentException] ("The argument 'securityContactEmails' is null or empty"); } $allEmails = @(); $allEmails += $this.ConvertToStringArray($securityContactEmails); if($allEmails.Count -eq 0) { throw [System.ArgumentException] ("Please provide valid email address(es)"); } if(-not [string]::IsNullOrWhiteSpace($alertResourceGroupLocation)) { $this.ResourceGroupLocation = $alertResourceGroupLocation; } [MessageData[]] $messages = @(); if(($this.Policy | Measure-Object).Count -ne 0) { $alertList = $this.GetApplicableAlerts(); if($alertList -ne 0) { $this.PublishCustomMessage("Warning: Preview mode will configure alerts with critical severity only.",[MessageType]::Warning); $criticalAlerts = $alertList | Where-Object { $_.Severity -eq "Critical" } $startMessage = [MessageData]::new("Processing AzSDK alerts. Total alerts: $($criticalAlerts.Count)"); $messages += $startMessage; $this.PublishCustomMessage($startMessage); $this.PublishCustomMessage("Note: Configuring alerts can take about 10-12 minutes depending on number of alerts to be processed...", [MessageType]::Warning); $disabledAlerts = $criticalAlerts | Where-Object { -not $_.Enabled }; if(($disabledAlerts | Measure-Object).Count -ne 0) { $disabledMessage = "Found alerts which are disabled. This is intentional. Total disabled alerts: $($disabledAlerts.Count)"; $messages += [MessageData]::new($disabledMessage, $disabledAlerts); } $enabledAlerts = @(); $enabledAlerts += $criticalAlerts | Where-Object { $_.Enabled }; if($enabledAlerts.Count -ne 0) { $messages += [MessageData]::new([Constants]::SingleDashLine + "`r`nAdding following alerts to the subscription. Total alerts: $($enabledAlerts.Count)", $enabledAlerts); # Check if Resource Group exists $existingRG = Get-AzureRmResourceGroup -Name $this.ResourceGroup -ErrorAction SilentlyContinue if(-not $existingRG) { New-AzureRmResourceGroup -Name $this.ResourceGroup -Location $this.ResourceGroupLocation -Tag @{ "AzSDKVersion" = $this.GetCurrentModuleVersion(); "CreationTime" = $this.RunIdentifier; "AzSDKFeature" = "Alerts" } -ErrorAction Stop | Out-Null } $messages += [MessageData]::new("All the alerts registered by this script will be placed in a resource group named: $($this.ResourceGroup)"); #Remove Resource Lock on Resource Group if any $messages += $this.RemoveAllResourceGroupLocks(); #Add-OutputLogEvent -OutputLogFilePath $outputLogFilePath -EventData ("A resource lock will be deployed on this resource group to protect from accidental deletion.") -RawOutput $isTargetResourceGroup = -not [string]::IsNullOrWhiteSpace($targetResourceGroup); # Accepting only first email address because of Azure issue in creating Alert rule with multiple emails $actionGroupResourceId = $this.SetupAlertActionGroup($allEmails) try { $criticalAlertList = @() $alertArm = $this.LoadServerConfigFile("Subscription.AlertARM.json"); $alert = $alertArm.resources | Select -First 1 $enabledAlerts | ForEach-Object { $alertObj = $alert.PSObject.Copy() $alertObj.name = $_.Name $alertObj.properties.description = $_.Description $operationDetails = $alertObj.properties.condition.allOf | Where-Object {$_.field -eq "operationName"} $operationDetails.equals = $_.OperationName $alertObj.properties.actions.actionGroups[0].actionGroupId = $actionGroupResourceId $criticalAlertList += $alertObj } $alertArm.resources = $criticalAlertList $armTemplatePath = "$env:TEMP/Subscription.AlertARM.json" $alertArm | ConvertTo-Json -Depth 100 | Out-File $armTemplatePath $alertDeployment = New-AzureRmResourceGroupDeployment -Name "AzSDKAlertsDeployment" -ResourceGroupName $this.ResourceGroup -TemplateFile $armTemplatePath -ErrorAction Stop Remove-Item $armTemplatePath -ErrorAction SilentlyContinue } catch { $messages += [MessageData]::new("Error while deploying alerts to the subscription", $_, [MessageType]::Error); } [MessageData[]] $resultMessages = @(); #Logic to validate if Alerts are configured. $configuredAlerts = @(); $configuredAlerts = Get-AzureRmResource -ResourceType "Microsoft.Insights/activityLogAlerts" -ResourceGroupName $this.ResourceGroup $actualConfiguredAlertsCount = ($configuredAlerts | Measure-Object).Count $notConfiguredAlertsCount = $enabledAlerts.Count - $actualConfiguredAlertsCount if( $actualConfiguredAlertsCount -eq $enabledAlerts.Count) { $resultMessages += [MessageData]::new("All AzSDK alerts have been configured successfully.`r`n" + [Constants]::SingleDashLine, [MessageType]::Update); } else { $resultMessages += [MessageData]::new("$notConfiguredAlertsCount/$($enabledAlerts.Count) alert(s) have not been added to the subscription. Please add the alerts manually.", [MessageType]::Error); $resultMessages += [MessageData]::new("$actualConfiguredAlertsCount/$($enabledAlerts.Count) alert(s) have been added to the subscription successfully`r`n" + [Constants]::SingleDashLine, [MessageType]::Update); } $messages += $resultMessages; $this.PublishCustomMessage($resultMessages); # Create the lock $messages += $this.AddResourceGroupLock(); } } else { $this.PublishCustomMessage("No alerts have been found that matches the specified tags. Tags:[$([string]::Join(",", $this.FilterTags))].", [MessageType]::Warning); } } else { $this.PublishCustomMessage("No alerts found in the alert policy file", [MessageType]::Warning); } return $messages; } hidden [string] SetupAlertActionGroup([string[]] $securityContactEmails) { $actionGroupResourceId = $null try{ #Get ARM template for action group $actionGroupArm = $this.LoadServerConfigFile("Subscription.AlertActionGroup.json"); $actionGroupArmResource = $actionGroupArm.resources | Where-Object { $_.Name -eq "AzSDKAlertActionGroup" } $emailReceivers = $actionGroupArmResource.properties.emailReceivers | Select-Object -first 1 $emailReceiversList = @(); $securityContactEmails | ForEach-Object { if(-not [string]::IsNullOrWhiteSpace($securityContactEmails)) { $email = $emailReceivers.PsObject.Copy() $email.name = $_.Split('@')[0] $email.emailAddress = $_ $emailReceiversList += $email } } $actionGroupArmResource.properties.emailReceivers = $emailReceiversList $armTemplatePath = "$env:TEMP/Subscription.AlertActionGroup.json" $actionGroupArm | ConvertTo-Json -Depth 100 | Out-File $armTemplatePath $actionGroupResource = New-AzureRmResourceGroupDeployment -Name "AzSDKAlertActionGroupDeployment" -ResourceGroupName $this.ResourceGroup -TemplateFile $armTemplatePath -ErrorAction Stop $actionGroupId = $actionGroupResource.Outputs | Where-Object actionGroupId $actionGroupResourceId = $actionGroupId.Values | Select -ExpandProperty Value Remove-Item $armTemplatePath -ErrorAction SilentlyContinue } catch { $messages += [MessageData]::new("Error while deploying alerts action group to the subscription", $_, [MessageType]::Error); } return $actionGroupResourceId } } |