private/Invoke-MsecDefenderRequest.ps1

function Invoke-MsecDefenderRequest {
    <#
    .SYNOPSIS
        Calls the Microsoft Defender for Endpoint API using the session's app token;
        follows @odata.nextLink when -All is supplied.

    .DESCRIPTION
        Returns the deserialized response body. For collection endpoints, -All emits each
        page's .value items to the pipeline as one concatenated stream. Without -All the raw
        object is returned, which is what the single-value endpoints (/api/exposureScore,
        /api/configurationScore) want.

        Paging is not optional for the endpoints that matter here. /api/machines returns a
        page at a time, and the vulnerability assessment export is one row per device and
        software finding - tens of thousands of rows on a mid-sized estate. Reading only the
        first page would silently under-report, which on a vulnerability report is the worst
        possible failure: it looks like a clean answer.

    .PARAMETER Path
        Path below the Defender API host, e.g. '/api/machines'.

    .PARAMETER Method
        GET or POST. Defaults to GET.

    .PARAMETER All
        For GET collection endpoints: follow @odata.nextLink and emit .value items across
        every page.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [Parameter()]
        [ValidateSet('GET', 'POST')]
        [string] $Method = 'GET',

        [Parameter()]
        [switch] $All
    )

    # Defender for Endpoint (securitycenter) is commercial-only. In clouds where it has no
    # endpoint (e.g. retired in Azure China), fail clearly instead of hitting a dead host.
    # Fall back to the commercial host for sessions that predate endpoint resolution.
    $base = if ($script:MsecSession.Endpoints) {
        $script:MsecSession.Endpoints.DefenderResource
    }
    else {
        'https://api.securitycenter.microsoft.com'
    }
    if (-not $base) {
        throw "Microsoft Defender for Endpoint (securitycenter) API is not available in the '$($script:MsecSession.Endpoints.EnvironmentName)' cloud - Defender functions are commercial-only."
    }

    $token = Get-MsecAccessToken -Resource $base

    $invokeParams = @{
        Method      = $Method
        Uri         = "$base$Path"
        Headers     = @{ Authorization = "Bearer $token" }
        ErrorAction = 'Stop'
    }

    if (-not $All) {
        return Invoke-RestMethod @invokeParams
    }

    # Paged collection: yield each page's .value items, follow @odata.nextLink.
    #
    # Progress is reported from the SECOND page onwards. The vulnerability export runs to
    # many pages on a real estate, and a caller blocked in here otherwise leaves whatever
    # status it set before the call frozen on screen, which reads as a hang. Suppressed for
    # single-page responses so ordinary calls stay silent, and given its own progress id so
    # it does not overwrite a caller's own bar.
    $progressId = 1731
    $page = 0
    $items = 0

    do {
        $response = Invoke-RestMethod @invokeParams
        $page++

        if ($null -ne $response.value) {
            $batch = @($response.value)
            $items += $batch.Count
            $batch
        }

        $next = $response.'@odata.nextLink'
        if ($next) {
            $invokeParams['Uri'] = $next
            # No total is available, so this is a live tally rather than a percentage. An
            # honest "still working, here is how much" beats a fabricated completion figure.
            Write-Progress -Id $progressId -Activity 'Paging Microsoft Defender for Endpoint' `
                -Status "$Path - $items item(s) after $page page(s)"
        }
    } while ($next)

    if ($page -gt 1) {
        Write-Progress -Id $progressId -Activity 'Paging Microsoft Defender for Endpoint' -Completed
    }
}