Public/Invoke-IdentityAtlas.ps1

function Invoke-IdentityAtlas {
    [CmdletBinding()]
    param(
        [string] $OutputPath = (Join-Path (Get-Location) "IdentityAtlasReport-$([datetime]::Now.ToString('yyyyMMdd-HHmmss'))"),

        [switch] $OpenReport,

        [ValidateSet('Auto', 'Core', 'Governance')]
        [string] $CollectionProfile = 'Auto',

        [switch] $SkipSlowCollectors,

        [ValidateSet('GroupMembersAndOwners', 'DeviceOwners', 'AuthenticationMethods', 'ApplicationRoleAssignments', 'ApplicationOwners')]
        [string[]] $SkipCollector = @(),

        [ValidateRange(1, 20)]
        [int] $BatchSize = 10,

        [ValidateRange(1024, 65535)]
        [int] $Port = 8766,

        [ValidateRange(0, 50)]
        [int] $PortSearchLimit = 20
    )

    if (-not (Get-Command -Name Get-MgContext -ErrorAction SilentlyContinue)) {
        throw 'Microsoft.Graph.Authentication is required for live collection. Run Connect-IdentityAtlas after installing the dependency.'
    }

    $context = Get-MgContext
    if (-not $context -or -not $context.TenantId) {
        throw 'No Microsoft Graph PowerShell session is active. Run Connect-IdentityAtlas first.'
    }

    if ($CollectionProfile -eq 'Auto') {
        $storedProfile = Get-Variable -Name IdentityAtlasCollectionProfile -Scope Script -ErrorAction SilentlyContinue
        $CollectionProfile = if ($storedProfile -and $storedProfile.Value) { $storedProfile.Value } else { 'Core' }
    }

    $skippedCollector = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::OrdinalIgnoreCase
    )
    foreach ($collectorName in $SkipCollector) {
        [void] $skippedCollector.Add($collectorName)
    }
    if ($SkipSlowCollectors) {
        foreach ($collectorName in @('GroupMembersAndOwners', 'DeviceOwners', 'AuthenticationMethods', 'ApplicationRoleAssignments', 'ApplicationOwners')) {
            [void] $skippedCollector.Add($collectorName)
        }
    }

    $stepCount = if ($CollectionProfile -eq 'Governance') { 13 } else { 9 }
    $effectiveBatchSize = $BatchSize
    $progressSummary = $null
    Initialize-AtlasProgress `
        -StepCount $stepCount `
        -CollectionProfile $CollectionProfile `
        -SkippedCollector @($skippedCollector) | Out-Null

    try {
        $permissionPreflight = New-AtlasPermissionPreflightResult -ContextScope @($context.Scopes) -CollectionProfile $CollectionProfile
        $users = Invoke-AtlasCollector -Name 'users' -DisplayName 'Users' -Step 1 -Collector {
            Get-AtlasUser -TenantId $context.TenantId
        }
        $groups = Invoke-AtlasCollector -Name 'groups' -DisplayName 'Groups' -Step 2 -Collector {
            Get-AtlasGroup `
                -TenantId $context.TenantId `
                -SkipMembersAndOwners:$skippedCollector.Contains('GroupMembersAndOwners')
        }
        $identityCollection = Merge-AtlasCollectionResult -Result @($permissionPreflight, $users, $groups)
        $devicesAndAuthentication = Invoke-AtlasCollector -Name 'devicesAndAuthentication' -DisplayName 'Devices and authentication methods' -Step 3 -Collector {
            Get-AtlasDeviceAndAuthentication `
                -TenantId $context.TenantId `
                -KnownNode @($identityCollection.Nodes) `
                -SkipDeviceOwners:$skippedCollector.Contains('DeviceOwners') `
                -SkipAuthenticationMethods:$skippedCollector.Contains('AuthenticationMethods') `
                -BatchSize $effectiveBatchSize
        }
        $roles = Invoke-AtlasCollector -Name 'directoryRoles' -DisplayName 'Directory roles' -Step 4 -Collector {
            Get-AtlasDirectoryRole -TenantId $context.TenantId -KnownNode @($identityCollection.Nodes)
        }
        $applications = Invoke-AtlasCollector -Name 'applications' -DisplayName 'Applications' -Step 5 -Collector {
            Get-AtlasApplication `
                -TenantId $context.TenantId `
                -KnownNode @($identityCollection.Nodes) `
                -SkipAppRoleAssignments:$skippedCollector.Contains('ApplicationRoleAssignments') `
                -SkipOwners:$skippedCollector.Contains('ApplicationOwners') `
                -BatchSize $effectiveBatchSize
        }
        $identityAndApplicationCollection = Merge-AtlasCollectionResult -Result @($identityCollection, $applications)
        $applicationManagementPolicies = Invoke-AtlasCollector -Name 'applicationManagementPolicies' -DisplayName 'Application management policies' -Step 6 -Collector {
            Get-AtlasApplicationManagementPolicy -TenantId $context.TenantId -KnownNode @($identityAndApplicationCollection.Nodes)
        }
        $crossTenantAccess = Invoke-AtlasCollector -Name 'crossTenantAccess' -DisplayName 'Cross-tenant access settings' -Step 7 -Collector {
            Get-AtlasCrossTenantAccess -TenantId $context.TenantId
        }
        $conditionalAccess = Invoke-AtlasCollector -Name 'conditionalAccess' -DisplayName 'Conditional Access policies' -Step 8 -Collector {
            Get-AtlasConditionalAccessPolicy -TenantId $context.TenantId -KnownNode @($identityAndApplicationCollection.Nodes)
        }
        $conditionalAccessReferences = Invoke-AtlasCollector -Name 'conditionalAccessReferences' -DisplayName 'Conditional Access references' -Step 9 -Collector {
            Get-AtlasConditionalAccessReference -TenantId $context.TenantId -KnownNode @($conditionalAccess.Nodes)
        }
        $coreCollection = Merge-AtlasCollectionResult -Result @(
            $identityCollection
            $devicesAndAuthentication
            $roles
            $applications
            $applicationManagementPolicies
            $crossTenantAccess
            $conditionalAccess
            $conditionalAccessReferences
        )

        $governanceResults = @()
        if ($CollectionProfile -eq 'Governance') {
            $administrativeUnits = Invoke-AtlasCollector -Name 'administrativeUnits' -DisplayName 'Administrative Units' -Step 10 -Collector {
                Get-AtlasAdministrativeUnit -TenantId $context.TenantId -KnownNode @($coreCollection.Nodes) -KnownEdge @($roles.Edges)
            }
            $pimGroups = Invoke-AtlasCollector -Name 'pimGroups' -DisplayName 'PIM for Groups assignments' -Step 11 -Collector {
                Get-AtlasPrivilegedGroupAssignment -TenantId $context.TenantId -KnownNode @($coreCollection.Nodes)
            }
            $governanceFoundation = Merge-AtlasCollectionResult -Result @($coreCollection, $administrativeUnits, $pimGroups)
            $entitlementManagement = Invoke-AtlasCollector -Name 'entitlementManagement' -DisplayName 'Entitlement Management' -Step 12 -Collector {
                Get-AtlasEntitlementManagement -TenantId $context.TenantId -KnownNode @($governanceFoundation.Nodes)
            }
            $governanceWithEntitlements = Merge-AtlasCollectionResult -Result @($governanceFoundation, $entitlementManagement)
            $accessReviews = Invoke-AtlasCollector -Name 'accessReviews' -DisplayName 'Access Reviews' -Step 13 -Collector {
                Get-AtlasAccessReview -TenantId $context.TenantId -KnownNode @($governanceWithEntitlements.Nodes)
            }
            $governanceResults = @($administrativeUnits, $pimGroups, $entitlementManagement, $accessReviews)
        }
        $collection = Merge-AtlasCollectionResult -Result (@($coreCollection) + $governanceResults)

        $collectors = @(
            @{ name = 'permissionPreflight'; status = $permissionPreflight.Status; metrics = $permissionPreflight.Metrics }
            @{ name = 'users'; status = $users.Status; metrics = $users.Metrics }
            @{ name = 'groups'; status = $groups.Status; metrics = $groups.Metrics }
            @{ name = 'devicesAndAuthentication'; status = $devicesAndAuthentication.Status; metrics = $devicesAndAuthentication.Metrics }
            @{ name = 'directoryRoles'; status = $roles.Status; metrics = $roles.Metrics }
            @{ name = 'applications'; status = $applications.Status; metrics = $applications.Metrics }
            @{ name = 'applicationManagementPolicies'; status = $applicationManagementPolicies.Status; metrics = $applicationManagementPolicies.Metrics }
            @{ name = 'crossTenantAccess'; status = $crossTenantAccess.Status; metrics = $crossTenantAccess.Metrics }
            @{ name = 'conditionalAccess'; status = $conditionalAccess.Status; metrics = $conditionalAccess.Metrics }
            @{ name = 'conditionalAccessReferences'; status = $conditionalAccessReferences.Status; metrics = $conditionalAccessReferences.Metrics }
        )
        if ($CollectionProfile -eq 'Governance') {
            $collectors += @(
                @{ name = 'administrativeUnits'; status = $administrativeUnits.Status; metrics = $administrativeUnits.Metrics }
                @{ name = 'pimGroups'; status = $pimGroups.Status; metrics = $pimGroups.Metrics }
                @{ name = 'entitlementManagement'; status = $entitlementManagement.Status; metrics = $entitlementManagement.Metrics }
                @{ name = 'accessReviews'; status = $accessReviews.Status; metrics = $accessReviews.Metrics }
            )
        }
        $report = New-AtlasReport -TenantId $context.TenantId -TenantDisplayName $context.TenantId -Collection $collection -Collectors $collectors -DataOrigin LiveTenant -CollectionProfile $CollectionProfile
        $indexFile = Write-AtlasReport -Report $report -OutputPath $OutputPath
        $progressSummary = Complete-AtlasProgress -Status Complete -OutputPath $indexFile.DirectoryName
    }
    catch [System.Management.Automation.PipelineStoppedException] {
        [void] (Complete-AtlasProgress -Status Cancelled -OutputPath $OutputPath)
        throw
    }
    catch [System.OperationCanceledException] {
        [void] (Complete-AtlasProgress -Status Cancelled -OutputPath $OutputPath)
        throw
    }
    catch {
        [void] (Complete-AtlasProgress -Status Failed -OutputPath $OutputPath)
        throw
    }

    $server = $null
    if ($OpenReport) {
        $server = Start-AtlasReportServer `
            -ReportRoot $indexFile.DirectoryName `
            -Port $Port `
            -PortSearchLimit $PortSearchLimit `
            -OpenBrowser
    }

    return [pscustomobject] @{
        OutputPath = $indexFile.DirectoryName
        IndexPath = $indexFile.FullName
        NodeCount = $report.manifest.counts.nodes
        EdgeCount = $report.manifest.counts.edges
        EvidenceCount = $report.manifest.counts.evidence
        CoverageStatus = $report.manifest.coverage.status
        CollectionProfile = $CollectionProfile
        Duration = $progressSummary.Duration
        RequestCount = $progressSummary.RequestCount
        RetryCount = $progressSummary.RetryCount
        SkippedCollectors = @($progressSummary.SkippedCollectors)
        ReportUrl = if ($server) { $server.Url } else { $null }
        ServerProcessId = if ($server) { $server.ProcessId } else { $null }
    }
}