0

I am working on a Powershell script that will take an OU (optional), a domain name (default is company.local), and a selection of AD properties to return (default is name,lastlogondate). I want to send the output to a CSV file.

I have two problems.

  1. The script returns the requested properties, as retrieved from all of the domain controllers. I want just want entry for each person, which has the most recent value for "LastLogonDate".
  2. I don't know how to handle building the property hash table with a random number of keys.

Any thoughts on how I should handle these issue? Thanks.

Here is the code I'm using right now:

[CmdletBinding()]
param(
    [string]$DomainName = "company.local",

    [string[]]$SearchPath = 'OU=people,DC=company,DC=local',

    [string[]]$OutputProperties = 'Name,lastlogondate'
)

Import-Module ActiveDirectory

$props = @{}

$temp = New-Object 'System.DirectoryServices.ActiveDirectory.DirectoryContext'("domain","$DomainName")
$dcs = [System.DirectoryServices.ActiveDirectory.DomainController]::FindAll($temp)

Foreach ($ou in $SearchPath) {
    $users = Get-ADUser -Filter * -SearchBase $ou -Properties $OutputProperties.Split(",") -Server $DomainName | Select $OutputProperties.Split(",")

    $time = 0

    Foreach ($dc in $dcs) {
        Foreach ($user in $users) {
            If ($user.LastLogonDate -gt $time) {
                $time = $user.LastLogonDate
            }
            $props.'LogonTime' = $time
            $props.'Name'=$user.Name
            New-Object Psobject -Property $props
        }
    }
} 
StackExchangeGuy
  • 741
  • 16
  • 36
  • For starters I would guess that you are looking to check the details for all users on all domain controllers? Should the line `$users = Get-ADUser -Filter * ....` not be inside the `Foreach ($dc` loop? You would also have to change the `-Server` to -`Server $dc`. Would you be happy with post processing? Getting all the details and then finding the newest one for each user then filtering out the old ones? – Matt Oct 02 '14 at 15:41

3 Answers3

0

Exanding on my comment assuming the end justifies the means. Instead of trying to determine the latest time inside the loop we can post process the data with some simple cmdlets. I think the way you are doing it now would be comparing the time between all users which is not what you intended. I present the following. Everything above this code I left the same ( For testing I edited the params)

$data = Foreach ($ou in $SearchPath) {

    Foreach ($dc in $dcs) {

    $users = Get-ADUser -Filter * -SearchBase $ou -Properties $OutputProperties.Split(",") -Server $dc | Select $OutputProperties.Split(",")

        Foreach ($user in $users) {
            $props.'LogonTime' = $user.LastLogonDate
            $props.'Domain Controller' = $dc
            $props.'Name'=$user.Name
            New-Object Psobject -Property $props
        }
    }
}
$data | Group-Object Name | ForEach-Object{$_.Group | Sort-Object LogonTime | Select-Object -Last 1}

I moved the Get-ADUser inside the dc loop so that we are querying all dc's for all requested users (Since it is possible that the LastLogon timestamp could be different). I removed the If statement and the references to $time because we are going to process after. You can take this out but I added a property for Domain Controller for testing as I was curious. Capture those results into the variable $data. Taking $data group by each user. For each group sort the logon times and pick the last one which would be the most recent. The last line comes from this SO question

Dynamic Fields

Alright, so you have $OutputProperties that could contain anything. There is no need to worry about what is contained. In the previous Select-Object you have the properties you want. Just echo the $user and it will be assigned to $data to then be sorted.

Foreach ($user in $users) {
    $user
}

I know this is a lot and I will update the answer to not be as verbose but I would like to incorporate TheMadTechnician's idea before i redo it. The more I look at this the more i think i can change. I'm making it more efficient now.

Community
  • 1
  • 1
Matt
  • 45,022
  • 8
  • 78
  • 119
  • For the sake of speed might I suggest running the Get-ADUser for each server to be run as a job, then you can use a Do/While((Get-Job).count -gt 0) loop to retrieve the results and create your arrays of users for processing. That way it isn't sitting there waiting for each server before it queries the next one. Just my two bits. – TheMadTechnician Oct 02 '14 at 16:25
  • @TheMadTechnician Not to shabby. That's how I learn anyway from other peoples ideas and constructive criticism. Will try and make that work and update. Thanks. – Matt Oct 02 '14 at 16:33
  • Good ideas and they address the first question. I added a bit of code to skip Server 2003 DCs (since they don't have ADWS). How do I create a hash table with the properties and value, without knowing which properties a user is going to want to retrieve? – StackExchangeGuy Oct 02 '14 at 19:19
  • In other words, how do I loop through the values in $OutputProperties to create the hash table with $user.something as the value? I know that I can use $props.add() to add a key, but not how to add the value. – StackExchangeGuy Oct 02 '14 at 19:26
  • `$OutputProperties` is going to by dynamic and change then? You want to be able to bulid the output to reflect that correct? – Matt Oct 02 '14 at 19:28
  • @StackExchangeGuy the dynamic part should be covered now. – Matt Oct 02 '14 at 20:39
  • Correct. I never know what properties are going to be in $OutputProperties. – StackExchangeGuy Oct 02 '14 at 21:55
  • Although, I wonder if you just need to put a $_ in the loop just to output everything that is there. Making the custom object seems like a waste of time now. Will look again once I am home – Matt Oct 02 '14 at 22:21
0

I'm not comfortable deleting my other answer at this time. Using the two bits in the comment from @TheMadTechincian I wanted to see if i could improve this while using jobs. It's my first crack at using Start-Job but I think it does the trick.

[CmdletBinding()]
param(
    [string]$DomainName = "domain.local",
    [string[]]$SearchPath = 'OU=container,dc=domain,dc=local',
    [string[]]$OutputProperties = @("Name","lastlogondate","samaccountname")
)

Import-Module ActiveDirectory

If(!($OutputProperties -contains "lastlogondate")){$OutputProperties += "lastlogondate"} 
$temp = New-Object 'System.DirectoryServices.ActiveDirectory.DirectoryContext'("domain","$DomainName")
$dcs = [System.DirectoryServices.ActiveDirectory.DomainController]::FindAll($temp)

$dcs | ForEach-Object{
    ForEach($singleOU in $SearchPath){
        $arguments = $singleOU,$OutputProperties,$_
        [void](Start-Job -ScriptBlock {
                Get-ADUser -Filter * -SearchBase $args[0] -Properties $args[1] -Server $args[2]
                } -ArgumentList $arguments)
    }
}

While((Get-Job -State 'Running').Count)
{
    Start-Sleep -Milliseconds 200
} 

Get-Job | Receive-Job| Group-Object Name | ForEach-Object{$_.Group | Sort-Object lastlogondate | Select-Object $OutputProperties -Last 1}

Start a job for each ou search on each dc. This could easily get out of hand. In case this is an issue i would refer to a post limiting the amount of jobs made here. Wait for all the jobs to complete and then process the output looking for the most recent lastlogondate for each user. Converting $OutputProperties into an array stops the need for splits and using it for all the selects removes the needs to worry about dynamic properties as they will always be in the output. Since the last part of the script requires lastlogondate I added a if statement that will add it in case a user didnt use it.

Community
  • 1
  • 1
Matt
  • 45,022
  • 8
  • 78
  • 119
0

I like the idea of using a Job, but was having trouble doing error handling with it. Since we still have so many Server 2003 DCs, I just went with the original idea. Here is the final script. Thanks for the feedback.

<#
.Synopsis
    Searches ActiveDirectory and returns a user-specified list of properties
.DESCRIPTION 
    This script takes a user-specified list OUs and a user-specified list of desired properties.
.NOTES 
    Author: Mike Hashemi
    V1 date: 15 August 2014
    V2 date: 6 October 2014
        - Converted the main part of the script, into a function.
        - Added routie to gather all DCs in a domain, for the ability to return LastLogonDate.
.LINK
    http://stackoverflow.com/questions/26163437/creating-objects-with-unknown-number-of-properties-powershell
.PARAMETER DomainName
    Default value is 'company.local'. This parameter represents the DNS domain name, of the domain.
.PARAMETER SearchPath
    Default value is 'OU=people,DC=company,DC=local'. This parameter represents a comma-separated list of OUs to search.
.PARAMETER OutputProperties
    Default value is 'Name,Enabled,LastLogonDate'. This parameter represents a comma-separated list of properties to return.
.EXAMPLE
    .\get-ADUserProperties-Parameterized.ps1
    This example get's a list of all users in 'OU=people,DC=company,DC=local' and outputs the Name, Enabled, and LostLogonDate attributes.
.EXAMPLE
    .\get-ADUserProperties-Parameterized.ps1 -SearchPath 'OU=people,DC=company,DC=local','OU=managers,DC=company,DC=local'
    This example get's a list of all users in the 'OU=people,DC=company,DC=local' and 'OU=managers,DC=company,DC=local' OUs and outputs the 
    Name, Enabled, and LostLogonDate attributes.
.EXAMPLE
    .\get-ADUserProperties-Parameterized.ps1 -SearchPath 'OU=people,DC=company,DC=local' -OutputProperties Name,telephoneNumber | Export-CSV c:\users.csv -NoTypeInformation
    This example get's a list of all users in the 'OU=people,DC=company,DC=local' OU and outputs the Name and Telephone Number attributes. 
    The output is exported to a CSV.
#>
[CmdletBinding()]
param(
    [string]$DomainName = 'managed.local',

    [string[]]$SearchPath = 'OU=people,DC=company,DC=local',

    [string[]]$OutputProperties = 'Name,Enabled,LastLogonDate'
)

Function Get-TheUsers {
    #Create the hash table, for later.
    $props = @{}

    Try {
        #The next two lines get the list of domain controllers, using the supplied DNS domain name.
        Write-Verbose ("Getting domain controllers from {0}" -f $DomainName)
        $temp = New-Object 'System.DirectoryServices.ActiveDirectory.DirectoryContext'("domain","$DomainName")
        $dcs = [System.DirectoryServices.ActiveDirectory.DomainController]::FindAll($temp)
    }
    Catch [System.Management.Automation.MethodInvocationException] {
        Write-Error ("Unable to connect to remote domains. Please run the script from a DC in {0}. " -f $DomainName)
        Exit
    }
    Catch {
        Write-Error ("There was an unexpected error. The message is: {0}" -f $_.Exception.Message)
        Exit
    }

    Foreach ($ou in $SearchPath) {
        Write-Verbose ("Getting users in {0}" -f $ou)
        Foreach ($dc in $dcs) {
            If ($dc.OSVersion -like '*2003*') {
                Write-Warning ("Skipping {0}, because it is not a Server 2008 (or higher) DC." -f $dc)
            }
            Else {
                Write-Verbose ("Searching {0} on {1}." -f $ou,$dc)
                Try {
                    $users = Get-ADUser -Filter * -SearchBase $ou -Properties $OutputProperties.Split(",") -Server $dc -ErrorAction Stop | Select $OutputProperties.Split(",")
                }
                Catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
                    Write-Error ("Unable to search {0}. It appears to be a non-existent OU. The specific error message is: {1}" -f $ou, $_.Exception.Message)
                    Exit
                }

                Foreach ($user in $users) {
                    ForEach($property in $OutputProperties.Split(",")) {
                        $props.$property = $user.$property
                    }
                    New-Object Psobject -Property $props
                }
            }
        }
    }
}

Try { 
    Import-Module ActiveDirectory -ErrorAction Stop
}
Catch [System.IO.FileNotFoundException] {
    Write-Error ("Unable to load the required module. The specific message is: {0}" -f $_.Exception.Message)
    Exit
}

$data = Get-TheUsers

#Takes the output of the Get-ADUser query and groups by the first property in $OutputProperties, then uses the LastLogonDate property (if present)
#to sort again and select only the last (most recent) entry.
Write-Verbose ("Sorting data.")
$data | Group-Object Name | ForEach-Object {$_.Group | Sort-Object LogonTimeDate | Select-Object -Last 1}
StackExchangeGuy
  • 741
  • 16
  • 36