functions/Get-XdrExposureManagementRecommendations.ps1
|
function Get-XdrExposureManagementRecommendations { <# .SYNOPSIS Retrieves recommendations from Exposure Management. .DESCRIPTION Gets security recommendations from Microsoft Defender XDR Exposure Management. Supports multiple data sources including TVM recommendations, vulnerability assessments, and device misconfigurations. This function includes caching support with a 30-minute TTL to reduce API calls. By default, returns the recommendations array. .PARAMETER Force Bypasses the cache and forces a fresh retrieval from the API. .PARAMETER CountOnly Returns only the total count of recommendations (numOfResults). .PARAMETER Top Limits the number of results returned. Useful for previewing data without fetching all pages. .PARAMETER Tags Retrieves vulnerability assessment recommendation tags. .PARAMETER ActiveVulnerabilities Retrieves vulnerability assessment recommendations with filters (Active or PartialException status). .PARAMETER AllVulnerabilities Retrieves all vulnerability assessment recommendations without status filters. .PARAMETER Misconfigurations Retrieves device misconfiguration recommendations from the posture/oversight API. .PARAMETER ByAssets Retrieves assets for a specific recommendation. Requires -RecommendationId. .PARAMETER ByOperatingSystems Retrieves operating systems for a specific recommendation. Requires -RecommendationId. .PARAMETER RecommendationId The recommendation ID required for -ByAssets and -ByOperatingSystems parameters. .EXAMPLE Get-XdrExposureManagementRecommendations Retrieves all recommendations using cached data if available. .EXAMPLE Get-XdrExposureManagementRecommendations -Force Forces a fresh retrieval of recommendations, bypassing the cache. .EXAMPLE Get-XdrExposureManagementRecommendations -CountOnly Returns only the total number of recommendations. .EXAMPLE Get-XdrExposureManagementRecommendations -Top 10 Returns only the first 10 recommendations. .EXAMPLE Get-XdrExposureManagementRecommendations -Tags Retrieves vulnerability assessment recommendation tags. .EXAMPLE Get-XdrExposureManagementRecommendations -ActiveVulnerabilities Retrieves vulnerability assessment recommendations with Active or PartialException status. .EXAMPLE Get-XdrExposureManagementRecommendations -AllVulnerabilities Retrieves all vulnerability assessment recommendations regardless of status. .EXAMPLE Get-XdrExposureManagementRecommendations -Misconfigurations Retrieves device misconfiguration recommendations. .EXAMPLE Get-XdrExposureManagementRecommendations -ByAssets -RecommendationId "sca-_-scid-69" Retrieves assets for a specific recommendation. .EXAMPLE Get-XdrExposureManagementRecommendations -ByOperatingSystems -RecommendationId "va-_-microsoft-_-windows_11" Retrieves operating system breakdown for a specific recommendation. .OUTPUTS System.Object[] Returns an array of recommendation objects. Output varies based on parameters used. .OUTPUTS System.Int64 When -CountOnly is specified, returns the total count as an integer. #> # Suppress false positive: Switch parameters are used via $PSCmdlet.ParameterSetName, not direct reference [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType([System.Object[]])] [OutputType([System.Int64], ParameterSetName = 'CountOnly')] param ( [Parameter()] [switch]$Force, [Parameter(ParameterSetName = 'CountOnly')] [switch]$CountOnly, [Parameter()] [ValidateRange(1, 10000)] [int]$Top, [Parameter(ParameterSetName = 'Tags')] [switch]$Tags, [Parameter(ParameterSetName = 'ActiveVulnerabilities')] [switch]$ActiveVulnerabilities, [Parameter(ParameterSetName = 'AllVulnerabilities')] [switch]$AllVulnerabilities, [Parameter(ParameterSetName = 'Misconfigurations')] [switch]$Misconfigurations, [Parameter(ParameterSetName = 'ByAssets')] [switch]$ByAssets, [Parameter(ParameterSetName = 'ByOperatingSystems')] [switch]$ByOperatingSystems, [Parameter(ParameterSetName = 'ByAssets', Mandatory = $true)] [Parameter(ParameterSetName = 'ByOperatingSystems', Mandatory = $true)] [string]$RecommendationId ) begin { Update-XdrConnectionSettings # Helper function for paginated requests function Invoke-PaginatedRequest { param ( [hashtable]$Headers, [scriptblock]$BuildUri, [string]$CountProperty = 'numOfResults', [string]$DisplayName, [int]$MaxResults = 0 ) $maxPages = 1000 $pageNum = 1 try { # Get first page $uri = & $BuildUri $pageNum Write-Verbose "Fetching $DisplayName page 1" $response = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $Headers $totalResults = $response.$CountProperty $targetResults = if ($MaxResults -gt 0 -and $MaxResults -lt $totalResults) { $MaxResults } else { $totalResults } Write-Information "Total $DisplayName`: $totalResults$(if ($MaxResults -gt 0) { " (fetching $targetResults)" })" -InformationAction Continue # Collect results with pagination $allResults = [System.Collections.Generic.List[object]]::new() if ($response.results) { $allResults.AddRange($response.results) } $pageNum = 2 # Show progress for larger result sets (more than one page) $showProgress = $targetResults -gt 25 while ($allResults.Count -lt $targetResults -and $pageNum -le $maxPages) { if ($showProgress) { $percentComplete = [math]::Min(100, [math]::Round(($allResults.Count / $targetResults) * 100)) Write-Progress -Activity "Retrieving $DisplayName" -Status "$($allResults.Count) of $targetResults" -PercentComplete $percentComplete } $uri = & $BuildUri $pageNum Write-Verbose "Fetching $DisplayName page $pageNum" $response = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $Headers if ($response.results) { $allResults.AddRange($response.results) } Write-Verbose "Retrieved $($allResults.Count) of $targetResults $DisplayName" $pageNum++ } if ($showProgress) { Write-Progress -Activity "Retrieving $DisplayName" -Completed } # Trim to MaxResults if specified $finalResults = if ($MaxResults -gt 0 -and $allResults.Count -gt $MaxResults) { $allResults.GetRange(0, $MaxResults).ToArray() } else { $allResults.ToArray() } return [PSCustomObject]@{ Count = $totalResults Results = $finalResults } } catch { Write-Progress -Activity "Retrieving $DisplayName" -Completed throw "Failed to retrieve $DisplayName`: $_" } } } process { # Define configuration for each parameter set $config = switch ($PSCmdlet.ParameterSetName) { 'Tags' { @{ CacheKey = "XdrExposureManagementRecommendations_Tags" Simple = $true Endpoint = "/va/tags" Extract = 'tags' } } 'ByOperatingSystems' { @{ CacheKey = "XdrExposureManagementRecommendations_OS_$RecommendationId" Simple = $true Endpoint = "/recommendation/operatingSystems?recommendationId=$RecommendationId" } } 'ByAssets' { @{ CacheKey = "XdrExposureManagementRecommendations_Assets_$RecommendationId" UseTvmHeaders = $true DisplayName = "assets for recommendation" BuildUri = { param($p) "https://security.microsoft.com/apiproxy/mtp/tvm/analytics/recommendations/recommendation/assets?recommendationId=$RecommendationId&pageIndex=$p" } } } 'ActiveVulnerabilities' { $filter = "`$filter=(status+eq+%27Active%27+or+status+eq+%27PartialException%27)" @{ CacheKey = "XdrExposureManagementRecommendations_ActiveVulnerabilities" UseTvmHeaders = $true DisplayName = "ActiveVulnerabilities recommendations" BuildUri = { param($p) "https://security.microsoft.com/apiproxy/mtp/tvm/analytics/recommendations/va?pageIndex=$p&pageSize=25&$filter" }.GetNewClosure() } } 'AllVulnerabilities' { @{ CacheKey = "XdrExposureManagementRecommendations_AllVulnerabilities" UseTvmHeaders = $true DisplayName = "AllVulnerabilities recommendations" BuildUri = { param($p) "https://security.microsoft.com/apiproxy/mtp/tvm/analytics/recommendations/va?pageIndex=$p&pageSize=25" } } } 'Misconfigurations' { $miscFilter = "filters[0].key=category&filters[0].value[0]=DeviceMisconfiguration&filters[0].operator=Contains" @{ CacheKey = "XdrExposureManagementRecommendations_Misconfigurations" UseTvmHeaders = $false DisplayName = "Misconfigurations recommendations" CountProperty = 'recordsCount' BuildUri = { param($p) "https://security.microsoft.com/apiproxy/mtp/posture/oversight/recommendations?calculationId=undefined&sort.sortDirection=desc&sort.sortByField=domainScoreImpact&pagination.pageNumber=$p&pagination.numberOfPageRecords=25&$miscFilter&highlights=false" }.GetNewClosure() } } { $_ -in 'Default', 'CountOnly' } { @{ CacheKey = "XdrExposureManagementRecommendations" UseTvmHeaders = $true DisplayName = "recommendations" BuildUri = { param($p) "https://security.microsoft.com/apiproxy/mtp/tvm/analytics/recommendations?pageIndex=$p" } } } } # Check cache first (skip if -Top is specified as we may need subset) $useCache = -not $Top -or $Top -eq 0 $currentCacheValue = Get-XdrCache -CacheKey $config.CacheKey -ErrorAction SilentlyContinue if ($useCache -and -not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) { Write-Verbose "Using cached data for $($config.CacheKey)" if ($config.Simple) { return $currentCacheValue.Value } $countProp = if ($config.CountProperty) { $config.CountProperty } else { 'numOfResults' } Write-Information "Total $($config.DisplayName): $($currentCacheValue.Value.$countProp)" -InformationAction Continue if ($PSCmdlet.ParameterSetName -eq 'CountOnly') { return $currentCacheValue.Value.$countProp } return $currentCacheValue.Value.results } if ($Force) { Write-Verbose "Force parameter specified, bypassing cache" Clear-XdrCache -CacheKey $config.CacheKey } elseif (-not $useCache) { Write-Verbose "Top parameter specified, fetching fresh data" } else { Write-Verbose "Cache is missing or expired for $($config.CacheKey)" } # Prepare headers $requestHeaders = if ($config.UseTvmHeaders -or $config.Simple) { $h = $script:headers.Clone() $h["api-version"] = "1.0" $h } else { $script:headers } try { # Handle simple (non-paginated) endpoints if ($config.Simple) { $uri = "https://security.microsoft.com/apiproxy/mtp/tvm/analytics/recommendations" + $config.Endpoint Write-Verbose "Retrieving from: $uri" $result = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $requestHeaders $valueToCache = if ($config.Extract) { $result.$($config.Extract) } else { $result } # Handle null/empty results gracefully if ($null -eq $valueToCache) { $valueToCache = @() } Set-XdrCache -CacheKey $config.CacheKey -Value $valueToCache -TTLMinutes 30 # Apply -Top if specified if ($Top -gt 0 -and $valueToCache.Count -gt $Top) { return $valueToCache | Select-Object -First $Top } return $valueToCache } # Handle paginated endpoints $countProp = if ($config.CountProperty) { $config.CountProperty } else { 'numOfResults' } $paginatedResult = Invoke-PaginatedRequest -Headers $requestHeaders -DisplayName $config.DisplayName -BuildUri $config.BuildUri -CountProperty $countProp -MaxResults $Top # Cache the full response only if we fetched everything if (-not $Top -or $Top -eq 0) { $cacheValue = [PSCustomObject]@{ $countProp = $paginatedResult.Count results = $paginatedResult.Results } Set-XdrCache -CacheKey $config.CacheKey -Value $cacheValue -TTLMinutes 30 } # Return based on parameters if ($PSCmdlet.ParameterSetName -eq 'CountOnly') { return $paginatedResult.Count } return $paginatedResult.Results } catch { Write-Error "Failed to retrieve $($config.DisplayName): $_" } } end { } } |