SHELL/APP.ROLES.STATUS.ps1
|
$CheckId = "APP.ROLES.STATUS" $Title = "Supplemental - Application permissions and critical app role assignments" $Level = "L1" $BenchmarkType = "Automated" function Get-PropValue { param( [AllowNull()]$Object, [string]$Name ) if ($null -eq $Object) { return $null } if ($Object -is [System.Collections.IDictionary]) { foreach ($Key in $Object.Keys) { if ([string]$Key -ieq $Name) { return $Object[$Key] } } } if ($Object.PSObject -and $Object.PSObject.Properties) { foreach ($Property in $Object.PSObject.Properties) { if ([string]$Property.Name -ieq $Name) { return $Property.Value } } } return $null } function Convert-ToStringArray { param([AllowNull()]$Value) if ($null -eq $Value) { return @() } if ($Value -is [string]) { return @($Value) } if ($Value -is [System.Collections.IEnumerable]) { return @($Value | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) } return @([string]$Value) } function Invoke-GraphPaged { param( [Parameter(Mandatory = $true)][string]$Uri, [hashtable]$Headers ) $Items = [System.Collections.Generic.List[object]]::new() $Next = $Uri while (-not [string]::IsNullOrWhiteSpace($Next)) { if ($Headers) { $Response = Invoke-MgGraphRequest -Method GET -Uri $Next -Headers $Headers -ErrorAction Stop } else { $Response = Invoke-MgGraphRequest -Method GET -Uri $Next -ErrorAction Stop } foreach ($Item in @(Get-PropValue -Object $Response -Name "value")) { $Items.Add($Item) } $Next = [string](Get-PropValue -Object $Response -Name "@odata.nextLink") } return @($Items) } function Load-PermissionLookup { param([string]$Path) $Lookup = @{} if (-not (Test-Path $Path)) { return $Lookup } foreach ($Line in (Get-Content -Path $Path -ErrorAction SilentlyContinue)) { if ($Line -match '"(?<id>[0-9a-fA-F-]{36})"\s*=\s*"(?<name>[^"]+)"') { $Lookup[[string]$Matches.id] = [string]$Matches.name } } return $Lookup } function Resolve-OwnerLabel { param( [AllowNull()]$Owners ) if ($null -eq $Owners) { return "N/A" } $Names = [System.Collections.Generic.List[string]]::new() foreach ($Owner in @($Owners)) { $Candidate = [string](Get-PropValue -Object $Owner -Name "userPrincipalName") if ([string]::IsNullOrWhiteSpace($Candidate)) { $Candidate = [string](Get-PropValue -Object $Owner -Name "displayName") } if ([string]::IsNullOrWhiteSpace($Candidate)) { $Candidate = [string](Get-PropValue -Object $Owner -Name "id") } if (-not [string]::IsNullOrWhiteSpace($Candidate)) { $Names.Add($Candidate) } } if ($Names.Count -eq 0) { return "N/A" } return (@($Names | Select-Object -Unique) -join "/") } function Is-CriticalPermission { param( [string]$PermissionName, [string]$PermissionType ) if ([string]::IsNullOrWhiteSpace($PermissionName)) { return $false } $ExactCritical = @( "RoleManagement.ReadWrite.Directory", "Directory.ReadWrite.All", "Directory.AccessAsUser.All", "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "DelegatedPermissionGrant.ReadWrite.All", "Policy.ReadWrite.ConditionalAccess", "PrivilegedAccess.ReadWrite.AzureAD", "PrivilegedAccess.ReadWrite.AzureADGroup", "UserAuthenticationMethod.ReadWrite.All", "Group.ReadWrite.All", "User.ReadWrite.All", "Organization.ReadWrite.All", "Domain.ReadWrite.All" ) if ($ExactCritical -contains $PermissionName) { return $true } # High-privilege wildcards that should always be reviewed when app-level consent is granted. $PatternCritical = @( '^.*\.ReadWrite\.All$', '^.*\.FullControl\.All$', '^.*\.Manage\.All$' ) if ($PermissionType -eq "Application") { foreach ($Pattern in $PatternCritical) { if ($PermissionName -match $Pattern) { return $true } } } return $false } try { $RunStamp = Get-Date -Format "yyyyMMdd_HHmmss" $ProjectRoot = Split-Path -Path $PSScriptRoot -Parent $ManualReviewDir = Join-Path $ProjectRoot "manual_review" if (-not (Test-Path $ManualReviewDir)) { New-Item -Path $ManualReviewDir -ItemType Directory -Force | Out-Null } $CsvExportPath = Join-Path $ManualReviewDir "app_roles_analyzer_$RunStamp.csv" $CriticalCsvExportPath = Join-Path $ManualReviewDir "app_roles_critical_$RunStamp.csv" $PermissionMapPath = Join-Path $ProjectRoot "app-id.txt" $PermissionLookup = Load-PermissionLookup -Path $PermissionMapPath $ServicePrincipals = @(Get-MgServicePrincipal -All -Property "id,appId,displayName,appRoles,oauth2PermissionScopes" -ErrorAction Stop) $RegisteredApps = @(Get-MgApplication -All -Property "id,appId,displayName,createdDateTime,requiredResourceAccess,accountEnabled" -ErrorAction Stop) $ServicePrincipalById = @{} $ServicePrincipalByAppId = @{} foreach ($Sp in $ServicePrincipals) { if ($Sp.Id) { $ServicePrincipalById[[string]$Sp.Id] = $Sp } if ($Sp.AppId) { $ServicePrincipalByAppId[[string]$Sp.AppId] = $Sp } } $Results = [System.Collections.Generic.List[object]]::new() $OwnerCache = @{} foreach ($Sp in $ServicePrincipals) { $SpId = [string]$Sp.Id $SpDisplayName = [string]$Sp.DisplayName if ([string]::IsNullOrWhiteSpace($SpId)) { continue } if (-not $OwnerCache.ContainsKey($SpId)) { try { $Owners = Invoke-GraphPaged -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SpId/owners?`$select=id,displayName,userPrincipalName" $OwnerCache[$SpId] = Resolve-OwnerLabel -Owners $Owners } catch { $OwnerCache[$SpId] = "N/A" } } $OwnerLabel = [string]$OwnerCache[$SpId] try { $DelegatedGrants = @(Invoke-GraphPaged -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SpId/oauth2PermissionGrants?`$top=999") } catch { $DelegatedGrants = @() } foreach ($Grant in $DelegatedGrants) { $RawScope = [string](Get-PropValue -Object $Grant -Name "scope") $Scopes = if ([string]::IsNullOrWhiteSpace($RawScope)) { @() } else { @($RawScope -split '\s+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) } $ResourceId = [string](Get-PropValue -Object $Grant -Name "resourceId") $ResourceSp = if ($ResourceId -and $ServicePrincipalById.ContainsKey($ResourceId)) { $ServicePrincipalById[$ResourceId] } else { $null } $ResourceName = if ($ResourceSp) { [string]$ResourceSp.DisplayName } else { "Unknown" } foreach ($Scope in $Scopes) { if ([string]::IsNullOrWhiteSpace($Scope)) { continue } $IsCritical = Is-CriticalPermission -PermissionName $Scope -PermissionType "Delegated" $Risk = if ($IsCritical) { "Critical" } else { "Low" } $Results.Add([pscustomobject]@{ AppName = $SpDisplayName AppObjectId = $SpId AppClientId = [string]$Sp.AppId AssignmentSource = "EnterpriseApplicationConsent" PermissionType = "Delegated" ResourceAppName = $ResourceName ResourceAppId = if ($ResourceSp) { [string]$ResourceSp.AppId } else { $null } PermissionId = $null PermissionName = $Scope IsCritical = $IsCritical Risk = $Risk Owners = $OwnerLabel CreatedDateTime = $null Disabled = $null Reason = if ($IsCritical) { "High-privilege delegated permission detected." } else { $null } }) } } try { $AppRoleAssignments = @(Invoke-GraphPaged -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$SpId/appRoleAssignments?`$top=999") } catch { $AppRoleAssignments = @() } foreach ($Assignment in $AppRoleAssignments) { $ResourceId = [string](Get-PropValue -Object $Assignment -Name "resourceId") $ResourceDisplayName = [string](Get-PropValue -Object $Assignment -Name "resourceDisplayName") $ResourceSp = if ($ResourceId -and $ServicePrincipalById.ContainsKey($ResourceId)) { $ServicePrincipalById[$ResourceId] } else { $null } if ([string]::IsNullOrWhiteSpace($ResourceDisplayName) -and $ResourceSp) { $ResourceDisplayName = [string]$ResourceSp.DisplayName } $PermissionId = [string](Get-PropValue -Object $Assignment -Name "appRoleId") $PermissionName = $null if ($PermissionId -and $PermissionLookup.ContainsKey($PermissionId)) { $PermissionName = [string]$PermissionLookup[$PermissionId] } if ([string]::IsNullOrWhiteSpace($PermissionName) -and $ResourceSp -and $ResourceSp.AppRoles) { $PermissionName = [string]( $ResourceSp.AppRoles | Where-Object { ([string]$_.Id) -eq $PermissionId } | Select-Object -First 1 -ExpandProperty Value ) } if ([string]::IsNullOrWhiteSpace($PermissionName)) { $PermissionName = "UnknownAppRole($PermissionId)" } $IsCritical = Is-CriticalPermission -PermissionName $PermissionName -PermissionType "Application" $Risk = if ($IsCritical) { "Critical" } else { "Low" } $Results.Add([pscustomobject]@{ AppName = $SpDisplayName AppObjectId = $SpId AppClientId = [string]$Sp.AppId AssignmentSource = "EnterpriseApplicationConsent" PermissionType = "Application" ResourceAppName = $ResourceDisplayName ResourceAppId = if ($ResourceSp) { [string]$ResourceSp.AppId } else { $null } PermissionId = $PermissionId PermissionName = $PermissionName IsCritical = $IsCritical Risk = $Risk Owners = $OwnerLabel CreatedDateTime = $null Disabled = $null Reason = if ($IsCritical) { "Critical application permission assigned to enterprise application." } else { $null } }) } } foreach ($App in $RegisteredApps) { $AppId = [string]$App.Id $OwnerLabel = "N/A" try { $Owners = Invoke-GraphPaged -Uri "https://graph.microsoft.com/v1.0/applications/$AppId/owners?`$select=id,displayName,userPrincipalName" $OwnerLabel = Resolve-OwnerLabel -Owners $Owners } catch { $OwnerLabel = "N/A" } foreach ($ResourceAccess in @($App.RequiredResourceAccess)) { $ResourceAppId = [string](Get-PropValue -Object $ResourceAccess -Name "resourceAppId") $ResourceSp = if ($ResourceAppId -and $ServicePrincipalByAppId.ContainsKey($ResourceAppId)) { $ServicePrincipalByAppId[$ResourceAppId] } else { $null } $ResourceAppName = if ($ResourceSp) { [string]$ResourceSp.DisplayName } else { "Unknown" } foreach ($Access in @(Get-PropValue -Object $ResourceAccess -Name "resourceAccess")) { $PermissionId = [string](Get-PropValue -Object $Access -Name "id") $AccessType = [string](Get-PropValue -Object $Access -Name "type") $PermissionType = if ($AccessType -eq "Role") { "Application" } else { "Delegated" } $PermissionName = $null if ($PermissionId -and $PermissionLookup.ContainsKey($PermissionId)) { $PermissionName = [string]$PermissionLookup[$PermissionId] } if ([string]::IsNullOrWhiteSpace($PermissionName) -and $ResourceSp) { if ($PermissionType -eq "Application" -and $ResourceSp.AppRoles) { $PermissionName = [string]( $ResourceSp.AppRoles | Where-Object { ([string]$_.Id) -eq $PermissionId } | Select-Object -First 1 -ExpandProperty Value ) } elseif ($PermissionType -eq "Delegated" -and $ResourceSp.Oauth2PermissionScopes) { $PermissionName = [string]( $ResourceSp.Oauth2PermissionScopes | Where-Object { ([string]$_.Id) -eq $PermissionId } | Select-Object -First 1 -ExpandProperty Value ) } } if ([string]::IsNullOrWhiteSpace($PermissionName)) { $PermissionName = "UnknownPermission($PermissionId)" } $IsCritical = Is-CriticalPermission -PermissionName $PermissionName -PermissionType $PermissionType $Risk = if ($IsCritical) { "Critical" } else { "Low" } $Results.Add([pscustomobject]@{ AppName = [string]$App.DisplayName AppObjectId = [string]$App.Id AppClientId = [string]$App.AppId AssignmentSource = "AppRegistrationRequested" PermissionType = $PermissionType ResourceAppName = $ResourceAppName ResourceAppId = $ResourceAppId PermissionId = $PermissionId PermissionName = $PermissionName IsCritical = $IsCritical Risk = $Risk Owners = $OwnerLabel CreatedDateTime = [string]$App.CreatedDateTime Disabled = $null Reason = if ($IsCritical -and $PermissionType -eq "Application") { "Critical application permission requested by app registration." } elseif ($IsCritical) { "High-privilege delegated permission requested by app registration." } else { $null } }) } } } $AllRows = @($Results) $CriticalRows = @($AllRows | Where-Object { [bool]$_.IsCritical -eq $true -and [string]$_.PermissionType -eq "Application" }) $AllRows | Sort-Object AppName, PermissionType, PermissionName | Export-Csv -Path $CsvExportPath -NoTypeInformation -Encoding UTF8 $CriticalRows | Sort-Object AppName, PermissionName | Export-Csv -Path $CriticalCsvExportPath -NoTypeInformation -Encoding UTF8 $Pass = ($CriticalRows.Count -eq 0) $ErrorMessage = if ($Pass) { $null } else { "Critical application permissions were identified for one or more applications." } [pscustomobject]@{ CheckId = $CheckId Title = $Title Level = $Level BenchmarkType = $BenchmarkType Status = if ($Pass) { "PASS" } else { "FAIL" } Pass = $Pass Evidence = [pscustomobject]@{ PermissionLookupSource = if (Test-Path $PermissionMapPath) { $PermissionMapPath } else { "NotFound" } TotalServicePrincipals = @($ServicePrincipals).Count TotalRegisteredApplications = @($RegisteredApps).Count TotalPermissionsDiscovered = @($AllRows).Count CriticalApplicationPermissionCount = @($CriticalRows).Count CriticalUniqueApplications = @($CriticalRows | Select-Object -ExpandProperty AppObjectId -Unique).Count CsvExportPath = $CsvExportPath CriticalCsvExportPath = $CriticalCsvExportPath PreviewCritical = @($CriticalRows | Select-Object -First 50) SourceDocument = "Supplemental application permissions and app role assignment analysis" } Error = $ErrorMessage Timestamp = Get-Date } } catch { [pscustomobject]@{ CheckId = $CheckId Title = $Title Level = $Level BenchmarkType = $BenchmarkType Status = "ERROR" Pass = $null Evidence = [pscustomobject]@{ SourceDocument = "Supplemental application permissions and app role assignment analysis" } Error = $_.Exception.Message Timestamp = Get-Date } } |