Private/ConvertTo-AACSecurityRecommendation.ps1

function ConvertTo-AACSecurityRecommendation {
    <#
    .SYNOPSIS
        Turns Defender for Cloud's unhealthy assessments (the Recommendations
        query of Get-AACDefenderQuery) into AAC.SecurityRecommendation
        objects - one per recommendation and resource, most severe first.
        Shared by Get-AACInventory and Get-AACSecurityPosture.
    .DESCRIPTION
        The resource's name, type and resource group come from its ID;
        -SubscriptionName names the subscriptions; -ControlOf (assessment key
        -> secure score control) names the control each recommendation
        belongs to. RemediationUrl is the recommendation's page in the Azure
        portal.
    #>

    [CmdletBinding()]
    [OutputType('AAC.SecurityRecommendation')]
    param(
        [AllowEmptyCollection()]
        [object[]] $Row = @(),

        [hashtable] $SubscriptionName = @{},

        [hashtable] $ControlOf = @{}
    )

    $value = { param($Object, [string] $Key) if ($Object -is [System.Collections.IDictionary]) { if ($Object.Contains($Key)) { $Object[$Key] } } else { Get-AACPropertyValue -InputObject $Object -Name $Key } }
    $text = { param($Object, [string] $Key) $v = & $value $Object $Key; if ($null -eq $v) { '' } else { [string]$v } }
    $severityRank = @{ High = 0; Medium = 1; Low = 2 }
    $items = @(foreach ($entry in $Row) {
            $id = & $text $entry 'resourceId'
            $subscriptionId = (& $text $entry 'subscriptionId').ToLowerInvariant()
            if (-not $subscriptionId -and $id -match '^/subscriptions/([^/]+)') { $subscriptionId = $Matches[1] }
            $type = if ($id -match '/providers/(.+)$') {
                $segments = $Matches[1] -split '/'
                # Namespace, then every other segment: microsoft.sql/servers/databases.
                ((@($segments[0]) + @(for ($i = 1; $i -lt $segments.Count; $i += 2) { $segments[$i] })) -join '/').ToLowerInvariant()
            }
            elseif ($id -match '^/subscriptions/[^/]+/resourcegroups/[^/]+$') { 'microsoft.resources/resourcegroups' }
            elseif ($id -match '^/subscriptions/[^/]+$') { 'microsoft.resources/subscriptions' }
            else { '' }
            $subscriptionLabel = if ($SubscriptionName.Contains($subscriptionId)) { $SubscriptionName[$subscriptionId] } else { $subscriptionId }
            $key = (& $text $entry 'key').ToLowerInvariant()
            $link = & $text $entry 'link'
            $since = [datetime]::MinValue
            [pscustomobject][ordered]@{
                PSTypeName       = 'AAC.SecurityRecommendation'
                Recommendation   = & $text $entry 'name'
                Severity         = & $text $entry 'severity'
                Impact           = & $text $entry 'impact'
                Effort           = & $text $entry 'effort'
                Category         = & $text $entry 'categories'
                Control          = $(if ($key -and $ControlOf.Contains($key)) { $ControlOf[$key] } else { '' })
                Resource         = $(if ($type -eq 'microsoft.resources/subscriptions') { $subscriptionLabel } elseif ($id) { ($id -split '/')[-1] } else { '' })
                Type             = $type
                ResourceGroup    = $(if ($id -match '/resourcegroups/([^/]+)') { $Matches[1] } else { '' })
                SubscriptionName = $subscriptionLabel
                SubscriptionId   = $subscriptionId
                Cause            = & $text $entry 'cause'
                Description      = & $text $entry 'description'
                Remediation      = & $text $entry 'remediation'
                RemediationUrl   = $(if ($link) { if ($link -match '^https?://') { $link } else { "https://$link" } } else { '' })
                Since            = $(if ([datetime]::TryParse((& $text $entry 'since'), [cultureinfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal, [ref]$since)) { $since } else { $null })
                ResourceId       = $id
                AssessmentKey    = $key
            }
        })
    @($items | Sort-Object -Property @{ Expression = { if ($severityRank.Contains($_.Severity)) { $severityRank[$_.Severity] } else { 3 } } }, Recommendation, Resource)
}