UPDATED · 24 JUN 2026 · EDIT ON GITHUB
GUIDES · OPERATE

Bring your own context.

A detection is only as smart as the context around the event. Enrichments attach your business knowledge — who owns this host, how critical it is, whether this employee is mid-offboarding — to every event as additive ctx fields, joined on entity_id at ingest. Nothing is re-ingested, and the raw event is never touched.

11 min read Intermediate YAML · API By C. Jian

How enrichment works

Every event already carries threatDefendr's own enrichments under ctx — asset, geo, identity, intel. A custom enrichment adds your fields to that same namespace. Because it is additive and keyed on entity_id, it never rewrites the event and never forces a re-normalization pass; a detection written next month reads it the same way it reads a native field.

  • Additive — your fields land under ctx alongside ours; the original payload under raw is untouched.
  • Joined on identity — the same entity_id that ties events across sources ties your context to them.
  • Evaluated once — enrichment happens on the way in, so detection and search read a precomputed field, not a runtime lookup.

Define an enrichment source

An enrichment source is declarative. Point it at a table or an API, name the join key, and map source columns to ctx fields. The example syncs your CMDB every six hours and decorates host events with owner, tier, and environment.

YAMLenrichments/cmdb-assets.yml
version: 1 id: cmdb-assets kind: lookup_table join: on: entity.target # the host / workload entity_id key: hostname source: type: jdbc refresh: 6h # re-sync every 6 hours query: > SELECT hostname, owner_team, tier, environment FROM cmdb.assets map: ctx.asset.owner: owner_team ctx.asset.tier: tier # crown-jewel | standard | sandbox ctx.asset.env: environment

Real-time vs batch

Stable facts — who owns an asset, what tier it is — belong in a batch table that syncs on an interval. Volatile facts — an employee's HR status, a ticket's state — belong behind a real-time API lookup with a strict latency budget, so a slow upstream never backs up ingest.

YAMLenrichments/hr-status.yml
version: 1 id: hr-status kind: api join: on: entity.actor key: email source: type: https url: https://hr.acme.io/v1/employees/by-email cache_ttl: 30m budget_ms: 40 # fail-open past the latency budget map: ctx.identity.status: status # active | offboarding | terminated
ModeFreshnessBest forCost control
Batch tableUp to the refresh intervalOwner, tier, environment, CMDB factsOne query per interval
Real-time APIPer event (cached)HR status, ticket state, risk scoreCache TTL + latency budget

Use enrichments in detections

Once mapped, a custom field is indistinguishable from a native one. Match on it, branch on it in a playbook, or pivot on it in search. The detection below only fires for unsigned execution on a crown-jewel asset — noise everywhere else, signal where it counts.

YAMLscoping a detection by tier
when: stream: endpoint.process match: ctx.asset.tier: crown-jewel signed: false window: 1m
Enrichments apply retroactively to search. Because the join is on entity_id, a back-test or investigation over historical events resolves ctx.asset.tier from the current table — so you can hunt “every crown-jewel touch last quarter” the day after you define the source.

Mind cardinality & cost

Enrichment is cheap until it isn't. Two things keep it cheap: cache real-time lookups aggressively, and keep the join key low-cardinality. A per-event API call with no cache and a unique key is how you turn a 40 ms budget into a backlog.

  • Cache and set a TTL — match the TTL to how fast the field actually changes; 30 minutes for HR status, seconds for nothing.
  • Fail open — past budget_ms, the event flows un-enriched rather than stalling the stream; the field simply isn't present.
  • Watch cardinality — the data fabric caps distinct ctx values per source; high-cardinality free text belongs in raw, not an enrichment.

Where to go next

← PREV SOAR Playbook Authoring NEXT → Webhook Security