From 5d1a0e71826940ff2e643bbc19d18b4e1bd84e7e Mon Sep 17 00:00:00 2001 From: rgunst Date: Wed, 11 Mar 2026 20:31:14 +0100 Subject: [PATCH] Fix Access Reviews contactedReviewers failing with 'Invalid guid' The contactedReviewers grandchild endpoint uses a URL template with two tokens: definitions//instances//contactedReviewers New-GraphBatchRequest replaces ALL occurrences with the same value (the instance ID), causing both the definition ID and instance ID slots to get the instance ID. This results in 400 errors like "Invalid guid 2G3-4TG6YU2J54hjnaRoPQE passed in" when definition IDs are non-GUID formatted strings. Fix: when _processChildrenRecursive encounters children whose GraphUri contains multiple tokens, it now queues them per parent ID with the first (ancestor) placeholder pre-resolved. This leaves exactly one for the batch request to fill with the correct child ID. Fixes #115 --- src/Export-Entra.ps1 | 66 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/src/Export-Entra.ps1 b/src/Export-Entra.ps1 index 8d4875e..2d7aeed 100644 --- a/src/Export-Entra.ps1 +++ b/src/Export-Entra.ps1 @@ -297,19 +297,63 @@ # recursively process children if they exist if ($children) { - # for grandchildren, we need to collect the parent IDs from the results - $childBasePath = if ($item.Path -match "\.json$") { - $basePath - } else { - Join-Path -Path $basePath -ChildPath $item.Path + # Check if any child has multiple tokens in its GraphUri. + # This happens when a grandchild URL includes ancestor IDs (e.g. + # definitions//instances//contactedReviewers). + # In that case, we queue children per parent ID so we can resolve the + # ancestor placeholder now, leaving only one for the next batch level. + $hasNestedPlaceholders = $children | Where-Object { + $_.GraphUri -and ([regex]::Matches($_.GraphUri, '')).Count -gt 1 } - # we'll process these after the current batch is executed and results are available - $script:childrenToProcess.Add(@{ - Children = $children - BasePath = $childBasePath - ParentPath = "$($item.Path)*" - }) + if ($hasNestedPlaceholders) { + foreach ($pid in $parentIds) { + $resolvedChildren = @(foreach ($child in $children) { + $clone = @{} + foreach ($key in $child.Keys) { $clone[$key] = $child[$key] } + if ($clone.GraphUri -and ([regex]::Matches($clone.GraphUri, '')).Count -gt 1) { + $clone.GraphUri = ([regex]'').Replace($clone.GraphUri, $pid, 1) + } + if ($clone.Children) { + $clone.Children = @(foreach ($gc in $clone.Children) { + $gcClone = @{} + foreach ($key in $gc.Keys) { $gcClone[$key] = $gc[$key] } + if ($gcClone.GraphUri -and ([regex]::Matches($gcClone.GraphUri, '')).Count -gt 1) { + $gcClone.GraphUri = ([regex]'').Replace($gcClone.GraphUri, $pid, 1) + } + $gcClone + }) + } + $clone + }) + + # Match the same Join-Path chain used when creating the batch request ID + # (lines above: Join-Path $basePath $pid, then Join-Path $result $item.Path) + # so that the BasePath comparison in the main loop finds the results. + $perParentBasePath = Join-Path -Path $basePath -ChildPath $pid + $perParentBasePath = Join-Path -Path $perParentBasePath -ChildPath $item.Path + + $script:childrenToProcess.Add(@{ + Children = $resolvedChildren + BasePath = $perParentBasePath + ParentPath = "$pid*" + }) + } + } else { + # for grandchildren, we need to collect the parent IDs from the results + $childBasePath = if ($item.Path -match "\.json$") { + $basePath + } else { + Join-Path -Path $basePath -ChildPath $item.Path + } + + # we'll process these after the current batch is executed and results are available + $script:childrenToProcess.Add(@{ + Children = $children + BasePath = $childBasePath + ParentPath = "$($item.Path)*" + }) + } } } }