How It Works - krazybeastx1/omenhound GitHub Wiki
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)
Packages
| 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. |
Data model
- 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.
Direction conventions (BloodHound CE compatible)
| 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) |
The abuse engine
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.
Progressive hardening / performance
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.
Single binary, no build step
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 β