Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
580 changes: 290 additions & 290 deletions .circleci/config.yml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions bloodhound/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
config.yml
**/__pycache__
13 changes: 13 additions & 0 deletions bloodhound/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# OPENAEV Environment Variables
# base URL to reach the OpenAEV server
# note this URL must be routable from inside the container
# so `localhost` will most likely not work
OPENAEV_URL=ChangeMe
# admin account API token from the OpenAEV server
OPENAEV_TOKEN=ChangeMe
OPENAEV_TENANT_ID=ChangeMe

# INJECTOR Environment Variables
INJECTOR_ID=bloodhound--ChangeMe
INJECTOR_NAME=BloodHound AD
INJECTOR_LOG_LEVEL=error
55 changes: 55 additions & 0 deletions bloodhound/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
FROM python:3.13-alpine AS builder

ENV PIP_VERSION=25.0.1

RUN apk update && apk upgrade && apk add git curl

WORKDIR /opt/injector_common
COPY --from=injector_common ./ ./

WORKDIR /
RUN git clone https://github.com/OpenAEV-Platform/client-python

RUN curl -sS https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \
python3 get-pip.py pip==${PIP_VERSION} && \
rm get-pip.py

RUN python3 -m pip install poetry==2.3.2 \
&& poetry config installer.re-resolve false \
&& poetry config virtualenvs.create false

ARG installdir=/opt/injector
ADD . ${installdir}
WORKDIR ${installdir}
RUN poetry install && \
python3 -m pip install --no-cache-dir pip==${PIP_VERSION}

FROM python:3.13-alpine AS runner

ENV PIP_VERSION=25.0.1

WORKDIR /opt/injector_common
COPY --from=injector_common ./ ./

ARG installdir=/opt/injector
WORKDIR ${installdir}
COPY --from=builder ${installdir} ${installdir}
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
# Bring the console scripts (notably `bloodhound-python`) installed by the
# builder into the runner; copying site-packages alone leaves them behind.
COPY --from=builder /usr/local/bin /usr/local/bin

ARG PYOAEV_GIT_BRANCH_OVERRIDE

RUN if [ -n "${PYOAEV_GIT_BRANCH_OVERRIDE}" ] ; then \
echo "Forcing specific version of client-python" && \
apk add --no-cache git curl && \
curl -sS https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \
python3 get-pip.py pip==${PIP_VERSION} && \
rm get-pip.py && \
pip install pip3-autoremove && \
pip-autoremove pyoaev -y && \
pip install git+https://github.com/OpenAEV-Platform/client-python@${PYOAEV_GIT_BRANCH_OVERRIDE} ; \
fi

CMD ["python3", "-m", "bloodhound_injector.openaev_bloodhound"]
50 changes: 50 additions & 0 deletions bloodhound/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# OpenAEV BloodHound AD Injector

Runs the [BloodHound.py](https://github.com/dirkjanm/BloodHound.py) Active
Directory collector (SharpHound-compatible) and surfaces users, computers and
privilege-escalation attack paths (Kerberoastable, AS-REP roastable) as
findings.

## Contract

- BloodHound - Collect AD attack paths: fields for domain, username, password
and domain controller. Produces VULNERABILITY (attack paths exist) and
DETECTION (enumeration detected) expectations.

## Credentials

The AD credentials are provided per inject through the contract fields and are
never logged (the password argument is redacted in logs).

## Configuration variables

The injector is configured either through environment variables (recommended, read from `docker-compose.yml` / the `.env` file for a Docker deployment) or through a `config.yml` file (for a manual deployment). Copy the provided `.env.sample` / `config.yml.sample` and fill in the values flagged with `ChangeMe`.

### OpenAEV environment variables

| Parameter | config.yml | Docker environment variable | Mandatory | Description |
|-------------------|---------------------|-----------------------------|-----------|------------------------------------------------------------------------------------|
| OpenAEV URL | `openaev.url` | `OPENAEV_URL` | Yes | The URL of the OpenAEV platform. Must be reachable from where the injector runs. |
| OpenAEV Token | `openaev.token` | `OPENAEV_TOKEN` | Yes | The administrator token of the OpenAEV platform. |
| OpenAEV Tenant ID | `openaev.tenant_id` | `OPENAEV_TENANT_ID` | No | Tenant identifier for multi-tenant deployments. When set, it must be a valid UUID. |

### Base injector environment variables

| Parameter | config.yml | Docker environment variable | Default | Mandatory | Description |
|---------------|----------------------|-----------------------------|--------------|-----------|-----------------------------------------------------------------|
| Injector ID | `injector.id` | `INJECTOR_ID` | / | Yes | A unique `UUIDv4` identifier for this injector instance. |
| Injector Name | `injector.name` | `INJECTOR_NAME` | BloodHound AD | No | The name of the injector as shown in OpenAEV. |
| Log Level | `injector.log_level` | `INJECTOR_LOG_LEVEL` | error | No | Verbosity of the logs. One of `debug`, `info`, `warn`, `error`. |

## Development

```bash
poetry install
poetry run python -m unittest
```

## Icon

`bloodhound_injector/img/icon-bloodhound.png` must follow the injector icon
standard (square 1:1, 512x512 PNG, solid opaque background, genuine BloodHound
artwork) - see OpenAEV-Platform/injectors#305.
2 changes: 2 additions & 0 deletions bloodhound/bloodhound_injector/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# OpenAEV BloodHound AD Attack-Path Injector
__version__ = "1.0.0"
Empty file.
27 changes: 27 additions & 0 deletions bloodhound/bloodhound_injector/configuration/config_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from bloodhound_injector.configuration.injector_config_override import (
InjectorConfigOverride,
)
from bloodhound_injector.contracts_bloodhound import BloodhoundContracts
from pydantic import Field
from pyoaev.configuration import ConfigLoaderOAEV, Configuration, SettingsLoader
Comment on lines +1 to +6


class ConfigLoader(SettingsLoader):
openaev: ConfigLoaderOAEV = Field(default_factory=ConfigLoaderOAEV)
injector: InjectorConfigOverride = Field(default_factory=InjectorConfigOverride)

def to_daemon_config(self) -> Configuration:
return Configuration(
config_hints={
"openaev_url": {"data": str(self.openaev.url)},
"openaev_token": {"data": self.openaev.token},
"openaev_tenant_id": {"data": self.openaev.tenant_id},
"injector_id": {"data": self.injector.id},
"injector_name": {"data": self.injector.name},
"injector_type": {"data": "openaev_bloodhound"},
"injector_contracts": {"data": BloodhoundContracts.build_contract()},
"injector_log_level": {"data": self.injector.log_level},
"injector_icon_filepath": {"data": self.injector.icon_filepath},
},
config_base_model=self,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pydantic import Field
from pyoaev.configuration import ConfigLoaderCollector


class InjectorConfigOverride(ConfigLoaderCollector):
id: str = Field(
description="A unique UUIDv4 identifier for this injector instance.",
)
name: str = Field(
default="BloodHound AD",
description="Name of the injector.",
)
icon_filepath: str | None = Field(
default="bloodhound_injector/img/icon-bloodhound.png",
description="Path to the icon file",
)
114 changes: 114 additions & 0 deletions bloodhound/bloodhound_injector/contracts_bloodhound.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from typing import List

from pyoaev.contracts import ContractBuilder
from pyoaev.contracts.contract_config import (
Contract,
ContractCardinality,
ContractConfig,
ContractElement,
ContractExpectations,
ContractOutputElement,
ContractOutputType,
ContractText,
Expectation,
ExpectationType,
SupportedLanguage,
prepare_contracts,
)
from pyoaev.security_domain.types import SecurityDomains

TYPE = "openaev_bloodhound"

AD_COLLECTION_CONTRACT = "a7d8e9f0-abbb-4ac8-8fd6-bc2d3e4f5a67"


class BloodhoundContracts:
@staticmethod
def build_contract():
contract_config = ContractConfig(
type=TYPE,
label={
SupportedLanguage.en: "BloodHound AD",
SupportedLanguage.fr: "BloodHound AD",
},
color_dark="#b00020",
color_light="#b00020",
expose=True,
)

domain = ContractText(key="domain", label="AD domain (FQDN)", mandatory=True)
username = ContractText(key="username", label="Username", mandatory=True)
password = ContractText(key="password", label="Password", mandatory=True)
domain_controller = ContractText(
key="domain_controller",
label="Domain controller (host or IP)",
mandatory=True,
)

expectations = ContractExpectations(
key="expectations",
label="Expectations",
mandatory=False,
cardinality=ContractCardinality.Multiple,
predefinedExpectations=[
Expectation(
expectation_type=ExpectationType.vulnerability,
expectation_name="Vulnerability",
expectation_description="Privilege-escalation attack paths exist.",
expectation_score=100,
expectation_expectation_group=False,
),
Expectation(
expectation_type=ExpectationType.detection,
expectation_name="Detection",
expectation_description="The AD enumeration is detected.",
expectation_score=100,
expectation_expectation_group=False,
),
],
)

output_users = ContractOutputElement(
type=ContractOutputType.Username,
field="users",
isMultiple=True,
isFindingCompatible=True,
labels=["bloodhound", "ad", "user"],
)
output_computers = ContractOutputElement(
type=ContractOutputType.Computer,
field="computers",
isMultiple=True,
isFindingCompatible=True,
labels=["bloodhound", "ad", "computer"],
)
output_paths = ContractOutputElement(
type=ContractOutputType.Vulnerability,
field="attack_paths",
isMultiple=True,
isFindingCompatible=True,
labels=["bloodhound", "ad", "attack_path"],
)

fields: List[ContractElement] = (
ContractBuilder()
.add_fields([domain, username, password, domain_controller, expectations])
.build_fields()
)

contract = Contract(
contract_id=AD_COLLECTION_CONTRACT,
config=contract_config,
label={
SupportedLanguage.en: "BloodHound - Collect AD attack paths",
SupportedLanguage.fr: "BloodHound - Collecter les chemins d'attaque AD",
},
fields=fields,
outputs=ContractBuilder()
.add_outputs([output_users, output_computers, output_paths])
.build_outputs(),
manual=False,
domains=[SecurityDomains.ENDPOINT.value],
)

return prepare_contracts([contract])
Empty file.
Loading
Loading