# encoding: ascii
# api: powershell
# title: Search-AD
# description: Search-AD without need to install Quest Active Role commandlets or install Active Directory Management Gateway.
# version: 0.1
# type: function
# author: P Sczepanski
# license: CC0
# function: Test-ObjectPath
# x-poshcode-id: 3788
# x-archived: 2016-03-25T09:39:00
# x-published: 2013-11-26T14:21:00
#
# You need to “load” the function using . sourcing.
# ie. Safe it as Search-AD.ps1 and load the functions into global using “. .\Search-Search-AD.ps1”. Then use get-help Search-AD for help
#
# add enumerations and types required for functions
Add-Type @'
using System;
namespace redtoo {
namespace AD {
public enum Provider : int {
LDAP,
GC,
}
}
public class ADsPathPart {
public string Provider;
public string Server;
public string BaseDN;
public string RDN;
public string DCComponent;
public string Parent;
public string DistinguishedName;
}
}
'@
function Test-ObjectPath {
<#
.SYNOPSIS
Tests if an Object in AD exists.
.DESCRIPTION
Tests if an Object in AD exists. Valid inputs are a DN or a DirectoryEntry object.
With parameter you can force an imput type. Without a parameter the script figures out if an input was a string or directoryEntry object.
.PARAMETER DistinguishedName
The distinguished name of the object to be tested. The DN may be specified using aDSPath or distinguished name.
If an incorrect DN syntax is supplied, it will return $false
.PARAMETER DirectoryEntry
A DirectoryEntry object to be tested.
If a string value is passed, it will be casted to a DirectoryEntry.
.PARAMETER isValid
Verifies that the syntax of the DN is correct.
Returns $true if valid and otherwise $false
.PARAMETER PassThru
Returns the [DirectoryServices.DirectoryEntry] object if object exists, otherwise returns false
.PARAMETER AsDN
Use together with -PassThru. Returns distinguished path as String
.PARAMETER AsADsPath
Use together with -PassThru. Returns ADsPath as String
.PARAMETER $SplitParts
Returns a PowerShell object with the different LDAP URI parts
.EXAMPLE
PS C:\> Test-ObjectPath "CN=myOU,DC=test,DC=intra"
-------------------------------------------------------
Test if an object with the DN CN=myOU,DC=test,DC=intra exists.
.EXAMPLE
PS C:\> Test-ObjectPath "CN=myOU,DC=test,DC=intra" -isValid
-------------------------------------------------------
Test if the DN CN=myOU,DC=test,DC=intra is correct.
.EXAMPLE
PS C:\> Test-ObjectPath -DirectoryEntry ([DirectoryServices.DirectoryEntry]"LDAP://CN=myOU,DC=test,DC=intra"
-------------------------------------------------------
Test if an object represented by the directoryEntry of CN=myOU,DC=test,DC=intra exists
.EXAMPLE
PS C:\> Test-ObjectPath "LDAP://CN=myOU,DC=test,DC=intra"
-------------------------------------------------------
Test if an object with the DN CN=myOU,DC=test,DC=intra exists.
.EXAMPLE
PS C:\> Test-ObjectPath ([DirectoryServices.DirectoryEntry]"LDAP://CN=myOU,DC=test,DC=intra"
-------------------------------------------------------
Test if an object represented by the directoryEntry of CN=myOU,DC=test,DC=intra exists
.INPUTS
System.String,DirectoryServices.DirectoryEntry
.OUTPUTS
System.Bool
.Notes
NAME: Test-ObjectPath
AUTHOR: Patrick Sczepanski (patrick@sczepanski.com)
Version: 20120716
#Requires -Version 2.0
#>
[Cmdletbinding(DefaultParameterSetName="String")]
Param(
[Alias('dn')]
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$True,Position=0,ParameterSetName="String")]
[string]$DistinguishedName,
[Parameter(ParameterSetName="String")]
[switch]$isValid,
[Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$True,Position=0,ParameterSetName="DirectoryEntry")]
[System.DirectoryServices.DirectoryEntry]
$DirectoryEntry,
# A System.DirectoryServices.SearchResult object representing the group to list the members from
# You may use Search-AD to find the group to be listed
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$True,Position=0,ParameterSetName="SearchResult")]
[AllowNull()]
[DirectoryServices.SearchResult]
$SearchResult,
[Parameter(ParameterSetName="String")]
[string]
$connection,
[Parameter()]
[switch]
$Passthru,
[Parameter()]
[switch]
$AsDN,
[Parameter()]
[switch]
$AsADsPath,
[Parameter()]
[switch]
$SplitParts
)
Begin {
$ThisFunctionName = $MyInvocation.MyCommand.Name
# the function uses different return points due to multiple decissions taken
# it seemed easier to do it this way than try to collect all variations and evaluate again at the end
if( ( $PSBoundParameters.ContainsKey("AsDN") -or $PSBoundParameters.ContainsKey("AsADsPath") -or $PSBoundParameters.ContainsKey("SplitParts") ) -and (-not $PSBoundParameters.ContainsKey("Passthru") ) ) {
Write-Warning "[$ThisFunctionName] :: You need to specify -Passthru if you want to use -AsDN and -AsADsPath"
}
function Split-ADSPath ( $ADSPath ) {
if ( $ADSPath -match '^(?:(?:(?<Provider>LDAP|GC)://)(?:(?<DC>(?:[\w\.-])+)/)*)+(?<Base>(?:(?:URI|CN|OU)=.*?,)*)(?<DCComponent>(?:DC=.*)+)$' ) {
# Removed " else { $null }"
New-Object redtoo.ADsPathPart -Property @{
"Provider" = if ($Matches.Contains("Provider") ) { $Matches.Provider };
"Server" = if ($Matches.Contains("DC") ) { $Matches.DC };
"BaseDN" = if ($Matches.Contains("Base") ) { $Matches.base -replace ",$","" };
"RDN" = if ($Matches.Contains("Base") ) { ($Matches.base.split(","))[0] };
"DCComponent" = if ($Matches.Contains("DCComponent") ) { $Matches.DCComponent };
"Parent" = if ($Matches.Contains("Provider") ) { ($Matches.base.split(",", [StringSplitOptions]::RemoveEmptyEntries) | select -Skip 1) -join "," };
"DistinguishedName" = if ($Matches.Contains("Base") -and `
$Matches.Contains("DCComponent") ) { "{0}{1}" -f $Matches.base, $Matches.DCComponent };
}
} else {
Write-Warning "[$ThisFunctionName] :: Failed to match LDAP Path: '$LDAPPath'"
}
}
}
Process {
switch ($pscmdlet.ParameterSetName) {
"String" {
if ( $connection ) {
$connection = $connection + "/"
} else {
$connection = ""
}
switch -regex ( $DistinguishedName ) {
#'^((?<Provider>(LDAP|GC)://)(?<DC>([\w\.-])+/)*)+(?<Base>((URI|CN|OU)=.*?,)*)(?<DCComponent>(DC=.*)+)$'
'^(((LDAP|GC)://)(([\w\.-])+/)*)+((UID|CN|OU)=.*?,)*(DC=.*)+$' {
$LDAPpath = $DistinguishedName
break
}
'^(([\w\.-])+/)*((UID|CN|OU)=.*?,)*(DC=.*)+$' {
# matching dn without machine or domain.
# Allows adding a machine name using -connection which is otherwise ignored
$LDAPpath = "LDAP://$connection$DistinguishedName"
break
}
'^WinNT://(?<Computer>\w.*|\.)/(?<Account>\w.*)$' {
# match WinNT provider such as "WinNT://mycomputer/jdoe,user"
$LDAPpath = $DistinguishedName
break
}
Default { return $false }
}
if ( $isValid ) {
# only verified syntax
return $true
}
$exists = try { [DirectoryServices.DirectoryEntry]::exists("$LDAPpath") } catch { $false }
if ( $exists ) {
if ( $Passthru ) {
if ( $AsDN ) {
return ([regex]::Match($LDAPpath,".*/(.*)")).Groups[1].Value
} elseif ( $AsADsPath ) {
return $LDAPpath
} elseif ( $SplitParts ) {
Split-ADSPath $LDAPpath
} else {
return [DirectoryServices.DirectoryEntry]"$LDAPpath"
}
} else {
return $true
}
} else {
return $false
}
break
}
"DirectoryEntry" {
if ( [string]::IsNullOrEmpty( ( $DirectoryEntry | Get-Member -MemberType Property ) ) ) {
return $false
} else {
if ( $Passthru ) {
if ( $AsDN ) {
return ([regex]::Match($DirectoryEntry.adspath,".*/(.*)")).Groups[1].Value
} elseif ( $AsADsPath ) {
return $DirectoryEntry.adspath
} elseif ( $SplitParts ) {
Split-ADSPath $DirectoryEntry.adspath
} else {
return $DirectoryEntry
}
} else {
return $true
}
}
break
}
"SearchResult" {
if ( [string]::IsNullOrEmpty( $SearchResult ) ) {
return $false
} else {
$DN = $SearchResult.Properties.adspath
if ( $Passthru ) {
if ( $AsDN ) {
return ([regex]::Match($SearchResult.Properties.adspath[0],".*/(.*)")).Groups[1].Value
} elseif ( $AsADsPath ) {
return $SearchResult.Properties.adspath[0]
} elseif ( $SplitParts ) {
Split-ADSPath ( $SearchResult.Properties.adspath[0] )
} else {
return [DirectoryServices.DirectoryEntry]$SearchResult.Properties.adspath
}
} else {
return $true
}
break
}
}
}
}
End {
}
}
function Search-AD {
<#
.Synopsis
Searching Active Directory
.Description
Searching Active Directory
.Parameter Provider
Provider to use to connect. Allowed are GC and LDAP
Default: LDAP
.Parameter Connection
Optional domain controller name used to connect to execute the search
Default: Any (closest)
.Parameter Forest
Use forest root DN as default searchbase, GC as default provider
and set the default scope to subtree
.Parameter Domain
Use current domain root DN as default searchbase
and set the default scope to subtree
.Parameter Searchbase
Distinguished name of the searchbase to start the search from
Default: Current Domain
.Parameter Filter
LDAP filter
Default: (objectclass=*)
.Parameter Attributes
Attributes to be returned
Default distinguishedname
.Parameter Scope
Search scope. Allowed scopes are base, onelevel, subtree
Default: base
.Parameter PageSize
Number of objects returned per page. In standard AD user a number below 1000 which is the default maximum object returned in one step
Default: 1000
.Parameter SizeLimit
Maximum number of objects returned. Enter 0 for unlimited numer
Default: 1000
.Parameter ChooseItem
Allows to choose a single item out of multiple items returned by the search
.Parameter PropertyNamesOnly
Returns only the property names without values
.Parameter FindOne
Returns the first object matching
.Example
$Group = Search-AD -connection "DC1.mydomain.com" -filter "(&(objectcategory=group)(samaccountname=group51)))" -searchbase "DC=mydomain,DC=com" -scope "subtree"
.OUTPUTS
[System.DirectoryServices.SearchResultCollection]
.INPUTS
NA
.Link
Search-AD
.Notes
NAME: Search-AD
AUTHOR: Patrick Sczepanski (patrick@sczepanski.com)
Version: 20120709
#Requires -Version 2.0
#>
[Cmdletbinding(DefaultParameterSetName="DN")]
Param (
[Parameter(ValueFromPipelineByPropertyName=$true,Position=0)]
[redtoo.AD.Provider]$Provider = [redtoo.AD.Provider]::LDAP,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[string]
$Connection,
[Alias('base','b')]
[Parameter(ValueFromPipelineByPropertyName=$true,Position=0,ParameterSetName="DN")]
[ValidateScript( {Test-ObjectPath $_ -IsValid} )]
[string]
$Searchbase = ([DirectoryServices.DirectoryEntry]"LDAP://RootDSE").DefaultNamingContext,
[Parameter(ValueFromPipelineByPropertyName=$true,ParameterSetName="Domain")]
[switch]
$DomainBase,
[Parameter(ValueFromPipelineByPropertyName=$true,ParameterSetName="Forest")]
[switch]
$ForestBase,
[Alias('f')]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[string]
$Filter = "(objectclass=*)",
[Alias('a','attribute','attrib')]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[string[]]
$Attributes,
[Alias('s')]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[DirectoryServices.SearchScope]
$Scope = "base",
[Parameter(ValueFromPipelineByPropertyName=$true)]
[int32]$PageSize = 1000,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[int32]$SizeLimit = 1000,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[switch]$PropertyNamesOnly,
[Alias('choose','select','c')]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[switch]
$ChooseItem,
[Alias('One')]
[switch]
$FindOne,
[switch]
$CountOnly,
[Alias('Chasing')]
[Parameter(ValueFromPipelineByPropertyName=$true)]
[DirectoryServices.ReferralChasingOption]
$ReferralChasing = [DirectoryServices.ReferralChasingOption]::None,
[DirectoryServices.SecurityMasks]
$SecurityMasks = [DirectoryServices.SecurityMasks]::Dacl
)
Begin {
$ThisFunctionName = $MyInvocation.MyCommand.Name
$RootDSE = [DirectoryServices.DirectoryEntry]"LDAP://RootDSE"
if ( ! $RootDSE ) {
Write-Warning "[$ThisFunctionName] :: You are not connected to AD. This function can only be used within an AD Forest."
Return $null
}
}
Process {
switch ( $psCmdlet.ParameterSetName ) {
"Forest" {
$SearchBase = $RootDSE.rootDomainNamingContext
if ( ! $PSBoundParameters.ContainsKey("Provider") ) {
$Provider = [redtoo.AD.Provider]::GC;
}
if ( ! $PSBoundParameters.ContainsKey("Scope") ) {
$Scope = [DirectoryServices.SearchScope]::Subtree
}
}
"Domain" {
$SearchBase = $RootDSE.defaultNamingContext
if ( ! $PSBoundParameters.ContainsKey("Scope") ) {
$Scope = [DirectoryServices.SearchScope]::Subtree
}
}
}
# Normalize the searchbase
# 1. Split it apart
$SearchBaseParts = Test-ObjectPath $SearchBase -passthru -SplitParts
# 2. Get the DC which could resolve the name to allow cross domain searches
if ( $Connection ) {
$SearchBaseParts.Server = $Connection
} else {
# get a DC of the domain on which the object is located
# Taking the DN of the object, getting its parent domain, replacing DC= to make a DNS name which is then used to get the domain context
$DomainDNS = ( $SearchBaseParts.DCComponent ) -replace "DC=","" -replace ",","."
$SearchBaseParts.Server = ( [adsi]"LDAP://$DomainDNS/RootDSE" ).dnsHostName
}
if ( $Provider ) {
$SearchBaseParts.Provider = $Provider
}
# Finally merge information again
$SearchBase = "{0}://{1}/{2}" -f $SearchBaseParts.Provider, $SearchBaseParts.Server, $SearchBaseParts.DistinguishedName
if ( $attributes -notcontains "distinguishedname" ) {
$attributes += "distinguishedname"
}
if ( $CountOnly ) {
$PropertyNamesOnly = $true
$cacheResults = $false
} else {
$cacheResults = $true
}
[DirectoryServices.DirectoryEntry]$searchbaseURI = $SearchBase
[DirectoryServices.DirectorySearcher]$Searcher = new-object DirectoryServices.DirectorySearcher($searchbaseURI)
if ( $attributes -match "NTSecurityDescriptor" ) {
$Searcher.SecurityMasks = $SecurityMasks
}
$Searcher.filter = $filter
$Searcher.CacheResults = $cacheResults
$Searcher.SearchScope = $scope
$Searcher.PageSize = $PageSize
$Searcher.PropertyNamesOnly = $PropertyNamesOnly
$Searcher.PropertiesToLoad.AddRange($attributes)
$Searcher.ReferralChasing = $ReferralChasing
try {
if ( $FindOne ) {
[System.DirectoryServices.SearchResult]$result = $searcher.FindOne()
} else {
[System.DirectoryServices.SearchResultCollection]$result = $searcher.FindAll()
}
}
catch {
Write-Warning "[$ThisFunctionName] :: $_`nBase: $SearchBase`nFilter: $filter`nSearchScope: $scope`nAttributes: $Attributes "
return $null
}
switch ( @(,$result) ) {
{ ($_ -is [System.DirectoryServices.SearchResultCollection]) -and ($_.count -lt 1) } { Write-Debug "[Search-AD] :: Cannot find an object in $searchbase using filter $filter"
if ( $CountOnly ) {
return 0
} else {
return $null
}
break }
{ ($_ -is [System.DirectoryServices.SearchResult] -or ($_.count -eq 1)) } { Write-Debug "[Search-AD] :: Found 1 Object"
if ( $CountOnly ) {
return 1
} else {
return $result
}
break }
{ ($_ -is [System.DirectoryServices.SearchResultCollection]) -and ($_.count -gt 1) } {
if ( $CountOnly ) {
return $_.count
} else {
if ( $chooseitem ) {
[int]$count = -1
foreach($object in $result) {
$count = $count + 1
Write-Host "[$count]: " $object.Properties.distinguishedname
}
$selection = Read-Host "Please select object"
if ( $($selection -lt 0) -or $($selection -gt $count) -or $($count -isnot [int]) ) { Write-Error "Selection '$selection' out of scope.`r`nPlease enter an integer value between 0 and $count."; exit(0) }
return $result[$selection]
} else {
return $result
}
}
break }
default { Write-Error -message "[$ThisFunctionName] :: Issue with switch statement. Please check code. Unexpected Error."; }
}
}
End {
}
}