Building Decoy Object Tagging in BloodHound: A Codebase Walkthrough

How a honeypot-hunting idea became a first-class BloodHound annotation, and how you can trace the files that needed to change without guessing.

Why This Feature Exists

The first version of this idea was not “BloodHound should detect honeypots.” That would be too confident.

The better version was: if an operator decides an object is likely a decoy, BloodHound should let them preserve that decision and keep its default attack-path workflows from routing through it.

The inspiration came from two places. The first was Charles F. Hamilton’s post, Hunting Honey Pots as Red Teamers, which focuses on the identity and activity gaps that honeytoken accounts often expose. One useful signal is an account that looks attractive but has little or no real logon history. The second was talking with SpecterOps folks during AT:IDOT training. BloodHound is where many operators reason about attack paths, so deception awareness belongs in the graph workflow, not in a separate note file.

That framing matters. The feature does not silently classify accounts as decoys. It adds a durable Decoy mark, shows it in the graph, lets users toggle it from familiar places, suggests review for suspicious user objects, and excludes confirmed decoys from predefined attack-path queries.

Step 0: Start With A Search, Not A Design Doc

The fastest way into the codebase is to find the feature that already behaves like the one you want. For Decoy, that feature is Owned.

Owned is already a manual analyst annotation. It is stored by the backend, materialized into graph data, exposed through graph API responses, rendered as a glyph, and available from graph context menus. Decoy needs almost the same lifecycle, with different semantics.

The first search I would run is:

rg -n "Owned|AssetGroupTagTypeOwned|Tag_Owned|isOwnedObject|OWNED_OBJECT_TAG"

The useful results point to a few anchor files:

cmd/api/src/model/assetgrouptags.go
cmd/api/src/database/assetgrouptags.go
packages/go/analysis/agt.go
packages/go/analysis/tiering/tiering.go
cmd/api/src/model/unified_graph.go
packages/javascript/bh-shared-ui/src/hooks/useAssetGroupTags/useAssetGroupTags.tsx
cmd/ui/src/views/Explore/utils.ts
packages/javascript/bh-shared-ui/src/views/Explore/ContextMenu/ContextMenuPrivilegeZonesEnabled.tsx
cmd/ui/src/views/Explore/ContextMenu/ContextMenu.tsx

Some grep hits are noise. Comments about file ownership, generic “owned by user” text, and test fixtures are not implementation anchors. The pattern that matters is where Owned crosses a system boundary: database row, analysis pass, graph node property, API response, UI helper, UI action, or query behavior.

That search teaches the first real lesson: Decoy is not one button. It has to exist in persistence, graph analysis, API shape, generated client types, shared frontend helpers, legacy UI, privilege-zone UI, predefined queries, and tests.

Step 1: Understand The Two Tag Worlds

BloodHound currently has two related systems for this kind of object annotation.

Concept Legacy path AGT / Privilege Zones path
Storage asset_groups asset_group_tags
Graph marker system_tags contains decoy graph kind Tag_Decoy
Feature flag tier_management_engine off tier_management_engine on
UI state check node.isDecoyObject or system_tags node.kinds contains Tag_Decoy
Similar existing feature legacy Owned asset group Owned asset group tag

This split drives the whole implementation.