Saltar al contenido
Microsoft scripts

Scripts de Entra

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.

Antes de correrloLa lista de permisos riesgosos es una selección editorial, no una lista oficial de Microsoft; ajústala con -PermisosRiesgosos. El script hace una consulta por cada entidad de servicio, así que en tenants grandes tarda. No revisa permisos delegados ni roles de directorio asignados a la app.

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.csv

Descubre 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

  1. Abre PowerShell 7 con el módulo Microsoft.Graph.Authentication instalado.
  2. Corre el script con -RutaCsv e inicia sesión con una cuenta con el rol Lectores de directorio.
  3. Revisa primero las filas con Origen distinto de Este tenant y con permisos de escritura sobre roles o aplicaciones.
  4. 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
}

Relacionados