[9.0] [SecuritySolution] Fix Data view refresh does not support the indexPattern parameter (#215151) (#215348)

# Backport

This will backport the following commits from `main` to `9.0`:
- [[SecuritySolution] Fix Data view refresh does not support the
indexPattern parameter
(#215151)](https://github.com/elastic/kibana/pull/215151)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Pablo
Machado","email":"pablo.nevesmachado@elastic.co"},"sourceCommit":{"committedDate":"2025-03-20T14:41:54Z","message":"[SecuritySolution]
Fix Data view refresh does not support the indexPattern parameter
(#215151)\n\n## Summary\n\nWhen the data view refresh API or task was
executed, it was overwriting\nthe engine's additional
`indexPattern`.\n\nThis PR updates the code to support `indexPattern`
and ensures the user\nhas privileges for all indices.\n\nI extracted the
merge function to add deduplicate logic.\n\n### How to reproduce it?\n*
Create an entity store using the indexPatterns param\n* Call refresh
dataview API
(`POST\nkbn:api/entity_store/engines/apply_dataview_indices`)\n* It will
apply the dataview and ignore the indexPatterns param\n\nAfter the fix,
we should be able to update the indexPatterns param, and\nthe task that
refreshes the index pattern should pick up the
change\nproperly.\n\n\n### Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"42183d6039c1bb71b42642747f88493fbe591c2e","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","v9.0.0","Team:
SecuritySolution","Theme: entity_analytics","Feature:Entity
Analytics","Team:Entity
Analytics","backport:version","v8.18.0","v9.1.0","v8.19.0"],"title":"[SecuritySolution]
Fix Data view refresh does not support the indexPattern
parameter","number":215151,"url":"https://github.com/elastic/kibana/pull/215151","mergeCommit":{"message":"[SecuritySolution]
Fix Data view refresh does not support the indexPattern parameter
(#215151)\n\n## Summary\n\nWhen the data view refresh API or task was
executed, it was overwriting\nthe engine's additional
`indexPattern`.\n\nThis PR updates the code to support `indexPattern`
and ensures the user\nhas privileges for all indices.\n\nI extracted the
merge function to add deduplicate logic.\n\n### How to reproduce it?\n*
Create an entity store using the indexPatterns param\n* Call refresh
dataview API
(`POST\nkbn:api/entity_store/engines/apply_dataview_indices`)\n* It will
apply the dataview and ignore the indexPatterns param\n\nAfter the fix,
we should be able to update the indexPatterns param, and\nthe task that
refreshes the index pattern should pick up the
change\nproperly.\n\n\n### Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"42183d6039c1bb71b42642747f88493fbe591c2e"}},"sourceBranch":"main","suggestedTargetBranches":["9.0","8.18","8.x"],"targetPullRequestStates":[{"branch":"9.0","label":"v9.0.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/215151","number":215151,"mergeCommit":{"message":"[SecuritySolution]
Fix Data view refresh does not support the indexPattern parameter
(#215151)\n\n## Summary\n\nWhen the data view refresh API or task was
executed, it was overwriting\nthe engine's additional
`indexPattern`.\n\nThis PR updates the code to support `indexPattern`
and ensures the user\nhas privileges for all indices.\n\nI extracted the
merge function to add deduplicate logic.\n\n### How to reproduce it?\n*
Create an entity store using the indexPatterns param\n* Call refresh
dataview API
(`POST\nkbn:api/entity_store/engines/apply_dataview_indices`)\n* It will
apply the dataview and ignore the indexPatterns param\n\nAfter the fix,
we should be able to update the indexPatterns param, and\nthe task that
refreshes the index pattern should pick up the
change\nproperly.\n\n\n### Checklist\n\n- [x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common
scenarios","sha":"42183d6039c1bb71b42642747f88493fbe591c2e"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>
This commit is contained in:
Kibana Machine 2025-03-20 17:25:55 +01:00 committed by GitHub
parent 9ee8417961
commit bc9c01a0e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 123 additions and 39 deletions

View file

@ -19,7 +19,10 @@ import { mockGlobalState } from '../../../../public/common/mock';
import type { EntityDefinition } from '@kbn/entities-schema';
import { convertToEntityManagerDefinition } from './entity_definitions/entity_manager_conversion';
import { EntityType } from '../../../../common/search_strategy';
import type { InitEntityEngineResponse } from '../../../../common/api/entity_analytics';
import type {
EngineDescriptor,
InitEntityEngineResponse,
} from '../../../../common/api/entity_analytics';
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
import { defaultOptions } from './constants';
import type { SecurityPluginStart } from '@kbn/security-plugin/server';
@ -50,6 +53,17 @@ const definition: EntityDefinition = convertToEntityManagerDefinition(
{ namespace: 'test', filter: '' }
);
const engine: EngineDescriptor = {
type: 'user',
frequency: '',
fieldHistoryLength: 0,
indexPattern: '',
lookbackPeriod: '',
timeout: '',
delay: '',
status: 'started',
};
const stubSecurityDataView = createStubDataView({
spec: {
id: 'security',
@ -57,6 +71,12 @@ const stubSecurityDataView = createStubDataView({
},
});
const defaultIndexPatterns = [
stubSecurityDataView.getIndexPattern(),
'.asset-criticality.asset-criticality-default',
'risk-score.risk-score-latest-default',
];
const dataviewService = {
...dataViewPluginMocks.createStartContract(),
get: () => Promise.resolve(stubSecurityDataView),
@ -427,7 +447,7 @@ describe('EntityStoreDataClient', () => {
});
it('applies data view indices to the entity store', async () => {
mockListDescriptor.mockResolvedValueOnce({ engines: [{}] });
mockListDescriptor.mockResolvedValueOnce({ engines: [engine] });
mockGetEntityDefinition.mockResolvedValueOnce({
definitions: [definition],
});
@ -439,6 +459,25 @@ describe('EntityStoreDataClient', () => {
expect(response.successes.length).toBe(1);
});
it('adds the engine indexPattern to the the entity store', async () => {
const indexPattern = 'testIndex';
mockListDescriptor.mockResolvedValueOnce({
engines: [{ ...engine, indexPattern }],
});
mockGetEntityDefinition.mockResolvedValueOnce({
definitions: [definition],
});
const response = await dataClient.applyDataViewIndices();
expect(mockUpdateEntityDefinition).toHaveBeenCalled();
expect(response.errors.length).toBe(0);
expect(response.successes.length).toBe(1);
expect(response.successes[0].changes).toEqual({
indexPatterns: [...defaultIndexPatterns, 'testIndex'],
});
});
it('returns empty successes and errors if no engines found', async () => {
mockListDescriptor.mockResolvedValueOnce({ engines: [] });
@ -448,7 +487,7 @@ describe('EntityStoreDataClient', () => {
expect(response.errors.length).toBe(0);
});
it('throws an error if the user does not have required privileges', async () => {
it('return an error if the user does not have required privileges', async () => {
mockCheckPrivileges.mockReturnValueOnce({
hasAllRequested: false,
privileges: {
@ -456,24 +495,27 @@ describe('EntityStoreDataClient', () => {
kibana: [],
},
});
mockListDescriptor.mockResolvedValueOnce({
engines: [engine],
});
mockGetEntityDefinition.mockResolvedValueOnce({
definitions: [definition],
});
mockListDescriptor.mockResolvedValueOnce({ engines: [{}] });
const result = await dataClient.applyDataViewIndices();
await expect(dataClient.applyDataViewIndices()).rejects.toThrow(
await expect(result.errors.length).toBe(1);
await expect(result.errors[0].message).toMatch(
/The current user does not have the required indices privileges.*/
);
});
it('skips update if index patterns are the same', async () => {
mockListDescriptor.mockResolvedValueOnce({ engines: [{}] });
mockListDescriptor.mockResolvedValueOnce({ engines: [engine] });
mockGetEntityDefinition.mockResolvedValueOnce({
definitions: [
{
indexPatterns: [
stubSecurityDataView.getIndexPattern(),
'.asset-criticality.asset-criticality-default',
'risk-score.risk-score-latest-default',
],
indexPatterns: defaultIndexPatterns,
},
],
});
@ -488,7 +530,7 @@ describe('EntityStoreDataClient', () => {
it('handles errors during update', async () => {
const testErrorMessages = 'Update failed';
mockUpdateEntityDefinition.mockRejectedValueOnce(new Error(testErrorMessages));
mockListDescriptor.mockResolvedValueOnce({ engines: [{}] });
mockListDescriptor.mockResolvedValueOnce({ engines: [engine] });
mockGetEntityDefinition.mockResolvedValueOnce({
definitions: [definition],
});

View file

@ -88,6 +88,7 @@ import {
getEntitiesIndexName,
isPromiseFulfilled,
isPromiseRejected,
mergeEntityStoreIndices,
} from './utils';
import { EntityEngineActions } from './auditing/actions';
import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../audit';
@ -802,31 +803,12 @@ export class EntityStoreDataClient {
};
}
const indexPatterns = await buildIndexPatterns(
const defaultIndexPatterns = await buildIndexPatterns(
this.options.namespace,
this.options.appClient,
this.options.dataViewsService
);
const privileges = await getEntityStoreSourceIndicesPrivileges(
this.options.request,
this.options.security,
indexPatterns
);
if (!privileges.has_all_required) {
const missingPrivilegesMsg = getAllMissingPrivileges(privileges).elasticsearch.index.map(
({ indexName, privileges: missingPrivileges }) =>
`Missing [${missingPrivileges.join(', ')}] privileges for index '${indexName}'.`
);
throw new Error(
`The current user does not have the required indices privileges.\n${missingPrivilegesMsg.join(
'\n'
)}`
);
}
const updateDefinitionPromises: Array<Promise<EngineDataviewUpdateResult>> = engines.map(
async (engine) => {
const originalStatus = engine.status;
@ -842,6 +824,8 @@ export class EntityStoreDataClient {
);
}
const indexPatterns = mergeEntityStoreIndices(defaultIndexPatterns, engine.indexPattern);
// Skip update if index patterns are the same
if (isEqual(definition.indexPatterns, indexPatterns)) {
logger.debug(
@ -854,6 +838,25 @@ export class EntityStoreDataClient {
);
}
const privileges = await getEntityStoreSourceIndicesPrivileges(
this.options.request,
this.options.security,
indexPatterns
);
if (!privileges.has_all_required) {
const missingPrivilegesMsg = getAllMissingPrivileges(privileges).elasticsearch.index.map(
({ indexName, privileges: missingPrivileges }) =>
`Missing [${missingPrivileges.join(', ')}] privileges for index '${indexName}'.`
);
throw new Error(
`The current user does not have the required indices privileges for updating the '${
engine.type
}' entity store.\n${missingPrivilegesMsg.join('\n')}`
);
}
// Update savedObject status
await this.engineClient.updateStatus(engine.type, ENGINE_STATUS.UPDATING);

View file

@ -17,7 +17,7 @@ import {
serviceEntityEngineDescription,
} from '../entity_definitions/entity_descriptions';
import type { EntityStoreConfig } from '../types';
import { buildEntityDefinitionId } from '../utils';
import { buildEntityDefinitionId, mergeEntityStoreIndices } from '../utils';
import type { EntityDescription } from '../entity_definitions/types';
import type { EntityEngineInstallationDescriptor } from './types';
import { merge } from '../../../../../common/utils/objects/merge';
@ -46,9 +46,7 @@ export const createEngineDescription = (params: EngineDescriptionParams) => {
};
const options = merge(defaultOptions, merge(fileConfig, requestParams));
const indexPatterns = options.indexPattern
? defaultIndexPatterns.concat(options.indexPattern.split(','))
: defaultIndexPatterns;
const indexPatterns = mergeEntityStoreIndices(defaultIndexPatterns, options.indexPattern);
const description = engineDescriptionRegistry[entityType];

View file

@ -52,9 +52,9 @@ export const applyDataViewIndicesEntityEngineRoute = (
if (successes.length === 0 && errors.length > 0) {
return siemResponse.error({
statusCode: 500,
body: `Error in ApplyEntityEngineDataViewIndices. Errors: [${errorMessages.join(
', '
)}]`,
body: `Errors applying data view changes to the entity store. Errors: \n${errorMessages.join(
'\n\n'
)}`,
});
}

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { mergeEntityStoreIndices } from './entity_utils';
describe('mergeEntityStoreIndices', () => {
it('returns the original indices if indexPattern is empty', () => {
const indices = ['index1', 'index2'];
const result = mergeEntityStoreIndices(indices, '');
expect(result).toEqual(indices);
});
it('merges indices with indexPattern when indexPattern is provided', () => {
const indices = ['index1', 'index2'];
const indexPattern = 'index3,index4';
const result = mergeEntityStoreIndices(indices, indexPattern);
expect(result).toEqual(['index1', 'index2', 'index3', 'index4']);
});
it('deduplicate indices', () => {
const indices = ['index1', 'index2'];
const indexPattern = 'index2,index3';
const result = mergeEntityStoreIndices(indices, indexPattern);
expect(result).toEqual(['index1', 'index2', 'index3']);
});
it('returns an empty array if both indices and indexPattern are empty', () => {
const indices: string[] = [];
const indexPattern = '';
const result = mergeEntityStoreIndices(indices, indexPattern);
expect(result).toEqual([]);
});
});

View file

@ -11,6 +11,7 @@ import {
entitiesIndexPattern,
} from '@kbn/entities-schema';
import type { DataViewsService, DataView } from '@kbn/data-views-plugin/common';
import { uniq } from 'lodash/fp';
import type { AppClient } from '../../../../types';
import { getRiskScoreLatestIndex } from '../../../../../common/entity_analytics/risk_engine';
import { getAssetCriticalityIndex } from '../../../../../common/entity_analytics/asset_criticality';
@ -77,3 +78,6 @@ export const isPromiseFulfilled = <T>(
export const isPromiseRejected = <T>(
result: PromiseSettledResult<T>
): result is PromiseRejectedResult => result.status === 'rejected';
export const mergeEntityStoreIndices = (indices: string[], indexPattern: string | undefined) =>
indexPattern ? uniq(indices.concat(indexPattern.split(','))) : indices;