Source: SpecterOps/BloodHound @
65074fd
How It Works
BloodHound CE is the graph-based attack path analysis platform for Active Directory and Azure environments. It ingests data collected by analyzing-sharphound (AD) and AzureHound (Azure), stores it in a graph database, and exposes pathfinding and analysis capabilities through a REST API served to a React SPA frontend.
The system is a monolithic Go binary (cmd/api/) that serves a REST API and embedded React static assets. It maintains two database backends: a relational PostgreSQL instance for users, roles, permissions, audit logs, and ingest metadata (~40 tables), and a graph database — either Neo4j or PostgreSQL-via-DAWGS — for the AD/Azure entity graph. The graph abstraction layer, DAWGS, is SpecterOps’ own library providing a database-agnostic graph.Database interface with typed query builders, batch operations, and pathfinding algorithms.
A background DataPipe daemon orchestrates the ingest-analyze lifecycle on a configurable tick: it purges stale data, processes queued ingestion files, then runs post-analysis operations (ADCS ESC edges, NTLM relay edges, Tier Zero tagging, GPO application). The analysis engine computes synthetic edges that SharpHound doesn’t collect directly — things like DCSync, SyncLAPSPassword, ADCSESC1 through ADCSESC13, and CoerceAndRelayNTLMToSMB — by reasoning over the ingested graph topology.
Architecture
SharpHound/AzureHound JSON/ZIP
│
▼
POST /api/v2/file-ingest
│
▼
┌─────────────────────────────────────────────┐
│ Go API Server (monolith) │
│ │
│ ┌──────────┐ ┌────────────┐ ┌───────────┐ │
│ │ FileIngest│→ │ Graphify │→ │ DAWGS │ │
│ │ Service │ │ Service │ │ Graph DB │ │
│ └──────────┘ └────────────┘ └───────────┘ │
│ │ │
│ ┌──────────────┐ ┌───────────┐ │ │
│ │ Analysis │← │ DataPipe │─────┘ │
│ │ Engine │ │ Daemon │ │
│ └──────────────┘ └───────────┘ │
│ │ │
│ ┌──────────────┐ ┌───────────┐ │ │
│ │ Pathfinding │→ │ DAWGS │ │ │
│ │ / Cypher API │ │ Queries │ │ │
│ └──────────────┘ └───────────┘ │ │
│ │ │
│ ┌──────────────┐ ┌───────────┐ │ │
│ │ Auth / RBAC │ │PostgreSQL│ │ │
│ │ (JWT/OIDC/ │ │ (users, │ │ │
│ │ SAML) │ │ roles) │ │ │
│ └──────────────┘ └───────────┘ │ │
└─────────────────────────────────────────────┘
│ REST API (gorilla/mux)
▼
React SPA (Vite + MUI + Sigma.js)
Frontend stack: React 18, TypeScript, Redux Toolkit + Redux Saga, MUI v5, Sigma.js v2 + Graphology for graph rendering with ForceAtlas2 layout. The frontend is a Yarn workspace monorepo including bh-shared-ui, doodle-ui (Storybook component library), and js-client-library.
The Spine
The primary request path — file upload through graph analysis to visualization — follows this flow:
- Upload:
POST /api/v2/file-ingest→ validates MIME type, creates afile_upload_jobin PostgreSQL, stores the file on disk - Queue: An ingest task is created in the
ingest_taskstable - DataPipe picks it up: The daemon dequeues and calls
Graphify.IngestFile() - Graphify dispatches by
meta.Typetag in the JSON stream — separate handlers for each AD entity type (User, Computer, Group, GPO, OU, Container, Domain, certificate infrastructure) and Azure types - Conversion: Typed converters (
convertors.go,azure_convertors.go) map raw JSON properties to DAWGS graph node/relationship types - Batch write: Nodes are upserted by
object_idwithlastseentimestamps using DAWGSBatchUpdater(threshold: 500 nodes per batch). Deduplication uses aChangeManagerinterface - Post-analysis runs:
RunAnalysisOperations()creates synthetic edges — ADCS ESC chains, NTLM relay opportunities, DCSync paths, LAPS sync, GPO application — using reader/writer worker patterns within graph transactions - Frontend queries: Pathfinding and entity queries hit
GET /api/v2/pathfinding/shortest-pathorGET /api/v2/ad/entity/{object_id}, which use DAWGSFetchAllShortestPaths()with filtered relationship sets
Key Patterns
DAWGS Graph Abstraction
The entire graph layer goes through SpecterOps’ DAWGS library rather than raw Cypher or SQL. This provides:
// Type-safe query building
criteria := []graph.Criteria{
query.Equals(query.StartID(), startNode.ID),
query.KindIn(pathfindingRelationships...),
}
return tx.Relationships().Filter(query.And(criteria...)).FetchAllShortestPaths(...)This abstracts over Neo4j vs. PostgreSQL backends — the same Go code generates Cypher for Neo4j or recursive SQL for PostgreSQL. The library includes Roaring bitmap optimizations, traversal algorithms (BFS/DFS), and a graph cache layer.
Cuelang-Generated Schema
Graph schema definitions live in packages/cue/bh/ad/, packages/cue/bh/azure/, and packages/cue/bh/common/ as CUE files. A code-generation step produces Go constants in packages/go/graphschema/ — 16 AD node kinds, 70+ relationship kinds, and parallel Azure types. This ensures compile-time safety between schema definition and application code.
Cypher Complexity Limiting
User-submitted Cypher queries (via POST /api/v2/query/cypher) are parsed through DAWGS’ cypher/frontend parser, walked via cypher/models/walk, and scored by cypher/analyzer. Queries exceeding a “fitness” threshold (default: -7) get reduced timeouts or rejection. Mutation queries require GraphDBMutate permission and are audit-logged.
Post-Processed Synthetic Edges
Many relationship types in the pathfinding graph are not collected by SharpHound at all — they’re computed post-ingestion:
| Edge Type | Source Data | Computation |
|---|---|---|
ADCSESC1–ADCSESC13 | Cert templates + CAs + enrollment agents | ESC chain analysis per template |
DCSync | Domain + high-privilege groups | Tier Zero group membership |
SyncLAPSPassword | GPO-linked computers + LAPS-enabled GPOs | GPO-to-computer LAPS mapping |
CoerceAndRelayNTLMToSMB | Computer objects with specific attributes | Petitpotam-to-SMB relay chain |
ProtectAdminGroups | SDProp + AdminSDHolder configuration | ACL inference |
Each is implemented as a separate analysis file in packages/go/analysis/ad/.
Non-Obvious Details
Dual graph backend is a first-class design decision, not a migration artifact. The dawgs/drivers/pg package provides a full PostgreSQL graph driver. This means BloodHound CE can run entirely on PostgreSQL without Neo4j — a significant operational simplification over BloodHound 4.x (Neo4j-only). The relational and graph data can live in the same PostgreSQL instance.
ETAC filtering happens at the API response layer, not in the graph query. The Environment Targeted Access Control system restricts users to specific AD domains or Azure tenants by filtering nodes/edges after the graph query executes. This means expensive queries still run to completion even if most results will be discarded — a correctness-over-performance trade-off.
JSON stream ingestion uses tagged streams, not plain JSON arrays. SharpHound output uses a tagged JSON stream format with meta blocks marking entity types. CreateIngestDecoder(reader, "data", 1) seeks to specific tags. ZIP files contain multiple independent streams that are processed sequentially.
The Database interface is explicitly deprecated — cmd/api/src/database/db.go has ~180 lines of method signatures with a comment directing new code to create narrow per-service interfaces. The MockDatabase generated by go.uber.org/mock must stay in sync with this interface per the repo’s AGENTS.md.
OpenGraph extension allows runtime schema changes. Users can define custom node kinds and relationship types through the API, stored in PostgreSQL (schema_extensions, schema_node_kinds tables) and synced to the graph. This enables mapping non-AD/Azure environments (cloud IAM, network topologies) onto the same pathfinding engine.
Assessment
BloodHound CE is a substantial engineering effort — ~142K lines of Go backend, ~153 frontend source files, with excellent project structure and clear architectural boundaries. The DAWGS abstraction layer is a key differentiator, enabling database portability and type-safe graph operations.
Strengths: The separation between data collection (SharpHound), graph storage (DAWGS), and visualization (React) is clean. The post-processing analysis engine that computes synthetic edges from raw collected data is well-designed — it means SharpHound stays a simple collector while complex reasoning lives server-side. The auth model (JWT + OIDC + SAML + TOTP with RBAC) makes it viable for enterprise deployment. The Cypher complexity limiter using AST analysis (not string matching) is a smart defensive measure. LikeC4 architecture documentation is well-maintained.
Concerns: ETAC filtering at the response layer will waste graph compute on multi-tenant deployments. The deprecated Database interface is still widely used despite the stated direction toward narrow interfaces. The monolithic Go binary bundles a lot — API server, graph queries, analysis engine, background daemons — which limits horizontal scaling of individual components.
Notable: BloodHound CE’s shift from Neo4j-only to a portable graph backend via DAWGS is architecturally significant. It eliminates the operational burden of running Neo4j alongside PostgreSQL, and the DAWGS query builder ensures the same application code works across both backends. The OpenGraph extension mechanism positions BloodHound as a general-purpose attack path analysis tool, not just an AD/Azure platform.
Related
- analyzing-sharphound — BloodHound’s AD data collector; produces the JSON files this backend ingests
- analyzing-scubagear — AD offensive security tool with complementary enumeration capabilities
- analyzing-netext — Network extension mapping; similar graph-based pathfinding approach for network topologies