miércoles, 27 de mayo de 2026

WINDOWS. GPO. Usuario bloqueado email

Propósito:

Por diferentes motivos las cuentas de nuestros usuarios del AD se bloquean, ya sea de manera automática, por culpa del propio usuario o cualquier otra circunstancia.

Esto al final acaba en tiempo de gestión para los administradores y poca visibilidad e información para los usuarios.

El script que voy a pegar podría hacerse de muchas maneras, en las que aportarían más información, pero he querido que sea sencillo y que no necesite mirar el visor de eventos (que tendría que ir a cada uno de nuestros DC si no están centralizados) y sin crear ningún fichero en el disco.

Al final acaba enviando un email al usuario e informándole que ha sido bloqueado. Te puedes preguntar ¿cómo lo va a leer si tiene el usuario del AD bloqueado?.. buena pregunta. En mi caso las cuentas de correo no están gestionadas por el AD, en otras cuentas de proveedor pongo la externa (no tienen cuenta de correo interna), hay muchos casos.

Esta claro que para que funcione la cuenta del usuario del AD debe tener documentado el email. Otra opción es que siempre que se bloquee, se envíe email solo a IT o con copia a IT.

La idea es que solo envíe una alerta por hora, para no ser muy pesado. Con lo que es importante configurar bien el programador de tareas.

Esto va muy bien sobre todo con proveedores, se dejan sesiones abiertas en los servidores durante mucho tiempo y cuando van a conectar porque les necesitamos no pueden. Con esta medida les llega un email para que cierren las sesiones y eviten este tipo de bloqueo.

En mi caso a los proveedores les caduca la contraseña automáticamente cada 24h, así que si no cierran sesión de un día para otro, esto les supone un bloqueo ya que tienen la sesión abierta con una contraseña caducada.

El resultado sería el siguiente email:


Pasos

El Script :

# ============================================================

# SCRIPT: AD-Lockout-UserNotify.ps1

# ============================================================

# FUNCION:

# Detecta usuarios bloqueados en Active Directory usando

# el atributo lockoutTime, consultando directamente contra

# el PDC Emulator para garantizar consistencia.

#

# ============================================================


Import-Module ActiveDirectory


# ==========================

# VARIABLES DEL USUARIO

# ==========================

$SMTPServer = "smtp.MiDominio.com"

$SMTPPort   = 25

$From       = "Servodorenvio@MiDominio.com"


# ==========================

# OBTENCION DEL PDC

# ==========================

$domain = Get-ADDomain

$PDC = $domain.PDCEmulator


# ==========================

# CONFIGURACION

# ==========================

$now = Get-Date


$startWindow = $now.AddMinutes(-65)

$endWindow   = $now.AddMinutes(-2)


$lockoutDuration = 60


$startFileTime = $startWindow.ToFileTime()

$endFileTime   = $endWindow.ToFileTime()


# ==========================

# BUSQUEDA

# ==========================

$lockedUsers = Get-ADUser -Server $PDC `

-Filter {lockoutTime -gt 0} `

-Properties lockoutTime,mail,displayName,lastLogonDate,lastBadPasswordAttempt,passwordLastSet


foreach ($user in $lockedUsers) {


    if ($user.lockoutTime -gt 0 -and $user.mail) {


        $lockoutDate = [DateTime]::FromFileTime($user.lockoutTime)


        if ($user.lockoutTime -ge $startFileTime -and $user.lockoutTime -le $endFileTime) {


            if ($lockoutDate.AddMinutes($lockoutDuration) -gt $now) {


                # ==========================

                # CONVERSIONES SEGURAS

                # ==========================

                $LastLogon = if ($user.LastLogonDate) { $user.LastLogonDate } else { "No disponible" }

                $LastBadPwd = if ($user.LastBadPasswordAttempt) { $user.LastBadPasswordAttempt } else { "No disponible" }

                $PwdLastSet = if ($user.PasswordLastSet) { $user.PasswordLastSet } else { "No disponible" }


                $remaining = [math]::Round(($lockoutDate.AddMinutes($lockoutDuration) - $now).TotalMinutes)

                if ($remaining -lt 0) { $remaining = 0 }


                # ==========================

                # ASUNTO CORPORATIVO

                # ==========================

                $fecha = Get-Date -Format "yyyyMMdd - HH:mm"

                $Subject = "$fecha. SE. SEGURIDAD. Cuenta bloqueada: $($user.SamAccountName)"

$mail = $($user.mail)


                # ==========================

                # EMAIL HTML

                # ==========================

                $html = @"

<html>

<body style='font-family:Calibri;font-size:14px'>


<p>Hola $($user.displayName),</p>


<p><b>Su cuenta ha sido bloqueada por un proceso automatico.</b></p>


<p>Motivos habituales:</p>


<ul>

<li>Cambio de password reciente</li>

<li>Password caducado</li>

<li>Sesion activa con credenciales antiguas (muy probable)</li>

<li>Servicios o aplicaciones con password antiguo</li>

</ul>


<p>Revise los equipos donde haya iniciado sesion y vuelva a autenticarse.</p>


<p><b>Datos del bloqueo:</b></p>


<ul>

<li>Usuario: $($user.SamAccountName)</li>

<li>Fecha bloqueo: $lockoutDate</li>

<li>Tiempo restante bloqueo: $remaining minutos</li>

</ul>


<p><b>Informacion adicional:</b></p>


<ul>

<li>Ultimo logon: $LastLogon</li>

<li>Ultimo intento fallido: $LastBadPwd</li>

<li>Password cambiado: $PwdLastSet</li>

</ul>


<p>Contacte con IT de soporte@MiDominio.com si persiste.</p>


</body>

</html>

"@


                Send-MailMessage `

                -SmtpServer $SMTPServer `

                -Port $SMTPPort `

                -From $From `

                -To $mail `

                -Subject $Subject `

                -Body $html `

                -BodyAsHtml


            }

        }

    }

}


 


La tarea programada :




Probar con un usuario:


Para probar y ver pon pantalla y si hay algín caso

Import-Module ActiveDirectory

$pdc = (Get-ADDomain).PDCEmulator

$lockedUsers = Search-ADAccount -LockedOut -Server $pdc

if ($lockedUsers) {

    Write-Host "Hay usuarios bloqueados"

    $lockedUsers | ForEach-Object {
        Write-Host $_.SamAccountName
    }

} else {

    Write-Host "No hay usuarios bloqueados"

}


by GoN | Published: May 2026 | Last Updated:

martes, 26 de mayo de 2026

WINDOWS. GPO. Documento GPO-OU

Propósito:

Tengo que hacer limpieza de un montón de OUs, ya que he movido mis usuarios/Pcs a una estructura nueva. No me he llevado las mismas OUs, ni las mismas GPOs para no arrastrar la "suciedad" y partir de una estructura entendible y bien organizada.

El problema podría venir si me he dejado algo, con lo que antes de limpiar la estructura antigua he preferido hacer una exportación donde relaciono las OU con las GPO, por si necesito recrearlo.

Pasos

El resultado vendría ser un fichero html con el siguiente formato


La ejecución podría tardar un par de minutos, depende lo grande que sea la estructura de OUs y el número de GPOs.

El código es el siguiente:

Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop

$fecha = Get-Date -Format "yyyyMMdd_HHmm"
$output = "AD_Informe_Completo_$fecha.html"

$html = @()

$html += @"
<html>
<head>
<title>Auditoria AD</title>
<style>
body { font-family: Arial; font-size: 12px; }
h1 { color:#2E86C1; }
h2 { color:#1F618D; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
th, td { border:1px solid #ccc; padding:6px; text-align:left; }
th { background-color:#f2f2f2; }

.directa { background-color:#D4EFDF; }
.heredada { background-color:#FDEBD0; }

.ok { color:green; font-weight:bold; }
.warn { color:red; font-weight:bold; }

.small { font-size: 11px; color: #555; }
</style>
</head>
<body>
"@

$html += "<h1>Auditoria OU y GPO</h1>"
$html += "<p>Fecha: $(Get-Date)</p>"

# Obtener todas las OUs
$ous = Get-ADOrganizationalUnit -Filter * | Sort-Object DistinguishedName

# Cache de links directos por OU
$linksPorOU = @{}

foreach ($ou in $ous) {
    try {
        $inh = Get-GPInheritance -Target $ou.DistinguishedName
        $linksPorOU[$ou.DistinguishedName] = $inh.GpoLinks
    } catch {
        $linksPorOU[$ou.DistinguishedName] = @()
    }
}

# Funcion para obtener padres de una OU
function Get-OUChain {
    param ($dn)

    $chain = @()
    $actual = $dn

    while ($actual -match "OU=") {
        $chain += $actual
        $actual = ($actual -split ',',2)[1]
    }

    # añadir dominio (final)
    if ($actual) { $chain += $actual }

    return $chain
}

foreach ($ou in $ous) {

    try {

        $html += "<h2>OU: $($ou.Name)</h2>"
        $html += "<p><b>DN:</b> $($ou.DistinguishedName)</p>"

        $inheritance = Get-GPInheritance -Target $ou.DistinguishedName

        if ($inheritance.GpoInheritanceBlocked) {
            $html += "<p class='warn'>Herencia bloqueada</p>"
        } else {
            $html += "<p class='ok'>Herencia activa</p>"
        }

        $chain = Get-OUChain $ou.DistinguishedName

        $html += "<table>"
        $html += "<tr>
                    <th>GPO</th>
                    <th>Enforced</th>
                    <th>Link Enabled</th>
                    <th>Origen</th>
                  </tr>"

        foreach ($gpo in $inheritance.InheritedGpoLinks) {

            $origen = "No localizado"
            $class = "heredada"

            # Buscar donde esta linkada realmente
            foreach ($nivel in $chain) {

                if ($linksPorOU.ContainsKey($nivel)) {

                    foreach ($link in $linksPorOU[$nivel]) {

                        if ($link.DisplayName -eq $gpo.DisplayName) {

                            if ($nivel -eq $ou.DistinguishedName) {
                                $origen = "Directa"
                                $class = "directa"
                            } else {
                                $origen = "Heredada desde: $nivel"
                                $class = "heredada"
                            }

                            break
                        }
                    }
                }

                if ($origen -ne "No localizado") { break }
            }

            $html += "<tr class='$class'>
                        <td>$($gpo.DisplayName)</td>
                        <td>$($gpo.Enforced)</td>
                        <td>$($gpo.Enabled)</td>
                        <td class='small'>$origen</td>
                      </tr>"
        }

        $html += "</table>"
        $html += "<hr>"

    }
    catch {
        $html += "<p class='warn'>Error en OU: $($ou.DistinguishedName)</p>"
    }
}

$html += "</body></html>"

$html | Out-File $output -Encoding UTF8

Write-Host "Informe generado: $output" -ForegroundColor Green


by GoN | Published: May 2026 | Last Updated:

jueves, 21 de mayo de 2026

WINDOWS. GPO. Asociar extensiones

Propósito:

Crear una GPO que asocie ciertas extensiones para que se abran con Notepad. Lo hago por motivos de seguridad: las extensiones son SCRIPTS/ejecutables y quiero impedir que puedan realizar acciones de forma sencilla.

Pasos

Antes de aplicar la GPO, me invento una extensión para ver como reacciona mi PC



Una vez ejecutada el resultado es evidente


La GPO puedo poner tantas asociaciones como quiera, aquí en el ejemplo se pueden ver un par:




by GoN | Published: May 2026 | Last Updated: