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**
### 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": "失败",