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 = $true)] | |
| [ScriptBlock]$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' | |
| } | |
| } | |
| } |