Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e1d3cdc
feat (bloodhound, reporting): implement a bloodhound client, globally…
zinic Jun 3, 2025
d388034
chore (bloodhound): clean up review nits
zinic Jun 5, 2025
b8aa3d8
chore (bloodhound): refactor location of get findings call for bloodh…
zinic Jun 5, 2025
148767e
Add project bloodhound config, update types
aparrill-specterops Jul 23, 2025
aa68de4
Merge branch 'master' into bloodhound-integration
aparrill-specterops Jul 29, 2025
8ca3d8b
Merge migrations, fix circular import
aparrill-specterops Jul 29, 2025
7a449b9
Merge branch 'master' into bloodhound-integration
aparrill-specterops Jul 31, 2025
5dc7a9a
Add migration merge
aparrill-specterops Aug 1, 2025
e172b5f
Merge branch 'master' into bloodhound-integration
chrismaddalena Aug 1, 2025
909fc57
Updated BloodHound text
chrismaddalena Aug 4, 2025
9651dd2
Added Ghostwriter version to User-Agent
chrismaddalena Aug 4, 2025
a9640e9
Initial documentation for the feature
chrismaddalena Aug 4, 2025
0d5d5da
Check for errors fetching from BloodHound
aparrill-specterops Aug 5, 2025
b0ae702
Add option to generate a report without BH info
aparrill-specterops Aug 5, 2025
8c297d6
Also check for JSON errors in bloodhound integration
aparrill-specterops Aug 6, 2025
ab03fe0
Add BloodHound config to Review Configuration page
aparrill-specterops Aug 6, 2025
06ff57b
Add BloodHound connectivity check
aparrill-specterops Aug 6, 2025
df9362b
Fix XLSX export
aparrill-specterops Aug 7, 2025
54e50f1
Fix tests
aparrill-specterops Aug 7, 2025
5e39b3e
Merge branch 'master' into bloodhound-integration
aparrill-specterops Aug 21, 2025
1a805d0
Unify bloodhound field and method names
aparrill-specterops Aug 21, 2025
c6634ac
Cache BloodHound findings in projects and BHConfig
aparrill-specterops Aug 22, 2025
c2f28b3
Merge branch 'master' into bloodhound-integration
chrismaddalena Sep 10, 2025
2de21fe
Removed unused imports
chrismaddalena Sep 11, 2025
ef8e130
Added a BloodHound paw icon
chrismaddalena Sep 11, 2025
f05b849
Fixed typo
chrismaddalena Sep 11, 2025
b20985c
Styled checkbox as a switch toggle
chrismaddalena Sep 11, 2025
6863230
Styled content to be consistent
chrismaddalena Sep 11, 2025
2586127
Merge branch 'master' into bloodhound-integration
chrismaddalena Sep 15, 2025
bcd1c7b
Fixed presentation of BH API URL
chrismaddalena Sep 18, 2025
6b84739
Show table of BloodHound findings
aparrill-specterops Aug 27, 2025
ec27083
Allow anyone to fetch global BloodHound findings
aparrill-specterops Sep 19, 2025
ebd019d
Added BloodHound logo icons
chrismaddalena Sep 19, 2025
11699f2
Added punctuation to messages for consistency
chrismaddalena Sep 19, 2025
a5cbf4a
Formatted buttons to be equal and in a group
chrismaddalena Sep 19, 2025
dc69e44
Log detected BloodHound edition
aparrill-specterops Sep 19, 2025
ab2a6ce
Add BloodHound domain info fetching
aparrill-specterops Sep 24, 2025
b348bd4
Add collapsible accordions to BloodHound data
aparrill-specterops Sep 25, 2025
44d255c
Styled content as an alert for consistency
chrismaddalena Sep 25, 2025
33faa0a
Fixed `accordion` class elements being hidden due to global CSS value
chrismaddalena Sep 25, 2025
b145d17
Fixed hidden attr on `accordion`
chrismaddalena Sep 25, 2025
f5ef18d
Styled accordions to match others for consistency
chrismaddalena Sep 25, 2025
98f8e1a
Fix BH OS count, add user count
aparrill-specterops Sep 26, 2025
6cde2eb
Added columns for OS and old passwords
chrismaddalena Sep 26, 2025
2a93c62
Rename "bloodhound_findings" to "bloodhound" in template data
aparrill-specterops Sep 26, 2025
f620b1d
Fix BH domain.users.with_old_pw
aparrill-specterops Sep 26, 2025
4befb86
Fix BH "could not connect" displayed in green on admin page
aparrill-specterops Sep 26, 2025
161b17a
BH: ignore passwords without last set time
aparrill-specterops Sep 26, 2025
d35182e
Switch BH fetch to be backgrounded with toasts
aparrill-specterops Sep 29, 2025
a430b6f
Decode and populate BloodHound finding assets
aparrill-specterops Oct 20, 2025
8414fa5
Pass down report template to report writers
aparrill-specterops Oct 21, 2025
8195d3c
Add field for offsetting BloodHound heading levels
aparrill-specterops Oct 22, 2025
8d9510e
Merge remote-tracking branch 'github/release/v6.1.0' into bloodhound-…
aparrill-specterops Oct 22, 2025
c75b469
Fix a few linting issues
aparrill-specterops Oct 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DOCS/configuring-global-settings/configuring-apis.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ Ghostwriter supports a growing number of APIs that the application leverages to

The supported APIs are:

<CardGroup cols={2}> <Card title="VirusTotal" icon="virus" iconType="solid" href="/configuring-global-settings/configuring-apis/configuring-virustotal" /> <Card title="Slack" icon="slack" iconType="brands" href="/configuring-global-settings/configuring-apis/configuring-slack" /> <Card title="Namecheap" icon="tag" iconType="solid" href="/configuring-global-settings/configuring-apis/configuring-namecheap" /> <Card title="AWS" icon="aws" iconType="brands" href="" /> <Card title="Digital Ocean" icon="droplet" iconType="solid" href="/configuring-global-settings/configuring-apis/configuring-cloud-services" /> </CardGroup>
<CardGroup cols={2}> <Card title="VirusTotal" icon="virus" iconType="solid" href="/configuring-global-settings/configuring-apis/configuring-virustotal" /> <Card title="Slack" icon="slack" iconType="brands" href="/configuring-global-settings/configuring-apis/configuring-slack" /> <Card title="Namecheap" icon="tag" iconType="solid" href="/configuring-global-settings/configuring-apis/configuring-namecheap" /> <Card title="AWS" icon="aws" iconType="brands" href="" /> <Card title="Digital Ocean" icon="droplet" iconType="solid" href="/configuring-global-settings/configuring-apis/configuring-cloud-services" /> <Card title="BloodHound" icon="paw" iconType="solid" href="/configuring-global-settings/configuring-apis/configuring-bloodhound" /> </CardGroup>

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: "Configuring BloodHound"
description: "Enabling the BloodHound API for reporting"
---

Ghostwriter integrates with BloodHound to pull data from the BloodHound graph and makes it available in your report
templates. If the BloodHound API is enabled and configured, Ghostwriter pulls the BloodHound data when you generate a
report.

To enable the BloodHound API, generate your access token, and configure the API in Ghostwriter, follow these steps:

### Generating an Access Token

Generate your BloodHound access token by logging into your BloodHound instance and opening the _Administration_ section
from the sidebar menu.

Click _Manage Users_ and then click the hamburger menu next to the user you want to use for the API access. Select
_Generate/Revoke API Tokens_.

<Frame>
<img src="/images/configuring-global-settings/configuring-apis/image-1.png" alt=""/>
</Frame>

Give your token a name and generate it. Take note of the _Key_ and _ID_ values in the modal window.

<Frame>
<img src="/images/configuring-global-settings/configuring-apis/image-2.png" alt=""/>
</Frame>

### Configuring the BloodHound API in Ghostwriter

Take the _ID_ and _Token_ values and plug them into the BloodHound API Configuration in Ghostwriter. You will also need
to add the URL of your BloodHound instance. The URL should be the base URL of your BloodHound instance, including the
protocol and port (if needed). For example, `http://bloodhound:8080`.

<Frame>
<img src="/images/configuring-global-settings/configuring-apis/image-3.png" alt=""/>
</Frame>

#### Testing the Configuration

Once you have configured the BloodHound API in Ghostwriter, an admin can test the configuration by clicking the _Test BloodHound_
button on the _Review Configuration_ page. This will send a request to the `/api/version` endpoint of the BloodHound API
and verify that the connection is successful.
1 change: 1 addition & 0 deletions DOCS/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"group": "Configuring APIs",
"pages": [
"configuring-global-settings/configuring-apis",
"configuring-global-settings/configuring-apis/configuring-bloodhound",
"configuring-global-settings/configuring-apis/configuring-slack",
"configuring-global-settings/configuring-apis/configuring-virustotal",
"configuring-global-settings/configuring-apis/configuring-namecheap",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
8 changes: 8 additions & 0 deletions ghostwriter/api/templatetags/json_enc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import json
from django.template import Library

register = Library()

@register.filter
def json_enc(value):
return json.dumps(value, indent="\t")
5 changes: 5 additions & 0 deletions ghostwriter/api/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ def form_data(
filename=None,
file_base64=None,
user_obj=None,
bloodhound_heading_offset=None,
**kwargs,
):
return ApiReportTemplateForm(
Expand All @@ -266,6 +267,7 @@ def form_data(
"p_style": p_style,
"filename": filename,
"file_base64": file_base64,
"bloodhound_heading_offset": bloodhound_heading_offset,
},
user_obj=user_obj,
)
Expand Down Expand Up @@ -341,6 +343,7 @@ def test_invalid_extension(self):
filename="test.txt",
file_base64="dGVzdA==",
user_obj=self.user,
bloodhound_heading_offset=0,
)
errors = form.errors.as_data()
self.assertFalse(form.is_valid())
Expand Down Expand Up @@ -382,6 +385,7 @@ def test_invalid_file(self):
filename="test.docx",
file_base64=self.valid_pptx,
user_obj=self.user,
bloodhound_heading_offset=0,
)
errors = form.errors.as_data()
self.assertFalse(form.is_valid())
Expand All @@ -402,6 +406,7 @@ def test_invalid_file(self):
filename="test.pptx",
file_base64=self.valid_docx,
user_obj=self.user,
bloodhound_heading_offset=0,
)
errors = form.errors.as_data()
self.assertFalse(form.is_valid())
Expand Down
7 changes: 7 additions & 0 deletions ghostwriter/commandcenter/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Django Imports
from django.contrib import admin
from django import forms
from django.template.loader import render_to_string

# Ghostwriter Libraries
from ghostwriter.commandcenter.forms import ReportConfigurationForm
Expand All @@ -13,6 +14,7 @@
ExtraFieldModel,
ExtraFieldSpec,
GeneralConfiguration,
BloodHoundConfiguration,
NamecheapConfiguration,
ReportConfiguration,
SlackConfiguration,
Expand Down Expand Up @@ -146,6 +148,11 @@ def has_delete_permission(self, request, obj=None) -> bool:

admin.site.register(ExtraFieldModel, ExtraFieldModelAdmin)

class BloodhoundConfigurationAdmin(SingletonModelAdmin):
change_form_template = "bloodhound_admin_change_form.html"

admin.site.register(BloodHoundConfiguration, BloodhoundConfigurationAdmin)

class CollabAdminBase(admin.ModelAdmin):
"""
Model Admin for collab-editor-enabled models.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 3.2.19 on 2025-06-03 17:39

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('commandcenter', '0033_auto_20241204_1810'),
]

operations = [
migrations.CreateModel(
name='BloodHoundConfiguration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('api_root_url', models.CharField(verbose_name='BloodHound API URL', help_text='The URL of the BloodHound instance', max_length=255, default="", blank=True)),
('api_key_id', models.CharField(verbose_name='BloodHound API Key ID', help_text='The ID portion of a BloodHound API Key', max_length=255, default="", blank=True)),
('api_key_token', models.CharField(verbose_name='BloodHound API Key Token', help_text='The token portion of a BloodHound API Key', max_length=255, default="", blank=True)),
],
options={
'verbose_name': 'BloodHound API Configuration',
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 3.2.19 on 2025-07-22 20:09

from django.db import migrations, models
import ghostwriter.commandcenter.models


class Migration(migrations.Migration):

dependencies = [
('commandcenter', '0034_bloodhoundconfiguration'),
]

operations = [
migrations.AlterField(
model_name='bloodhoundconfiguration',
name='api_root_url',
field=models.CharField(blank=True, default='', help_text='The URL of the BloodHound instance', max_length=255, validators=[ghostwriter.commandcenter.models.validate_endpoint], verbose_name='BloodHound API URL'),
),
]
12 changes: 12 additions & 0 deletions ghostwriter/commandcenter/migrations/0036_merge_20250729_1731.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Generated by Django 4.2 on 2025-07-29 17:31

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("commandcenter", "0035_bloodhound_project_config"),
("commandcenter", "0035_extrafieldmodel_is_collab_editable"),
]

operations = []
15 changes: 15 additions & 0 deletions ghostwriter/commandcenter/migrations/0042_merge_20250731_1842.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Generated by Django 4.2 on 2025-07-31 18:42

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
(
"commandcenter",
"0036_bannerconfiguration_and_more_squashed_0041_alter_bannerconfiguration_public_banner",
),
("commandcenter", "0036_merge_20250729_1731"),
]

operations = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.2 on 2025-08-21 17:11

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("commandcenter", "0042_merge_20250731_1842"),
]

operations = [
migrations.RenameField(
model_name="bloodhoundconfiguration",
old_name="api_key_id",
new_name="bloodhound_api_key_id",
),
migrations.RenameField(
model_name="bloodhoundconfiguration",
old_name="api_key_token",
new_name="bloodhound_api_key_token",
),
migrations.RenameField(
model_name="bloodhoundconfiguration",
old_name="api_root_url",
new_name="bloodhound_api_root_url",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2 on 2025-08-21 18:31

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
(
"commandcenter",
"0043_rename_api_key_id_bloodhoundconfiguration_bloodhound_api_key_id_and_more",
),
]

operations = [
migrations.AddField(
model_name="bloodhoundconfiguration",
name="bloodhound_results",
field=models.JSONField(
editable=False, null=True, verbose_name="Bloodhound Data"
),
),
]
77 changes: 75 additions & 2 deletions ghostwriter/commandcenter/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import json
from typing import Any, Callable, NamedTuple
from django import forms
from urllib.parse import urlparse

# Django Imports
from django.db import models
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

# 3rd Party Libraries
from ghostwriter.modules.reportwriter.forms import JinjaRichTextField
Expand All @@ -14,7 +17,6 @@
# Ghostwriter Libraries
from ghostwriter.singleton.models import SingletonModel


def sanitize(sensitive_thing):
"""
Sanitize the provided input and return for display in a template.
Expand All @@ -41,6 +43,75 @@ def sanitize(sensitive_thing):
sanitized_string = sensitive_thing[0:4] + "\u2717" * (length - 8) + sensitive_thing[length - 5 : length - 1]
return sanitized_string

def validate_endpoint(value: str):
url = urlparse(value)
if not url.scheme:
raise ValidationError(_("Missing scheme on URL"))
if not url.hostname:
raise ValidationError(_("Missing hostname on URL"))
if url.path:
raise ValidationError(_("Paths on endpoint URL are not supported"))

class BloodHoundConfiguration(SingletonModel):
"""
BloodHoundConfiguration represents a global BloodHound API integration that can be used to
access the BloodHound API of the configured instance.
"""
bloodhound_api_root_url = models.CharField(
max_length=255,
verbose_name="BloodHound API URL",
help_text="The URL of the BloodHound instance",
default="",
blank=True,
validators=[validate_endpoint],
)

bloodhound_api_key_id = models.CharField(
max_length=255,
verbose_name="BloodHound API Key ID",
help_text="The ID portion of a BloodHound API Key",
default="",
blank=True,
)

bloodhound_api_key_token = models.CharField(
max_length=255,
verbose_name="BloodHound API Key Token",
help_text="The token portion of a BloodHound API Key",
default="",
blank=True,
)

bloodhound_results = models.JSONField(
null=True,
verbose_name="Bloodhound Data",
editable=False,
)

def has_bloodhound_api(self) -> bool:
return self.bloodhound_api_root_url != "" and self.bloodhound_api_key_id != "" and self.bloodhound_api_key_token != ""

def __str__(self):
return "BloodHound API Configuration"

class Meta:
verbose_name = "BloodHound API Configuration"
# constraints = [
# models.CheckConstraint(
# check=models.Q(
# api_root_url="",
# api_key_id="",
# api_key_token=""
# ) | models.Q(
# ~models.Q(api_root_url="") &
# ~models.Q(api_key_id="") &
# ~models.Q(api_key_token="")
# ),
# name="commandcenter_bloodhoundconfiguration_all_or_none_set",
# #violation_error_message="Incomplete BloodHound API Configuration",
# ),
# ]


class NamecheapConfiguration(SingletonModel):
enable = models.BooleanField(default=False)
Expand Down Expand Up @@ -394,11 +465,13 @@ class ExtraFieldType(NamedTuple):
# Returns an "empty" value
empty_value: Callable[[], Any]


def float_widget(*args, **kwargs):
widget = forms.widgets.NumberInput(*args, **kwargs)
widget.attrs.setdefault("step", "any")
return widget


# Also edit frontend/src/extra_fields.tsx
EXTRA_FIELD_TYPES = {
"checkbox": ExtraFieldType(
Expand Down
Loading