A small collection specialised scripts for Active Directory.
Includes:
- Compare-ADMemberOf
- Get-ADSystemInfo
- Get-GroupMemberTree
- Get-LdapObject
- Get-MemberOfTree
- Test-LdapSslConnection
- Get-ADSchemaAttribute
A small collection specialised scripts for Active Directory.
Includes:
function Compare-ADMemberOf { | |
param ( | |
# A filter which defines the set of objects to compare. | |
[Parameter(Mandatory)] | |
[string]$Filter, | |
# Sort alphatically by default, otherwise sort by most common group (then alphabetically) | |
[ValidateSet('Alphabetical', 'MostCommon')] | |
[string]$SortBy = 'Alphabetical' | |
) | |
# An arbitrary query to define a set of users to compare | |
$results = Get-ADUser -Filter $Filter | ForEach-Object { | |
$dn = $_.DistinguishedName | |
$entry = [PSCustomObject]@{ | |
Name = $_.Name | |
} | |
# Get the list of groups this object belongs to. | |
Get-ADGroup -Filter { member -eq $dn } | ForEach-Object { | |
$entry | Add-Member $_.Name 'x' -Force | |
} | |
$entry | |
} | |
# Give everything the same list of Groups (as noteproperty members) | |
$allGroups = $results | | |
ForEach-Object { $_.PSObject.Properties | Where-Object Name -ne Name | ForEach-Object Name } | | |
Sort-Object | | |
Select-Object -Unique | |
$allGroups = switch ($SortBy) { | |
'Alphabetical' { $allGroups } | |
'MostCommon' { | |
$results | | |
ForEach-Object { $_.PSObject.Properties } | | |
Where-Object Name -ne Name | | |
Group-Object Name -NoElement | | |
Sort-Object @{Expression = 'Count'; Descending = $true}, Name | | |
Select-Object -ExpandProperty Name | |
} | |
} | |
$results | Select-Object (@('Name') + $allGroups) | |
} |
function Get-ADSchemaAttribute { | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory, Position = 1, ValueFromPipeline)] | |
[String]$Name | |
) | |
process { | |
$params = @{ | |
Filter = 'objectClass -eq "classSchema" -and ldapDisplayName -eq $Name' | |
SearchBase = (Get-ADRootDSE).SchemaNamingContext | |
Properties = @( | |
'mayContain' | |
'systemMayContain' | |
'systemMustContain' | |
'systemAuxiliaryClass' | |
'auxiliaryClass' | |
'subClassOf' | |
) | |
} | |
$class = Get-ADObject @params | |
$class.mayContain | |
$class.systemMayContain | |
$class.systemMustContain | |
foreach ($attributeName in 'auxiliaryClass', 'systemAuxiliaryClass') { | |
if ($class.$attributeName.Count -gt 0) { | |
$class.$attributeName | Get-ADSchemaAttribute | |
} | |
} | |
if ($class.subClassOf -and $class.Name -ne $class.subClassOf) { | |
$class.subClassOf | Get-ADSchemaAttribute | |
} | |
} | |
} |
function Get-ADSystemInfo { | |
<# | |
.SYNOPSIS | |
Rewrites properties and methods associated with the ComObject | |
.DESCRIPTION | |
Get-ADSystemInfo adds members to the ADSystemInfo COM Object improving the accessibility of each property and method (greatly simplifying syntax). | |
.EXAMPLE | |
Get-ADSystemInfo | |
.EXAMPLE | |
(Get-ADSystemInfo).GetAnyDCName() | |
.EXAMPLE | |
(Get-ADSystemInfo).GetDCSiteName("A-DC") | |
.EXAMPLE | |
Get-ADSystemInfo | Get-Member | |
#> | |
[CmdletBinding()] | |
param( ) | |
# Dynamically enumerating these is really hard. Hard-coded. | |
$properties = 'ComputerName', | |
'DomainDnsName', | |
'DomainShortName', | |
'ForestDnsName', | |
'IsNativeMode', | |
'PDCRoleOwner', | |
'SchemaRoleOwner', | |
'SiteName', | |
'UserName' | |
# Methods excluding GetDCSiteName, requires a single hostname as an argument | |
$methodsWithoutArgs = 'GetAnyDCName', | |
'GetTrees', | |
'RefreshSchemaCache' | |
# Create ComObject as the basic return value | |
$adSystemInfo = New-Object -ComObject ADSystemInfo | |
# Add each of the known properties as a NoteProperty to the base object | |
foreach ($property in $properties) { | |
$adSystemInfo | Add-Member $property ([__ComObject].InvokeMember($property, 'GetProperty', $null, $adSystemInfo, $null)) | |
} | |
# Create a ScriptMethod caller for each of the known methods and add each to the base object | |
foreach ($method in $methodsWithoutArgs) { | |
$adSystemInfo | Add-Member -Name $method -Type ScriptMethod -Value ( | |
[ScriptBlock]::Create( | |
'[__ComObject].InvokeMember("{0}", "InvokeMethod", $null, $this, $null)' -f $method | |
) | |
) | |
} | |
# Add GetDCSiteName taking a single (first) argument via $args[0] | |
$adSystemInfo | Add-Member -Name GetDCSiteName -Type ScriptMethod -Value { | |
[__ComObject].InvokeMember('GetDCSiteName', 'InvokeMethod', $null, $this, $args[0]) | |
} | |
# Return the updated object | |
$adSystemInfo | |
} |
function Get-GroupMemberTree { | |
<# | |
.SYNOPSIS | |
Get members of a group and present output as a tree. | |
.DESCRIPTION | |
A recursive function which uses repeated ADSI searches to build a member tree. | |
#> | |
[CmdletBinding(DefaultParameterSetName = 'ManualSearchRoot')] | |
param ( | |
# A DN or SamAccountName used to start the search. | |
[Parameter(Mandatory = $true, Position = 1)] | |
[String]$Identity, | |
# The root of the current domain by default. A fixed value can be supplied if required. Note that the search root is also used to locate the suer if a DN is not supplied. | |
[Parameter(ParameterSetName = 'ManualSearchRoot', Position = 2)] | |
[String]$SearchRoot = (([ADSI]'LDAP://RootDSE').defaultNamingContext[0]), | |
# Use a Global Catalog to search instead of LDAP (used for forest-wide searches). | |
[Alias('GlobalCatalog')] | |
[Switch]$GC, | |
# Sets the SearchRoot value to the forest root domain taken from RootDSE. | |
[Parameter(Mandatory = $true, ParameterSetName = 'AutomaticForestSearchRoot')] | |
[Switch]$UseForestRoot, | |
# The character to use to indent values. | |
[Parameter(Position = 4)] | |
[String]$IndentChar = ' ', | |
# The starting indent level (repetition of the IndentCharacter value). | |
[Parameter()] | |
[UInt32]$IndentLevel = 0, | |
[System.Collections.Generic.HashSet[String]]$loopPrevention = (New-Object System.Collections.Generic.HashSet[String]) | |
) | |
if ((Get-PSCallStack)[1].InvocationInfo.InvocationName -ne $myinvocation.InvocationName) { | |
'{0}{1}' -f ($IndentChar * $IndentLevel), $Identity | |
$IndentLevel++ | |
} | |
$protocol = 'LDAP' | |
# Switch the protocol if the GC switch parameter is used. | |
if ($GC) { | |
$protocol = 'GC' | |
} | |
if ($UseForestRoot) { | |
$SearchRoot = ([ADSI]'LDAP://RootDSE').rootDomainNamingContext[0] | |
} | |
$searcher = [ADSISearcher]('{0}://{1}' -f $Protocol, $SearchRoot) | |
$searcher.PageSize = 1000 | |
$searcher.PropertiesToLoad.AddRange(@('name', 'distinguishedName', 'objectClass')) | |
# If the value passed as identity is not an object DN or a GUID treat the value as a sAMAccountName | |
# and execute a search using the SearchRoot and GC parameters. | |
$guid = [Guid]::NewGuid() | |
if ([Guid]::TryParse($Identity, [Ref]$guid)) { | |
$guidHex = $guid.ToByteArray() | ForEach-Object { $_.ToString('X2') } | |
$filter = '(objectGuid=\{0})' -f ($guidHex -join '\') | |
} elseif ($Identity -notmatch '^CN=.+(?:DC=w+){1,}') { | |
$filter = '(|(sAMAccountName={0})(userPrincipalName={0}))' -f $Identity | |
} | |
# Attempt to resolve the identity to a DN | |
if ($filter) { | |
try { | |
$searcher.Filter = $filter | |
$searchResult = $searcher.FindOne() | |
if ($searchResult) { | |
$Identity = $searchResult.Properties['distinguishedName'][0] | |
} | |
} catch { | |
$pscmdlet.ThrowTerminatingError($_) | |
} | |
} | |
try { | |
$searcher.Filter = '(memberOf={0})' -f $Identity | |
$searcher.FindAll() | ForEach-Object { | |
'{0}{1}' -f ($IndentChar * $IndentLevel), $_.Properties['name'][0] | |
if (@($_.Properties['objectClass'])[-1] -eq 'group') { | |
$psboundparameters.Identity = $_.Properties['distinguishedName'][0] | |
$psboundparameters.IndentLevel = $IndentLevel + 1 | |
$psboundparameters.LoopPrevention = $loopPrevention | |
if ($loopPrevention.Contains($psboundparameters.Identity)) { | |
Write-Debug ('Triggered loop avoidance: {0}' -f $_.Properties['distinguishedName'][0]) | |
} else { | |
$null = $loopPrevention.Add($psboundparameters.Identity) | |
Get-GroupMemberTree @psboundparameters | |
} | |
} | |
} | |
} catch { | |
throw | |
} | |
} |
Add-Type -Assembly System.DirectoryServices.Protocols | |
function Get-LdapObject { | |
<# | |
.SYNOPSIS | |
An LDAP search function using System.DirectoryServices.Protocols. | |
.DESCRIPTION | |
Get-ADObject uses System.DirectoryServices.Protocols to execute searches against an LDAP directory. | |
Return values are, in most cases, raw and comparitively complex to work with. This function is written for speed and flexibility over ease of use. | |
Get-ADObject has only been tested against Active Directory. | |
.EXAMPLE | |
C:\PS> $RootDSE = Get-LdapObject -SearchScope Base @Params | |
C:\PS> $RootDSE.Attributes.AttributeNames | ForEach-Object { | |
>> Write-Host "" | |
>> Write-Host "Attribute Name: $_" -ForegroundColor Green | |
>> Write-Host "" | |
>> $Count = $RootDSE.Attributes[$_].Count | |
>> for ($i = 0; $i -lt $Count; $i++) { | |
>> $RootDSE.Attributes[$_].Item($_) | |
>> } | |
>>} | |
Returns the content of RootDSE. | |
.EXAMPLE | |
C:\PS> Get-LdapObject -LdapFilter "(sAMAccountName=cdent)" -SearchRoot "DC=indented,DC=co,DC=uk" | |
Returns the user with sAMAccountName cdent from the LDAP directory indented.co.uk. | |
.EXAMPLE | |
C:\PS> Get-LdapObject -LdapFilter "(&(objectClass=user)(objectCategory=person))" -SearchRoot "DC=domain,DC=com" -Server "ServerName" -UseSSL -Credential (Get-Credential) | |
Attempt to bind to the specified directory using SSL, then execute a query for user objects. | |
#> | |
[CmdletBinding()] | |
[OutputType([System.DirectoryServices.Protocols.SearchResultEntry[]])] | |
param ( | |
# An optional server to use for this query. If server is not populated Get-ADObject uses serverless binding, passing off server selection to the site-aware DC locator process. | |
# | |
# Server is mandatory when executing a query against a remote domain. | |
[String]$Server, | |
# The LDAP port to query. If UseSSL is set, the port will be changed to the default secure port, 636, unless a value is explictly supplied for this parameter. | |
[UInt16]$Port = 389, | |
# Specifies a user account that has permittion to perform this action. The default is the current user. | |
# | |
# Get-Credential can be used to create a PSCredential object for this parameter. | |
[PSCredential]$Credential, | |
# An LDAP filter to use with the search. The filter (objectClass=*) is used by default. | |
[String]$LdapFilter = "(objectClass=*)", | |
# The search root must be specified as a distinguishedName. The default value (blank) will allow Get-LdapObject to return values from RootDSE with a SearchScope to set to Base (see Examples). | |
[String]$SearchRoot, | |
# The search scope is either Base, OneLevel or Subtree. Subtree is the default value. | |
[DirectoryServices.Protocols.SearchScope]$SearchScope = "Subtree", | |
# An optional array of LDAP property names to return in the search result. | |
[String[]]$Properties, | |
# By default, Get-LdapObject expects to use a plain text connection. SSL can be requested. | |
[Switch]$UseSSL, | |
# Ignore errors raised when attempting to validate the server certificate. | |
[Switch]$IgnoreCertificateError | |
) | |
if ($UseSSL -and -not $psboundparameters.ContainsKey('Port')) { | |
$Port = 636 | |
} | |
$directoryIdentifier = New-Object DirectoryServices.Protocols.LdapDirectoryIdentifier($Server, $Port) | |
if ($Credential) { | |
$networkCredential = $Credential.GetNetworkCredential() | |
$ldapConnection = New-Object DirectoryServices.Protocols.LdapConnection($directoryIdentifier, $NetworkCredential) | |
$ldapConnection.AuthType = [DirectoryServices.Protocols.AuthType]::Basic | |
} else { | |
$ldapConnection = New-Object DirectoryServices.Protocols.LdapConnection($directoryIdentifier) | |
$ldapConnection.AuthType = [DirectoryServices.Protocols.AuthType]::Kerberos | |
} | |
if ($UseSSL) { | |
$ldapConnection.SessionOptions.ProtocolVersion = 3 | |
$ldapConnection.SessionOptions.SecureSocketLayer = $true | |
} | |
if ($IgnoreCertiticateError) { | |
$ldapConnection.SessionOptions.VerifyServerCertificate = { | |
param ( | |
[DirectoryServices.Protocols.LdapConnection]$Connection, | |
[Security.Cryptography.X509Certificates.X509Certificate2]$Certificate | |
) | |
$true | |
} | |
} | |
$searchRequest = New-Object DirectoryServices.Protocols.SearchRequest | |
$searchRequest.DistinguishedName = $SearchRoot | |
$searchRequest.Filter = $LdapFilter | |
$searchRequest.Scope = $SearchScope | |
if ($Properties) { | |
$searchRequest.Attributes.AddRange($Properties) | |
} | |
$pageRequest = New-Object DirectoryServices.Protocols.PageResultRequestControl(1000) | |
$searchRequest.Controls.Add($pageRequest) | Out-Null | |
$null = $searchRequest.Controls.Add((New-Object DirectoryServices.Protocols.SearchOptionsControl("DomainScope"))) | |
try { | |
$ldapConnection.Bind() | |
while (!$complete) { | |
$searchResponse = $LdapConnection.SendRequest($searchRequest) | |
if ($searchResponse.Controls) { | |
$PageResponse = [DirectoryServices.Protocols.PageResultResponseControl]$searchResponse.Controls[0] | |
if ($PageResponse.Cookie.Length -gt 0) { | |
$searchRequest.Controls[0].Cookie = $PageResponse.Cookie | |
} else { | |
$complete = $true | |
} | |
} else { | |
$complete = $true | |
} | |
# Leave the result entries in the output pipeline | |
$searchResponse.Entries | |
} | |
} catch { | |
$pscmdlet.ThrowTerminatingError($_) | |
} | |
} |
function Get-MemberOfTree { | |
<# | |
.SYNOPSIS | |
Get memberOf for an object and present output as a tree. | |
.DESCRIPTION | |
A recursive function which uses repeated ADSI searches to build a memberOf tree. | |
#> | |
[CmdletBinding(DefaultParameterSetName = 'ManualSearchRoot')] | |
param ( | |
# A DN or SamAccountName used to start the search. | |
[Parameter(Mandatory = $true, Position = 1)] | |
[String]$Identity, | |
# The root of the current domain by default. A fixed value can be supplied if required. Note that the search root is also used to locate the suer if a DN is not supplied. | |
[Parameter(ParameterSetName = 'ManualSearchRoot', Position = 2)] | |
[String]$SearchRoot = (([ADSI]'LDAP://RootDSE').defaultNamingContext[0]), | |
# Use a Global Catalog to search instead of LDAP (used for forest-wide searches). | |
[Alias('GlobalCatalog')] | |
[Switch]$GC, | |
# Sets the SearchRoot value to the forest root domain taken from RootDSE. | |
[Parameter(Mandatory = $true, ParameterSetName = 'AutomaticForestSearchRoot')] | |
[Switch]$UseForestRoot, | |
# The character to use to indent values. | |
[Parameter(Position = 4)] | |
[String]$IndentChar = ' ', | |
# The starting indent level (repetition of the IndentCharacter value). | |
[Parameter()] | |
[UInt32]$IndentLevel = 0, | |
[System.Collections.Generic.HashSet[String]]$loopPrevention = (New-Object System.Collections.Generic.HashSet[String]) | |
) | |
if ((Get-PSCallStack)[1].InvocationInfo.InvocationName -ne $myinvocation.InvocationName) { | |
'{0}{1}' -f ($IndentChar * $IndentLevel), $Identity | |
$IndentLevel++ | |
} | |
$protocol = 'LDAP' | |
# Switch the protocol if the GC switch parameter is used. | |
if ($GC) { | |
$protocol = 'GC' | |
} | |
if ($UseForestRoot) { | |
$SearchRoot = ([ADSI]'LDAP://RootDSE').rootDomainNamingContext[0] | |
} | |
$searcher = [ADSISearcher]('{0}://{1}' -f $Protocol, $SearchRoot) | |
$searcher.PageSize = 1000 | |
$searcher.PropertiesToLoad.AddRange(@('name', 'distinguishedName', 'objectClass')) | |
# If the value passed as identity is not an object DN or a GUID treat the value as a sAMAccountName | |
# and execute a search using the SearchRoot and GC parameters. | |
$guid = [Guid]::NewGuid() | |
if ([Guid]::TryParse($Identity, [Ref]$guid)) { | |
$guidHex = $guid.ToByteArray() | ForEach-Object { $_.ToString('X2') } | |
$filter = '(objectGuid=\{0})' -f ($guidHex -join '\') | |
} elseif ($Identity -notmatch '^CN=.+(?:DC=w+){1,}') { | |
$filter = '(|(sAMAccountName={0})(userPrincipalName={0}))' -f $Identity | |
} | |
# Attempt to resolve the identity to a DN | |
if ($filter) { | |
try { | |
$searcher.Filter = $filter | |
$searchResult = $searcher.FindOne() | |
if ($searchResult) { | |
$Identity = $searchResult.Properties['distinguishedName'][0] | |
} | |
} catch { | |
$pscmdlet.ThrowTerminatingError($_) | |
} | |
} | |
try { | |
$searcher.Filter = '(member={0})' -f $Identity | |
$searcher.FindAll() | ForEach-Object { | |
'{0}{1}' -f ($IndentChar * $IndentLevel), $_.Properties['name'][0] | |
if (@($_.Properties['objectClass'])[-1] -eq 'group') { | |
$psboundparameters.Identity = $_.Properties['distinguishedName'][0] | |
$psboundparameters.IndentLevel = $IndentLevel + 1 | |
$psboundparameters.LoopPrevention = $loopPrevention | |
if ($loopPrevention.Contains($psboundparameters.Identity)) { | |
Write-Debug ('Triggered loop avoidance: {0}' -f $_.Properties['distinguishedName'][0]) | |
} else { | |
$null = $loopPrevention.Add($psboundparameters.Identity) | |
Get-MemberOfTree @psboundparameters | |
} | |
} | |
} | |
} catch { | |
throw | |
} | |
} |
function Test-LdapSslConnection { | |
<# | |
.SYNOPSIS | |
Test and LDAPS connection. | |
.DESCRIPTION | |
Test an LDAP connection returning information about the negotiated SSL connection including the server certificate. | |
The state message "The LDAP server is unavailable" indicates the server is either offline or unwilling to negotiate an SSL connection. | |
.INPUTS | |
System.String | |
.EXAMPLE | |
Test-LdapSSLConnection | |
Attempt to bind using SSL and serverless binding. | |
.EXAMPLE | |
Test-LdapSSLConnection -ComputerName servername | |
Attempt to negotiate SSL with "servername". | |
.NOTES | |
Change log: | |
31/03/2015 - Chris Dent - First release. | |
#> | |
[CmdletBinding()] | |
[OutputType('Indented.LDAP.ConnectionInformation')] | |
param ( | |
# The name of a computer to test. By default serverless binding is used. | |
[Parameter(ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true)] | |
[Alias('DnsHostName')] | |
[String]$ComputerName = "", | |
# The port to connect to, by default the LDAPS port (636) is used. | |
[UInt16]$Port = 636, | |
# Credentials to use for the bind attempt. This command requires no special privileges. | |
[PSCredential]$Credential | |
) | |
process { | |
$directoryIdentifier = New-Object DirectoryServices.Protocols.LdapDirectoryIdentifier($ComputerName, $Port) | |
if ($psboundparameters.ContainsKey("Credential")) { | |
$connection = New-Object DirectoryServices.Protocols.LdapConnection($directoryIdentifier, $Credential.GetNetworkCredential()) | |
$connection.AuthType = [DirectoryServices.Protocols.AuthType]::Basic | |
} else { | |
$connection = New-Object DirectoryServices.Protocols.LdapConnection($directoryIdentifier) | |
$connection.AuthType = [DirectoryServices.Protocols.AuthType]::Kerberos | |
} | |
$connection.SessionOptions.ProtocolVersion = 3 | |
$connection.SessionOptions.SecureSocketLayer = $true | |
# Declare a script level variable which can be used to return information from the delegate. | |
New-Variable LdapCertificate -Scope Script -Force | |
# Create a callback delegate to retrieve the negotiated certificate. | |
# Note: | |
# * The certificate is unlikely to return the subject. | |
# * The delegate is documented as using the X509Certificate type, automatically casting this to X509Certificate2 allows access to more information. | |
$connection.SessionOptions.VerifyServerCertificate = { | |
param( | |
[DirectoryServices.Protocols.LdapConnection]$Connection, | |
[Security.Cryptography.X509Certificates.X509Certificate2]$Certificate | |
) | |
$Script:LdapCertificate = $Certificate | |
return $true | |
} | |
$state = "Connected" | |
try { | |
$connection.Bind() | |
} catch { | |
$state = "Failed ($($_.Exception.InnerException.Message.Trim()))" | |
} | |
[PSCustomObject]@{ | |
ComputerName = $ComputerName | |
Port = $Port | |
State = $state | |
Protocol = $connection.SessionOptions.SslInformation.Protocol | |
AlgorithmIdentifier = $connection.SessionOptions.SslInformation.AlgorithmIdentifier | |
CipherStrength = $connection.SessionOptions.SslInformation.CipherStrength | |
Hash = $connection.SessionOptions.SslInformation.Hash | |
HashStrength = $connection.SessionOptions.SslInformation.HashStrength | |
KeyExchangeAlgorithm = [Security.Authentication.ExchangeAlgorithmType][Int]$Connection.SessionOptions.SslInformation.KeyExchangeAlgorithm | |
ExchangeStrength = $connection.SessionOptions.SslInformation.ExchangeStrength | |
X509Certificate = $Script:LdapCertificate | |
PSTypeName = 'Indented.LDAP.ConnectionInformation' | |
} | |
} | |
} |
I think these are missing: (all links are yours)
Thanks for the great code examples!! I don't know how to properly use GitHub, so could I make a suggestion for the "Test-LdapSslConnection"? On my system, the "ExchangeAlgorithmType" cannot be mapped because, apparently, it doesn't exist in my computer. I'm not sure what the correct name mapping is, but I found one Microsoft (social forums) that mentioned my specific case, so I added this code, where I just typecast the value to a string and add the probable name in parens: