Leveraging Altair to Visualize BloodHound Results


Import Libraries

from py2neo import Graph
import plotly.graph_objects as go
import altair as alt
alt.renderers.enable('notebook')
RendererRegistry.enable('notebook')

Initialize BloodHound neo4j Database Connection

g = Graph("bolt://206.189.85.93:7687", auth=("neo4j", "BloodHound"))
g
<Graph database=<Database uri='bolt://206.189.85.93:7687' secure=False user_agent='py2neo/4.3.0 neobolt/1.7.15 Python/3.7.3-final-0 (linux)'> name='data'>

Retrieve Security Groups with Local Admin Rights over Computers

Now we’ll construct the Cypher query that tells us the name of each security group in Active Directory and the number of computers that group has local admin rights on.

sg_computers_df = g.run("""
MATCH (g:Group)
OPTIONAL MATCH (g)-[:AdminTo]->(c1:Computer)
OPTIONAL MATCH (g)-[:MemberOf*1..]->(:Group)-[:AdminTo]->(c2:Computer)
WITH g, COLLECT(c1) + COLLECT(c2) AS tempVar
UNWIND tempVar AS computers
RETURN g.name AS GroupName,COUNT(DISTINCT(computers)) AS AdminRightCount
ORDER BY AdminRightCount DESC
""").to_data_frame()
sg_computers_df
GroupName AdminRightCount
0 DOMAIN ADMINS@TOKYO.JAPAN.LOCAL 6
1 DESKTOPADMINS@TOKYO.JAPAN.LOCAL 4
2 DOMAIN ADMINS@JAPAN.LOCAL 4
3 MACHINEADMINS@JAPAN.LOCAL 3
4 ENTERPRISE ADMINS@JAPAN.LOCAL 2
5 DOMAIN ADMINS@SINGAPORE.LOCAL 2
6 ENTERPRISE ADMINS@SINGAPORE.LOCAL 1
7 WORKSTATIONADMINS@SINGAPORE.LOCAL 1

Create Horizontal Bar Chart: Most Privileged Active Directory Security Groups

bars = alt.Chart(sg_computers_df, title="Most Privileged Active Directory Security Groups").mark_bar().encode(
    x='AdminRightCount:Q',
    y=alt.Y(
        "GroupName:N",
        sort=alt.EncodingSortField(
            field="AdminRightCount",
            order="descending"
        )
    )
)

text = bars.mark_text(
    align='left',
    baseline='middle',
    dx=3
).encode(
    text='AdminRightCount:Q'
)

(bars + text).properties(height=300)
../../_images/03_altair_bloodhound_results_10_0.png

Create Horizontal Bar Chart: Most Privileged Active Directory Security Groups

users_to_da = g.run("""
MATCH (totalUsers:User {domain:'TOKYO.JAPAN.LOCAL'})
MATCH p=shortestPath((UsersWithPath:User {domain:'TOKYO.JAPAN.LOCAL'})-[r*1..]->(g:Group {name:'DOMAIN ADMINS@TOKYO.JAPAN.LOCAL'}))
WITH COUNT(DISTINCT(totalUsers)) as totalUsers, COUNT(DISTINCT(UsersWithPath)) as UsersWithPath
RETURN 100.0 * UsersWithPath / totalUsers AS percentUsersToDA
""").to_data_frame()
users_to_da
percentUsersToDA
0 13.580247
value_df = users_to_da['percentUsersToDA'].values[0]
value_df
13.580246913580247
fig = go.Figure(go.Indicator(
    domain = {'x': [0, 1], 'y': [0, 1]},
    value = (value_df),
    mode = "gauge+number",
    title = {'text': "Percentage of Users with a Path to Domain Admin"},
    gauge = {'axis': {'range': [None, 100]},
             'steps' : [{'range': [0, 250], 'color': "lightgray"}],
             'threshold' : {'line': {'color': "red", 'width': 4}, 'thickness': 0.75, 'value': 490}}))

fig.show()