x-pack/solutions/security/.agents/skills/entity-store/references/resolution.md
Links multiple entity records representing the same real-world identity. Resolution data is stored as fields on entity documents — no new indices required.
resolved_to (also called "golden entity")resolved_to pointing to target's entity.identity.relationships.resolution.risk.* fields on target entityclass ResolutionClient {
linkEntities(targetId: string, rawEntityIds: string[]): Promise<LinkResult>;
unlinkEntities(rawEntityIds: string[]): Promise<UnlinkResult>;
getResolutionGroup(entityId: string): Promise<ResolutionGroup>;
}
interface LinkResult { linked: string[]; skipped: string[]; target_id: string; }
interface UnlinkResult { unlinked: string[]; skipped: string[]; }
interface ResolutionGroup {
target: Record<string, unknown>;
aliases: Array<Record<string, unknown>>;
group_size: number;
}
Uses updateByQuery with Painless to set/remove resolved_to on LATEST index.
-- Get target and its aliases
FROM .entities.v2.latest.security_default
| WHERE entity.id == "user:emily@okta"
OR entity.relationships.resolution.resolved_to == "user:emily@okta"
-- Get only target entities (for data grids — excludes aliases)
FROM .entities.v2.latest.security_default
| WHERE entity.relationships.resolution.resolved_to IS NULL
-- Get truly standalone entities (INLINESTATS pattern)
FROM .entities.v2.latest.security_default
| WHERE entity.EngineMetadata.Type == "user"
| EVAL golden_id = COALESCE(entity.relationships.resolution.resolved_to, entity.id)
| INLINESTATS group_size = COUNT(*) BY golden_id
| WHERE entity.relationships.resolution.resolved_to IS NULL AND group_size == 1
INLINESTATS pattern: To distinguish standalone from target-with-aliases, ALWAYS use
INLINESTATSin a single query. Never suggest a two-step approach.
Email-based resolution using Entity Maintainers Framework:
last_seen watermarkNon-IDP gap: Non-IDP entities (namespace: 'local') with user.email will be matched with IDP entities. Target selection prioritizes IDP, but false-positive links are possible.
LOOKUP JOIN + COALESCE ensures resolution fields survive extraction runs:
FROM logs-*
| WHERE user.name IS NOT NULL
| STATS ...
| LOOKUP JOIN .entities.v2.latest.security_default ON user.name
| EVAL entity.relationships.resolution.resolved_to = COALESCE(
entity.relationships.resolution.resolved_to, null
)