From 0c743a5e7771509a8d36d41b40845b241a993830 Mon Sep 17 00:00:00 2001 From: Ash <1849116+ashokaditya@users.noreply.github.com> Date: Fri, 2 Feb 2024 19:31:26 +0100 Subject: [PATCH] [Security Solution][Endpoint][UI] Add `agentTypes` filter to action history (#175810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds agent type filter values to the `Type` filter on actions history. So in addition to filtering by action type one can also filter with agent types. - With the feature flag enabled, the filter name changes to `Types` as it now holds Action and Agent types filter options. A new URL param called `agentTypes` is added when agent type options are selected. The existing `types` URL param works the way it does now. - Without the feature flag enabled the filter behaves and looks the way it does currently. **with feature flag `responseActionsSentinelOneV1Enabled` on** Screenshot 2024-02-01 at 11 27 52 AM ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../common/api/endpoint/actions/list_route.ts | 45 +- .../common/endpoint/schema/actions.test.ts | 635 +++++++++--------- .../service/response_actions/type_guards.ts | 12 +- .../public/common/translations.ts | 2 +- .../components/actions_log_filter.tsx | 101 ++- .../components/actions_log_filter_popover.tsx | 13 +- .../components/actions_log_filters.tsx | 49 +- .../components/hooks.tsx | 108 ++- .../use_action_history_url_params.test.ts | 101 ++- .../use_action_history_url_params.ts | 79 ++- .../response_actions_log.test.tsx | 6 +- .../response_actions_log.tsx | 34 +- .../translations.tsx | 23 +- .../history_log.cy.ts | 10 +- .../use_get_endpoint_action_list.ts | 3 +- .../view/response_actions_list_page.test.tsx | 203 +++++- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 19 files changed, 982 insertions(+), 445 deletions(-) diff --git a/x-pack/plugins/security_solution/common/api/endpoint/actions/list_route.ts b/x-pack/plugins/security_solution/common/api/endpoint/actions/list_route.ts index 1f6ffe4e50613..3fe188198bc4b 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/actions/list_route.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/actions/list_route.ts @@ -21,16 +21,28 @@ const commandsSchema = schema.oneOf( RESPONSE_ACTION_API_COMMANDS_NAMES.map((command) => schema.literal(command)) ); -// TODO: fix the odd TS error -// @ts-expect-error TS2769: No overload matches this call -const statusesSchema = schema.oneOf(RESPONSE_ACTION_STATUS.map((status) => schema.literal(status))); -// @ts-expect-error TS2769: No overload matches this call -const typesSchema = schema.oneOf(RESPONSE_ACTION_TYPE.map((type) => schema.literal(type))); +const statusesSchema = { + // @ts-expect-error TS2769: No overload matches this call + schema: schema.oneOf(RESPONSE_ACTION_STATUS.map((status) => schema.literal(status))), + options: { minSize: 1, maxSize: RESPONSE_ACTION_STATUS.length }, +}; -const agentTypesSchema = schema.oneOf( +const actionTypesSchema = { // @ts-expect-error TS2769: No overload matches this call - RESPONSE_ACTION_AGENT_TYPE.map((agentType) => schema.literal(agentType)) -); + schema: schema.oneOf(RESPONSE_ACTION_TYPE.map((type) => schema.literal(type))), + options: { minSize: 1, maxSize: RESPONSE_ACTION_TYPE.length }, +}; + +const agentTypesSchema = { + schema: schema.oneOf( + // @ts-expect-error TS2769: No overload matches this call + RESPONSE_ACTION_AGENT_TYPE.map((agentType) => schema.literal(agentType)) + ), + options: { + minSize: 1, + maxSize: RESPONSE_ACTION_AGENT_TYPE.length, + }, +}; export const EndpointActionListRequestSchema = { query: schema.object({ @@ -42,10 +54,8 @@ export const EndpointActionListRequestSchema = { ), agentTypes: schema.maybe( schema.oneOf([ - schema.arrayOf(agentTypesSchema, { - minSize: 1, - }), - agentTypesSchema, + schema.arrayOf(agentTypesSchema.schema, agentTypesSchema.options), + agentTypesSchema.schema, ]) ), commands: schema.maybe( @@ -58,7 +68,10 @@ export const EndpointActionListRequestSchema = { startDate: schema.maybe(schema.string()), // date ISO strings or moment date endDate: schema.maybe(schema.string()), // date ISO strings or moment date statuses: schema.maybe( - schema.oneOf([schema.arrayOf(statusesSchema, { minSize: 1, maxSize: 3 }), statusesSchema]) + schema.oneOf([ + schema.arrayOf(statusesSchema.schema, statusesSchema.options), + statusesSchema.schema, + ]) ), userIds: schema.maybe( schema.oneOf([ @@ -86,8 +99,12 @@ export const EndpointActionListRequestSchema = { }), ]) ), + // action types types: schema.maybe( - schema.oneOf([schema.arrayOf(typesSchema, { minSize: 1, maxSize: 2 }), typesSchema]) + schema.oneOf([ + schema.arrayOf(actionTypesSchema.schema, actionTypesSchema.options), + actionTypesSchema.schema, + ]) ), }), }; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts index b523d00336c4f..bc32080fab1be 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/actions.test.ts @@ -10,6 +10,7 @@ import { v4 as uuidv4 } from 'uuid'; import { RESPONSE_ACTION_AGENT_TYPE, RESPONSE_ACTION_API_COMMANDS_NAMES, + RESPONSE_ACTION_TYPE, } from '../service/response_actions/constants'; import { createHapiReadableStreamMock } from '../../../server/endpoint/services/actions/mocks'; import type { HapiReadableStream } from '../../../server/types'; @@ -29,364 +30,394 @@ describe('actions schemas', () => { }).not.toThrow(); }); - it.each(['manual', 'automated'])('should accept types param', (value) => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ types: value }); - }).not.toThrow(); - }); - it.each([['manual'], ['automated']])('should accept types param in array', (value) => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ types: value }); - }).not.toThrow(); - }); - - it('should accept multiple types in an array', () => { + it('should work with all required query params', () => { expect(() => { EndpointActionListRequestSchema.query.validate({ - types: ['manual', 'automated'], + page: 10, + pageSize: 100, + startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + endDate: new Date().toISOString(), // today }); }).not.toThrow(); }); - it('should not accept empty types in an array', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - types: [], - }); - }).toThrow(); - }); - it('should require at least 1 agent ID', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentIds: [] }); // no agent_ids provided - }).toThrow(); - }); - - it('should accept an agent ID if not in an array', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentIds: uuidv4() }); - }).not.toThrow(); - }); + describe('page and pageSize', () => { + it('should not work with invalid value for `page` query param', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ page: -1 }); + }).toThrow(); + expect(() => { + EndpointActionListRequestSchema.query.validate({ page: 0 }); + }).toThrow(); + }); - it('should accept an agent ID in an array', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentIds: [uuidv4()] }); - }).not.toThrow(); + it('should not work with invalid value for `pageSize` query param', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ pageSize: 100001 }); + }).toThrow(); + expect(() => { + EndpointActionListRequestSchema.query.validate({ pageSize: 0 }); + }).toThrow(); + }); }); - it('should accept multiple agent IDs in an array', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - agentIds: [uuidv4(), uuidv4(), uuidv4()], - }); - }).not.toThrow(); - }); + describe('types', () => { + it.each(RESPONSE_ACTION_TYPE)('should accept valid %s `types`', (value) => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ types: value }); + }).not.toThrow(); + }); + + it.each(RESPONSE_ACTION_TYPE.map((e) => [e]))( + 'should accept valid %s `types` as a list', + (value) => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ types: value }); + }).not.toThrow(); + } + ); + + it('should accept multiple valid `types` as a list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + types: RESPONSE_ACTION_TYPE, + }); + }).not.toThrow(); + }); - it('should not limit multiple agent IDs', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - agentIds: Array(255) - .fill(1) - .map(() => uuidv4()), - }); - }).not.toThrow(); + it('should not accept an empty list for `types`', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + types: [], + }); + }).toThrow(); + }); }); - it('should accept undefined agentTypes ', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentTypes: undefined }); - }).not.toThrow(); - }); + describe('agentIds', () => { + it('should require at least 1 agent ID', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentIds: [] }); // no agent_ids provided + }).toThrow(); + }); - it('should not accept empty agentTypes list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentTypes: [] }); - }).toThrow(); - }); + it('should accept an agent ID if not in an array', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentIds: uuidv4() }); + }).not.toThrow(); + }); - it('should not accept invalid agentTypes list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentTypes: ['x'] }); - }).toThrow(); - }); + it('should accept an agent ID in an array', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentIds: [uuidv4()] }); + }).not.toThrow(); + }); - it('should not accept invalid string agentTypes ', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentTypes: 'non-agent' }); - }).toThrow(); - }); + it('should accept multiple agent IDs in an array', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + agentIds: [uuidv4(), uuidv4(), uuidv4()], + }); + }).not.toThrow(); + }); - it('should not accept empty string agentTypes ', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentTypes: '' }); - }).toThrow(); + it('should not limit multiple agent IDs', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + agentIds: Array(255) + .fill(1) + .map(() => uuidv4()), + }); + }).not.toThrow(); + }); }); - it.each(RESPONSE_ACTION_AGENT_TYPE)('should accept allowed %s agentTypes ', (agentTypes) => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentTypes }); - }).not.toThrow(); - }); + describe('agentTypes', () => { + it('should accept undefined agentTypes ', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentTypes: undefined }); + }).not.toThrow(); + }); - it.each(RESPONSE_ACTION_AGENT_TYPE)( - 'should accept allowed %s agentTypes in a list', - (agentTypes) => { + it.each(RESPONSE_ACTION_AGENT_TYPE)('should accept allowed %s agentTypes ', (agentTypes) => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentTypes }); + }).not.toThrow(); + }); + + it.each(RESPONSE_ACTION_AGENT_TYPE)( + 'should accept allowed %s agentTypes in a list', + (agentTypes) => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentTypes: [agentTypes] }); + }).not.toThrow(); + } + ); + + it('should accept allowed agentTypes in list', () => { expect(() => { - EndpointActionListRequestSchema.query.validate({ agentTypes: [agentTypes] }); + EndpointActionListRequestSchema.query.validate({ + agentTypes: RESPONSE_ACTION_AGENT_TYPE, + }); }).not.toThrow(); - } - ); + }); - it('should accept allowed agentTypes in list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ agentTypes: RESPONSE_ACTION_AGENT_TYPE }); - }).not.toThrow(); - }); + it('should not accept empty agentTypes list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentTypes: [] }); + }).toThrow(); + }); - it('should not accept invalid agentTypes in list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - agentTypes: [...RESPONSE_ACTION_AGENT_TYPE, 'non-agent'], - }); - }).toThrow(); - }); + it('should not accept invalid agentTypes list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentTypes: ['x'] }); + }).toThrow(); + }); - it('should not accept `undefined` agentTypes in list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - agentTypes: [undefined], - }); - }).toThrow(); - }); + it('should not accept invalid string agentTypes ', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentTypes: 'non-agent' }); + }).toThrow(); + }); - it('should work with all required query params', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - page: 10, - pageSize: 100, - startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday - endDate: new Date().toISOString(), // today - }); - }).not.toThrow(); - }); + it('should not accept empty string agentTypes ', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ agentTypes: '' }); + }).toThrow(); + }); - it('should not work with invalid value for `page` query param', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ page: -1 }); - }).toThrow(); - expect(() => { - EndpointActionListRequestSchema.query.validate({ page: 0 }); - }).toThrow(); - }); + it('should not accept invalid agentTypes in list', () => { + const excludedAgentType = + RESPONSE_ACTION_AGENT_TYPE[Math.round(Math.random() * RESPONSE_ACTION_AGENT_TYPE.length)]; - it('should not work with invalid value for `pageSize` query param', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ pageSize: 100001 }); - }).toThrow(); - expect(() => { - EndpointActionListRequestSchema.query.validate({ pageSize: 0 }); - }).toThrow(); - }); + const partialAllowedAgentTypes = RESPONSE_ACTION_AGENT_TYPE.filter( + (type) => type !== excludedAgentType + ); - it('should not work without valid userIds', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - page: 10, - pageSize: 100, - startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday - endDate: new Date().toISOString(), // today - userIds: [], - }); - }).toThrow(); - }); + expect(() => { + EndpointActionListRequestSchema.query.validate({ + agentTypes: [...partialAllowedAgentTypes, 'non-agent'], + }); + }).toThrow(); + }); - it('should work with a single userIds query params', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - page: 10, - pageSize: 100, - startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday - endDate: new Date().toISOString(), // today - userIds: ['elastic'], - }); - }).not.toThrow(); + it('should not accept `undefined` agentTypes in list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + agentTypes: [undefined], + }); + }).toThrow(); + }); }); - it('should work with multiple userIds query params', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - page: 10, - pageSize: 100, - startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday - endDate: new Date().toISOString(), // today - userIds: ['elastic', 'fleet'], - }); - }).not.toThrow(); - }); + describe('userIds', () => { + it('should not work without valid userIds', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + page: 10, + pageSize: 100, + startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + endDate: new Date().toISOString(), // today + userIds: [], + }); + }).toThrow(); + }); - it.each(RESPONSE_ACTION_API_COMMANDS_NAMES)( - 'should work with commands query params with %s action', - (command) => { + it('should work with a single userIds query params', () => { expect(() => { EndpointActionListRequestSchema.query.validate({ page: 10, pageSize: 100, startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday endDate: new Date().toISOString(), // today - commands: command, + userIds: ['elastic'], }); }).not.toThrow(); - } - ); + }); - it('should work with commands query params with a single action type in a list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - page: 10, - pageSize: 100, - startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday - endDate: new Date().toISOString(), // today - commands: ['isolate'], - }); - }).not.toThrow(); - }); + it('should work with multiple userIds query params', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + page: 10, + pageSize: 100, + startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + endDate: new Date().toISOString(), // today + userIds: ['elastic', 'fleet'], + }); + }).not.toThrow(); + }); + }); + + describe('commands', () => { + it.each(RESPONSE_ACTION_API_COMMANDS_NAMES)( + 'should work with commands query params with %s action', + (command) => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + page: 10, + pageSize: 100, + startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + endDate: new Date().toISOString(), // today + commands: command, + }); + }).not.toThrow(); + } + ); + + it('should work with commands query params with a single action type in a list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + page: 10, + pageSize: 100, + startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + endDate: new Date().toISOString(), // today + commands: ['isolate'], + }); + }).not.toThrow(); + }); - it('should not work with commands query params with empty array', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - page: 10, - pageSize: 100, - startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday - endDate: new Date().toISOString(), // today - commands: [], - }); - }).toThrow(); - }); + it('should not work with commands query params with empty array', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + page: 10, + pageSize: 100, + startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + endDate: new Date().toISOString(), // today + commands: [], + }); + }).toThrow(); + }); - it('should work with commands query params with multiple types', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - page: 10, - pageSize: 100, - startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday - endDate: new Date().toISOString(), // today - commands: ['isolate', 'unisolate'], - }); - }).not.toThrow(); + it('should work with commands query params with multiple types', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + page: 10, + pageSize: 100, + startDate: new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(), // yesterday + endDate: new Date().toISOString(), // today + commands: ['isolate', 'unisolate'], + }); + }).not.toThrow(); + }); }); - it('should work with at least one `status` filter in a list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: ['failed'], - }); - }).not.toThrow(); - }); + describe('statuses', () => { + it('should work with at least one `statuses` filter in a list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: ['failed'], + }); + }).not.toThrow(); + }); - it.each(['failed', 'pending', 'successful'])('should work alone with %s filter', (status) => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: status, - }); - }).not.toThrow(); - }); + it.each(['failed', 'pending', 'successful'])('should work alone with %s filter', (status) => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: status, + }); + }).not.toThrow(); + }); - it('should not work with empty list for `status` filter', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: [], - }); - }).toThrow(); - }); + it('should work with at multiple `statuses` filter', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: ['failed', 'pending', 'successful'], + }); + }).not.toThrow(); + }); - it('should not work with more than allowed list for `status` filter', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: ['failed', 'pending', 'successful', 'xyz'], - }); - }).toThrow(); - }); + it('should not work with empty list for `statuses` filter', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: [], + }); + }).toThrow(); + }); - it('should not work with any string for `status` filter', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: ['xyz', 'pqr', 'abc'], - }); - }).toThrow(); - }); + it('should not work with more than allowed list for `statuses` filter', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: ['failed', 'pending', 'successful', 'xyz'], + }); + }).toThrow(); + }); - it('should work with at multiple `status` filter', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: ['failed', 'pending', 'successful'], - }); - }).not.toThrow(); + it('should not work with any string for `statuses` filter', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: ['xyz', 'pqr', 'abc'], + }); + }).toThrow(); + }); }); - it('should not work with only spaces for a string in `withOutputs` list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: ['failed', 'pending', 'successful'], - withOutputs: ' ', - }); - }).toThrow(); - }); + describe('withOutputs', () => { + it('should not work with only spaces for a string in `withOutputs` list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: ['failed', 'pending', 'successful'], + withOutputs: ' ', + }); + }).toThrow(); + }); - it('should not work with empty string in `withOutputs` list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: ['failed', 'pending', 'successful'], - withOutputs: '', - }); - }).toThrow(); - }); + it('should not work with empty string in `withOutputs` list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: ['failed', 'pending', 'successful'], + withOutputs: '', + }); + }).toThrow(); + }); - it('should not work with empty strings in `withOutputs` list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: ['failed', 'pending', 'successful'], - withOutputs: ['action-id-1', ' ', 'action-id-2'], - }); - }).toThrow(); - }); + it('should not work with empty strings in `withOutputs` list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: ['failed', 'pending', 'successful'], + withOutputs: ['action-id-1', ' ', 'action-id-2'], + }); + }).toThrow(); + }); - it('should work with a single action id in `withOutputs` list', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: ['failed', 'pending', 'successful'], - withOutputs: 'action-id-1', - }); - }).not.toThrow(); - }); + it('should work with a single action id in `withOutputs` list', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: ['failed', 'pending', 'successful'], + withOutputs: 'action-id-1', + }); + }).not.toThrow(); + }); - it('should work with multiple `withOutputs` filter', () => { - expect(() => { - EndpointActionListRequestSchema.query.validate({ - startDate: 'now-1d', // yesterday - endDate: 'now', // today - statuses: ['failed', 'pending', 'successful'], - withOutputs: ['action-id-1', 'action-id-2'], - }); - }).not.toThrow(); + it('should work with multiple `withOutputs` filter', () => { + expect(() => { + EndpointActionListRequestSchema.query.validate({ + startDate: 'now-1d', // yesterday + endDate: 'now', // today + statuses: ['failed', 'pending', 'successful'], + withOutputs: ['action-id-1', 'action-id-2'], + }); + }).not.toThrow(); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts index 7786c9ebb1f57..6c65c9e07de15 100644 --- a/x-pack/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts +++ b/x-pack/plugins/security_solution/common/endpoint/service/response_actions/type_guards.ts @@ -9,12 +9,13 @@ import type { ActionDetails, MaybeImmutable, ResponseActionExecuteOutputContent, + ResponseActionGetFileOutputContent, + ResponseActionGetFileParameters, ResponseActionsExecuteParameters, ResponseActionUploadOutputContent, ResponseActionUploadParameters, - ResponseActionGetFileOutputContent, - ResponseActionGetFileParameters, } from '../../types'; +import { RESPONSE_ACTION_AGENT_TYPE, RESPONSE_ACTION_TYPE } from './constants'; type SomeObjectWithCommand = Pick; @@ -38,3 +39,10 @@ export const isGetFileAction = ( ): action is ActionDetails => { return action.command === 'get-file'; }; + +// type guards to ensure only the matching string values are attached to the types filter type +export const isAgentType = (type: string): type is typeof RESPONSE_ACTION_AGENT_TYPE[number] => + RESPONSE_ACTION_AGENT_TYPE.includes(type as typeof RESPONSE_ACTION_AGENT_TYPE[number]); + +export const isActionType = (type: string): type is typeof RESPONSE_ACTION_TYPE[number] => + RESPONSE_ACTION_TYPE.includes(type as typeof RESPONSE_ACTION_TYPE[number]); diff --git a/x-pack/plugins/security_solution/public/common/translations.ts b/x-pack/plugins/security_solution/public/common/translations.ts index 534e553824ad6..551e977ec9861 100644 --- a/x-pack/plugins/security_solution/public/common/translations.ts +++ b/x-pack/plugins/security_solution/public/common/translations.ts @@ -74,7 +74,7 @@ export const UNSAVED_TIMELINE_SAVE_PROMPT_TITLE = i18n.translate( } ); -const getAgentTypeName = (agentType: ResponseActionAgentType) => { +export const getAgentTypeName = (agentType: ResponseActionAgentType) => { switch (agentType) { case 'endpoint': return 'Endpoint'; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx index 64ac7258a515c..a7bf44db48f3b 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter.tsx @@ -8,12 +8,22 @@ import { orderBy } from 'lodash/fp'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPopoverTitle, EuiSelectable } from '@elastic/eui'; +import { + isActionType, + isAgentType, +} from '../../../../../common/endpoint/service/response_actions/type_guards'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP, type ResponseActionsApiCommandNames, } from '../../../../../common/endpoint/service/response_actions/constants'; import { ActionsLogFilterPopover } from './actions_log_filter_popover'; -import { type FilterItems, type FilterName, useActionsLogFilter } from './hooks'; +import { + type ActionsLogPopupFilters, + type FilterItems, + type TypesFilters, + useActionsLogFilter, +} from './hooks'; import { ClearAllButton } from './clear_all_button'; import { UX_MESSAGES } from '../translations'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; @@ -21,17 +31,23 @@ import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; export const ActionsLogFilter = memo( ({ filterName, + typesFilters, isFlyout, onChangeFilterOptions, 'data-test-subj': dataTestSubj, }: { - filterName: FilterName; + filterName: ActionsLogPopupFilters; + typesFilters?: TypesFilters; isFlyout: boolean; - onChangeFilterOptions: (selectedOptions: string[]) => void; + onChangeFilterOptions?: (selectedOptions: string[]) => void; 'data-test-subj'?: string; }) => { const getTestId = useTestIdGenerator(dataTestSubj); + const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled( + 'responseActionsSentinelOneV1Enabled' + ); + // popover states and handlers const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onPopoverButtonClick = useCallback(() => { @@ -55,11 +71,11 @@ export const ActionsLogFilter = memo( setUrlActionsFilters, setUrlHostsFilters, setUrlStatusesFilters, + setUrlTypesFilters, setUrlTypeFilters, } = useActionsLogFilter({ filterName, isFlyout, - isPopoverOpen, searchString, }); @@ -82,18 +98,18 @@ export const ActionsLogFilter = memo( [filterName, isPopoverOpen] ); - // augmented options based on hosts filter + // augmented options based on the host filter const sortedHostsFilterOptions = useMemo(() => { if (shouldPinSelectedHosts() || areHostsSelectedOnMount) { // pin checked items to the top return orderBy('checked', 'asc', items); } - // return options as is for other filters + // return options as are for other filters return items; }, [areHostsSelectedOnMount, shouldPinSelectedHosts, items]); const isSearchable = useMemo( - () => filterName !== 'statuses' && filterName !== 'type', + () => filterName !== 'statuses' && filterName !== 'types', [filterName] ); @@ -102,14 +118,31 @@ export const ActionsLogFilter = memo( // update filter UI options state setItems(newOptions.map((option) => option)); - // compute selected list of options + // compute a selected list of options const selectedItems = newOptions.reduce((acc, curr) => { - if (curr.checked === 'on') { + if (curr.checked === 'on' && curr.key) { acc.push(curr.key); } return acc; }, []); + const groupedSelectedTypeFilterOptions = selectedItems.reduce<{ + agentTypes: string[]; + actionTypes: string[]; + }>( + (acc, item) => { + if (isAgentType(item)) { + acc.agentTypes.push(item); + } + if (isActionType(item)) { + acc.actionTypes.push(item); + } + + return acc; + }, + { actionTypes: [], agentTypes: [] } + ); + if (!isFlyout) { // update URL params if (filterName === 'actions') { @@ -127,20 +160,39 @@ export const ActionsLogFilter = memo( setUrlHostsFilters(selectedItems.join()); } else if (filterName === 'statuses') { setUrlStatusesFilters(selectedItems.join()); - } else if (filterName === 'type') { - setUrlTypeFilters(selectedItems.join()); + } else if (filterName === 'types') { + if (isSentinelOneV1Enabled) { + setUrlTypesFilters({ + agentTypes: groupedSelectedTypeFilterOptions.agentTypes.join(), + actionTypes: groupedSelectedTypeFilterOptions.actionTypes.join(), + }); + } else { + setUrlTypeFilters(selectedItems.join()); + } } // reset shouldPinSelectedHosts, setAreHostsSelectedOnMount shouldPinSelectedHosts(false); setAreHostsSelectedOnMount(false); } - // update query state - onChangeFilterOptions(selectedItems); + // update overall query state + if (typesFilters && typeof onChangeFilterOptions === 'undefined') { + typesFilters.agentTypes.onChangeFilterOptions( + groupedSelectedTypeFilterOptions.agentTypes + ); + typesFilters.actionTypes.onChangeFilterOptions( + groupedSelectedTypeFilterOptions.actionTypes + ); + } else { + if (typeof onChangeFilterOptions !== 'undefined') { + onChangeFilterOptions(selectedItems); + } + } }, [ setItems, isFlyout, + typesFilters, onChangeFilterOptions, filterName, shouldPinSelectedHosts, @@ -148,6 +200,8 @@ export const ActionsLogFilter = memo( setUrlActionsFilters, setUrlHostsFilters, setUrlStatusesFilters, + isSentinelOneV1Enabled, + setUrlTypesFilters, setUrlTypeFilters, ] ); @@ -163,29 +217,38 @@ export const ActionsLogFilter = memo( ); if (!isFlyout) { - // update URL params based on filter + // update URL params based on filter on page if (filterName === 'actions') { setUrlActionsFilters(''); } else if (filterName === 'hosts') { setUrlHostsFilters(''); } else if (filterName === 'statuses') { setUrlStatusesFilters(''); - } else if (filterName === 'type') { - setUrlTypeFilters(''); + } else if (filterName === 'types') { + setUrlTypesFilters({ agentTypes: '', actionTypes: '' }); + } + } + + // update query state for flyout filters + if (typesFilters && typeof onChangeFilterOptions === 'undefined') { + typesFilters.agentTypes.onChangeFilterOptions([]); + typesFilters.actionTypes.onChangeFilterOptions([]); + } else { + if (typeof onChangeFilterOptions !== 'undefined') { + onChangeFilterOptions([]); } } - // update query state - onChangeFilterOptions([]); }, [ setItems, items, isFlyout, + typesFilters, onChangeFilterOptions, filterName, setUrlActionsFilters, setUrlHostsFilters, setUrlStatusesFilters, - setUrlTypeFilters, + setUrlTypesFilters, ]); return ( diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter_popover.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter_popover.tsx index 5834345466acb..8eb7f8e2d298d 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filter_popover.tsx @@ -6,7 +6,8 @@ */ import React, { memo, useMemo } from 'react'; -import { EuiPopover, EuiFilterButton, useGeneratedHtmlId } from '@elastic/eui'; +import { EuiFilterButton, EuiPopover, useGeneratedHtmlId } from '@elastic/eui'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { FILTER_NAMES } from '../translations'; import type { FilterName } from './hooks'; import { useTestIdGenerator } from '../../../hooks/use_test_id_generator'; @@ -34,6 +35,9 @@ export const ActionsLogFilterPopover = memo( 'data-test-subj'?: string; }) => { const getTestId = useTestIdGenerator(dataTestSubj); + const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled( + 'responseActionsSentinelOneV1Enabled' + ); const filterGroupPopoverId = useGeneratedHtmlId({ prefix: 'filterGroupPopover', @@ -50,7 +54,11 @@ export const ActionsLogFilterPopover = memo( hasActiveFilters={hasActiveFilters} numActiveFilters={numActiveFilters} > - {FILTER_NAMES[filterName]} + {filterName === 'types' + ? isSentinelOneV1Enabled + ? FILTER_NAMES.types('s') + : FILTER_NAMES.types('') + : FILTER_NAMES[filterName]} ), [ @@ -58,6 +66,7 @@ export const ActionsLogFilterPopover = memo( getTestId, hasActiveFilters, isPopoverOpen, + isSentinelOneV1Enabled, numActiveFilters, numFilters, onButtonClick, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx index 9e22884d3b14b..f403ba6f6aacd 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/actions_log_filters.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React, { memo, useCallback, useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFilterGroup, EuiSuperUpdateButton } from '@elastic/eui'; +import { EuiFilterGroup, EuiFlexGroup, EuiFlexItem, EuiSuperUpdateButton } from '@elastic/eui'; import type { DurationRange, OnRefreshChangeProps, @@ -13,8 +13,8 @@ import type { import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import type { useGetEndpointActionList } from '../../../hooks'; import { - type DateRangePickerValues, ActionLogDateRangePicker, + type DateRangePickerValues, } from './actions_log_date_range_picker'; import { ActionsLogFilter } from './actions_log_filter'; import { ActionsLogUsersFilter } from './actions_log_users_filter'; @@ -26,6 +26,7 @@ export const ActionsLogFilters = memo( isDataLoading, isFlyout, onClick, + onChangeAgentTypesFilter, onChangeHostsFilter, onChangeCommandsFilter, onChangeStatusesFilter, @@ -40,6 +41,7 @@ export const ActionsLogFilters = memo( dateRangePickerState: DateRangePickerValues; isDataLoading: boolean; isFlyout: boolean; + onChangeAgentTypesFilter: (selectedAgentTypes: string[]) => void; onChangeHostsFilter: (selectedCommands: string[]) => void; onChangeCommandsFilter: (selectedCommands: string[]) => void; onChangeStatusesFilter: (selectedStatuses: string[]) => void; @@ -56,6 +58,11 @@ export const ActionsLogFilters = memo( const responseActionsEnabled = useIsExperimentalFeatureEnabled( 'endpointResponseActionsEnabled' ); + + const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled( + 'responseActionsSentinelOneV1Enabled' + ); + const filters = useMemo(() => { return ( <> @@ -79,25 +86,39 @@ export const ActionsLogFilters = memo( onChangeFilterOptions={onChangeStatusesFilter} data-test-subj={dataTestSubj} /> - {responseActionsEnabled && ( - - )} + {isSentinelOneV1Enabled + ? responseActionsEnabled && ( + + ) + : responseActionsEnabled && ( + + )} ); }, [ - dataTestSubj, + showHostsFilter, isFlyout, - onChangeCommandsFilter, + isSentinelOneV1Enabled, onChangeHostsFilter, - onChangeTypeFilter, + dataTestSubj, + onChangeCommandsFilter, onChangeStatusesFilter, responseActionsEnabled, - showHostsFilter, + onChangeAgentTypesFilter, + onChangeTypeFilter, ]); const onClickRefreshButton = useCallback(() => onClick(), [onClick]); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx index 70d7b9062cb5f..36d5d8d1f556b 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/hooks.tsx @@ -10,8 +10,11 @@ import type { DurationRange, OnRefreshChangeProps, } from '@elastic/eui/src/components/date_picker/types'; +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { getAgentTypeName } from '../../../../common/translations'; import { ExperimentalFeaturesService } from '../../../../common/experimental_features_service'; import { + RESPONSE_ACTION_AGENT_TYPE, RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP, RESPONSE_ACTION_API_COMMANDS_NAMES, RESPONSE_ACTION_STATUS, @@ -20,8 +23,7 @@ import { type ResponseActionStatus, } from '../../../../../common/endpoint/service/response_actions/constants'; import type { DateRangePickerValues } from './actions_log_date_range_picker'; -import type { FILTER_NAMES } from '../translations'; -import { FILTER_TYPE_OPTIONS, UX_MESSAGES } from '../translations'; +import { FILTER_NAMES, FILTER_TYPE_OPTIONS, UX_MESSAGES } from '../translations'; import { ResponseActionStatusBadge } from './response_action_status_badge'; import { useActionHistoryUrlParams } from './use_action_history_url_params'; import { useGetEndpointsList } from '../../../hooks/endpoint/use_get_endpoints_list'; @@ -120,10 +122,11 @@ export const useDateRangePicker = (isFlyout: boolean) => { }; export type FilterItems = Array<{ - key: string; + key?: string; label: string; - checked: 'on' | undefined; - 'data-test-subj': string; + isGroupLabel?: boolean; + checked?: 'on' | undefined; + 'data-test-subj'?: string; }>; export const getActionStatus = (status: ResponseActionStatus): string => { @@ -138,15 +141,84 @@ export const getActionStatus = (status: ResponseActionStatus): string => { }; export type FilterName = keyof typeof FILTER_NAMES; +// maps filter name to a function that updates the query state +export type TypesFilters = { + [k in Extract]: { + onChangeFilterOptions: (selectedOptions: string[]) => void; + }; +}; + +export type ActionsLogPopupFilters = Extract< + FilterName, + 'actions' | 'hosts' | 'statuses' | 'types' +>; + +/** + * + * @param isSentinelOneV1Enabled + * @param isFlyout + * @param agentTypes + * @param types + * @returns FilterItems + * @description + * sets the initial state of the types filter options + */ +const getTypesFilterInitialState = ( + isSentinelOneV1Enabled: boolean, + isFlyout: boolean, + agentTypes?: string[], + types?: string[] +): FilterItems => { + const getFilterOptions = ({ key, label, checked }: FilterItems[number]): FilterItems[number] => ({ + key, + label, + isGroupLabel: false, + checked, + 'data-test-subj': `types-filter-option`, + }); + + // action types filter options + const defaultFilterOptions = RESPONSE_ACTION_TYPE.map((type) => + getFilterOptions({ + key: type, + label: getTypeDisplayName(type), + checked: !isFlyout && types?.includes(type) ? 'on' : undefined, + }) + ); + + // v8.13 onwards + // for showing agent types and action types in the same filter + if (isSentinelOneV1Enabled) { + return [ + { + label: FILTER_NAMES.agentTypes, + isGroupLabel: true, + }, + ...RESPONSE_ACTION_AGENT_TYPE.map((type) => + getFilterOptions({ + key: type, + label: getAgentTypeName(type), + checked: !isFlyout && agentTypes?.includes(type) ? 'on' : undefined, + }) + ), + { + label: FILTER_NAMES.actionTypes, + isGroupLabel: true, + }, + ...defaultFilterOptions, + ]; + } + + return defaultFilterOptions; +}; + export const useActionsLogFilter = ({ filterName, isFlyout, - isPopoverOpen, searchString, }: { - filterName: FilterName; + filterName: ActionsLogPopupFilters; isFlyout: boolean; - isPopoverOpen: boolean; searchString: string; }): { areHostsSelectedOnMount: boolean; @@ -160,9 +232,16 @@ export const useActionsLogFilter = ({ setUrlActionsFilters: ReturnType['setUrlActionsFilters']; setUrlHostsFilters: ReturnType['setUrlHostsFilters']; setUrlStatusesFilters: ReturnType['setUrlStatusesFilters']; + setUrlTypesFilters: ReturnType['setUrlTypesFilters']; + // TODO: remove this when `responseActionsSentinelOneV1Enabled` is enabled and removed setUrlTypeFilters: ReturnType['setUrlTypeFilters']; } => { + const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled( + 'responseActionsSentinelOneV1Enabled' + ); + const { + agentTypes = [], commands, statuses, hosts: selectedAgentIdsFromUrl, @@ -170,11 +249,12 @@ export const useActionsLogFilter = ({ setUrlActionsFilters, setUrlHostsFilters, setUrlStatusesFilters, + setUrlTypesFilters, setUrlTypeFilters, } = useActionHistoryUrlParams(); const isStatusesFilter = filterName === 'statuses'; const isHostsFilter = filterName === 'hosts'; - const isTypeFilter = filterName === 'type'; + const isTypesFilter = filterName === 'types'; const { data: endpointsList, isFetching } = useGetEndpointsList({ searchString, selectedAgentIds: selectedAgentIdsFromUrl, @@ -193,13 +273,8 @@ export const useActionsLogFilter = ({ // filter options const [items, setItems] = useState( - isTypeFilter - ? RESPONSE_ACTION_TYPE.map((type) => ({ - key: type, - label: getTypeDisplayName(type), - checked: !isFlyout && types?.includes(type) ? 'on' : undefined, - 'data-test-subj': `${filterName}-filter-option`, - })) + isTypesFilter + ? getTypesFilterInitialState(isSentinelOneV1Enabled, isFlyout, agentTypes, types) : isStatusesFilter ? RESPONSE_ACTION_STATUS.map((statusName) => ({ key: statusName, @@ -276,6 +351,7 @@ export const useActionsLogFilter = ({ setUrlHostsFilters, setUrlStatusesFilters, setUrlTypeFilters, + setUrlTypesFilters, }; }; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.test.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.test.ts index cd5655c7b96ea..8fedb85d06d0c 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.test.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.test.ts @@ -5,53 +5,73 @@ * 2.0. */ import { actionsLogFiltersFromUrlParams } from './use_action_history_url_params'; -import type { ConsoleResponseActionCommands } from '../../../../../common/endpoint/service/response_actions/constants'; -import { CONSOLE_RESPONSE_ACTION_COMMANDS } from '../../../../../common/endpoint/service/response_actions/constants'; -describe('#actionsLogFiltersFromUrlParams', () => { - const getConsoleCommandsAsString = (): string => { - return [...CONSOLE_RESPONSE_ACTION_COMMANDS].sort().join(','); - }; +import { + CONSOLE_RESPONSE_ACTION_COMMANDS, + RESPONSE_ACTION_AGENT_TYPE, + RESPONSE_ACTION_TYPE, + type ConsoleResponseActionCommands, + type ResponseActionAgentType, + type ResponseActionType, +} from '../../../../../common/endpoint/service/response_actions/constants'; +describe('#actionsLogFiltersFromUrlParams', () => { const getConsoleCommandsAsArray = (): ConsoleResponseActionCommands[] => { return [...CONSOLE_RESPONSE_ACTION_COMMANDS].sort(); }; - it('should not use invalid command values from URL params', () => { - expect(actionsLogFiltersFromUrlParams({ commands: 'asa,was' })).toEqual({ - commands: undefined, - endDate: undefined, - hosts: undefined, - startDate: undefined, - statuses: undefined, - users: undefined, + const getActionTypesAsArray = (): ResponseActionType[] => { + return [...RESPONSE_ACTION_TYPE].sort(); + }; + + const getAgentTypesAsArray = (): ResponseActionAgentType[] => { + return [...RESPONSE_ACTION_AGENT_TYPE].sort(); + }; + + it('should not use invalid `agentType` values from URL params', () => { + expect(actionsLogFiltersFromUrlParams({ agentTypes: 'asa,was' })).toEqual({}); + }); + + it('should use valid `agentTypes` values from URL params', () => { + expect( + actionsLogFiltersFromUrlParams({ + agentTypes: getAgentTypesAsArray().join(), + }) + ).toEqual({ + agentTypes: getAgentTypesAsArray(), }); }); + it('should not use invalid `types` values from URL params', () => { + expect(actionsLogFiltersFromUrlParams({ types: 'asa,was' })).toEqual({}); + }); + + it('should use valid `types` values from URL params', () => { + expect( + actionsLogFiltersFromUrlParams({ + types: getActionTypesAsArray().join(), + }) + ).toEqual({ + types: getActionTypesAsArray(), + }); + }); + + it('should not use invalid command values from URL params', () => { + expect(actionsLogFiltersFromUrlParams({ commands: 'asa,was' })).toEqual({}); + }); + it('should use valid command values from URL params', () => { expect( actionsLogFiltersFromUrlParams({ - commands: getConsoleCommandsAsString(), + commands: getConsoleCommandsAsArray().join(), }) ).toEqual({ commands: getConsoleCommandsAsArray(), - endDate: undefined, - hosts: undefined, - startDate: undefined, - statuses: undefined, - users: undefined, }); }); it('should not use invalid status values from URL params', () => { - expect(actionsLogFiltersFromUrlParams({ statuses: 'asa,was' })).toEqual({ - commands: undefined, - endDate: undefined, - hosts: undefined, - startDate: undefined, - statuses: undefined, - users: undefined, - }); + expect(actionsLogFiltersFromUrlParams({ statuses: 'asa,was' })).toEqual({}); }); it('should use valid status values from URL params', () => { @@ -60,19 +80,14 @@ describe('#actionsLogFiltersFromUrlParams', () => { statuses: 'successful,pending,failed', }) ).toEqual({ - commands: undefined, - endDate: undefined, - hosts: undefined, - startDate: undefined, statuses: ['failed', 'pending', 'successful'], - users: undefined, }); }); it('should use valid command and status along with given host, user and date values from URL params', () => { expect( actionsLogFiltersFromUrlParams({ - commands: getConsoleCommandsAsString(), + commands: getConsoleCommandsAsArray().join(), statuses: 'successful,pending,failed', hosts: 'host-1,host-2', users: 'user-1,user-2', @@ -96,12 +111,8 @@ describe('#actionsLogFiltersFromUrlParams', () => { endDate: 'now', }) ).toEqual({ - commands: undefined, endDate: 'now', - hosts: undefined, startDate: 'now-24h/h', - statuses: undefined, - users: undefined, }); }); @@ -112,12 +123,8 @@ describe('#actionsLogFiltersFromUrlParams', () => { endDate: '2022-09-12T08:30:33.140Z', }) ).toEqual({ - commands: undefined, endDate: '2022-09-12T08:30:33.140Z', - hosts: undefined, startDate: '2022-09-12T08:00:00.000Z', - statuses: undefined, - users: undefined, }); }); @@ -127,12 +134,7 @@ describe('#actionsLogFiltersFromUrlParams', () => { hosts: 'agent-id-1,agent-id-2', }) ).toEqual({ - commands: undefined, - endDate: undefined, hosts: ['agent-id-1', 'agent-id-2'], - startDate: undefined, - statuses: undefined, - users: undefined, }); }); @@ -142,11 +144,6 @@ describe('#actionsLogFiltersFromUrlParams', () => { users: 'usernameA,usernameB', }) ).toEqual({ - commands: undefined, - endDate: undefined, - hosts: undefined, - startDate: undefined, - statuses: undefined, users: ['usernameA', 'usernameB'], }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.ts index 58483bee39333..2d7a0ee932af5 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/components/use_action_history_url_params.ts @@ -6,10 +6,17 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import type { ConsoleResponseActionCommands } from '../../../../../common/endpoint/service/response_actions/constants'; + +import { + isActionType, + isAgentType, +} from '../../../../../common/endpoint/service/response_actions/type_guards'; +import type { ResponseActionType } from '../../../../../common/endpoint/service/response_actions/constants'; import { + type ConsoleResponseActionCommands, RESPONSE_ACTION_API_COMMANDS_NAMES, RESPONSE_ACTION_STATUS, + type ResponseActionAgentType, type ResponseActionsApiCommandNames, type ResponseActionStatus, } from '../../../../../common/endpoint/service/response_actions/constants'; @@ -17,6 +24,7 @@ import { useUrlParams } from '../../../hooks/use_url_params'; import { DEFAULT_DATE_RANGE_OPTIONS } from './hooks'; interface UrlParamsActionsLogFilters { + agentTypes: string; commands: string; hosts: string; statuses: string; @@ -28,6 +36,7 @@ interface UrlParamsActionsLogFilters { } interface ActionsLogFiltersFromUrlParams { + agentTypes?: ResponseActionAgentType[]; commands?: ConsoleResponseActionCommands[]; hosts?: string[]; withOutputs?: string[]; @@ -41,19 +50,37 @@ interface ActionsLogFiltersFromUrlParams { setUrlStatusesFilters: (statuses: UrlParamsActionsLogFilters['statuses']) => void; setUrlUsersFilters: (users: UrlParamsActionsLogFilters['users']) => void; setUrlWithOutputs: (outputs: UrlParamsActionsLogFilters['withOutputs']) => void; - setUrlTypeFilters: (outputs: UrlParamsActionsLogFilters['types']) => void; + // TODO: erase this function + // once we enable and remove responseActionsSentinelOneV1Enabled + setUrlTypeFilters: (actionTypes: UrlParamsActionsLogFilters['types']) => void; + setUrlTypesFilters: ({ + agentTypes, + actionTypes, + }: { + agentTypes: UrlParamsActionsLogFilters['agentTypes']; + actionTypes: UrlParamsActionsLogFilters['types']; + }) => void; users?: string[]; } type FiltersFromUrl = Pick< ActionsLogFiltersFromUrlParams, - 'commands' | 'hosts' | 'withOutputs' | 'statuses' | 'users' | 'startDate' | 'endDate' | 'types' + | 'agentTypes' + | 'commands' + | 'hosts' + | 'withOutputs' + | 'statuses' + | 'users' + | 'startDate' + | 'endDate' + | 'types' >; export const actionsLogFiltersFromUrlParams = ( urlParams: Partial ): FiltersFromUrl => { const actionsLogFilters: FiltersFromUrl = { + agentTypes: [], commands: [], hosts: [], statuses: [], @@ -64,6 +91,17 @@ export const actionsLogFiltersFromUrlParams = ( types: [], }; + const urlAgentTypes = urlParams.agentTypes + ? (String(urlParams.agentTypes).split(',') as ResponseActionAgentType[]).reduce< + ResponseActionAgentType[] + >((acc, curr) => { + if (isAgentType(curr)) { + acc.push(curr); + } + return acc.sort(); + }, []) + : []; + const urlCommands = urlParams.commands ? String(urlParams.commands) .split(',') @@ -80,7 +118,17 @@ export const actionsLogFiltersFromUrlParams = ( : []; const urlHosts = urlParams.hosts ? String(urlParams.hosts).split(',').sort() : []; - const urlTypes = urlParams.types ? String(urlParams.types).split(',').sort() : []; + const urlTypes = urlParams.types + ? (String(urlParams.types).split(',') as ResponseActionType[]).reduce( + (acc, curr) => { + if (isActionType(curr)) { + acc.push(curr); + } + return acc.sort(); + }, + [] + ) + : []; const urlWithOutputs = urlParams.withOutputs ? String(urlParams.withOutputs).split(',').sort() @@ -99,6 +147,7 @@ export const actionsLogFiltersFromUrlParams = ( const urlUsers = urlParams.users ? String(urlParams.users).split(',').sort() : []; + actionsLogFilters.agentTypes = urlAgentTypes.length ? urlAgentTypes : undefined; actionsLogFilters.commands = urlCommands.length ? urlCommands : undefined; actionsLogFilters.hosts = urlHosts.length ? urlHosts : undefined; actionsLogFilters.statuses = urlStatuses.length ? urlStatuses : undefined; @@ -174,13 +223,30 @@ export const useActionHistoryUrlParams = (): ActionsLogFiltersFromUrlParams => { }, [history, location, toUrlParams, urlParams] ); + + const setUrlTypesFilters = useCallback( + ({ agentTypes, actionTypes }: { agentTypes: string; actionTypes: string }) => { + history.push({ + ...location, + search: toUrlParams({ + ...urlParams, + agentTypes: agentTypes.length ? agentTypes : undefined, + types: actionTypes.length ? actionTypes : undefined, + }), + }); + }, + [history, location, toUrlParams, urlParams] + ); + + // TODO: erase this function + // once we enable responseActionsSentinelOneV1Enabled const setUrlTypeFilters = useCallback( - (types: string) => { + (actionTypes: string) => { history.push({ ...location, search: toUrlParams({ ...urlParams, - types: types.length ? types : undefined, + types: actionTypes.length ? actionTypes : undefined, }), }); }, @@ -232,5 +298,6 @@ export const useActionHistoryUrlParams = (): ActionsLogFiltersFromUrlParams => { setUrlStatusesFilters, setUrlUsersFilters, setUrlTypeFilters, + setUrlTypesFilters, }; }; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx index bc129f864330c..2b909e637bd20 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx @@ -28,8 +28,8 @@ import { getActionListMock } from '../mocks'; import { useGetEndpointsList } from '../../../hooks/endpoint/use_get_endpoints_list'; import { v4 as uuidv4 } from 'uuid'; import { - RESPONSE_ACTION_API_COMMANDS_NAMES, RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP, + RESPONSE_ACTION_API_COMMANDS_NAMES, } from '../../../../../common/endpoint/service/response_actions/constants'; import { useUserPrivileges as _useUserPrivileges } from '../../../../common/components/user_privileges'; import { responseActionsHttpMocks } from '../../../mocks/response_actions_http_mocks'; @@ -240,6 +240,7 @@ describe('Response actions history', () => { page: 1, pageSize: 10, agentIds: undefined, + agentTypes: [], commands: [], statuses: [], types: [], @@ -309,6 +310,7 @@ describe('Response actions history', () => { expect(useGetEndpointActionListMock).toHaveBeenLastCalledWith( { agentIds: undefined, + agentTypes: [], commands: [], endDate: 'now', page: 1, @@ -1160,6 +1162,7 @@ describe('Response actions history', () => { expect(useGetEndpointActionListMock).toHaveBeenLastCalledWith( { agentIds: undefined, + agentTypes: [], commands: [], endDate: 'now', page: 1, @@ -1362,6 +1365,7 @@ describe('Response actions history', () => { expect(useGetEndpointActionListMock).toHaveBeenLastCalledWith( { agentIds: ['id-0', 'id-2', 'id-4', 'id-6'], + agentTypes: [], commands: [], endDate: 'now', page: 1, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx index 8f482d062ce13..ff8908e1368ca 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/response_actions_log.tsx @@ -9,6 +9,8 @@ import type { CriteriaWithPagination } from '@elastic/eui'; import { EuiEmptyPrompt, EuiFlexItem } from '@elastic/eui'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import type { ResponseActionAgentType } from '../../../../common/endpoint/service/response_actions/constants'; import { RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP, type ResponseActionsApiCommandNames, @@ -48,17 +50,22 @@ export const ResponseActionsLog = memo< const { pagination: paginationFromUrlParams, setPagination: setPaginationOnUrlParams } = useUrlPagination(); const { + agentTypes: agentTypesFromUrl, commands: commandsFromUrl, hosts: agentIdsFromUrl, statuses: statusesFromUrl, users: usersFromUrl, - types: typesFromUrl, + types: actionTypesFromUrl, withOutputs: withOutputsFromUrl, setUrlWithOutputs, } = useActionHistoryUrlParams(); const getTestId = useTestIdGenerator(dataTestSubj); + const isSentinelOneV1Enabled = useIsExperimentalFeatureEnabled( + 'responseActionsSentinelOneV1Enabled' + ); + // Used to decide if display global loader or not (only the fist time tha page loads) const [isFirstAttempt, setIsFirstAttempt] = useState(true); @@ -66,6 +73,7 @@ export const ResponseActionsLog = memo< page: isFlyout ? 1 : paginationFromUrlParams.page, pageSize: isFlyout ? 10 : paginationFromUrlParams.pageSize, agentIds: isFlyout ? agentIds : agentIdsFromUrl?.length ? agentIdsFromUrl : agentIds, + agentTypes: [], commands: [], statuses: [], userIds: [], @@ -78,6 +86,11 @@ export const ResponseActionsLog = memo< if (!isFlyout) { setQueryParams((prevState) => ({ ...prevState, + agentTypes: isSentinelOneV1Enabled + ? agentTypesFromUrl?.length + ? agentTypesFromUrl + : prevState.agentTypes + : [], commands: commandsFromUrl?.length ? commandsFromUrl.map( (commandFromUrl) => RESPONSE_CONSOLE_COMMAND_TO_API_COMMAND_MAP[commandFromUrl] @@ -89,18 +102,22 @@ export const ResponseActionsLog = memo< : prevState.statuses, userIds: usersFromUrl?.length ? usersFromUrl : prevState.userIds, withOutputs: withOutputsFromUrl?.length ? withOutputsFromUrl : prevState.withOutputs, - types: typesFromUrl?.length ? (typesFromUrl as ResponseActionType[]) : prevState.types, + types: actionTypesFromUrl?.length + ? (actionTypesFromUrl as ResponseActionType[]) + : prevState.types, })); } }, [ + actionTypesFromUrl, + agentTypesFromUrl, commandsFromUrl, agentIdsFromUrl, isFlyout, + isSentinelOneV1Enabled, statusesFromUrl, setQueryParams, usersFromUrl, withOutputsFromUrl, - typesFromUrl, ]); // date range picker state and handlers @@ -176,6 +193,16 @@ export const ResponseActionsLog = memo< [setQueryParams] ); + const onChangeAgentTypesFilter = useCallback( + (selectedAgentTypes: string[]) => { + setQueryParams((prevState) => ({ + ...prevState, + agentTypes: selectedAgentTypes as ResponseActionAgentType[], + })); + }, + [setQueryParams] + ); + const onChangeTypeFilter = useCallback( (selectedTypes: string[]) => { setQueryParams((prevState) => ({ @@ -256,6 +283,7 @@ export const ResponseActionsLog = memo< onChangeCommandsFilter={onChangeCommandsFilter} onChangeStatusesFilter={onChangeStatusesFilter} onChangeUsersFilter={onChangeUsersFilter} + onChangeAgentTypesFilter={onChangeAgentTypesFilter} onChangeTypeFilter={onChangeTypeFilter} onRefresh={onRefresh} onRefreshChange={onRefreshChange} diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx index 2190e550d460e..907348790e92d 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/translations.tsx @@ -185,6 +185,15 @@ export const FILTER_NAMES = Object.freeze({ actions: i18n.translate('xpack.securitySolution.responseActionsList.list.filter.actions', { defaultMessage: 'Actions', }), + actionTypes: i18n.translate( + 'xpack.securitySolution.responseActionsList.list.filter.actionTypes', + { + defaultMessage: 'Action types', + } + ), + agentTypes: i18n.translate('xpack.securitySolution.responseActionsList.list.filter.agentTypes', { + defaultMessage: 'Agent types', + }), hosts: i18n.translate('xpack.securitySolution.responseActionsList.list.filter.Hosts', { defaultMessage: 'Hosts', }), @@ -194,9 +203,17 @@ export const FILTER_NAMES = Object.freeze({ users: i18n.translate('xpack.securitySolution.responseActionsList.list.filter.users', { defaultMessage: 'Filter by username', }), - type: i18n.translate('xpack.securitySolution.responseActionsList.list.filter.type', { - defaultMessage: 'Type', - }), + // TODO: change it to just a value instead of a function + // when responseActionsSentinelOneV1Enabled is enabled/removed + types: (suffix: string) => + i18n.translate('xpack.securitySolution.responseActionsList.list.filter.types', { + defaultMessage: `Type{suffix}`, + values: { suffix }, + }), + // replace above with: + // types: i18n.translate('xpack.securitySolution.responseActionsList.list.filter.types', { + // defaultMessage: 'Types', + // }), }); export const ARIA_LABELS = Object.freeze({ diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/history_log.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/history_log.cy.ts index cbf66d5b5cbbc..84d160c2e492a 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/history_log.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/automated_response_actions/history_log.cy.ts @@ -77,21 +77,21 @@ describe( cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength); }); - cy.getByTestSubj('response-actions-list-type-filter-popoverButton').click(); - cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click(); + cy.getByTestSubj('response-actions-list-types-filter-popoverButton').click(); + cy.getByTestSubj('types-filter-option').contains('Triggered by rule').click(); cy.getByTestSubj('response-actions-list').within(() => { cy.get('tbody .euiTableRow').should('have.lengthOf', 1); cy.get('tbody .euiTableRow').eq(0).contains('Triggered by rule'); }); - cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click(); + cy.getByTestSubj('types-filter-option').contains('Triggered by rule').click(); cy.getByTestSubj('response-actions-list').within(() => { cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength); }); - cy.getByTestSubj('type-filter-option').contains('Triggered manually').click(); + cy.getByTestSubj('types-filter-option').contains('Triggered manually').click(); cy.getByTestSubj('response-actions-list').within(() => { cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength - 1); }); - cy.getByTestSubj('type-filter-option').contains('Triggered by rule').click(); + cy.getByTestSubj('types-filter-option').contains('Triggered by rule').click(); cy.getByTestSubj('response-actions-list').within(() => { cy.get('tbody .euiTableRow').should('have.lengthOf', maxLength); cy.get('tbody .euiTableRow').eq(0).contains('Triggered by rule').click(); diff --git a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.ts b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.ts index 88371413598eb..459ecad26d7e4 100644 --- a/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.ts +++ b/x-pack/plugins/security_solution/public/management/hooks/response_actions/use_get_endpoint_action_list.ts @@ -6,8 +6,8 @@ */ import type { UseQueryOptions, UseQueryResult } from '@tanstack/react-query'; -import type { IHttpFetchError } from '@kbn/core-http-browser'; import { useQuery } from '@tanstack/react-query'; +import type { IHttpFetchError } from '@kbn/core-http-browser'; import type { EndpointActionListRequestQuery } from '../../../../common/api/endpoint'; import { useHttp } from '../../../common/lib/kibana'; import { BASE_ENDPOINT_ACTION_ROUTE } from '../../../../common/endpoint/constants'; @@ -41,6 +41,7 @@ export const useGetEndpointActionList = ( version: '2023-10-31', query: { agentIds: query.agentIds, + agentTypes: query.agentTypes, commands: query.commands, endDate: query.endDate, page: query.page, diff --git a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx index ea720f79a66c6..19ce46111d1c7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/response_actions/view/response_actions_list_page.test.tsx @@ -387,6 +387,59 @@ describe('Response actions history page', () => { // verify 5 rows that are expanded are the ones from before expect(expandedButtons).toEqual([0, 2, 3, 4, 5]); }); + + it('should read and set action type filter values using `types` URL params', () => { + const filterPrefix = 'types-filter'; + + reactTestingLibrary.act(() => { + history.push(`${MANAGEMENT_PATH}/response_actions_history?types=automated,manual`); + }); + + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + const selectedFilterOptions = allFilterOptions.reduce((acc, option) => { + if (option.getAttribute('aria-checked') === 'true') { + acc.push(option.textContent?.split('-')[0].trim() as string); + } + return acc; + }, []); + + expect(selectedFilterOptions.length).toEqual(2); + expect(selectedFilterOptions).toEqual([ + 'Triggered by rule. Checked option.', + 'Triggered manually. Checked option.', + ]); + expect(history.location.search).toEqual('?types=automated,manual'); + }); + + it('should read and set agent type filter values using `agentTypes` URL params', () => { + mockedContext.setExperimentalFlag({ + responseActionsSentinelOneV1Enabled: true, + }); + const filterPrefix = 'types-filter'; + reactTestingLibrary.act(() => { + history.push(`${MANAGEMENT_PATH}/response_actions_history?agentTypes=endpoint`); + }); + + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + const selectedFilterOptions = allFilterOptions.reduce((acc, option) => { + if (option.getAttribute('aria-checked') === 'true') { + acc.push(option.textContent?.split('-')[0].trim() as string); + } + return acc; + }, []); + + expect(selectedFilterOptions.length).toEqual(1); + expect(selectedFilterOptions).toEqual(['Endpoint. Checked option.']); + expect(history.location.search).toEqual('?agentTypes=endpoint'); + }); }); describe('Set selected/set values to URL params', () => { @@ -487,7 +540,7 @@ describe('Response actions history page', () => { expect(history.location.search).toEqual('?endDate=now&startDate=now-15m'); }); - it('should set actionIds using `withOutputs` to URL params ', async () => { + it('should set actionIds to URL params using `withOutputs`', async () => { const allActionIds = mockUseGetEndpointActionList.data?.data.map((action) => action.id) ?? []; const actionIdsWithDetails = allActionIds .reduce((acc, e, i) => { @@ -514,5 +567,153 @@ describe('Response actions history page', () => { // verify 2 rows are expanded and are the ones from before expect(history.location.search).toEqual(`?withOutputs=${actionIdsWithDetails}`); }); + + it('should set selected action type to URL params using `types`', () => { + const filterPrefix = 'types-filter'; + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + allFilterOptions.forEach((option) => { + option.style.pointerEvents = 'all'; + if (option.title.includes('Triggered')) { + userEvent.click(option); + } + }); + + expect(history.location.search).toEqual('?types=automated%2Cmanual'); + }); + + it('should set selected agent type filter options to URL params using `agentTypes`', () => { + mockedContext.setExperimentalFlag({ + responseActionsSentinelOneV1Enabled: true, + }); + const filterPrefix = 'types-filter'; + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + allFilterOptions.forEach((option) => { + option.style.pointerEvents = 'all'; + if (!option.title.includes('Triggered')) { + userEvent.click(option); + } + }); + + expect(history.location.search).toEqual('?agentTypes=endpoint%2Csentinel_one'); + }); + }); + + describe('Clear all selected options on a filter', () => { + it('should clear all selected options on `actions` filter', () => { + const filterPrefix = 'actions-filter'; + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + allFilterOptions.forEach((option) => { + option.style.pointerEvents = 'all'; + userEvent.click(option); + }); + + expect(history.location.search).toEqual( + '?commands=isolate%2Crelease%2Ckill-process%2Csuspend-process%2Cprocesses%2Cget-file%2Cexecute%2Cupload' + ); + + const clearAllButton = getByTestId(`${testPrefix}-${filterPrefix}-clearAllButton`); + clearAllButton.style.pointerEvents = 'all'; + userEvent.click(clearAllButton); + expect(history.location.search).toEqual(''); + }); + + it('should clear all selected options on `hosts` filter', () => { + const filterPrefix = 'hosts-filter'; + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + allFilterOptions.forEach((option) => { + option.style.pointerEvents = 'all'; + userEvent.click(option); + }); + + expect(history.location.search).toEqual( + '?hosts=agent-id-0%2Cagent-id-1%2Cagent-id-2%2Cagent-id-3%2Cagent-id-4%2Cagent-id-5%2Cagent-id-6%2Cagent-id-7%2Cagent-id-8' + ); + + const clearAllButton = getByTestId(`${testPrefix}-${filterPrefix}-clearAllButton`); + clearAllButton.style.pointerEvents = 'all'; + userEvent.click(clearAllButton); + expect(history.location.search).toEqual(''); + }); + + it('should clear all selected options on `statuses` filter', () => { + const filterPrefix = 'statuses-filter'; + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + allFilterOptions.forEach((option) => { + option.style.pointerEvents = 'all'; + userEvent.click(option); + }); + + expect(history.location.search).toEqual('?statuses=failed%2Cpending%2Csuccessful'); + + const clearAllButton = getByTestId(`${testPrefix}-${filterPrefix}-clearAllButton`); + clearAllButton.style.pointerEvents = 'all'; + userEvent.click(clearAllButton); + expect(history.location.search).toEqual(''); + }); + + it('should clear `actionTypes` selected options on `types` filter', () => { + const filterPrefix = 'types-filter'; + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + allFilterOptions.forEach((option) => { + option.style.pointerEvents = 'all'; + userEvent.click(option); + }); + + expect(history.location.search).toEqual('?types=automated%2Cmanual'); + + const clearAllButton = getByTestId(`${testPrefix}-${filterPrefix}-clearAllButton`); + clearAllButton.style.pointerEvents = 'all'; + userEvent.click(clearAllButton); + expect(history.location.search).toEqual(''); + }); + + it('should clear `agentTypes` and `actionTypes` selected options on `types` filter', () => { + mockedContext.setExperimentalFlag({ + responseActionsSentinelOneV1Enabled: true, + }); + const filterPrefix = 'types-filter'; + render(); + const { getAllByTestId, getByTestId } = renderResult; + userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`)); + const allFilterOptions = getAllByTestId(`${filterPrefix}-option`); + + allFilterOptions.forEach((option) => { + option.style.pointerEvents = 'all'; + userEvent.click(option); + }); + + expect(history.location.search).toEqual( + '?agentTypes=endpoint%2Csentinel_one&types=automated%2Cmanual' + ); + + const clearAllButton = getByTestId(`${testPrefix}-${filterPrefix}-clearAllButton`); + clearAllButton.style.pointerEvents = 'all'; + userEvent.click(clearAllButton); + expect(history.location.search).toEqual(''); + }); }); }); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3d784e40249b6..4c35a928b3f5d 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -35626,7 +35626,6 @@ "xpack.securitySolution.responseActionsList.list.filter.Hosts": "Hôtes", "xpack.securitySolution.responseActionsList.list.filter.manual": "Déclenché manuellement", "xpack.securitySolution.responseActionsList.list.filter.statuses": "Statuts", - "xpack.securitySolution.responseActionsList.list.filter.type": "Type", "xpack.securitySolution.responseActionsList.list.filter.users": "Filtrer par nom d'utilisateur", "xpack.securitySolution.responseActionsList.list.hosts": "Hôtes", "xpack.securitySolution.responseActionsList.list.item.badge.failed": "Échoué", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4dcd88dd9f4fe..6acca4e21b517 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -35626,7 +35626,6 @@ "xpack.securitySolution.responseActionsList.list.filter.Hosts": "ホスト", "xpack.securitySolution.responseActionsList.list.filter.manual": "手動でトリガー済み", "xpack.securitySolution.responseActionsList.list.filter.statuses": "ステータス", - "xpack.securitySolution.responseActionsList.list.filter.type": "型", "xpack.securitySolution.responseActionsList.list.filter.users": "ユーザー名でフィルター", "xpack.securitySolution.responseActionsList.list.hosts": "ホスト", "xpack.securitySolution.responseActionsList.list.item.badge.failed": "失敗", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 65228022a6acf..620655bf4f6ba 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -35608,7 +35608,6 @@ "xpack.securitySolution.responseActionsList.list.filter.Hosts": "主机", "xpack.securitySolution.responseActionsList.list.filter.manual": "已手动触发", "xpack.securitySolution.responseActionsList.list.filter.statuses": "状态", - "xpack.securitySolution.responseActionsList.list.filter.type": "类型", "xpack.securitySolution.responseActionsList.list.filter.users": "按用户名筛选", "xpack.securitySolution.responseActionsList.list.hosts": "主机", "xpack.securitySolution.responseActionsList.list.item.badge.failed": "失败",