Nested ForEach loop running incredibly slowly - powershell

I have written a script That is designed to search an OU for all the users contained whiten. it then gets all groups that that user it finds are members of, and if the DistinguishedName of a group is found to match a string it will remove thate user from that group specifically.
However it is incredibly slow taking anywhere from 5-45 seconds an entry. is this normal or is there any way for me to expedite it?
$OUs = "OU=Terminated,OU=####,OU=####,DC=####,DC=####"
foreach ($ou in $OUs)
{
$users = Get-ADUser -SearchBase $ou -Filter *
foreach ($user in $users)
{
$groups = Get-ADPrincipalGroupMembership -Identity $User | ? {$_.distinguishedName -like "*Groups_I_Want_Removed*" }
foreach($group in $groups)
{
Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $group -whatif
}
}
}
$results = foreach ($OU in $OUs)
{
get-aduser -SearchBase $OU -filter * -Properties MemberOf | ? MemberOf -like "*Distribution Lists*"
}
$results | Export-Csv .\Output_of_users_remaining.csv -NoTypeInformation
My concern is that this script is going through thousands of users and the last time I ran it it was unable to finish within 3 hours and I stopped it at the end of the day. in that time it said it had corrected about 5000~ish users.

Related

PowerShell - add an exclusion into Remove-ADGroupMember command?

When somebody leaves my organization, we remove all AD group memberships apart from the PrimaryGroup which is Domain Users. We often process these in batches, so pull the affected usernames from a CSV file.
I have the following code, and while it does the job of deleting all group memberships, I get an error for each user:
The user cannot be removed from a group because the group is currently the user's primary group
Whilst it does the job, how can I "clean up" the process to avoid this message each time? Is there a way to exclude Domain Users from the groups it removes the user from, or should I do this another way?
$users = Import-Csv "c:\temp\leavers.csv"
foreach ($user in $users) {
Get-ADPrincipalGroupMembership -identity $user.username | foreach {Remove-ADGroupMember $_ -Members $user.username -Confirm:$false}
}
You can use Where-Object for filtering those groups that are not in an array of groups to exclude. In case you only want to filter for 1 specific group, you would use -NE instead of -NotIn in below example.
$groupToExclude = 'Domain Users', 'someOtherGroup'
$users = Import-Csv "c:\temp\leavers.csv"
foreach ($user in $users) {
try {
Get-ADPrincipalGroupMembership -Identity $user.username |
Where-Object Name -NotIn $groupToExclude |
Remove-ADGroupMember -Members $user.username -Confirm:$false
}
catch {
Write-Warning $_.Exception.Message
}
}
If you get the ADUser object before the ADGroup memberships, you can get the PrimaryGroup of the user and ensure that the list of groups to remove from are not its PrimaryGroup:
$users = Import-Csv "c:\temp\leavers.csv"
foreach ($user in $users) {
$primaryGroup = ( Get-ADUser $user.UserName -Properties PrimaryGroup ).PrimaryGroup
Get-ADPrincipalGroupMembership -Identity $user.UserName | Where-Object {
$_ -ne $primaryGroup
} | ForEach-Object {
Remove-ADGroupMember $_ -Members $user.username -Confirm:$False -WhatIf
}
}
Since this has the potential to be a very destructive command, I have included a safeguard in the example above. Remove the -WhatIf parameter from Remove-ADGroupMember to actually perform the removal.
I'd propose a slightly different approach - just drop Get-ADPrincipalGroupMembership altogether. For example:
$users = Import-Csv -Path c:\temp\leavers.csv
foreach ($user in $users) {
# Assuming DN is not in the csv...
$distinguishedName = (Get-ADUser -Identity $user.UserName).DistinguishedName
Get-ADGroup -LdapFilter "(member=$distinguishedName)"
# Alternatively, just pipe memberOf property to Get-ADGroup...
(Get-ADUser -Identity $user.UserName -Property MemberOf).MemberOf |
Get-ADGroup
}
That way you don't have to filter out something you insisted on getting (by using above mentioned cmdlet).

Remove all groups withen a specific OU

I'm attempting to make an AD cleanup script that will go through a terminated OU and verify all users are removed from specific OU's. currently if I run it it will remove all users in the terminated OU from all OU's. I might just be blind but is there an easy way to have it only remove groups from selected OU's?
$OUs = "OU=Terminated,OU=####,OU=####,DC=####,DC=####"
$results = foreach ($OU in $OUs) {
get-aduser -SearchBase $OU -filter * -properties MemberOf | foreach-object {
? $_.MemberOf -like "*OU I want removed*" | Remove-ADGroupMember -Members $_.DistinguishedName -Confirm:$false -whatif
}
}
$results | Export-Csv '.\Users groups have been remoed from.csv' -NoTypeInformation
I thought it would work, however all it gives me is:
Where-Object : A positional parameter cannot be found that accepts argument 'Microsoft.ActiveDirectory.Management.ADPropertyValueCollection'.
At C:\###\###\###\accounts script.ps1:8 char:13
+ ? $_.MemberOf -like "*Distrobution Lists*" | <#%{$keep -n ...
Given that you have a separate OU for groups, you can iterate over the groups that a terminated user has and see if any of the groups belong to that specific OU. If thats the case, then remove all those groups.
$results = ""
foreach ($ou in $OUs)
{
$users = Get-ADUser -SearchBase $ou -Filter *
foreach ($user in $users)
{
$groups = Get-ADPrincipalGroupMembership -Identity $User | ? {$_.distinguishedName -like "*OU I WANT TO REMOVE FROM*" }
foreach($group in $groups)
{
Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $group -whatif
results += "$user removed from its Groups: $($groups | % { $_.name })\r\n"
}
}
}
results | Out-File -Append C:\temp\new.txt
$groups will have members in this format. You can use distinguishedName as a filter type and use something like "OU=Groups,DC=this,DC=com" instead of "OU=Groups" that might be considered broad.
distinguishedName : CN=GroupName,OU=****,DC=****,DC=****
GroupCategory : Security
GroupScope : Global
name : <Name Of The Group>
objectClass : group
objectGUID : <Object Guid>
SamAccountName : <Name Of The Group>
SID : <SID>
I like to keep the variables so i can use them to log what changes are being performed.
NOTE: I used -whatif to make sure it doesnt do what you intend to for testing reasons. Remove-ADPrincipalGroupMembership also updates user with one group.
Another Way to go about it
foreach ($ou in $OUs)
{
$users = Get-ADUser -SearchBase $ou -Filter *
$groups = Get-ADGroup -Filter * -SearchBase $DecomOUGROUP
foreach($group in $groups) {
Remove-ADGroupMember -Identity $group -Members $users -ErrorAction SilentlyContinue
}
}

Find security and distribution groups with owners whose account is disabled

I'm looking for some guidance on creating a powershell script that will check security and distribution groups from specific OU's and see if the owner is a user who's disabled.
We have lots of old groups in our AD created by ex employees that need to be cleaned up.
This is what i've started with.
$managedByGroups = get-adgroup -filter 'groupCategory -eq "Distribution"' -SearchBase "OU=SydExchangeGroups,OU=SydGroups,OU=Sydney,DC=my,DC=org,DC=biz" -Properties distinguishedname, managedby | select sAMAccountName, managedby
$disabledUsers = Get-ADUser -Filter {Enabled -eq $false} -SearchBase "OU=SydDisabledUsers,OU=SydMisc,OU=Sydney,DC=my,DC=org,DC=biz" | select distinguishedname
foreach ($group in $managedByGroups){
if($managedByGroups.managedby -eq $disabledUsers.distinguishedname)
{
write-output
}
}
Thanks
There are a number of issues with your if block:
you are looping through $managedByGroups, but you are never using that variable (it should be $group.managedby)
you are trying to compare 1 element with a list of elements, in this case consider using -in operator instead of -eq.
you should treat the case when there is no value for managedby attribute, in case you do not get the desired results.
An alternative to your code may is below.
I'm first getting the list of managedby users, then i'm looping though each entry, and if it is not null, we try to do a get-aduser filtering by enabled status and the distinguishedname.
$DisabledManagedBy variable will contains ADUser objects which are disabled.
$grp = get-adgroup -filter 'groupCategory -eq "Distribution"' -Properties ManagedBy,DistinguishedName
$DisabledManagedBy = foreach ($item in $grp.ManagedBy) {
if ($item) {
Get-ADUser -Filter {Enabled -eq $false -and DistinguishedName -like $item} -Properties DistinguishedName
}
}
I worked this out eventually by doing the following:
$myDisabledUsers = #()
$date = get-date -format dd-MM-yyyy
$managedSydGroups = Get-ADGroup -Filter * -Properties * -Searchbase "OU=SydExchangeGroups,OU=SydGroups,OU=Sydney,DC=my,DC=biz,DC=org" | where {$_.managedby -ne $null} | select name, managedby
$disabledSydUser = Get-ADUser -Filter * -SearchBase "OU=SydDisabledUsers,OU=SydMisc,OU=Sydney,DC=my,DC=biz,DC=org" | where {$_.enabled -eq $false} | select -ExpandProperty distinguishedname
$disabledOwners = foreach($group in $managedSydGroups)
{
$managedByString = [string]$group.managedby
if($disabledSydUser -contains $managedByString)
{$myDisabledUsers += $group}
}

Target all users in two OU's and remove Distribution Lists - Adding date criteria

#BenH and #TheMadTechnician were extremely helpful in assisting me with a script, to remove Distro Lists (only) from users in specific AD OU's. I forgot to add a needed criteria, so decided to post this as a separate question (original thread here)
#BenH's approach was like this:
$OUs = 'OU=PendingDeletion,OU=Users,DC=Stuff,DC=Place,DC=net','OU=HoldForReview,OU=Users,DC=Stuff,DC=Place,DC=net'
$Users = ForEach ($OU in $OUs) {
Get-ADUser -Filter * -SearchBase $OU
}
ForEach ($User in $Users) {
Get-ADPrincipalGroupMembership -Identity $user |
Where-Object {$_.GroupCategory -eq 0} |
ForEach-Object {
Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $_
}
}
My question - can I force the script to only take action on accounts that have expired more than 30days ago, by adding a variable and "Where-Object" logic to the first loop like this?:
$OUs = 'OU=PendingDeletion,OU=Users,DC=Stuff,DC=Place,DC=net','OU=HoldForReview,OU=Users,DC=Stuff,DC=Place,DC=net'
$30DaysOld = (Get-Date).AddDays(-30)
$Users = ForEach ($OU in $OUs) {
Get-ADUser -Filter * -SearchBase $OU |
Where-Object {$_.AccountExpirationDate -gt $30DaysOld}}
ForEach ($User in $Users) {
Get-ADPrincipalGroupMembership -Identity $user |
Where-Object {$_.GroupCategory -eq 0} |
ForEach-Object {
Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $_
}
}
Possible? Or would I need to change the -gt to a -lt in order to get the correct date range?
Thanks for looking!
Making an answer as requested. The problem is with the Where statement:
Where-Object {$_.AccountExpirationDate -gt $30DaysOld}
While logically is sounds like it's right 'where the account expired more than 30 days ago' it actually comes out to 'where the Date that the account expired is greater than what the Date was 30 days ago'. When you consider that some systems measure dates as seconds passed since the Unix Epoch (Jan 1, 1970 at 12:00:00 AM UTC), and dates are converted to integers, and it makes more sense that the -gt operator selects whichever date happens later chronologically as more seconds have passed since the epoch, and the integer is a larger number.
If you change the -gt to -lt it accomplishes what you're looking for. Also, adding -and $_.AccountExpirationDate to it makes sure that the AccountExpirationDate is not null. So we end up with:
Where-Object {$_.AccountExpirationDate -lt $30DaysOld -and $_.AccountExpirationDate}
#TheMadTechnician nailed it:
Perhaps Where{$_.AccountExpirationDate -lt $30DaysOld -and $_.AccountExpirationDate} would work for you. That makes sure they're more than 30 days old, and makes sure that the value isn't $null. If you still feel that too many results are being generated, go and look at the dates. Are there actually any that expired less than 30 days ago, or have not yet expired?
I needed to change the nested "where" statement to:
Where-Object {$_.AccountExpirationDate -lt $30DaysOld -and $_.AccountExpirationDate}
So the functioning code is:
$30DaysOld = (Get-Date).AddDays(-30)
$OUs = 'OU=PendingDeletion,OU=Users,DC=Stuff,DC=Place,DC=net','OU=HoldForReview,OU=Users,DC=Stuff,DC=Place,DC=net'
$Users = ForEach ($OU in $OUs) {
Get-ADUser -Filter * -Properties AccountExpirationDate -SearchBase $OU |
Where-Object {$_.AccountExpirationDate -lt $30DaysOld -and $_.AccountExpirationDate}
}
ForEach ($User in $Users) {
Get-ADPrincipalGroupMembership -Identity $user |
Where-Object {$_.GroupCategory -eq 0} |
ForEach-Object {
Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $_ -Confirm:$false
}
}
OP here - I've been continuing to work on this (with assistance), and added some additional embellishments that I thought someone else might find useful, so I wanted to share it back.
TheMadTechnician and BenH deserve all the credit for breaking the back of this.
This will now write the names of distros removed to the AD account, use a semicolon separator (to cut/paste the names if you need to re-add the distros), and won't add clutter to the AD account if it's run against the account more than once.
# Variables
$30DaysOld = (Get-Date).AddDays(-30)
$OUs = (
'OU=PendingDeletion,OU=Users,DC=Stuff,DC=Place,DC=net',
'OU=HoldForReview,OU=Users,DC=Stuff,DC=Place,DC=net'
)
# Collect the needed users
$Users = ForEach ($OU in $OUs) {
Get-ADUser -Filter * -Properties AccountExpirationDate,info -SearchBase $OU |
Where-Object {$_.AccountExpirationDate -lt $30DaysOld -and $_.AccountExpirationDate}
}
# Collect each user's Distro Lists & REMOVE
ForEach ($User in $Users) {
$distrosremoved=#()
Get-ADPrincipalGroupMembership -Identity $user |
Where-Object {$_.GroupCategory -eq "distribution"} |
ForEach-Object {
$distrosremoved+=$_.name
Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $_ -Confirm:$false
}
# Collect info from the Telephone > Notes field, and ADD the list of Distros into the existing info
if($distrosremoved){
$distro_str="Removed Distro Lists: `r`n"+($distrosremoved -join "; ")
if ($user.info){
$newinfo=$user.info+"`r`n"+$distro_str
Set-ADUser $user -Replace #{info=$newinfo}
}else{
$newinfo=$distro_str
Set-ADUser $user -Add #{info=$distro_str}
}
}
}

Target all users in two OU's and remove Distribution Lists

hoping to get a little help here – I looked around the site but didn’t see anything quite like this (please direct me if there IS and I missed it).
I need to incorporate a new step in our user offboarding process, which would remove them from any AD Distribution Lists. I would like to set this up as a scheduled task to run once a night against two OU’s where the inactivated user accounts can be found.
I’d like to run this by pointing it at the USERS instead of the OU where the Distro Lists live, because I suspect that we’ll ultimately get the request to remove these users from OTHER types of group as well.
This snippet will remove AD Distro Lists from a single user, but leave all other types of AD groups alone:
# GroupCategory 0 = Distro List
# GroupCategory 1 = Security Group
# GroupScope 0 = DomainLocal
# GroupScope 1 = Global
# GroupScope 2 = Universal
$user = "userlogon"
Get-ADPrincipalGroupMembership -Identity $user|
Where {$_.GroupCategory -eq 0} |
ForEach {Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $_ -Confirm:$false}
THIS snippet will look at an OU and return some info (just my example for using a variable with -searchbase):
$OUs = 'OU=PendingDeletion,OU=Users,DC=Stuff,DC=Place,DC=net','OU=HoldForReview,OU=Users,DC=Stuff,DC=Place,DC=net'
$OU | ForEach {Get-ADGroup -Filter * -Properties ManagedBy -SearchBase $_ } |
Select Name, ManagedBy |
Sort -Property Name
Out-GridView
BUT – Does it hold together that in order to complete my objective, I would do something like this?! I'm a bit out of my depth here, any advice for a re-write is appreciated:
$OUs = 'OU=PendingDeletion,OU=Users,DC=Stuff,DC=Place,DC=net','OU=HoldForReview,OU=Users,DC=Stuff,DC=Place,DC=net'
$user = "*"
$OUs | ForEach {
Get-ADPrincipalGroupMembership -Identity $user|
Where {$_.GroupCategory -eq 0} |
ForEach {Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $_ -Confirm:$false}
}
There’s always a couple of ways to do stuff in PoSh, so I’m sure there’s a less-complicated way to do the same thing. If anyone has a different approach please feel free to suggest an alternative.
Thanks for taking a look!
So it sounds like you need three loops.
First, you will need to loop over the OU list to get the Users. We'll store the user objects in $Users
$OUs = 'OU=PendingDeletion,OU=Users,DC=Stuff,DC=Place,DC=net','OU=HoldForReview,OU=Users,DC=Stuff,DC=Place,DC=net'
$Users = ForEach ($OU in $OUs) {
Get-ADUser -Filter * -SearchBase $OU
}
Next loop over the users to get the groups that you want to remove. Then loop over the groups to remove each one.
ForEach ($User in $Users) {
Get-ADPrincipalGroupMembership -Identity $user |
Where-Object {$_.GroupCategory -eq 0} |
ForEach-Object {
Remove-ADPrincipalGroupMembership -Identity $user -MemberOf $_
}
}
I think I'd take this a little differently, by getting the group membership of all users, then grouping by AD group, and processing each group that way. Seems like it would be a lot fewer calls to AD. So I'd start out getting all of the users, just like BenH, except I would include their MemberOf property. Then I'd build a list of potential groups and filter down to just the Distribution Lists. I'd make a Hashtable of those as the keys, and make the value an array of each user that is in that group. Then loop through that removing the value of each from the associated key.
$OUs = 'OU=PendingDeletion,OU=Users,DC=Stuff,DC=Place,DC=net','OU=HoldForReview,OU=Users,DC=Stuff,DC=Place,DC=net'
$Users = ForEach ($OU in $OUs) {
Get-ADUser -Filter * -SearchBase $OU -Properties MemberOf
}
$UsersByGroup = #{}
ForEach($Group in ($Users.MemberOf | Select -Unique | Get-ADGroup | Where{ $_.GroupCategory -eq 0 })) {
$UsersByGroup.Add($Group.DistinguishedName,($Users | Where{ $Group.DistinguishedName -in $_.MemberOf}))
}
$UsersByGroup.Keys | ForEach{
Remove-ADGroupMember -Identity $_ -Members $UsersByGroup[$_] -Confirm:$false
}