ADSI Report in PowerShell - Recursive groups - powershell

I have this code in PowerShell that looks into groups and users and creates a tab delimited txt file with the fields that I want.
However it only finds half of the information. I am trying to replace another process that generates an identical report and this report gathers around 580,000 lines of data where as this PowerShell report only generates around 300,000.
I think I need to look into groups recursively, but I'm not sure how to do that.
code:
#requires -version 2
$ScriptName = $MyInvocation.MyCommand.Name
$ADS_GROUP_TYPE_SECURITY_ENABLED = 0x80000000
$PageSize = 250 # Adjust as needed
# Create the Pathname object and enable its EscapedMode property
$ADS_ESCAPEDMODE_ON = 2
$ADS_SETTYPE_DN = 4
$ADS_FORMAT_X500_DN = 7
$Pathname = new-object -comobject "Pathname"
[Void] $Pathname.GetType().InvokeMember("EscapedMode", "SetProperty", $NULL, $Pathname, $ADS_ESCAPEDMODE_ON)
# Returns correctly escaped DN using Pathname object
function Get-EscapedPath {
param(
[String] $distinguishedName
)
[Void] $Pathname.GetType().InvokeMember("Set", "InvokeMethod", $NULL, $Pathname, ($distinguishedName, $ADS_SETTYPE_DN))
$Pathname.GetType().InvokeMember("Retrieve", "InvokeMethod", $NULL, $Pathname, $ADS_FORMAT_X500_DN)
}
# Returns a property from a ResultPropertyCollection if it's defined
function Get-SearchResultProperty {
param(
[System.DirectoryServices.ResultPropertyCollection] $properties,
[String] $propertyName
)
if ( $properties[$propertyName] ) {
$properties[$propertyName][0]
}
else {
""
}
}
# Returns a property from a DirectoryEntry if it's defined
function Get-DirEntryProperty {
param(
[System.DirectoryServices.DirectoryEntry] $dirEntry,
[String] $propertyName
)
if ( $dirEntry.$propertyName ) {
$dirEntry.$propertyName[0]
}
else {
""
}
}
write-progress $ScriptName "Enumerating groups"
$domain = [ADSI] ""
$searcher = [ADSISearcher] "(objectClass=group)"
$searcher.SearchRoot = $domain
$searcher.PageSize = $PageSize
$searcher.SearchScope = "subtree";
$searcher.PropertiesToLoad.AddRange(#("name","grouptype","distinguishedname","description","managedby","member"))
$searchResults = $searcher.FindAll()
$groupCounter = 0
$groupCount = $searchResults.Count
foreach ( $searchResult in $searchResults ) {
$properties = $searchResult.Properties
$domainName = "domainname"
$groupName = Get-SearchResultProperty $properties "name"
$groupType = Get-SearchResultProperty $properties "grouptype"
if ( ($groupType -band $ADS_GROUP_TYPE_SECURITY_ENABLED) -ne 0 ) {
$groupTypeString = "Security"
}
else {
$groupTypeString = "Distribution"
}
$groupDescription = Get-SearchResultProperty $properties "description"
$groupDN = Get-SearchResultProperty $properties "distinguishedname"
$groupManagedBy = Get-SearchResultProperty $properties "managedby"
$member = $properties["member"]
if ( $member ) {
$memberCounter = 0
$memberCount = ($member | measure-object).Count
foreach ( $memberDN in $member ) {
$memberDirEntry = [ADSI] "LDAP://$(Get-EscapedPath $memberDN)"
"" | select-object `
#{Name = "Domain"; Expression = {$domainName}},
#{Name = "Group Name"; Expression = {$groupName}},
#{Name = "Type"; Expression = {$groupTypeString}},
#{Name = "Description"; Expression = {$groupDescription}},
#{Name = "Distinguished Name"; Expression = {$groupDN}},
#{Name = "Managed By"; Expression = {$groupManagedBy}},
#{Name = "Members"; Expression = {$memberDN}},
#{Name = "Full Name"; Expression = {Get-DirEntryProperty $memberDirEntry "name"}},
#{Name = "User Name"; Expression = {Get-DirEntryProperty $memberDirEntry "samaccountname"}},
#{Name = "Display Name"; Expression = {Get-DirEntryProperty $memberDirEntry "displayname"}}
$memberCounter++
$memberPercent = ($memberCounter / $memberCount) * 100 -as [Int]
$params = #{
"Activity" = $ScriptName
"Completed" = $memberPercent -eq 100
"CurrentOperation" = "Enumerating '$groupDN'"
"PercentComplete" = $memberPercent
"Status" = "Groups: {0}/{1} [{2:P2}] - Members: {3}/{4} [{5:P2}]" -f
$groupCounter,
$groupCount,
($groupCounter / $groupCount),
$memberCounter,
$memberCount,
($memberCounter / $memberCount)
}
write-progress #params
}
}
else {
# Group contains no members
"" | select-object `
#{Name = "Domain"; Expression = {$domainName}},
#{Name = "Group Name"; Expression = {$groupName}},
#{Name = "Type"; Expression = {$groupTypeString}},
#{Name = "Description"; Expression = {$groupDescription}},
#{Name = "Distinguished Name"; Expression = {$groupDN}},
#{Name = "Managed By"; Expression = {$groupManagedBy}},
#{Name = "Members"; Expression = {""}},
#{Name = "Full Name"; Expression = {""}},
#{Name = "User Name"; Expression = {""}},
#{Name = "Display Name"; Expression = {""}}
}
$groupCounter++
$groupPercent = ($groupCounter / $groupCount) * 100 -as [Int]
$params = #{
"Activity" = $ScriptName
"Completed" = $groupPercent -eq 100
"CurrentOperation" = "Enumerating '$groupDN'"
"PercentComplete" = $groupPercent
"Status" = "Groups: {0}/{1} [{2:P2}]" -f
$groupCounter,
$groupCount,
($groupCounter / $groupCount)
}
write-progress #params
# Periodically force garbage collection to reduce memory usage
if ( ($groupCounter % $PageSize) -eq 0 ) {
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
}
$searchResults.Dispose()
EDIT:
I have tried using this line, just before the line containing "subtree":
$searcher.Filter = "(member:1.2.840.113556.1.4.1941:=*)"
and tried editing this line:
$searcher = [ADSISearcher] "(objectClass=group)"
to this
$searcher = [ADSISearcher] "(&(objectClass=group)(memberof:1.2.840.113556.1.4.1941L:=*))"
Neither work, it just returns immediately with no output, presumably as the filter is not picking up anything. I didn't have a filter before because I wanted everything

There's an LDAP filter (1.2.840.113556.1.4.1941) for that:
LDAP Matching Rule in Chain

Related

Find duplicates in array of hashtables

I have an array of hashtables and I need to find if there are elements who has the same Name.
I have this HasDuplicate function which return True or False if the array contains duplicate or not.
What I am doing here is that I am iterating through each element and add Name of it to another array, and then check it if it exists. But this code does not looks good, and I was thinking if there is another way of achieving this
# object looks like this
$array = #(
#{ Name = 'First', Passed = $True }
#{ Name = 'First', Passed = $False }
)
Function HasDuplicate
{
param($array)
$all = #()
foreach($item in $array)
{
$item_name = $item.Name
if($all -contains $item_name)
{
Write-Error "Duplicate name ""$item_name"""
return $True
}
else
{
$all += $item_name
}
}
return $False
}
Group-Object is probably the easiet, something like this:
$array = #(
#{ Name = 'First'; Passed = $True }
#{ Name = 'First'; Passed = $False }
)
$array.Name | Group-Object | Where-Object Count -GT 1
Another way you could do it using an hash table:
$array = #(
#{ Name = 'First'; Passed = $True }
#{ Name = 'First'; Passed = $False }
)
$h = #{}
$array | % {$h[$_.Name] += 1 }
$h.GetEnumerator() | Where value -GT 1
This might not be very good looking compared to the other answers, but you could just count your names in another hashtable then output the duplicates afterwards.
$array = #(
#{ Name = 'First'; Passed = $True }
#{ Name = 'First'; Passed = $False }
);
# Count names in array
$counts = #{}
foreach ($object in $array) {
$name = $object.Name
if (-not $counts.ContainsKey($name)) {
$counts[$name] = 0
}
$counts[$name] += 1
}
# Output duplicates
foreach ($name in $counts.Keys) {
if ($counts[$name] -gt 1) {
Write-Output ("Duplicate Name: " + $name)
}
}
Output:
Duplicate Name: First

Is it possible to autocomplete a Read-Host prompt with previous Get command output. Without saving an output file

I am writing a script to deploy VM hosts and I want to run a Get command to show them available options and then use their input with autocomplete from previous GET command. I do this because I want to avoid any typos that can be made during manual input.
I have tried using Select-string but I think it saves to .txt file and I don't want this to be saved in a txt file. I would rather have it saved in variable.
Get-VMHost | Select-Object -Property Name | Format-Table -Property Name
$VMHost = Read-Host -Prompt 'Please select the host for your VM
I expect the user to be able to autocomplete string with output from previously executed GET command. Please help if you can
Here is a New-ChoicePrompt function I use for similar purposes.
function New-ChoicePrompt { [cmdletBinding()]
param(
[parameter(mandatory=$true)]$Choices,
$Property,
$ReadProperty,
$ExprLabel,
[switch]$AllowManualInput,
[Scriptblock]$ReadPropertyExpr,
$ManualInputLabel = "Type my own"
)
if ( $choices[0] -isnot [string] -and !$property ) {"Please include New-ChoicePrompt -Property unless -Choices is an array of strings."; break}
if ( $choices[0] -is [string] -and ($property -or $ReadProperty) ) {"When New-ChoicePrompt -Choices is an array of strings, please omit -Property and -ReadProperty."; break}
#if ( $choices[0] -isnot [string] -and $allowManualInput ) {"When New-ChoicePrompt -Choices is a PSobject, please omit -AllowManualInput"; break}
$x = 0; $script:options = #()
$script:propty = $property
$script:choices = $choices
$manualInputLabel = "<" + $manualInputLabel + ">"
foreach ($item in $choices) { $value = $null
$x += 1
if ($property) { $value = $item | select -expand $property } `
else {$value = $item}
if ($readProperty) {
$readVal = $item | select -expand $readProperty
$row = new-object -type psObject -property #{Press = $x; 'to select' = $value; $readproperty = $readVal}
} ` #close if readProperty
elseif ($readPropertyExpr) `
{
$readVal = & $ReadPropertyExpr
$row = new-object -type psObject -property #{Press = $x; 'to select' = $value; $ExprLabel = $readVal}
}` #close if readPropertyExpr
else { $row = new-object -type psObject -property #{'to select' = $value; Press = $x} }
$script:options += $row
} #close foreach
if ($AllowManualInput) {
$row = new-object -type psObject -property #{'to select' = $manualInputLabel; Press = ($x + 1) }
$script:options += $row
} #close if allowManualInput
if ($ReadProperty) { $script:options | Select Press, "to select", $readproperty | ft -auto }
elseif ($ReadPropertyExpr) { $script:options | Select Press, "to select", $ExprLabel | ft -auto }
else { $script:options | Select Press, "to select" | ft -auto }
} #end function new-choicePrompt
Here is a usage example.
$vmhosts = Get-VMHost | sort Name
if ($vmhosts.count -gt 1) {
do {
new-choicePrompt -choices $vmhosts -property name
$in = read-host -prompt 'Please select a target host'
$range = $options | select -expand press
} #close do
until ($range -contains $in)
$selection = $options | where {$_.press -eq $in} | select -expand 'To select'
$choice = $choices | where {$_.#($propty) -eq $selection}
$vmHost = $choice
} else {$vmhost = $vmhosts} #close if multiple hosts
"Target host: " + $vmhost.name
If it was a function parameter, you could limit the values with [ValidateSet] like from here: https://www.mssqltips.com/sqlservertip/4205/powershell-parameters-part-ii--validateset-and-validatepattern/ It ends up supporting tab completion as well. If you set it to mandatory, it will prompt for it as well, if it's not given.
Function Pass-Set {
Param(
[ValidateSet("oro","plata")][string]$specificstring
)
Process
{
Write-Host "It must be one of two words; in this case $specificstring."
}
}
pass-set -specificstring oro
It must be one of two words; in this case oro.
pass-set -specificstring plata
It must be one of two words; in this case plata.
pass-set -specificstring plata2
Pass-Set : Cannot validate argument on parameter 'specificstring'. The argument "plata2" does not belong to the set "oro,plata" specified by the
ValidateSet attribute. Supply an argument that is in the set and then try the command again.
At line:1 char:26
+ pass-set -specificstring plata2
+ ~~~~~~
+ CategoryInfo : InvalidData: (:) [Pass-Set], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Pass-Set

Writing the output of Get-NetTCPConnection to an array - no output

I am trying to write the values of the PowerShell cmdlet Get-NetTCPConnection to an array but nothing is being written to the list.
$list= #()
$outputs = Get-NetTCPConnection
foreach ($output in $outputs) {
$obj = New-Object PSObject -Property #{
TheLocalAddress = "EMPTY"
TheLocalPort = "EMPTY"
TheRemoteAddress = "EMPTY"
TheRemotePort = "EMPTY"
}
$obj.TheLocalAddress = $output.LocalAddress
$obj.TheLocalPort = $output.LocalPort
$obj.TheRemoteAddress = $output.RemoteAddress
$obj.TheRemotePort = $output.RemotePort
$list += $obj
}
$list
If the prefix The isn't required for the properties, why not use
$list = Get-NetTCPConnection | Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort
Or a more efficient [PSCustomObject] ?
$list = foreach ($Conn in Get-NetTCPConnection) {
[PSCustomObject]#{
TheLocalAddress = $Conn.LocalAddress
TheLocalPort = $Conn.LocalPort
TheRemoteAddress = $Conn.RemoteAddress
TheRemotePort = $Conn.RemotePort
}
}
$list

Set WMI permissions for 'Domain users'

I've created two functions in PowerShell, one that creates the Namespace ROOT\CustomCMClasses and one that creates the class Test. This piece works fine:
Param (
$Namespace = 'CustomCMClasses',
$Class = 'Test'
)
Function New-WMINamespaceHC{
if (Get-WmiObject -Namespace 'root' -Class '__NAMESPACE' | Where-Object {$_.Name -eq $Namespace}) {
Write-Verbose "WMI Namespace 'root\$Namespace' exists"
}
else {
Write-Verbose "Create WMI namespace 'root\$Namespace'"
$Ns = [WMICLASS]'root:__Namespace'
$NewNamespace = $Ns.createInstance()
$NewNamespace.Name = $Namespace
$NewNamespace.Put()
}
}
Function New-WMIClassHC {
if (Get-WmiObject -List -Namespace "root\$Namespace" | Where-Object {$_.Name -eq $Class}) {
Write-Verbose "WMI Class '$Class' exists"
}
else {
Write-Verbose "Create WMI Class '$Class'"
$NewClass = New-Object System.Management.ManagementClass ("root\$Namespace", [String]::Empty, $Null);
$NewClass['__CLASS'] = $Class
$NewClass.Qualifiers.Add('Static', $true)
$NewClass.Properties.Add('Key', [System.Management.CimType]::String, $false)
$NewClass.Properties['Key'].Qualifiers.Add('Key', $true)
$NewClass.Properties.Add('Value1', [System.Management.CimType]::UInt32, $false)
$NewClass.Properties.Add('Value2', [System.Management.CimType]::String, $false)
$NewClass.Put()
}
}
At this point I need to grant Domain users permissions on the new namespace, so they are able to write data to it on their local client (no remoting needed). This is the point where I find a lot of information but am now stuck.
On the Microsoft blog there is a script I tried to tweak, but it's overly complicated for my needs and it failed so I found and tweaked the following code:
Function Add-WMIPermissions {
[CmdLetBinding()]
Param (
[String]$Principal = 'DOMAIN.NET\Domain users',
[String]$Namespace = 'CustomCMClasses'
)
Function Get-Sid {
Param (
$DSIdentity
)
$ID = new-object System.Security.Principal.NTAccount($DSIdentity)
Return $ID.Translate([System.Security.Principal.SecurityIdentifier]).toString()
}
$Sid = Get-Sid $Principal
$WMISDDL = "A;CI;CCWP;;;$Sid"
$WMISDDLPartialMatch = "A;\w*;\w+;;;$Sid"
$security = Get-WmiObject -Namespace root\$Namespace -Class __SystemSecurity
$binarySD = #($null)
$result = $security.PsBase.InvokeMethod('GetSD',$binarySD)
$converter = New-Object system.management.ManagementClass Win32_SecurityDescriptorHelper
$CurrentWMISDDL = $converter.BinarySDToSDDL($binarySD[0])
if (($CurrentWMISDDL.SDDL -match $WMISDDLPartialMatch) -and ($CurrentWMISDDL.SDDL -notmatch $WMISDDL)) {
$NewWMISDDL = $CurrentWMISDDL.SDDL -replace $WMISDDLPartialMatch, $WMISDDL
}
else {
$NewWMISDDL = $CurrentWMISDDL.SDDL += '(' + $WMISDDL + ')'
}
$WMIbinarySD = $converter.SDDLToBinarySD($NewWMISDDL)
$WMIconvertedPermissions = ,$WMIbinarySD.BinarySD
if ($CurrentWMISDDL.SDDL -match $WMISDDL) {
Write-Verbose 'Current WMI Permissions matches desired value'
}
else {
$result = $security.PsBase.InvokeMethod('SetSD',$WMIconvertedPermissions)
if($result='0'){
Write-Verbose 'WMI permissions applied'
}
}
}
Add-WMIPermissions -Verbose
It's clearly stating that the permissions are correctly applied but the user still can't write data to WMI. Working with WMI is new to me, so any help is greatly appreciated.
The test code to see if a regular user (Domain users) has permissions:
$WMIClass = [WMICLASS]('root\' + $Namespace + ':' + $Class)
$WMIInstance = $WMIClass.CreateInstance()
$WMIInstance.Key = 'Unique value identifier 5'
$WMIInstance.Value1 = 101
$WMIInstance.Value2 = 'Status Ok'
$WMIInstance.Put()
Solved the problem for Partial Write:
Function Set-WMIPermissionsHC {
Param (
[String]$Namespace = 'CustomCMClasses',
[String]$Class = 'Test',
[String]$Account = 'DOMAIN\Domain users',
[String]$Computer = $env:COMPUTERNAME
)
Function Get-Sid {
Param (
$Account
)
$ID = New-Object System.Security.Principal.NTAccount($Account)
Return $ID.Translate([System.Security.Principal.SecurityIdentifier]).toString()
}
$SID = Get-Sid $Account
$SDDL = "A;CI;CCSWWP;;;$SID"
$DCOMSDDL = "A;;CCDCRP;;;$SID"
$Reg = [WMICLASS]"\\$Computer\root\default:StdRegProv"
$DCOM = $Reg.GetBinaryValue(2147483650,'software\microsoft\ole','MachineLaunchRestriction').uValue
$Security = Get-WmiObject -ComputerName $Computer -Namespace "root\$Namespace" -Class __SystemSecurity
$Converter = New-Object System.Management.ManagementClass Win32_SecurityDescriptorHelper
$BinarySD = #($null)
$Result = $Security.PsBase.InvokeMethod('GetSD', $BinarySD)
$OutSDDL = $Converter.BinarySDToSDDL($BinarySD[0])
$OutDCOMSDDL = $Converter.BinarySDToSDDL($DCOM)
$NewSDDL = $OutSDDL.SDDL += '(' + $SDDL + ')'
$NewDCOMSDDL = $OutDCOMSDDL.SDDL += '(' + $DCOMSDDL + ')'
$WMIbinarySD = $Converter.SDDLToBinarySD($NewSDDL)
$WMIconvertedPermissions = ,$WMIbinarySD.BinarySD
$DCOMbinarySD = $Converter.SDDLToBinarySD($NewDCOMSDDL)
$DCOMconvertedPermissions = ,$DCOMbinarySD.BinarySD
$Result = $Security.PsBase.InvokeMethod('SetSD', $WMIconvertedPermissions)
$Result = $Reg.SetBinaryValue(2147483650,'software\microsoft\ole','MachineLaunchRestriction', $DCOMbinarySD.binarySD)
Write-Verbose 'WMI Permissions set'
}
Thanks to this blog and the Microsoft blog for explaining the permissions

Unable to trim/trimend "$"

I am trying to modify a PS script from online resource:
Trap {"Error: $_"; Break;}
$D = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$Domain = [ADSI]"LDAP://$D"
$Searcher = New-Object System.DirectoryServices.DirectorySearcher
$Searcher.PageSize = 200
$Searcher.SearchScope = "subtree"
$Searcher.Filter = "(objectCategory=computer)"
$Searcher.PropertiesToLoad.Add("samAccountName") > $Null
$Searcher.PropertiesToLoad.Add("lastLogon") > $Null
# Create hash table of users and their last logon dates.
$arrComp = #{}
# Enumerate all Domain Controllers.
ForEach ($DC In $D.DomainControllers)
{
$Server = $DC.Name
$Searcher.SearchRoot = "LDAP://$Server/" + $Domain.distinguishedName
$Results = $Searcher.FindAll()
ForEach ($Result In $Results)
{
$DN = $Result.Properties.Item("samAccountName")
$LL = $Result.Properties.Item("lastLogon")
If ($LL.Count -eq 0)
{
$Last = [DateTime]0
}
Else
{
$Last = [DateTime]$LL.Item(0)
}
If ($Last -eq 0)
{
$LastLogon = $Last.AddYears(1600)
}
Else
{
$LastLogon = $Last.AddYears(1600).ToLocalTime()
}
If ($arrComp.ContainsKey("$DN"))
{
If ($LastLogon -gt $arrComp["$DN"])
{
$arrComp["$DN"] = $LastLogon
}
}
Else
{
$arrComp.Add("$DN", $LastLogon)
}
}
}
Script above give me the computername & its' last logon date, however the computernames are having "$" at the end. I would like to trim the "$" in order for me to use it remove the computer from AD later. However my script is not working.
$Compdollar = $arrComp.getEnumerator() | Select-Object Key | out-string
$AllComp = #()
Foreach ($inactD in $Compdollar) {
$AllComp += $inactD.Trim("$")
}
$Allcomp
The output is still computer name with "$", can anyone tells me why it wasn't trimmed?
Don't use double quotes with a $ as it is treated like a variable. Use single quotes instead.
$AllComp += $inactD.Trim('$')
Or use the backtick to escape the dollar sign.
$AllComp += $inactD.Trim("`$")