Búsqueda de sitios web

Creación de una herramienta PowerShell para cambios en Active Directory


Hace unos días, publiqué un script de PowerShell que escribí y que crea un informe HTML formateado, completo con regiones plegables, que muestra los cambios recientes en los objetos de su dominio de Active Directory. Incluyendo objetos que se han eliminado, suponiendo que haya habilitado la función Papelera de reciclaje de Active Directory. Estoy satisfecho con el resultado y muchos de ustedes lo encontraron útil y al menos vale la pena verlo. Sin embargo, ahora me doy cuenta de que realicé el proceso al revés. Es cierto que el script de generación de HTML se basa en código de PowerShell que puedo ejecutar de forma interactiva para mostrar los cambios de Active Directory. Pero debería haber ido más lejos. Debería haber creado una herramienta de PowerShell que me mostrara los mismos cambios, pero desde un mensaje de la consola de PowerShell. Insinué esto en el artículo original. El script que escribí es útil, pero sólo puede hacer aquello para lo que fue diseñado: crear un informe HTML. Así es como solucioné mi descuido.

Comience con un cmdlet de PowerShell

Al igual que con cualquier proyecto de creación de herramientas de PowerShell, desea comenzar con un cmdlet y una expresión o conjunto de expresiones de PowerShell, que puede ejecutar de forma interactiva para lograr el resultado deseado. O al menos una cantidad suficiente del resultado que puedas embellecer. Tenía ese código usando Get-ADObject del script anterior. Pero mientras que el script de informe HTML sólo podía hacer una cosa, quería que mi herramienta de línea de comandos fuera más flexible.

Además de admitir algunos de los parámetros de Get-ADObject, como Credential, Server y SearchBase, también quería que el usuario pudiera especificar el tipo de objeto. Quizás ahora solo necesite ver objetos de grupo, pero mañana quiero ver usuarios y computadoras. Necesitaré un parámetro como este:

[Parameter(HelpMessage = "Specify the types of objects to query.")]
[ValidateSet("User","Group","Computer","OU")]
[ValidateNotNullOrEmpty()]
[string[]]$Category = "User",

Hay muchos tipos de objetos de Active Directory pero estos son los que quiero observar con regularidad. Observe el uso de [ValidateSet()]. Estos son los únicos valores posibles y PowerShell los completará con tabulaciones. Recuerde que si especifica un valor predeterminado, se incluye en el conjunto de validación.

También pensé en qué propiedades quería ver. No estás limitado al objeto original. Tenía una lista de lo que quería del comando Get-ADObject. Pero también quería incluir el nombre del controlador de dominio. Es posible que obtenga resultados diferentes según la replicación del controlador de dominio, por lo que quería capturar el controlador de dominio. No pude encontrar ningún lugar en el resultado que me mostrara eso, así que decidí establecer un valor de parámetro predeterminado para el servidor para tener siempre algo que usar.

[Parameter(HelpMessage = "Specifies the Active Directory Domain Services domain controller to query. The default is your Logon server.")]
[alias("DC")]
[ValidateNotNullorEmpty()]
[string]$Server = $env:LOGONSERVER.SubString(2)

La variable de entorno usa un formato como \\DOM1 pero Get-ADObject espera un valor de servidor de DOM1, por lo que mi valor predeterminado analiza el valor del entorno. Podría haber hecho que el parámetro fuera obligatorio, pero me pareció excesivo. Aunque podría haber agregado un ArgumentCompleter al parámetro Servidor.

[ArgumentCompleter({(Get-ADDomain).ReplicaDirectoryServers})]

En un dominio pequeño, esta podría ser una buena opción.

Consultas LDAP

Mi secuencia de comandos de informes HTML utilizó un único filtro para modificar todos los objetos desde una fecha y hora determinada. Pero esperaba que mi nueva herramienta PowerShell fuera un poco más granular. Comencé intentando crear una expresión de filtrado compleja para Get-ADObject sobre la marcha, basada en los valores de los parámetros de mi función. Esto pronto se salió de control. Además, había una peculiaridad en Active Directory que me había olvidado. Mis primeras versiones devolvieron objetos modificados para usuarios Y computadoras, incluso cuando solo buscaba usuarios. Esto se debe a que el objeto de computadora se deriva del objeto de usuario y la búsqueda de este último incluye el primero.

Mi solución fue recurrir a los filtros LDAP, lo que resultó ser algo bueno. Ahora podría tener un filtro que devolviera exactamente el tipo de objeto que estaba buscando. Sin embargo, incluir el filtrado por el valor de fecha y hora añadió otro problema. No podría incluir algo como "Cuando cambió >= '28/01/2021 2:00 p.m.'". Necesitaba convertir el valor de fecha y hora en una cadena formateada como aaaaMMddhhmmss.ff seguida de un desplazamiento de zona horaria como -0400. Escribí una función auxiliar privada para reformatear el valor de fecha y hora.

Function _ConvertToLDAPTime {
        #a private helper function to convert a date time object into a LDAP query-compatible value
        Param([datetime]$Date)
        $offset = (Get-TimeZone).baseUtcOffset
        #values must be formatted with leading zeros to the specified number of decimal places
        $tz = "{0:d2}{1:d2}" -f $offset.hours,$offset.Minutes
        "{0:yyyyMMddhhmmss}.0{1}" -f $date,$tz
}

Estoy usando un nombre no estándar porque esta función no está expuesta al usuario y solo se llama dentro de la función principal.

$dt = _ConvertToLDAPTime -Date $Since

En mi función, recorreré cada clase de objeto utilizando un filtro LDAP.

foreach ($objClass in $Category) {
        Switch ($objclass) {
            "User"     { $ldap = "(&(WhenChanged>=$dt)(objectclass=user)(!(objectclass=computer)))" }
            "Computer" { $ldap = "(&(WhenChanged>=$dt)(objectclass=computer))"}
            "Group"    { $ldap = "(&(WhenChanged>=$dt)(objectclass=group))"}
            "OU"       { $ldap = "(&(WhenChanged>=$dt)(objectclass=organizationalunit))"}
        }
        Write-Verbose "[$(Get-Date)] Using LDAP filter $ldap"
        $getparams["LDAPFilter"] = $ldap
        Get-ADObject @getParams | Foreach-Object { $items.Add($_)}
}

Creando nuevos objetos

Los resultados se guardan en un objeto de lista que luego se procesa y se convierte en un nuevo objeto personalizado con un nombre de tipo ADChange.

foreach ($item in $items) {
    if ($item.WhenCreated -ge $since) {
        $isNew = $True
    }
    else {
        $isNew = $false
    }
    #create a custom object based on each search result
    [PSCustomObject]@{
        PSTypeName        = "ADChange"
        ObjectClass       = $item.ObjectClass
        ObjectGuid        = $item.ObjectGuid
        DistinguishedName = $item.DistinguishedName
        Name              = $item.Name
        DisplayName       = $item.DisplayName
        Description       = $item.Description
        WhenCreated       = $item.WhenCreated
        WhenChanged       = $item.WhenChanged
        IsNew             = $IsNew
        IsDeleted         = $item.Deleted
        Container         = $item.distinguishedname.split(",", 2)[1]
        DomainController  = $Server.toUpper()
        ReportDate        = $ReportDate
    }
} #foreach item

¿Por qué hacer este esfuerzo? Para poder personalizar el objeto para que sea más fácil de usar en la tubería. Por ejemplo, puedo definir algunas propiedades de alias así como un conjunto de propiedades predeterminadas.

Update-TypeData -TypeName ADChange -DefaultDisplayPropertySet DistinguishedName,WhenCreated,WhenChanged,IsNew,IsDeleted,ObjectClass,ReportDate -Force
#define some alias properties for the custom object
Update-TypeData -TypeName ADChange -MemberType AliasProperty -MemberName class -Value ObjectClass -Force
Update-TypeData -TypeName ADChange -MemberType AliasProperty -MemberName DN -Value DistinguishedName -Force

También puedo crear un archivo de formato personalizado, de modo que no solo pueda obtener la vista formateada predeterminada que deseo, sino que también pueda incluir otras vistas.

Update-FormatData $PSScriptRoot\ADchange.format.ps1xml

Utilizo salida detallada todo el tiempo en mi trabajo de PowerShell. En esta función incluyo lo que llamo metadatos de tiempo de ejecución.

La idea es que si alguien tiene problemas al ejecutar el comando, puedo pedirle que inicie una transcripción, ejecute el comando con -Verbose, detenga la transcripción y me la envíe. Esperemos que el resultado detallado pueda ayudarme a aislar el problema.

Obtenga el código

Algunos de ustedes ya están buscando el resto del código. Como esto es algo en lo que podría seguir jugando, publiqué la función Get-ADChange y el archivo de formato en Github. Vaya a https://gist.github.com/jdhitsolutions/3bce157bd64717dd616b949f6e280433 y obtenga ambos archivos. Los archivos deben ir en la misma carpeta y asegurarse de que el archivo de formato esté guardado como adchange.format.ps1xml. Luego puede obtener el archivo de secuencia de comandos:

. c:\scripts\get-adchange.ps1

Obviamente use el camino apropiado. El script también cargará el archivo de formato en su sesión de PowerShell. Debería poder ejecutar el comando Get-ADChange desde un escritorio de Windows 10 que tenga instalado el módulo ActiveDirectory.

Dicho esto, este código se ofrece tal cual, sin garantías ni soporte. Le recomiendo encarecidamente que realice pruebas exhaustivas en un entorno que no sea de producción.

Resumen

Podría volver a mi script de informes HTML y modificarlo para usar esta nueva función. O podría crear otros scripts de controlador a su alrededor. Y permítame reiterar que si tiene una infraestructura de Active Directory más grande, será mejor que invierta en verdaderas soluciones de administración y generación de informes. Varios lectores querían poder descubrir quién podría haber realizado un cambio. Esa información no se almacena con el objeto de Active Directory. Ahí es donde entran en juego la auditoría y el registro y donde vale la pena invertir en un producto de gestión "real".

Aún así, espero que aprendas algo sobre el proceso de creación de una herramienta PowerShell a partir de esto. Como siempre, preguntas y comentarios son bienvenidos.

Artículos relacionados