feat(bus): identity.* topic family (formed / observation.linked / merged)
Fourth of the five-step identity-resolution substrate. Constants and builder ship now; no publishers exist yet — they land with the clusterer worker. Subscribers (webhook worker, dashboard SSE relay) can register against identity.> from day one. * decnet/bus/topics.py — IDENTITY root + IDENTITY_FORMED / IDENTITY_OBSERVATION_LINKED / IDENTITY_MERGED leaves; identity() builder mirroring the attacker() / system() helpers. Module docstring topic-tree updated. * tests/bus/test_topics.py — assert builder produces the expected three topic strings + rejects empty event_type. Wiki Service-Bus.md and a new Identity-Resolution.md page land in the companion wiki-checkout commit.
This commit is contained in:
@@ -14,6 +14,9 @@ Token structure (NATS-style, dot-separated):
|
|||||||
attacker.scored
|
attacker.scored
|
||||||
attacker.session.started
|
attacker.session.started
|
||||||
attacker.session.ended
|
attacker.session.ended
|
||||||
|
identity.formed
|
||||||
|
identity.observation.linked
|
||||||
|
identity.merged
|
||||||
credential.captured
|
credential.captured
|
||||||
credential.reuse.detected
|
credential.reuse.detected
|
||||||
system.log
|
system.log
|
||||||
@@ -33,6 +36,7 @@ from __future__ import annotations
|
|||||||
TOPOLOGY = "topology"
|
TOPOLOGY = "topology"
|
||||||
DECKY = "decky"
|
DECKY = "decky"
|
||||||
ATTACKER = "attacker"
|
ATTACKER = "attacker"
|
||||||
|
IDENTITY = "identity"
|
||||||
SYSTEM = "system"
|
SYSTEM = "system"
|
||||||
CREDENTIAL = "credential"
|
CREDENTIAL = "credential"
|
||||||
|
|
||||||
@@ -83,6 +87,27 @@ ATTACKER_SESSION_ENDED = "session.ended"
|
|||||||
# provider summary so SIEM-bound webhooks don't need to re-query the DB.
|
# provider summary so SIEM-bound webhooks don't need to re-query the DB.
|
||||||
ATTACKER_INTEL_ENRICHED = "intel.enriched"
|
ATTACKER_INTEL_ENRICHED = "intel.enriched"
|
||||||
|
|
||||||
|
# Identity-resolution event types (second/third tokens under ``identity``).
|
||||||
|
# Published by the (future) clusterer worker — see
|
||||||
|
# development/IDENTITY_RESOLUTION.md. Constants ship in this commit;
|
||||||
|
# no publishers exist yet, but consumers (webhook worker, dashboard
|
||||||
|
# SSE relay) can subscribe to ``identity.>`` from day one and receive
|
||||||
|
# events the instant the clusterer comes online.
|
||||||
|
#
|
||||||
|
# identity.formed — clusterer creates a new identity from
|
||||||
|
# one or more observations
|
||||||
|
# identity.observation.linked — observation attached to an existing
|
||||||
|
# identity (or reattached from another)
|
||||||
|
# identity.merged — two identities collapsed; loser gets
|
||||||
|
# ``merged_into_uuid`` set, subscribers
|
||||||
|
# re-key cached references to the winner
|
||||||
|
#
|
||||||
|
# ``identity.campaign.assigned`` is deferred; it ships when the campaign
|
||||||
|
# clusterer ships. YAGNI before then.
|
||||||
|
IDENTITY_FORMED = "formed"
|
||||||
|
IDENTITY_OBSERVATION_LINKED = "observation.linked"
|
||||||
|
IDENTITY_MERGED = "merged"
|
||||||
|
|
||||||
# Credential event types (second/third tokens under ``credential``).
|
# Credential event types (second/third tokens under ``credential``).
|
||||||
# ``credential.captured`` fires once per upserted Credential row — the
|
# ``credential.captured`` fires once per upserted Credential row — the
|
||||||
# correlator listens for it and runs the cred-reuse query in response,
|
# correlator listens for it and runs the cred-reuse query in response,
|
||||||
@@ -186,6 +211,19 @@ def attacker(event_type: str) -> str:
|
|||||||
return f"{ATTACKER}.{event_type}"
|
return f"{ATTACKER}.{event_type}"
|
||||||
|
|
||||||
|
|
||||||
|
def identity(event_type: str) -> str:
|
||||||
|
"""Build ``identity.<event_type>``.
|
||||||
|
|
||||||
|
*event_type* is typically one of :data:`IDENTITY_FORMED`,
|
||||||
|
:data:`IDENTITY_OBSERVATION_LINKED`, :data:`IDENTITY_MERGED`. Dotted
|
||||||
|
leaves (``observation.linked``) are permitted — same rationale as
|
||||||
|
:func:`system`.
|
||||||
|
"""
|
||||||
|
if not event_type:
|
||||||
|
raise ValueError("identity topic requires a non-empty event_type")
|
||||||
|
return f"{IDENTITY}.{event_type}"
|
||||||
|
|
||||||
|
|
||||||
def system_health(worker: str) -> str:
|
def system_health(worker: str) -> str:
|
||||||
"""Build ``system.<worker>.health``.
|
"""Build ``system.<worker>.health``.
|
||||||
|
|
||||||
|
|||||||
@@ -72,3 +72,17 @@ def test_system_control_builder() -> None:
|
|||||||
def test_system_control_rejects_bad_segments(bad: str) -> None:
|
def test_system_control_rejects_bad_segments(bad: str) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
topics.system_control(bad)
|
topics.system_control(bad)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Identity resolution topics (commit 4 of IDENTITY_RESOLUTION.md) ─────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_identity_builder() -> None:
|
||||||
|
assert topics.identity(topics.IDENTITY_FORMED) == "identity.formed"
|
||||||
|
assert topics.identity(topics.IDENTITY_OBSERVATION_LINKED) == "identity.observation.linked"
|
||||||
|
assert topics.identity(topics.IDENTITY_MERGED) == "identity.merged"
|
||||||
|
|
||||||
|
|
||||||
|
def test_identity_builder_rejects_empty() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
topics.identity("")
|
||||||
|
|||||||
Reference in New Issue
Block a user