Enterprise apps con permisos de alto privilegio
Busca las aplicaciones con permisos de aplicación peligrosos, como escribir en todo el directorio o leer todos los buzones. Estos permisos funcionan sin usuario, así que conviene revisarlos uno por uno.
Ejemplo en México
Clínica en Mérida, 120 personas
Un proveedor de expediente electrónico pidió consentimiento de administrador hace años y nadie recuerda qué permisos le dieron, y ahora hay revisión de seguridad.
.\get-apps-permisos-riesgosos.ps1 -RutaCsv .\apps-riesgo.csvDescubre que el conector del proveedor tiene Mail.ReadWrite y Directory.ReadWrite.All. Pide al proveedor justificarlos y retira el permiso de directorio.
Cómo implementarlo
- Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
- Corre el script con -RutaCsv e inicia sesión con una cuenta con el rol Lectores de directorio.
- Revisa primero las filas con Origen distinto de Este tenant y con permisos de escritura sobre roles o aplicaciones.
- Pregunta para qué necesita cada app ese permiso, y retira el que no se justifique desde el centro de administración de Microsoft Entra.
Ver el código completo de get-apps-permisos-riesgosos.ps1
<#
.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
}