Adsi.psm1
function Add-DomainFqdnToLdapPath { param ( [Parameter(ValueFromPipeline)] [string[]]$DirectoryPath ) begin { $PathRegEx = '(?<Path>LDAP:\/\/[^\/]*)' $DomainRegEx = '(?i)DC=\w{1,}?\b' } process { ForEach ($ThisPath in $DirectoryPath) { if ($ThisPath -match $PathRegEx) { #$NewPath = $Matches.Path if ($ThisPath -match $DomainRegEx) { $DomainDN = $null $DomainFqdn = $null $DomainDN = ([regex]::Matches($ThisPath, $DomainRegEx) | ForEach-Object { $_.Value }) -join ',' $DomainFqdn = $DomainDN | ConvertTo-Fqdn if ($ThisPath -match "LDAP:\/\/$DomainFqdn\/") { #Write-Debug "Domain FQDN already found in the directory path: $($ThisPath)" $FQDNPath = $ThisPath } else { $FQDNPath = $ThisPath -replace 'LDAP:\/\/', "LDAP://$DomainFqdn/" } } else { #Write-Debug "Domain DN not found in the directory path: $($ThisPath)" $FQDNPath = $ThisPath } } else { #Write-Debug "Not an expected directory path: $($ThisPath)" $FQDNPath = $ThisPath } Write-Output $FQDNPath } } } function Add-SidInfo { param ( # Expecting a DirectoryEntry from the LDAP or WinNT providers # Must contain the objectSid property [Parameter(ValueFromPipeline)] $InputObject, [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})), $TrustedDomainSidNameMap = (Get-TrustedDomainSidNameMap -DirectoryEntryCache $DirectoryEntryCache) ) begin {} process { ForEach ($Object in $InputObject) { if ($null -eq $Object) { continue } elseif ($Object.objectSid.Value) { [string]$SID = [System.Security.Principal.SecurityIdentifier]::new([byte[]]$Object.objectSid.Value, 0) } elseif ($Object.Properties['objectSid'].Value) { [string]$SID = [System.Security.Principal.SecurityIdentifier]::new([byte[]]$Object.Properties['objectSid'].Value, 0) } elseif ($Object.Properties['objectSid']) { [string]$SID = [System.Security.Principal.SecurityIdentifier]::new([byte[]]($Object.Properties['objectSid'] | ForEach-Object { $_ }), 0) } if ($Object.Properties['samaccountname']) { $SamAccountName = $Object.Properties['samaccountname'] } else { #DirectoryEntries from the WinNT provider for local accounts do not have a samaccountname attribute so we use name instead $SamAccountName = $Object.Properties['name'] } # The SID of the domain is the SID of the user minus the last block of numbers $DomainSid = $SID.Substring(0, $Sid.LastIndexOf("-")) # Lookup other information about the domain using its SID as the key $DomainObject = $TrustedDomainSidNameMap[$DomainSid] #Write-Debug "$SamAccountName`t$SID" $Object | Add-Member -PassThru -Force @{ SidString = $SID Domain = $DomainObject SamAccountName = $SamAccountName } } } end { } } function ConvertTo-DistinguishedName { # https://docs.microsoft.com/en-us/windows/win32/api/iads/nn-iads-iadsnametranslate param ([string]$Domain) $IADsNameTranslateComObject = New-Object -comObject "NameTranslate" $IADsNameTranslateInterface = $IADsNameTranslateComObject.GetType() $null = $IADsNameTranslateInterface.InvokeMember("Init", "InvokeMethod", $Null, $IADsNameTranslateComObject, (3, $Null)) $null = $IADsNameTranslateInterface.InvokeMember("Set", "InvokeMethod", $Null, $IADsNameTranslateComObject, (3, "$Domain\")) $DNSDomain = $IADsNameTranslateInterface.InvokeMember("Get", "InvokeMethod", $Null, $IADsNameTranslateComObject, 1) Write-Output $DNSDomain } function ConvertTo-Fqdn { param ( [Parameter(ValueFromPipeline)] [string[]]$DistinguishedName ) process { ForEach ($DN in $DistinguishedName) { $DN -replace ',DC=', '.' -replace 'DC=', '' } } } function ConvertTo-HexStringRepresentation { param ( [byte[]]$SIDByteArray ) $SIDByteArray | ForEach-Object { '{0:X}' -f $_ } } function ConvertTo-HexStringRepresentationForLDAPFilterString { param ( [byte[]]$SIDByteArray ) $Hexes = $SIDByteArray | ForEach-Object { '{0:X}' -f $_ } | ForEach-Object { if ($_.Length -eq 2) { $_ } else { "0$_" } } "\$($Hexes -join '\')" } function ConvertTo-SidByteArray { param ( [Parameter(ValueFromPipeline)] [string]$SidString ) process { $SID = [System.Security.Principal.SecurityIdentifier]::new($SidString) [byte[]]$Bytes = [byte[]]::new($SID.BinaryLength) $SID.GetBinaryForm($Bytes, 0) Write-Output $Bytes } } function Expand-AdsiGroupMember { param ( [parameter(ValueFromPipeline)] $DirectoryEntry, [string[]]$PropertiesToLoad = @('operatingSystem', 'objectSid', 'samAccountName', 'objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'Department', 'Title'), $TrustedDomainSidNameMap = (Get-TrustedDomainSidNameMap -DirectoryEntryCache $DirectoryEntryCache), [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) begin { $i = 0 } process { ForEach ($Entry in $DirectoryEntry) { $i++ $status = ("$(Get-Date -Format s)`t$(hostname)`tExpand-AdsiGroupMember`tStatus: Using ADSI to get info on group member $i`: " + $Entry.Name) #Write-Debug " $status" $Principal = $null if ($Entry.objectClass -contains 'foreignSecurityPrincipal') { if ($Entry.distinguishedName.Value -match '(?>^CN=)(?<SID>[^,]*)') { [string]$SID = $Matches.SID #The SID of the domain is the SID of the user minus the last block of numbers $DomainSid = $SID.Substring(0, $Sid.LastIndexOf("-")) $Domain = $TrustedDomainSidNameMap[$DomainSid] $Success = $true try { $Principal = Get-DirectoryEntry -DirectoryPath "LDAP://$($Domain.Dns)/<SID=$SID>" -DirectoryEntryCache $DirectoryEntryCache } catch { $Success = $false $Principal = $Entry Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-AdsiGroupMember`t$SID could not be retrieved from $Domain" } if ($Success -eq $true) { $null = $Principal.RefreshCache($PropertiesToLoad) # Recursively enumerate group members if ($Principal.properties['objectClass'].Value -contains 'group') { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-AdsiGroupMember`t'$($Principal.properties['name'])' is a group in $Domain" $Principal = ($Principal | Get-ADSIGroupMember -DirectoryEntryCache $DirectoryEntryCache).FullMembers | Expand-AdsiGroupMember -DirectoryEntryCache $DirectoryEntryCache -TrustedDomainSidNameMap $TrustedDomainSidNameMap } } } } else { $Principal = $Entry } $Principal | Add-SidInfo -DirectoryEntryCache $DirectoryEntryCache -TrustedDomainSidNameMap $TrustedDomainSidNameMap } } } function Expand-IdentityReference { # Use ADSI to collect more information about the IdentityReference in NTFS Access Control Entries param ( # The NTFS AccessControlEntry object(s), grouped by their IdentityReference property [Parameter(ValueFromPipeline)] [System.Object[]]$AccessControlEntry, # Get group members [bool]$GroupMember = $true, # Get group members recursively # If true, implies $GroupMember = $true [bool]$GroupMemberRecursion = $true, # Thread-safe hashtable to use for caching directory entries and avoiding duplicate directory queries [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})), # Thread-safe hashtable to use for caching directory entries and avoiding duplicate directory queries [hashtable]$IdentityReferenceCache = ([hashtable]::Synchronized(@{})) ) begin { #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$(($AccessControlEntry | Measure).Count) unique IdentityReferences found in the $(($AccessControlEntry | Measure).Count) ACEs" # Get the SID of the current domain $CurrentDomain = (Get-CurrentDomain) # Convert the objectSID attribute (byte array) to a security descriptor string formatted according to SDDL syntax (Security Descriptor Definition Language) [string]$CurrentDomainSID = [System.Security.Principal.SecurityIdentifier]::new([byte[]]$CurrentDomain.objectSid.Value, 0) $LocalDomains = @('NT AUTHORITY', 'BUILTIN', "$(hostname)") $KnownDomains = @{} $i = 0 } process { ForEach ($ThisIdentity in $AccessControlEntry) { $ThisIdentityGroup = $ThisIdentity.Group $i++ #Calculate the completion percentage, and format it to show 0 decimal places $percentage = "{0:N0}" -f (($i / ($AccessControlEntry.Count)) * 100) #Display the progress bar $status = $percentage + "% - Using ADSI to get info on NTFS IdentityReference $i of " + $AccessControlEntry.Count + ": " + $ThisIdentity.Name #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tStatus: $status" #Write-Progress -Activity ("Unique IdentityReferences: " + $AccessControlEntry.Count) -Status $status -PercentComplete $percentage if ($null -eq $IdentityReferenceCache[$ThisIdentity.Name]) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tIdentityReferenceCache miss for '$($ThisIdentity.Name)'" $DomainDN = $null $DirectoryEntry = $null $Members = $null $StartingIdentityName = $ThisIdentity.Name $split = $StartingIdentityName.Split('\') $domainNetbiosString = $split[0] $name = $split[1] if ($null -ne $name -and ($ThisIdentity.Group.AdsiProvider | Select-Object -First 1) -eq 'LDAP') { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) is a domain security principal" # Add this domain to our list of known domains if (!($KnownDomains[$domainNetbiosString])) { $KnownDomains[$domainNetbiosString] = ConvertTo-DistinguishedName -Domain $domainNetbiosString Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tCache miss for domain $($domainNetbiosString). Adding its Distinguished Name to dictionary of known domains for future lookup" } # Search the domain for the principal $DomainDn = $KnownDomains[$domainNetbiosString] try { $SearchPath = "LDAP://$DomainDn" | Add-DomainFqdnToLdapPath $DirectoryEntry = Search-Directory -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath $SearchPath -Filter "(samaccountname=$Name)" -PropertiesToLoad @('objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'Department', 'Title') } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) could not be resolved against its directory" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($_.Exception.Message)" } } elseif (((($StartingIdentityName -split '-') | Select-Object -SkipLast 1) -join '-') -eq $CurrentDomainSID) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) is an unresolved SID from the current domain" # Get the distinguishedName and netBIOSName of the current domain. This also determines whether the domain is online. $DomainDN = $CurrentDomain.distinguishedName.Value $DomainFQDN = $DomainDN | ConvertTo-Fqdn $PartitionsPath = "LDAP://cn=partitions,cn=configuration,$DomainDn" | Add-DomainFqdnToLdapPath $DomainCrossReference = Search-Directory -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath $PartitionsPath -Filter "(&(objectcategory=crossref)(dnsroot=$DomainFQDN)(netbiosname=*))" -PropertiesToLoad netbiosname if ($DomainCrossReference.Properties ) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tThe domain '$DomainFQDN' is online" $domainNetbiosString = $DomainCrossReference.Properties['netbiosname'] # TODO: The domain is online, so let's see if any domain trusts have issues? Determine if SID is foreign security principal? # TODO: What if the foreign security principal exists but the corresponding domain trust is down? Don't want to recommend deletion of the ACE in that case. } $SidObject = [System.Security.Principal.SecurityIdentifier]::new($StartingIdentityName) $SidBytes = [byte[]]::new($SidObject.BinaryLength) $null = $SidObject.GetBinaryForm($SidBytes, 0) $ObjectSid = ConvertTo-HexStringRepresentationForLDAPFilterString -SIDByteArray $SidBytes try { $DirectoryEntry = Search-Directory -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath "LDAP://$DomainDn" -Filter "(objectsid=$ObjectSid)" -PropertiesToLoad @('objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'Department', 'Title') } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) could not be resolved against its directory" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($_.Exception.Message)" } } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) is a local security principal or unresolved SID" # Determine if SID belongs to current domain $IdentityDomainSID = (($StartingIdentityName -split '-') | Select-Object -SkipLast 1) -join '-' if ($IdentityDomainSID -eq $CurrentDomainSID) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) belongs to the current domain. Could be a deleted user. ?possibly a foreign security principal corresponding to an offline trusted domain or deleted user in the trusted domain?" } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) does not belong to the current domain. Could be a local security principal or belong to an unresolvable domain." <# #Write-Host ($ThisIdentity | Fl * | out-string) -ForegroundColor Red $Domains = $ThisIdentityGroup.Path | ForEach-Object {($_ -split '\\')[2]} $ThisIdentity = ForEach ($domainNetbiosString in $Domains) { $DomainDN = "dc=$domainNetbiosString" switch ($StartingIdentityName) { 'NT AUTHORITY\SYSTEM' { $StartingIdentityName = "$domainNetbiosString\SYSTEM" } default { } } [pscustomobject]@{ Count = $ThisIdentity.Count Name = $StartingIdentityName Group = $ThisIdentityGroup | Where-Object -FilterScript {($_.Path -split '\\')[2] -eq $domainNetbiosString} DomainNetBios = $domainNetbiosString } }#> } if ($null -eq $name) { $name = $StartingIdentityName } if ($name -match 'S-\d+-\d+-\d+-\d+-\d+\-\d+\-\d+') { if ($Domains.Count -gt 1) { $DirectoryEntry = ForEach ($domainNetbiosString in $Domains) { try { $UsersGroup = Get-DirectoryEntry -DirectoryPath "WinNT://$domainNetbiosString/Users,group" -DirectoryEntryCache $DirectoryEntryCache } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tCould not connect to $domainNetbiosString using PSRemoting" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$_" } $MembersOfUsersGroup = Get-WinNTGroupMember -DirectoryEntry $UsersGroup -DirectoryEntryCache $DirectoryEntryCache $MembersOfUsersGroup | Where-Object -FilterScript { ($name -eq [System.Security.Principal.SecurityIdentifier]::new([byte[]]$_.Properties['objectSid'].Value, 0)) } $ThisIdentity = [pscustomobject]@{ Count = $(($ThisIdentityGroup | Measure-Object).Count) Name = "$domainNetbiosString\" + $DirectoryEntry.Name Group = $ThisIdentityGroup | Where-Object -FilterScript { ($_.Path -split '\\')[2] -eq $domainNetbiosString } } } } else { try { $UsersGroup = Get-DirectoryEntry -DirectoryPath "WinNT://$domainNetbiosString/Users,group" -DirectoryEntryCache $DirectoryEntryCache } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tCould not connect to $domainNetbiosString using PSRemoting" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$_" } $MembersOfUsersGroup = Get-WinNTGroupMember -DirectoryEntry $UsersGroup -DirectoryEntryCache $DirectoryEntryCache $DirectoryEntry = $MembersOfUsersGroup | Where-Object -FilterScript { ($name -eq [System.Security.Principal.SecurityIdentifier]::new([byte[]]$_.Properties['objectSid'].Value, 0)) } $ThisIdentity = [pscustomobject]@{ Count = $(($ThisIdentityGroup | Measure-Object).Count) Name = "$domainNetbiosString\" + $DirectoryEntry.Name Group = $ThisIdentityGroup } } } else { if ($Domains.Count -gt 1) { $DirectoryEntry = ForEach ($domainNetbiosString in $Domains) { $DirectoryPath = "WinNT://$domainNetbiosString/$name" try { Get-DirectoryEntry -DirectoryPath $DirectoryPath -PropertiesToLoad members -DirectoryEntryCache $DirectoryEntryCache } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($DirectoryPath) could not be resolved" } } } else { $DirectoryPath = "WinNT://$domainNetbiosString/$name" try { $DirectoryEntry = Get-DirectoryEntry -DirectoryPath $DirectoryPath -PropertiesToLoad members -DirectoryEntryCache $DirectoryEntryCache } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($DirectoryPath) could not be resolved" } } } } $ObjectType = $null if ($null -ne $DirectoryEntry) { $ThisIdentity | Add-Member -Name 'DirectoryEntry' -Value $DirectoryEntry -MemberType NoteProperty -Force # Not needed after changes but not yet ready to let go in case I need it again for troubleshooting after I mess it up the same way again #if ($DirectoryEntry.Properties.GetType().FullName -eq 'System.DirectoryServices.ResultPropertyCollection') { #$ThisIdentity | Add-Member -Force -NotePropertyMembers $DirectoryEntry.Properties -ErrorAction SilentlyContinue #} if ( $DirectoryEntry.Properties['objectClass'] -contains 'group' -or $DirectoryEntry.SchemaClassName -contains 'Group' ) { $ObjectType = 'Group' } else { $ObjectType = 'User' } if ($GroupMember) { if ($DirectoryEntry.Properties['objectClass'] -contains 'group') { # Retrieve the members of groups from the LDAP provider $Members = (Get-ADSIGroup -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath $DirectoryEntry.Path).FullMembers } else { # Retrieve the members of groups from the WinNT provider $Members = Get-WinNTGroupMember -DirectoryEntryCache $DirectoryEntryCache -DirectoryEntry $DirectoryEntry -KnownDomains $KnownDomains } if ($Members) { $Members | ForEach-Object { if ($_.Domain) { $_ | Add-Member -Force -NotePropertyMembers @{ Group = $ThisIdentityGroup } } else { $_ | Add-Member -Force -NotePropertyMembers @{ Group = $ThisIdentityGroup Domain = [pscustomobject]@{ Dns = $domainNetbiosString Netbios = $domainNetbiosString Sid = ($name -split '-') | Select-Object -Last 1 } } } } } $ThisIdentity | Add-Member -Name 'Members' -Value $Members -MemberType NoteProperty -Force } } else { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`t$($StartingIdentityName) could not be matched to a DirectoryEntry" } $ThisIdentity | Add-Member -Name "DomainDn" -Type NoteProperty -Value $DomainDn -Force $ThisIdentity | Add-Member -Name "DomainNetbios" -Type NoteProperty -Value $DomainNetBiosString -Force $ThisIdentity | Add-Member -Name "ObjectType" -Type NoteProperty -Value $ObjectType -Force $IdentityReferenceCache[$StartingIdentityName] = $ThisIdentity } else { #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-IdentityReference`tIdentityReferenceCache hit for '$($ThisIdentity.Name)'" $null = $IdentityReferenceCache[$ThisIdentity.Name].Group.Add($ThisIdentityGroup) $ThisIdentity = $IdentityReferenceCache[$ThisIdentity.Name] } Write-Output $ThisIdentity } } end { #Write-Progress -Activity Completed -Completed } } function Expand-WinNTGroupMember { param ( [Parameter(ValueFromPipeline)] $DirectoryEntry, [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) begin {} process { ForEach ($ThisEntry in $DirectoryEntry) { if (!($ThisEntry.Properties)) { Write-Warning "'$ThisEntry' has no properties" } elseif ($ThisEntry.Properties['objectClass'] -contains 'group') { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-WinNTGroupMember`t$($ThisEntry.Path) is a group" (Get-ADSIGroup -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath $ThisEntry.Path).FullMembers | Add-SidInfo -DirectoryEntryCache $DirectoryEntryCache } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tExpand-WinNTGroupMember`t'$($ThisEntry.Path) is an account" $ThisEntry | Add-SidInfo -DirectoryEntryCache $DirectoryEntryCache } } } end {} } function Find-AdsiProvider { param ( [string]$AdsiServer, [hashtable]$KnownServers = [hashtable]::Synchronized(@{}) ) $AdsiProvider = $null if ($KnownServers[$AdsiServer]) { $AdsiProvider = $KnownServers[$AdsiServer] } else { try { $null = [System.DirectoryServices.DirectoryEntry]::Exists("LDAP://$AdsiServer") $AdsiProvider = 'LDAP' } catch {} if (!$AdsiProvider) { try { $null = [System.DirectoryServices.DirectoryEntry]::Exists("WinNT://$AdsiServer") $AdsiProvider = 'WinNT' } catch {} } if (!$AdsiProvider) { $AdsiProvider = 'none' } $KnownServers[$AdsiServer] = $AdsiProvider } Write-Output $AdsiProvider } function Get-ADSIGroup { param ( [string]$DirectoryPath = (([adsisearcher]'').SearchRoot.Path), [string]$GroupName, [string[]]$PropertiesToLoad = @('objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'department', 'title'), [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) $SearchParams = @{ PropertiesToLoad = $PropertiesToLoad DirectoryPath = $DirectoryPath DirectoryEntryCache = $DirectoryEntryCache } if ($GroupName) { $SearchParams['Filter'] = "(&(objectClass=group)(cn=$GroupName))" } else { $SearchParams['Filter'] = "(objectClass=group)" } Search-Directory @SearchParams | Get-ADSIGroupMember -DirectoryEntryCache $DirectoryEntryCache } function Get-ADSIGroupMember { <# Get a group and its members #> param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $Group, [string[]]$PropertiesToLoad = @('operatingSystem', 'objectSid', 'samAccountName', 'objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'department', 'title'), [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) begin {} process { foreach ($ThisGroup in $Group) { $SearchParameters = @{ # Recursive search Filter = "(memberof:1.2.840.113556.1.4.1941:=$($ThisGroup.Properties['distinguishedname']))" # Non-recursive search #Filter = "(memberof=$($ThisGroup.Properties['distinguishedname']))" PropertiesToLoad = $PropertiesToLoad DirectoryEntryCache = $DirectoryEntryCache } $PathRegEx = '(?<Path>LDAP:\/\/[^\/]*)' if ($ThisGroup.Path -match $PathRegEx) { $SearchParameters['DirectoryPath'] = $Matches.Path | Add-DomainFqdnToLdapPath $DomainRegEx = '(?i)DC=\w{1,}?\b' if ($ThisGroup.Path -match $DomainRegEx) { $Domain = ([regex]::Matches($ThisGroup.Path, $DomainRegEx) | ForEach-Object { $_.Value }) -join ',' $SearchParameters['DirectoryPath'] = "LDAP://$Domain" | Add-DomainFqdnToLdapPath } else { $SearchParameters['DirectoryPath'] = $ThisGroup.Path | Add-DomainFqdnToLdapPath } } else { $SearchParameters['DirectoryPath'] = $ThisGroup.Path | Add-DomainFqdnToLdapPath } #> #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-AdsiGroupMember`t$($SearchParameters['Filter'])" $GroupMemberSearch = Search-Directory @SearchParameters if ($GroupMemberSearch.Count -gt 0) { $CurrentADGroupMembers = $GroupMemberSearch | ForEach-Object { $FQDNPath = $_.Path | Add-DomainFqdnToLdapPath Get-DirectoryEntry -DirectoryPath $FQDNPath -DirectoryEntryCache $DirectoryEntryCache } } else { $CurrentADGroupMembers = $null } Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-AdsiGroupMember`t$($ThisGroup.Properties.name) has $(($CurrentADGroupMembers | Measure-Object).Count) members" $TrustedDomainSidNameMap = Get-TrustedDomainSidNameMap -DirectoryEntryCache $DirectoryEntryCache $ProcessedGroupMembers = $CurrentADGroupMembers | Expand-AdsiGroupMember -DirectoryEntryCache $DirectoryEntryCache -TrustedDomainSidNameMap $TrustedDomainSidNameMap $ThisGroup | Add-Member -MemberType NoteProperty -Name FullMembers -Value $ProcessedGroupMembers -Force -PassThru } } end {} } function Get-CurrentDomain { $Obj = [adsi]::new() $Obj.RefreshCache({ 'objectSid' }) Write-Output $Obj } function Get-DirectoryEntry { <# .SYNOPSIS Use Active Directory Service Interfaces to retrieve an object from a directory .DESCRIPTION Retrieve a directory entry using either the WinNT or LDAP provider for ADSI .EXAMPLE Get-DirectoryEntry distinguishedName : {DC=ad,DC=contoso,DC=com} Path : LDAP://DC=ad,DC=contoso,DC=com As the current user, bind to the current domain and retrieve the DirectoryEntry for the root of the domain #> [OutputType([PSObject[]])] [CmdletBinding()] param ( <# Path to the directory object to retrieve Defaults to the root of the current domain (but don't use it for that, just do this instead: [System.DirectoryServices.DirectorySearcher]::new()) #> [string]$DirectoryPath = (([System.DirectoryServices.DirectorySearcher]'').SearchRoot.Path), <# Credentials to use to bind to the directory Defaults to the credentials of the current user #> [pscredential]$Credential, # Properties of the target object to retrieve [string[]]$PropertiesToLoad, <# A hashtable containing cached directory entries so they don't have to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) $DirectoryEntry = $null if ($null -eq $DirectoryEntryCache[$DirectoryPath]) { <# The WinNT provider only throws an error if you try to retrieve certain accounts/identities We will create own dummy objects instead of performing the query #> switch -regex ($DirectoryPath) { '^WinNT\:\/\/[^\/]*\/CREATOR OWNER$' { $SidByteAray = 'S-1-3-0' | ConvertTo-SidByteArray $DirectoryEntry = [pscustomobject]@{ Name = 'CREATOR OWNER' Description = 'A SID to be replaced by the SID of the user who creates a new object. This SID is used in inheritable ACEs.' objectSid = $SidByteAray Parent = $DirectoryPath | Split-Path -Parent Path = $DirectoryPath Properties = @{ Name = 'CREATOR OWNER' Description = 'A SID to be replaced by the SID of the user who creates a new object. This SID is used in inheritable ACEs.' objectSid = $SidByteAray } SchemaClassName = 'User' SchemaEntry = [System.DirectoryServices.DirectoryEntry] } $DirectoryEntry | Add-Member -MemberType ScriptMethod -Name RefreshCache -Force -Value {} } '^WinNT\:\/\/[^\/]*\/SYSTEM$' { $SidByteAray = 'S-1-5-18' | ConvertTo-SidByteArray $DirectoryEntry = [pscustomobject]@{ Name = 'SYSTEM' Description = 'By default, the SYSTEM account is granted Full Control permissions to all files on an NTFS volume' objectSid = $SidByteAray Parent = $DirectoryPath | Split-Path -Parent Path = $DirectoryPath Properties = @{ Name = 'SYSTEM' Description = 'By default, the SYSTEM account is granted Full Control permissions to all files on an NTFS volume' objectSid = $SidByteAray } SchemaClassName = 'User' SchemaEntry = [System.DirectoryServices.DirectoryEntry] } $DirectoryEntry | Add-Member -MemberType ScriptMethod -Name RefreshCache -Force -Value {} } '^WinNT\:\/\/[^\/]*\/INTERACTIVE$' { $SidByteAray = 'S-1-5-4' | ConvertTo-SidByteArray $DirectoryEntry = [pscustomobject]@{ Name = 'INTERACTIVE' Description = 'Users who log on for interactive operation. This is a group identifier added to the token of a process when it was logged on interactively.' objectSid = $SidByteAray Parent = $DirectoryPath | Split-Path -Parent Path = $DirectoryPath Properties = @{ Name = 'INTERACTIVE' Description = 'Users who log on for interactive operation. This is a group identifier added to the token of a process when it was logged on interactively.' objectSid = $SidByteAray } SchemaClassName = 'Group' SchemaEntry = [System.DirectoryServices.DirectoryEntry] } $DirectoryEntry | Add-Member -MemberType ScriptMethod -Name RefreshCache -Force -Value {} } '^WinNT\:\/\/[^\/]*\/Authenticated Users$' { $SidByteAray = 'S-1-5-11' | ConvertTo-SidByteArray $DirectoryEntry = [pscustomobject]@{ Name = 'Authenticated Users' Description = 'Any user who accesses the system through a sign-in process has the Authenticated Users identity.' objectSid = $SidByteAray Parent = $DirectoryPath | Split-Path -Parent Path = $DirectoryPath Properties = @{ Name = 'Authenticated Users' Description = 'Any user who accesses the system through a sign-in process has the Authenticated Users identity.' objectSid = $SidByteAray } SchemaClassName = 'Group' SchemaEntry = [System.DirectoryServices.DirectoryEntry] } $DirectoryEntry | Add-Member -MemberType ScriptMethod -Name RefreshCache -Force -Value {} } default { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t[System.DirectoryServices.DirectoryEntry]::new('$DirectoryPath')" if ($Credential) { $DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath, $($Credential.UserName), $($Credential.GetNetworkCredential().password)) } else { $DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath) } } } $DirectoryEntryCache[$DirectoryPath] = $DirectoryEntry } else { #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`tDirectoryEntryCache hit for '$DirectoryPath'" $DirectoryEntry = $DirectoryEntryCache[$DirectoryPath] } try { if ($PropertiesToLoad) { # If the $DirectoryPath was invalid, this line will return an error $null = $DirectoryEntry.RefreshCache($PropertiesToLoad) } Write-Output $DirectoryEntry } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t'$DirectoryPath' could not be retrieved." # Ensure that the error message appears on 1 line # Use .Trim() to remove leading and trailing whitespace # Use -replace to remove an errant line break in the following specific error I encountered: The following exception occurred while retrieving member "RefreshCache": "The group name could not be found.`r`n" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t'$($_.Exception.Message.Trim() -replace '\s"',' "')" } } function Get-TrustedDomainSidNameMap { param ( [Switch]$KeyByNetbios, [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) $Map = @{} $nltestresults = & nltest /domain_trusts $NlTestRegEx = '[\d]*: .*' $TrustRelationships = $nltestresults -match $NlTestRegEx foreach ($TrustRelationship in $TrustRelationships) { $RegEx = '(?<index>[\d]*): (?<netbios>\S*) (?<dns>\S*).*' if ($TrustRelationship -match $RegEx) { $DomainDnsName = $Matches.dns $DomainNetbios = $Matches.netbios } $DomainDirectoryEntry = Get-DirectoryEntry -DirectoryPath "LDAP://$DomainDnsName" -DirectoryEntryCache $DirectoryEntryCache $DistinguishedName = ConvertTo-DistinguishedName -Domain $DomainNetbios try { $DomainDirectoryEntry.RefreshCache({ "objectSid" }) $DomainSid = [System.Security.Principal.SecurityIdentifier]::new([byte[]]$DomainDirectoryEntry.Properties["objectSid"].Value, 0).ToString() if ($KeyByNetbios -eq $true) { $Map[$DomainNetbios] = [pscustomobject]@{ Dns = $DomainDnsName Netbios = $DomainNetbios Sid = $DomainSid DistinguishedName = $DistinguishedName } } else { $Map[$DomainSid] = [pscustomobject]@{ Dns = $DomainDnsName Netbios = $DomainNetbios Sid = $DomainSid DistinguishedName = $DistinguishedName } } } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGet-TrustedDomainSidNameMap`tDomain: '$DomainDnsName' - $($_.Exception.Message)" } } $LocalAccountSID = Get-CimInstance -Query "SELECT SID FROM Win32_UserAccount WHERE LocalAccount = 'True'" | Select-Object -First 1 -ExpandProperty SID $DomainSid = $LocalAccountSID.Substring(0, $LocalAccountSID.LastIndexOf("-")) $DomainNetBios = hostname $DomainDnsName = "$DomainNetbios.$((Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters').'NV Domain')" $Map[$DomainSid] = [pscustomobject]@{ Dns = $DomainDnsName Netbios = $DomainNetbios Sid = $DomainSid } Write-Output $Map } function Get-WinNTGroupMember { param ( $DirectoryEntry, [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) #TODO: Default should know at least any trusted domains $KnownDomains = Get-TrustedDomainSidNameMap -DirectoryEntryCache $DirectoryEntryCache -KeyByNetbios $SourceDomain = $DirectoryEntry.Path | Split-Path -Parent | Split-Path -Leaf # Retrieve the members of local groups if ($null -ne $DirectoryEntry.Properties['groupType']) { $DirectoryMembers = $DirectoryEntry.Invoke('Members') ForEach ($DirectoryMember in $DirectoryMembers) { # Convert the COM Objects from the WinNT provider to proper [System.DirectoryServices.DirectoryEntry] objects from the LDAP provider $DirectoryPath = Invoke-ComObject -ComObject $DirectoryMember -Property 'ADsPath' $MemberDomainDn = $null if ($DirectoryPath -match 'WinNT:\/\/(?<Domain>[^\/]*)\/(?<Acct>.*$)') { $MemberName = $Matches.Acct $MemberDomainNetbios = $Matches.Domain if ($KnownDomains[$MemberDomainNetbios] -and $MemberDomainNetbios -ne $SourceDomain) { $MemberDomainDn = $KnownDomains[$MemberDomainNetbios].DistinguishedName } if ($DirectoryPath -match 'WinNT:\/\/(?<Domain>[^\/]*)\/(?<Middle>[^\/]*)\/(?<Acct>.*$)') { if ($Matches.Middle -eq ($DirectoryEntry.Path | Split-Path -Parent | Split-Path -Leaf)) { $MemberDomainDn = $null } } } if ($MemberDomainDn) { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-WinNTGroupMember`t'$MemberName' is a domain security principal" $MemberDirectoryEntry = Search-Directory -DirectoryEntryCache $DirectoryEntryCache -DirectoryPath "LDAP://$MemberDomainDn" -Filter "(samaccountname=$MemberName)" -PropertiesToLoad @('objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'Department', 'Title', 'samAccountName', 'objectSid') } else { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-WinNTGroupMember`t'$DirectoryPath' is a local security principal" $MemberDirectoryEntry = Get-DirectoryEntry -DirectoryPath $DirectoryPath -PropertiesToLoad @('objectClass', 'distinguishedName', 'name', 'grouptype', 'description', 'managedby', 'member', 'objectClass', 'Department', 'Title', 'samAccountName', 'objectSid') -DirectoryEntryCache $DirectoryEntryCache } $MemberDirectoryEntry | Expand-WinNTGroupMember -DirectoryEntryCache $DirectoryEntryCache } } } function Invoke-ComObject { param ( [Parameter(Mandatory)] $ComObject, [Parameter(Mandatory)] [String]$Property, $Value, [Switch]$Method ) <# If ($ComObject -IsNot "__ComObject") { If (!$ComInvoke) { $Global:ComInvoke = @{} } If (!$ComInvoke.$ComObject) { $ComInvoke.$ComObject = New-Object -ComObject $ComObject } $ComObject = $ComInvoke.$ComObject } #> If ($Method) { $Invoke = "InvokeMethod" } ElseIf ($MyInvocation.BoundParameters.ContainsKey("Value")) { $Invoke = "SetProperty" } Else { $Invoke = "GetProperty" } [__ComObject].InvokeMember($Property, $Invoke, $Null, $ComObject, $Value) } function New-FakeDirectoryEntry { <# .SYNOPSIS Returns a PSCustomObject in place of a DirectoryEntry for certain WinNT security principals that do not have objects in the directory .DESCRIPTION Retrieve a directory entry using either the WinNT or LDAP provider for ADSI .EXAMPLE ---------- EXAMPLE 1 ---------- As the current user, bind to the current domain and retrieve the DirectoryEntry for the root of the domain Get-DirectoryEntry #> [OutputType([PSObject[]])] [CmdletBinding()] param ( <# Path to the directory object to retrieve Defaults to the root of the current domain (but don't use it for that, just do this instead: [System.DirectoryServices.DirectorySearcher]::new()) #> [string]$DirectoryPath = (([System.DirectoryServices.DirectorySearcher]'').SearchRoot.Path), <# Credentials to use to bind to the directory Defaults to the credentials of the current user #> [pscredential]$Credential, # Properties of the target object to retrieve [string[]]$PropertiesToLoad, <# A hashtable containing cached directory entries so they don't have to be retrieved from the directory again Uses a thread-safe hashtable by default #> [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) $DirectoryEntry = $null if ($null -eq $DirectoryEntryCache[$DirectoryPath]) { <# The WinNT provider only throws an error if you try to retrieve certain accounts/identities We will create own dummy objects instead of performing the query #> switch -regex ($DirectoryPath) { '^WinNT\:\/\/[^\/]*\/CREATOR OWNER$' { $SidByteAray = 'S-1-3-0' | ConvertTo-SidByteArray $DirectoryEntry = [pscustomobject]@{ Name = 'CREATOR OWNER' Description = 'A SID to be replaced by the SID of the user who creates a new object. This SID is used in inheritable ACEs.' objectSid = $SidByteAray Parent = $DirectoryPath | Split-Path -Parent Path = $DirectoryPath Properties = @{ Name = 'CREATOR OWNER' Description = 'A SID to be replaced by the SID of the user who creates a new object. This SID is used in inheritable ACEs.' objectSid = $SidByteAray } SchemaClassName = 'User' SchemaEntry = [System.DirectoryServices.DirectoryEntry] } $DirectoryEntry | Add-Member -MemberType ScriptMethod -Name RefreshCache -Force -Value {} } '^WinNT\:\/\/[^\/]*\/SYSTEM$' { $SidByteAray = 'S-1-5-18' | ConvertTo-SidByteArray $DirectoryEntry = [pscustomobject]@{ Name = 'SYSTEM' Description = 'By default, the SYSTEM account is granted Full Control permissions to all files on an NTFS volume' objectSid = $SidByteAray Parent = $DirectoryPath | Split-Path -Parent Path = $DirectoryPath Properties = @{ Name = 'SYSTEM' Description = 'By default, the SYSTEM account is granted Full Control permissions to all files on an NTFS volume' objectSid = $SidByteAray } SchemaClassName = 'User' SchemaEntry = [System.DirectoryServices.DirectoryEntry] } $DirectoryEntry | Add-Member -MemberType ScriptMethod -Name RefreshCache -Force -Value {} } '^WinNT\:\/\/[^\/]*\/INTERACTIVE$' { $SidByteAray = 'S-1-5-4' | ConvertTo-SidByteArray $DirectoryEntry = [pscustomobject]@{ Name = 'INTERACTIVE' Description = 'Users who log on for interactive operation. This is a group identifier added to the token of a process when it was logged on interactively.' objectSid = $SidByteAray Parent = $DirectoryPath | Split-Path -Parent Path = $DirectoryPath Properties = @{ Name = 'INTERACTIVE' Description = 'Users who log on for interactive operation. This is a group identifier added to the token of a process when it was logged on interactively.' objectSid = $SidByteAray } SchemaClassName = 'Group' SchemaEntry = [System.DirectoryServices.DirectoryEntry] } $DirectoryEntry | Add-Member -MemberType ScriptMethod -Name RefreshCache -Force -Value {} } '^WinNT\:\/\/[^\/]*\/Authenticated Users$' { $SidByteAray = 'S-1-5-11' | ConvertTo-SidByteArray $DirectoryEntry = [pscustomobject]@{ Name = 'Authenticated Users' Description = 'Any user who accesses the system through a sign-in process has the Authenticated Users identity.' objectSid = $SidByteAray Parent = $DirectoryPath | Split-Path -Parent Path = $DirectoryPath Properties = @{ Name = 'Authenticated Users' Description = 'Any user who accesses the system through a sign-in process has the Authenticated Users identity.' objectSid = $SidByteAray } SchemaClassName = 'Group' SchemaEntry = [System.DirectoryServices.DirectoryEntry] } $DirectoryEntry | Add-Member -MemberType ScriptMethod -Name RefreshCache -Force -Value {} } default { Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t[System.DirectoryServices.DirectoryEntry]::new('$DirectoryPath')" if ($Credential) { $DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath, $($Credential.UserName), $($Credential.GetNetworkCredential().password)) } else { $DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath) } } } $DirectoryEntryCache[$DirectoryPath] = $DirectoryEntry } else { #Write-Debug " $(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`tDirectoryEntryCache hit for '$DirectoryPath'" $DirectoryEntry = $DirectoryEntryCache[$DirectoryPath] } try { if ($PropertiesToLoad) { # If the $DirectoryPath was invalid, this line will return an error $null = $DirectoryEntry.RefreshCache($PropertiesToLoad) } Write-Output $DirectoryEntry } catch { Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t'$DirectoryPath' could not be retrieved." # Ensure that the error message appears on 1 line # Use .Trim() to remove leading and trailing whitespace # Use -replace to remove an errant line break in the following specific error I encountered: The following exception occurred while retrieving member "RefreshCache": "The group name could not be found.`r`n" Write-Warning "$(Get-Date -Format s)`t$(hostname)`tGet-DirectoryEntry`t'$($_.Exception.Message.Trim() -replace '\s"',' "')" } } function Resolve-IdentityReference { param ( [psobject[]]$AccessControlEntry, [hashtable]$KnownServers = [hashtable]::Synchronized(@{}) ) begin {} process { ForEach ($ThisACE in $AccessControlEntry) { $ThisServer = $null $AdsiProvider = $null $ThisServer = $ThisACE.Path -split '\\' | Where-Object { $_ -ne '' } | Select-Object -First 1 $ResolvedIdentityReference = $ThisACE.IdentityReference -replace 'NT AUTHORITY', $ThisServer -replace 'BUILTIN', $ThisServer $ThisServer = $ResolvedIdentityReference -split '\\' | Where-Object { $_ -ne '' } | Select-Object -First 1 $AdsiProvider = Find-AdsiProvider -AdsiServer $ThisServer -KnownServers $KnownServers $ThisACE | Add-Member -PassThru -Force -NotePropertyMembers @{ ResolvedIdentityReference = $ResolvedIdentityReference AdsiProvider = $AdsiProvider } } } end {} } function Search-Directory { param ( [string]$DirectoryPath = (([adsisearcher]'').SearchRoot.Path), [string]$Filter, [int]$PageSize = 1000, [string[]]$PropertiesToLoad, [pscredential]$Credential, [string]$SearchScope = 'subtree', [hashtable]$DirectoryEntryCache = ([hashtable]::Synchronized(@{})) ) if ($Credential) { #$DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath,$($Credential.UserName),$($Credential.GetNetworkCredential().password)) $DirectoryEntry = Get-DirectoryEntry -DirectoryPath $DirectoryPath -Credential $Credential -DirectoryEntryCache $DirectoryEntryCache } else { #$DirectoryEntry = [System.DirectoryServices.DirectoryEntry]::new($DirectoryPath) $DirectoryEntry = Get-DirectoryEntry -DirectoryPath $DirectoryPath -DirectoryEntryCache $DirectoryEntryCache } $DirectorySearcher = [System.DirectoryServices.DirectorySearcher]::new($DirectoryEntry) if ($Filter) { $DirectorySearcher.Filter = $Filter } $DirectorySearcher.PageSize = $PageSize $DirectorySearcher.SearchScope = $SearchScope ForEach ($Property in $PropertiesToLoad) { $null = $DirectorySearcher.PropertiesToLoad.Add($Property) } $SearchResultCollection = $DirectorySearcher.FindAll() #$null = $DirectorySearcher.Dispose() #$null = $DirectoryEntry.Dispose() $Output = [System.DirectoryServices.SearchResult[]]::new($SearchResultCollection.Count) $SearchResultCollection.CopyTo($Output, 0) #$null = $SearchResultCollection.Dispose() Write-Output $Output } function Test-PublicFunction_511f9c72-4f82-4b90-be93-ad7576481d5b { <# .SYNOPSIS Short synopsis of the function .DESCRIPTION Long description of the function .EXAMPLE ---------- EXAMPLE 1 ---------- This is a demo example with no parameters. It may not even be valid. Test-PublicFunction_511f9c72-4f82-4b90-be93-ad7576481d5b #> [OutputType([PSObject[]])] [CmdletBinding()] param ( # Comment-based help for $InputObject [Parameter(ValueFromPipeline)] [PSObject[]]$InputObject ) begin { } process { ForEach ($ThisObject in $InputObject) { Write-Output $ThisObject } } end { } } <#$ScriptFiles = Get-ChildItem -Path "$PSScriptRoot\*.ps1" -Recurse # Dot source any functions ForEach ($ThisScript in $ScriptFiles) { # Dot source the function . $($ThisScript.FullName) } # Add any custom C# classes as usable (exported) types $CSharpFiles = Get-ChildItem -Path "$PSScriptRoot\*.cs" ForEach ($ThisFile in $CSharpFiles) { Add-Type -Path $ThisFile.FullName -ErrorAction Stop } # Export any public functions $PublicScriptFiles = $ScriptFiles | Where-Object -FilterScript { ($_.PSParentPath | Split-Path -Leaf) -eq 'public' } $publicFunctions = $PublicScriptFiles.BaseName Export-ModuleMember -Function @('Add-DomainFqdnToLdapPath','Add-SidInfo','ConvertTo-DistinguishedName','ConvertTo-Fqdn','ConvertTo-HexStringRepresentation','ConvertTo-HexStringRepresentationForLDAPFilterString','ConvertTo-SidByteArray','Expand-AdsiGroupMember','Expand-IdentityReference','Expand-WinNTGroupMember','Find-AdsiProvider','Get-ADSIGroup','Get-ADSIGroupMember','Get-CurrentDomain','Get-DirectoryEntry','Get-TrustedDomainSidNameMap','Get-WinNTGroupMember','Invoke-ComObject','New-FakeDirectoryEntry','Resolve-IdentityReference','Search-Directory','Test-PublicFunction_511f9c72-4f82-4b90-be93-ad7576481d5b') #> Export-ModuleMember -Function @('Add-DomainFqdnToLdapPath','Add-SidInfo','ConvertTo-DistinguishedName','ConvertTo-Fqdn','ConvertTo-HexStringRepresentation','ConvertTo-HexStringRepresentationForLDAPFilterString','ConvertTo-SidByteArray','Expand-AdsiGroupMember','Expand-IdentityReference','Expand-WinNTGroupMember','Find-AdsiProvider','Get-ADSIGroup','Get-ADSIGroupMember','Get-CurrentDomain','Get-DirectoryEntry','Get-TrustedDomainSidNameMap','Get-WinNTGroupMember','Invoke-ComObject','New-FakeDirectoryEntry','Resolve-IdentityReference','Search-Directory','Test-PublicFunction_511f9c72-4f82-4b90-be93-ad7576481d5b') |