Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { EntityStoreEuid } from '@kbn/entity-store/common/euid_helpers';
import type { VulnSeverity } from './types/vulnerabilities';
import type { MisconfigurationEvaluationStatus } from './types/misconfigurations';

Expand Down Expand Up @@ -166,36 +167,32 @@ export const GRAPH_TARGET_ENTITY_FIELDS = [
* These mirror the identity fields from Entity Store definitions.
* Server-side code derives these dynamically via euid.getEuidSourceFields().
*/
export const GRAPH_ACTOR_EUID_SOURCE_FIELDS = [
// user EUID source fields
'user.email',
'user.id',
'user.name',
// host EUID source fields
'host.id',
'host.name',
'host.hostname',
// service EUID source field
'service.name',
// generic entity id
'entity.id',
] as const;
export const getGraphActorEuidSourceFields = (euid: EntityStoreEuid) => {
return {
user: [...euid.getEuidSourceFields('user').identitySourceFields],
host: [...euid.getEuidSourceFields('host').identitySourceFields],
service: [...euid.getEuidSourceFields('service').identitySourceFields],
generic: [...euid.getEuidSourceFields('generic').identitySourceFields],
all: ['event.dataset', 'event.module', 'data_stream.dataset'],
};
};

function toTargetField(field: string): string {
return field.replace('.', '.target.');
}

/**
* Raw source fields used to compute target EUIDs in entity store v2.
* Target-namespace equivalents of GRAPH_ACTOR_EUID_SOURCE_FIELDS.
*/
export const GRAPH_TARGET_EUID_SOURCE_FIELDS = [
// user target EUID source fields
'user.target.email',
'user.target.id',
'user.target.name',
// host target EUID source fields
'host.target.id',
'host.target.name',
'host.target.hostname',
// service target EUID source field
'service.target.name',
// generic target entity id
'entity.target.id',
] as const;
export const getGraphTargetEuidSourceFields = (euid: EntityStoreEuid) => {
return {
user: [...euid.getEuidSourceFields('user').identitySourceFields.map(toTargetField)],
host: [...euid.getEuidSourceFields('host').identitySourceFields.map(toTargetField)],
service: [...euid.getEuidSourceFields('service').identitySourceFields.map(toTargetField)],
generic: [...euid.getEuidSourceFields('generic').identitySourceFields.map(toTargetField)],
all: ['event.dataset', 'event.module', 'data_stream.dataset'],
};
};

export type EuidSourceFields = ReturnType<typeof getGraphActorEuidSourceFields>;
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,21 @@ export const entitySchema = schema.object({
name: schema.maybe(schema.string()),
type: schema.maybe(schema.string()),
sub_type: schema.maybe(schema.string()),
engine_type: schema.maybe(schema.string()),
engine_type: schema.maybe(
schema.oneOf([
schema.literal('host'),
schema.literal('user'),
schema.literal('service'),
schema.literal('generic'),
])
),
host: schema.maybe(
schema.object({
ip: schema.maybe(schema.string()),
})
),
availableInEntityStore: schema.maybe(schema.boolean()),
sourceFields: schema.maybe(
schema.recordOf(
schema.string(),
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
)
),
sourceFields: schema.maybe(schema.object({}, { unknowns: 'allow' })),
});

export const nodeDocumentDataSchema = schema.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
destroyFilterStore,
emitFilterToggle,
emitEntityRelationshipToggle,
emitPinnedEuidToggle,
isFilterActiveForScope,
isEntityRelationshipExpandedForScope,
isPinnedForScope,
} from './filter_store';

// Simple phrase filter builder for tests
Expand Down Expand Up @@ -187,17 +189,67 @@ describe('FilterStore', () => {
});
});

describe('togglePinnedEuid', () => {
it('should pin an entity with action "show"', () => {
const store = new FilterStore(uniqueScopeId());
store.togglePinnedEuid('user:alice@okta', 'show');
expect(store.isPinned('user:alice@okta')).toBe(true);
store.destroy();
});

it('should unpin an entity with action "hide"', () => {
const store = new FilterStore(uniqueScopeId());
store.togglePinnedEuid('user:alice@okta', 'show');
store.togglePinnedEuid('user:alice@okta', 'hide');
expect(store.isPinned('user:alice@okta')).toBe(false);
store.destroy();
});

it('should handle multiple pinned entities independently', () => {
const store = new FilterStore(uniqueScopeId());
store.togglePinnedEuid('user:alice@okta', 'show');
store.togglePinnedEuid('host:server-1', 'show');

expect(store.isPinned('user:alice@okta')).toBe(true);
expect(store.isPinned('host:server-1')).toBe(true);

store.togglePinnedEuid('user:alice@okta', 'hide');
expect(store.isPinned('user:alice@okta')).toBe(false);
expect(store.isPinned('host:server-1')).toBe(true);
store.destroy();
});
});

describe('subscribeToPinnedEuids', () => {
it('should notify subscribers when pinned EUIDs change', () => {
const store = new FilterStore(uniqueScopeId());
const callback = jest.fn();
const subscription = store.subscribeToPinnedEuids(callback);

// BehaviorSubject emits current value on subscribe
expect(callback).toHaveBeenCalledWith(new Set());

store.togglePinnedEuid('user:alice@okta', 'show');
expect(callback).toHaveBeenCalledWith(new Set(['user:alice@okta']));

subscription.unsubscribe();
store.destroy();
});
});

describe('reset', () => {
it('should clear filters and expanded entity IDs', () => {
it('should clear filters, expanded entity IDs, and pinned EUIDs', () => {
const store = new FilterStore(uniqueScopeId());
store.setDataViewId('data-view-1');
store.toggleFilter('user.name', 'alice', 'show');
store.toggleEntityRelationship('entity-1', 'show');
store.togglePinnedEuid('user:alice@okta', 'show');

store.reset();

expect(store.getFilters()).toEqual([]);
expect(store.getExpandedEntityIds().size).toBe(0);
expect(store.getPinnedEuids().size).toBe(0);
store.destroy();
});
});
Expand Down Expand Up @@ -275,6 +327,38 @@ describe('event bus', () => {
destroyFilterStore(idB);
});
});

describe('emitPinnedEuidToggle', () => {
it('should deliver pinned EUID events to the matching store', () => {
const id = uniqueScopeId();
const store = getOrCreateFilterStore(id);

emitPinnedEuidToggle(id, 'user:alice@okta', 'show');

expect(store.isPinned('user:alice@okta')).toBe(true);
destroyFilterStore(id);
});

it('should not deliver pinned EUID events to a different scope', () => {
const idA = uniqueScopeId();
const idB = uniqueScopeId();
getOrCreateFilterStore(idA);
getOrCreateFilterStore(idB);

emitPinnedEuidToggle(idA, 'user:alice@okta', 'show');

expect(isPinnedForScope(idA, 'user:alice@okta')).toBe(true);
expect(isPinnedForScope(idB, 'user:alice@okta')).toBe(false);
destroyFilterStore(idA);
destroyFilterStore(idB);
});

it('should not throw when no store exists for the scopeId', () => {
expect(() => {
emitPinnedEuidToggle('non-existent', 'user:alice@okta', 'show');
}).not.toThrow();
});
});
});

describe('registry functions', () => {
Expand Down Expand Up @@ -376,4 +460,26 @@ describe('scope helper functions', () => {
expect(isEntityRelationshipExpandedForScope('non-existent', 'entity-1')).toBe(false);
});
});

describe('isPinnedForScope', () => {
it('should return true when entity is pinned', () => {
const id = uniqueScopeId();
const store = getOrCreateFilterStore(id);
store.togglePinnedEuid('user:alice@okta', 'show');

expect(isPinnedForScope(id, 'user:alice@okta')).toBe(true);
destroyFilterStore(id);
});

it('should return false when entity is not pinned', () => {
const id = uniqueScopeId();
getOrCreateFilterStore(id);
expect(isPinnedForScope(id, 'user:alice@okta')).toBe(false);
destroyFilterStore(id);
});

it('should return false when store does not exist', () => {
expect(isPinnedForScope('non-existent', 'user:alice@okta')).toBe(false);
});
});
});
Loading
Loading