Public/Reconnaissance/Invoke-MSGraph.ps1
function Invoke-MsGraph { [cmdletbinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $false, ParameterSetName = 'Standard')] [string]$relativeUrl, [Parameter(Mandatory = $true, ParameterSetName = 'BatchRequest')] [ValidateNotNull()] [System.Collections.Generic.List[hashtable]]$BatchRequests, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $false)] [switch]$NoBatch, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $false)] [int]$MaxRetries = 3, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $false)] [int]$RetryDelaySeconds = 5, # Initial delay in seconds [Parameter(Mandatory = $false)] [ValidateSet("Object", "JSON", "CSV", "Table")] [Alias("output", "o")] [string]$OutputFormat = "Object", [Parameter(Mandatory = $false)] [switch]$SkipCache, [Parameter(Mandatory = $false)] [int]$CacheExpirationMinutes = 30, [Parameter(Mandatory = $false)] [int]$MaxCacheSize = 100, [Parameter(Mandatory = $false)] [switch]$CompressCache ) begin { $MyInvocation.MyCommand.Name | Invoke-BlackCat -ResourceTypeName 'MSGraph' } process { # Handle batch requests differently if ($PSCmdlet.ParameterSetName -eq 'BatchRequest') { Write-Verbose "Processing batch request with $($BatchRequests.Count) items" # The Graph API batch endpoint can handle up to 20 requests per batch $maxBatchSize = 20 $batchResults = @{} $retries = 0 do { try { # Process requests in batches of 20 for ($i = 0; $i -lt $BatchRequests.Count; $i += $maxBatchSize) { $currentBatchRequests = $BatchRequests | Select-Object -Skip $i -First $maxBatchSize Write-Verbose "Processing batch $($i / $maxBatchSize + 1) with $($currentBatchRequests.Count) requests" # Create the batch request payload $payload = @{ requests = $currentBatchRequests } $requestParam = @{ Headers = $script:graphHeader Uri = '{0}/$batch' -f $sessionVariables.graphUri Method = 'POST' ContentType = 'application/json' Body = $payload | ConvertTo-Json -Depth 10 UserAgent = $($sessionVariables.userAgent) ErrorVariable = 'Err' } # Execute the batch request try { $response = Invoke-RestMethod @requestParam # Process the responses and add to the results dictionary foreach ($responseItem in $response.responses) { $requestId = $responseItem.id if ($responseItem.status -eq 200) { $batchResults[$requestId] = @{ Success = $true Data = $responseItem.body Status = $responseItem.status } } else { $batchResults[$requestId] = @{ Success = $false Error = $responseItem.body.error Status = $responseItem.status } # Handle 404 errors with verbose message instead of warning if ($responseItem.status -eq 404 -or ($responseItem.body.error -and ( $responseItem.body.error.code -eq "Request_ResourceNotFound" -or $responseItem.body.error.code -eq "ResourceNotFound" -or $responseItem.body.error.message -match "not found" -or $responseItem.body.error.message -match "Invalid object identifier" )) ) { # Extract resource ID from the URL if possible $resourceIdMatch = $null if ($responseItem.body.error.message -match "'([^']+)'") { $resourceIdMatch = $matches[1] } $resourceId = $resourceIdMatch ?? "resource" Write-Verbose "Batch request $($requestId): Resource '$($resourceId)' not found or inaccessible (status $($responseItem.status))" } else { Write-Verbose "Request $requestId failed with status $($responseItem.status): $($responseItem.body.error.message)" } } } } catch { Write-Warning "Error in batch request: $($_.Exception.Message)" # Increment retry counter and continue to the next batch $retries++ if ($retries -lt $MaxRetries) { Start-Sleep -Seconds ($RetryDelaySeconds * $retries) continue } else { Write-Error "Max retries reached for batch request." return $null } } } # Return the batch results return $batchResults } catch { Write-Warning "Error processing batch requests: $($_.Exception.Message)" $retries++ if ($retries -lt $MaxRetries) { Start-Sleep -Seconds ($RetryDelaySeconds * $retries) continue } else { Write-Error "Max retries reached for batch request." return $null } } } while ($retries -lt $MaxRetries) return $null } else { # Standard single request processing $cacheParams = @{ NoBatch = $NoBatch.IsPresent } $cacheKey = ConvertTo-CacheKey -BaseIdentifier $relativeUrl -Parameters $cacheParams if (-not $SkipCache) { try { $cachedResult = Get-BlackCatCache -Key $cacheKey -CacheType 'MSGraph' if ($null -ne $cachedResult) { Write-Verbose "Retrieved result from cache for: $relativeUrl" $formatParam = @{ Data = $cachedResult OutputFormat = $OutputFormat FunctionName = $MyInvocation.MyCommand.Name FilePrefix = 'MSGraph' } return Format-BlackCatOutput @formatParam } } catch { Write-Verbose "Error retrieving from cache: $($_.Exception.Message). Proceeding with fresh API call." } } $retries = 0 do { try { if ($NoBatch) { $uri = "$($sessionVariables.graphUri)/$relativeUrl" -replace 'applications/\(', 'applications(' Write-Verbose "Invoking Microsoft Graph API: $uri" $requestParam = @{ Headers = $script:graphHeader Uri = $uri Method = 'GET' UserAgent = $($sessionVariables.userAgent) ErrorVariable = 'Err' } } else { $payload = @{ requests = @( @{ id = "List" method = 'GET' url = '/{0}' -f "$relativeUrl" } ) } $requestParam = @{ Headers = $script:graphHeader Uri = '{0}/$batch' -f $sessionVariables.graphUri Method = 'POST' ContentType = 'application/json' Body = $payload | ConvertTo-Json -Depth 10 UserAgent = $($sessionVariables.userAgent) ErrorVariable = 'Err' } } try { $initialResponse = (Invoke-RestMethod @requestParam) } catch { if ($Err) { $ErrorMessage = $null try { $ErrorMessage = ($Err.Message | ConvertFrom-Json).error.message } catch { $ErrorMessage = $Err.Message } # Check if this is a resource not found or invalid ID error if ($ErrorMessage -match "not exist|not found|Invalid object identifier") { # For resource not found errors, just log verbose and return null without error $resourceId = "unknown" if ($ErrorMessage -match "'([^']+)'") { $resourceId = $matches[1] } Write-Verbose "Resource '$resourceId' not found or invalid identifier (non-fatal error)" return $null } else { Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "$($ErrorMessage)" -Severity 'Error' } } return $null } if ($null -eq $initialResponse) { Write-Verbose "No data returned from API call to: $relativeUrl" return $null } try { if ($NoBatch) { $result = $initialResponse } else { if ($initialResponse.Headers."Retry-After") { $retryAfter = [int]$initialResponse.Headers."Retry-After" Write-Warning "Throttled! Waiting $($retryAfter) seconds before retrying." Start-Sleep -Seconds $retryAfter $retries++ # Increment retries, important to track continue # Skip the rest of the loop and retry } $allItems = Get-AllPages -ProcessLink $initialResponse $result = $allItems } # Check for batch request that returned no results if ($null -eq $result -or ($result -is [array] -and $result.Count -eq 0)) { Write-Verbose "No data found for: $relativeUrl" return $null } } catch { Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "Error processing response: $($_.Exception.Message)" -Severity 'Error' return $null } if (-not $SkipCache -and $null -ne $result) { try { Set-BlackCatCache -Key $cacheKey -Data $result -ExpirationMinutes $CacheExpirationMinutes -CacheType 'MSGraph' -MaxCacheSize $MaxCacheSize -CompressData:$CompressCache Write-Verbose "Cached result for: $relativeUrl (expires in $CacheExpirationMinutes minutes)" } catch { Write-Verbose "Failed to cache result for: $relativeUrl - $($_.Exception.Message)" } } if ($null -eq $result) { Write-Verbose "No data to format for: $relativeUrl" return $null } $formatParam = @{ Data = $result OutputFormat = $OutputFormat FunctionName = $MyInvocation.MyCommand.Name FilePrefix = 'MSGraph' } return Format-BlackCatOutput @formatParam } catch { # Handle throttling errors if ($_.Exception.Response.StatusCode -eq 429 || $_.Exception.Message -match "429" || $_.Exception.Message -match "too many requests") { $retryAfter = $RetryDelaySeconds * ($retries + 1) # Exponential backoff # Try to get the retry-after header if available if ($_.Exception.Response -and $_.Exception.Response.Headers -and $_.Exception.Response.Headers."Retry-After") { $retryAfter = [int]$_.Exception.Response.Headers."Retry-After" } $retries++ Write-Warning "Throttled! Retry $($retries) of $($MaxRetries). Waiting $($retryAfter) seconds before retrying." Start-Sleep -Seconds $retryAfter } # Handle authentication errors elseif ($_.Exception.Response.StatusCode -eq 401 || $_.Exception.Message -match "401" || $_.Exception.Message -match "unauthorized" -or $_.Exception.Message -match "access.*denied") { Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "Unauthorized access to the Graph API. Your token might be expired or invalid." -Severity 'Error' break } # Handle not found errors with verbose logging instead of errors elseif ($_.Exception.Response.StatusCode -eq 404 || $_.Exception.Message -match "404" || $_.Exception.Message -match "not found" || $_.Exception.Message -match "Invalid object identifier" || $_.Exception.Message -match "does not exist") { $resourceId = $relativeUrl -replace '.*/([^/]+)$', '$1' Write-Verbose "Resource not found: $resourceId - it may have been deleted or you may not have access" return $null } else { Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message $($_.Exception.Message) -Severity 'Error' if ($retries -lt $MaxRetries) { $retries++ $retryAfter = $RetryDelaySeconds * $retries # Exponential backoff Write-Warning "Error occurred. Retry $retries of $MaxRetries. Waiting $retryAfter seconds before retrying." Start-Sleep -Seconds $retryAfter } else { break } } } } while ($retries -lt $MaxRetries) if ($retries -ge $MaxRetries) { Write-Error "Max retries reached. Failed to execute request after $($MaxRetries) attempts." } return $null } } end { # Empty end block to complete the function structure } <# .SYNOPSIS Invokes a request to the Microsoft Graph API. .DESCRIPTION This function sends a request to the Microsoft Graph API using the specified parameters. It handles authentication and constructs the appropriate headers for the request. The function supports various output formats and includes retry logic for handling throttling. .PARAMETER relativeUrl The relative URL for the Microsoft Graph API endpoint to call. Used in the standard parameter set for single requests. .PARAMETER BatchRequests An array of request objects for batch processing. Each request should be a hashtable with: - id: A unique identifier for the request - method: HTTP method (typically "GET") - url: The relative URL for the request (starting with "/") Used in the BatchRequest parameter set for efficiently processing multiple requests at once. .PARAMETER NoBatch When specified, sends individual requests instead of using batch requests. .PARAMETER MaxRetries The maximum number of retries when encountering throttling or transient errors. Default is 3. .PARAMETER RetryDelaySeconds The initial delay in seconds between retries, with exponential backoff. Default is 5 seconds. .PARAMETER OutputFormat Specifies the output format for results. Valid values are: - Object: Returns PowerShell objects (default) - JSON: Saves results to a JSON file with timestamp - CSV: Saves results to a CSV file with timestamp - Table: Returns results in formatted table Aliases: output, o .PARAMETER SkipCache When specified, bypasses the cache and forces a fresh API call. .PARAMETER CacheExpirationMinutes Sets the cache expiration time in minutes. Default is 30 minutes. This parameter controls how long the cached results remain valid. .PARAMETER MaxCacheSize Maximum number of entries to store in the cache. Default is 100. When this limit is reached, least recently used entries are removed. .PARAMETER CompressCache When specified, compresses cache data to reduce memory usage. Recommended for large datasets or memory-constrained environments. .EXAMPLE Invoke-MSGraph -relativeUrl "applications" This example sends a GET request to the Microsoft Graph API to retrieve information about the applications. .EXAMPLE Invoke-MSGraph -relativeUrl "users" -OutputFormat JSON This example retrieves users from Microsoft Graph and saves the results to a JSON file. .EXAMPLE Invoke-MSGraph -relativeUrl "groups" -OutputFormat Table This example retrieves groups from Microsoft Graph and displays the results in a formatted table. .EXAMPLE Invoke-MSGraph -relativeUrl "applications" -SkipCache This example forces a fresh API call to retrieve applications, bypassing any cached results. .EXAMPLE Invoke-MSGraph -relativeUrl "users" -CacheExpirationMinutes 60 This example retrieves users and caches the results for 60 minutes instead of the default 30 minutes. .EXAMPLE Invoke-MSGraph -relativeUrl "applications" -MaxCacheSize 50 -CompressCache This example retrieves applications with a smaller cache size and enables compression to save memory. .EXAMPLE Invoke-MSGraph -relativeUrl "groups" -CompressCache This example retrieves groups and compresses the cached data to reduce memory usage in large environments. #> } |