diff --git a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_action_list.test.ts b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_action_list.test.ts index 328a465603e6b..0afd8788ad335 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_action_list.test.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_action_list.test.ts @@ -64,7 +64,7 @@ describe('useGetEndpointActionList hook', () => { page: 2, pageSize: 20, startDate: 'now-5d', - userIds: ['elastic', 'citsale'], + userIds: ['*elastic*', '*citsale*'], }, }); }); diff --git a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_action_list.ts b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_action_list.ts index 957afe5e0c151..eba9b162d67f6 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_action_list.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_get_endpoint_action_list.ts @@ -24,6 +24,14 @@ export const useGetEndpointActionList = ( ): UseQueryResult> => { const http = useHttp(); + // prepend and append * to userIds for fuzzy search + let userIds = query.userIds; + if (typeof query.userIds === 'string') { + userIds = `*${query.userIds}*`; + } else if (Array.isArray(query.userIds)) { + userIds = query.userIds.map((userId) => `*${userId}*`); + } + return useQuery>({ queryKey: ['get-action-list', query], ...options, @@ -37,7 +45,7 @@ export const useGetEndpointActionList = ( pageSize: query.pageSize, startDate: query.startDate, statuses: query.statuses, - userIds: query.userIds, + userIds, }, }); }, diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts index b2402b968ee79..f08b82a490718 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions/action_list.test.ts @@ -225,53 +225,171 @@ describe('When using `getActionList()', () => { startDate: 'now-10d', endDate: 'now', commands: ['isolate', 'unisolate', 'get-file'], - userIds: ['elastic'], + userIds: ['*elastic*'], }); expect(esClient.search).toHaveBeenNthCalledWith( 1, - expect.objectContaining({ + { body: { query: { bool: { - filter: [ - { - term: { - input_type: 'endpoint', - }, - }, - { - term: { - type: 'INPUT_ACTION', - }, - }, + must: [ { - range: { - '@timestamp': { - gte: 'now-10d', - }, + bool: { + filter: [ + { + term: { + input_type: 'endpoint', + }, + }, + { + term: { + type: 'INPUT_ACTION', + }, + }, + { + range: { + '@timestamp': { + gte: 'now-10d', + }, + }, + }, + { + range: { + '@timestamp': { + lte: 'now', + }, + }, + }, + { + terms: { + 'data.command': ['isolate', 'unisolate', 'get-file'], + }, + }, + { + terms: { + agents: ['123'], + }, + }, + ], }, }, { - range: { - '@timestamp': { - lte: 'now', - }, - }, - }, - { - terms: { - 'data.command': ['isolate', 'unisolate', 'get-file'], + bool: { + should: [ + { + query_string: { + fields: ['user_id'], + query: '*elastic*', + }, + }, + ], + minimum_should_match: 1, }, }, + ], + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + from: 0, + index: '.logs-endpoint.actions-default', + size: 20, + }, + { ignore: [404], meta: true } + ); + }); + + it('should call search with exact usernames when no wildcards are present', async () => { + // mock metadataService.findHostMetadataForFleetAgents resolved value + (endpointAppContextService.getEndpointMetadataService as jest.Mock) = jest + .fn() + .mockReturnValue({ + findHostMetadataForFleetAgents: jest.fn().mockResolvedValue([]), + }); + await getActionList({ + esClient, + logger, + metadataService: endpointAppContextService.getEndpointMetadataService(), + pageSize: 10, + startDate: 'now-1d', + endDate: 'now', + userIds: ['elastic', 'kibana'], + }); + + expect(esClient.search).toHaveBeenNthCalledWith( + 1, + { + body: { + query: { + bool: { + must: [ { - terms: { - user_id: ['elastic'], + bool: { + filter: [ + { + term: { + input_type: 'endpoint', + }, + }, + { + term: { + type: 'INPUT_ACTION', + }, + }, + { + range: { + '@timestamp': { + gte: 'now-1d', + }, + }, + }, + { + range: { + '@timestamp': { + lte: 'now', + }, + }, + }, + ], }, }, { - terms: { - agents: ['123'], + bool: { + should: [ + { + bool: { + should: [ + { + match: { + user_id: 'elastic', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + user_id: 'kibana', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, }, }, ], @@ -287,12 +405,9 @@ describe('When using `getActionList()', () => { }, from: 0, index: '.logs-endpoint.actions-default', - size: 20, - }), - expect.objectContaining({ - ignore: [404], - meta: true, - }) + size: 10, + }, + { ignore: [404], meta: true } ); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.test.ts b/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.test.ts index 9ea0e66a510cd..b2145bc8740f5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.test.ts @@ -33,15 +33,21 @@ describe('action helpers', () => { body: { query: { bool: { - filter: [ - { - term: { - input_type: 'endpoint', - }, - }, + must: [ { - term: { - type: 'INPUT_ACTION', + bool: { + filter: [ + { + term: { + input_type: 'endpoint', + }, + }, + { + term: { + type: 'INPUT_ACTION', + }, + }, + ], }, }, ], @@ -65,6 +71,7 @@ describe('action helpers', () => { } ); }); + it('should query with additional filter options provided', async () => { const esClient = mockScopedEsClient.asInternalUser; @@ -77,7 +84,7 @@ describe('action helpers', () => { elasticAgentIds: ['agent-123', 'agent-456'], endDate: 'now', commands: ['isolate', 'unisolate', 'get-file'], - userIds: ['elastic'], + userIds: ['*elastic*', '*kibana*'], }); expect(esClient.search).toHaveBeenCalledWith( @@ -85,44 +92,180 @@ describe('action helpers', () => { body: { query: { bool: { - filter: [ + must: [ { - term: { - input_type: 'endpoint', + bool: { + filter: [ + { + term: { + input_type: 'endpoint', + }, + }, + { + term: { + type: 'INPUT_ACTION', + }, + }, + { + range: { + '@timestamp': { + gte: 'now-10d', + }, + }, + }, + { + range: { + '@timestamp': { + lte: 'now', + }, + }, + }, + { + terms: { + 'data.command': ['isolate', 'unisolate', 'get-file'], + }, + }, + { + terms: { + agents: ['agent-123', 'agent-456'], + }, + }, + ], }, }, { - term: { - type: 'INPUT_ACTION', - }, - }, - { - range: { - '@timestamp': { - gte: 'now-10d', - }, - }, - }, - { - range: { - '@timestamp': { - lte: 'now', - }, - }, - }, - { - terms: { - 'data.command': ['isolate', 'unisolate', 'get-file'], + bool: { + should: [ + { + bool: { + should: [ + { + query_string: { + fields: ['user_id'], + query: '*elastic*', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + query_string: { + fields: ['user_id'], + query: '*kibana*', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, }, }, + ], + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + from: 5, + index: '.logs-endpoint.actions-default', + size: 20, + }, + { + ignore: [404], + meta: true, + } + ); + }); + + it('should search with exact usernames when given', async () => { + const esClient = mockScopedEsClient.asInternalUser; + + applyActionListEsSearchMock(esClient); + await getActions({ + esClient, + size: 10, + from: 1, + startDate: 'now-1d', + endDate: 'now', + userIds: ['elastic', 'kibana'], + }); + + expect(esClient.search).toHaveBeenCalledWith( + { + body: { + query: { + bool: { + must: [ { - terms: { - user_id: ['elastic'], + bool: { + filter: [ + { + term: { + input_type: 'endpoint', + }, + }, + { + term: { + type: 'INPUT_ACTION', + }, + }, + { + range: { + '@timestamp': { + gte: 'now-1d', + }, + }, + }, + { + range: { + '@timestamp': { + lte: 'now', + }, + }, + }, + ], }, }, { - terms: { - agents: ['agent-123', 'agent-456'], + bool: { + should: [ + { + bool: { + should: [ + { + match: { + user_id: 'elastic', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + user_id: 'kibana', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, }, }, ], @@ -136,9 +279,9 @@ describe('action helpers', () => { }, ], }, - from: 5, + from: 1, index: '.logs-endpoint.actions-default', - size: 20, + size: 10, }, { ignore: [404], @@ -146,6 +289,7 @@ describe('action helpers', () => { } ); }); + it('should return expected output', async () => { const esClient = mockScopedEsClient.asInternalUser; const actionRequests = createActionRequestsEsSearchResultsMock(); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.ts b/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.ts index a522cbde6d9e1..ee0a2d6e6e2ca 100644 --- a/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/utils/action_list_helpers.ts @@ -9,6 +9,7 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import type { SearchRequest } from '@kbn/data-plugin/public'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { TransportResult } from '@elastic/elasticsearch'; +import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; import { ENDPOINT_ACTIONS_INDEX } from '../../../common/endpoint/constants'; import type { @@ -49,10 +50,6 @@ export const getActions = async ({ }); } - if (userIds?.length) { - additionalFilters.push({ terms: { user_id: userIds } }); - } - if (elasticAgentIds?.length) { additionalFilters.push({ terms: { agents: elasticAgentIds } }); } @@ -70,15 +67,27 @@ export const getActions = async ({ ...additionalFilters, ]; + const must: SearchRequest = [ + { + bool: { + filter: actionsFilters, + }, + }, + ]; + + if (userIds?.length) { + const userIdsKql = userIds.map((userId) => `user_id:${userId}`).join(' or '); + const mustClause = toElasticsearchQuery(fromKueryExpression(userIdsKql)); + must.push(mustClause); + } + const actionsSearchQuery: SearchRequest = { index: ENDPOINT_ACTIONS_INDEX, size, from, body: { query: { - bool: { - filter: actionsFilters, - }, + bool: { must }, }, sort: [ {