Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 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>';

@alexreal1314 alexreal1314 Jan 7, 2026

Copy link
Copy Markdown
Contributor Author

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 @@ -39,6 +39,7 @@ describe('fetchGraph', () => {

logger = {
trace: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
error: jest.fn(),
} as unknown as Logger;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,27 @@ import {
DOCUMENT_TYPE_ENTITY,
INDEX_PATTERN_REGEX,
} from '@kbn/cloud-security-posture-common/schema/graph/v1';
import { getEnrichPolicyId } from '@kbn/cloud-security-posture-common/utils/helpers';
import {
getEnrichPolicyId,
getEntitiesLatestIndexName,
} from '@kbn/cloud-security-posture-common/utils/helpers';
import {
GRAPH_ACTOR_ENTITY_FIELDS,
GRAPH_TARGET_ENTITY_FIELDS,
} from '@kbn/cloud-security-posture-common/constants';
import { generateFieldHintCases, concatPropIfExists } from './utils';
import {
generateFieldHintCases,
formatJsonProperty,
buildLookupJoinEsql,
buildEnrichPolicyEsql,
} from './utils';
import type { EsQuery, GraphEdge, OriginEventId } from './types';

interface BuildEsqlQueryParams {
indexPatterns: string[];
originEventIds: OriginEventId[];
originAlertIds: OriginEventId[];
isLookupIndexAvailable: boolean;
isEnrichPolicyExists: boolean;
spaceId: string;
alertsMappingsIncluded: boolean;
Expand Down Expand Up @@ -65,8 +74,12 @@ export const fetchGraph = async ({
}
});

const isEnrichPolicyExists = await checkEnrichPolicyExists(esClient, logger, spaceId);

// Check if the entities lookup index exists and is in lookup mode (preferred)
// If not, fall back to checking if the enrich policy exists (deprecated)
const isLookupIndexAvailable = await checkIfEntitiesIndexLookupMode(esClient, logger, spaceId);
const isEnrichPolicyExists = isLookupIndexAvailable
? false
: await checkEnrichPolicyExists(esClient, logger, spaceId);
const SECURITY_ALERTS_PARTIAL_IDENTIFIER = '.alerts-security.alerts-';
const alertsMappingsIncluded = indexPatterns.some((indexPattern) =>
indexPattern.includes(SECURITY_ALERTS_PARTIAL_IDENTIFIER)
Expand All @@ -76,6 +89,7 @@ export const fetchGraph = async ({
indexPatterns,
originEventIds,
originAlertIds,
isLookupIndexAvailable,
isEnrichPolicyExists,
spaceId,
alertsMappingsIncluded,
Expand Down Expand Up @@ -159,6 +173,11 @@ const checkEnrichPolicyExists = async (
name: getEnrichPolicyId(spaceId),
});

logger.debug(
`Enrich policy check for [${getEnrichPolicyId(spaceId)}]: found ${
policies?.length
} policies, policies: ${JSON.stringify(policies?.map((p) => p.config.match?.name))}`
);
return policies.some((policy) => policy.config.match?.name === getEnrichPolicyId(spaceId));
} catch (error) {
logger.error(`Error fetching enrich policy ${error.message}`);
Expand All @@ -167,10 +186,97 @@ const checkEnrichPolicyExists = async (
}
};

/**
* Checks if the entities latest index exists and is configured in lookup mode.
* This is the preferred method for entity enrichment (replaces deprecated ENRICH policy).
*/
const checkIfEntitiesIndexLookupMode = async (
esClient: IScopedClusterClient,
logger: Logger,
spaceId: string
): Promise<boolean> => {
const indexName = getEntitiesLatestIndexName(spaceId);
try {
const response = await esClient.asInternalUser.indices.getSettings({
index: indexName,
});
const indexSettings = response[indexName];
if (!indexSettings) {
logger.debug(`Entities index ${indexName} not found`);
return false;
}

// Check if index is in lookup mode
const mode = indexSettings.settings?.index?.mode;
const isLookupMode = mode === 'lookup';

if (!isLookupMode) {
logger.debug(`Entities index ${indexName} exists but is not in lookup mode (mode: ${mode})`);
}

return isLookupMode;
} catch (error) {
if (error.statusCode === 404) {
logger.debug(`Entities index ${indexName} does not exist`);
return false;
}
logger.error(`Error checking entities index ${indexName}: ${error.message}`);
return false;
}
};

/**
* Generates ESQL statements for building entity fields with enrichment data.
* This is used when entity store enrichment is available (via LOOKUP JOIN or ENRICH).
*/
const buildEnrichedEntityFieldsEsql = (): string => {

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.

I'd move this function to esql.utils.ts too since buildEnrichPolicyEsql and buildLookupJoinEsql live there already

return `// Construct actor and target entities data
// Build entity field conditionally - only include fields that have values
| EVAL actorEntityField = CASE(
actorEntityName IS NOT NULL OR actorEntityType IS NOT NULL OR actorEntitySubType IS NOT NULL,
CONCAT(",\\"entity\\":", "{",
${formatJsonProperty('name', 'actorEntityName', false)},
${formatJsonProperty('type', 'actorEntityType')},
${formatJsonProperty('sub_type', 'actorEntitySubType')},
CASE(
actorHostIp IS NOT NULL,
CONCAT(",\\"host\\":", "{", "\\"ip\\":\\"", TO_STRING(actorHostIp), "\\"", "}"),
""
),
",\\"availableInEntityStore\\":true",
",\\"ecsParentField\\":\\"", actorEntityFieldHint, "\\"",
"}"),
CONCAT(",\\"entity\\":", "{",
"\\"availableInEntityStore\\":false",
",\\"ecsParentField\\":\\"", actorEntityFieldHint, "\\"",
"}")
)
| EVAL targetEntityField = CASE(
targetEntityName IS NOT NULL OR targetEntityType IS NOT NULL OR targetEntitySubType IS NOT NULL,
CONCAT(",\\"entity\\":", "{",
${formatJsonProperty('name', 'targetEntityName', false)},
${formatJsonProperty('type', 'targetEntityType')},
${formatJsonProperty('sub_type', 'targetEntitySubType')},
CASE(
targetHostIp IS NOT NULL,
CONCAT(",\\"host\\":", "{", "\\"ip\\":\\"", TO_STRING(targetHostIp), "\\"", "}"),
""
),
",\\"availableInEntityStore\\":true",
",\\"ecsParentField\\":\\"", targetEntityFieldHint, "\\"",
"}"),
CONCAT(",\\"entity\\":", "{",
"\\"availableInEntityStore\\":false",
",\\"ecsParentField\\":\\"", targetEntityFieldHint, "\\"",
"}")
)`;
};

const buildEsqlQuery = ({
indexPatterns,
originEventIds,
originAlertIds,
isLookupIndexAvailable,
isEnrichPolicyExists,
spaceId,
alertsMappingsIncluded,
Expand Down Expand Up @@ -226,52 +332,17 @@ ${targetFieldHintCases},
""
)
${
isEnrichPolicyExists
isLookupIndexAvailable
? `
${buildLookupJoinEsql(getEntitiesLatestIndexName(spaceId))}

| ENRICH ${enrichPolicyName} ON actorEntityId WITH actorEntityName = entity.name, actorEntityType = entity.type, actorEntitySubType = entity.sub_type, actorHostIp = host.ip
| ENRICH ${enrichPolicyName} ON targetEntityId WITH targetEntityName = entity.name, targetEntityType = entity.type, targetEntitySubType = entity.sub_type, targetHostIp = host.ip
${buildEnrichedEntityFieldsEsql()}
`
: isEnrichPolicyExists
? `
${buildEnrichPolicyEsql(enrichPolicyName)}

// Construct actor and target entities data
// Build entity field conditionally - only include fields that have values
| EVAL actorEntityField = CASE(
actorEntityName IS NOT NULL OR actorEntityType IS NOT NULL OR actorEntitySubType IS NOT NULL,
CONCAT(",\\"entity\\":", "{",
${concatPropIfExists('name', 'actorEntityName', false)},
${concatPropIfExists('type', 'actorEntityType')},
${concatPropIfExists('sub_type', 'actorEntitySubType')},
CASE(
actorHostIp IS NOT NULL,
CONCAT(",\\"host\\":", "{", "\\"ip\\":\\"", TO_STRING(actorHostIp), "\\"", "}"),
""
),
",\\"availableInEntityStore\\":true",
",\\"ecsParentField\\":\\"", actorEntityFieldHint, "\\"",
"}"),
CONCAT(",\\"entity\\":", "{",
"\\"availableInEntityStore\\":false",
",\\"ecsParentField\\":\\"", actorEntityFieldHint, "\\"",
"}")
)
| EVAL targetEntityField = CASE(
targetEntityName IS NOT NULL OR targetEntityType IS NOT NULL OR targetEntitySubType IS NOT NULL,
CONCAT(",\\"entity\\":", "{",
${concatPropIfExists('name', 'targetEntityName', false)},
${concatPropIfExists('type', 'targetEntityType')},
${concatPropIfExists('sub_type', 'targetEntitySubType')},
CASE(
targetHostIp IS NOT NULL,
CONCAT(",\\"host\\":", "{", "\\"ip\\":\\"", TO_STRING(targetHostIp), "\\"", "}"),
""
),
",\\"availableInEntityStore\\":true",
",\\"ecsParentField\\":\\"", targetEntityFieldHint, "\\"",
"}"),
CONCAT(",\\"entity\\":", "{",
"\\"availableInEntityStore\\":false",
",\\"ecsParentField\\":\\"", targetEntityFieldHint, "\\"",
"}")
)
${buildEnrichedEntityFieldsEsql()}
`
: `
| EVAL actorEntityField = CONCAT(",\\"entity\\":", "{",
Expand Down
Loading