Friday, October 7, 2022
HomePowershellHigher PowerShell Properties • The Lonely Administrator

Higher PowerShell Properties • The Lonely Administrator


I used to be chatting with my pal Gladys Kravitz lately about some PowerShell scripting she was doing with Lively Listing and the DirectorySearcher object. For a variety of causes, the Lively Listing module from Distant Server Administration Instruments (RSAT) will not be an possibility. However that’s effective. Gladys is an expertise AD admin and PowerShell scripter. Nonetheless, she had a query concerning the output from her instructions, which led to some fascinating work that you just may discover useful. Regardless that I’m going to be discussing a particular object sort, there are a number of rules that you could apply to your PowerShell scripting.

Handle and Report Lively Listing, Change and Microsoft 365 with
ManageEngine ADManager Plus – Obtain Free Trial

Search Outcome Properties

Let’s start with some PowerShell code to discover a person account utilizing the DirectorySearcher object.

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

The DirectorySearcher has strategies of FindOne() and FindAll(). The ensuing object has a Properties property.

You should utilize this in any variety of methods.

New-Object -typename PSObject -property $searcher.FindOne().Properties | Choose Title,Description,WhenChanged
new object from result properties

Leaving the formatting of the values apart, discover the property names. Practical, however not professional-looking. That is all the time an possibility.

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

Or you may outline a customized object with a kind identify and use a customized format file. However that’s lots of work and never versatile.

Right here’s one other method to creating an object however with correctly cased property names.

$r = $searcher.FindOne()
$r.Properties.GetEnumerator() | 
The place-Object Description | 
ForEach-Object -Start {
    #initialize an empty hashtable
    $h = @{}
} -Course of {
    #convert to title case
    $n = [cultureinfo]::CurrentCulture.TextInfo.ToTitleCase($_.identify.tolower())
    if ($_.worth.depend -gt 1) {
        #get the array of values
        $v = $_.worth
    }
    else {
        #get the one worth
        $v = $_.worth[0]
    }
    #add the property identify and worth to the hashtable
    $h.Add($n, $v)
} -Finish { 
    #create a customized object from the hashtable
    [pscustomobject]$h 
}

On this specific state of affairs, the item I’m formatting is a DirectoryServices.ResultCollection, which is, in essence, a hashtable, so it requires a distinct method. The GetEnumerator() methodology writes every aspect within the assortment as an object with Key and Worth properties. My instance is filtering keys for particular “properties.”

The filtered objects are then piped to ForEach-Object. Earlier than any objects are processed within the pipeline, I initialize an empty hashtable. Subsequent, for every filtered outcome, I convert the identify, which is an alias for ‘key,’ to title case. For this to work, I all the time make the string all decrease case earlier than invoking the strategy. This reformatted identify and the corresponding worth are added to the hashtable. After all the things has been processed, I flip the hashtable right into a customized object.

custom search result with proper title case

That is promising however must be extra versatile. The ForEach-Object construction is similar format as a complicated PowerShell perform that takes pipeline enter. In different phrases, I’ve a prototype.

Creating Configuration Knowledge

My concept is to take a DirectorySearcher outcome and create a customized object changing the property identify with a correctly formatted identify and the worth. However in trying on the output, I would like greater than title case. ‘Logoncount’ may look nicer formatted as ‘LogonCount.’ I favor SamAccountName. I need to exchange the incoming property identify with a correctly formatted identify. I’ll want an inventory of all doable property names formatted the best way I would like. As a result of I do know I’ll finally need to use this listing as a hashtable, I’ll create a configuration information file that I can use later with Import-PowerShellDataFile.

Right here’s the PowerShell script to generate totally different configuration information information relying on the item sort.

#requires -version 5.1

<# Create-ADPropertyFile.ps1
 proof-of-concept code to create an Lively Listing property configuration information file
 .Create-ADPropertyFile.ps1 -Filter "(&(objectclass=organizationalunit)(Title=Staff))" -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 = "Person",

    [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") {

    Swap ($ObjectType) {
        "Person" { $filter = "(&(objectcategory=particular person)(objectclass=person))" }
        "Group" { $filter = "(Objectclass=group)" }
        "Pc" { $filter = "(objectclass=laptop)" }
        "OU" { $filter = "(objectclass=organizationalunit)" }
    }
}

$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.filter = $filter
#do not want values from the search
$searcher.PropertyNamesOnly = $true
$searcher.FindOne().Properties.GetEnumerator() | Type-Object -Property Key |
ForEach-Object -Start {
    #initialize an inventory
    $listing = [System.Collections.Generic.List[string]]::new()
    $listing.add("@{")
} -Course of {
    $worth = [cultureinfo]::CurrentCulture.TextInfo.ToTitleCase($($_.key.ToLower()))
    #add every property to the listing
    $Listing.Add("$($_.key) = '$worth'")
} -Finish {
    #shut the psd1
    $listing.Add("}")
}
#save the listing to a file
$listing | Out-File -FilePath $FilePath

#edit psd1 file along with your desired formatting

The default filters will discover the primary matching outcome. Nevertheless, the DirectorySearcher will solely return property names which have been outlined. You may need to create a template object with all of the properties outlined that you just intend to make use of after which construct the property file utilizing a customized LDAP filter to that object.

I’ll use the script to create a psd1 file for person objects.

c:scriptsCreate-ADPropertyFile.ps1 -ObjectType Person -FilePath c:scriptsaduser.psd1

I can edit the file and alter the property names.

@{
    accountexpires="AccountExpires"
    admincount="AdminCount"
    adspath="AdsPath"
    badpasswordtime="BadPasswordTime"
    badpwdcount="BadPwdCount"
    cn                     = 'CN'
    codepage="CodePage"
    countrycode="CountryCode"
    division             = "Division"
    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"
    identify="Title"
    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'
}

}

If you understand extra LDAP property names, you may manually add them. The final step is to make use of this file.

Optimize AD Search Outcome

We are likely to keep away from monolithic instructions in PowerShell. As an alternative, we need to leverage the pipeline. I’ve code utilizing the DirectorySearcher to seek out objects.

directory searcher results

I need to take these outcomes and optimize the property names.

Perform 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
    )
    Start {
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Beginning $($myinvocation.mycommand)"
        #import the configuration information
        Write-Verbose "[$((Get-Date).TimeofDay) BEGIN  ] Importing configuration information from $(Convert-Path $ConfigurationData)"
        $PropertyData = Import-PowerShellDataFile -Path $ConfigurationData
    } #start
    Course of {
        Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing enter"

        $InputObject.properties.GetEnumerator() |
        ForEach-Object -Start {
            $new = [ordered]@{ }
            if ($TypeName) {
                Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Utilizing typename $TypeName"
                $new.Add("PSTypename", $TypeName)
            }
        } -Course of {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing property $($_.key)"
            #Get formatted property identify from configuration information
            if ($PropertyData.Accommodates($_.key)) {
                $identify = $PropertyData["$($_.key)"]
            }
            else {
                $identify = $_.key
            }
            if ($_.worth.depend -gt 1) {
                $worth = $_.worth
            }
            else {
                $worth = $_.worth[0]
            }
            $new.Add($identify, $worth)
        } -Finish {
            Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating output"
            New-Object -TypeName psobject -Property $new
        }
    } #course of
    Finish {
        Write-Verbose "[$((Get-Date).TimeofDay) END    ] Ending $($myinvocation.mycommand)"
    } #finish
}

The perform seems to be at every search outcome and tries to match the property identify from the configuration information. If it finds a match, it will likely be used. In any other case, PowerShell will use the unique property identify.

I may have saved all the property identify hashtables within the perform, however that will have added complexity and pointless size. If I would like so as to add a property, I’ve to edit the perform. It’s significantly better to separate information from the code that implements it. My perform is an efficient instance of utilizing configuration information.

I can take the search outcomes, optimize them with this perform, after which choose what I would like.

 $searcher = New-Object System.DirectoryServices.DirectorySearcher
 $searcher.filter = "(&(objectcategory=particular person)(objectclass=person)(division=gross sales))"
 $searcher.FindAll() | 
 Optimize-ADSearchResult -ConfigurationData C:scriptsaduser.psd1 |
 Choose-Object -property DistinguishedName,Givenname,Surname,Description,Title
property optimized search results

Abstract

There’s a lot happening on this submit—many transferring elements and presumably some new instructions. I hope you’ll evaluation the code and no less than stroll by it in your head. Please be at liberty to depart feedback and questions.


Learn PowerShell Scripting in a Month of Lunches, 2nd Ed. MEAP


RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments