Skip to content

Pin individual entities in graph#251299

Merged
albertoblaz merged 31 commits intoelastic:mainfrom
albertoblaz:graph-ungroup
Feb 19, 2026
Merged

Pin individual entities in graph#251299
albertoblaz merged 31 commits intoelastic:mainfrom
albertoblaz:graph-ungroup

Conversation

@albertoblaz
Copy link
Contributor

@albertoblaz albertoblaz commented Feb 2, 2026

Summary

Part of the fixes needed to close #239954

PRs:

  1. Reorganize popover files #249622
  2. Pin individual entities in graph #251299 <-- this PR
  3. Mutate Graph filters from flyout actions #250105

API allows for pinning events/alerts by document ID but we don't have a proper way to represent that in the UI so we're skipping for now

Screenshots

Pinning no entities from the group Screenshot 2026-02-02 at 19 18 56
Pinning "admin2@example.com" entity Screenshot 2026-02-02 at 19 22 15
Pinning "admin2@example.com" and "admin-user2@example.com" entities Screenshot 2026-02-02 at 19 23 11
Pinning all entities in actor node Screenshot 2026-02-02 at 19 28 10
pinnedIds field sent in the request payload Screenshot 2026-02-02 at 19 35 56

How to test

  1. Deploy a local env using the following command:
    yarn es snapshot --license trial -E xpack.security.authc.api_key.enabled=true
  2. Run kibana using yarn start --no-base-path
  3. Go to Advanced settings and make sure these toggles are on:
    • securitySolution:enableGraphVisualization
    • securitySolution:enableAssetInventory
  4. Run these commands:
    node scripts/es_archiver load x-pack/solutions/security/test/cloud_security_posture_functional/es_archives/logs_gcp_audit --es-url http://elastic:changeme@localhost:9200 --kibana-url http://elastic:changeme@localhost:5601
    
    node scripts/es_archiver load x-pack/solutions/security/test/cloud_security_posture_functional/es_archives/security_alerts_modified_mappings --es-url http://elastic:changeme@localhost:9200 --kibana-url http://elastic:changeme@localhost:5601
    
    node scripts/es_archiver load x-pack/solutions/security/test/cloud_security_posture_functional/es_archives/entity_store --es-url http://elastic:changeme@localhost:9200 --kibana-url http://elastic:changeme@localhost:5601
    
    node scripts/es_archiver load x-pack/solutions/security/test/cloud_security_posture_functional/es_archives/entity_store_v2 --es-url http://elastic:changeme@localhost:9200 --kibana-url http://elastic:changeme@localhost:5601
  5. Go to Security -> Explore -> Network/Users/Hosts
  6. Open an event's flyout, then expand Graph Visualization. Apply filters to see events from September, 1st 2017 till Now. Add an event.action filter set to "google.iam.admin.v1.CreateUser", then keep adding user.entity.id or event.id filters set to the IDs in the entity node using the OR operator
  7. Individual entities/events/alerts should be pinned correctly in the graph, outside the entity/label groups

Checklist

  • Any text added follows EUI's writing guidelines, uses sentence case text and includes i18n support
  • Documentation was added for features that require explanation or tutorials
  • Unit or functional tests were updated or added to match the most common scenarios
  • If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the docker list
  • This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The release_note:breaking label should be applied in these situations.
  • Flaky Test Runner was used on any tests changed
  • The PR description includes the appropriate Release Notes section, and the correct release_note:* label is applied per the guidelines
  • Review the backport guidelines and apply applicable backport:* labels.

Identify risks

@albertoblaz albertoblaz self-assigned this Feb 2, 2026
@albertoblaz albertoblaz added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting Team:Cloud Security Cloud Security team related v9.4.0 labels Feb 2, 2026
@albertoblaz albertoblaz marked this pull request as ready for review February 3, 2026 09:19
@albertoblaz albertoblaz requested a review from a team as a code owner February 3, 2026 09:19
@elasticmachine
Copy link
Contributor

Pinging @elastic/contextual-security-apps (Team:Cloud Security)

.join(', ')}), targetEntityId,
null
)`
: '| EVAL pinned = TO_STRING(null)';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why TO_STRING?

Comment on lines 296 to 300
return getFilterValues(searchFilters, [
EVENT_ID,
...GRAPH_ACTOR_ENTITY_FIELDS,
...GRAPH_TARGET_ENTITY_FIELDS,
]).map(String);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reads all kinds of entity IDs from applied filters (user.entity.id, service.target.entity.id, ...). Reads also from event.id, though we might read from _id instead. @kfirpeled WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to _id

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a nice workaround, but it is a workaround.
Eventually, we wish to manage state of list of nodes that are pinned

Since we don't have design yet for pinning an event. From your work here, I assume that if we had the ability to pin an event/alert we will add a filter of _id: eventDocId/alertDocId

That filter is really adds anything to the graph, except for pinning the node. So that's why it is a workaround for a missing pinning capability.

Just be aware that we haven't discussed it yet, to add a filter in order to pin a node. While it is true for actors and targets, it is not true for alert and event.

We previously did it for POC purposes, but it wasn't a requirement

@albertoblaz albertoblaz requested a review from a team February 4, 2026 15:11
};
showUnknownTarget: boolean;
nodesLimit?: number;
pinnedIds?: string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@albertoblaz I assume we are also going to use this feature when showing the graph in the entities flyout - in that case how would we know if we should pass the pinnedIds param to the fetch events query or the fetch entities query?
In order to solve it need to we could make the pinnedIds param an array of objects:

Suggested change
pinnedIds?: string[];
pinnedIds?: Array<{ id: string; type: 'event' | 'alert' | 'entity' }>

then we can filter the pinnedIds array for relevant types before calling fetch events or fetch entities and we also have an explicit context - each id is tagged with its type, so there’s no ambiguity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see that necessary unless I'm missing something.

The current implementation is intentionally type-agnostic. If you check the ESQL query, when one of the IDs in pinnedIds matches an existing _id in our data, we return it. And for entities, we try to match each ID with every possible entity ID (user.entity.id, user.target.entity.id, service.entity.id, etc). IDs in our backend are inherently distinct so we can't have collisions across data types.

So I'd say in the entities flyout, it's the client's responsibility to pass entities-only when fetching entities and events-only when fetching events. And the backend will handle the filtering implicitly. Adding type metadata is unnecessary and makes the API schema more verbose without a clear benefit since the current approach already handles the separation correctly and allows for more data types without adding a new domain-related field i.e. pinnedRelationships.

But if I misunderstood you or you still think there's an edge case we're not covering here, please let me know and I'll update the code

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @albertoblaz for now, in case it is not necessary yet. we can leave it as is.
Worst case scenario, we can introduce a breaking change to the API and increase the API version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexreal1314 as we discussed in the sync, if you see it fit. you can change it later on

Copy link
Contributor

@kfirpeled kfirpeled left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rejecting fast: I expect to see API tests

Specifically, how it handles pinning entities. To verify if the event is not being duplicated.

UPDATE:
Looking at the second example you've attached, it seems to me it duplicates nodes like target and dedup the event action.

However, I'd expect to see only the actor being extracted. And the events should be grouped to 6.
Can you elaborate on the product/user experience reasons you chose to implement it this way?

Comment on lines 296 to 300
return getFilterValues(searchFilters, [
EVENT_ID,
...GRAPH_ACTOR_ENTITY_FIELDS,
...GRAPH_TARGET_ENTITY_FIELDS,
]).map(String);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a nice workaround, but it is a workaround.
Eventually, we wish to manage state of list of nodes that are pinned

Since we don't have design yet for pinning an event. From your work here, I assume that if we had the ability to pin an event/alert we will add a filter of _id: eventDocId/alertDocId

That filter is really adds anything to the graph, except for pinning the node. So that's why it is a workaround for a missing pinning capability.

Just be aware that we haven't discussed it yet, to add a filter in order to pin a node. While it is true for actors and targets, it is not true for alert and event.

We previously did it for POC purposes, but it wasn't a requirement


const pinnedParamsStr = pinnedIds.map((_id, idx) => `?pinned_id${idx}`).join(', ');

return `| EVAL pinned = CASE(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

* @param pinnedIds - Array of IDs to check against (document _id, entity IDs)
* @returns ESQL statement string
*/
export const buildPinnedEsql = (pinnedIds?: string[]): string => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not sure if I'd place this function under esql_utils.ts
as it is not a repeated logic, there's no need to place it here and export it.

we should look at esql_utils.ts as a utility function that can be used by others
examples like: building json's string, lookup join with entity store. Are good examples that can be re-used by others.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point was not to pollute the main file and split it in smaller functions. But moved it back to fetch_graph.ts

});
});

describe('Pinned IDs', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

@kibanamachine
Copy link
Contributor

Flaky Test Runner Stats

🎉 All tests passed! - kibana-flaky-test-suite-runner#10725

[✅] x-pack/solutions/security/test/cloud_security_posture_api/config.ts: 25/25 tests passed.

see run history

Copy link
Contributor

@kfirpeled kfirpeled left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, @albertoblaz please check if you can find a good way to pin only a specific entity without its entire sub-graph

};
showUnknownTarget: boolean;
nodesLimit?: number;
pinnedIds?: string[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexreal1314 as we discussed in the sync, if you see it fit. you can change it later on

@albertoblaz
Copy link
Contributor Author

Waiting on #251178 to merge

@albertoblaz albertoblaz enabled auto-merge (squash) February 19, 2026 18:24
@elasticmachine
Copy link
Contributor

elasticmachine commented Feb 19, 2026

💛 Build succeeded, but was flaky

  • Buildkite Build
  • Commit: 5a4f749
  • Kibana Serverless Image: docker.elastic.co/kibana-ci/kibana-serverless:pr-251299-5a4f749174c6

Failed CI Steps

Test Failures

  • [job] [logs] FTR Configs #152 / discover/ccs_compatible discover search CCS cancel classic mode should show warning and results

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
securitySolution 11.1MB 11.1MB +506.0B

History

cc @albertoblaz

@albertoblaz albertoblaz merged commit 06f92ef into elastic:main Feb 19, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport:skip This PR does not require backporting ci:build-serverless-image release_note:skip Skip the PR/issue when compiling release notes Team:Cloud Security Cloud Security team related v9.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update graph visualization through actions in grouped entities/events flyout

5 participants