Showing posts with label Recursive. Show all posts
Showing posts with label Recursive. Show all posts

Thursday, May 19, 2011

Exporting the Membership of a Large, Nested, Active Directory Group

One of the most common requests I receive is to enumerate the membership of an Active Directory group and provide some basic information back about the members. Simple enough task if the group is flat and contains a limited number of members. Once you start dealing with nested groups and groups containing more than 1,500 members (1,000 members for an Active Directory 2000 Forest), you need to start planning how you output the information.

With groups containing more than 1,500 members, performing the following will not provide the entire population.
$groupObject = New-Object System.DirectoryServices.DirectoryEntry("GC://CN=All Employees,OU=Distribution Lists,DC=ad,DC=mydomain,DC=local")
foreach($member in $groupObject.member) { Write-Output $member }
You will only enumerate the first 1,500 distinguished names of the multivalued attribute 'member' and output them to the console. Big enough to convince you that you retrieved all the members on first glance but on further investigation you will realize you are missing some people. Hopefully, you caught that and not the person that requested the report with the resolved distinguished names. To overcome this limitation, you need to search within the group object itself and return a range of values within the 'member' attribute; looping until you have enumerated each and every distinguished name.

Active Directory groups can contain user objects, computer objects, contact objects and other groups. To obtain the full membership of a Security Group to understand who has rights to an asset or a Distribution List to understand who will receive an e-mail, you must be aware of nested groups. If a group is a member is a member of the initial group, you will need to enumerate that group and any other groups that it may contain and so on. One of the issues that you can encounter with nested groups is the Administrator that nested a group that is member of a group that itself is patriline to that group. This leads to looping in reporting and must be accounted when recursively enumerating the membership. I tackle this problem by storing the nested group distinguished names that were enumerated in an array and at each recursion evaluating if that distinguished name is an element.

The code sample below deals with these two issues related to group enumeration. I have tested this against code against a group that contained 32,000+ member objects with multiple layers of nesting genreating a 2.9 megabyte report. The only big deficit in the code is there is no provision to deal with objects that are of the foreignSecurityPrincipal class. If you have cross-forest membership in groups, you can add that to the if-then statement and add your own formatting function for those objects. A nice feature of this script is that if you need to enumerate a large number of groups that have similar sAMAccountNames you can wild card them in the command line argument, such as "-group *AdSales*" or "-group DL_*". The code in the script is fairly portable. You can recombine these functions to enumerate the groups associated to NTFS security to provide an access report. You just need to update the functions that deal with the formatting of the objects to provide the desired information.
param([string]$domain,[string]$group,[switch]$verbose)
#-------------------------------------------------------------------------------------------------#
Function Find-GroupDistinguishedNamesBysAMAccountName($domain,$sAMAccountName) {
 $groupDistinguishedNames = @()
 $directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
 $directorySearcher.SearchRoot = (New-Object System.DirectoryServices.DirectoryEntry(("LDAP://" + (Get-LocalDomainController $domain) + "/" + (Get-DomainDn $domain))))
 $directorySearcher.Filter = "(&(objectClass=group)(objectCategory=group)(sAMAccountName=$sAMAccountName))"
 $directorySearcher.PropertiesToLoad.Clear()
 $directorySearcher.PropertiesToLoad.Add("distinguishedName") | Out-Null
 $searchResults = $directorySearcher.FindAll()

 if($searchResults -ne $null) {
  foreach($searchResult in $searchResults) {
   if($searchResult.Properties.Contains("distinguishedName")) {
    $groupDistinguishedNames += $searchResult.Properties.Item("distinguishedName")[0]
   }
  }
 } else {
  $groupDistinguishedNames = $null
 }
 $directorySearcher.Dispose()
 return $groupDistinguishedNames
}
#-------------------------------------------------------------------------------------------------#
Function Get-GroupType($groupType) {
 if($groupType -eq -2147483646) {
  $groupType = "Global Security Group"
 } elseif($groupType -eq -2147483644) {
  $groupType = "Domain Local Security Group"
 } elseif($groupType -eq -2147483643) {
  $groupType = "BuiltIn Group"
 } elseif($groupType -eq -2147483640) {
  $groupType = "Universal Security Group"
 } elseif($groupType -eq 2) {
  $groupType = "Global Distribution List"
 } elseif($groupType -eq 4) {
  $groupType = "Local Distribution List"
 } elseif($groupType -eq 8) {
  $groupType = "Universal Distribution List"
 } else {
  $groupType = "Unknown Group Type"
 }
 return $groupType
}
#-------------------------------------------------------------------------------------------------#
Function Get-GroupMembers($distinguishedName,$level,$groupsReported) {
 $reportText = @()
 $groupObject = New-Object System.DirectoryServices.DirectoryEntry("GC://" + ($distinguishedName -replace "/","\/"))
 if($groupObject.groupType[0] -ne -2147483640 -or $groupObject.groupType[0] -ne 8) {
  $groupObject = New-Object System.DirectoryServices.DirectoryEntry("LDAP://" + (Get-LocalDomainController(Get-ObjectAdDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
 }
 $directorySearcher = New-Object System.DirectoryServices.DirectorySearcher($groupObject)
 $lastQuery = $false
 $quitLoop = $false
 if($groupObject.member.Count -ge 1000) {
  $rangeStep = 1000
 } elseif($groupObject.member.Count -eq 0) {
  $lastQuery = $true
  $quitLoop = $true
 } else {
  $rangeStep = $groupObject.member.Count
 }
 $rangeLow = 0
 $rangeHigh = $rangeLow + ($rangeStep - 1)
 $level = $level + 2
 while(!$quitLoop) {
  if(!$lastQuery) {
   $attributeWithRange = "member;range=$rangeLow-$rangeHigh"
  } else {
   $attributeWithRange = "member;range=$rangeLow-*"  }
  $directorySearcher.PropertiesToLoad.Clear()
  $directorySearcher.PropertiesToLoad.Add($attributeWithRange) | Out-Null
  $searchResult = $directorySearcher.FindOne()
  $directorySearcher.Dispose()
  if($searchResult.Properties.Contains($attributeWithRange)) {
   foreach($member in $searchResult.Properties.Item($attributeWithRange)) {
    $memberObject = Get-ActiveDirectoryObject $member
    if($memberObject.objectClass -eq "group") {
     $reportText += Format-Group $memberObject $level $groupsReported
    } elseif ($memberObject.objectClass -eq "contact") {
     $reportText += Format-Contact $memberObject $level
    } elseif ($memberObject.objectClass -eq "computer") {
     $reportText += Format-Computer $memberObject $level
    } elseif ($memberObject.objectClass -eq "user") {
     $reportText += Format-User $memberObject $level
    } else {
     Write-Warning "NOT SUPPORTED: $member"
    }
   }
   if($lastQuery) { $quitLoop = $true }
  } else {
   $lastQuery = $true
  }
  if(!$lastQuery) {
   $rangeLow = $rangeHigh + 1
   $rangeHigh = $rangeLow + ($rangeStep - 1)
  }
 }
 return $reportText
}
#-------------------------------------------------------------------------------------------------#
Function Format-User($userObject,$level) {
 $reportText = @()
 if($userObject.displayName) {
  $identity = ((" " * $level) + $userObject.displayName.ToString() + " [" + (Get-ObjectNetBiosDomain $userObject.distinguishedName) + "\" + $userObject.sAMAccountName.ToString() + "]")
 } else {
  $identity = ((" " * $level) + $userObject.name.ToString() + " [" + (Get-ObjectNetBiosDomain $userObject.distinguishedName) + "\" + $userObject.sAMAccountName.ToString() + "]")
 }
 if($userObject.mail) {
  $identity = ("$identity <" + $userObject.mail.ToString() + ">")
 }
 if($userObject.userAccountControl[0] -band 0x0002) {
  $identity = "$identity (User Disabled)"
 } else {
  $identity = "$identity (User Enabled)"
 }
 $reportText += $identity
 $description = ((" " * $level) + "  Description: " + $userObject.description.ToString())
 $reportText += $description
 if($verbose) { Write-Host ($reportText | Out-String) }
 return $reportText
}

Function Format-Contact($contactObject,$level) {
 $reportText = @()
 $identity = ((" " * $level) + $contactObject.displayName.ToString() + " [" + (Get-ObjectNetBiosDomain $contactObject.distinguishedName) + "] <" + $contactObject.mail.ToString() + "> (Contact)")
 $description = ((" " * $level) + "  Description: " + $contactObject.description.ToString())
 $reportText += $identity
 $reportText += $description
 if($verbose) { Write-Host ($reportText | Out-String) }
 return $reportText
}

Function Format-Computer($computerObject,$level) {
 $reportText = @()
    $identity = ((" " * $level) + $computerObject.name + " [" + (Get-ObjectNetBiosDomain $computerObject.distinguishedName) + "\" + $computerObject.sAMAccountName.ToString() + "] (Computer)")
 $operatingSystem = ((" " * $level) + "  OS: " + $computerObject.operatingSystem.ToString())
 $reportText += $identity
    if($computerObject.dNSHostName) {
     $fqdn = ((" " * $level) + "  FQDN: " + ($computerObject.dNSHostName.ToString()).ToLower())
  $reportText += $fqdn
  $reportText += $operatingSystem
    } else {
  $reportText += $operatingSystem
    }
 if($verbose) { Write-Host ($reportText | Out-String) }
 return $reportText
}

Function Format-Group($groupObject,$level,$groupsReported) {
 $reportText = @()
 $identity = ((" " * $level) + $groupObject.name.ToString() + " [" + (Get-ObjectNetBIOSDomain $groupObject.distinguishedName) + "\" + $groupObject.sAMAccountName.ToString() + "]")
 if($groupObject.mail) {
  $identity = ("$identity <" + $groupObject.mail.ToString() + ">")
 }
 $identity = ("$identity (" + (Get-GroupType $groupObject.groupType) + ")")
 $reportText += $identity
 if($verbose) { Write-Host ($reportText | Out-String) }
 if($groupsReported -notcontains $groupObject.distinguishedName) {
  $groupsReported += $groupObject.distinguishedName
  $reportText += Get-GroupMembers $groupObject.distinguishedName.ToString() $level $groupsReported
 } else {
  $reportText += ((" " * $level) + "  [previously reported nested group]")
 }
 $reportText += ""
 return $reportText
}
#-------------------------------------------------------------------------------------------------#
Function Get-AllForestDomains {
 $domains = @()
 $forestInfo = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
 foreach($domain in $forestInfo.Domains) {
  $domains += $domain.name
 }
 return $domains
}

Function Get-DomainDn($domain) {
 return ((New-Object System.DirectoryServices.DirectoryEntry("LDAP://$domain/RootDSE")).defaultNamingContext).ToString()
}

Function Get-ObjectAdDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}

Function Get-ObjectNetBiosDomain($distinguishedName) {
 return ((Get-ObjectAdDomain $distinguishedName).Split(".")[0]).ToUpper()
}

Function Get-LocalDomainController($domain) {
 return ((New-Object System.DirectoryServices.DirectoryEntry("LDAP://$domain/RootDSE")).dnsHostName).ToString()
}

Function Get-ActiveDirectoryObject($distinguishedName) {
 return New-Object System.DirectoryServices.DirectoryEntry("LDAP://" + (Get-LocalDomainController (Get-ObjectAdDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#-------------------------------------------------------------------------------------------------#
Set-Variable -name reportsDirectory -option Constant -value "$pwd\Reports"
Set-Variable -name forestDomains -option Constant -value @(Get-AllForestDomains)
#-------------------------------------------------------------------------------------------------#
if(!([bool]$domain) -or !([bool]$group)) {
 Write-Host ("  Example: .\Export-GroupMembership.ps1 -domain " + $forestDomains[0] + " -group Administrators -verbose") -foregroundcolor Yellow
 Write-Host ""
}

if(!([bool]$domain)) {
 Write-Host " You are missing the `"-domain`" Switch" -Foregroundcolor Red
 Write-Host ""
 Write-Host " The domain of the group."
 Write-Host "  Valid Domains:"
 foreach($forestDomain in $forestDomains) {
  Write-Host ("   $forestDomain [" + ($forestDomain.Split("."))[0] + "]")
 }
 Write-Host ""
 Write-Host " Please enter the Domain below"
 while(!([bool]$domain)) {
  $domain = Read-Host -prompt "`tDomain"
 }
 Write-Host ""
}

if(!([bool]$group)) {
 Write-Host " You are missing the `"-group`" Switch" -Foregroundcolor Red
 Write-Host ""
 Write-Host " Please enter the group name below"
 while(!([bool]$group)) {
  $group = Read-Host -prompt "`tGroup"
 }
 Write-Host ""
}

$validDomain = $false
foreach($forestDomain in $forestDomains) {
 if($forestDomain -eq $domain) {
  $validDomain = $true
  break
 }
 if((($forestDomain.Split("."))[0]) -eq $domain) {
  $validDomain = $true
  break
 }
}

if($validDomain -eq $false) {
 Write-Host ""
 Write-Host "$domain is not a valid domain in your current forest." -foregroundcolor Red
 Write-Host ""
 exit
}

if(!(Test-Path -path $reportsDirectory)) {
 New-Item -path $reportsDirectory -type Directory | Out-Null
}

$groupDistinguishedNames = Find-GroupDistinguishedNamesBysAMAccountName $domain $group

if($groupDistinguishedNames -eq $null) {
 Write-Host "Unable to locate $domain\$group. Exiting..." -foregroundColor Red
 exit
}

foreach($groupDistinguishedName in $groupDistinguishedNames) {
 $groupObject = Get-ActiveDirectoryObject $groupDistinguishedName
 $groupName = $groupObject.name.ToString()
 $groupsAMAccountName = $groupObject.sAMAccountName.ToString()
 $groupDomain = Get-ObjectAdDomain $groupDistinguishedName
 $groupNetBiosDomain = Get-ObjectNetBiosDomain $groupDistinguishedName
 $groupType = Get-GroupType $groupObject.groupType
 $outputFilename = ($groupDomain + "-" + $groupsAMAccountName + ".txt")
 $reportText = @()
 $reportText += ("#" + ("-" * 78) + "#")
 $reportText += "  Members of $groupName [$groupNetBiosDomain\$groupsAMAccountName]"
 $reportText += ("  Description: " + $groupObject.description.ToString())
 if($groupObject.mail -and $groupType -match "Distribution") {
  $reportText += "  Distribution List E-Mail Address: " + $groupObject.mail.ToString()
 } elseif($groupObject.mail) {
  $reportText += "  Security Group E-Mail Address: " + $groupObject.mail.ToString()
 }
 $reportText += "  Group Type: $GroupType"
 $reportText += ("  Report generated on " + (Get-Date -format D) + " at " + (Get-Date -format T))
 $reportText += ("#" + ("-" * 78) + "#")
 if($verbose) { Write-Host ($reportText | Out-String) }
 $reportText += Get-GroupMembers $groupDistinguishedName 0 @()
 $reportText += ("#" + ("-" * 78) + "#")
 if($verbose) { Write-Host ("#" + ("-" * 78) + "#") }
 Set-Content -path "$reportsDirectory\$outputFilename" -value ($reportText | Out-String)
}

Thursday, March 3, 2011

Quick Organizational Chart from Active Directory

Building on my blog post, Export Manager's Nested Direct Reports, I quickly threw together a revision of the code that will create visual (ASCII at least) organizational chart based on the directReports attribute of an employee in Active Directory. The nesting method I am using could easy be converted to simulate the "tree" DOS command using Get-ChildObject on file systems or about any programming challenge that deals with nested objects. The one main fault of the code, is that it is not pretty when it gets to the last nested object of a parent. It leaves the "|" in front of the last nested subordinate object. A little more work and I could clean this presentation issue, but not today.
Function Get-DirectReports($distinguishedName,$level) {
 $managerObject = Get-ActiveDirectoryObject $distinguishedName
 if($managerObject.directReports.count -gt 0) {
  foreach($directReport in $managerObject.directReports) {
   $directReportObject = Get-ActiveDirectoryObject $directReport
   Write-Output (("|  " * $level) + "|")
   Write-Output (("|  " * $level) + "+-" + ($directReportObject.givenName).ToString() + " " + ($directReportObject.sn).ToString())
   Get-DirectReports $directReport ($level + 1)
  }
 }
}

Function Get-LocalDomainController($objectDomain) {
 return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
   
Function Get-ObjectADDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
   
Function Get-ActiveDirectoryObject($distinguishedName) {
 return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
#--------------------------------------------------------------------------------------------------#
$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectConnection.Open("Provider=ADsDSOObject;")
$objectCommand.ActiveConnection = $objectConnection

$manager = "the.ceo@mycompany.local"
 
$ldapBase = "GC://$forestRootDn"
$ldapAttr = "distinguishedName"
$ldapScope = "subtree"
$ldapFilter = "(&(objectClass=user)(proxyAddresses=smtp:$manager))"
$ldapQuery = "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
$objectCommand.CommandText = $ldapQuery
$objectRecordSet = $objectCommand.Execute()
 
while(!$objectRecordSet.EOF) {
 $firstLevelObject = Get-ActiveDirectoryObject $objectRecordSet.Fields.Item('distinguishedName').Value
 $topLevelManager = (($firstLevelObject.givenName).ToString() + " " + ($firstLevelObject.sn).ToString())
 Write-Output $topLevelManager
 Get-DirectReports $firstLevelObject.distinguishedName 0
 $objectRecordSet.MoveNext()
}

Wednesday, February 23, 2011

Export Manager's Nested Direct Reports

In the code sample below, I create an export in two flavors for the nested direct reports of managers. By taking a text file listing the e-mail addresses of user objects in a forest, this code proceeds to drill recursively through the mutli-valued string directReports attribute of the object until it reaches a nested user object with that attribute set to null. After obtaining all the distinguished names, the code proceeds to perform LDAP lookups to obtain relevant attribute data. At the conclusion, it saves two files, a comma separated values text file with all the relevant attributes and an HTML file with a subset of those attributes.

The concept for this script was done before PowerShell V2 was released and provided better typing including 64 bit integers. Like in my older Perl and VBScript code dealing with Active Directory attributes that stored 64 bit integers like accountExpires, it required manipulating the return data in order to perform comparisons. The code below contains a Function written by Adam Weigert which has saved me the time translating the same conversion I've done before in Perl and VBScript. A link to his blog entry on the topic is commented in the code for your review.
param([string]$file)

Function Get-DirectReports($objectDn, $distinguishedNames) {
 $distinguishedNames += $objectDn
 $managerObject = Get-ActiveDirectoryObject $objectDn
 if($managerObject.directReports.count -gt 0) {
  foreach($directReport in $managerObject.directReports) {
   $distinguishedNames = Get-DirectReports $directReport $distinguishedNames
  }
 }
 return $distinguishedNames
}
Function Read-UserStatus($userFlags) {
 if ($userFlags -band 0x0002) {
  $enabled = $false
    } else {
  $enabled = $true
 } 
 return $enabled
}
Function Convert-ADSLargeInteger([object]$adsLargeInteger) { # http://weblogs.asp.net/adweigert/archive/2007/03/23/powershell-convert-active-directory-iadslargeinteger-to-system-int64.aspx
 $highPart = $adsLargeInteger.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)
 $lowPart  = $adsLargeInteger.GetType().InvokeMember("LowPart",  [System.Reflection.BindingFlags]::GetProperty, $null, $adsLargeInteger, $null)

 $bytes = [System.BitConverter]::GetBytes($highPart)
 $temp   = [System.Byte[]]@(0,0,0,0,0,0,0,0)
 [System.Array]::Copy($bytes, 0, $temp, 4, 4)
 $highPart = [System.BitConverter]::ToInt64($temp, 0)

 $bytes = [System.BitConverter]::GetBytes($lowPart)
 $lowPart = [System.BitConverter]::ToUInt32($bytes, 0)
 
 return $lowPart + $highPart
}
Function Get-LocalDomainController($objectDomain) {
 return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
  
Function Get-ObjectADDomain($distinguishedName) {
 return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
  
Function Get-ActiveDirectoryObject($distinguishedName) {
 return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
#--------------------------------------------------------------------------------------------------#
if(![bool]$file) {
 Write-Host "You are missing the `"-file`" command line argument" -foregroundColor Red
 Write-Host ""
 Write-Host "This is the file that contains the list of employee e-mail addresses"
 Write-Host ""
 while((![bool]$file)) {
  $file = Read-Host -prompt "`tFile"
 }
 Write-Host ""
}
if(!(Test-Path -path $file)) {
 Write-Host "Unable to locate: $file" -foregroundColor Red
 Write-Host ""
 Write-Host "Please review the correct path"
 Write-Host ""
 while(!(Test-Path -path $file)) {
  $file = Read-Host -prompt "`tFile"
 }
 Write-Host ""
}
   
$answer = $null
while($answer -ne "Y" -and $answer -ne "N") {
 $answer = Read-Host -prompt "Are you sure you want to use $file (Y/N)?"
}
   
if($answer -eq "N") {
 Write-Host ""
 Write-Host "Quiting..." -foregroundColor Yellow
 exit
}
 
$outputFile = (((Get-Item -path $file).name).Replace(((Get-Item -path $file).extension),".csv"))
 
$managers = Get-Content -path $file

$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectConnection.Open("Provider=ADsDSOObject;")
$objectCommand.ActiveConnection = $objectConnection

$distinguishedNames = @()
$users = @()

foreach($manager in $managers) {

 $ldapBase = "GC://$forestRootDn"
 $ldapAttr = "distinguishedName"
 $ldapScope = "subtree"
 $ldapFilter = "(&(objectClass=user)(ProxyAddresses=smtp:$manager))"
 $ldapQuery= "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
 $objectCommand.CommandText = $ldapQuery
 $objectRecordSet = $objectCommand.Execute()

 while(!$objectRecordSet.EOF) {
  $distinguishedNames = Get-DirectReports $objectRecordSet.Fields.Item('distinguishedName').Value $distinguishedNames
  $objectRecordSet.MoveNext()
 }
}

foreach($distinguishedName in $distinguishedNames) {
 $userObject = Get-ActiveDirectoryObject $distinguishedName

 if($userObject.objectClass -eq "User") {
  $accountExpires = (Convert-ADSLargeInteger $userObject.accountExpires[0])

  if($accountExpires -ne 0 -and $accountExpires -ne 9223372036854775807) {
   $account_expiration = (Get-Date ($userObject.psbase.invokeget("AccountExpirationDate")) -Format d)
  } else {
   $account_expiration = "N/A"
  }
 
  if($userObject.manager) {
   $manager_object = Get-ActiveDirectoryObject $userObject.manager
   if($manager_object.displayName) {
    $manager = ($manager_object.displayName).ToString()
   } else {
    $manager = ($manager_object.name).ToString()
   }
  } else {
   $manager = "N/A"
  }
  
  $user = New-Object -typeName PSObject
  Add-Member -inputObject $user -type NoteProperty -name "domain" -value ((Get-ObjectADDomain $userObject.distinguishedName).Split(".")[0]).ToUpper()
  Add-Member -inputObject $user -type NoteProperty -name "sAMAccountName" -value (($userObject.sAMAccountName).ToString()).ToLower()
  Add-Member -inputObject $user -type NoteProperty -name "givenName" -value ($userObject.givenName).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "sn" -value ($userObject.sn).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "displayName" -value ($userObject.displayName).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "mail" -value ($userObject.mail).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "title" -value ($userObject.title).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "department" -value ($userObject.department).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "manager" -value $manager
  Add-Member -inputObject $user -type NoteProperty -name "streetAddress" -value (($userObject.streetAddress).ToString() -Replace "`r`n",", ")
  Add-Member -inputObject $user -type NoteProperty -name "l" -value ($userObject.l).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "st" -value ($userObject.st).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "postalCode" -value ($userObject.postalCode).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "c" -value ($userObject.c).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "telephoneNumber" -value ($userObject.telephoneNumber).ToString()
  Add-Member -inputObject $user -type NoteProperty -name "accountExpiration" -value $account_expiration
  Add-Member -inputObject $user -type NoteProperty -name "enabled" -value (Read-UserStatus $userObject.userAccountControl[0])
  Add-Member -inputObject $user -type NoteProperty -name "organizationalUnit" -value ($userObject.psBase.parent.distinguishedName).ToString()
  $users += $user
 }
}

$users | Export-Csv -path $outputFile -noTypeInformation
$users | ConvertTo-Html -property givenName,sn,mail,telephoneNumber,title,department -title "Important Contacts" > "contacts.html"