Repository navigation
How It Works
OmenHound is a small set of independent Go packages wired together by one binary.
Graph JSON
collector ─────────────────────────▶ ingest ──▶ Store ──▶ Neo4j / memory
(ldap · sample · BH import) │
├──▶ API ──▶ Web UI (canvas graph)
│ └─▶ abuse engine (edge → command + OPSEC)
└──▶ audit (JSON-lines, redacted)
| Package | Responsibility |
|---|---|
pkg/model |
Node/edge vocabulary, Graph, Meta, SID-keyed identity, O(1) dedup index. |
internal/collector |
Collector interface; sample and ldap collectors (LDAP + SMB/RPC passes); method planning; DC SRV discovery. |
internal/bloodhound |
BloodHound CE JSON/--zip import and export. |
internal/graph |
Store interface; memory (tests/demos) and neo4j (production) backends. |
internal/ingest |
Strict decode, validation (size, referential integrity, vocabulary), write. |
internal/api |
HTTP endpoints, bearer-token auth, security headers, rate limiting, embedded UI. |
internal/abuse |
Edge → dynamic command + alternates + OPSEC + event IDs + warnings. |
internal/creds |
Session-scoped, memory-only credential store with TTL and zeroing. |
internal/audit |
Redacting JSON-lines audit logger. |
internal/act |
The only package that writes to AD (unicodePwd over LDAPS). |
web |
Embedded static UI (embed.FS) — no build step, no CDN. |
-
Node identity is the AD
objectSID(users, groups, computers, domains) orobjectGUID(OUs, GPOs, containers) — stable and domain-unique. This is exactly what BloodHound CE uses, so imported objects and collected objects merge by SID. -
Node kinds mirror CE labels:
User,Group,Computer,Domain,OU,GPO,Container,Trust, andBase(CE's placeholder label for a referenced-but-not-collected object). -
Edge kinds mirror CE relationship names (
GenericAll,WriteDacl,MemberOf,AdminTo,HasSession,GPLink,TrustedBy, …) so existing BloodHound Cypher keeps working. See Neo4j Schema & Attack Paths. -
Graph.Meta.Warningscarries honest, non-fatal caveats (e.g. skipped collection methods) all the way through export and the API.
| Edge | Direction | Meaning |
|---|---|---|
MemberOf |
member → group | principal belongs to group |
GPLink |
GPO → container | GPO is linked at the OU/domain/site |
Contains |
container → child | structural containment |
TrustedBy |
trusted → trusting | source domain is trusted by target |
HasSession |
user → computer | user has a session on the host |
AdminTo |
principal → computer | principal is a local admin |
AllowedToDelegate |
computer → target | constrained delegation |
AllowedToAct |
principal → computer | RBCD (allowed to act on behalf) |
For a concrete edge (src) —kind→ (tgt), abuse.Render interpolates real node
properties into an attack template:
-
{DOMAIN},{DC},{SOURCE_SAM},{SOURCE_DN},{SOURCE_SID},{SOURCE_NAME} -
{TARGET_SAM},{TARGET_DN},{TARGET_SID},{TARGET_NAME}
Each template carries a summary, a primary command, alternates, an OPSEC/detection
note, Windows event IDs, and required preconditions. Any placeholder with no
backing data becomes an explicit ⚠ warning rather than a silently broken command.
The current implementation is the result of a dedicated hardening + optimization pass:
-
Graph dedup —
Graphkeeps O(1) node/edge indexes (nodeIdx/edgeIdx), so large collections don't degrade quadratically. -
Store adjacency — the memory store maintains
out/inadjacency maps, making neighbor lookups O(degree) instead of O(E). -
Batched Neo4j writes — writes are grouped by label/relationship type and sent via
UNWINDin batches (5000), cutting round-trips by orders of magnitude. - Strict ingest — payloads are size-bounded (512 MiB), unknown JSON fields are rejected, and trailing JSON is refused.
-
API hardening —
Content-Security-Policy,nosniff,HSTS(on TLS), a per-IP token-bucket limiter plus a separate write-method limiter, pagination on list endpoints (default 1000, max 10000), andclientIPthat deliberately ignoresX-Forwarded-For. -
Collector bounds — dial timeouts (SMB gated by a TCP probe, 10 s dial / 30 s
per-host RPC), paged LDAP searches (500/page, 500k entries cap, 60s server-side time
limit), and a plaintext-LDAP refusal. The SMB pass runs computers sequentially (an
OPSEC choice) under the run's
--timeout. -
Credential hygiene — secrets are
[]byte, length-capped, explicitly zeroed withruntime.KeepAlive, and swept by a background janitor; the audit logger redacts sensitive field names before writing.
web/static is embedded with //go:embed. The frontend is a vanilla-JS canvas force
graph — no npm, no bundler, no CDN — so bin/omenhound is fully self-contained and works
in an air-gapped environment.
Next: Web UI Guide →
MIT licensed · schema-compatible with BloodHound CE (Apache-2.0) · no BloodHound code is vendored.
Use only against systems you own or are explicitly authorised to test.