I know this is pulling quite a bit of data, but at present it's capping my memory consumption when I run it on my local machine. The good news is, it's returning the output that I need. Can someone help me with performance optimization? So far, I haven't done much for fear of messing up a script that returns my desired output. Thanks in advance for any suggestions.
#// Start of script
#// Get year and month for csv export file
$DateTime = Get-Date -f "yyyy-MM"
#// Set CSV file name
$CSVFile = "C:\Temp\AD_Groups"+$DateTime+".csv"
#// Create emy array for CSV data
$CSVOutput = #()
Measure-Command {
#// Get all AD groups in the domain
$ADGroups = Get-ADGroup -Filter "GroupScope -ne 'DomainLocal' -AND GroupCategory -eq 'Security' -AND Member -like '*'" -SearchBase "OU=SHS, DC=shs, DC=net" -Properties Member #-ResultSetSize 1000 Name -like '*''s*' -AND
#// Set progress bar variables
$i=0
$tot = $ADGroups.count
foreach ($ADGroup in $ADGroups) {
#// Set up progress bar
$i++
$status = "{0:N0}" -f ($i / $tot * 100)
Write-Progress -Activity "Exporting AD Groups" -status "Processing Group $i of $tot : $status% Completed" -PercentComplete ($i / $tot * 100)
#// Ensure Members variable is empty
$Members = ""
#// Get group members which are also groups and add to string
$MembersArr = Get-ADGroup $ADGroup.DistinguishedName -Properties Member | Select-Object -ExpandProperty Member
if ($MembersArr) {
foreach ($Member in $MembersArr) {
$ADObj = Get-ADObject -filter {DistinguishedName -eq $Member}
#// Initialize regex variable
$matches = ""
if ($ADObj.ObjectClass -eq "user") {
$UserObj = Get-ADObject -filter {DistinguishedName -eq $Member}
$match = $UserObj -match '\([a-zA-Z0-9]+\)'
$empid=$matches[0] -replace ".*\(","" -replace "\)",""
if ($UserObj.Enabled -eq $False) {
continue
}
$Members = $empid
}
# Check for null members to avoid error for empty groups
if ([string]::IsNullOrEmpty($Members)) {
continue
}
$HashTab = [ordered]#{
"GroupName" = $ADGroup.Name -replace "'s", "''s"
"GroupCategory" = $ADGroup.GroupCategory
"GroupScope" = $ADGroup.GroupScope
"MemberID" = if([string]::IsNullOrEmpty($empid)){""}
else{$empid}
}
#// Add hash table to CSV data array
$CSVOutput += New-Object PSObject -Property $HashTab
}
}
#// Export to CSV files
$CSVOutput | Sort-Object Name, Member | Export-Csv $CSVFile -NoTypeInformation
}
}
I've experienced this too with code that loops through thousands of accounts. The problem is that the garbage collector doesn't have time during the loop to clean up, since your code is constantly doing something. In .NET, I'd call .Dispose() manually to make sure stuff is cleaned up, but here you can't.
You can try calling [System.GC]::Collect() after you assign each variable in the loop. For example, after $MembersArr = and after $ADObj = to (hopefully) make it deallocate the memory used for the previous value.
Also, I think that $UserObj = Get-ADObject... line should be calling Get-ADUser, not Get-ADObject. As it is, $UserObj.Enabled will never have a value and your continue will never be hit.
But you can save yourself the use of Get-ADUser entirely by asking for the userAccountControl value in Get-ADObject and using that to determine if the user is disabled. For example:
$ADObj = Get-ADObject -filter {DistinguishedName -eq $Member} -Properties userAccountControl
# Clean up the old $ADObj value
[System.GC]::Collect()
#// Initialize regex variable
$matches = ""
if ($ADObj.ObjectClass -eq "user") {
$match = $ADObj -match '\([a-zA-Z0-9]+\)'
$empid=$matches[0] -replace ".*\(","" -replace "\)",""
if ($ADObj.userAccountControl -band 2) {
continue
}
$Members = $empid
}
The $ADObj.userAccountControl -band 2 condition checks is a bitwise AND comparison to check if the second bit of the userAccountControl value is set, which means that the account is disabled.
Related
I have this portion of code here that has worked in the past in multiple AD environments, however after testing within a new AD environment, I am getting no output to CSV or any errors being thrown. The size of the CSV file is always zero.
if (![string]::IsNullOrEmpty($searchbase))
{
$ADComputers = get-adcomputer -searchBase $searchbase -filter * -properties * -ResultPageSize $resultpagesize
}
else
{
$ADComputers=Get-ADComputer -Filter * -Properties * -ResultPageSize $resultpagesize
}
$data = #()
foreach ($computer in $ADComputers) {
$computer.member| foreach-object {$members += $_}
$computer.memberof | foreach-object {$memberof += $_}
$memstr = ($members -join ";")
$memstr2 = ($memberof -join ";")
$ADcomp = Get-ADComputer $computer -properties logonCount, ManagedBy | select-object logonCount, ManagedBy
$row = New-Object -TypeName psobject -Property #{
PrincipalID = $script:ctr;
logonCount=$ADcomp.logonCount;
ManagedBy=$ADcomp.ManagedBy;
}
$data += $row
$script:ctr++
}
$data | Export-Csv "ADComputers.csv" -NoTypeInformation
I'm not sure exactly where to go from here because I have tested multiple different options, so any help would be greatly appreciated!
The only reason you get no output is that $ADComputers has no elements. This may be related to a value in the variable $searchbase that does not exist or simply has no computer accounts in it.
But here are some general recommendations:
you do:
if (![string]::IsNullOrEmpty($searchbase))
you could also do:
If ($searchbase)
In principle if you have different scenarios to cover and so the parameter may change take a look at splatting.
Then you query all computers with all available properties but later in the loop you query the specific computer again which is not necessary. Also you should avoid adding elements to an array by using +=, this causes to rebuild the array each time which is slow.
Furthermore $computer.memberof is already an array holding the information but you pipe it to foreach and build a new array holding the identical information only to join it later to a string.
If this is not part of function I don't know why you raise the scope of the variable $ctr from local to script think this is not necessary.
Putting this all together you could do:
#Define HashTable for splatting
$parametersHt = #{
filer='*'
properties='*'
resultsetpagesize=$resultpagesize
}
#If searchbase is specified add parameter to splatting HashTable
If ($searchbase){
$parametersHt.add('Searchbase',$searchbase)
}
#Get computers
$ADComputers = get-adcomputer #parametersHt
#Set Counter
$ctr = 0
$data = #(
foreach ($computer in $ADComputers){
$ctr++
[PSCustomObject]#{
PrincipalId = $ctr #Really the counter here - not $computer.samaccountname?
logonCount=$computer.logonCount
manageddBy=$computer.ManagedBy
memberof=($computer.memberof -join ";") #added this, but in your code sample you don't return this value, if not needed remove. btw. a computer can be memberof a group but it can't have members
}
}
)
$data | Export-Csv -path ".\ADComputers.csv" -NoTypeInformation
My goal is to dump a CSV of our AD groups, their members, and whether those member objects are enabled, but I'm running into a strange (probably self-inflicted) issue, wherein a Foreach-Object loop is behaving unexpectedly.
The output almost works. It dumps a CSV file. The file has rows for each group, populated with the correct group-related data, and the right number of rows, following the number of group members. However, group member properties on those rows is repeated, showing the same user data over and over for each groupmember result, apparently following the properties of the last returned object from Get-ADGroupMember.
To try to diagnose the issue, I added the line Write-Host $GroupMember.Name -ForegroundColor Gray. This is how I knew the entries in the CSV were the last-returned results for each group. Confusingly, the console correctly echoes each group member's display name.
I'm assuming there's some kind of logic error at work here, but I have had no luck finding it. Any help would be appreciated!
clear
Import-Module ActiveDirectory
# CONFIG ========================================
# Plant Number OU to scan. Used in $CSV and in Get-ADComputer's search base.
$PlantNumber = "1234"
# FQDN of DC you want to query against. Used by the Get-AD* commands.
$ServerName = "server.com"
# Output directory for the CSV. Default is [Environment]::GetFolderPath("Desktop"). Used in $CSV. NOTE: If setting up as an automated task, change this to a more sensible place!
$OutputDir = [Environment]::GetFolderPath("Desktop")
# CSV Output string. Default is "$OutputDir\$PlantNumber"+"-ComputersByOS_"+"$(get-date -f yyyy-MM-dd).csv" (+'s used due to underscores in name)
$CSV = "$OutputDir\$PlantNumber"+"GroupMembers_"+"$(get-date -f yyyy-MM-dd).csv"
# Create empty array for storing collated results
$collectionTable = #()
# Get AD groups, return limited properties
Get-AdGroup -filter * -Property Name, SamAccountName, Description, GroupScope -SearchBase "OU=Security Groups,OU=$PlantNumber,OU=Plants,DC=SERVER,DC=COM" -server $ServerName | Select SamAccountName, Description, GroupScope | Foreach-Object {
Write-Host "Querying" $_.SamAccountName "..."
#Initialize $collectionRow, providing the columns we want to collate
$collectionRow = "" | Select GroupName, GroupScope, GroupDesc, MemberObjectClass, MemberName, MemberDisplayName, Enabled
# Populate Group-level collectionRow properties
$collectionRow.GroupName = $_.SamAccountName
$collectionRow.GroupDesc = $_.Description
$collectionRow.GroupScope = $_.GroupScope
# Process group members
Get-ADGroupMember -Identity ($collectionRow.GroupName) -Server $ServerName -Recursive | ForEach-Object {
$GroupMember = $_
# Echo member name to console
Write-Host $GroupMember.Name -ForegroundColor Gray
$collectionRow.MemberName = $GroupMember.SamAccountName
$collectionRow.MemberDisplayName = $GroupMember.name
$collectionRow.MemberObjectClass = $GroupMember.ObjectClass
# If the member object is a user, collect some additional data
If ($collectionRow.MemberObjectClass -eq "user") {
Try {
$collectionRow.Enabled = (Get-ADUser $GroupMember.SamAccountName -Property Enabled -ErrorAction Stop).Enabled
If ($collectionRow.Enabled -eq "TRUE") {$collectionTable += $collectionRow}
}
Catch {
$collectionRow.Enabled = "ERROR"
$collectionTable += $collectionRow
}
}
}
}
Write-Host "`n"
# Attempt to save results to CSV. If an error occurs, alert the user and try again.
$ExportSuccess = 'false'
while ($ExportSuccess -eq 'false') {
Try
{
# Export results to $CSV
$collectionTable| Export-csv $CSV -NoTypeInformation -ErrorAction Stop
# If the above command is successful, the rest of the Try section will execute. If not, Catch is triggered instead.
$ExportSuccess = 'true'
Write-Host "`nProcessing complete. Results output to"$CSV
}
Catch
{
Write-Host "Error writing to"$CSV"!" -ForegroundColor Yellow
Read-Host -Prompt "Ensure the file is not open, then press any key to try again"
}
}
There are many things from your code you need to fix, I'll just point out the most important ones:
Don't use #() and +=
You keep using 'True' and 'False' which are strings, PowerShell booleans are $true and $false.
There is also too much redundant code. Also ForEach-Object is slow, if your groups have many members and since you're using -Recursive it's better to use a fast loop instead.
$PlantNumber = "1234"
$ServerName = "server.com"
$OutputDir = [Environment]::GetFolderPath("Desktop")
$fileName = "${PlantNumber}GroupMembers_$(Get-Date -f yyyy-MM-dd).csv"
$CSV = Join-Path $OutputDir -ChildPath $fileName
# $collectionTable = #() => Don't do this to collect results, ever
$adGroupParams = #{
# Name and SAM are default, no need to add them
Properties = 'Description', 'GroupScope'
SearchBase = "OU=Security Groups,OU=$PlantNumber,OU=Plants,DC=SERVER,DC=COM"
Server = $ServerName
Filter = '*'
}
# Get AD groups, return limited properties
$collectionTable = foreach($group in Get-AdGroup #adGroupParams)
{
Write-Host "Querying $($group.samAccountName)..."
foreach($member in Get-ADGroupMember $group -Server $ServerName -Recursive)
{
# if this member is 'user' the Enabled property
# will be a bool ($true / $false) else it will be $null
$enabled = if($member.ObjectClass -eq 'User')
{
(Get-ADUser $member).Enabled
}
[pscustomobject]#{
GroupName = $group.SamAccountName
GroupDesc = $group.Description
GroupScope = $group.GroupScope
MemberName = $member.SamAccountName
MemberDisplayName = $member.Name
MemberObjectClass = $member.ObjectClass
Enabled = $enabled
}
}
}
as i understand, you need to export list of groups with members to a csv file and know if member accounts are enabled or not, if this what you want, you can check the below code
$output = #()
Import-Module ActiveDirectory
$ServerName = "server.com"
$PlantNumber = "1234"
$OutputDir = [Environment]::GetFolderPath("Desktop")
$CSV = "$OutputDir\$PlantNumber"+"GroupMembers_"+"$(get-date -f yyyy-MM-dd).csv"
$groups = Get-AdGroup -filter * -Property Description -SearchBase "OU=Security Groups,OU=$PlantNumber,OU=Plants,DC=SERVER,DC=COM" -server $ServerName
foreach ($group in $groups){
$members = Get-ADGroupMember -Identity $group.SamAccountName -Recursive
foreach ($member in $members){
$output += [pscustomobject]#{
GroupName = $group.SamAccountName
GroupDesc = $group.Description
GroupScope = $group.GroupScope
MemberName = $member.samaccountname
MemberDisplayName = $member.Name
MemberObjectClass = $member.ObjectClass
Enabled = $(Get-ADUser -Identity $member.samaccountname).enabled
}
}
}
$output | Export-Csv $CSV -NoTypeInformation
I am explicitly NOT refering your code. I'd just like to show how I would approach this task. I hope it'll help you anyway.
$Server = 'Server01.contoso.com'
$SearchBase = 'OU=BaseOU,DC=contoso,DC=com'
$CSVOutputPath = '... CSV path '
$ADGroupList = Get-ADGroup -Filter * -Properties Description -SearchBase $SearchBase -Server $Server
$ADUserList = Get-ADUser -Filter * -Properties Description -SearchBase $SearchBase -Server $Server
$Result =
foreach ($ADGroup in $ADGroupList) {
$ADGroupMemberList = Get-ADGroupMember -Identity $ADGroup.sAMAccountName -Recursive
foreach ($ADGroupmember in $ADGroupMemberList) {
$ADUser = $ADUserList | Where-Object -Property sAMAccountName -EQ -Value $ADGroupmember.sAMAccountName
[PSCustomObject]#{
ADGroupName = $ADGroup.Name
ADGroupDescription = $ADGroup.Description
ADGroupMemberName = $ADUser.Name
ADGroupMemberSamAccountName = $ADUser.sAMAccountName
ADGroupMemberDescription = $ADUser.Description
ADGroupMemberStatus = if ($ADUser.Enabled) { 'enabled' }else { 'diabled' }
}
}
}
$Result |
Export-Csv -Path $CSVOutputPath -NoTypeInformation -Delimiter ',' -Encoding utf8
It'll output only the a few properties but I hope you get the idea.
BTW: The properties DistinguishedName, Enabled, GivenName, Name, ObjectClass, ObjectGUID, SamAccountName, SID, Surname, UserPrincipalName are included in the default return set of Get-ADUser and the properties DistinguishedName, GroupCategory, GroupScope, Name, ObjectClass, ObjectGUID, SamAccountName, SID are included in the default return set of Get-ADGroup. You don't need to query them explicitly with the parameter -Properties.
I have 10 security groups all that are called " 'companyname' RDS Users"
I am trying to create a script that does the following: List all the groups and then list all of the members excluding the disabled members, then have it email a csv. I have done the following but cant get the disabled user excluded.
The Script belows shows how far i got but the disabled users show in there which basically means the script is pointless.
$mailServer = ""
$mailFrom = ""
$mailTo = ""
$mailSubject = ""
$file = "somepath\RDSUsers.csv"
Import-Module ActiveDirectory
$US = Get-ADUser -Filter * -Property Enabled |where {$_.Enabled -eq "True"}| FT Name, Enabled -Autosize
$Groups = (Get-AdGroup -filter * | Where {$_.name -like "*RDS Users" -and $_.name -ne "RDS Users"}| select name -expandproperty name)
$Table = #()
$Record = [ordered]#{
"Group Name" = ""
"Name" = ""
"Username" = ""
}
Foreach ($Group in $Groups)
{
$Arrayofmembers = Get-ADGroupMember -identity $Group |select name,samaccountname
foreach ($Member in $Arrayofmembers)
{
$Record."Group Name" = $Group
$Record."Name" = $Member.name
$Record."UserName" = $Member.samaccountname
$objRecord = New-Object PSObject -property $Record
$Table += $objrecord
}
}
if ($Table -eq "RDS Users") {}
$Table
there is usualy a line here that sends the email with excel attachment
The following should produce the output you want in the $Table variable. You can then pipe $Table to one of the format-* commands.
Import-Module ActiveDirectory
$US = Get-ADUser -Filter "Enabled -eq '$true'" -Property Enabled
$Groups = Get-ADGroup -Filter "Name -like '*RDS Users' -and Name -ne 'RDS Users'" |
Select-Object -ExpandProperty Name
$Table = Foreach ($Group in $Groups)
{
try
{
$Arrayofmembers = Get-ADGroupMember -Identity $Group -ErrorAction Stop | Select-Object Name, SamAccountName
$compare = Compare-Object -ReferenceObject $US -DifferenceObject $Arrayofmembers -ExcludeDifferent -IncludeEqual -PassThru -Property SamAccountName -ErrorAction Stop |
Select-Object Name, SamAccountName
$compare | ForEach-Object {
[pscustomobject]#{
"Group Name" = $Group
"Name" = $_.Name
"UserName" = $_.SamAccountName
}
}
}
catch
{
[pscustomobject]#{
"Group Name" = $Group
"Name" = $null
"UserName" = $null
}
Continue
}
}
$Table
Explanation:
The Get-ADGroupMember command will not provide the Enabled property of its returned objects. You will need to feed its output into another command like Get-ADUser for that data. Since you already stored all of the enabled users in $US, we can simply compare $US collection to the results of each Get-ADGroupMember output.
I removed most of the Where-Object commands in favor of using the -Filter parameter on the AD commands. Almost always, the -Filter parameter will be faster especially when you are comparing AD indexed attributes like Name and Enabled.
You do not need to store each output object in a variable unless you are going to further manipulate it. This is why $Record was removed. Instead, all returned objects are stored in the array $Table. I removed the += operator mainly because of its inefficiency when repeatedly building arrays. Also, you can simply set a variable to the output of a foreach loop, which will result in the array you require. Since we created a custom object on each loop iteration and provided the properties at the time of declaration, [ordered] is not required. However, if you create the hash table first and then create a corresponding object, you will potentially need to use [ordered]. As an aside when you are creating custom objects that are involved in a loop, it is usually best practice to create a new object each time. Otherwise, you could unintentionally update values on the wrong objects. Just because you add an object to an array, you can still update its properties after the fact.
The Compare-Object command ties everything together. The -ExcludeDifferent -IncludeEqual parameter combination will only output objects with matching property values. Since we are comparing $Arrayofmembers and $US, that is ideal. The -PassThru switch allows the objects to be returned with all of the properties that were passed into the command. Then you can use the Select-Object command to pick which properties matter to you.
I have a script that searches all machines in a domain and is pulling details about them and presents them in a report.
ipmo ActiveDirectory ;
$ADSearchBase = "DC=contoso,DC=chelu,DC=ro,DC=com"
write-host
write-host "Preparing your data..."
write-host
$AD_Results = Get-ADComputer -filter '(Enabled -eq $true)' -SearchScope Subtree -SearchBase $ADSearchBase -properties Description, PasswordNeverExpires, LastLogonTimeStamp, PasswordLastSet, operatingSystem, operatingSystemServicePack, whenCreated, distinguishedname, canonicalname
$count = $AD_Results.count
"Analyzing $count machines..."
# MAIN LOOP
ForEach ($Result In $AD_Results)
{
$i++
if ($i % 16 -eq 0) { $i }
$ComputerName=$result.name
$OS = $result.operatingSystem
$DESC = $result.Description
$DN = $result.distinguishedname
$PNE = $result.passwordneverexpires
if ($ComputerName.Length -ge 15)
{
$ComputerName = $ComputerName.substring(0,15)
}
## BEGIN TIME CONVERSIONS
$LLTS = 0 #AD LastLogonTimestamp
$PLS = 0 #AD PasswordLastSet
If ($result.lastLogonTimeStamp -eq $Null)
{
$T = [Int64]0
}
Else
{
$T = [DateTime]::FromFileTime([Int64]::Parse($result.lastlogontimestamp)).ToString("dd/MM/yyyy HH:mm:ss")
}
$LLTS = $T
$WCR = $result.whencreated.ToString("dd/MM/yyyy HH:mm:ss")
If (!($result.passWordLastSet -eq $Null))
{
$PLS = $result.passwordLastSet.ToString("dd/MM/yyyy HH:mm:ss")
}
## END TIME CONVERSIONS
# 1/2 is in Exceptions?
if ($DN -match "Domain Controllers") {"$computername : DOMAIN CONTROLLER -> Skipping..." ; $Skipped++ ; continue}
if ($DN -match "HVCL") {"$computername : Virtual Cluster Name -> Skipping..." ; $Skipped++ ; continue}
#2/2: isWin?
if ($result.operatingSystem -notlike '*windows*')
{
$Skipped++
continue
}
$isServer=0
if (($DN -match "Servers") -or ($result.operatingSystem -like '*server*'))
{$isServer=1}
The script is skipping some machines based on their DN (distinguishedname) as it can be seen in the
"# 1/2 is in Exceptions?" and in "#2/2: isWin?"
Meanwhile I got a request from a user to except some other (extra) machines that cannot be sorted using the initial query in the AD, which is:
$AD_Results = Get-ADComputer -filter '(Enabled -eq $true)' -SearchScope Subtree -SearchBase $ADSearchBase -properties Description, PasswordNeverExpires, LastLogonTimeStamp, PasswordLastSet, operatingSystem, operatingSystemServicePack, whenCreated, distinguishedname, canonicalname
Basically the user wants to except from the report some specific machines (machine1, machine2, machine3) which are not real computer accounts but "connection points" for clustered resources. Now, there are 2 ways to do that:
To use a script to find all these"connection points" for clustered resources. The only way to detect CNO and VCO is to look at "Service Principal name" attribute from the computer object. If you find "MSClusterVirtualServer" then the object is a CNO or a VCO
Here is what I could come up with:
$serviceType="MSClusterVirtualServer"
$spns = #{}
$filter = "(servicePrincipalName=$serviceType/*)"
$domain = New-Object System.DirectoryServices.DirectoryEntry
$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.SearchRoot = $domain
$searcher.PageSize = 1000
$searcher.Filter = $filter
$results = $searcher.FindAll()
foreach ($result in $results){
$account = $result.GetDirectoryEntry()
foreach ($spn in $account.servicePrincipalName.Value){
if($spn.contains("$serviceType/")){
$spns[$("$spn`t$($account.samAccountName)")]=1;
}
}
}
$spns.keys | sort-object
To actually create a "whitelist" or "blacklist" where to include machines by names, assuming that in the future some other users might come up with similar requests to except machines from showing up in the report, and that these last machines might not be Virtual Clusters. I prefer this method. What I did is to create an LDAP filter to find that 3 specific machines. Here it is:
(&(&(&(objectCategory=computer)(objectClass=computer)(|(cn=machine1)(cn=machine2)(cn=machine3)))))
QUESTION: Can you help me put together an IF clause that will point towards a "whitelist" in csv format that will contain the names list of the machines to be excepted from the report? The whitelist should reside on the same folder where the script is residing. Should I use the above LDAP filter? How do I do that?
Based on your $AD_Result I would try something along these lines:
ForEach ($exception In (Get-Content "exceptions.txt")) {
$AD_Result = $AD_Result | ? { $_.Name -ine $exception }
}
Why did you want your exceptions file in csv format?
I need some help writing a script as i am struggling to understand the logic.
I basically have a list of user ids that i need to check to see if they have two certain AD groups. If they have, these need to be outputted into a csv and highlighted.
Can anyone help to get me started? I need to use the Quest Powershell cmdlets
Here is the code
$textFileContents = Get-Content C:\temp\powershell\users.txt
$results = #()
foreach($username in $textFileContents){
$groups = get-qaduser $username |select -expand memberof
if ($groups -match "grpuip1" -and $groups -match "group2"){
echo $group
}
}
check this to begin :
"user1","user2" | foreach {
$groups = get-qaduser $_ |select -expand memberof
if ($groups -match "GROUP1" -and $groups -match "GROUP2"){
echo $_
}
}
I'd use the cmdlet Get-QADMemberOf instead of Get-QADUser. There's nothing wrong with what you're doing, but it's retrieving more information than you need.
Try this to start with:
$textFileContents = Get-Content C:\temp\powershell\users.txt
# Rather than initializing the array, and adding new elements,
# just output each element of the loop to the pipeline, and
# assign the results of the whole pipeline to the variable.
# This is *much* faster than adding to an array
$results = $textFileContents | ForEach-Object {
$userGroups = Get-QADMemberOf $_
if ($userGroups -contains "group1" -and $userGroups -contains "group2") {
New-Object -TypeName PSObject -Property #{"UserName" = $_; "Groups" = ($userGroups -join ",");}
}
}
$results | ConvertTo-Csv -NoTypeInformation | Set-Content C:\Filename.txt