Skip to content

Cypher Queries

All queries below run in the BloodHound CE Raw Query panel (or directly in the Neo4j browser if you are querying Neo4j directly).


User hunting

All Salesforce users

MATCH (m:SFUser) RETURN m

All relationships for a specific user (up to 6 hops)

MATCH (u:SFUser)
WHERE u.name = "PETER WIENER"
MATCH p = (u)-[*1..6]->(n)
RETURN DISTINCT p

All system permissions held by a specific user

MATCH (u:SFUser {Username: "username@example.com"})-[:AssignedProfile|AssignedPermissionSet]->(ps)-[perm]->(org:SFOrganization)
WHERE ps:SFProfile OR ps:SFPermissionSet
RETURN u.name AS UserName,
ps.name AS PermissionSetOrProfile,
type(perm) AS SystemPermission

System permission queries

Shortest path to org compromise (Tier 0 permissions)

MATCH p=(u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps)-[r:ModifyAllData|ManageSharing|ManageProfilesPermissionsets|CustomizeApplication|AuthorApex|ManageUsers|ManageRoles]->(org:SFOrganization)
WHERE (ps:SFProfile OR ps:SFPermissionSet)
AND u <> org
RETURN p
LIMIT 1000

All users with a specific high-risk permission (swap ModifyAllData as needed)

MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet]->(ps)-[:ModifyAllData]->(org:SFOrganization)
WHERE ps:SFProfile OR ps:SFPermissionSet
RETURN path

Count how many users have each system permission (useful for risk prioritisation)

MATCH (u:SFUser)-[:AssignedProfile|AssignedPermissionSet]->(ps)-[perm]->(org:SFOrganization)
WHERE ps:SFProfile OR ps:SFPermissionSet
RETURN type(perm) AS Permission, COUNT(DISTINCT u) AS UserCount
ORDER BY UserCount DESC

Available system permission edge types:

ModifyAllData · ViewAllData · ViewSetup · ManageUsers · ManageRoles · ManageSharing · ManageProfilesPermissionsets · ManageTranslation · CustomizeApplication · AuthorApex · ApiEnabled · EditTask · EditEvent


Object permission queries

All custom objects (potential sensitive data)

MATCH (obj:SFSObject)
WHERE obj.IsCustom = True
RETURN obj.name, obj.Label, obj.InternalSharingModel

Users with access to crown jewel objects

MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(ps)-[r:CanCreate|CanRead|CanEdit|CanDelete|CanViewAll|CanModifyAll]->(obj:SFSObject)
WHERE u.name = "PETER WIENER" AND obj.name = "SECRETDATA__C"
AND (ps:SFPermissionSet OR ps:SFProfile)
RETURN path

Shortest path from any user to custom SObjects

MATCH p=(u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet|CanCreate|CanRead|CanEdit|CanDelete|CanViewAll|CanModifyAll*1..10]->(f:SFSObject)
WHERE f.name ENDS WITH '__C'
AND u <> f
RETURN p
LIMIT 1000

All shortest paths to custom objects

MATCH p=allShortestPaths((u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet|CanCreate|CanRead|CanEdit|CanDelete|CanViewAll|CanModifyAll*1..10]->(f:SFSObject))
WHERE f.name ENDS WITH '__C'
AND u <> f
RETURN p
LIMIT 1000

All users who can delete a specific object

MATCH (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(p)-[:CanDelete]->(obj:SFSObject {name: "SECRETDATA__C"})
WHERE (p:SFPermissionSet OR p:SFProfile)
RETURN DISTINCT u.name AS User, p.name AS GrantedBy, obj.Label AS Object

Users with ModifyAll on custom objects (bypass sharing)

MATCH (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(p)-[:CanModifyAll]->(obj:SFSObject)
WHERE obj.IsCustom = True
AND (p:SFPermissionSet OR p:SFProfile)
RETURN DISTINCT u.name AS User, obj.name AS CustomObject, obj.Label
ORDER BY u.name, obj.name

Users with suspicious permission combo: Create + Delete + ModifyAll on same object

MATCH (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(p1)-[:CanCreate]->(obj:SFSObject),
(u)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(p2)-[:CanDelete]->(obj),
(u)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..5]->(p3)-[:CanModifyAll]->(obj)
WHERE (p1:SFPermissionSet OR p1:SFProfile)
AND (p2:SFPermissionSet OR p2:SFProfile)
AND (p3:SFPermissionSet OR p3:SFProfile)
RETURN DISTINCT u.name, obj.name, obj.Label

Field permission (FLS) queries

Shortest path from a specific user to a specific field

MATCH p=shortestPath((u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet|CanCreate|CanRead|CanEdit|CanDelete|CanViewAll|CanModifyAll|IsVisible|ReadOnly|Contains*1..10]->(f:SFField))
WHERE u.name = "PETER WIENER"
AND f.name = "SECRETDATA__C.HIGHLYSENSITIVEFIELD__C"
AND u <> f
RETURN p
LIMIT 1000

Shortest path to crown jewel fields (from any user)

MATCH p=shortestPath((u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet|CanCreate|CanRead|CanEdit|CanDelete|CanViewAll|CanModifyAll|IsVisible|ReadOnly|Contains*1..10]->(f:SFField))
WHERE f.name = "SECRETDATA__C.HIGHLYSENSITIVEFIELD__C"
AND u <> f
RETURN p
LIMIT 1000

All users with access to any custom fields

MATCH p=(u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet|CanCreate|CanRead|CanEdit|CanDelete|CanViewAll|CanModifyAll|IsVisible|ReadOnly|Contains*1..10]->(f:SFField)
WHERE f.name ENDS WITH '__C'
AND u <> f
RETURN p
LIMIT 1000

Role hierarchy queries

All users and their role assignments

MATCH (u:SFUser)-[h:HasRole]->(r:SFRole)
RETURN u, h, r

Role hierarchy tree (up to 5 levels)

MATCH path = (child:SFRole)-[:InheritsRole*1..5]->(ancestor:SFRole)
WHERE NOT (ancestor)-[:InheritsRole]->()
RETURN path

Portal role users

MATCH (u:SFUser)-[:HasRole]->(r:SFRole)
WHERE r.IsPortalRole = True
RETURN u, r

Transitive object access via role hierarchy

These queries surface the scenario where a subordinate user holds an explicit object permission (e.g. CanRead on SECRETDATA__C), and a senior user above them in the role hierarchy therefore gains implicit visibility to records on that same object — without holding any explicit permission to that object themselves.

All transitive access paths — senior users, their role chain, subordinates, and the objects subordinates have permissions on

MATCH path = (senior:SFUser)-[:HasRole]->(seniorRole:SFRole)-[:InheritsRole*1..5]->(juniorRole:SFRole)<-[:HasRole]-(junior:SFUser)-[:AssignedProfile|AssignedPermissionSet]->(ps)-[:CanRead|CanCreate|CanEdit|CanDelete|CanViewAll|CanModifyAll]->(obj:SFSObject)
WHERE senior <> junior
AND (ps:SFProfile OR ps:SFPermissionSet)
AND NOT obj.InternalSharingModel IN ["Public Read/Write", "ReadWrite"]
RETURN path
LIMIT 200

Scope to a specific senior user — all subordinates and objects that senior can view via role hierarchy

MATCH path = (senior:SFUser)-[:HasRole]->(seniorRole:SFRole)-[:InheritsRole*1..5]->(juniorRole:SFRole)<-[:HasRole]-(junior:SFUser)-[:AssignedProfile|AssignedPermissionSet]->(ps)-[:CanRead|CanCreate|CanEdit|CanDelete|CanViewAll|CanModifyAll]->(obj:SFSObject)
WHERE senior.name = "TONY STARK"
AND (ps:SFProfile OR ps:SFPermissionSet)
RETURN path
LIMIT 200

Scope to a specific senior/junior pair — confirm one senior inherits access to a specific subordinate’s objects

MATCH path = (senior:SFUser)-[:HasRole]->(seniorRole:SFRole)-[:InheritsRole*1..5]->(juniorRole:SFRole)<-[:HasRole]-(junior:SFUser)-[:AssignedProfile|AssignedPermissionSet]->(ps)-[:CanRead|CanCreate|CanEdit|CanDelete|CanViewAll|CanModifyAll]->(obj:SFSObject)
WHERE senior.name = "SENIOR USER NAME"
AND junior.name = "JUNIOR USER NAME"
AND (ps:SFProfile OR ps:SFPermissionSet)
RETURN path
LIMIT 200

Effective access profile

The complete set of permissions a single user holds — combining all explicit grants (object CRUD, field-level security, system permissions, connected app authorization) reached through any assignment chain up to 4 degrees, plus implicit record visibility gained through the role hierarchy. Together these two queries constitute a full effective-access audit for a user.

System and app permissions only (high-signal, no object/field noise)

Use this for a clean view of what the user can do administratively. Excludes object CRUD and field-level edges, which can produce hundreds of nodes and obscure the signal in the BloodHound UI.

MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..4]->(ps)-[access:ModifyAllData|ViewAllData|ViewSetup|ManageUsers|ManageRoles|ManageSharing|ManageProfilesPermissionsets|AuthorApex|CustomizeApplication|ApiEnabled|EditTask|EditEvent|ManageTranslation|CanAuthorize]->(target)
WHERE u.name = "PETER WIENER"
AND (ps:SFProfile OR ps:SFPermissionSet OR ps:SFPermissionSetGroup)
RETURN path
LIMIT 500

Full-fidelity — system permissions plus object CRUD and field-level security

Adds object CRUD (CanCreate through CanModifyAll) and field visibility (IsVisible, ReadOnly) on top of the system permissions above. Expect a dense graph for users with broad profiles — reduce LIMIT or add an AND target:SFSObject / AND target:SFField filter if needed.

MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet|AssignedPermissionSetGroup|HasPermissionSet|IncludesPermissionSet*1..4]->(ps)-[access:CanCreate|CanRead|CanEdit|CanDelete|CanViewAll|CanModifyAll|IsVisible|ReadOnly|ModifyAllData|ViewAllData|ViewSetup|ManageUsers|ManageRoles|ManageSharing|ManageProfilesPermissionsets|AuthorApex|CustomizeApplication|ApiEnabled|EditTask|EditEvent|ManageTranslation|CanAuthorize]->(target)
WHERE u.name = "PETER WIENER"
AND (ps:SFProfile OR ps:SFPermissionSet OR ps:SFPermissionSetGroup)
RETURN path
LIMIT 500

Group & queue queries

All Public Groups

MATCH (g:SFGroup) RETURN g

Public Group membership (including nested groups)

MATCH (g:SFGroup)-[h:HasMember]->(m)
RETURN g, h, m

Users in a specific Public Group (direct and via nested groups, up to 3 hops)

MATCH path = (g:SFGroup {name: 'KaiberSecInternalUsers'})-[:HasMember*1..3]->(u:SFUser)
RETURN path

All Queues

MATCH (q:SFQueue) RETURN q

Queue members

MATCH (q:SFQueue)-[h:HasMember]->(m)
RETURN q, h, m

Connected App queries

All Connected Apps

MATCH (app:SFConnectedApp) RETURN app

Connected Apps and their creators

MATCH p = (app:SFConnectedApp)-[:CreatedBy]->(u:SFUser)
RETURN p

Which admin created the most apps?

MATCH (app:SFConnectedApp)-[:CreatedBy]->(u:SFUser)
RETURN u.name AS Admin, count(app) AS AppCount
ORDER BY AppCount DESC

Which Profiles/PermissionSets can authorise which Connected Apps?

MATCH p = (ps)-[:CanAuthorize]->(app:SFConnectedApp)
RETURN p

Complete attack path: user → Connected App authorisation

MATCH path = (u:SFUser)-[:AssignedProfile|AssignedPermissionSet*1..2]->(ps)-[:CanAuthorize]->(app:SFConnectedApp)
RETURN path

Connected Apps that allow self-authorisation (security risk)

MATCH (app:SFConnectedApp)
WHERE app.AdminApprovedUsersOnly = False
RETURN app.name, app.AdminApprovedUsersOnly, app.CreatedDate

Sharing model queries

Objects with most restrictive sharing (Private)

MATCH (obj:SFSObject)
WHERE obj.InternalSharingModel = "Private"
RETURN obj
ORDER BY obj.name
LIMIT 50

Custom objects with public access (potential data leak)

MATCH (obj:SFSObject)
WHERE obj.IsCustom = True
AND obj.InternalSharingModel IN ["Public Read/Write", "ReadWrite", "Public Read Only", "Read"]
RETURN obj

Sharing model mismatch (internal vs external)

MATCH (obj:SFSObject)
WHERE obj.InternalSharingModel <> obj.ExternalSharingModel
RETURN obj
LIMIT 50

Generic counts

MATCH (m:SFUser) RETURN count(m) AS Users
MATCH (m:SFPermissionSet) RETURN count(m) AS PermissionSets
MATCH (m:SFProfile) RETURN count(m) AS Profiles

All nodes and relationships (expensive — limit appropriately)

MATCH (n)
OPTIONAL MATCH (n)-[r]->(m)
RETURN n, r, m
LIMIT 500