Collectors/Defender.ps1
|
function Get-AerDefender { [CmdletBinding()] param( [Parameter(Mandatory)] [string[]] $SubscriptionIds, [Parameter(Mandatory)] $SubscriptionMap ) $subLookup = @{} if ($SubscriptionMap -is [hashtable]) { $subLookup = $SubscriptionMap } elseif ($SubscriptionMap) { $SubscriptionMap.PSObject.Properties | ForEach-Object { $subLookup[$_.Name] = $_.Value } } function SubName($id) { if (-not $id) { return 'Unknown' }; $n = $subLookup[$id.ToLowerInvariant()]; if ($n) { $n } else { $id } } function Leaf($id) { if ($id) { ($id -split '/')[-1] } else { '' } } function FromJson($s) { if ($s -and "$s" -ne 'null') { try { $s | ConvertFrom-Json } catch { $null } } else { $null } } function ArgRows($query) { Expand-AerRows (Invoke-AerArgQuery -SubscriptionIds $SubscriptionIds -Query $query) } function ResScope($resId) { $s = "$resId" $sub = if ($s -match '/subscriptions/([^/]+)') { $matches[1] } else { '' } $rg = if ($s -match '/resourceGroups/([^/]+)') { $matches[1] } else { '' } $subName = if ($sub) { $subLookup[$sub.ToLowerInvariant()] ?? $sub } else { '' } if ($rg) { "$rg ($subName)" } elseif ($subName) { $subName } else { $s } } function NormRisk($risk, $sev) { $r = if ($risk) { "$risk" } elseif ($sev) { "$sev" } else { '' } switch -regex ($r) { '^Critical' { 'Critical' } '^High' { 'High' } '^Medium' { 'Medium' } '^Low' { 'Low' } default { if ($r) { $r } else { 'Unknown' } } } } # ── Security assessments (recommendations) — scalar projections so the # columnar-safe ArgRows can be used ───────────────────────────────────── $recs = [System.Collections.Generic.List[object]]::new() $healthy = 0; $unhealthy = 0; $na = 0 $sev = [ordered]@{ Critical = 0; High = 0; Medium = 0; Low = 0 } try { foreach ($a in (ArgRows @' securityresources | where type =~ 'microsoft.security/assessments' | project id, status = tostring(properties.status.code), statusDesc = tostring(properties.status.description), severity = tostring(properties.metadata.severity), riskLevel = tostring(properties.risk.level), riskFactors = tostring(properties.risk.riskFactors), title = tostring(properties.displayName), remediation = tostring(properties.metadata.remediationDescription), statusChange = tostring(properties.status.statusChangeDate) '@)) { $st = "$($a.status)" if ($st -eq 'Healthy') { $healthy++ } elseif ($st -eq 'Unhealthy') { $unhealthy++ } elseif ($st -eq 'NotApplicable') { $na++ } $risk = NormRisk $a.riskLevel $a.severity if ($st -eq 'Unhealthy' -and $sev.Contains($risk)) { $sev[$risk]++ } $resId = ("$($a.id)" -split '(?i)/providers/microsoft.security/assessments/')[0] $resName = if ($resId -match '^/subscriptions/([^/]+)/?$') { 'Subscription: ' + (SubName $matches[1]) } else { (Leaf $resId) } $rf = @(FromJson $a.riskFactors | ForEach-Object { if ($_ -is [string]) { $_ } elseif ($_.title) { "$($_.title)" } elseif ($_.type) { "$($_.type)" } else { "$_" } }) $chRaw = $a.statusChange $chStr = if ($chRaw -is [datetime]) { $chRaw.ToString('yyyy-MM-dd') } elseif ($chRaw) { ("$chRaw" -replace 'T.*$', '') } else { '' } $recs.Add([pscustomobject]@{ RiskLevel = $risk Title = if ($a.title) { "$($a.title)" } else { (Leaf $a.id) } Resource = $resName ResourceId = $resId Status = $st Scope = (ResScope $resId) LastChange = $chStr RiskFactors = @($rf) Remediation = "$($a.remediation)" Description = "$($a.statusDesc)" }) } } catch { Write-Warning "[Defender.assessments] $($_.Exception.Message)" } # ── Defender plans (pricings) per subscription via REST ────────────────── $subs = [System.Collections.Generic.List[object]]::new() foreach ($sid in $SubscriptionIds) { try { $r = Invoke-AzRestMethod -Method GET -Uri "https://management.azure.com/subscriptions/$sid/providers/Microsoft.Security/pricings?api-version=2024-01-01" if ($r.StatusCode -lt 200 -or $r.StatusCode -ge 300) { continue } $plans = foreach ($p in @(($r.Content | ConvertFrom-Json).value)) { $tier = "$($p.properties.pricingTier)" $active = $tier -eq 'Standard' $exts = foreach ($e in @($p.properties.extensions)) { if (-not $e) { continue } [pscustomobject]@{ Name = "$($e.name)"; Enabled = ($e.isEnabled -eq $true -or "$($e.isEnabled)" -eq 'True') } } $exts = @($exts) $partial = $active -and (@($exts | Where-Object { -not $_.Enabled }).Count -gt 0) [pscustomobject]@{ Name = "$($p.name)"; Tier = $tier; SubPlan = "$($p.properties.subPlan)" Active = $active; Partial = $partial; Extensions = $exts } } $plans = @($plans) $subs.Add([pscustomobject]@{ Subscription = (SubName $sid) StandardCount = @($plans | Where-Object { $_.Active }).Count FreeCount = @($plans | Where-Object { -not $_.Active }).Count Total = $plans.Count Plans = $plans }) } catch { Write-Warning "[Defender.pricings:$sid] $($_.Exception.Message)" } } return [pscustomobject]@{ Summary = [pscustomobject]@{ Total = $healthy + $unhealthy + $na Healthy = $healthy; Unhealthy = $unhealthy; NotApplicable = $na } SeverityCounts = [pscustomobject]@{ Critical = $sev.Critical; High = $sev.High; Medium = $sev.Medium; Low = $sev.Low } Recommendations = @($recs) Subscriptions = @($subs) } } |