Modules/Private/Extraction/Start-AZTIEntraExtraction.ps1

<#
.Synopsis
    Extract Entra ID (Azure AD) resources via Microsoft Graph API.
 
.DESCRIPTION
    Queries Microsoft Graph for 15 Entra resource types and normalizes each item
    into a standard structure with a synthetic TYPE property (e.g., 'entra/users').
 
    Each normalized resource has:
      - id : Original object ID from Entra
      - name : Display name or principal name
      - type : Synthetic TYPE string (e.g., 'entra/users')
      - tenantId : The tenant ID
      - properties : Nested PSObject containing the full original data
 
    Uses Invoke-AZSCGraphRequest (Phase 2) for all Graph calls with automatic
    pagination, throttle handling, and exponential backoff.
 
.PARAMETER TenantID
    The Azure AD / Entra ID tenant identifier.
 
.OUTPUTS
    [PSCustomObject] with property EntraResources (array of normalized objects).
 
.LINK
    https://github.com/thisismydemo/azure-scout
 
.COMPONENT
    This PowerShell Module is part of Azure Scout (AZSC)
 
.NOTES
    Version: 1.0.0
    Authors: thisismydemo
#>

function Start-AZSCEntraExtraction {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$TenantID
    )

    Write-Host 'Starting Entra ID Extraction: ' -NoNewline
    Write-Host '15 Resource Types' -ForegroundColor Cyan

    $allEntraResources = [System.Collections.Generic.List[object]]::new()

    # ── Helper: normalize a single Graph item into standard structure ──
    function Add-NormalizedResource {
        param(
            [object[]]$Items,
            [string]$SyntheticType,
            [string]$NameProperty = 'displayName'
        )
        foreach ($item in $Items) {
            if ($null -eq $item) { continue }

            $name = $null
            if ($item.PSObject.Properties.Name -contains $NameProperty) {
                $name = $item.$NameProperty
            }
            elseif ($item.PSObject.Properties.Name -contains 'displayName') {
                $name = $item.displayName
            }
            elseif ($item.PSObject.Properties.Name -contains 'userPrincipalName') {
                $name = $item.userPrincipalName
            }

            $normalized = [PSCustomObject]@{
                id         = $item.id
                name       = $name
                TYPE       = $SyntheticType
                tenantId   = $TenantID
                properties = $item
            }

            $allEntraResources.Add($normalized)
        }
    }

    # ── Define the 15 Entra resource type queries ──
    $entraQueries = @(
        @{
            Name         = 'Users'
            Uri          = '/v1.0/users?$select=id,displayName,userPrincipalName,userType,accountEnabled,createdDateTime,assignedLicenses,onPremisesSyncEnabled,department,jobTitle,mail,lastPasswordChangeDateTime'
            Type         = 'entra/users'
            NameProperty = 'userPrincipalName'
        },
        @{
            Name         = 'Groups'
            Uri          = '/v1.0/groups?$select=id,displayName,groupTypes,securityEnabled,mailEnabled,isAssignableToRole,membershipRule,onPremisesSyncEnabled,description'
            Type         = 'entra/groups'
            NameProperty = 'displayName'
        },
        @{
            Name         = 'Applications'
            Uri          = '/v1.0/applications?$select=id,displayName,appId,signInAudience,keyCredentials,passwordCredentials,requiredResourceAccess,publisherDomain,createdDateTime'
            Type         = 'entra/applications'
            NameProperty = 'displayName'
        },
        @{
            Name         = 'Service Principals'
            Uri          = '/v1.0/servicePrincipals?$select=id,displayName,appId,servicePrincipalType,accountEnabled,appOwnerOrganizationId,keyCredentials,passwordCredentials,tags'
            Type         = 'entra/serviceprincipals'
            NameProperty = 'displayName'
        },
        @{
            Name         = 'Managed Identities'
            Uri          = '/v1.0/servicePrincipals?$filter=servicePrincipalType eq ''ManagedIdentity''&$select=id,displayName,appId,servicePrincipalType,alternativeNames'
            Type         = 'entra/managedidentities'
            NameProperty = 'displayName'
        },
        @{
            Name         = 'Directory Roles'
            Uri          = '/v1.0/directoryRoles?$select=id,displayName,roleTemplateId,description'
            Type         = 'entra/directoryroles'
            NameProperty = 'displayName'
        },
        @{
            Name         = 'PIM Assignments'
            Uri          = '/v1.0/roleManagement/directory/roleAssignments?$expand=principal($select=id,displayName),roleDefinition($select=id,displayName)'
            Type         = 'entra/pimassignments'
            NameProperty = 'principalId'
        },
        @{
            Name         = 'Conditional Access Policies'
            Uri          = '/v1.0/identity/conditionalAccess/policies'
            Type         = 'entra/conditionalaccesspolicies'
            NameProperty = 'displayName'
        },
        @{
            Name         = 'Named Locations'
            Uri          = '/v1.0/identity/conditionalAccess/namedLocations'
            Type         = 'entra/namedlocations'
            NameProperty = 'displayName'
        },
        @{
            Name         = 'Administrative Units'
            Uri          = '/v1.0/directory/administrativeUnits?$select=id,displayName,description,membershipType,membershipRule'
            Type         = 'entra/administrativeunits'
            NameProperty = 'displayName'
        },
        @{
            Name         = 'Domains'
            Uri          = '/v1.0/domains'
            Type         = 'entra/domains'
            NameProperty = 'id'
        },
        @{
            Name         = 'Subscribed SKUs'
            Uri          = '/v1.0/subscribedSkus'
            Type         = 'entra/subscribedskus'
            NameProperty = 'skuPartNumber'
        },
        @{
            Name         = 'Cross-Tenant Access'
            Uri          = '/v1.0/policies/crossTenantAccessPolicy/partners'
            Type         = 'entra/crosstenantaccess'
            NameProperty = 'tenantId'
        },
        @{
            Name         = 'Security Policies'
            Uri          = '/v1.0/policies/authorizationPolicy'
            Type         = 'entra/securitypolicies'
            NameProperty = 'displayName'
            SingleObject = $true
        },
        @{
            Name         = 'Risky Users'
            Uri          = '/v1.0/identityProtection/riskyUsers'
            Type         = 'entra/riskyusers'
            NameProperty = 'userPrincipalName'
        },
        @{
            Name         = 'Identity Providers'
            Uri          = '/v1.0/identity/identityProviders'
            Type         = 'entra/identityproviders'
            NameProperty = 'displayName'
        },
        @{
            Name         = 'Security Defaults'
            Uri          = '/v1.0/policies/identitySecurityDefaultsEnforcementPolicy'
            Type         = 'entra/securitydefaults'
            NameProperty = 'displayName'
            SingleObject = $true
        }
    )

    # ── Execute each query with graceful degradation ──
    $queryIndex = 0
    $totalQueries = $entraQueries.Count

    foreach ($query in $entraQueries) {
        $queryIndex++
        $percentComplete = [math]::Round(($queryIndex / $totalQueries) * 100)

        Write-Progress -Activity 'Entra ID Extraction' -Status "$($query.Name) ($queryIndex/$totalQueries)" -PercentComplete $percentComplete
        Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Entra: Querying $($query.Name) [$($query.Uri)]")

        try {
            $result = Invoke-AZSCGraphRequest -Uri $query.Uri

            if ($null -ne $result) {
                # Handle single-object endpoints (e.g., authorizationPolicy)
                if ($query.ContainsKey('SingleObject') -and $query.SingleObject) {
                    # Single object — wrap in array
                    if ($result -is [array]) {
                        Add-NormalizedResource -Items $result -SyntheticType $query.Type -NameProperty $query.NameProperty
                    }
                    else {
                        Add-NormalizedResource -Items @($result) -SyntheticType $query.Type -NameProperty $query.NameProperty
                    }
                }
                else {
                    # Collection endpoint — result is already an array from Invoke-AZSCGraphRequest
                    if ($result -is [array]) {
                        Add-NormalizedResource -Items $result -SyntheticType $query.Type -NameProperty $query.NameProperty
                    }
                    else {
                        Add-NormalizedResource -Items @($result) -SyntheticType $query.Type -NameProperty $query.NameProperty
                    }
                }

                Write-Host " [" -NoNewline
                Write-Host "OK" -ForegroundColor Green -NoNewline
                Write-Host "] $($query.Name): " -NoNewline

                $count = if ($result -is [array]) { $result.Count } else { 1 }
                Write-Host "$count items" -ForegroundColor Cyan
            }
            else {
                Write-Host " [" -NoNewline
                Write-Host "--" -ForegroundColor DarkGray -NoNewline
                Write-Host "] $($query.Name): " -NoNewline
                Write-Host "No data returned" -ForegroundColor DarkGray
            }
        }
        catch {
            # Graceful degradation — log the error but continue with remaining resource types
            Write-Host " [" -NoNewline
            Write-Host "SKIP" -ForegroundColor Yellow -NoNewline
            Write-Host "] $($query.Name): " -NoNewline
            Write-Host "$($_.Exception.Message)" -ForegroundColor Yellow
            Write-Debug ((Get-Date -Format 'yyyy-MM-dd_HH_mm_ss') + " - Entra: FAILED $($query.Name) — $($_.Exception.Message)")
        }
    }

    Write-Progress -Activity 'Entra ID Extraction' -Completed

    $entraCount = $allEntraResources.Count
    Write-Host "Entra ID Extraction Complete: " -NoNewline -ForegroundColor Green
    Write-Host "$entraCount total resources across $totalQueries types" -ForegroundColor Cyan

    return [PSCustomObject]@{
        EntraResources = $allEntraResources.ToArray()
    }
}