Define the target
Agree what healthy privilege, ownership and access should look like across AD and Entra ID.
With twenty years of inherited permissions, nested groups and forgotten service accounts, attackers abuse it every day.
GrexID shows you what you actually have, what matters first, and which permission you can remove today without breaking anything.
Other tools hand you hundreds of findings and leave the prioritizing to you. GrexID keeps only the actions that close real paths to Domain Admin, ranks them by security gain against operational cost, and shows what happens before you touch production.
Not hundreds of findings to triage. GrexID keeps only the actions that close real paths to Domain Admin, ranks them by security gain against operational cost, and says who loses access with each one. The first fix alone takes 22% of the risk off the table.

Each solution is a single concrete edge: who holds it, what it reaches and why it matters. The graph draws the route to Domain Admin, and the numbers say what removing it closes: nineteen objects lose their only escalation path, fifteen of them Tier Zero.

Before a permission goes, GrexID previews which attack paths disappear and whether legitimate access keeps working. Zero broken dependencies means the change can go out today.
Want to see this on your own directory?
Start cleaning up, safelyA continuous workflow for defining the desired state, understanding exposure, improving safely and watching for privilege abuse.
Agree what healthy privilege, ownership and access should look like across AD and Entra ID.
Collect identity, relationship and permission data without changing production.
Map risky permissions, attack paths and escalation routes to the assets that matter.
Propose improvements with security impact and operational impact shown before teams act.
Monitor change continuously and detect privilege abuse before it becomes a new path.
The core product surface: quantify identity risk, understand the path, simulate safe fixes and monitor privilege abuse after the change.
Heuristics find what's dangerous. The AI layer reasons about your roles and ownership model to find what's unjustified — the access nobody can explain, the rights that drifted, the privilege no role should hold.
Score users, groups and permissions by exposure, scope and proximity to Domain Admin so teams know what matters first.
Map how privilege escalates across AD and Entra ID, then rank the routes that create real identity risk.
Preview the security gain and operational blast radius before a permission is removed from production.
Watch permission, group and descriptor changes in near real time and catch escalation from its first hop.
Inspect dangerous rights such as GenericAll, WriteDACL and DCSync with risk classification and remediation guidance.
Push scored risk and change events into SIEM, ticketing or automation when another system needs to act.
Want to see which attack path GrexID finds first in your directory?
Start cleaning up, safelyMove through the product the way teams use it: prioritize identity risk, inspect the path, prove controls and show progress.
Walk the graph from any low-privileged account to Domain Admin, hop by hop.

GrexID runs where your security and compliance model needs it: on-premise, SaaS, or an agentless first assessment that scales into continuous monitoring.
GrexID is a European company, built and supported by a team inside the EU and running end to end on European infrastructure — hosting, email, analytics and backups included. Risk scoring and semantic analysis execute within your perimeter, so no directory data and no prompts ever reach a third-party AI API. Data sovereignty here is architecture, not a hosting region you pick at checkout.
Run them against your domain and read the names. If there is someone on a list you think shouldn't be there, that is where the cleanup starts. Nothing here changes production, and nothing needs installing.
Domain Admins, Enterprise Admins, Schema Admins, Administrators and the Operators groups, resolved by well-known SID so it works whatever language your domain speaks. The built-in Administrator and the groups nested in each other are filtered out. Everyone left is worth a look.
# Built-in Tier Zero groups resolved by well-known SID. Plain .NET DirectoryServices: no RSAT, no modules.
$domainDN = [string]([ADSI]"LDAP://RootDSE").defaultNamingContext
$domain = [ADSI]"LDAP://$domainDN"
$domainSid = (New-Object System.Security.Principal.SecurityIdentifier($domain.Properties['objectsid'][0], 0)).Value
function ConvertTo-SidString($bytes) { (New-Object System.Security.Principal.SecurityIdentifier($bytes, 0)).Value }
function ConvertTo-LdapLiteral($s) { $s -replace '\\', '\5c' -replace '\(', '\28' -replace '\)', '\29' -replace '\*', '\2a' }
$domainRids = 512, 519, 518, 520, 526, 527 # Domain/Enterprise/Schema Admins, GPO Creator Owners, Key Admins
$builtinRids = 544, 548, 549, 550, 551 # Administrators, Account/Server/Print/Backup Operators
$groupSids = @($domainRids | ForEach-Object { "$domainSid-$_" }) + @($builtinRids | ForEach-Object { "S-1-5-32-$_" })
$groups = foreach ($sid in $groupSids) {
try {
$g = [ADSI]"LDAP://<SID=$sid>"
[pscustomobject]@{ Sid = $sid; Name = $g.Properties['name'][0]; DN = $g.Properties['distinguishedname'][0] }
} catch { } # Enterprise and Schema Admins only exist in the forest root domain
}
# Expected by default: the built-in Administrator and the Tier Zero groups nested in each other.
$expected = @("$domainSid-500") + @($groups | ForEach-Object { $_.Sid })
foreach ($g in $groups) {
$filter = "(memberOf:1.2.840.113556.1.4.1941:=$(ConvertTo-LdapLiteral $g.DN))"
$search = New-Object System.DirectoryServices.DirectorySearcher($domain, $filter)
$search.PageSize = 1000
foreach ($m in $search.FindAll()) {
if ($expected -notcontains (ConvertTo-SidString $m.Properties['objectsid'][0])) {
[pscustomobject]@{ Group = $g.Name; Member = $m.Properties['name'][0]; Class = @($m.Properties['objectclass'])[-1] }
}
}
}Anyone with GenericAll, WriteDacl, WriteOwner or replication rights on the domain object can become Domain Admin without ever joining the group. The principals that hold those rights by default are excluded. Every other name needs an explanation.
# Control rights on the domain object itself. Plain .NET DirectoryServices: no RSAT, no modules.
$domainDN = [string]([ADSI]"LDAP://RootDSE").defaultNamingContext
$domain = [ADSI]"LDAP://$domainDN"
$domainSid = (New-Object System.Security.Principal.SecurityIdentifier($domain.Properties['objectsid'][0], 0)).Value
# Default holders of rights on the domain root; anything else deserves a look.
$expected = @(
"$domainSid-512", "$domainSid-519", "$domainSid-516", "$domainSid-498", "$domainSid-522", "$domainSid-526", "$domainSid-527",
# Domain Admins, Enterprise Admins, Domain Controllers, Enterprise RODCs, Cloneable DCs, Key Admins, Enterprise Key Admins
"S-1-5-32-544", "S-1-5-18", "S-1-5-9", "S-1-3-0", "S-1-5-10"
# Administrators, SYSTEM, Enterprise Domain Controllers, Creator Owner, Self
)
$replication = @(
'1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', # DS-Replication-Get-Changes
'1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', # DS-Replication-Get-Changes-All
'00000000-0000-0000-0000-000000000000' # all extended rights
)
$domain.ObjectSecurity.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]) |
Where-Object {
$_.AccessControlType -eq 'Allow' -and $expected -notcontains $_.IdentityReference.Value -and (
$_.ActiveDirectoryRights -match 'GenericAll|WriteDacl|WriteOwner' -or
($_.ActiveDirectoryRights -match 'ExtendedRight' -and $replication -contains $_.ObjectType.ToString())
)
} |
ForEach-Object {
try { $who = $_.IdentityReference.Translate([System.Security.Principal.NTAccount]).Value } catch { $who = $_.IdentityReference.Value }
[pscustomobject]@{ Principal = $who; Rights = $_.ActiveDirectoryRights; Inherited = $_.IsInherited }
} |
Sort-Object Principal -UniqueThe recursive membership of Domain Admins, plus every principal that can add members to the group, reset a member's password or otherwise take control of a member. Expected principals are excluded. The second list is usually the surprising one.
# Recursive members of Domain Admins, plus whoever can take control of the group or one of its members. No modules.
$domainDN = [string]([ADSI]"LDAP://RootDSE").defaultNamingContext
$domain = [ADSI]"LDAP://$domainDN"
$domainSid = (New-Object System.Security.Principal.SecurityIdentifier($domain.Properties['objectsid'][0], 0)).Value
function ConvertTo-SidString($bytes) { (New-Object System.Security.Principal.SecurityIdentifier($bytes, 0)).Value }
function ConvertTo-LdapLiteral($s) { $s -replace '\\', '\5c' -replace '\(', '\28' -replace '\)', '\29' -replace '\*', '\2a' }
$expected = @(
"$domainSid-500", "$domainSid-512", "$domainSid-519", "$domainSid-516", "$domainSid-526", "$domainSid-527",
# Administrator, Domain Admins, Enterprise Admins, Domain Controllers, Key Admins, Enterprise Key Admins
"S-1-5-32-544", "S-1-5-18", "S-1-5-9", "S-1-3-0", "S-1-5-10"
# Administrators, SYSTEM, Enterprise Domain Controllers, Creator Owner, Self
)
$da = [ADSI]"LDAP://<SID=$domainSid-512>"
$filter = "(memberOf:1.2.840.113556.1.4.1941:=$(ConvertTo-LdapLiteral $da.Properties['distinguishedname'][0]))"
$search = New-Object System.DirectoryServices.DirectorySearcher($domain, $filter)
$search.PageSize = 1000
$members = @($search.FindAll())
"== Recursive members of Domain Admins =="
foreach ($m in $members) {
if ($expected -notcontains (ConvertTo-SidString $m.Properties['objectsid'][0])) {
[pscustomobject]@{ Name = $m.Properties['name'][0]; Class = @($m.Properties['objectclass'])[-1] }
}
}
"== Who can take control of Domain Admins or one of its members =="
$memberAttr = 'bf9679c0-0de6-11d0-a285-00aa003049e2' # member
$forcePwd = '00299570-246d-11d0-a768-00aa006e0529' # User-Force-Change-Password
$anyObject = '00000000-0000-0000-0000-000000000000'
$targets = @($da) + @($members | ForEach-Object { $_.GetDirectoryEntry() })
$findings = foreach ($t in $targets) {
$t.ObjectSecurity.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]) |
Where-Object {
$_.AccessControlType -eq 'Allow' -and $expected -notcontains $_.IdentityReference.Value -and (
$_.ActiveDirectoryRights -match 'GenericAll|GenericWrite|WriteDacl|WriteOwner' -or
($_.ActiveDirectoryRights -match 'WriteProperty' -and $_.ObjectType.ToString() -in @($anyObject, $memberAttr)) -or
($_.ActiveDirectoryRights -match 'ExtendedRight' -and $_.ObjectType.ToString() -in @($anyObject, $forcePwd))
)
} |
ForEach-Object {
try { $who = $_.IdentityReference.Translate([System.Security.Principal.NTAccount]).Value } catch { $who = $_.IdentityReference.Value }
[pscustomobject]@{ Target = $t.Properties['name'][0]; Principal = $who; Rights = $_.ActiveDirectoryRights }
}
}
$findings | Sort-Object Target, Principal -UniqueThese three lists are the tip of the iceberg. GrexID walks the real paths, however many hops they take, and weighs the security gain of every fix against its operational impact.
Request a guided demo and we'll walk through your attack paths, the safe-remediation preview and the deployment model that fits your directory.