Búsqueda de sitios web

Creación de un observador de Active Directory con CIM y PowerShell


La semana pasada, Adam Bertram envió un tweet buscando algún código de PowerShell para notificar al usuario cuando se había creado una nueva cuenta de usuario de Active Directory. Desenterré un código muy, muy antiguo que usaba una suscripción a eventos WMI para observar Active Directory en busca de tal evento. El código que compartí fue algo que armé en los días de PowerShell v2, pero todavía funcionaba hoy. Sin embargo, utilizó Register-WMIEvent, que en el mundo actual de PowerShell considero obsoleto. Deberíamos utilizar los cmdlets equivalentes de CIM más nuevos. Decidí tomarme un poco de tiempo, pulir el código y llevarlo a (mis) estándares actuales.

Suscripciones a eventos WMI/CIM

En el mundo de Windows, cuando sucede algo como el inicio de un servicio o el final de un proceso, obtenemos un evento. En Active Directory, la creación de una nueva cuenta de usuario también desencadena un evento. En muchos casos, puede observar estos eventos mediante WMI/CIM. Esto se logra creando una suscripción a un evento temporal. Esto es como cualquier otra suscripción. Estás diciendo: "Envíame un nuevo número cuando haya algo nuevo que informar". Podría recibir un nuevo "problema" cada 2 semanas o cada 2 minutos.

Utilizará Register-CimIndicationEvent para crear la suscripción. Puede crear suscripciones para eventos en su computadora local o remota. Esta suscripción solo dura la duración de su sesión de PowerShell. Pero una advertencia: tendría cuidado al crear un sistema de gestión completo a su alrededor, excepto en entornos pequeños. Si bien es bueno saber cuándo sucede algo, como la interrupción de un servicio o la modificación de una cuenta de usuario, en entornos más grandes es mejor invertir en herramientas y software diseñados para ese tipo de monitoreo. Usaría suscripciones a eventos temporales para monitoreo o resolución de problemas a corto plazo. Teniendo esto en cuenta, considere el código de este artículo como una prueba de concepto.

Registro-CIMIndicaciónEvento

Confío en que se tomará el tiempo para leer la ayuda completa y los ejemplos de este cmdlet. Los eventos de WMI son un tema complicado y no voy a profundizar demasiado. Hay varias formas de utilizar el cmdlet. Voy a utilizar una consulta antigua.

Register-CimIndicationEvent -Query "Select * from __InstanceCreationEvent Within 10 where TargetInstance ISA 'DS_USER'" -Namespace root\directory\ldap -SourceIdentifier NewUser

Estoy ejecutando este código en un escritorio de Windows 10 miembro del dominio. La consulta dice: "busque un evento de creación cada 10 segundos donde el tipo de objeto sea una cuenta de usuario". La parte Dentro se conoce como encuesta. No querrás realizar encuestas demasiado rápido. Si realmente necesita saber en cuestión de segundos cuándo ocurre un evento, necesita encontrar una verdadera solución de software. Estoy usando 10 segundos para mi demostración.

Esto creará una suscripción al evento.

El SourceIdentifier es la forma en que puede hacer referencia al suscriptor y a los eventos posteriores. Si no especifica uno, obtendrá un GUID. Personalmente, me resulta mucho más fácil definir mis propios identificadores.

El suscriptor del evento se ejecuta en mi sesión local de PowerShell y busca cada 10 segundos un evento de creación de un nuevo usuario en el dominio. Crearé un nuevo usuario de dominio. 10 segundos después no ha pasado nada en mi sesión. Cambiaremos eso más tarde. En lugar de eso, ejecutaré Get-Event.

La información sobre el usuario queda oculta en el evento.

Volveré a este objeto en un momento.

Registrarse-ADWatcher

Con estos conceptos básicos en mente, escribí una función de PowerShell que incluye Register-CimIndicationEvent. La función me permite observar diferentes tipos de eventos para diferentes categorías de objetos de Active Directory.

Function Register-ADWatcher {
    [cmdletbinding(SupportsShouldProcess)]
    Param(
        [Parameter(Mandatory, HelpMessage = "What type of object do you want to watch?")]
        [ValidateSet("User", "Group", "Computer", "OU")]
        [string]$Category,
        [Parameter(Mandatory, HelpMessage = "What type of activity watch?")]
        [ValidateSet("Create", "Modify", "Remove")]
        [string]$Activity,
        [Parameter(HelpMessage = "How often do you want to poll? Enter a value in seconds. The minimum and default is 10.")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ $_ -ge 10 })]
        [int]$Poll = 10,
        [Parameter(HelpMessage = "Specify an action scriptblock when the event fires.")]
        [scriptblock]$Action,
        [Parameter(HelpMessage = "Specify additional data to associate with the watcher.")]
        [string]$MessageData,
        [Parameter(HelpMessage = "Specify a name for the watcher")]
        [string]$SourceIdentifier,
        [parameter(HelpMessage = "Specify the name of a computer where you want to create the watcher.")]
        [string]$Computername,
        [switch]$Passthru
    )

    Write-Verbose "Starting $($myinvocation.mycommand)"
    #remove bound parameters that don't belong to Register-CimIndicationEvent
    $drop = "Category", "Activity", "Poll", "WhatIf", "Confirm","Passthru"
    foreach ($item in $drop) {
        if ($PSBoundParameters.ContainsKey($item)) {
            [void]$PSBoundParameters.Remove($item)
        }
    }

    Switch ($Category) {
        "User" { $DS = "DS_USER" }
        "Group" { $DS = "DS_GROUP" }
        "Computer" { $DS = "DS_COMPUTER" }
        "OU" { $DS = "DS_ORGANIZATIONALUNIT" }
    }

    Switch ($Activity) {
        "Create" { $DSAction = "__InstanceCreationEvent" }
        "Modify" { $DSAction = "__InstanceModificationEvent" }
        "Remove" { $DSAction = "__InstanceDeletionEvent" }
    }

    $query = "Select * from $DSAction Within $Poll where TargetInstance ISA '$DS'"
    $PSBoundParameters.Add("Query", $query)
    $PSBoundParameters.Add("Namespace", "root\directory\ldap")

    Write-Verbose "Creating an AD Watcher with these parameters"
    $PSBoundParameters | Out-String | Write-Verbose

    $target = "$Activity $Category within $Poll seconds"
    If ($pscmdlet.ShouldProcess($target.toUpper(), "Register AD Watcher")) {
        [void](Register-CimIndicationEvent @PSBoundParameters)
        if ($Passthru) {
            Start-sleep -Seconds 2
            Get-EventSubscriber | Sort-Object -property SubscriberID | Select-Object -last 1
        }
    }
    Write-Verbose "Ending $($myinvocation.mycommand)"
}

Usaré esta función para buscar nuevos grupos.

Register-ADWatcher -Category Group -Activity Create -MessageData "A new group has been created" -SourceIdentifier "NewGroup"

Eventualmente reemplazaré la suscripción del nuevo usuario, por lo que eliminaré la actual.

Get-EventSubscriber newuser | Unregister-Event

Ahora a crear un nuevo grupo y ver el evento.

Funciones auxiliares

Sé que hay información sobre el grupo allí, pero soy demasiado vago para analizar el objeto. En lugar de eso, escribiré una función auxiliar.

Function Get-TargetInstance {
    [cmdletbinding()]
    Param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]$EventResult,
        [Parameter(HelpMessage = "Only show defined properties")]
        [switch]$ShowDefined
    )

    Process {
        if ($ShowDefined) {
            $t = $EventResult.SourceEventArgs.NewEvent.TargetInstance
            $t.psobject.Properties.where({ $_.name -match "^DS|ADSIPath" -AND $_.value }) |
            ForEach-Object -Begin {
                #create a temporary ordered hashtable
                $h = [ordered]@{
                    TimeGenerated = $EventResult.timeGenerated
                    EventClass    = $EventResult.SourceEventArgs.NewEvent.CimSystemProperties.Classname
                }
            } -Process {
                #add each property that has a value to the hashtable
                $h.add($_.name, $_.value) } -End {
                #Create a custom object from the hashtable
                New-Object -TypeName PSObject -Property $h
            }
        }
        else {
            $EventResult.sourceEventArgs.NewEvent.TargetInstance
        }
    }
}

Como sé que el objeto tendrá muchos valores de propiedad indefinidos o nulos, agregué un parámetro para mostrar solo las propiedades pobladas.

Comportamiento

Al mirar esto, puedo ver que mencioné el nombre del grupo. Perfecto. Creemos un observador de cambios en los objetos del grupo. También puedo mostrarte cómo agregar una acción. Este es un bloque de script que se ejecuta cuando se activa el evento. Voy a utilizar el módulo BurntToast, que puedes instalar desde la Galería de PowerShell, para mostrarme una notificación.

Register-ADWatcher -Category Group -Activity Modify -SourceIdentifier "GroupChange" -Action { New-BurntToastNotification -Text "A group has changed"}

Pero existe una compensación cuando se utiliza una acción.

No hay ningún objeto de evento. Bueno, debería decir que no hay ningún objeto escrito en mi consola. Hay un objeto al que puedo hacer referencia en mi bloque de script de acción. Pero primero déjame mostrarte qué es eso.

Aquí hay un observador de objetos de usuario modificados.

Register-ADWatcher -Category User -Activity Modify -SourceIdentifier UserChange -Passthru

Cuando se activa un evento de cambio, obtienes un objeto "antes" y "después". Naturalmente le gustaría saber qué cambió. Intenté usar Compare-Object, que parecía la herramienta adecuada. Pero a menos que especifique una propiedad, no me da ningún resultado. Entonces escribí mi propia función de comparación.

Function Compare-DSObject {
    #get-event -sourceIdentifier changeduser | Compare-DSObject

    [cmdletbinding()]
    Param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]$EventResult
    )

    Process {
        #write-host "Processing an event" -ForegroundColor Green
        $p = $EventResult.SourceEventArgs.NewEvent.PreviousInstance
        $t = $EventResult.SourceEventArgs.NewEvent.TargetInstance
        $prop = $p.psobject.Properties.name.where( { $_ -match "^DS" })
        Write-Verbose "Processing Changes to $($t.ADSIPath)"
        foreach ($item in $prop) {
            if (Compare-Object -ReferenceObject $p -DifferenceObject $t -Property $item) {
                [pscustomobject]@{
                    PSTypeName    = "DSComparison"
                    ADSIPath      = $t.ADSIPath
                    TimeGenerated = $eventResult.TimeGenerated
                    Property      = $item
                    PreviousValue = $p.$item
                    NewValue      = $t.$item
                }
            }
        }
    } #process
}

Como puede ver, realmente depende de lo que quiera hacer o ver cuando se activa un evento.

En el bloque de script de acción, puede hacer referencia al objeto de evento con el objeto $event incorporado. Aquí hay un observador de nuevas cuentas de usuario que usarán BurntToast y mostrarán el nombre completo del nuevo objeto.

$action = { 
 $toast = @{
  Text = "$($event.MessageData) $($event.SourceEventArgs.NewEvent.TargetInstance.DS_distinguishedName)" 
  Sound = "Alarm5"
 }
 New-BurntToastNotification @toast
 }
 $watch = @{
  Category = "User"
  Activity = "Create"
  MessageData = "A domain user account has been created." 
  SourceIdentifier = "NewUser" 
  Action = $Action
 }
 Register-ADWatcher @watch

El valor de MessageData se utilizará en la notificación.

Poniendolo todo junto

Permítanme concluir este artículo reuniendo todo. Veamos un evento de eliminación. Me gustaría mantener un registro de auditoría en mi pequeño dominio. Cuando se elimina un usuario, quiero guardar la información del evento en el disco y recibir mi notificación del sistema. En mi bloque de script de acción, exportaré el evento al disco usando Export-CliXML.

$action = {
     #export event to disk
     if (-not (Test-Path C:\ADWatch)) {
         New-Item -Path C:\ -Name ADWatch -ItemType Directory
     }
     $file = "{0}_{1}.xml" -f $event.sourceIdentifier.Replace(" ",""),($event.TimeGenerated.tostring("u").replace(" ","-").replace(":",""))
     $export = Join-Path -Path C:\ADWatch -ChildPath $file
     $event | Export-Clixml -Path $export
     $ti = $event.SourceEventArgs.NewEvent.TargetInstance
     $msg = "[$($event.TimeGenerated)] $($event.messagedata) $($ti.DS_DistinguishedName) See $export for details."
     $toast = @{
         Header = New-BTHeader -Title "AD Watcher"
         Text =  $msg
         SnoozeAndDismiss = $True
         AppLogo = "C:\scripts\db.png"
         Sound = "alarm10"
     }
     New-BurntToastNotification @toast
 }
 Register-ADWatcher -Category User -Activity Remove -Action $action -SourceIdentifier "DeleteUser" -MessageData "ALERT! A domain user account has been removed." -Verbose

La instancia de destino del usuario eliminado se serializará en un archivo XML en C:\ADWatch. Estoy creando una marca de tiempo UTC como parte del nombre del archivo.

El mensaje de alerta indica el archivo que se puede devolver fácilmente a PowerShell.

Puedo usar mi función de ayuda para ver la información del usuario eliminada.

Resumen

Como ha visto, es posible recibir notificaciones cuando suceden cosas en Active Directory. Pero sólo porque puedas hacerlo no significa que debas. Hay muchas otras opciones que son mejores, especialmente cuanto más cerca de la notificación en tiempo real necesita. Yo diría que también depende de lo que necesites hacer con la información. ¿Solo necesitas saberlo o tiene que pasar algo?

Y para evitar una pregunta que seguramente recibiré, no tengo idea de si esto funcionaría con Azure AD y cómo. Alguien con un entorno de Azure AD tendrá que hacérmelo saber.

Pero para eventos locales, ya sea en Active Directory o en otro lugar, el uso de eventos CIM puede ser una herramienta útil en su caja de herramientas de PowerShell.

Artículos relacionados