Mostrando entradas con la etiqueta GPO. Mostrar todas las entradas
Mostrando entradas con la etiqueta GPO. Mostrar todas las entradas

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:

jueves, 14 de mayo de 2026

WINDOWS. GPO. Update-Install Adobe Acrobat


Propósito:

Nos ha llegado una alerta de seguridad de Acrobat, que requiere parchear urgentemente las versiones que estén expuestas.

El número de PCs de mi empresa es bastante elevado y no disponemos de un programa para distribuir software, con lo que me ha tocado preparar una GPO.

Para hacerla he completado lo siguiente:

  • Indica SI se actualizó o NO
  • Detecta versión antes y después
  • Te dice EXACTAMENTE qué ha fallado
  • Saca todo por pantalla en ejecución manual, no por GPO.
  • Log completo
  • Instalación silenciosa sin pedir credenciales )
  • Compatible con un EXE 

Pasos

La GPO arranca en el script de inicio del PC. Tarda unos minutos en hacer la actualización/instalación.


Si lo ejecutas a mano puedes seguir el proceso en pantalla:

Si dudas si esta funcionando cuando se aplica la GPO, abre el log que deja en cada máquina en el fichero C:\Windows\Temp\acrobat_fix.log

Aquí dejarémos la versión de Acrobat a instalar: \\NAS01\Adobe\AcroRdrDC2600121529_es_ES.exe

El Script:
$log = "C:\Windows\Temp\acrobat_fix.log"
$installer = "\\NAS01\Adobe\AcroRdrDC2600121529_es_ES.exe"

function Write-Log {
    param($text)
    $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $text"
    Write-Host $line
    Add-Content -Path $log -Value $line
}

Write-Log "==== INICIO SCRIPT ===="

# Obtener versiones existentes
$apps = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* ,
                         HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |
        Where-Object { $_.DisplayName -match "Adobe.*(Acrobat|Reader)" }

if ($apps) {
    Write-Log "Adobe detectado → se elimina SIEMPRE"

    foreach ($app in $apps) {
        Write-Log "Desinstalando $($app.DisplayName)"

        if ($app.PSChildName) {
            Start-Process "msiexec.exe" -ArgumentList "/x $($app.PSChildName) /qn /norestart" -Wait
        }
    }
}
else {
    Write-Log "No había Adobe instalado"
}

# Espera para asegurar limpieza
Start-Sleep 5

# Instalación limpia
if (Test-Path $installer) {

    Write-Log "Instalando nueva versión (silent)..."

    $proc = Start-Process $installer -ArgumentList "/sAll /rs /rps /msi /qn /norestart" -Wait -PassThru

    Write-Log "Código salida instalación: $($proc.ExitCode)"

    if ($proc.ExitCode -eq 0) {
        Write-Log " INSTALACIÓN COMPLETA OK"
    }
    else {
        Write-Log " ERROR EN INSTALACIÓN"
    }

} else {
    Write-Log " Instalador no encontrado"
}

Write-Log "==== FIN SCRIPT ===="


by GoN | Published: May 2026 | Last Updated:

martes, 12 de mayo de 2026

WINDOWS. GPO. Alertas Windows


 Propósito:

El objetivo de este post es avisar a los usuarios que tienen pendiente un reinicio.

Para mi una de las peores cosas que tiene Microsoft es como alertar o advertir a los usuarios de un evento, sinceramente no me gusta nada los pequeños mensajes que pasan desapercibidos en la parte izquierda de la pantalla.

Otra cosa que no da muchas opciones Microsoft, es forzar un reinicio con un numero importante de alertas a los usuarios. Sin utilizar comandos, solo lo permite el Wsus un reinicio forzoso y sus avisos no son los mejores. Con lo que he tenido que pensar en otra manera de alertar a mis usuarios, de las muchas que hay se me ha ocurrido la siguiente, solo avisando, que he querido compartir aquí.

Aunque lo parezca, es uno de los script que me ha llevado más tiempo crear, he pasado por muchas formas y versiones y con la ayuda de Copilot ha acabado como aquí voy a explicar.

Lo he preparado para que salte en el logon de inicio y en cada bloqueo y desbloqueo de la pantalla. Dejará de aparecer cuando ya no tenga ningún reinicio pendiente.

Pasos

La GPO.

Como veréis es una GPO de usuario, no de máquina, ya que es la mejor manera de que las alertas interactúen con el usuario conectado.

Tenemos que crear en la GPO una tarea programada, esta tarea la inyectaremos en los PCs.




Argumentos: -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command "& { $Source='\\Midominio.com\SYSVOL\midominio.com\scripts\WSUS\avisoreiniciopendiente.ps1'; $Target=$env:ProgramData+'\RebootAlert\avisoreiniciopendiente.ps1'; if (-not (Test-Path (Split-Path $Target))) { New-Item -ItemType Directory -Path (Split-Path $Target) -Force | Out-Null }; Copy-Item $Source $Target -Force; Start-Process powershell.exe -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File', $Target) -WindowStyle Hidden }"









Poner el script en un recurso compartido donde puedan leer todos los usuarios.

Código:

 # ==================================================
# ALERTA DE REINICIO PENDIENTE - PRODUCCION
# ==================================================
# - Muestra aviso WPF si hay reinicio pendiente
# - Muestra aviso si el equipo lleva 15 dias sin reiniciar
# - Sin control de tiempo (sale siempre que haya reboot)
# - Evita doble ejecucion mediante Mutex
# - Pensado para ejecucion via GPO / Tarea programada
# ==================================================

Add-Type -AssemblyName PresentationFramework

# --------------------------------------------------
# EVITAR DOBLE EJECUCION DEL SCRIPT
# --------------------------------------------------
$mutexName = "Global\RebootPendingAlertMutex"
$createdNew = $false
$mutex = New-Object System.Threading.Mutex($true, $mutexName, [ref]$createdNew)

if (-not $createdNew) {
    # Ya hay otra instancia ejecutandose
    exit
}

# --------------------------------------------------
# FUNCION: Detectar reinicio pendiente
# --------------------------------------------------
function Test-PendingReboot {

    if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") {
        return $true
    }

    if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending") {
        return $true
    }

    return $false
}

# --------------------------------------------------
# FUNCION: Motivo del reinicio pendiente
# --------------------------------------------------
function Get-PendingRebootReason {

    if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") {
        return "actualizaciones de Windows"
    }

    if (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending") {
        return "actualizaciones del sistema"
    }

    return "tareas de mantenimiento del sistema"
}

# --------------------------------------------------
# FUNCION: Detectar equipos con 15 dias sin reiniciar
# --------------------------------------------------
function Test-Uptime15Days {

    $LastBoot = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
    $DaysUp = ((Get-Date) - $LastBoot).Days

    if ($DaysUp -ge 15) {
        return $true
    }

    return $false
}

# --------------------------------------------------
# SI NO HAY REINICIO PENDIENTE NI 15 DIAS SIN REINICIAR
# --------------------------------------------------
if ((-not (Test-PendingReboot)) -and (-not (Test-Uptime15Days))) {
    exit
}

# --------------------------------------------------
# OBTENER MOTIVO DEL AVISO
# --------------------------------------------------
if (Test-PendingReboot) {
    $rebootReason = Get-PendingRebootReason
}
else {
    $rebootReason = "mantenimiento recomendado. El equipo lleva mas de 15 dias sin reiniciarse"
}

# --------------------------------------------------
# VENTANA WPF
# --------------------------------------------------
[xml]$xaml = @"
        Title="Mi empresa. Reinicio pendiente"
        Height="240"
        Width="540"
        WindowStartupLocation="CenterScreen"
        ResizeMode="NoResize"
        Topmost="True">

    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <Border Background="#B71C1C" Grid.Row="0" Padding="10">
            <TextBlock Text="Reinicio pendiente."
                       Foreground="White"
                       FontSize="16"
                       FontWeight="Bold"
                       HorizontalAlignment="Center"/>
        </Border>

        <Border Grid.Row="1" Padding="20">
            <TextBlock
                Text="El equipo necesita reiniciarse para completar $rebootReason.

Por favor, guarda tu trabajo y reinicia cuando te sea posible.
Departamento de IT. "
                TextWrapping="Wrap"
                TextAlignment="Center"
                VerticalAlignment="Center"
                FontSize="13"/>
        </Border>

        <Button Name="AceptarBtn"
                Grid.Row="2"
                Width="120"
                Height="35"
                Margin="10"
                HorizontalAlignment="Right"
                Background="#D32F2F"
                Foreground="White"
                FontWeight="Bold"
                Content="Aceptar"/>
    </Grid>
</Window>
"@

$reader = New-Object System.Xml.XmlNodeReader $xaml
$window = [Windows.Markup.XamlReader]::Load($reader)

$button = $window.FindName("AceptarBtn")
$button.Add_Click({ $window.Close() })

$window.ShowDialog() | Out-Null

Se me ocurre muchos puntos de mejora, pero los dejo para más adelante.

Sinceramente, se puede usar para lanzar cualquier tipo de alertas.

by GoN | Published: May 2026 | Last Updated: Jul 26