Private/Get-AACDefenderAssessmentQuery.ps1
|
function Get-AACDefenderAssessmentQuery { <# .SYNOPSIS What Invoke-AACDefenderAssessment reads: the Azure Resource Graph queries (securityresources) and the Defender for Cloud REST API calls for the settings Resource Graph doesn't hold. .DESCRIPTION -Section picks what is read (Recommendations, AttackPaths, Alerts, Inventory, Vulnerabilities, Posture, Compliance, Settings); the subscriptions and the Defender plans are always read. Returns @{ Graph (an ordered hashtable for Invoke-AACGraphBatch); Rest (a hashtable: name -> the URI under a subscription, with {0} for its ID) }. Resource Graph: subscriptions names Plans the Defender plans (pricings) with their sub-plan, extensions and since when Scores, Controls, ControlAssessments secure scores and their controls (Get-AACDefenderQuery) RecommendationSummary per recommendation: its unhealthy, healthy and not applicable resources and its metadata Unhealthy every unhealthy recommendation on a resource, with its risk level, risk factors and attack paths (Defender CSPM) InventorySummary per resource: its assessments by status and unhealthy ones by severity AttackPaths attack paths (Defender CSPM) Alerts security alerts generated within -AlertDays, every status Vulnerabilities unhealthy sub-assessments: CVEs on machines, SQL and container images Standards, ComplianceControls, ComplianceAssessments regulatory compliance (Get-AACDefenderQuery) REST (per subscription): Contacts security contacts and notifications Settings integrations (MDE, Defender for Cloud Apps, Sentinel) Jit just-in-time VM access policies Connectors AWS, GCP, GitHub, Azure DevOps and GitLab connectors Suppression alert suppression rules Every Resource Graph query keeps an id column, so it is paged. #> [CmdletBinding()] [OutputType([hashtable])] param( [ValidateSet('Recommendations', 'AttackPaths', 'Alerts', 'Inventory', 'Vulnerabilities', 'Posture', 'Compliance', 'Settings')] [string[]] $Section = @('Recommendations', 'AttackPaths', 'Alerts', 'Inventory', 'Vulnerabilities', 'Posture', 'Compliance', 'Settings'), [ValidateRange(1, 365)] [int] $AlertDays = 30 ) $resourceId = 'tolower(coalesce(tostring(properties.resourceDetails.Id), tostring(properties.resourceDetails.ResourceId), tostring(properties.resourceDetails.AzureResourceId), tostring(properties.resourceDetails.NativeResourceId)))' $assessments = "securityresources | where type =~ 'microsoft.security/assessments' | extend resourceId = $resourceId, status = tostring(properties.status.code), severity = tostring(properties.metadata.severity)" $defender = Get-AACDefenderQuery -Name 'Scores', 'Controls', 'ControlAssessments', 'Standards', 'ComplianceControls', 'ComplianceAssessments' $graph = [ordered]@{} $graph['subscriptions'] = "resourcecontainers | where type =~ 'microsoft.resources/subscriptions' | project id, subscriptionId, name" $graph['Plans'] = "securityresources | where type =~ 'microsoft.security/pricings' | where properties.deprecated != true | project id, subscriptionId, plan = name, tier = tostring(properties.pricingTier), subPlan = tostring(properties.subPlan), since = tostring(properties.enablementTime), extensions = properties.extensions" if ($Section -contains 'Recommendations' -or $Section -contains 'Posture') { $graph['Scores'] = $defender['Scores'] + ' | extend id = subscriptionId' $graph['Controls'] = $defender['Controls'] + ", id = strcat(subscriptionId, '|', tostring(properties.displayName))" $graph['ControlAssessments'] = $defender['ControlAssessments'] + " | extend id = strcat(control, '|', key)" } if ($Section -contains 'Recommendations') { $graph['RecommendationSummary'] = "$assessments | summarize unhealthy = countif(status == 'Unhealthy'), healthy = countif(status == 'Healthy'), notApplicable = countif(status == 'NotApplicable'), subscriptions = dcountif(subscriptionId, status == 'Unhealthy'), name = take_any(tostring(properties.displayName)), severity = take_any(severity), impact = take_any(tostring(properties.metadata.userImpact)), effort = take_any(tostring(properties.metadata.implementationEffort)), categories = take_any(strcat_array(properties.metadata.categories, ', ')), threats = take_any(strcat_array(properties.metadata.threats, ', ')), description = take_any(tostring(properties.metadata.description)), remediation = take_any(tostring(properties.metadata.remediationDescription)), assessmentType = take_any(tostring(properties.metadata.assessmentType)), preview = take_any(tostring(properties.metadata.preview)), policyDefinitionId = take_any(tostring(properties.metadata.policyDefinitionId)) by key = tolower(name) | extend id = key" } if ($Section -contains 'Recommendations' -or $Section -contains 'Inventory') { $graph['Unhealthy'] = "$assessments | where status == 'Unhealthy' | project id = tolower(id), key = tolower(name), resourceId, subscriptionId, name = tostring(properties.displayName), severity, cause = tostring(properties.status.cause), statusDescription = tostring(properties.status.description), link = tostring(properties.links.azurePortal), since = tostring(properties.status.statusChangeDate), firstEvaluated = tostring(properties.status.firstEvaluationDate), riskLevel = tostring(properties.risk.level), riskFactors = properties.risk.riskFactors, attackPaths = array_length(properties.risk.attackPathsReferences), source = tostring(properties.resourceDetails.Source)" } if ($Section -contains 'Inventory') { $graph['InventorySummary'] = "$assessments | where isnotempty(resourceId) and status in ('Healthy', 'Unhealthy', 'NotApplicable') | summarize healthy = countif(status == 'Healthy'), unhealthy = countif(status == 'Unhealthy'), notApplicable = countif(status == 'NotApplicable'), high = countif(status == 'Unhealthy' and severity == 'High'), medium = countif(status == 'Unhealthy' and severity == 'Medium'), low = countif(status == 'Unhealthy' and severity == 'Low'), source = take_any(tostring(properties.resourceDetails.Source)) by resourceId, subscriptionId | extend id = resourceId" } if ($Section -contains 'AttackPaths' -or $Section -contains 'Inventory') { # The whole properties: their shape varies, and there are few of them. $graph['AttackPaths'] = "securityresources | where type =~ 'microsoft.security/attackpaths' | project id, name, subscriptionId, properties" } if ($Section -contains 'Alerts' -or $Section -contains 'Inventory') { $graph['Alerts'] = "securityresources | where type =~ 'microsoft.security/locations/alerts' | extend p = properties | extend time = todatetime(coalesce(p.TimeGeneratedUtc, p.timeGeneratedUtc)) | where time > ago($($AlertDays)d) | project id, subscriptionId, status = tostring(coalesce(p.Status, p.status)), name = tostring(coalesce(p.AlertDisplayName, p.alertDisplayName)), severity = tostring(coalesce(p.Severity, p.severity)), intent = tostring(coalesce(p.Intent, p.intent)), techniques = coalesce(p.Techniques, p.techniques), subTechniques = coalesce(p.SubTechniques, p.subTechniques), alertType = tostring(coalesce(p.AlertType, p.alertType)), time, start = tostring(coalesce(p.StartTimeUtc, p.startTimeUtc)), end = tostring(coalesce(p.EndTimeUtc, p.endTimeUtc)), description = tostring(coalesce(p.Description, p.description)), remediation = coalesce(p.RemediationSteps, p.remediationSteps), link = tostring(coalesce(p.AlertUri, p.alertUri)), entity = tostring(coalesce(p.CompromisedEntity, p.compromisedEntity)), resources = coalesce(p.ResourceIdentifiers, p.resourceIdentifiers), product = tostring(coalesce(p.ProductName, p.productName)), isIncident = tostring(coalesce(p.IsIncident, p.isIncident))" } if ($Section -contains 'Vulnerabilities' -or $Section -contains 'Inventory') { $graph['Vulnerabilities'] = "securityresources | where type =~ 'microsoft.security/assessments/subassessments' | where tostring(properties.status.code) == 'Unhealthy' | project id, subscriptionId, assessmentKey = tolower(extract('(?i)/assessments/([^/]+)/subassessments/', 1, id)), resourceId = tolower(coalesce(tostring(properties.resourceDetails.id), tostring(properties.resourceDetails.Id), extract('(?i)^(.+)/providers/Microsoft\\.Security/assessments/', 1, id))), name = tostring(properties.displayName), severity = tostring(properties.status.severity), category = tostring(properties.category), impact = tostring(properties.impact), remediation = tostring(properties.remediation), vulnerabilityId = tostring(properties.id), cve = properties.additionalData.cve, cveId = tostring(properties.additionalData.vulnerabilityDetails.cveId), patchable = tostring(properties.additionalData.patchable), image = tostring(coalesce(properties.additionalData.artifactDetails.repositoryName, properties.additionalData.repositoryName)), generated = tostring(properties.timeGenerated)" } if ($Section -contains 'Compliance') { $graph['Standards'] = $defender['Standards'] + " | extend id = strcat(subscriptionId, '|', standard)" $graph['ComplianceControls'] = $defender['ComplianceControls'] + " | extend id = strcat(subscriptionId, '|', standard, '|', control)" $graph['ComplianceAssessments'] = $defender['ComplianceAssessments'] + " | extend id = strcat(subscriptionId, '|', standard, '|', control, '|', key)" } $rest = @{} if ($Section -contains 'Settings' -or $Section -contains 'Posture') { $rest['Contacts'] = '/subscriptions/{0}/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview' $rest['Settings'] = '/subscriptions/{0}/providers/Microsoft.Security/settings?api-version=2022-05-01' $rest['Connectors'] = '/subscriptions/{0}/providers/Microsoft.Security/securityConnectors?api-version=2023-10-01-preview' $rest['Jit'] = '/subscriptions/{0}/providers/Microsoft.Security/jitNetworkAccessPolicies?api-version=2020-01-01' } if ($Section -contains 'Alerts' -or $Section -contains 'Settings') { $rest['Suppression'] = '/subscriptions/{0}/providers/Microsoft.Security/alertsSuppressionRules?api-version=2019-01-01-preview' } @{ Graph = $graph; Rest = $rest } } |