Find AD Security Groups With Cross-Domain Members for Cleanup
In a multi-domain forest, group membership sprawls across domain boundaries over the years: a universal or domain-local group in corp ends up holding users from emea, or Foreign Security Principals (FSPs) from a trusted external forest that nobody remembers adding. Before you clean any of that up, you need an accurate inventory. This guide gives you a read-only report of every group in one domain that contains members from another domain, plus a carefully gated way to remove the ones you've decided to drop.
Before you run this
What it does: The first script enumerates the groups in a single domain, reads each group's raw member attribute, and flags any member whose distinguished name belongs to a different domain partition, or any member that is a Foreign Security Principal (a principal from another forest or external trust). It writes the findings to a CSV. It changes nothing.
Privileges: The report is read-only and runs fine as an ordinary authenticated domain user — any account that can read group membership. You do not need Domain Admin to generate it. The optional removal step at the end changes directory data, so that part needs rights to modify the target groups (Domain Admin or delegated write permission on those groups). Run it from a machine with the RSAT ActiveDirectory PowerShell module installed. I'm assuming PowerShell 5.1 on a domain-joined Windows Server 2019/2022 or a Windows 10/11 admin workstation with RSAT.
Test first: Read the script before you run it. Run the report against your real domain freely — it's harmless — but run the removal step against a single test group first (make one, add a cross-domain member, remove it, confirm) before you touch anything production.
This is a change that matters: Removing a member from a group is reversible — you re-add the same member DN — but the moment it's removed, that principal loses whatever access the group grants. If the group gates file shares, VPN, or logon rights, people get locked out of resources until you re-add them. Treat the removal step with that in mind, do it in a maintenance window, and keep the CSV so you know exactly what to put back.
Step 1 — Generate the report
Save this as Find-CrossDomainGroupMembers.ps1. Replace corp.example.com with the FQDN of the domain whose groups you want to audit, and adjust the output path.
#Requires -Modules ActiveDirectory
# --- Edit these two lines ---
$TargetDomain = 'corp.example.com' # domain whose groups you audit
$OutFile = 'C:\path\to\CrossDomainMembers.csv'
# ----------------------------
$domain = Get-ADDomain -Server $TargetDomain
$domainDN = $domain.DistinguishedName # e.g. DC=corp,DC=example,DC=com
# Pull every group with its raw member list. This reads DNs only, so it does
# NOT need to contact the other domains to resolve the members.
$groups = Get-ADGroup -Server $TargetDomain -Filter * -Properties member
$results = foreach ($g in $groups) {
foreach ($memberDN in $g.member) {
# The DC=... portion of a DN tells you which domain partition it lives in.
$memberDomainDN = ($memberDN -split ',' |
Where-Object { $_ -like 'DC=*' }) -join ','
# FSPs live in the local domain's ForeignSecurityPrincipals container
# and represent principals from another forest / external trust.
$isForeign = $memberDN -like '*CN=ForeignSecurityPrincipals,*'
# A member whose DN ends in a different domain DN is an intra-forest
# cross-domain member.
$isCrossDomain = -not $isForeign -and ($memberDomainDN -ne $domainDN)
if ($isForeign -or $isCrossDomain) {
[pscustomobject]@{
Group = $g.Name
GroupScope = $g.GroupScope
GroupDN = $g.DistinguishedName
MemberDN = $memberDN
MemberType = if ($isForeign) {
'ForeignSecurityPrincipal (external/other forest)'
} else {
'Intra-forest other domain'
}
}
}
}
}
$results | Sort-Object Group |
Export-Csv -Path $OutFile -NoTypeInformation -Encoding UTF8
Write-Host "Found $($results.Count) cross-domain memberships. Written to $OutFile"
Why read the member attribute directly instead of Get-ADGroupMember? Because Get-ADGroupMember tries to resolve every member, and it is known to error out or return incomplete results when members live in other domains or are FSPs that it can't translate. Reading the stored DNs avoids that entirely — you get the raw truth of what's in the group.
Reading the CSV
Two things worth understanding in the output:
Intra-forest other domain— a real user/group/computer object that lives in another domain of this forest. TheMemberDNis its actual DN, fully readable.ForeignSecurityPrincipal— theMemberDNCN is a SID string (e.g.CN=S-1-5-21-…). That's the raw stored form. To find out who that SID is, translate it. From a machine with the right trust path:
# Resolve one FSP SID to a name (the SID is the CN of the FSP's DN)
$sid = 'S-1-5-21-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-yyyy' # from MemberDN
(New-Object System.Security.Principal.SecurityIdentifier($sid)).Translate([System.Security.Principal.NTAccount])
If the translate call throws, the SID is likely orphaned (a deleted account from the other side of a trust) — those are usually the strongest cleanup candidates, but confirm with the resource owner first.
A couple of honest limitations: this reports direct members only, which is what you want for cleanup. It does not expand nested groups, and it ignores primary-group membership (primaryGroupID), which is same-domain by nature. On a very large domain, Get-ADGroup -Filter * is a heavy query — scope it with -SearchBase to an OU if you want to chunk the work.
Step 2 — Review, then remove (gated)
Do not script a mass removal from the CSV blindly. Decide, per line, what should actually go. Once you've trimmed the CSV down to only the rows you intend to remove, do a dry run first:
$toRemove = Import-Csv 'C:\path\to\CrossDomainMembers.csv'
foreach ($row in $toRemove) {
# -WhatIf shows exactly what WOULD happen and changes nothing.
Remove-ADGroupMember -Server $TargetDomain `
-Identity $row.GroupDN `
-Members $row.MemberDN `
-WhatIf
}
Read every What if: line. When you're satisfied, remove -WhatIf. Remove-ADGroupMember prompts for confirmation by default; keep that prompt (don't add -Confirm:$false) so you approve each change deliberately.
Remove-ADGroupMember accepts the member's DN directly, which is exactly what the CSV holds — including FSP DNs — so no extra translation is needed to remove them.
Verify it worked
After removal, confirm from both the group's side and by re-running the report:
# The member should no longer appear in the group's member list
(Get-ADGroup -Server $TargetDomain -Identity '<GroupDN from CSV>' -Properties member).member
# Re-run Step 1 — the removed rows should be gone from the new CSV
Undo
Removal is reversible as long as you kept the CSV. Re-add the exact member DN to the exact group:
Add-ADGroupMember -Server $TargetDomain `
-Identity '<GroupDN>' `
-Members '<MemberDN>'
Re-adding restores the membership and the access it granted. This is why I keep the original CSV until well after the maintenance window closes and nobody has reported a lost resource.
For the exact parameters and behaviour of these cmdlets, see Microsoft Learn for Get-ADGroup, Remove-ADGroupMember, and Add-ADGroupMember, and the documentation on Foreign Security Principals under Active Directory Domain Services.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Find Disabled AD Users That Still Hold Group Membership
A disabled account is not a harmless account. If it still holds membership in security groups, it still carries the access rights those groups grant — file shares, application roles, delegated permissions. If that account is ever re-enabled (by mistake, or by an attacker who compromised a helpdesk process), it lights up with all its old access instantly. Periodically finding disabled users that still sit in security groups is a basic hygiene task, and it is easy to script.
Clean Up Stale Computer Accounts in Active Directory with PowerShell
This guide finds Active Directory computer accounts that haven't logged in for a long time and retires them in three deliberate stages: report , disable and move to a holding OU , then delete . The purpose is to keep AD tidy and reduce the attack surface of forgotten machine accounts without accidentally killing a computer that's simply been powered off for a while.
Bulk-Disable and Archive Inactive AD Accounts with PowerShell
This procedure finds enabled user accounts that have not logged on for a set number of days , disables them, stamps a note in the account's description, and moves them into a dedicated "archive" OU so they are out of your working OUs but not deleted. Its purpose is routine account hygiene: a disabled, quarantined account can't be used for a logon, which shrinks your attack surface, but nothing is destroyed and everything is reversible.
Find and Report AD Accounts That Have Never Logged In
This guide gives you a PowerShell script that reads Active Directory and produces a report (on screen and as a CSV) of user accounts that have never authenticated against any domain controller. It changes nothing — it does not disable, delete, or edit a single account. Its purpose is to hand you a clean list of candidates for review before you decide what to do with them.




