functions/Get-XdrCloudAppsDiscovery.ps1

function Get-XdrCloudAppsDiscovery {
    <#
    .SYNOPSIS
        Retrieves Cloud Discovery data from Microsoft Defender for Cloud Apps.

    .DESCRIPTION
        Gets Cloud Discovery data from Microsoft Defender for Cloud Apps. This consolidated
        cmdlet provides access to discovery data types including categories, entities,
        top rankings, locations, constants, unsanctioned apps, and user deanonymization
        through a single interface. This function includes caching support to reduce API calls.

        When no StreamId or StreamName is specified for types that require streams, queries
        ALL available discovery streams and includes StreamId/StreamName properties on each result.

    .PARAMETER Type
        The type of discovery data to retrieve. Valid values are:
        - Category: App category definitions (no StreamId required)
        - CategoryStat: Category statistics with traffic/user data
        - Constant: Discovery constants and enumerations (no StreamId required)
        - Entity: Entities (IP, Machine, User, Resource) - use with -EntityType
        - Location: Discovery service locations (no StreamId required)
        - Top: Top apps, categories, or entities - use with -TopType
        - UnsanctionedApp: Apps marked as unsanctioned/blocked

    .PARAMETER ListStreams
        When specified, lists all available discovery streams. Useful for discovering
        stream IDs and names before querying data.

    .PARAMETER DeanonymizeUser
        When specified, deanonymizes Cloud Discovery usernames using the provided
        justification text.

    .PARAMETER Usernames
        One or more anonymized Cloud Discovery usernames to deanonymize.

    .PARAMETER Justification
        Required justification for deanonymizing Cloud Discovery usernames.

    .PARAMETER StreamId
        The ID of the discovery stream to query. If not specified for types that require it,
        queries all available streams.
        Accepts pipeline input from Get-XdrCloudAppsConfiguration -Type DiscoveryStream via the _id property.

    .PARAMETER StreamName
        The name of the discovery stream to query. Supports wildcards (e.g., "Defender*").
        If not specified along with StreamId, queries all available streams for types that require it.

    .PARAMETER EntityType
        Required when Type is Entity. Specifies the entity type to retrieve.
        Valid values are: IP, Machine, User, Resource

    .PARAMETER TopType
        Required when Type is Top. Specifies what top data to retrieve.
        Valid values are: App, Category, Entity (use with -TopEntityField)

    .PARAMETER TopEntityField
        Required when Type is Top and TopType is Entity. Specifies the entity field.
        Valid values are: users, machines, ipAddresses

    .PARAMETER Timeframe
        The number of days to include in the results. Default is 30 days.
        Applies to CategoryStat, Entity, Top, and UnsanctionedApp types.

    .PARAMETER AppId
        Optional app ID to filter entities by a specific application.
        Only applies to Entity type (not Resource EntityType).

    .PARAMETER Limit
        Maximum number of results to return. Applies to Entity and Top types.

    .PARAMETER Skip
        Number of results to skip for pagination. Applies to Entity type.

    .PARAMETER Offset
        Number of results to skip for pagination. Applies to Top type.

    .PARAMETER SortField
        The field to sort results by. Applies to Entity type. Default is "lastSeen".

    .PARAMETER SortDirection
        The sort direction. Valid values are "asc" or "desc". Default is "desc".

    .PARAMETER Filters
        A hashtable of filters to apply to the query. Applies to Entity type.

    .PARAMETER CategoryFilter
        Filter top apps to a specific category. Only applies to Top type with TopType App.

    .PARAMETER Metric
        The metric used to rank results. Valid values are traffic, users, transactions, upload.
        Applies to Top type with TopType App or Category. Default is traffic.

    .PARAMETER LocationType
        The type of location to retrieve. Valid values are "hq", "branch", or "".
        Only applies to Location type. Default is "hq".

    .PARAMETER Search
        A search string to filter locations. Only applies to Location type.

    .PARAMETER LocationId
        A specific location ID to retrieve. Only applies to Location type.

    .PARAMETER ExcludeSanctioned
        Excludes sanctioned apps. Only applies to Top type with TopType Category.

    .PARAMETER ExcludeUnsanctioned
        Excludes unsanctioned apps. Only applies to Top type with TopType Category.

    .PARAMETER ExcludeOther
        Excludes apps with no sanction status. Only applies to Top type with TopType Category.

    .PARAMETER Force
        Bypasses the cache and forces a fresh retrieval from the API.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -ListStreams
        Lists all available discovery streams.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -DeanonymizeUser -Usernames "User_aaaaaabbbbb=" -Justification "Incident response investigation"
        Deanonymizes a Cloud Discovery username.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type Category
        Retrieves all app category definitions.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type Constant
        Retrieves discovery constants and enumerations.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type Location -LocationType branch
        Retrieves branch office locations.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type CategoryStat
        Retrieves category statistics from ALL streams (includes stream context on results).

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type CategoryStat -StreamName "Defender*"
        Retrieves category statistics from streams matching the wildcard pattern.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type Entity -EntityType IP
        Retrieves discovered IP addresses from ALL streams.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type Entity -StreamId "64a75731967076e7d6bd00ea" -EntityType User -Limit 50
        Retrieves up to 50 discovered users from a specific stream.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type Top -TopType App
        Retrieves top discovered apps from ALL streams.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type Top -StreamName "Defender-managed endpoints" -TopType Entity -TopEntityField users
        Retrieves top users by app usage from a specific stream.

    .EXAMPLE
        Get-XdrCloudAppsDiscovery -Type UnsanctionedApp
        Retrieves apps marked as unsanctioned from ALL streams.

    .EXAMPLE
        Get-XdrCloudAppsConfiguration -Type DiscoveryStream | Get-XdrCloudAppsDiscovery -Type Entity -EntityType Machine
        Retrieves discovered machines from all streams via pipeline.

    .OUTPUTS
        Returns discovery data objects based on the Type parameter. Each type returns
        appropriately typed objects (XdrCloudAppsDiscoveryCategory, XdrCloudAppsDiscoveryEntity, etc.)
        When querying multiple streams, includes SourceStreamId and SourceStreamName properties.

        XdrCloudAppsConfigurationDiscoveryStream[]
        When -ListStreams is specified, returns available discovery streams.

        XdrCloudAppsDiscoveryDeanonymizedUser[]
        When -DeanonymizeUser is specified, returns deanonymized Cloud Discovery usernames.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ListStreams', Justification = 'Parameter used for parameter set selection')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DeanonymizeUser', Justification = 'Parameter used for parameter set selection')]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param (
        [Parameter(ParameterSetName = 'Default', Mandatory = $true)]
        [ValidateSet("Category", "CategoryStat", "Constant", "Entity", "Location", "Top", "UnsanctionedApp")]
        [string]$Type,

        [Parameter(ParameterSetName = 'ListStreams', Mandatory = $true)]
        [switch]$ListStreams,

        [Parameter(ParameterSetName = 'DeanonymizeUser', Mandatory = $true)]
        [switch]$DeanonymizeUser,

        [Parameter(ParameterSetName = 'DeanonymizeUser', Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Username')]
        [ValidateNotNullOrEmpty()]
        [string[]]$Usernames,

        [Parameter(ParameterSetName = 'DeanonymizeUser', Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Justification,

        [Parameter(ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true)]
        [Alias('_id')]
        [string]$StreamId,

        [Parameter(ParameterSetName = 'Default')]
        [SupportsWildcards()]
        [string]$StreamName,

        [Parameter(ParameterSetName = 'Default')]
        [ValidateSet("IP", "Machine", "User", "Resource")]
        [string]$EntityType,

        [Parameter(ParameterSetName = 'Default')]
        [ValidateSet("App", "Category", "Entity")]
        [string]$TopType,

        [Parameter(ParameterSetName = 'Default')]
        [ValidateSet("users", "machines", "ipAddresses")]
        [string]$TopEntityField,

        [Parameter(ParameterSetName = 'Default')]
        [ValidateRange(1, 365)]
        [int]$Timeframe = 30,

        [Parameter(ParameterSetName = 'Default')]
        [int]$AppId,

        [Parameter(ParameterSetName = 'Default')]
        [ValidateRange(1, 1000)]
        [int]$Limit,

        [Parameter(ParameterSetName = 'Default')]
        [ValidateRange(0, [int]::MaxValue)]
        [int]$Skip = 0,

        [Parameter(ParameterSetName = 'Default')]
        [ValidateRange(0, [int]::MaxValue)]
        [int]$Offset = 0,

        [Parameter(ParameterSetName = 'Default')]
        [string]$SortField = "lastSeen",

        [Parameter(ParameterSetName = 'Default')]
        [ValidateSet("asc", "desc")]
        [string]$SortDirection = "desc",

        [Parameter(ParameterSetName = 'Default')]
        [hashtable]$Filters = @{},

        [Parameter(ParameterSetName = 'Default')]
        [string]$CategoryFilter = "all",

        [Parameter(ParameterSetName = 'Default')]
        [ValidateSet("traffic", "users", "transactions", "upload")]
        [string]$Metric = "traffic",

        [Parameter(ParameterSetName = 'Default')]
        [ValidateSet("hq", "branch", "")]
        [string]$LocationType = "hq",

        [Parameter(ParameterSetName = 'Default')]
        [string]$Search = "",

        [Parameter(ParameterSetName = 'Default')]
        [string]$LocationId = "",

        [Parameter(ParameterSetName = 'Default')]
        [switch]$ExcludeSanctioned,

        [Parameter(ParameterSetName = 'Default')]
        [switch]$ExcludeUnsanctioned,

        [Parameter(ParameterSetName = 'Default')]
        [switch]$ExcludeOther,

        [Parameter()]
        [switch]$Force
    )

    begin {
        Update-XdrConnectionSettings
        $usernamesToDeanonymize = [System.Collections.Generic.List[string]]::new()

        if ($PSCmdlet.ParameterSetName -eq 'Default') {
            # Validate EntityType is provided when Type is Entity
            if ($Type -eq "Entity" -and -not $PSBoundParameters.ContainsKey('EntityType')) {
                throw "The -EntityType parameter is required when -Type is 'Entity'. Valid values are: IP, Machine, User, Resource"
            }

            # Validate TopType is provided when Type is Top
            if ($Type -eq "Top" -and -not $PSBoundParameters.ContainsKey('TopType')) {
                throw "The -TopType parameter is required when -Type is 'Top'. Valid values are: App, Category, Entity"
            }

            # Validate TopEntityField is provided when TopType is Entity
            if ($Type -eq "Top" -and $TopType -eq "Entity" -and -not $PSBoundParameters.ContainsKey('TopEntityField')) {
                throw "The -TopEntityField parameter is required when -TopType is 'Entity'. Valid values are: users, machines, ipAddresses"
            }
        }
    }

    process {
        # Handle ListStreams
        if ($PSCmdlet.ParameterSetName -eq 'ListStreams') {
            return Get-XdrCloudAppsDiscoveryStream -Force:$Force
        }

        if ($PSCmdlet.ParameterSetName -eq 'DeanonymizeUser') {
            foreach ($username in $Usernames) {
                $usernamesToDeanonymize.Add($username)
            }
            return
        }

        # Helper function to add stream context to results
        function Add-StreamContext {
            param ($Items, $StreamIdValue, $StreamNameValue, $AddContext)
            if ($AddContext -and $Items) {
                foreach ($item in $Items) {
                    $item | Add-Member -NotePropertyName 'SourceStreamId' -NotePropertyValue $StreamIdValue -Force
                    $item | Add-Member -NotePropertyName 'SourceStreamName' -NotePropertyValue $StreamNameValue -Force
                }
            }
            return $Items
        }

        switch ($Type) {
            "Category" {
                $CacheKey = "XdrCloudAppsDiscoveryCategory"
                $currentCacheValue = Get-XdrCache -CacheKey $CacheKey -ErrorAction SilentlyContinue
                if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
                    Write-Verbose "Using cached Cloud Apps discovery categories"
                    return $currentCacheValue.Value
                } elseif ($Force) {
                    Write-Verbose "Force parameter specified, bypassing cache"
                    Clear-XdrCache -CacheKey $CacheKey
                } else {
                    Write-Verbose "Cloud Apps discovery categories cache is missing or expired"
                }

                $Uri = "https://security.microsoft.com/apiproxy/mcas/cas/api/v1/discovery/categories/"
                Write-Verbose "Retrieving Cloud Apps discovery categories"

                try {
                    $response = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $script:headers
                    $result = if ($null -ne $response.data) { $response.data } else { $response }
                    if ($null -ne $result) {
                        foreach ($item in $result) {
                            $item.PSObject.TypeNames.Insert(0, 'XdrCloudAppsDiscoveryCategory')
                        }
                        Set-XdrCache -CacheKey $CacheKey -Value $result -TTLMinutes 15
                    }
                    return $result
                } catch {
                    Write-Error "Failed to retrieve Cloud Apps discovery categories: $_"
                }
            }

            "CategoryStat" {
                # Resolve streams to query
                $resolveParams = @{ Force = $Force }
                if ($PSBoundParameters.ContainsKey('StreamId')) { $resolveParams.StreamId = $StreamId }
                if ($PSBoundParameters.ContainsKey('StreamName')) { $resolveParams.StreamName = $StreamName }

                $streamsToQuery = Get-XdrCloudAppsDiscoveryStream @resolveParams
                if (-not $streamsToQuery -or $streamsToQuery.Count -eq 0) {
                    Write-Warning "No streams to query. Use -ListStreams to see available streams."
                    return
                }

                $multipleStreams = $streamsToQuery.Count -gt 1

                foreach ($stream in $streamsToQuery) {
                    $currentStreamId = $stream._id
                    $currentStreamName = $stream.displayName

                    $CacheKey = "XdrCloudAppsDiscoveryCategoryStat_${currentStreamId}_${Timeframe}"
                    $currentCacheValue = Get-XdrCache -CacheKey $CacheKey -ErrorAction SilentlyContinue
                    if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
                        Write-Verbose "Using cached Cloud Apps discovery category statistics for stream '$currentStreamName'"
                        $cachedResult = $currentCacheValue.Value
                        Add-StreamContext -Items $cachedResult -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                        $cachedResult
                        continue
                    } elseif ($Force) {
                        Write-Verbose "Force parameter specified, bypassing cache for stream '$currentStreamName'"
                        Clear-XdrCache -CacheKey $CacheKey
                    } else {
                        Write-Verbose "Cloud Apps discovery category statistics cache is missing or expired for stream '$currentStreamName'"
                    }

                    $Uri = "https://security.microsoft.com/apiproxy/mcas/cas/api/v1/discovery/category_stats/?streamId=$currentStreamId&timeframe=$Timeframe"
                    Write-Verbose "Retrieving Cloud Apps discovery category statistics for stream '$currentStreamName' ($currentStreamId)"

                    try {
                        $response = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $script:headers
                        $result = if ($null -ne $response.data) { $response.data } else { $response }
                        if ($null -ne $result) {
                            foreach ($item in $result) {
                                $item.PSObject.TypeNames.Insert(0, 'XdrCloudAppsDiscoveryCategoryStat')
                            }
                            Add-StreamContext -Items $result -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                            Set-XdrCache -CacheKey $CacheKey -Value $result -TTLMinutes 5
                        }
                        $result
                    } catch {
                        Write-Error "Failed to retrieve Cloud Apps discovery category statistics for stream '$currentStreamName': $_"
                    }
                }
            }

            "Constant" {
                $CacheKey = "XdrCloudAppsDiscoveryConstant"
                $currentCacheValue = Get-XdrCache -CacheKey $CacheKey -ErrorAction SilentlyContinue
                if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
                    Write-Verbose "Using cached Cloud Apps discovery constants"
                    return $currentCacheValue.Value
                } elseif ($Force) {
                    Write-Verbose "Force parameter specified, bypassing cache"
                    Clear-XdrCache -CacheKey $CacheKey
                } else {
                    Write-Verbose "Cloud Apps discovery constants cache is missing or expired"
                }

                $Uri = "https://security.microsoft.com/apiproxy/mcas/cas/api/v1/discovery/constants/"
                Write-Verbose "Retrieving Cloud Apps discovery constants"

                try {
                    $response = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $script:headers
                    $result = if ($null -ne $response.data) { $response.data } else { $response }
                    if ($null -ne $result) {
                        $result.PSObject.TypeNames.Insert(0, 'XdrCloudAppsDiscoveryConstant')
                        Set-XdrCache -CacheKey $CacheKey -Value $result -TTLMinutes 15
                    }
                    return $result
                } catch {
                    Write-Error "Failed to retrieve Cloud Apps discovery constants: $_"
                }
            }

            "Entity" {
                $entityEndpoints = @{
                    "IP"       = "ips"
                    "Machine"  = "machines"
                    "User"     = "users"
                    "Resource" = "resources"
                }
                $endpoint = $entityEndpoints[$EntityType]

                # Set default Timeframe for Entity if needed
                if (-not $PSBoundParameters.ContainsKey('Timeframe')) {
                    $Timeframe = if ($EntityType -eq "Resource") { 30 } else { 90 }
                }

                # Set default Limit if not specified
                if (-not $PSBoundParameters.ContainsKey('Limit')) {
                    $Limit = if ($EntityType -eq "Resource") { 20 } else { 100 }
                }

                # Resolve streams to query
                $resolveParams = @{ Force = $Force }
                if ($PSBoundParameters.ContainsKey('StreamId')) { $resolveParams.StreamId = $StreamId }
                if ($PSBoundParameters.ContainsKey('StreamName')) { $resolveParams.StreamName = $StreamName }

                $streamsToQuery = Get-XdrCloudAppsDiscoveryStream @resolveParams
                if (-not $streamsToQuery -or $streamsToQuery.Count -eq 0) {
                    Write-Warning "No streams to query. Use -ListStreams to see available streams."
                    return
                }

                $multipleStreams = $streamsToQuery.Count -gt 1

                foreach ($stream in $streamsToQuery) {
                    $currentStreamId = $stream._id
                    $currentStreamName = $stream.displayName

                    $CacheKey = "XdrCloudAppsDiscovery${EntityType}-$currentStreamId-$Timeframe-$AppId-$Limit-$Skip-$SortField-$SortDirection"
                    $currentCacheValue = Get-XdrCache -CacheKey $CacheKey -ErrorAction SilentlyContinue
                    if (-not $Force -and ($null -eq $Filters -or $Filters.Count -eq 0)) {
                        if ($currentCacheValue.NotValidAfter -gt (Get-Date)) {
                            Write-Verbose "Using cached Cloud Apps discovery $EntityType entities for stream '$currentStreamName'"
                            $cachedResult = $currentCacheValue.Value
                            Add-StreamContext -Items $cachedResult -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                            $cachedResult
                            continue
                        }
                    }
                    if ($Force) {
                        Write-Verbose "Force parameter specified, bypassing cache for stream '$currentStreamName'"
                        Clear-XdrCache -CacheKey $CacheKey
                    } else {
                        Write-Verbose "Cloud Apps discovery $EntityType cache is missing or expired for stream '$currentStreamName'"
                    }

                    $Uri = "https://security.microsoft.com/apiproxy/mcas/cas/api/v1/discovery/$endpoint/"

                    $bodyObj = @{
                        filters           = $Filters
                        limit             = $Limit
                        performAsyncTotal = if ($EntityType -eq "Resource") { $true } else { $false }
                        skip              = $Skip
                        sortDirection     = $SortDirection
                        sortField         = $SortField
                        streamId          = $currentStreamId
                        timeframe         = $Timeframe.ToString()
                    }

                    if ($EntityType -ne "Resource") {
                        if ($PSBoundParameters.ContainsKey('AppId')) {
                            $bodyObj.appId = $AppId
                        } else {
                            $bodyObj.appId = $null
                        }
                    }

                    $Body = $bodyObj | ConvertTo-Json -Compress -Depth 10
                    Write-Verbose "Retrieving Cloud Apps discovery ${EntityType}s for stream '$currentStreamName' ($currentStreamId)"

                    try {
                        $response = Invoke-RestMethod -Uri $Uri -Method Post -Body $Body -ContentType "application/json" -WebSession $script:session -Headers $script:headers
                        $result = if ($null -ne $response.data) { $response.data } else { $response }
                        if ($null -ne $result) {
                            foreach ($item in $result) {
                                $item.PSObject.TypeNames.Insert(0, "XdrCloudAppsDiscovery$EntityType")
                            }
                            Add-StreamContext -Items $result -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                            if ($null -eq $Filters -or $Filters.Count -eq 0) {
                                Set-XdrCache -CacheKey $CacheKey -Value $result -TTLMinutes 5
                            }
                        }
                        $result
                    } catch {
                        Write-Error "Failed to retrieve Cloud Apps discovery ${EntityType}s for stream '$currentStreamName': $_"
                    }
                }
            }

            "Location" {
                $CacheKey = "XdrCloudAppsDiscoveryLocation_${LocationType}_${Search}"
                $currentCacheValue = Get-XdrCache -CacheKey $CacheKey -ErrorAction SilentlyContinue
                if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
                    Write-Verbose "Using cached Cloud Apps discovery locations"
                    return $currentCacheValue.Value
                } elseif ($Force) {
                    Write-Verbose "Force parameter specified, bypassing cache"
                    Clear-XdrCache -CacheKey $CacheKey
                } else {
                    Write-Verbose "Cloud Apps discovery locations cache is missing or expired"
                }

                $Uri = "https://security.microsoft.com/apiproxy/mcas/cas/api/v1/discovery/get_locations/?locationId=$LocationId&locationType=$LocationType&search=$Search"
                Write-Verbose "Retrieving Cloud Apps discovery locations"

                try {
                    $response = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $script:headers
                    $result = if ($null -ne $response.data) { $response.data } else { $response }
                    if ($null -ne $result) {
                        foreach ($item in $result) {
                            $item.PSObject.TypeNames.Insert(0, 'XdrCloudAppsDiscoveryLocation')
                        }
                        Set-XdrCache -CacheKey $CacheKey -Value $result -TTLMinutes 15
                    }
                    return $result
                } catch {
                    Write-Error "Failed to retrieve Cloud Apps discovery locations: $_"
                }
            }

            "Top" {
                # Resolve streams to query
                $resolveParams = @{ Force = $Force }
                if ($PSBoundParameters.ContainsKey('StreamId')) { $resolveParams.StreamId = $StreamId }
                if ($PSBoundParameters.ContainsKey('StreamName')) { $resolveParams.StreamName = $StreamName }

                $streamsToQuery = Get-XdrCloudAppsDiscoveryStream @resolveParams
                if (-not $streamsToQuery -or $streamsToQuery.Count -eq 0) {
                    Write-Warning "No streams to query. Use -ListStreams to see available streams."
                    return
                }

                $multipleStreams = $streamsToQuery.Count -gt 1

                switch ($TopType) {
                    "App" {
                        if (-not $PSBoundParameters.ContainsKey('Limit')) { $Limit = 15 }

                        foreach ($stream in $streamsToQuery) {
                            $currentStreamId = $stream._id
                            $currentStreamName = $stream.displayName

                            $CacheKey = "XdrCloudAppsDiscoveryTopApp_${currentStreamId}_${Timeframe}_${CategoryFilter}_${Metric}_${Limit}_${Offset}"
                            $currentCacheValue = Get-XdrCache -CacheKey $CacheKey -ErrorAction SilentlyContinue
                            if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
                                Write-Verbose "Using cached Cloud Apps discovery top apps for stream '$currentStreamName'"
                                $cachedResult = $currentCacheValue.Value
                                Add-StreamContext -Items $cachedResult -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                                $cachedResult
                                continue
                            } elseif ($Force) {
                                Write-Verbose "Force parameter specified, bypassing cache for stream '$currentStreamName'"
                                Clear-XdrCache -CacheKey $CacheKey
                            } else {
                                Write-Verbose "Cloud Apps discovery top apps cache is missing or expired for stream '$currentStreamName'"
                            }

                            $Uri = "https://security.microsoft.com/apiproxy/mcas/cas/api/v1/discovery/top_apps/?streamId=$currentStreamId&timeframe=$Timeframe&category=$CategoryFilter&metric=$Metric&limit=$Limit&offset=$Offset"
                            Write-Verbose "Retrieving Cloud Apps discovery top apps for stream '$currentStreamName' ($currentStreamId)"

                            try {
                                $response = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $script:headers
                                $result = if ($null -ne $response.data) { $response.data } else { $response }
                                if ($null -ne $result) {
                                    foreach ($item in $result) {
                                        $item.PSObject.TypeNames.Insert(0, 'XdrCloudAppsDiscoveryTopApp')
                                    }
                                    Add-StreamContext -Items $result -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                                    Set-XdrCache -CacheKey $CacheKey -Value $result -TTLMinutes 5
                                }
                                $result
                            } catch {
                                Write-Error "Failed to retrieve Cloud Apps discovery top apps for stream '$currentStreamName': $_"
                            }
                        }
                    }

                    "Category" {
                        if (-not $PSBoundParameters.ContainsKey('Limit')) { $Limit = 10 }

                        $sanctioned = if ($ExcludeSanctioned) { "false" } else { "true" }
                        $unsanctioned = if ($ExcludeUnsanctioned) { "false" } else { "true" }
                        $other = if ($ExcludeOther) { "false" } else { "true" }

                        foreach ($stream in $streamsToQuery) {
                            $currentStreamId = $stream._id
                            $currentStreamName = $stream.displayName

                            $CacheKey = "XdrCloudAppsDiscoveryTopCategory_${currentStreamId}_${Timeframe}_${Metric}_${Limit}_${Offset}"
                            $currentCacheValue = Get-XdrCache -CacheKey $CacheKey -ErrorAction SilentlyContinue
                            if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
                                Write-Verbose "Using cached Cloud Apps discovery top categories for stream '$currentStreamName'"
                                $cachedResult = $currentCacheValue.Value
                                Add-StreamContext -Items $cachedResult -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                                $cachedResult
                                continue
                            } elseif ($Force) {
                                Write-Verbose "Force parameter specified, bypassing cache for stream '$currentStreamName'"
                                Clear-XdrCache -CacheKey $CacheKey
                            } else {
                                Write-Verbose "Cloud Apps discovery top categories cache is missing or expired for stream '$currentStreamName'"
                            }

                            $Uri = "https://security.microsoft.com/apiproxy/mcas/cas/api/v1/discovery/top_categories/?streamId=$currentStreamId&timeframe=$Timeframe&metric=$Metric&limit=$Limit&offset=$Offset&sanctioned=$sanctioned&unsanctioned=$unsanctioned&other=$other"
                            Write-Verbose "Retrieving Cloud Apps discovery top categories for stream '$currentStreamName' ($currentStreamId)"

                            try {
                                $response = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $script:headers
                                $result = if ($null -ne $response.data) { $response.data } else { $response }
                                if ($null -ne $result) {
                                    foreach ($item in $result) {
                                        $item.PSObject.TypeNames.Insert(0, 'XdrCloudAppsDiscoveryTopCategory')
                                    }
                                    Add-StreamContext -Items $result -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                                    Set-XdrCache -CacheKey $CacheKey -Value $result -TTLMinutes 5
                                }
                                $result
                            } catch {
                                Write-Error "Failed to retrieve Cloud Apps discovery top categories for stream '$currentStreamName': $_"
                            }
                        }
                    }

                    "Entity" {
                        foreach ($stream in $streamsToQuery) {
                            $currentStreamId = $stream._id
                            $currentStreamName = $stream.displayName

                            $CacheKey = "XdrCloudAppsDiscoveryTopEntity_${currentStreamId}_${TopEntityField}_${Timeframe}"
                            $currentCacheValue = Get-XdrCache -CacheKey $CacheKey -ErrorAction SilentlyContinue
                            if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
                                Write-Verbose "Using cached Cloud Apps discovery top entities ($TopEntityField) for stream '$currentStreamName'"
                                $cachedResult = $currentCacheValue.Value
                                Add-StreamContext -Items $cachedResult -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                                $cachedResult
                                continue
                            } elseif ($Force) {
                                Write-Verbose "Force parameter specified, bypassing cache for stream '$currentStreamName'"
                                Clear-XdrCache -CacheKey $CacheKey
                            } else {
                                Write-Verbose "Cloud Apps discovery top entities cache is missing or expired for stream '$currentStreamName'"
                            }

                            $Uri = "https://security.microsoft.com/apiproxy/mcas/cas/api/v1/discovery/top_entities/?field=$TopEntityField&streamId=$currentStreamId&timeframe=$Timeframe"
                            Write-Verbose "Retrieving Cloud Apps discovery top entities ($TopEntityField) for stream '$currentStreamName' ($currentStreamId)"

                            try {
                                $response = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $script:headers
                                $result = if ($null -ne $response.data) { $response.data } else { $response }
                                if ($null -ne $result) {
                                    foreach ($item in $result) {
                                        $item.PSObject.TypeNames.Insert(0, 'XdrCloudAppsDiscoveryTopEntity')
                                    }
                                    Add-StreamContext -Items $result -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                                    Set-XdrCache -CacheKey $CacheKey -Value $result -TTLMinutes 5
                                }
                                $result
                            } catch {
                                Write-Error "Failed to retrieve Cloud Apps discovery top entities for stream '$currentStreamName': $_"
                            }
                        }
                    }
                }
            }

            "UnsanctionedApp" {
                # Resolve streams to query
                $resolveParams = @{ Force = $Force }
                if ($PSBoundParameters.ContainsKey('StreamId')) { $resolveParams.StreamId = $StreamId }
                if ($PSBoundParameters.ContainsKey('StreamName')) { $resolveParams.StreamName = $StreamName }

                $streamsToQuery = Get-XdrCloudAppsDiscoveryStream @resolveParams
                if (-not $streamsToQuery -or $streamsToQuery.Count -eq 0) {
                    Write-Warning "No streams to query. Use -ListStreams to see available streams."
                    return
                }

                $multipleStreams = $streamsToQuery.Count -gt 1

                foreach ($stream in $streamsToQuery) {
                    $currentStreamId = $stream._id
                    $currentStreamName = $stream.displayName

                    $CacheKey = "XdrCloudAppsDiscoveryUnsanctionedApp_${currentStreamId}_${Timeframe}"
                    $currentCacheValue = Get-XdrCache -CacheKey $CacheKey -ErrorAction SilentlyContinue
                    if (-not $Force -and $currentCacheValue.NotValidAfter -gt (Get-Date)) {
                        Write-Verbose "Using cached Cloud Apps discovery unsanctioned apps for stream '$currentStreamName'"
                        $cachedResult = $currentCacheValue.Value
                        Add-StreamContext -Items $cachedResult -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                        $cachedResult
                        continue
                    } elseif ($Force) {
                        Write-Verbose "Force parameter specified, bypassing cache for stream '$currentStreamName'"
                        Clear-XdrCache -CacheKey $CacheKey
                    } else {
                        Write-Verbose "Cloud Apps discovery unsanctioned apps cache is missing or expired for stream '$currentStreamName'"
                    }

                    $Uri = "https://security.microsoft.com/apiproxy/mcas/cas/api/discovery/get_unsanctioned_apps/?streamId=$currentStreamId&timeframe=$Timeframe"
                    Write-Verbose "Retrieving Cloud Apps discovery unsanctioned apps for stream '$currentStreamName' ($currentStreamId)"

                    try {
                        $response = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -WebSession $script:session -Headers $script:headers
                        $result = if ($null -ne $response.data) { $response.data } else { $response }
                        if ($null -ne $result) {
                            foreach ($item in $result) {
                                $item.PSObject.TypeNames.Insert(0, 'XdrCloudAppsDiscoveryUnsanctionedApp')
                            }
                            Add-StreamContext -Items $result -StreamIdValue $currentStreamId -StreamNameValue $currentStreamName -AddContext $multipleStreams
                            Set-XdrCache -CacheKey $CacheKey -Value $result -TTLMinutes 5
                        }
                        $result
                    } catch {
                        Write-Error "Failed to retrieve Cloud Apps discovery unsanctioned apps for stream '$currentStreamName': $_"
                    }
                }
            }
        }
    }

    end {
        if ($PSCmdlet.ParameterSetName -eq 'DeanonymizeUser') {
            if ($usernamesToDeanonymize.Count -eq 0) {
                return
            }

            # Observed Cloud Apps deanonymization API value for user identities.
            # The current public cmdlet surface intentionally supports users only.
            $userEntityType = 1

            $body = @{
                usernames     = @($usernamesToDeanonymize)
                justification = $Justification
                entityType    = $userEntityType
            }

            return Invoke-XdrCloudAppsRequest -Path '/mcas/cas/api/v1/discovery/deanonymize_entity_names/' -Method Post -Body $body -TypeName 'XdrCloudAppsDiscoveryDeanonymizedUser' -Force:$Force
        }
    }
}