Skip to content

O(N×M) and O(N×R) scans in App Registration processing #20

@StrongWind1

Description

@StrongWind1

Disclaimer: This issue was identified and written by Claude Code (model: claude-opus-4-6-1m) during an automated code review, and has had a cursory review by a human before submission.

Summary

check_AppRegistrations.psm1 contains two patterns that perform full scans of large collections for every app registration processed. In large tenants (thousands of app registrations, enterprise apps, and role assignments), these become significant bottlenecks.

Affected file

modules/check_AppRegistrations.psm1

Issue 1: Enterprise App matching — O(N×M)

Evidence (line 606)

For every app registration, the entire $EnterpriseApps hashtable is iterated to find the matching service principal by AppId:

# Line 606-610
$EnterpriseApps.GetEnumerator() | Where-Object { $_.Value.AppId -eq $item.AppId } |
    Select-Object -First 1 | ForEach-Object {
        $ImpactScore += $_.Value.Impact
        $SPObjectID = $_.Name
        $ApiDelegatedCount = $_.Value.ApiDelegated
    }

If there are 2,000 app registrations and 5,000 enterprise apps, this performs 10,000,000 property comparisons.

Suggested fix

Build a lookup hashtable before the main loop:

# Before the main processing loop:
$SPByAppId = @{}
$EnterpriseApps.GetEnumerator() | ForEach-Object {
    $SPByAppId[$_.Value.AppId] = $_
}

# Inside the loop (replaces lines 606-610):
$matchingSP = $SPByAppId[$item.AppId]
if ($matchingSP) {
    $ImpactScore += $matchingSP.Value.Impact
    $SPObjectID = $matchingSP.Name
    $ApiDelegatedCount = $matchingSP.Value.ApiDelegated
}

This reduces the operation from O(N×M) to O(N+M).

Issue 2: Per-app role assignment scanning — O(N×R)

Evidence (lines 493-510)

For each app registration, the code scans ALL tenant role assignments to find scoped Cloud Application Administrators and Application Administrators:

# Line 493 (inside the per-app-registration loop)
$CloudAppAdminCurrentApp = $TenantRoleAssignments.Values |
    ForEach-Object { $_ |
        Where-Object {
            $_.RoleDefinitionId -eq "158c047a-c907-4556-b7ef-446551a6b5f7" -and
            $_.DirectoryScopeId -eq "/$($item.Id)"
        }
    } | Select-Object PrincipalId, AssignmentType

The same pattern repeats for Application Administrator at line 507. With 2,000 app registrations and 10,000 total role assignments, this is 40,000,000 comparisons (2 roles × 2,000 apps × 10,000 assignments).

Suggested fix

Pre-index scoped role assignments by DirectoryScopeId before the main loop:

# Before the main loop:
$ScopedCloudAppAdmins = @{}
$ScopedAppAdmins = @{}
$TenantRoleAssignments.Values | ForEach-Object {
    $_ | Where-Object { $_.RoleDefinitionId -eq "158c047a-c907-4556-b7ef-446551a6b5f7" } |
        ForEach-Object { $ScopedCloudAppAdmins[$_.DirectoryScopeId] += @($_) }
    $_ | Where-Object { $_.RoleDefinitionId -eq "9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3" } |
        ForEach-Object { $ScopedAppAdmins[$_.DirectoryScopeId] += @($_) }
}

# Inside the loop:
$CloudAppAdminCurrentApp = $ScopedCloudAppAdmins["/$($item.Id)"]
$AppAdminCurrentApp = $ScopedAppAdmins["/$($item.Id)"]

Note

The tenant-wide admin lookups at lines 187-208 run once (not per-app) and are not a performance concern. Only the per-app scoped lookups at lines 493-510 are quadratic.

Version

V20260316

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions