> ## Documentation Index
> Fetch the complete documentation index at: https://specterops-bp-2731-onprem-architecture.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenGraph Graph Theory

> Attack Graph Model Design Requirements and Examples

<img noZoom src="https://mintcdn.com/specterops-bp-2731-onprem-architecture/cSg9r7u_qWc5sQQj/assets/enterprise-AND-community-edition-pill-tag.svg?fit=max&auto=format&n=cSg9r7u_qWc5sQQj&q=85&s=332ab7c1e25fb10fd9f92afff92c688b" alt="Applies to BloodHound Enterprise and CE" width="482" height="45" data-path="assets/enterprise-AND-community-edition-pill-tag.svg" />

# Introduction

For several years, one of the biggest pain-points with contributing to BloodHound has been in getting nodes and edges ingested and correctly displayed in the GUI. BloodHound OpenGraph changes that. Now it is easy for anyone to add nodes and edges into BloodHound through the easy-to-use `/file-upload/` endpoint.

However, while the process of adding nodes and edges to the product is greatly simplified, the product will not function as expected without a well-designed attack graph model. This document seeks to educate users on attack graph model design theory, best-practices, and requirements.

An attack graph is a tool - a powerful force multiplier when wielded correctly, a frustrating and confusing hazard when not. This document aims to equip you with the knowledge and skills necessary to effectively wield this tool.

# Basic Attack Graph Vocabulary and Design Theory

Graphs are [well-understood](https://en.wikipedia.org/wiki/Graph_%28discrete_mathematics%29), well-studied mathematical constructs. You can find thousands of guides, tools, and academic papers that make use of graphs. This document will not replace a proper education or time spent working with graphs. But in this section we will touch on the most fundamental aspects of a graph you must understand in order to effectively get BloodHound to work with your nodes and edges.

Every graph is constructed from two fundamental components: vertices (nodes) and edges (relationships):

<img noZoom src="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-1.png?fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=6bfd18dd7ec52917bae999d22ee997ce" alt="Node1 -- Edge1 --> Node2" data-og-width="1012" width="1012" data-og-height="508" height="508" data-path="assets/og-bp-1.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-1.png?w=280&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=e5f0875eb319fc203962ffde9d8da19e 280w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-1.png?w=560&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=c1a93dc9d800359ee4a0b7317fdf702f 560w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-1.png?w=840&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=f4c08aba7e6f2c14da3c0b09275da592 840w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-1.png?w=1100&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=b2daec92aa6b9e9900921e116fb268c3 1100w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-1.png?w=1650&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=831b3f045ec732a2db472b8be4b320ef 1650w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-1.png?w=2500&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=9168e2dbdd5194692bf173ad6bf23f69 2500w" />

The above graph has two nodes and one edge. The edge is **directed**. The source node of the edge is “Node 1”. The destination node of the edge is “Node 2”.

**Every** edge in a BloodHound attack graph is **directed**, and is **one-way**. There are no bi-directional (“two-way”) edges in a BloodHound graph.

In a BloodHound attack graph, the direction of the **edge** must match the direction of **access** or **attack**. Let's look at an example with Active Directory group memberships.

In the BloodHound attack graph, we model Active Directory security group memberships like this:

<img noZoom src="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-2.png?fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=790f7c47947f5ad3d714fa004dba288e" alt="User -- MemberOf --> Group" data-og-width="1024" width="1024" data-og-height="432" height="432" data-path="assets/og-bp-2.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-2.png?w=280&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=fea5d8e70bfe3547712e55afd0a6bd7d 280w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-2.png?w=560&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=14c977a4bb331f0eadcb4803264e903a 560w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-2.png?w=840&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=5d220418d4c4a8e95063e7f0a247e1a6 840w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-2.png?w=1100&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=2627b4af651848b54144b615915523f3 1100w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-2.png?w=1650&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=a7919b19b960f0166a586af1f100dfc2 1650w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-2.png?w=2500&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=6e7a1a8206dd48afc5e436d26656a8c1 2500w" />

Think about the direction of the edge. Now think for a moment and try to figure out why we don't model AD security group memberships like this instead:

<img noZoom src="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-3.png?fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=d18de35d951ce5c1b5f607455e54c00e" alt="Group -- HasMember --> User" data-og-width="1018" width="1018" data-og-height="424" height="424" data-path="assets/og-bp-3.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-3.png?w=280&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=0a3718ae24df81aea9feb1907700a13d 280w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-3.png?w=560&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=34c698b244759d84e745e33feeab96ac 560w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-3.png?w=840&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=27bbe58633b90400e880f74e9c7772f4 840w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-3.png?w=1100&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=1fea873767947470da689988fc09a9d0 1100w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-3.png?w=1650&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=707b7491ca998b063b9abddaae26ac2c 1650w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-3.png?w=2500&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=f3347ae3645e391284699929dff3aedb 2500w" />

This seems perfectly reasonable at first glance, does it not? But remember that we are constructing an **attack graph** in order to discover **attack paths**. Edge directionality must serve attack path discovery.

The direction of the edge going from the group to the user does not expose any attack path. Just because a user is a member of a group does not mean the group has any “control” of the user. But when the direction of the edge is from the user to the group, that DOES serve attack path discovery.

Why? Because in Windows and Active Directory, members of security groups gain the privileges held by those groups. Let's extend the model a bit to make this easier to see:

<img noZoom src="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-4.png?fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=64c91246af618f580e66d37ba045fc46" alt="User -- MemberOf -> Group -- GenericAll --> Domain" data-og-width="1582" width="1582" data-og-height="414" height="414" data-path="assets/og-bp-4.png" data-optimize="true" data-opv="3" srcset="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-4.png?w=280&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=03f179d02f0bb5b5f6b6c70de4916a96 280w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-4.png?w=560&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=59b99acc9684be1136e420a356a677cf 560w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-4.png?w=840&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=6d305919e54c938f701166d60dce4e4e 840w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-4.png?w=1100&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=57f8e04b12f8776e94669c3d617f62e4 1100w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-4.png?w=1650&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=dbbf27ddf9bd1e0719362f36bc40123e 1650w, https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-4.png?w=2500&fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=fa3cc13f4a6f59d6874d5be8d6a71346 2500w" />

The user is a member of a group, and the group has full control of the domain. When the user authenticates to Active Directory, their Kerberos ticket will include the SID of the group. When the user uses that ticket to perform some action against the domain object, the security reference monitor will inspect the ticket, see the group SID, and grant the user all the permissions against the domain that the group has.

**In reality the process is much more involved than this, but work with me here, people.**

The above diagram shows a **path** connecting two **non-adjacent** nodes. **Adjacent** nodes are those that are connected together by an edge. In the above diagram, the adjacent nodes are:

1. “User” and “Group” via the “MemberOf” edge

2. “Group” and “Domain” via the “GenericAll” edge

The “User” and “Domain” nodes are non-adjacent, yet there is a **path** connecting the “User” node to the “Domain” node.

When designing your attack graph model, you **must** be aware of the **patterns** that will emerge from your design. There are many examples out there of people who want to make a contribution to the BloodHound graph who do not seem to be aware of this. Instead of proposing nodes/edges that create multi-node patterns, they propose nodes/edges that result **only** in one-to-one patterns:

<img noZoom src="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-5.png?fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=d4b20f92c7019631878de2d8a066f47c" alt="Badly connected nodes" width="1012" height="772" data-path="assets/og-bp-5.png" />

In the above graph there are two patterns:

1. From the red (top left) to the pink (top right) node

2. From the blue (bottom left) to the green (bottom right) node

What's wrong with this design?

Think of the graph as a map of **one-way streets**. In the above graph we have two one-way streets. But this map kinda sucks, doesn't it? You can only start in two places and you can only go to two places. You can't go from the red (top left) node to the blue (bottom left) node because there is no **path** connecting those nodes.

This is a much better map:

<img noZoom src="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-6.png?fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=62216ebadb89ac10a36bf45cd5862224" alt="Well connected nodes" width="1002" height="770" data-path="assets/og-bp-6.png" />

Now is there a **path** from the red (top left) node to the blue (bottom left) node? Yes! It goes **through** the green (bottom right) node!

The difference in the two graphs is the level of **connectedness**, or how well-linked the nodes are to one another.

Let's belabor the point a little more to make it even more clear. The top model would be analogous to having a node represent both a **person** and the **address** where they live, with the edge representing the fact that they live at that address:

<img noZoom src="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-7.png?fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=007f377c6ad246248f7e7f6e1f3383c0" alt="Badly connected nodes" width="980" height="808" data-path="assets/og-bp-7.png" />

While the bottom graph would be analogous to having the nodes represent the **addresses** and the edges represent **streets**:

<img noZoom src="https://mintcdn.com/specterops-bp-2731-onprem-architecture/MtBGgI_6sTZPy-3L/assets/og-bp-8.png?fit=max&auto=format&n=MtBGgI_6sTZPy-3L&q=85&s=a82eb8512423d87df5bb37f55d6026ee" alt="Well connected nodes" width="1004" height="826" data-path="assets/og-bp-8.png" />

It should be obvious that for the sake of **pathfinding**, the **second** model is the **only** model that will work.

**This is actually how Google Maps works under the hood — it is a graph where locations are nodes and streets are edges.**

<Warning>
  If your graph model does not create paths connecting non-adjacent nodes, you should use a relational database instead. A graph database is the wrong tool for data that only produces one-to-one patterns.
</Warning>

<Note>
  This article is adapted from [Andy Robbins](https://www.linkedin.com/in/robbinsandy/)' blog post, “[Attack Graph Model Design Requirements and Examples](https://specterops.io/blog/2025/08/01/attack-graph-model-design-requirements-and-examples/),” which goes beyond what's described here.
</Note>
