Private/Get-DefenderXDR.ps1
|
# Known Defender XDR advanced hunting tables the Sentinel connector can stream. # A table is only "streaming" if it actually exists in the workspace. $script:KnownXDRTables = @( 'DeviceInfo', 'DeviceNetworkInfo', 'DeviceProcessEvents', 'DeviceNetworkEvents', 'DeviceFileEvents', 'DeviceRegistryEvents', 'DeviceLogonEvents', 'DeviceImageLoadEvents', 'DeviceEvents', 'DeviceFileCertificateInfo', 'EmailAttachmentInfo', 'EmailEvents', 'EmailPostDeliveryEvents', 'EmailUrlInfo', 'UrlClickEvents', 'IdentityDirectoryEvents', 'IdentityLogonEvents', 'IdentityQueryEvents', 'CloudAppEvents', 'AlertInfo', 'AlertEvidence' ) function Get-DefenderXDR { <# .SYNOPSIS Queries Defender XDR for custom detection rules and streaming configuration. Requires -IncludeDefenderXDR flag. Uses delegated Microsoft Graph auth with CustomDetection.Read.All (or CustomDetection.ReadWrite.All) scope. Falls back to an Az access-token REST call if delegated Graph auth is unavailable. .OUTPUTS PSCustomObject with custom detection rules and XDR table analysis. #> [CmdletBinding()] param( [Parameter(Mandatory)][PSCustomObject]$Context ) # Fetch custom detection rules. # Prefer delegated user context via Microsoft Graph PowerShell for CustomDetection.Read.All. $customRules = [System.Collections.Generic.List[object]]::new() $fetched = $false $graphBase = Get-LogHorizonEndpoint -Name Graph -Context $Context $graphEnvironment = Get-LogHorizonEndpoint -Name GraphEnvironment -Context $Context $endpoints = @( "$graphBase/beta/security/rules/detectionRules", "$graphBase/v1.0/security/rules/detectionRules" ) $mgCmd = Get-Command Invoke-MgGraphRequest -ErrorAction SilentlyContinue if ($mgCmd) { try { $requiredScopes = @('CustomDetection.Read.All', 'CustomDetection.ReadWrite.All') $mgContext = Get-MgContext -ErrorAction SilentlyContinue $hasRequiredScope = $false if ($mgContext -and $mgContext.Scopes) { $hasRequiredScope = @($mgContext.Scopes | Where-Object { $_ -in $requiredScopes }).Count -gt 0 } if (-not $hasRequiredScope) { $connectParams = @{ Scopes = @('CustomDetection.Read.All') ContextScope = 'Process' NoWelcome = $true } if ($Context.PSObject.Properties.Name -contains 'TenantId' -and -not [string]::IsNullOrWhiteSpace($Context.TenantId)) { $connectParams.TenantId = $Context.TenantId } if ($graphEnvironment -and $graphEnvironment -ne 'Global') { $connectParams.Environment = $graphEnvironment } Connect-MgGraph @connectParams -ErrorAction Stop | Out-Null $mgContext = Get-MgContext -ErrorAction SilentlyContinue $hasRequiredScope = $mgContext -and $mgContext.Scopes -and (@($mgContext.Scopes | Where-Object { $_ -in $requiredScopes }).Count -gt 0) } if ($hasRequiredScope) { foreach ($endpoint in $endpoints) { try { $uri = $endpoint do { $response = Invoke-MgGraphRequest -Method GET -Uri $uri -OutputType PSObject -ErrorAction Stop if ($response -and $response.PSObject.Properties.Name -contains 'value') { foreach ($v in @($response.value)) { $customRules.Add($v) } } if ($response -and $response.PSObject.Properties.Name -contains '@odata.nextLink' -and -not [string]::IsNullOrWhiteSpace($response.'@odata.nextLink')) { $uri = $response.'@odata.nextLink' } else { $uri = $null } } while ($uri) $fetched = $true Write-Verbose "Fetched Defender custom detection rules using delegated Graph user context (${endpoint})." break } catch { Write-Verbose "Delegated Graph request failed for ${endpoint}: $_" } } } else { Write-Warning 'Defender XDR retrieval could not establish delegated Microsoft Graph scope CustomDetection.Read.All.' } } catch { Write-Verbose "Delegated Graph auth/request path failed: $_" } } # Fallback: if delegated Graph auth/request did not fetch results, try Az token + raw REST. # This keeps delegated Graph as the preferred path while still supporting non-interactive/CI environments. $fetchError = $null if (-not $fetched) { $graphToken = $null try { $tenantId = if ($Context.PSObject.Properties.Name -contains 'TenantId' -and -not [string]::IsNullOrWhiteSpace($Context.TenantId)) { $Context.TenantId } else { $null } $graphToken = Resolve-AzToken -ResourceUrl $graphBase -TenantId $tenantId } catch { $fetchError = "Cannot acquire Microsoft Graph token: $($_.Exception.Message)" Write-Warning "$fetchError. Defender XDR custom detection coverage is unavailable for this run." return ConvertTo-DefenderXDRResult -Fetched $false -FetchError $fetchError } $headers = @{ Authorization = "Bearer $graphToken" 'Content-Type' = 'application/json' } foreach ($endpoint in $endpoints) { try { $uri = $endpoint do { $response = Invoke-AzRestWithRetry -Uri $uri -Headers $headers if ($response -and $response.PSObject.Properties.Name -contains 'value') { foreach ($v in @($response.value)) { $customRules.Add($v) } } if ($response -and $response.PSObject.Properties.Name -contains '@odata.nextLink' -and -not [string]::IsNullOrWhiteSpace($response.'@odata.nextLink')) { $uri = $response.'@odata.nextLink' } else { $uri = $null } } while ($uri) $fetched = $true break } catch { Write-Verbose "Could not fetch Defender custom detection rules from ${endpoint}: $_" } } } if (-not $fetched) { $missing = if (-not $mgCmd) { ' Install Microsoft.Graph.Authentication for delegated access (CustomDetection.Read.All).' } else { '' } $fetchError = "Could not fetch Defender custom detection rules from Graph API (beta/v1.0).$missing" Write-Warning $fetchError return ConvertTo-DefenderXDRResult -Fetched $false -FetchError $fetchError } # Parse enabled XDR rule queries for table references $xdrTableCoverage = @{} $projectedRules = [System.Collections.Generic.List[object]]::new() foreach ($rule in $customRules) { $query = $null if ($rule.PSObject.Properties.Name -contains 'queryCondition' -and $rule.queryCondition) { $query = $rule.queryCondition.queryText } if (-not $query -and $rule.PSObject.Properties.Name -contains 'detectionAction' -and $rule.detectionAction -and $rule.detectionAction.PSObject.Properties.Name -contains 'queryCondition' -and $rule.detectionAction.queryCondition) { $query = $rule.detectionAction.queryCondition.queryText } $isEnabled = -not ($rule.PSObject.Properties.Name -contains 'isEnabled' -and $rule.isEnabled -eq $false) # Only the fields the analysis reads are kept; Graph objects also carry createdBy/lastModifiedBy identities $projectedRules.Add([PSCustomObject]@{ id = $(if ($rule.PSObject.Properties.Name -contains 'id') { $rule.id } else { $null }) displayName = $(if ($rule.PSObject.Properties.Name -contains 'displayName') { $rule.displayName } else { $null }) isEnabled = $isEnabled queryCondition = [PSCustomObject]@{ queryText = $query } schedule = $(if ($rule.PSObject.Properties.Name -contains 'schedule' -and $rule.schedule) { [PSCustomObject]@{ period = $rule.schedule.period } } else { $null }) }) if (-not $isEnabled) { continue } if ($query) { $tables = @(Get-TablesFromKql -Kql $query) foreach ($t in $tables) { if (-not $xdrTableCoverage.ContainsKey($t)) { $xdrTableCoverage[$t] = 0 } $xdrTableCoverage[$t]++ } } } ConvertTo-DefenderXDRResult -Fetched $true -CustomRules @($projectedRules) -XDRTableCoverage $xdrTableCoverage } function ConvertTo-DefenderXDRResult { <# .SYNOPSIS The one shape every Get-DefenderXDR path returns, so callers never see $null. Fetched=$false with FetchError tells the run and the exports that the flag had no data. #> [CmdletBinding()] param( [bool]$Fetched, [string]$FetchError, [array]$CustomRules = @(), [hashtable]$XDRTableCoverage = @{} ) [PSCustomObject]@{ Fetched = $Fetched FetchError = $FetchError CustomRules = @($CustomRules) TotalXDRRules = @($CustomRules).Count XDRTableCoverage = $XDRTableCoverage KnownXDRTables = @($script:KnownXDRTables) } } |