ad-entity-walk · git:20260504.2a64374 · 2026-05-04 · sha256 7a275376c501778a

ad-entity-walk git:20260504.2a64374A

Immutable. This exact content is served forever at /api/v1/blob/7a275376c501778a.

---
name: ad-entity-walk
description: Investigate a single AD or Azure principal — who controls it, what it controls, where it has admin rights, and what's reachable via its credentials. Use when the caller asks "tell me about this user", "what does X have access to", "who can compromise Y", or when triaging a finding tied to a specific principal.
---

# AD entity walk

When a single principal is in the spotlight — a user from an attack-path finding, a service account that just landed in Tier Zero, a compromised computer from an incident — the question is always some variation of "what's its blast radius?". This skill pulls together the relationship info BHE has on that principal so the agent can answer.

## Preconditions

- `bhe-bootstrap` has run.
- A principal identifier — either an object_id (preferred) or a name to resolve.

## Workflow

### 1. Resolve to an object_id

If the caller supplied a name ("jdoe@example.com", "DC01.example.com"), call `search_graph(query=<name>, kind=<optional>)` to translate to an object_id. The first result is usually correct; if multiple results land, surface them and let the caller disambiguate before proceeding.

If the caller supplied an object_id, skip this step.

### 2. Pull basic info

Call `get_entity(object_id)`. The response carries:

- Kind (User, Computer, Group, Domain, AZUser, ...).
- Name and distinguished name.
- Domain.
- Counts of every relationship type (Sessions, AdminTo, MemberOf, ...).

Use the kind to choose the right downstream tools. `User` and `Computer` have rich per-kind endpoints (admin-rights, sessions, delegation); other kinds (Group, GPO, Domain) are mostly served by the generic `entity_controllers` / `entity_controllables` pair.

### 3. Walk inbound — who can compromise this?

Call `entity_controllers(object_id)`. The result is the set of principals with rights to take over this entity (Owner, GenericAll, GenericWrite, WriteOwner, ResetPassword, etc.).

For each controller:

- Note the kind. Group controllers are particularly important because their members all inherit the right.
- Spot Tier Zero entries. If a Tier Zero member controls the principal, the principal is effectively Tier Zero too — flag that.
- Spot stale memberships. A controller named `*_LEGACY` or in an OU labelled `_disabled` is drift worth surfacing.

If the principal is a `User`, also call `user_admins(user_id)` for the focused list of who has admin *on the user account itself* (this overlaps with `entity_controllers` but the per-kind endpoint sometimes surfaces more detail).

### 4. Walk outbound — what does this principal control?

Call `entity_controllables(object_id)`. The result is the set of nodes this principal can take over.

For users + computers, also call the per-kind right walks:

- `user_admin_rights(user_id, kind="admin-rights")` — local admin on remote machines.
- `user_admin_rights(user_id, kind="rdp-rights")` — RDP targets.
- `user_admin_rights(user_id, kind="powershell-remote-rights")` — WinRM targets.
- `user_admin_rights(user_id, kind="dcom-rights")` — DCOM execution.
- `user_admin_rights(user_id, kind="sql-admin-rights")` — SQL admin.
- `user_admin_rights(user_id, kind="constrained-delegation-rights")` — delegation targets.

Replace `user_admin_rights` with `computer_admin_rights` when the principal is a Computer.

### 5. Walk active sessions

For users: `user_sessions(user_id)` shows current logon footprint. Sessions are a hot target — every machine the user is logged into is a credential-theft opportunity for an attacker who compromises that machine.

For computers: `computer_sessions(computer_id)` shows who's logged into the machine. Combined with the inbound controllers walk, this answers "if this machine is compromised, whose credentials are at risk?".

### 6. Walk membership

For users: `user_membership(user_id)` shows direct + transitive group membership. Useful for spotting unexpected admin-group inclusions.

### 7. Tier and certification context

Call `search_asset_group_tags(query=<object_id>)` to see whether the principal is in any tag (Tier Zero, Crown Jewels, Owned, ...). If it's in Tier Zero, the blast radius interpretation flips — the principal's access becomes *the* attack target rather than a means of attack.

### 8. Output

```
{
  "principal": {
    "object_id": "...",
    "kind": "User",
    "name": "...",
    "domain": "...",
    "tags": ["Tier Zero", "Owned"],
    "is_certified": true
  },
  "inbound": {
    "controllers": [...],
    "admins": [...],
    "tier_zero_controllers": [...]
  },
  "outbound": {
    "admin_targets": N,
    "rdp_targets": N,
    "ps_remote_targets": N,
    "dcom_targets": N,
    "sql_admin_targets": N,
    "delegation_targets": N,
    "groups": [...]
  },
  "sessions": [...],
  "blast_radius_summary": "..."
}
```

The `blast_radius_summary` is the agent's contribution — one short paragraph synthesising the structured data into "if this account is compromised, the attacker reaches X, Y, and via Z gets to Tier Zero through finding W".

## Cost budget

- 5–8 tool calls per principal in the typical case.
- One `search_graph` for name resolution.
- Skip per-kind walks for kinds that don't have them (the per-kind endpoints 404 on Groups / OUs / Domains).

## What NOT to do

- Don't recurse into every controller. The walk fans out exponentially; stick to the immediate inbound / outbound layer for one principal. Use Cypher's `shortestPath` if you need transitive reachability.
- Don't propose remediation as part of the walk. The walk is descriptive; remediation is a separate operation. Hand off to `attack-path-triage` for that.
- Don't rely on `enabled = false` to dismiss a principal. Disabled accounts can still be re-enabled and used; their inclusion in the graph is still informative.