<# .SYNOPSIS ¿Qué enterprise apps tienen permisos de aplicación de alto privilegio? .DESCRIPTION Revisa los permisos de aplicación (app roles) otorgados a cada entidad de servicio y marca los que están en la lista de alto privilegio. Estos permisos funcionan sin usuario, así que una app comprometida con ellos puede leer correo o escalar privilegios en todo el tenant. Solo lectura: no cambia nada en el tenant. .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .PARAMETER PermisosRiesgosos Nombres de los permisos que quieres marcar. La lista por defecto es una selección editorial, no una lista oficial de Microsoft. .PARAMETER ZonaHoraria Zona horaria para mostrar las fechas. Por defecto, Central Standard Time (Mexico). .EXAMPLE .\get-apps-permisos-riesgosos.ps1 -RutaCsv .\apps-riesgo.csv .NOTES Permisos: Application.Read.All Rol mínimo: Lectores de directorio (Directory Readers) Licencia: Microsoft Entra ID Free Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas. #> [CmdletBinding()] param( [string]$RutaCsv, [string[]]$PermisosRiesgosos = @( 'RoleManagement.ReadWrite.Directory', 'AppRoleAssignment.ReadWrite.All', 'Application.ReadWrite.All', 'Directory.ReadWrite.All', 'User.ReadWrite.All', 'User-PasswordProfile.ReadWrite.All', 'UserAuthenticationMethod.ReadWrite.All', 'Group.ReadWrite.All', 'GroupMember.ReadWrite.All', 'Domain.ReadWrite.All', 'Policy.ReadWrite.ConditionalAccess', 'Mail.ReadWrite', 'Mail.Send', 'MailboxSettings.ReadWrite', 'Files.ReadWrite.All', 'Sites.ReadWrite.All', 'Sites.FullControl.All', 'full_access_as_app', 'Exchange.ManageAsApp' ), [string]$ZonaHoraria = 'Central Standard Time (Mexico)' ) $ErrorActionPreference = 'Stop' $permisos = @('Application.Read.All') if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) { throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser' } $zona = [TimeZoneInfo]::FindSystemTimeZoneById($ZonaHoraria) Connect-MgGraph -Scopes $permisos -NoWelcome function Get-TodasLasPaginas { param([string]$Uri) while ($Uri) { $pagina = Invoke-MgGraphRequest -Method GET -Uri $Uri $pagina.value $Uri = $pagina.'@odata.nextLink' } } function Format-FechaLocal { param($Valor) if (-not $Valor) { return $null } $utc = if ($Valor -is [datetimeoffset]) { $Valor.UtcDateTime } elseif ($Valor -is [datetime]) { if ($Valor.Kind -eq 'Unspecified') { [datetime]::SpecifyKind($Valor, 'Utc') } else { $Valor.ToUniversalTime() } } else { [datetimeoffset]::Parse("$Valor", [cultureinfo]::InvariantCulture).UtcDateTime } [TimeZoneInfo]::ConvertTimeFromUtc($utc, $zona).ToString('yyyy-MM-dd HH:mm', [cultureinfo]::InvariantCulture) } $base = 'https://graph.microsoft.com/v1.0' $miTenant = (Get-MgContext).TenantId # Una asignación solo trae el GUID del permiso; el nombre sale de los appRoles de la API de destino. $rolesPorRecurso = @{} # En esta API la página máxima es de 100. $filas = foreach ($sp in Get-TodasLasPaginas "$base/servicePrincipals?`$select=id,appId,displayName,servicePrincipalType,accountEnabled,appOwnerOrganizationId&`$top=100") { foreach ($a in Get-TodasLasPaginas "$base/servicePrincipals/$($sp.id)/appRoleAssignments") { if (-not $rolesPorRecurso.ContainsKey($a.resourceId)) { $mapa = @{} foreach ($r in (Invoke-MgGraphRequest -Method GET -Uri "$base/servicePrincipals/$($a.resourceId)?`$select=appRoles").appRoles) { $mapa[$r.id] = $r.value } $rolesPorRecurso[$a.resourceId] = $mapa } $nombre = $rolesPorRecurso[$a.resourceId][$a.appRoleId] if ($nombre -notin $PermisosRiesgosos) { continue } [pscustomobject]@{ Aplicacion = $sp.displayName AppId = $sp.appId TipoEntidad = $sp.servicePrincipalType Origen = if (-not $sp.appOwnerOrganizationId) { 'Sin tenant propietario' } elseif ($sp.appOwnerOrganizationId -eq $miTenant) { 'Este tenant' } else { 'Otro tenant (proveedor o Microsoft)' } Habilitada = if ($sp.accountEnabled) { 'Sí' } else { 'No' } Permiso = $nombre ApiDestino = $a.resourceDisplayName OtorgadoEl = Format-FechaLocal $a.createdDateTime } } } $filas = $filas | Sort-Object Aplicacion, Permiso Write-Host "Permisos de alto privilegio encontrados: $(@($filas).Count), en $(@($filas | Select-Object -ExpandProperty AppId -Unique).Count) aplicaciones" if ($RutaCsv) { # Los nombres de app los elige quien la publica: Excel los ejecutaría como fórmula si empiezan con estos caracteres. foreach ($fila in $filas) { foreach ($p in $fila.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@]') { $p.Value = "'" + $p.Value } } } # UTF-8 con BOM para que Excel en español muestre bien los acentos. $filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM Write-Host "CSV guardado en $RutaCsv" } else { $filas | Format-Table -AutoSize }