Skip to content

Fix unstable node IDs by namespacing with account SID (#23)#24

Merged
p0dalirius merged 1 commit into
mainfrom
bugfix-stable-node-ids-from-sid
Jun 22, 2026
Merged

Fix unstable node IDs by namespacing with account SID (#23)#24
p0dalirius merged 1 commit into
mainfrom
bugfix-stable-node-ids-from-sid

Conversation

@p0dalirius

Copy link
Copy Markdown
Contributor

Linked Issue

Closes #23

Root Cause

KeyCredential node ids were built as <Identifier>.<distinguishedName>. A distinguishedName is mutable — it changes on a rename or an OU move — while BloodHound merges nodes idempotently on their id. As a result, re-collecting a principal after it was renamed or relocated produced a brand-new id and therefore a duplicate KeyCredential node (and, since the old node is no longer re-emitted, an orphan). The account objectSid, which the collector already reads to build the HasKeyCredential edge, is immutable for the principal's lifetime and is the correct stable namespace.

Fix Description

  • Resolve objectSid once per LDAP entry at the top of the outer loop (it is identical for every credential on the account), and skip the entry if the SID is missing or fails to parse.
  • Namespace the node id by accountSid.String() instead of distinguishedName: <SID>.<Identifier> (or <SID>.Unknown-<n> when the credential has no identifier).
  • Remove the now-redundant per-credential SID extraction that previously sat just before the HasKeyCredential edge; the edge now uses the hoisted accountSid.

The key-material node id is intentionally left keyed on kc.Identifier (the public-key hash) so credentials that share the same key still converge on a single key-material node — the basis of the shared-key query.

How Verified

Static: keyCredentialNodeId is now derived solely from accountSid (immutable) and kc.Identifier/positional fallback, so re-collection after a rename or OU move yields the same id and merges. go build ./..., go vet ./..., and gofmt -l parse.go (no output) all pass.

Test Coverage

None: ParseResults consumes *ldap.Entry values and parsed msDS-KeyCredentialLink blobs from a live directory; no fixture harness exists in the repo. The change is limited to id derivation and the relocation of SID extraction.

Scope of Change

  • Files changed: parse.go
  • Submodule pointer updated: no
  • Behavioral changes outside the bug fix: SID resolution now happens once per entry instead of once per credential, and an entry whose objectSid is missing/unparseable is skipped entirely (previously its nodes were still emitted but left unlinked to the principal). This is a direct consequence of needing the SID before node creation; a KeyCredential that cannot be linked to its principal has no useful standalone meaning.

Risk and Rollout

Local to parse.go; changes the id scheme, so a one-time re-ingest will create the new SID-based nodes alongside any previously ingested DN-based nodes until the old source-kind data is cleared. Safe to merge.

@p0dalirius
p0dalirius merged commit 1e426e8 into main Jun 22, 2026
8 checks passed
@p0dalirius
p0dalirius deleted the bugfix-stable-node-ids-from-sid branch June 22, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KeyCredential node IDs derived from distinguishedName are unstable across renames/moves

1 participant