feat(bloodhound): add Active Directory attack-path injector (#316) - #326
feat(bloodhound): add Active Directory attack-path injector (#316)#326SamuelHassine wants to merge 7 commits into
Conversation
Runs the BloodHound.py AD collector and surfaces users, computers and privilege-escalation attack paths as findings. Wires the injector into the CircleCI build, publish and test matrices.
There was a problem hiding this comment.
Pull request overview
This PR introduces a new bloodhound/ injector module that wraps BloodHound.py to collect Active Directory data and emit OpenAEV findings (users, computers, and privilege-escalation “attack path” indicators), and wires the new injector into CircleCI build/test/publish matrices.
Changes:
- Add the BloodHound injector implementation (contracts, config loader, executor, entrypoint) plus unit tests for contract shape and JSON parsing.
- Add containerization + runtime scaffolding for the new injector (Dockerfile, compose, samples, manifest metadata, docs).
- Extend CircleCI matrices to include
bloodhoundfor tests and image build/publish.
Reviewed changes
Copilot reviewed 17 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| bloodhound/test/test_bloodhound.py | Adds unit tests for contract outputs and collection JSON parsing. |
| bloodhound/test/init.py | Test package marker. |
| bloodhound/README.md | Documents injector purpose, contract, dev workflow, and icon requirements. |
| bloodhound/pyproject.toml | Defines the new injector package metadata and dependencies. |
| bloodhound/manifest-metadata.json | Adds catalog/manager metadata for the new injector image. |
| bloodhound/Dockerfile | Builds and runs the injector container image. |
| bloodhound/docker-compose.yml | Provides a local compose service definition. |
| bloodhound/config.yml.sample | Adds sample runtime configuration. |
| bloodhound/bloodhound_injector/openaev_bloodhound.py | Implements the injector entrypoint and execution callback handling. |
| bloodhound/bloodhound_injector/img/README.md | Documents required icon asset placement and standards. |
| bloodhound/bloodhound_injector/helpers/bloodhound_executor.py | Runs bloodhound-python and parses emitted JSON into outputs. |
| bloodhound/bloodhound_injector/helpers/init.py | Helpers package marker. |
| bloodhound/bloodhound_injector/contracts_bloodhound.py | Defines the injector contract inputs/outputs and expectations. |
| bloodhound/bloodhound_injector/configuration/injector_config_override.py | Declares injector config overrides (id/name/icon path). |
| bloodhound/bloodhound_injector/configuration/config_loader.py | Wires config hints and contracts into the OpenAEV configuration object. |
| bloodhound/bloodhound_injector/configuration/init.py | Configuration package marker. |
| bloodhound/bloodhound_injector/init.py | Declares module version metadata. |
| bloodhound/.env.sample | Adds sample environment variables. |
| bloodhound/.dockerignore | Excludes config and caches from Docker build context. |
| .circleci/config.yml | Adds bloodhound to the test/build/publish matrices. |
Adds coverage for __init__, process_message, start and the BloodHound collection runner to satisfy the codecov patch gate.
#316) Report a failure when bloodhound-python exits non-zero instead of returning success=True with empty outputs, including a redacted stderr/stdout snippet for troubleshooting. Redact the password argument by position when logging the command. Raise an actionable error when the injector icon asset is missing so the startup failure is self-explanatory. Add a test for the non-zero exit path asserting the password never leaks into the failure message.
|
Independent review + fix pass complete. Read the full changed files (not just the diff) and addressed all findings in 911facd:
Verification: local isort (--profile black) / black / flake8 clean; full unit-test suite passes (10 tests); all CI checks green (GitHub Actions test matrix, CircleCI test + build_docker_images, codecov, signed-commits, linked-issue, PR-title). Resolved all 3 review threads (Copilot). Non-CI follow-up: the genuine 512x512 opaque BloodHound icon PNG still needs to be committed at Note: I could not submit a formal Approve review - GitHub blocks approving your own pull request, and the PR author is the reviewing account. A separate reviewer approval is still required to clear |
- Dockerfile: use a POSIX `[ -n "..." ]` test for the PYOAEV_GIT_BRANCH_OVERRIDE block so it works under Alpine /bin/sh (the Bash-only `[[ ... ]]` never evaluated true). - Dockerfile: copy /usr/local/bin from the builder into the runner so the `bloodhound-python` console script is actually present at runtime; copying site-packages alone left it behind and every collection would have failed with "bloodhound-python not found in the image". - openaev_bloodhound: read the icon from the configurable `injector_icon_filepath` (fallback to the default path) and use that same resolved path in the missing-icon error message. - bloodhound_executor: de-duplicate parsed names with a set and sort the glob for deterministic output; make `_privileged` use the same name/samaccountname fallback as `_names`, sort the glob, and de-duplicate findings. - test: cover de-duplication across multiple *_users.json files and the samaccountname fallback.
|
Second review-and-fix pass complete (commit 653f22d). New review comments addressed (all 4 threads resolved):
Additional finding from the independent re-review (also fixed in 653f22d):
Verification:
Non-CI follow-up (unchanged): the genuine 512x512 opaque BloodHound icon PNG still needs to be committed at Note: I could not submit a formal Approve review - GitHub blocks approving your own pull request and the reviewing account is the PR author. A second maintainer approval is still required to clear |
Harden the collector parser against a present-but-null "Properties" object in the BloodHound JSON (valid JSON that previously crashed parse_collection with an AttributeError) by treating it as an empty mapping in both _names() and _privileged(). Extract a shared _name_or_none() helper so the name -> samaccountname fallback ordering lives in one place, keeping the deliberate difference that enumeration lists skip nameless entries while privileged findings label them "unknown". Add a regression test for the null-Properties case.
|
Review-and-fix pass complete (commit 3427c95). Independent senior review of the full changed files (not just the diff), plus all prior reviewer threads addressed. LGTM. New finding fixed this pass:
Verification:
All 8 review threads are resolved. PR is Note: I could not submit a formal Approve review - GitHub blocks approving your own pull request and the reviewing account is the PR author. Consider this comment an explicit LGTM; a second maintainer approval is still required to clear Non-CI follow-up (unchanged): the genuine 512x512 opaque BloodHound icon PNG still needs to be committed at |
…#316) - Drop the `--zip` flag from the bloodhound-python command. With `--zip` the collector compresses the per-object JSON files into an archive and then deletes the loose files, so parse_collection (which globs the loose *_users.json / *_computers.json in the workdir) always found zero objects and every successful run reported "Collected 0 users and 0 computers". Without `--zip` the loose JSON stays on disk for parsing. - Log the exception via injector_logger before sending the ERROR callback in process_message, matching the injector implementation guide and easing operational troubleshooting. - Document the OpenAEV and base injector environment variables in the README (Configuration variables section), consistent with the other injectors.
|
Review-and-fix pass complete (commit bd9938c). Independent senior review of the full changed files (not just the diff), plus both remaining Copilot threads addressed and resolved. LGTM. New findings fixed this pass:
Verification:
All review threads are resolved. PR is MERGEABLE; mergeStateStatus is BLOCKED only on REVIEW_REQUIRED. Note: I could not submit a formal Approve review - GitHub blocks approving your own pull request and the reviewing account is the PR author. Consider this comment an explicit LGTM; a second maintainer approval is still required to clear REVIEW_REQUIRED and unblock merge. Non-CI follow-up (unchanged): the genuine 512x512 opaque BloodHound icon PNG still needs to be committed at |
| from bloodhound_injector.configuration.config_loader import ConfigLoader | ||
| from bloodhound_injector.helpers.bloodhound_executor import BloodhoundExecutor | ||
| from pyoaev.helpers import OpenAEVConfigHelper, OpenAEVInjectorHelper | ||
|
|
||
| from injector_common.data_helpers import DataHelpers | ||
| from injector_common.dump_config import intercept_dump_argument |
| 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 |
Summary
bloodhoundto the CircleCIbuild_docker_images,publish_imagesandtestmatrices.Notes
bloodhound-pythonexit returns an ERROR result with a trimmed, password-scrubbed stderr/stdout snippet instead of a false SUCCESS with empty outputs. The generic handler also logs the exception viainjector_loggerbefore the ERROR callback.FileNotFoundError. The icon path is read from the configurableinjector_icon_filepath(falling back to the default), and that same resolved path is used in the error message._namesand_privilegediterate over a sorted glob, de-duplicate via a set, and share a singlename->samaccountnamefallback. Parsing also tolerates a present-but-nullPropertiesobject without crashing.bloodhound-python -c All(without--zip) so the loose per-object JSON files stay on disk forparse_collectionto glob; with--zipthe tool compresses then deletes them, which would make every run report zero users/computers.Known follow-up (non-CI blocker)
bloodhound_injector/img/icon-bloodhound.png(or the path set viainjector_icon_filepath) before the injector can start in a real deployment; until then startup raises the actionable error above. CI is unaffected because the tests mock the icon read.Test plan
Propertiestolerance) - 12 testsbuild_docker_images-bloodhoundmatrixCloses #316