Búsqueda de sitios web

Mejores propiedades de PowerShell


Recientemente estuve conversando con mi amiga Gladys Kravitz sobre algunas secuencias de comandos de PowerShell que estaba haciendo con Active Directory y el objeto DirectorySearcher. Por varias razones, el módulo Active Directory de Herramientas de administración remota del servidor (RSAT) no es una opción. Pero eso está bien. Gladys es una administradora de AD con experiencia y scripter de PowerShell. Aún así, tenía una pregunta sobre el resultado de sus comandos, lo que la llevó a realizar un trabajo interesante que podría resultarle valioso. Aunque voy a analizar un tipo de objeto específico, existen varios principios que puede aplicar a sus secuencias de comandos de PowerShell.

Propiedades de resultados de búsqueda

Comencemos con algo de código de PowerShell para encontrar una cuenta de usuario utilizando el objeto DirectorySearcher.

$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.filter ="(&(objectcategory=person)(objectclass=user))"
$searcher.FindOne().Properties

DirectorySearcher tiene métodos de FindOne() y FindAll(). El objeto resultante tiene una propiedad Propiedades.

Puedes utilizar esto de varias maneras.

New-Object -typename PSObject -property $searcher.FindOne().Properties | Select Name,Description,WhenChanged

Dejando a un lado el formato de los valores, observe los nombres de las propiedades. Funcional, pero no de aspecto profesional. Esta es siempre una opción.

New-Object -typename PSObject -property $searcher.FindOne().Properties | 
Select-Object @{Name="Name";Expression = {$_.name}},
@{Name="Description";Expression = {$_.Description}},
@{Name="WhenChanged";Expression = {$_.WhenChanged}}

O puede definir un objeto personalizado con un nombre de tipo y utilizar un archivo de formato personalizado. Pero eso es mucho trabajo y no es flexible.

Aquí hay otro método para crear un objeto, pero con nombres de propiedad correctamente escritos.

$r = $searcher.FindOne()
$r.Properties.GetEnumerator() | 
Where-Object { $_.key -match "Name|Description|WhenChanged|memberof|logoncount" } | 
ForEach-Object -Begin {
    #initialize an empty hashtable
    $h = @{}
} -Process {
    #convert to title case
    $n = [cultureinfo]::CurrentCulture.TextInfo.ToTitleCase($_.name.tolower())
    if ($_.value.count -gt 1) {
        #get the array of values
        $v = $_.value
    }
    else {
        #get the single value
        $v = $_.value[0]
    }
    #add the property name and value to the hashtable
    $h.Add($n, $v)
} -End { 
    #create a custom object from the hashtable
    [pscustomobject]$h 
}

En esta situación particular, el objeto que estoy formateando es DirectoryServices.ResultCollection, que es, en esencia, una tabla hash, por lo que requiere un enfoque diferente. El método GetEnumerator() escribe cada elemento de la colección como un objeto con propiedades Clave y Valor. Mi ejemplo es filtrar claves para "propiedades" específicas.

Luego, los objetos filtrados se canalizan a ForEach-Object. Antes de procesar cualquier objeto en la canalización, inicializo una tabla hash vacía. A continuación, para cada resultado filtrado, convierto el nombre, que es un alias de "clave", a mayúsculas y minúsculas. Para que esto funcione, siempre pongo la cadena en minúsculas antes de invocar el método. Este nombre reformateado y el valor correspondiente se agregan a la tabla hash. Una vez procesado todo, convierto la tabla hash en un objeto personalizado.

Esto es prometedor pero necesita ser más flexible. La estructura ForEach-Object tiene el mismo formato que una función avanzada de PowerShell que recibe entradas de canalización. En otras palabras, tengo un prototipo.

Crear datos de configuración

Mi idea es tomar un resultado de DirectorySearcher y crear un objeto personalizado reemplazando el nombre de la propiedad con un nombre y el valor con el formato adecuado. Pero al observar el resultado, quiero algo más que mayúsculas y minúsculas. "Logoncount" podría verse mejor con el formato "LogonCount". Prefiero SamAccountName. Quiero reemplazar el nombre de la propiedad entrante con un nombre con el formato adecuado. Necesitaré una lista de todos los nombres de propiedades posibles formateados como quiero. Como sé que eventualmente querré usar esta lista como tabla hash, crearé un archivo de datos de configuración que puedo usar más adelante con Import-PowerShellDataFile.

Aquí está el script de PowerShell para generar diferentes archivos de datos de configuración según el tipo de objeto.

#requires -version 5.1

<# Create-ADPropertyFile.ps1
 proof-of-concept code to create an Active Directory property configuration data file
 .\Create-ADPropertyFile.ps1 -Filter "(&(objectclass=organizationalunit)(Name=Employees))" -FilePath .\adou.psd1
 .\Create-ADPropertyFile.ps1 Group -FilePath .\adgroup.psd1
#>

[cmdletbinding(SupportsShouldProcess, DefaultParameterSetName = "byType")]
Param(
    [Parameter(Position = 0, ParameterSetName = "byType")]
    [ValidateSet("User", "Group", "Computer", "OU")]
    [string]$ObjectType = "User",

    [Parameter(HelpMessage = "Specify an LDAP search filter to a template object.", ParameterSetName = "byFilter")]
    [ValidateNotNullOrEmpty()]
    [string]$Filter,

    [Parameter(Mandatory, HelpMessage = "Specify the filename and path to the psd1 file.")]
    [ValidateScript({ Test-Path (Split-Path $_) })]
    [string]$FilePath
)

if ($pscmdlet.ParameterSetName -eq "byType") {

    Switch ($ObjectType) {
        "User" { $filter = "(&(objectcategory=person)(objectclass=user))" }
        "Group" { $filter = "(Objectclass=group)" }
        "Computer" { $filter = "(objectclass=computer)" }
        "OU" { $filter = "(objectclass=organizationalunit)" }
    }
}

$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.filter = $filter
#don't need values from the search
$searcher.PropertyNamesOnly = $true
$searcher.FindOne().Properties.GetEnumerator() | Sort-Object -Property Key |
ForEach-Object -Begin {
    #initialize a list
    $list = [System.Collections.Generic.List[string]]::new()
    $list.add("@{")
} -Process {
    $value = [cultureinfo]::CurrentCulture.TextInfo.ToTitleCase($($_.key.ToLower()))
    #add each property to the list
    $List.Add("$($_.key) = '$value'")
} -End {
    #close the psd1
    $list.Add("}")
}
#save the list to a file
$list | Out-File -FilePath $FilePath

#edit psd1 file with your desired formatting

Los filtros predeterminados encontrarán el primer resultado coincidente. Sin embargo, DirectorySearcher solo devolverá nombres de propiedades que se hayan definido. Es posible que desee crear un objeto de plantilla con todas las propiedades definidas que desea utilizar y luego crear el archivo de propiedades utilizando un filtro LDAP personalizado. a ese objeto.

Usaré el script para crear un archivo psd1 para objetos de usuario.

c:\scripts\Create-ADPropertyFile.ps1 -ObjectType User -FilePath c:\scripts\aduser.psd1

Puedo editar el archivo y ajustar los nombres de las propiedades.

@{
    accountexpires         = 'AccountExpires'
    admincount             = 'AdminCount'
    adspath                = 'AdsPath'
    badpasswordtime        = 'BadPasswordTime'
    badpwdcount            = 'BadPwdCount'
    cn                     = 'CN'
    codepage               = 'CodePage'
    countrycode            = 'CountryCode'
    department             = "Department"
    description            = 'Description'
    displayname            = "DisplayName"
    distinguishedname      = 'DistinguishedName'
    dscorepropagationdata  = 'DscorePropagationData'
    givenname              = "GivenName"
    instancetype           = 'InstanceType'
    iscriticalsystemobject = 'IsCriticalSystemObject'
    lastlogoff             = 'LastLogoff'
    lastlogon              = 'Lastlogon'
    lastlogontimestamp     = 'LastLogonTimestamp'
    logoncount             = 'LogonCount'
    logonhours             = 'LogonHours'
    memberof               = 'MemberOf'
    name                   = 'Name'
    objectcategory         = 'ObjectCategory'
    objectclass            = 'ObjectClass'
    objectguid             = 'ObjectGuid'
    objectsid              = 'ObjectSid'
    primarygroupid         = 'PrimaryGroupId'
    pwdlastset             = 'PwdlastSet'
    samaccountname         = 'SamAccountName'
    samaccounttype         = 'SamAccountType'
    sn                     = "Surname"
    title                  = "Title"
    useraccountcontrol     = 'UserAccountcontrol'
    usnchanged             = 'UsnChanged'
    usncreated             = 'UsnCreated'
    whenchanged            = 'WhenChanged'
    whencreated            = 'WhenCreated'
}

}

Si conoce nombres de propiedades LDAP adicionales, puede agregarlos manualmente. El último paso es utilizar este archivo.

Optimizar el resultado de la búsqueda de AD

Tendemos a evitar comandos monolíticos en PowerShell. En cambio, queremos aprovechar el oleoducto. Tengo un código que usa DirectorySearcher para buscar objetos.

Quiero tomar estos resultados y optimizar los nombres de las propiedades.

Function Optimize-ADSearchResult {
    [cmdletbinding()]
    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline,
            HelpMessage = "This should be the input from an ADSearcher FindOne() or FindAll() method."
        )]
        [System.DirectoryServices.SearchResult]$InputObject,

        [Parameter(
            Mandatory,
            HelpMessage = "Enter the path to the psd1 file with your property names. See Create-ADPropertyFile."
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ })]
        [ValidatePattern('\.psd1$')]
        [string]$ConfigurationData,

        [Parameter(HelpMessage = "Specify a custom type name, like CorpADUser. You might add this if using a custom format file or type extensions.")]
        [ValidateNotNullOrEmpty()]
        [string]$TypeName
    )
    Begin {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Starting $($myinvocation.mycommand)"
        #import the configuration data
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Importing configuration data from $(Convert-Path $ConfigurationData)"
        $PropertyData = Import-PowerShellDataFile -Path $ConfigurationData
    } #begin
    Process {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing input"

        $InputObject.properties.GetEnumerator() |
        ForEach-Object -Begin {
            $new = [ordered]@{ }
            if ($TypeName) {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Using typename $TypeName"
                $new.Add("PSTypename", $TypeName)
            }
        } -Process {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing property $($_.key)"
            #Get formatted property name from configuration data
            if ($PropertyData.Contains($_.key)) {
                $name = $PropertyData["$($_.key)"]
            }
            else {
                $name = $_.key
            }
            if ($_.value.count -gt 1) {
                $value = $_.value
            }
            else {
                $value = $_.value[0]
            }
            $new.Add($name, $value)
        } -End {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating output"
            New-Object -TypeName psobject -Property $new
        }
    } #process
    End {
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"
    } #end
}

La función analiza cada resultado de búsqueda e intenta hacer coincidir el nombre de propiedad de los datos de configuración. Si encuentra una coincidencia, se utilizará. De lo contrario, PowerShell utilizará el nombre de propiedad original.

Podría haber almacenado todas las tablas hash de nombres de propiedades en la función, pero eso habría agregado complejidad y longitud innecesaria. Si necesito agregar una propiedad, tengo que editar la función. Es mucho mejor separar los datos del código que los implementa. Mi función es un buen ejemplo del uso de datos de configuración.

Puedo tomar los resultados de la búsqueda, optimizarlos con esta función y luego seleccionar lo que necesito.

 $searcher = New-Object System.DirectoryServices.DirectorySearcher
 $searcher.filter = "(&(objectcategory=person)(objectclass=user)(department=sales))"
 $searcher.FindAll() | 
 Optimize-ADSearchResult -ConfigurationData C:\scripts\aduser.psd1 |
 Select-Object -property DistinguishedName,Givenname,Surname,Description,Title

Resumen

Están sucediendo muchas cosas en esta publicación: muchas partes móviles y posiblemente algunos comandos nuevos. Espero que revises el código y al menos lo repases mentalmente. No dude en dejar comentarios y preguntas.

Artículos relacionados