Public/Reconnaissance/Invoke-MSGraph.ps1

function Invoke-MsGraph {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $false)]
        [string]$relativeUrl,

        [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 {
        $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 = ($Err.Message | ConvertFrom-Json).error.message
                        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
                    }

                    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 {
                if ($_.Exception.Message -contains "*429*") { # Check for specific throttling error
                    $retries++
                    $retryAfter = $RetryDelaySeconds * ($retries) # Exponential backoff
                    Write-Warning "Throttled! Retry $($retries) of $($MaxRetries). Waiting $($retryAfter) seconds before retrying."
                    Start-Sleep -Seconds $retryAfter
                }
                elseif ($_.Exception.Message -contains "*401") {
                    Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message "Unauthorized access to the Graph API." -Severity 'Error'
                    break
                }
                else {
                    Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message $($_.Exception.Message) -Severity 'Error'
                    break
                }
            }
        } while ($retries -lt $MaxRetries)

        if ($retries -ge $MaxRetries) {
            Write-Error "Max retries reached. Failed to execute request after $($MaxRetries) attempts."
        }
    }
    <#
    .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.
 
    .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.
#>

}