functions/New-AzStateDiscovery.ps1

#################################
# Internal function definitions #
#################################

function Get-AzStateChildrenByType {
    ###############################################
    # Configure PSScriptAnalyzer rule suppression #
    ###############################################

    # The following SuppressMessageAttribute entries are used to surpress
    # PSScriptAnalyzer tests against known exceptions as per:
    # https://github.com/powershell/psscriptanalyzer#suppressing-rules
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ExcludePathIds', Justification = 'False positive: used in process block of function')]

    [CmdletBinding()]
    [OutputType([AzState[]])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [AzState[]]$AzStateInputs,
        [Parameter(Mandatory = $false, HelpMessage = "If provided, ExcludePathIds is used to surpress specific paths in the discovery process")]
        [String[]]$ExcludePathIds,
        [Parameter()]
        [Switch]$IncludeManagementGroups,
        [Parameter()]
        [Switch]$IncludeSubscriptions,
        [Parameter()]
        [Switch]$IncludeResourceGroups,
        [Parameter()]
        [Switch]$IncludeResources,
        [Parameter()]
        [Switch]$IncludeIAM,
        [Parameter()]
        [Switch]$IncludePolicy,
        [Parameter()]
        [Int]$ThrottleLimit,
        [Parameter()]
        [Switch]$SkipCache
    )

    begin {

        # The begin block is used to setup the environment.
        # This includes initialising all variables and determining
        # which resource types to discover.

        [AzState[]]$AzStateOutput = @()
        [String[]]$FilterChildrenByType = @()
        $ChildrenToProcess = @()
        $IAMToProcess = @()
        $PolicyToProcess = @()

        if ($IncludeManagementGroups) {
            $FilterChildrenByType += "Microsoft.Management/managementGroups"
        }

        if ($IncludeSubscriptions) {
            $FilterChildrenByType += "Microsoft.Management/managementGroups/subscriptions", "Microsoft.Resources/subscriptions"
        }

        if ($IncludeResourceGroups) {
            $FilterChildrenByType += "Microsoft.Resources/resourceGroups"
        }

        if ($IncludeIAM -and $IncludePolicy) {
            $DiscoveryMode = [DiscoveryMode]"IncludeBoth"
        }
        elseif ($IncludeIAM) {
            $DiscoveryMode = [DiscoveryMode]"IncludeIAM"
        }
        elseif ($IncludePolicy) {
            $DiscoveryMode = [DiscoveryMode]"IncludePolicy"
        }
        else {
            $DiscoveryMode = [DiscoveryMode]"ExcludeBoth"
        }

        if ($SkipCache) {
            $CacheMode = [CacheMode]"SkipCache"
        }
        else {
            $CacheMode = [CacheMode]"UseCache"
        }

    }

    process {

        # The process block is used to build a list of all
        # resources from AzStateInputs.
        # This ensures that each AzStateInputs object is added
        # to the xToProcess variables before building the
        # AzStateOutput to return.

        foreach ($AzStateInput in $AzStateInputs) {
            if ($AzStateInput.Children) {
                # The following is to avoid needing to list all Resource Types in
                # FilterChildrenByType when IncludeResources is specified
                if ($IncludeResources -and ($AzStateInput.Type -ieq "Microsoft.Resources/resourceGroups")) {
                    $ChildrenToProcess += $AzStateInput.Children | `
                        Where-Object { $_.Id -ne "" } | `
                        Where-Object { $_.Id -inotin $ExcludePathIds }
                }
                else {
                    $ChildrenToProcess += $AzStateInput.Children | `
                        Where-Object { $_.Id -ne "" } | `
                        Where-Object { $_.Id -inotin $ExcludePathIds } | `
                        Where-Object { $_.Type -iin $FilterChildrenByType }
                }
            }
            if ($IncludeIAM) {
                foreach ($IamPathSuffix in [AzState]::IamPathSuffixes($_.Type)) {
                    $IAMPath = $_.Id + $IamPathSuffix
                    $IAMToProcess += $IAMPath
                }
            }
            if ($IncludePolicy) {
                foreach ($PolicyPathSuffix in [AzState]::PolicyPathSuffixes($_.Type)) {
                    $PolicyPath = $_.Id + $PolicyPathSuffix
                    $PolicyToProcess += $PolicyPath
                }
            }
        }

    }

    end {

        # The end block is used to generate and return the
        # AzStateOutput from all xToProcess variables.
        # This ensure optimal parallel processing as the content
        # of all AzStateInputs is aggregated first.

        if ($ChildrenToProcess) {
            # Determine how many of each Resource Type are to be processed (for logging information only)
            $ResourceProfile = $ChildrenToProcess | Group-Object -Property Type
            foreach ($Profile in $ResourceProfile) {
                Write-Verbose "[Get-AzStateChildrenByType] Processing [$($Profile.Count)] Resources of Type [$($Profile.Name)]"
            }
            $IdsToProcess = $ChildrenToProcess.Id | Sort-Object
            if ($ThrottleLimit) {
                $AzStateOutput += [AzState]::FromIds($IdsToProcess, $ThrottleLimit, $CacheMode, $DiscoveryMode)
            }
            else {
                $AzStateOutput += [AzState]::FromIds($IdsToProcess, $CacheMode, $DiscoveryMode)
            }
        }
        if ($IAMToProcess) {
            Write-Verbose "[Get-AzStateChildrenByType] Processing [IAM] settings for [$($IAMToProcess.Count)] Resources"
            if ($CacheMode) {
                $AzStateOutput += [AzState]::DirectFromScope($IAMToProcess, $CacheMode) | Sort-Object -Property Id -Unique
            }
            else {
                $AzStateOutput += [AzState]::DirectFromScope($IAMToProcess) | Sort-Object -Property Id -Unique
            }
        }
        if ($PolicyToProcess) {
            Write-Verbose "[Get-AzStateChildrenByType] Processing [Policy] settings for [$($PolicyToProcess.Count)] Resources"
            if ($CacheMode) {
                $AzStateOutput += [AzState]::DirectFromScope($PolicyToProcess, $CacheMode) | Sort-Object -Property Id -Unique
            }
            else {
                $AzStateOutput += [AzState]::DirectFromScope($PolicyToProcess) | Sort-Object -Property Id -Unique
            }
        }

        return $AzStateOutput | Sort-Object -Property Id -Unique

    }

}

###############################
# Primary function definition #
###############################

function New-AzStateDiscovery {
    ###############################################
    # Configure PSScriptAnalyzer rule suppression #
    ###############################################

    # The following SuppressMessageAttribute entries are used to surpress
    # PSScriptAnalyzer tests against known exceptions as per:
    # https://github.com/powershell/psscriptanalyzer#suppressing-rules
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Only creating new object with custom type')] # May refactor to support ShouldProcess

    [CmdletBinding()]
    [OutputType([Object[]])]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [String[]]$RootId,
        [Parameter()]
        [String[]]$ExcludePathIds,
        [Parameter()]
        [Switch]$IncludeManagementGroups,
        [Parameter()]
        [Switch]$IncludeSubscriptions,
        [Parameter()]
        [Switch]$IncludeResourceGroups,
        [Parameter()]
        [Switch]$IncludeResources,
        [Parameter()]
        [Switch]$IncludeIAM,
        [Parameter()]
        [Switch]$IncludePolicy,
        [Parameter()]
        [Switch]$Recurse,
        [Parameter()]
        [Int]$ThrottleLimit,
        [Parameter()]
        [Switch]$SkipCache
    )

    begin {

        [AzState[]]$AzStateDiscoveryOutput = @()

        Write-Verbose -Message "############################################################"
        Write-Verbose -Message "[AzStateDiscovery] Starting AzState Discovery for [$($RootId.Count)] Root Nodes"
        Write-Verbose -Message "$("[AzStateDiscovery] {0} Management Groups" -f $(($IncludeManagementGroups) ? {Including} : {Excluding} ))"
        Write-Verbose -Message "$("[AzStateDiscovery] {0} Subscriptions" -f $(($IncludeSubscriptions) ? {Including} : {Excluding} ))"
        Write-Verbose -Message "$("[AzStateDiscovery] {0} Resource Groups" -f $(($IncludeResourceGroups) ? {Including} : {Excluding} ))"
        Write-Verbose -Message "$("[AzStateDiscovery] {0} Resources" -f $(($IncludeResources) ? {Including} : {Excluding} ))"
        Write-Verbose -Message "$("[AzStateDiscovery] {0} Access control (IAM)" -f $(($IncludeIAM) ? {Including} : {Excluding} ))"
        Write-Verbose -Message "$("[AzStateDiscovery] {0} Policy" -f $(($IncludePolicy) ? {Including} : {Excluding} ))"
        Write-Verbose -Message "$("[AzStateDiscovery] Using Recurse [{0}]" -f $(($Recurse) ? {True} : {False} ))"
        Write-Verbose -Message "$("[AzStateDiscovery] Using ThrottleLimit [{0}]" -f $(($ThrottleLimit) ? {$ThrottleLimit} : {Default} ))"
        Write-Verbose -Message "$("[AzStateDiscovery] Using Cache Mode [{0}]" -f $(($SkipCache) ? {SkipCache} : {UseCache} ))"
        if ($ExcludePathIds) {
            Write-Verbose -Message "[AzStateDiscovery] Excluding Resource IDs:"
            $ExcludePathIds | ForEach-Object { Write-Verbose -Message "[AzStateDiscovery] - [$_]" }
        }
        Write-Verbose -Message "############################################################"

    }

    process {

        foreach ($Id in $RootId) {

            Write-Verbose "[AzStateDiscovery] Setting Root Id [$Id]"

            $ArgumentList = @{
                Id            = $Id
                IncludeIAM    = $IncludeIAM
                IncludePolicy = $IncludePolicy
                SkipCache     = $SkipCache
            }

            $RootAzState = New-AzState @ArgumentList

            $AzStateDiscoveryOutput += $RootAzState

            $ArgumentListChildren = @{
                IncludeManagementGroups = $IncludeManagementGroups
                IncludeSubscriptions    = $IncludeSubscriptions
                IncludeResourceGroups   = $IncludeResourceGroups
                IncludeResources        = $IncludeResources
                IncludeIAM              = $IncludeIAM
                IncludePolicy           = $IncludePolicy
                SkipCache               = $SkipCache
            }
            if ($ExcludePathIds) {
                $ArgumentListChildren += @{
                    ExcludePathIds = $ExcludePathIds
                }
            }
            if ($ThrottleLimit) {
                $ArgumentListChildren += @{
                    ThrottleLimit = $ThrottleLimit
                }
            }

            # The following loop will discovery all children by type based on the selected
            # switches and will recurse if selected
            $DiscoveryComplete = $false
            do {
                $RootAzState = $RootAzState | Get-AzStateChildrenByType @ArgumentListChildren
                $AzStateDiscoveryOutput += $RootAzState
                if ((-not $RootAzState) -or (-not $Recurse)) {
                    $DiscoveryComplete = $true
                }
            } until ($DiscoveryComplete)

        }
    }

    end {
        # Once processing is complete return the final array of AzState objects
        # Need to sort unique due to duplicate Policy and Role definitions across multiple resources
        return $AzStateDiscoveryOutput | Sort-Object -Property Id -Unique
    }

}