Tags:
Welcome to the first in a series of blog posts about automating your Azure security worries away. In this post, we’ll cover how to automate the assessment and reporting of your cloud security configuration opportunities.
Cloud computing is a wonderful innovation, allowing organizations to quickly and efficiently build services to support their operations. In a DevOps world, it allows for maximizing creativity without the headache of building traditional infrastructure. However, since you’re visiting the SANS website, you probably already know that public cloud can also be a dangerous place. With all this wonderful automation and ease of use, it’s sometimes TOO easy to build something rife with vulnerabilities or misconfiguration. As a bonus, public clouds are inherently Internet-facing. We’ve all read stories about those misconfigured storage buckets with troves of data available to whoever brute forces the resource name.
Blue Teamers' jobs are hard enough; we shouldn’t also have to worry about self-inflicted mistakes leading to incidents. Fortunately, public cloud providers generally provide capabilities to quickly identify and correct these mistakes. Furthermore, with PowerShell automation (see SEC586 for all kinds of Blue Team automation with PowerShell), we don’t need to browse to yet another portal to find this information. This post will cover PowerShell automation to centralize vulnerabilities and risks without opening yet another browser tab.
Setup Information
To follow along, you’ll need the following:
- An Azure Tenant with resources deployed
- Azure Defender for Cloud enabled
- PowerShell
- Az Module
Getting Started
First things first – if you have zero security recommendations, the following automation will produce…nothing… and we should be happy about that! That said, this automation can still be used to ensure that when a mistake happens, we’ll know quickly and can enact remediation plans. We’ll use a throwaway subscription with a vulnerable storage account to demonstrate how this can be used for lightning-fast, custom detection.
Digging In
Let’s go find some issues in an Azure Tenant. Using the Az.Accounts module’s `Connect-AzAccount` cmdlet, we can connect to an existing tenant/subscription. The cmdlet supports several types of authentication, and this example assumes the user is leveraging an interactive login with Multi-Factor Authentication. A browser window will open where the user can supply credentials and pass the MFA challenge.
Connect-AzAccount
The account is now connected, and one or more contexts may be available. In complex environments with multiple subscriptions, tenants, etc. it is important to ensure we’re working in the correct context.
Get-AzContext will list the current context of the session, and the -ListAvailable parameter can show available contexts. Note that this is limited to 25 which may be far less than the total possible. See this resource for more information on how to use Connect-AzAccount -MaxContextPopulation to be able to query against a larger set. Get-AzSubscription can also be used to find one’s bearings and set a new context if new to the Az PowerShell module.
Once oriented appropriately, as with all things PowerShell, there are multiple ways to achieve our goal.
Option 1 – Az cmdlets
The simplest is to leverage the Az module’s cmdlets to pull back basic vulnerability data. Get-AzSecurityAssessment can be run with no parameters to return security assessment results. The output object’s type can be examined via Get-Member to understand what properties exist.
Get-AzSecurityAssessment | Get-Member
Next, let’s take a look at a sample return object by selecting the first returned assessment:
Get-AzSecurityAssessment | Select-Object -First 1
You might be asking, “What is the status of this finding?” The Status property is another object and can be investigated using Select-Object with -ExpandProperty as well.
Get-AzSecurityAssessment | Select-Object -First 1 -ExpandProperty Status
This shows that we’re already configured correctly when it comes to KeyVault leveraging Defender. Let’s exclude any findings where the Status property has a code of Healthy. Unfortunately, there aren’t many parameters with this cmdlet to adopt the “Filter Left, Format Right” paradigm, and instead we can filter on the client-side, albeit less efficiently.
Get-AzSecurityAssessment | Where-Object {$_.Status.Code -ne "Healthy"} | Select-Object -First 1
We’re now working with return objects where the status is something other than “Healthy.” Let’s dig into the ResourceDetails property to understand which resource is misconfigured. Similar to the Status property, the ResourceDetails property is also an object. We can expand it to inspect its contents:
Get-AzSecurityAssessment | Where-Object {$_.Status.Code -ne "Healthy"} | Select-Object -First 1 -ExpandProperty ResourceDetails
So our ‘badstorageacct’ storage account is misconfigured. Let’s find out more about this resource:
Remember that the beauty of PowerShell’s pipeline is that we can pass output objects from a cmdlet as input objects to a separate cmdlet (assuming type compatibility). Get-AzResource accepts objects of type Microsoft.Azure.Commands.Security.Models.Assessments.PSSecurityAzureResourceDetails, so we can simply pipe the output of the previous command into Get-AzResource to understand more about our vulnerable resource.
Get-AzSecurityAssessment | Where-Object {$_.Status.Code -ne "Healthy"} | Select-Object -First 1 -ExpandProperty ResourceDetails | Get-AzResource
Notice we now have the context as to where this exists, including the resource group name and any tags that are applied to the resource.
Let’s now tie the two objects together, taking useful pieces of each and creating a custom object representing our finding as well as the affected resource. One way to do this is to leverage a Foreach-Object loop, where $PSItem or $_ represents the input object(s) and allows us to access properties of interest. Note that some findings may be associated with the overall subscription and Get-AzResource will throw an error. We can suppress those errors with the -ErrorAction SilentlyContinue parameter. We can take the output of each call to Get-AzResource and combine it with the finding by creating a PSCustomObject containing the properties in which we are interested.
Get-AzSecurityAssessment | Where-Object {$_.Status.Code -ne "Healthy"} | Foreach-Object {
$vuln = $_
$_ | Select-Object -First 1 -ExpandProperty ResourceDetails |
ForEach-Object {
$affectedResource = $_ | Get-AzResource -ErrorAction
SilentlyContinue
[PSCustomObject]@{
Vulnerability = $vuln.DisplayName
ResourceID = $affectedResource.ResourceId
Name = $affectedResource.Name
Tags = $affectedResource.Tags
}
}
}
We can also add logic such that if the resource is defined at a subscription level, return subscription details instead of resource details. However, the current technique is requiring significant client-side filtering and multiple requests from the client. Let’s take a look at an alternative technique to gather similar data.
Option 2 – Azure Resource Graph
Azure Resource Graph is a powerful utility that allows for quick and efficient queries against subscriptions, resources, and even security recommendations. Resource Graph leverages the Kusto Query Language (KQL) which provides a full-featured syntax meant for querying databases. With Resource Graph, Azure provides several prebuilt and prepopulated tables with useful data. See the Microsoft docs for more information on KQL Syntax.
Compared to our first approach towards aggregating resource and assessment data, Resource Graph queries allow us to offload all compute and aggregation tasks to the Azure cloud and simplifies the PowerShell we need to write. Furthermore, when initiating the queries via PowerShell, we are returned objects with the properties we’re requesting. The difference in our technique means that or KQL Query logic must perform the queries and aggregations, and we will use the Az module’s Search-AzGraph cmdlet to execute the query and handle the results. To learn how to build queries and ensure we’re capturing the correct data for automation, the Azure Resource Graph Explorer is a fantastic tool to build queries.
First things first, we need to connect to Azure. Connect-AzAccount is still the cmdlet here, even though we’re using a different mechanism to query for our data.
Once connected, we can craft a simple KQL query to ensure things are working properly. Security assessments are contained within the SecurityResources table, with columns and nested properties providing context as to the finding, affected resource and more. KQL queries are often several lines, and in many PowerShell examples, look like one long string. To help with readability, the here-string can allow for a multi-line string to be supplied as the KQL query. Let’s start with a simple query to return assessment results, like the output of Get-AzSecurityAssessment.
$query = @"
SecurityResources
| where type == 'microsoft.security/assessments'
"@
Search-AzGraph -Query $query
Several results are returned. Instead of filtering output with Where-Object, instead we can now leverage KQL to offload that filtering to Resource Graph using the where operator.
Let’s filter on the subscription of interest (if you’re following along with copy/paste make sure you update the subscriptionId field to match a subscription you can access!) and on the status code to ensure we’re not returning resources that are already healthy/applicable.
$query = @"
SecurityResources
| where type == 'microsoft.security/assessments'
| where subscriptionId == "40a48b56-e4b2-4fda-a655-8948290f2e40"
| where properties.status.code !in ("Healthy","NotApplicable")
"@
Search-AzGraph -Query $query
Now that we have known issues, let’s use the project operator to return only fields of interest and produce a summary report, leveraging the summarize and order operators. Note that when we use dot notation to dig into nested properties, the returned object includes properties replacing the dots with underscores, making it straightforward to keep track of where data originated.
$query = @"
SecurityResources
| where type == 'microsoft.security/assessments'
| where subscriptionId == "40a48b56-e4b2-4fda-a655-8948290f2e40"
| where properties.status.code !in ("Healthy","NotApplicable")
| project properties.status.firstEvaluationDate,properties.status.code,properties.displayName,properties.metadata.severity,properties.resourceDetails.Id,properties.links.azurePortal
| summarize count() by tostring(properties_metadata_severity),tostring(properties_displayName)
| order by ['count_'] desc
"@
$results = Search-AzGraph -Query $query
This can be useful for high-level reporting, but it still doesn’t show us details of affected resources. We can re-run that query and store the results in a variable to leverage the findings and identify resources needing changes:
$results = Search-AzGraph -Query $query
With findings stored in the $results variable, we can iterate through each finding using a ForEach-Object loop, identifying affected resources. We can use string substitution in order to update our query each pass through the loop, such that each pass looks for a different finding. A nested ForEach-Object loop can then inspect, for each finding, affected resources. A PSCustomObject can be created to capture relevant finding data, combined with details for each specific resource. Here is the full code snippet:
$reportObj = @()
#iterate through each finding, projecting details including the affected resource ID
$results | ForEach-Object {
#Query is updated to look at a different finding each pass through the loop
$findingQuery = @"
SecurityResources
| where type == 'microsoft.security/assessments'
| where subscriptionId == "40a48b56-e4b2-4fda-a655-8948290f2e40"
| where properties.status.code !in ("Healthy","NotApplicable")
| where properties.displayName == `"$($_.properties_displayName)`"
| project properties.displayName,properties.status.code,properties.status.firstEvaluationDate,properties.resourceDetails.Id,properties.links.azurePortal
"@
#Search-AzGraph will only return the top 100 results by default. A maximum of 1000 results can be specified
$findingResults = Search-AzGraph -Query $findingQuery -First 1000
#Now iterate through each affected resource
$findingResults | Foreach-Object {
#Query is updated to look at each affected resource for each finding
$resourceQuery = @"
Resources
| where id =~ `"$($_.properties_resourceDetails_Id)`"
| mv-expand tags=tags
| project tags
"@
$resourceDetails = Search-AzGraph -query $resourceQuery
#Custom object captures all relevant data about the finding and affected resource
$resultObj = [PSCustomObject]@{
AllTags = $resourceDetails.Tags
Type = $resourceDetails.Tags.'type' | Where-Object {$_ -ne $null}
Team = $resourceDetails.Tags.'team' | Where-Object {$_ -ne $null}
Contact = $resourceDetails.Tags.'adminContact' | Where-Object {$_ -ne $null}
ResourceId = $_.properties_resourceDetails_Id
Finding = $_.properties_displayName
FindingLink = $_.properties_links_azurePortal
DateIdentified = $_.properties_status_firstEvaluationDate
}
$reportObj += $resultObj
}
}
Inspecting the $reportObj variable, we see that we now have specific details about the findings and affected resources. Tags can be crucial for cloud inventories as they can be used to correlate resources to business purpose, environments and even personnel/groups who can be contacted about findings.
Wrapping Up
Cloud risks don’t have to be difficult to identify and triage. With PowerShell, we have a capable automation framework that can be used to identify and even (stay tuned to this blog series) remediate known issues. While we were running these commands manually and stepping through the output, identification and reporting can be fully automated. Leveraging cloud-native technology like Function Apps, the above PowerShell can be scheduled to run regularly, and reporting of new issues can be automated as well. If Blue Teams are leveraging Teams or Slack as messaging tools, we can use techniques like web hooks to automatically format findings and post to channels for immediate notification of new risks.
Intimated by syntax?
If automation is important to you, and PowerShell seems out of reach, don’t worry! SEC586 Security Automation with PowerShellcovers PowerShell from the ground up, using Blue Team use cases like the above to ensure not just an academic understanding of the syntax, but practical hands-on capabilities. Students with no coding background have attended SEC586 and have left with the ability to automate everything from host-based analytics to cloud integrations. Seasoned professionals with PowerShell experience attending SEC586 can also be confident they will leave with an even deeper understanding and new skills to automate their security programs.
For part 2 of the series, go here: https://www.sans.org/blog/how-to-automate-in-azure-using-powershell-part-2/