mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
9ee8417961
commit
bc9c01a0e0
6 changed files with 123 additions and 39 deletions
|
@ -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],
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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'
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue