Uri WeismanMike Paquette

How a team of entity maintainers monitors, connects and scores entities in Elastic Security

Inside Elastic Security, background jobs called maintainers each own one piece of every user, host and service record, from building entities out of raw logs to resolving identities and scoring risk.

13 min readInternals, Enablement

Open the entity analytics (EA) graph in Elastic Security and you'll see a user wired to the hosts they log in to and the devices they own, along with scattered accounts that turn out to be the same user. While interesting on its own, it provides a critical piece of context during a threat hunting or incident investigation. This post gives an overview of EA fundamentals and opens the hood to see how edges are drawn and accounts are resolved. Why is that important? Well, everything downstream, including baselines, risk, and AI reasoning, is only as good as the entity records underneath it.

Before we go deeper, let's set some context. Many SIEMs treat entities as flat records, a snapshot of what's true right now, rebuilt on demand from raw logs. That works until you need to know how the environment got here, what changed, what was resolved, how a risk score compounded over time. Elastic's Entity Store is architected differently. Every entity is a living record, continuously enriched by background jobs called maintainers, each responsible for one facet of the entity. Maintainers establish relationships, perform identity resolution, and calculate risk scoring. These are all composed onto the record over time, all inspectable, and all correctable. A companion post on entity record quality explains why this matters.

Going one level deeper, this piece explores how records are built and connected in the first place, along with how their risk scores are updated, relationships are tracked, and identities are resolved. The short answer is a single abstraction. Entity store v2, introduced in Elastic v9.4, is built on maintainers. Each maintainer runs on its own clock, continuously updating the record you eventually query. The entity store is the result of a set of maintainers enriching each entity based on the raw or derived signal around it, not an append-only index you simply write entities into.

Let's build that picture from the engine up.

What the entity store does and why it runs on ES|QL

The Entity Store is the layer that turns your telemetry (for example endpoint, identity provider, and cloud logs), identity and asset inventories, and threat detections into one queryable profile per user, host, and service. This means analysts can pivot on information and insights gained from an enriched entity record, instead of reconstructing it from raw logs every time.

Entity store Version 1 did that with Elasticsearch transforms.

Version 2 rebuilds the engine on Elasticsearch Query Language (ES|QL). Each entity extraction cycle runs a query that filters the relevant logs and aggregates them. In the next crucial step, the cycle performs a LOOKUP JOIN back against the existing entity index to carry forward previously observed entity attributes and behaviors.

FROM logs-*
  | WHERE ...                                          // logs for this entity type + window
  | STATS ... BY entity.id                             // collapse many events into one record
  | LOOKUP JOIN .entities.v2.latest.security_default-00001
      ON entity.id                                     // field retention against the store itself
  | EVAL ...                                            // keep-latest / keep-first retention

ES|QL provides query flexibility and coverage for the fields and field types that the schema needs. It also provides a pipeline that you can reason about and extend, with retention and merge logic expressed directly, instead of through the limited flexibility and operational choreography needed with transforms.

How an entity record is built, from raw signal to resolved identity

Here's the part that's usually glossed over. Let's take a look at the entity extraction and building process by following one host from its raw event to its finished record.

  • Step 1: The raw signal ingested to Elasticsearch. Your endpoint detection and response (EDR) agent and identity provider write events into data streams that are matched by a logs-* index pattern, as do your cloud integrations. There isn’t anything entity-shaped yet, just events with fields like user.name, host.name, and event.category.

  • Step 2: Extraction collapses events into a record. During its run cycle, the ES|QL query above filters the logs relevant to an entity definition, and then STATS ... BY entity.id collapses potentially thousands of events into one entity store row/record per entity, keeping the first-seen and latest entity lifecycle values that matter. The LOOKUP JOIN merges that fresh row with the entity's existing record, so nothing observed outside the window is lost. The output is a single, denormalized document in the latest index: the entity's base identity and lifecycle, in addition to its attributes.

  • Step 3: Identity is keyed deliberately. Which entity a row belongs to is the highest-stakes decision in the whole system. Key it wrong and you can blend several distinct users into one entity, or split one user across many. So the store doesn't guess. It derives a deterministic identifier, the entity unique ID (EUID), from the fields that actually identify the entity.

  • Step 4. Maintainers compose the rest. The entity record is there, but it doesn’t yet know who it talks to (relationships) or what behaviors it exhibits. It also doesn’t yet know which various account entities are used by the same user or how risky the entity is. Those facets are layered onto the entity record by maintainers, and that's the heart of Entity Analytics.

The key to building entity relationships - the deterministic entity ID**

The Entity Store currently supports three primary, schema-backed entity types: users, hosts, and services. Alongside these sits a fourth, polymorphic category known as generic entities.
The generic entity type isn’t constrained to a fixed schema, but rather provides an extensibility layer for the entity store platform. For example, by leveraging cloud and orchestrator fields, it represents resources like EC2 instances, S3 buckets, and Kubernetes clusters as first-class entities today. This flexibility allows the entity store to encompass a broad spectrum of assets without the architectural overhead of defining a new entity type for every resource class.

Everything the entity store does, including merging today's events onto the current entity record and linking two accounts into one resolved user, hinges on one field: entity.id, the EUID.

The EUID is a deterministic, human-readable string (a type prefix and then the fields that actually identify the entity, joined with @), rather than a random universally unique identifier (UUID) or a hash. Note the addition of a user entity namespace at the end of the user EUID. We’ll explain the role that plays below.

user:jane.doe@example.com@okta
host:9f86d081-1e0c-4b3f-8a2d-2c1e7bed425e
service:api-gateway

How is the EUID derived? For each entity, the store walks an ordered list of candidate identity fields and takes the first complete one. If a field a candidate needs is missing, that candidate is skipped and the next is tried, so a partial observation never produces a malformed ID.

Why do we need a namespace for user entities? While host and service entities rely on sufficiently unique keys like host.id or service.name, a user's identity, for example an email address, can be observed in many different log sources, but is authoritative only within its issuing domain. The namespace provides the necessary disambiguation, resolving the specific collision challenges unique to user identity records.

Entity typeID fields (priority order)Example EUIDNamespace
Hosthost.id, then host.name, then host.hostnamehost:9f86d081-1e0c-4b3f-8a2d-2c1e7bed425e(none)
Serviceservice.nameservice:api-gateway(none)
User (identity provider)user.email, then user.id, then user.name@domain, then user.nameuser:jane.doe@elastic.com@oktaProvider name: okta, entra_id, microsoft_365, active_directory
User (local/endpoint)user.name scoped to host.iduser:jdoe@9f86d081-1e0c-4b3f-8a2d-2c1e7bed425e@locallocal
  • Host: The first present of host.id, host.name, host.hostname, giving host:<value>.
  • Service: service.name, giving service:<name>.
  • User: This is the interesting one, because a user's identity depends on where their activity was observed.

Users: Identity provider versus the local host. A user's EUID always ends in a namespace, the last @-delimited segment, and that segment is what stops two accounts that merely look alike from colliding.

When a user comes from an identity provider, the namespace is that provider (okta, entra_id, microsoft_365, active_directory) and the store keys on the most authoritative identifier it has: email, then user id, then name@domain, and then name.

user:jane.doe@elastic.com@okta

The same user's Entra ID account becomes …@entra_id, a different EUID, on purpose. They’re two authoritative accounts until resolution ties them together. We are going to see how resolution achieves this later in the blog.

When a user is seen only through endpoint or host telemetry, with no authoritative directory account behind them, the store scopes them to the machine and suffix the ID as local:

user:jdoe@9f86d081-1e0c-4b3f-8a2d-2c1e7bed425e@local

The middle segment is the host's durable identifier (host.id) and not its renamable hostname, so jdoe on a laptop stays distinct from jdoe on a shared bastion host, and the identity survives a machine being renamed or reimaged. The local namespace is an explicit signal: This is activity on this box, not a verified global identity. (Note the host in the example above and this local user share the same host.id. That's how the Entity Store knows they belong together.)

What makes an entity source authoritative and how to construct one

The EUID logic leans on a word that deserves a precise definition: authoritative. When an identity-provider-backed user earns a real namespace (okta or entra_id, among others) and high-confidence treatment, it's because the incoming events cleared a specific bar. Here's the bar and how to clear it with your own integrations.

What qualifies as authoritative identity data. The store treats an event as an authoritative identity signal when either of these is true:

Classification pathRequired ECS fieldsConfidenceResulting namespace
Asset inventoryevent.kind: assetHighProvider name (okta, entra_id, etc.)
Endpoint-observed (neither bar cleared)user.name + host.id present, but no authoritative signalMediumlocal (scoped to host)
UnclassifiableNone of the aboveN/ANo entity created
  • The event is an asset / inventory document with event.kind: asset. This is how Elastic's identity integrations (Okta, Entra ID, Active Directory, and the cloud asset sources) publish their user and account inventories.

Clear either bar and the user becomes a first-class, identity-provider-backed entity at high confidence, and the preferred canonical record when identities are later resolved. Miss both, but carry a user.name and a host.id, and the user is instead scoped to that host in the local namespace at medium confidence. If both of these conditions are false and no host exists to associate the event with, no entity is created at all. That last case is deliberate restraint, not a gap; the store would rather create nothing than manufacture a noisy identity from an ambiguous event.

Two filters apply before any of this runs: The event's event.outcome must not be failure (a failed login isn’t evidence that an account exists), and it must carry at least one of user.email, user.id, or user.name.

Which namespace you get. Once an event qualifies, the namespace is derived from its source, the first non-empty of event.module or the leading segment of data_stream.dataset.

An authoritative event from an unrecognized source still creates a real entity. It just lands in the unknown namespace and you lose provider-level disambiguation (identical usernames from two namespaces can collide) and clean grouping. So, for a custom integration, the goal isn't only to qualify as authoritative, it's to be recognized.

How to accommodate a custom integration or pipeline. If you ingest identity data through a custom Fleet integration, a Logstash pipeline, or an Elasticsearch ingest pipeline, set these Elastic Common Schema (ECS) fields so the store classifies your entities the way you intend:

  1. Mark the shape. For an account inventory, such as a local directory service, or configuration management database (CMDB), set event.kind: asset.
  2. Provide an identity. Populate at least one of user.email (this is best, since it's the top priority), user.id, or user.name, plus user.domain where you have it.
  3. Name your source. Set event.module (or the leading segment of data_stream.dataset) to a value the store maps. If you're feeding one of the known providers, reuse its naming so you inherit the right namespace. If your source is genuinely new, expect unknown until a mapping is added.
  4. Don't let real identities look local. The local classification triggers when an identity event carries both user.name and host.id but doesn't clear the authoritative bar. If you're publishing directory data, don't attach a host.id to it. That field is the signal that says "endpoint-observed, scope it to this box." Reserve it for genuinely host-local activity.
  5. Skip the noise. Don't emit failure outcomes as identity evidence. Note, too, that the store already excludes common shared and service account names (root, jenkins, deploy, postgres, admin, and similar) from the local namespace, so they never become per-host user entities.

Get these right, and your custom source behaves exactly like a built-in one: Authoritative users resolve and score at full fidelity, and endpoint-observed users stay correctly host-scoped. Plus, nothing downstream has to special-case where the data came from.

How maintainers add entity resolution, relationships, and risk scoring to entities

As discussed at the start of this post, the entity store is updated by dedicated background tasks called maintainers. Each of these has a specific job: building relationships between entities and resolving identities, along with updating risk scores.

The framework gives every maintainer its lifecycle, scheduling, and health reporting for free, making the entity store extensible by design. Any new entity enrichment capability ships as a new maintainer on the same rails, without re-architecting the entity store or breaking backward compatibility. Several maintainers already run in the current version, and recent additions, such as entity relationships derived from observed entity behaviors, have been added exactly this way. Even entity risk scoring, one of the most impactful risk-centric capabilities in the product, is implemented as just another maintainer composing one more facet of the entity record.

Maintainers discover and store entity relationships

Let’s take a look at one of the relationship maintainers. Once a day, the accesses_frequently / accesses_infrequently maintainer runs an ES|QL query over relevant telemetry and, for each actor→target pair, counts successful accesses over a 30-day window. If the count is:

  • greater than or equal to an Elastic-defined threshold, the relationship becomes accesses_frequently.
  • less than the threshold, the relationship becomes accesses_infrequently.

Today, it reads from Elastic Defend (endpoint logins), AWS CloudTrail (StartSession / SendSSHPublicKey), system auth (SSH logins), and system security (Windows events 4624/4648). A sibling, communicates_with, builds communication links from Elastic Defend, system auth, system security, Jamf Pro, and AWS CloudTrail.

The relationships are written straight onto the entity record as arrays of target IDs:

// a user entity in .entities.v2.latest.security_default-00001
"entity": {
  "id": "user-abc…",
  "relationships": {
    "accesses_frequently": { "ids": ["host-def…", "host-ghi…"] },
    "communicates_with":   { "ids": ["service-jkl…"] }
  }
}

A maintainer performs entity resolution

The resolution maintainer links fragmented accounts, the Okta jdoe, the Entra ID jdoe, and the on-prem Active Directory jdoe, into one resolved user. The risk maintainer then scores the resolved user, so the risky service token and the benign laptop login that both belong to John Doe are scored as John Doe: one record, not three that an analyst has to reconcile in their head.

Resolution happens two ways. Most of it is automatic: a background task runs every five minutes and links user entities that share the same user.email, so accounts converge on their own as the data arrives. When you need to step in, you can link or unlink manually from the entity flyout in the UI, or call the API directly.

POST /api/security/entity_store/resolution/link
{
  "target_id": "user:jane.doe@elastic.com@okta",
  "entity_ids": ["user:jane.doe@elastic.com@entra_id"]
}
POST /api/security/entity_store/resolution/unlink
{
  "entity_ids": ["user:jane.doe@elastic.com@entra_id"]
}

The near-term plan is to expand the out-of-the-box matching logic so more identities link automatically without anyone manually creating the links.

A maintainer calculates and updates entity risk scores

The risk score maintainer runs hourly by default. On each run, it queries the detection alerts over a configurable rolling time window (past 30 days by default) and for each entity those alerts touch, aggregates the alert risk scores into a single normalized entity risk score and risk level.

Entity risk scoring also extends across resolved identities. First the risk scoring maintainer calculates an entity risk score for each user entity (one per account) based on any detection alerts related to that account. Next it calculates a distinct entity risk score for the resolved user across all of their linked accounts. For example, a malicious service token alert on one account, and an excessive laptop login rate alert from a different account, where both accounts belong to John Doe, roll up into a single user entity risk score for the resolved John Doe. That holistic view is possible because the resolution maintainer ran first, then the risk scoring maintainer reads the graph (set of linked accounts) that the resolution maintainer produced, and is able to assign a risk score to the user, not only the constituent accounts If an analyst or AI agent decides to investigate the provenance of a resolved user risk score, the answer is easily obtained.

Every entity store decision is inspectable and correctable

The store's engine and its relationship maintainers are built on ES|QL, the same query language that Elastic users already employ, so what a maintainer reads,computes, and writes is inspectable, rather than proprietary magic. Every entity lives as a document in an Elasticsearch index, so its full state is there to read, including the relationships that were drawn and the accounts that were resolved into one user, plus the signals behind a risk score.

Entity data is wrong sometimes; that's the reality of identity data. Because the state is represented as Elasticsearch documents, you can correct them through APIs. When resolution gets it wrong, two people merged or one user split across two records, you can link or unlink identities right through the UI, and the risk score maintainer rescores the corrected resolved user on its next run. When you need to pull an entity into scope or set its criticality, watchlists let you say so directly. With Elastic Agent Builder, an AI agent can walk you through an entity's data and make those corrections for you, without leaving the Elastic UI.

What entity maintainers mean for security analysts

With entity resolution, relationship mapping, and risk scoring running as automated maintainers, human analysts and AI agents gain efficient access to the entity context they need to perform investigations. The entity store is extensible, so entities continue gaining relationships and enrichments as new maintainers ship or new data sources are ingested, with no migration or re-architecture. Nothing is hidden, so every risk score traces back to the alerts and other factors (for example, asset criticality, watchlist membership) that produced it, and a finding becomes something you can explain using the Agent Builder chat with the entity analytics tools and skills rather than a black box you have to trust.

In addition, wrong data doesn't have to stay wrong; a bad resolution link takes one API call or Agent Builder action to fix. What you're left with is a single queryable record per entity, host, and service that can evolve over time.

What’s coming next in the Entity Analytics Roadmap

The science of entity analytics is ongoing. One area we’re digging into is integrating non-human identities (NHI) as entities. AI agents, service accounts, and agentic workloads are among the fastest-growing concerns across today’s attack surface. While this research continues, the core objectives involve finding an optimal way to link an AI agent's sessions to existing entities by tracking its utilized Service accounts or connected devices and services, as well as assessing the risk of AI agent behavior

Also under the hood, we're reworking log extraction to split the work by data source confidence, so authoritative identities and lower-confidence enrichment are handled separately.
Additionally, we’re exploring enhancing the agentic UEBA capabilities to reason about an entity, connect the dots, and suggest relevant leads for the analyst to explore.

Entity risk scoring is getting continued attention too. Currently, a detection alert carries one static alert risk score that affects the entity risk score of every entity it involves, so the actor and the target can come out looking equally risky. The direction is towards a dynamic, entity-centric risk score that gives each entity its own contribution based on the role it played in the observed interaction, and its own context.

For practitioners with their hands on the Elastic Security UI, the entity analytics overview experience is due for a refresh, with risky entities shown more simply and clearer actions to investigate them.

Entity analytics is available in Elastic Security. Learn more about entity analytics.

Share this article