Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
30a210e
add entities enrichment using LOOKUP JOIN support with fallback to EN…
alexreal1314 Jan 5, 2026
5772f3e
stabalize graph api and ftr tests
alexreal1314 Jan 5, 2026
07a88b0
remove unsued consts from test files
alexreal1314 Jan 6, 2026
e51ce82
refactor graph api and ftr tests entity store init process
alexreal1314 Jan 6, 2026
6a42c44
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Jan 6, 2026
60adfab
fix types
alexreal1314 Jan 6, 2026
3833d5e
Merge branch 'main' into 232226-lookup-entity-enrichment
alexreal1314 Jan 6, 2026
a13e49a
fix graph entity store test cases flakines
alexreal1314 Jan 6, 2026
f35926e
add waitForEntityStoreReady to wait before executing enrich policy
alexreal1314 Jan 7, 2026
8d330e0
investigate flaky tests
alexreal1314 Jan 7, 2026
f17534a
investigate flaky tests
alexreal1314 Jan 7, 2026
5ce1210
add init of default data view in graph ftr tests
alexreal1314 Jan 7, 2026
9909ec9
fix flaky tests
alexreal1314 Jan 7, 2026
fbfbc5c
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Jan 7, 2026
66be86a
refactor entity store init
alexreal1314 Jan 7, 2026
acd53af
add init entity store with retry
alexreal1314 Jan 7, 2026
ded7ba8
alight graph api integration test entity store init with ftr tests
alexreal1314 Jan 7, 2026
3edc11c
uncomment all ftr tests
alexreal1314 Jan 7, 2026
685748f
fix fetch graph unit test
alexreal1314 Jan 7, 2026
d1af562
convert graph ftr tests to run sequentially and load entity store onl…
alexreal1314 Jan 7, 2026
ec33938
make graph api integrations tests to run entity store tests sequentia…
alexreal1314 Jan 8, 2026
f541fc8
fix missing import
alexreal1314 Jan 8, 2026
62f75fa
Merge branch 'main' into 232226-lookup-entity-enrichment
alexreal1314 Jan 11, 2026
3028fa3
Merge branch 'main' into 232226-lookup-entity-enrichment
alexreal1314 Jan 12, 2026
6f6e21b
update esql query to handle null value in entity store documents
alexreal1314 Jan 12, 2026
3e12aae
move graph api entity store tests to a custom space
alexreal1314 Jan 13, 2026
d3d72cb
fix entity flyout graph ftr tests and test both enrich and lookup flows
alexreal1314 Jan 13, 2026
23093da
Merge branch 'main' into 232226-lookup-entity-enrichment
alexreal1314 Jan 13, 2026
03fed8e
extract LOOKUP JOIN and ENRICH ESQL builders into utility functions
alexreal1314 Jan 15, 2026
77c7d0a
Merge branch 'main' into 232226-lookup-entity-enrichment
alexreal1314 Jan 15, 2026
b7e0285
Merge branch 'main' into 232226-lookup-entity-enrichment
alexreal1314 Jan 18, 2026
a443c38
rename entity_flyout ftr test to entity_preview_flyout
alexreal1314 Jan 19, 2026
8c07a8c
handle partial entity data in ESQL JSON construction
alexreal1314 Jan 19, 2026
b86620b
remove empty line
alexreal1314 Jan 19, 2026
44310e6
Merge branch 'main' into 232226-lookup-entity-enrichment
alexreal1314 Jan 20, 2026
83a2e71
simplify ESQL entity JSON construction and add Storage icon mapping
alexreal1314 Jan 21, 2026
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 @@ -111,6 +111,12 @@ export const GENERIC_ENTITY_INDEX_ENRICH_POLICY =

export const CLOUD_SECURITY_PLUGIN_VERSION = '1.9.0';

/**
* Entity store latest index pattern for LOOKUP JOIN queries.
* The <space> placeholder should be replaced with the actual space ID.
*/
export const ENTITIES_LATEST_INDEX = '.entities.v2.latest.security_generic_<space>';
Copy link
Copy Markdown
Contributor Author

@alexreal1314 alexreal1314 Jan 7, 2026

Choose a reason for hiding this comment

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

waiting for confirmation whether we are moving to a new v2 index or migrating existing v1.
cc @romulets @uri-weisman

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Gonna keep it under v2 - atm migration of v1 to lookup mode is not planned


/**
* ECS entity actor fields used for graph visualization.
* NOTE: The order has meaning - it represents the fallback mechanism for detecting the actor field.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
buildMisconfigurationEntityFlyoutPreviewQuery,
buildVulnerabilityEntityFlyoutPreviewQuery,
getEnrichPolicyId,
getEntitiesLatestIndexName,
} from './helpers';

const fallbackMessage = 'thisIsAFallBackMessage';
Expand Down Expand Up @@ -495,4 +496,36 @@ describe('test helper methods', () => {
expect(policyId2).toEqual('entity_store_field_retention_generic_space2_v1.0.0');
});
});

describe('getEntitiesLatestIndexName', () => {
it('should return the index name with the provided spaceId', () => {
const indexName = getEntitiesLatestIndexName('default');
const expected = '.entities.v2.latest.security_generic_default';
expect(indexName).toEqual(expected);
});

it('should return a index name with a custom space', () => {
const space = 'test-space';
const indexName = getEntitiesLatestIndexName(space);
const expected = '.entities.v2.latest.security_generic_test-space';
expect(indexName).toEqual(expected);
});

it('should handle special characters in space IDs', () => {
const space = 'special-chars_123';
const indexName = getEntitiesLatestIndexName(space);
const expected = '.entities.v2.latest.security_generic_special-chars_123';
expect(indexName).toEqual(expected);
});

it('should produce a different index name for each space', () => {
const space1 = 'space1';
const space2 = 'space2';
const indexName1 = getEntitiesLatestIndexName(space1);
const indexName2 = getEntitiesLatestIndexName(space2);
expect(indexName1).not.toEqual(indexName2);
expect(indexName1).toEqual('.entities.v2.latest.security_generic_space1');
expect(indexName2).toEqual('.entities.v2.latest.security_generic_space2');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types

import { i18n } from '@kbn/i18n';
import type { CspBenchmarkRulesStates } from '../schema/rules/latest';
import { GENERIC_ENTITY_INDEX_ENRICH_POLICY } from '../constants';
import { GENERIC_ENTITY_INDEX_ENRICH_POLICY, ENTITIES_LATEST_INDEX } from '../constants';

interface BuildEntityAlertsQueryParams {
field: string;
Expand Down Expand Up @@ -201,3 +201,10 @@ export const buildEntityAlertsQuery = ({
export const getEnrichPolicyId = (space: string = 'default'): string => {
return GENERIC_ENTITY_INDEX_ENRICH_POLICY.replace('<space>', space);
};

/**
* Gets the entities latest index name for a specific space
*/
export const getEntitiesLatestIndexName = (spaceId: string = 'default'): string => {
return ENTITIES_LATEST_INDEX.replace('<space>', spaceId);
};
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const buildEntityTypeMappings = (): EntityTypeMappings => {
'Snapshot',
'Volume',
'Volume Claim',
'Storage',
'Storage Bucket',
'Backup Service',
'Managed Certificate',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
import { fetchGraph } from './fetch_graph';
import type { Logger } from '@kbn/core/server';
import type { OriginEventId, EsQuery } from './types';
import { getEnrichPolicyId } from '@kbn/cloud-security-posture-common/utils/helpers';
import {
getEnrichPolicyId,
getEntitiesLatestIndexName,
} from '@kbn/cloud-security-posture-common/utils/helpers';

describe('fetchGraph', () => {
const esClient = elasticsearchServiceMock.createScopedClusterClient();
Expand Down Expand Up @@ -39,6 +42,7 @@ describe('fetchGraph', () => {

logger = {
trace: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
} as unknown as Logger;
Expand Down Expand Up @@ -138,7 +142,161 @@ describe('fetchGraph', () => {
expect(result).toEqual([{ id: 'dummy' }]);
});

describe('LOOKUP JOIN integration', () => {
it('should include LOOKUP JOIN clause when entities index is in lookup mode', async () => {
const indexName = getEntitiesLatestIndexName('default');

// Mock the indices.getSettings to return lookup mode
(esClient.asInternalUser.indices as jest.Mocked<any>).getSettings = jest
.fn()
.mockResolvedValueOnce({
[indexName]: {
settings: {
index: {
mode: 'lookup',
},
},
},
});

const validIndexPatterns = ['valid_index'];
const params = {
esClient,
logger,
start: 0,
end: 1000,
originEventIds: [] as OriginEventId[],
showUnknownTarget: false,
indexPatterns: validIndexPatterns,
spaceId: 'default',
esQuery: undefined as EsQuery | undefined,
};

const result = await fetchGraph(params);

expect(esClient.asCurrentUser.helpers.esql).toBeCalledTimes(1);
const esqlCallArgs = esClient.asCurrentUser.helpers.esql.mock.calls[0];
const query = esqlCallArgs[0].query;

// Verify LOOKUP JOIN is used (preferred over ENRICH)
expect(query).toContain(`LOOKUP JOIN ${indexName} ON entity.id`);

// Verify LOOKUP JOIN populates expected fields
expect(query).toContain('actorEntityName');
expect(query).toContain('actorEntityType');
expect(query).toContain('actorEntitySubType');
expect(query).toContain('targetEntityName');
expect(query).toContain('targetEntityType');
expect(query).toContain('targetEntitySubType');

// Verify ENRICH is NOT used when LOOKUP JOIN is available
expect(query).not.toContain('ENRICH');

expect(result).toEqual([{ id: 'dummy' }]);
});

it('should not include LOOKUP JOIN clause when entities index is not in lookup mode', async () => {
const indexName = getEntitiesLatestIndexName('default');

// Mock the indices.getSettings to return standard mode (not lookup)
(esClient.asInternalUser.indices as jest.Mocked<any>).getSettings = jest
.fn()
.mockResolvedValueOnce({
[indexName]: {
settings: {
index: {
mode: 'standard',
},
},
},
});

// Also mock enrich policy to not exist
(esClient.asInternalUser.enrich as jest.Mocked<any>).getPolicy = jest
.fn()
.mockResolvedValueOnce({
policies: [],
});

const validIndexPatterns = ['valid_index'];
const params = {
esClient,
logger,
start: 0,
end: 1000,
originEventIds: [] as OriginEventId[],
showUnknownTarget: false,
indexPatterns: validIndexPatterns,
spaceId: 'default',
esQuery: undefined as EsQuery | undefined,
};

const result = await fetchGraph(params);

expect(esClient.asCurrentUser.helpers.esql).toBeCalledTimes(1);
const esqlCallArgs = esClient.asCurrentUser.helpers.esql.mock.calls[0];
const query = esqlCallArgs[0].query;

// Verify LOOKUP JOIN is NOT used
expect(query).not.toContain('LOOKUP JOIN');

// Verify ENRICH is also NOT used (no policy exists)
expect(query).not.toContain('ENRICH');

// Verify fallback EVALs are present for null values
expect(query).toMatch(/EVAL\s+actorEntityName\s*=\s*TO_STRING\(null\)/);
expect(query).toMatch(/EVAL\s+targetEntityName\s*=\s*TO_STRING\(null\)/);

expect(result).toEqual([{ id: 'dummy' }]);
});

it('should not include LOOKUP JOIN when entities index does not exist', async () => {
// Mock the indices.getSettings to throw 404 (index not found)
(esClient.asInternalUser.indices as jest.Mocked<any>).getSettings = jest
.fn()
.mockRejectedValueOnce({ statusCode: 404 });

// Also mock enrich policy to not exist
(esClient.asInternalUser.enrich as jest.Mocked<any>).getPolicy = jest
.fn()
.mockResolvedValueOnce({
policies: [],
});

const validIndexPatterns = ['valid_index'];
const params = {
esClient,
logger,
start: 0,
end: 1000,
originEventIds: [] as OriginEventId[],
showUnknownTarget: false,
indexPatterns: validIndexPatterns,
spaceId: 'default',
esQuery: undefined as EsQuery | undefined,
};

const result = await fetchGraph(params);

expect(esClient.asCurrentUser.helpers.esql).toBeCalledTimes(1);
const esqlCallArgs = esClient.asCurrentUser.helpers.esql.mock.calls[0];
const query = esqlCallArgs[0].query;

// Verify LOOKUP JOIN is NOT used
expect(query).not.toContain('LOOKUP JOIN');

expect(result).toEqual([{ id: 'dummy' }]);
});
});

describe('ENRICH policy integration', () => {
beforeEach(() => {
// Default: no lookup index available (falls back to ENRICH check)
(esClient.asInternalUser.indices as jest.Mocked<any>).getSettings = jest
.fn()
.mockRejectedValue({ statusCode: 404 });
});

it('should include ENRICH clause when policy exists', async () => {
// Mock the enrich.getPolicy method to return a policy that exists
(esClient.asInternalUser.enrich as jest.Mocked<any>).getPolicy = jest
Expand Down
Loading
Loading