diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index bae99649c2e01..245abdd49a62d 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -6,7 +6,6 @@ */ export * from './all'; -export * from './authentications'; export * from './common'; export * from './details'; export * from './first_last_seen'; @@ -15,8 +14,6 @@ export * from './overview'; export * from './uncommon_processes'; export enum HostsQueries { - authentications = 'authentications', - authenticationsEntities = 'authenticationsEntities', details = 'hostDetails', firstOrLastSeen = 'firstOrLastSeen', hosts = 'hosts', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 1902f0a0618ff..e59aec9e000d9 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -11,8 +11,6 @@ import { HostDetailsStrategyResponse, HostDetailsRequestOptions, HostsOverviewStrategyResponse, - HostAuthenticationsRequestOptions, - HostAuthenticationsStrategyResponse, HostOverviewRequestOptions, HostFirstLastSeenStrategyResponse, HostsQueries, @@ -88,12 +86,17 @@ import { TotalUsersKpiStrategyResponse, } from './users/kpi/total_users'; import { UsersRequestOptions, UsersStrategyResponse } from './users/all'; +import { + UserAuthenticationsRequestOptions, + UserAuthenticationsStrategyResponse, +} from './users/authentications'; export * from './cti'; export * from './hosts'; export * from './risk_score'; export * from './matrix_histogram'; export * from './network'; +export * from './users'; export type FactoryQueryTypes = | HostsQueries @@ -132,8 +135,6 @@ export type StrategyResponseType = T extends HostsQ ? HostDetailsStrategyResponse : T extends HostsQueries.overview ? HostsOverviewStrategyResponse - : T extends HostsQueries.authentications - ? HostAuthenticationsStrategyResponse : T extends HostsQueries.firstOrLastSeen ? HostFirstLastSeenStrategyResponse : T extends HostsQueries.uncommonProcesses @@ -148,6 +149,8 @@ export type StrategyResponseType = T extends HostsQ ? UserDetailsStrategyResponse : T extends UsersQueries.kpiTotalUsers ? TotalUsersKpiStrategyResponse + : T extends UsersQueries.authentications + ? UserAuthenticationsStrategyResponse : T extends UsersQueries.users ? UsersStrategyResponse : T extends NetworkQueries.details @@ -194,8 +197,6 @@ export type StrategyRequestType = T extends HostsQu ? HostDetailsRequestOptions : T extends HostsQueries.overview ? HostOverviewRequestOptions - : T extends HostsQueries.authentications - ? HostAuthenticationsRequestOptions : T extends HostsQueries.firstOrLastSeen ? HostFirstLastSeenRequestOptions : T extends HostsQueries.uncommonProcesses @@ -206,6 +207,8 @@ export type StrategyRequestType = T extends HostsQu ? HostsKpiHostsRequestOptions : T extends HostsKpiQueries.kpiUniqueIps ? HostsKpiUniqueIpsRequestOptions + : T extends UsersQueries.authentications + ? UserAuthenticationsRequestOptions : T extends UsersQueries.details ? UserDetailsRequestOptions : T extends UsersQueries.kpiTotalUsers diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts index ae1ace6ab9090..ef637031dd899 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts @@ -7,9 +7,8 @@ import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; -import { AuthenticationHit } from '../hosts'; import { Inspect, Maybe, TimerangeInput } from '../../common'; -import { RequestBasicOptions } from '../'; +import { AuthenticationHit, RequestBasicOptions } from '../'; import { AlertsGroupData } from './alerts'; import { AnomaliesActionGroupData, AnomalyHit } from './anomalies'; import { DnsHistogramGroupData } from './dns'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/authentications/index.ts similarity index 82% rename from x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts rename to x-pack/plugins/security_solution/common/search_strategy/security_solution/users/authentications/index.ts index 85255f51382fa..205169c8e6b0e 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/authentications/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/authentications/index.ts @@ -19,17 +19,23 @@ import { Hit, TotalHit, } from '../../../common'; -import { RequestOptionsPaginated } from '../../'; +import { RequestOptionsPaginated } from '../..'; -export interface HostAuthenticationsStrategyResponse extends IEsSearchResponse { +export interface UserAuthenticationsStrategyResponse extends IEsSearchResponse { edges: AuthenticationsEdges[]; totalCount: number; pageInfo: PageInfoPaginated; inspect?: Maybe; } -export interface HostAuthenticationsRequestOptions extends RequestOptionsPaginated { +export interface UserAuthenticationsRequestOptions extends RequestOptionsPaginated { defaultIndex: string[]; + stackByField: AuthStackByField; +} + +export enum AuthStackByField { + userName = 'user.name', + hostName = 'host.name', } export interface AuthenticationsEdges { @@ -41,7 +47,7 @@ export interface AuthenticationItem { _id: string; failures: number; successes: number; - user: UserEcs; + stackedValue: UserEcs['name'] | HostEcs['name']; lastSuccess?: Maybe; lastFailure?: Maybe; } @@ -58,7 +64,7 @@ export interface AuthenticationHit extends Hit { lastSuccess?: LastSourceHost; lastFailure?: LastSourceHost; }; - user: string; + stackedValue: string; failures: number; successes: number; cursor?: string; @@ -66,9 +72,7 @@ export interface AuthenticationHit extends Hit { } export interface AuthenticationBucket { - key: { - user_uid: string; - }; + key: string; doc_count: number; failures: { doc_count: number; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/details/index.ts index 43de36a50802d..2b74a1c251385 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/details/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/details/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import { Inspect, Maybe, TimerangeInput } from '../../../common'; @@ -23,7 +22,3 @@ export interface UserDetailsRequestOptions extends Partial timerange: TimerangeInput; inspect?: Maybe; } - -export interface AggregationRequest { - [aggField: string]: estypes.AggregationsAggregationContainer; -} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/index.ts index d7b50ded32039..7b3937de2913e 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/index.ts @@ -7,10 +7,18 @@ import { TotalUsersKpiStrategyResponse } from './kpi/total_users'; +export * from './all'; +export * from './common'; +export * from './kpi'; +export * from './details'; +export * from './authentications'; + export enum UsersQueries { details = 'userDetails', kpiTotalUsers = 'usersKpiTotalUsers', users = 'allUsers', + authentications = 'authentications', + authenticationsEntities = 'authenticationsEntities', } export type UserskKpiStrategyResponse = Omit; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/index.ts new file mode 100644 index 0000000000000..bc735313a7314 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/users/kpi/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './common'; +export * from './total_users'; diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/authentications.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/authentications.ts index 82509c041deba..eeddb79d345d2 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/authentications.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/authentications.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const AUTHENTICATIONS_TABLE = '[data-test-subj="table-authentications-loading-false"]'; +export const AUTHENTICATIONS_TABLE = '[data-test-subj="table-users-authentications-loading-false"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/users/user_authentications.ts b/x-pack/plugins/security_solution/cypress/screens/users/user_authentications.ts index 32340803c4eb1..2a91b5197b783 100644 --- a/x-pack/plugins/security_solution/cypress/screens/users/user_authentications.ts +++ b/x-pack/plugins/security_solution/cypress/screens/users/user_authentications.ts @@ -8,4 +8,4 @@ export const AUTHENTICATIONS_TAB = '[data-test-subj="navigation-authentications"]'; export const HEADER_SUBTITLE = '[data-test-subj="header-panel-subtitle"]'; export const USER_NAME_CELL = '[data-test-subj="render-content-user.name"]'; -export const AUTHENTICATIONS_TABLE = '[data-test-subj="table-authentications-loading-false"]'; +export const AUTHENTICATIONS_TABLE = '[data-test-subj="table-users-authentications-loading-false"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap new file mode 100644 index 0000000000000..b0a55bb72acb8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_host_table.test.tsx.snap @@ -0,0 +1,538 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Authentication Host Table Component rendering it renders the host authentication table 1`] = ` +.c2 { + margin-top: 8px; +} + +.c2 .siemSubtitle__item { + color: #7a7f89; + font-size: 12px; + line-height: 1.5; +} + +.c1 { + margin-bottom: 24px; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.c0 { + position: relative; +} + +.c3 tbody th, +.c3 tbody td { + vertical-align: top; +} + +.c3 tbody .euiTableCellContent { + display: block; +} + +.c4 { + margin-top: 4px; +} + +@media only screen and (min-width:575px) { + .c2 .siemSubtitle__item { + display: inline-block; + margin-right: 16px; + } + + .c2 .siemSubtitle__item:last-child { + margin-right: 0; + } +} + +
+
+
+
+
+
+
+
+ +
+
+

+ + Authentications + +

+
+
+
+

+ Showing: 0 users +

+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + + User + + + + + + Successes + + + + + + Failures + + + + + + Last success + + + + + + Last successful source + + + + + + Last successful destination + + + + + + Last failure + + + + + + Last failed source + + + + + + Last failed destination + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+`; diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_user_table.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_user_table.test.tsx.snap new file mode 100644 index 0000000000000..0796eebbe95fc --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/authentication/__snapshots__/authentications_user_table.test.tsx.snap @@ -0,0 +1,538 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Authentication User Table Component rendering it renders the user authentication table 1`] = ` +.c2 { + margin-top: 8px; +} + +.c2 .siemSubtitle__item { + color: #7a7f89; + font-size: 12px; + line-height: 1.5; +} + +.c1 { + margin-bottom: 24px; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.c0 { + position: relative; +} + +.c3 tbody th, +.c3 tbody td { + vertical-align: top; +} + +.c3 tbody .euiTableCellContent { + display: block; +} + +.c4 { + margin-top: 4px; +} + +@media only screen and (min-width:575px) { + .c2 .siemSubtitle__item { + display: inline-block; + margin-right: 16px; + } + + .c2 .siemSubtitle__item:last-child { + margin-right: 0; + } +} + +
+
+
+
+
+
+
+
+ +
+
+

+ + Authentications + +

+
+
+
+

+ Showing: 0 users +

+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + + User + + + + + + Successes + + + + + + Failures + + + + + + Last success + + + + + + Last successful source + + + + + + Last successful destination + + + + + + Last failure + + + + + + Last failed source + + + + + + Last failed destination + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+`; diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/authentications_host_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_host_table.test.tsx new file mode 100644 index 0000000000000..9138e35252597 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_host_table.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import '../../mock/match_media'; + +import * as i18n from './translations'; +import { AuthenticationsHostTable } from './authentications_host_table'; +import { hostsModel } from '../../../hosts/store'; +import { TestProviders } from '../../../common/mock'; +import { useAuthentications } from '../../../common/containers/authentications'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; + +jest.mock('../../../common/containers/query_toggle', () => ({ + useQueryToggle: jest.fn().mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }), +})); +jest.mock('../../containers/authentications', () => ({ + useAuthentications: jest.fn().mockReturnValue([ + false, + { + authentications: [], + totalCount: 0, + pageInfo: {}, + loadPage: jest.fn(), + inspect: {}, + isInspected: false, + refetch: jest.fn(), + }, + ]), +})); + +describe('Authentication Host Table Component', () => { + const mockUseAuthentications = useAuthentications as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + + const startDate = '2020-07-07T08:20:18.966Z'; + const endDate = '3000-01-01T00:00:00.000Z'; + const defaultProps = { + type: hostsModel.HostsType.page, + startDate, + endDate, + skip: false, + setQuery: jest.fn(), + indexNames: [], + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('rendering', () => { + test('it renders the host authentication table', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('authentications-host-table-loading-false')).toMatchSnapshot(); + }); + }); + + describe('columns', () => { + test('on hosts page, we expect to get all 9 columns', () => { + const { queryAllByRole, queryByText } = render( + + + + ); + + expect(queryAllByRole('columnheader').length).toEqual(9); + + // it should have Last Successful Destination column + expect(queryByText(i18n.LAST_SUCCESSFUL_DESTINATION)).toBeInTheDocument(); + // it should have Last Failed Destination column + expect(queryByText(i18n.LAST_FAILED_DESTINATION)).toBeInTheDocument(); + }); + + test('on hosts page, we expect to get 7 user details columns', () => { + const { queryAllByRole, queryByText } = render( + + + + ); + + expect(queryAllByRole('columnheader').length).toEqual(7); + + // it should not have Successful Destination column + expect(queryByText(i18n.LAST_SUCCESSFUL_DESTINATION)).not.toBeInTheDocument(); + // it should not have Failed Destination column + expect(queryByText(i18n.LAST_FAILED_DESTINATION)).not.toBeInTheDocument(); + }); + }); + + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(false); + }); + + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/authentications_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_host_table.tsx new file mode 100644 index 0000000000000..710b862570086 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_host_table.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useState } from 'react'; + +import { getOr } from 'lodash/fp'; +import { useDispatch } from 'react-redux'; +import { PaginatedTable } from '../paginated_table'; + +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; + +import * as i18n from './translations'; +import { + getHostDetailsAuthenticationColumns, + getHostsPageAuthenticationColumns, + rowItems, +} from './helpers'; +import { useAuthentications } from '../../containers/authentications'; +import { useQueryInspector } from '../page/manage_query'; +import { HostsComponentsQueryProps } from '../../../hosts/pages/navigation/types'; +import { hostsActions, hostsModel, hostsSelectors } from '../../../hosts/store'; +import { useQueryToggle } from '../../containers/query_toggle'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { AuthStackByField } from '../../../../common/search_strategy'; + +const TABLE_QUERY_ID = 'authenticationsHostsTableQuery'; + +const tableType = hostsModel.HostsTableType.authentications; + +const AuthenticationsHostTableComponent: React.FC = ({ + docValueFields, + endDate, + filterQuery, + indexNames, + skip, + startDate, + type, + setQuery, + deleteQuery, +}) => { + const usersEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); + const dispatch = useDispatch(); + const { toggleStatus } = useQueryToggle(TABLE_QUERY_ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + + const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); + const { activePage, limit } = useDeepEqualSelector((state) => + getAuthenticationsSelector(state, type) + ); + + const [ + loading, + { authentications, totalCount, pageInfo, loadPage, inspect, isInspected, refetch }, + ] = useAuthentications({ + docValueFields, + endDate, + filterQuery, + indexNames, + skip: querySkip, + startDate, + stackByField: AuthStackByField.userName, + activePage, + limit, + }); + + const columns = + type === hostsModel.HostsType.details + ? getHostDetailsAuthenticationColumns(usersEnabled) + : getHostsPageAuthenticationColumns(usersEnabled); + + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + hostsActions.updateTableLimit({ + hostsType: type, + limit: newLimit, + tableType, + }) + ), + [type, dispatch] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + hostsActions.updateTableActivePage({ + activePage: newPage, + hostsType: type, + tableType, + }) + ), + [type, dispatch] + ); + + useQueryInspector({ + queryId: TABLE_QUERY_ID, + loading, + refetch, + setQuery, + deleteQuery, + inspect, + }); + + return ( + + ); +}; + +AuthenticationsHostTableComponent.displayName = 'AuthenticationsHostTableComponent'; + +export const AuthenticationsHostTable = React.memo(AuthenticationsHostTableComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.test.tsx new file mode 100644 index 0000000000000..6be9c630a20bf --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import '../../mock/match_media'; + +import { TestProviders } from '../../../common/mock'; +import { useAuthentications } from '../../../common/containers/authentications'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { AuthenticationsUserTable } from './authentications_user_table'; +import { usersModel } from '../../../users/store'; + +jest.mock('../../../common/containers/query_toggle', () => ({ + useQueryToggle: jest.fn().mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }), +})); +jest.mock('../../containers/authentications', () => ({ + useAuthentications: jest.fn().mockReturnValue([ + false, + { + authentications: [], + totalCount: 0, + pageInfo: {}, + loadPage: jest.fn(), + inspect: {}, + isInspected: false, + refetch: jest.fn(), + }, + ]), +})); + +describe('Authentication User Table Component', () => { + const mockUseAuthentications = useAuthentications as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + + const startDate = '2020-07-07T08:20:18.966Z'; + const endDate = '3000-01-01T00:00:00.000Z'; + const defaultProps = { + type: usersModel.UsersType.page, + startDate, + endDate, + skip: false, + setQuery: jest.fn(), + indexNames: [], + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('rendering', () => { + test('it renders the user authentication table', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('table-users-authentications-loading-false')).toMatchSnapshot(); + }); + }); + + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(false); + }); + + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.tsx b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.tsx new file mode 100644 index 0000000000000..572c06ef4da90 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/authentication/authentications_user_table.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { getOr } from 'lodash/fp'; +import { useDispatch } from 'react-redux'; +import { AuthStackByField } from '../../../../common/search_strategy/security_solution/users/authentications'; +import { PaginatedTable } from '../paginated_table'; + +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; + +import * as i18n from './translations'; +import { getHostsPageAuthenticationColumns, rowItems } from './helpers'; +import { useAuthentications } from '../../containers/authentications'; +import { useQueryInspector } from '../page/manage_query'; +import { useQueryToggle } from '../../containers/query_toggle'; +import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { usersActions, usersModel, usersSelectors } from '../../../users/store'; +import { UsersComponentsQueryProps } from '../../../users/pages/navigation/types'; + +const TABLE_QUERY_ID = 'authenticationsUsersTableQuery'; + +const AuthenticationsUserTableComponent: React.FC = ({ + docValueFields, + endDate, + filterQuery, + indexNames, + skip, + startDate, + type, + setQuery, + deleteQuery, +}) => { + const usersEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); + + const dispatch = useDispatch(); + const { toggleStatus } = useQueryToggle(TABLE_QUERY_ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + + const getAuthenticationsSelector = useMemo(() => usersSelectors.authenticationsSelector(), []); + const { activePage, limit } = useDeepEqualSelector((state) => getAuthenticationsSelector(state)); + + const [ + loading, + { authentications, totalCount, pageInfo, loadPage, inspect, isInspected, refetch }, + ] = useAuthentications({ + docValueFields, + endDate, + filterQuery, + indexNames, + skip: querySkip, + startDate, + activePage, + limit, + stackByField: AuthStackByField.userName, + }); + + const columns = getHostsPageAuthenticationColumns(usersEnabled); + + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + usersActions.updateTableLimit({ + usersType: type, + limit: newLimit, + tableType: usersModel.UsersTableType.authentications, + }) + ), + [type, dispatch] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + usersActions.updateTableActivePage({ + activePage: newPage, + usersType: type, + tableType: usersModel.UsersTableType.authentications, + }) + ), + [type, dispatch] + ); + + useQueryInspector({ + queryId: TABLE_QUERY_ID, + loading, + refetch, + setQuery, + deleteQuery, + inspect, + }); + + return ( + + ); +}; + +AuthenticationsUserTableComponent.displayName = 'AuthenticationsUserTableComponent'; + +export const AuthenticationsUserTable = React.memo(AuthenticationsUserTableComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/authentication/helpers.tsx new file mode 100644 index 0000000000000..d541e0e452943 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/authentication/helpers.tsx @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { has } from 'lodash/fp'; +import React from 'react'; + +import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../drag_and_drop/helpers'; +import { getEmptyTagValue } from '../empty_value'; +import { FormattedRelativePreferenceDate } from '../formatted_date'; +import { Columns, ItemsPerRow } from '../paginated_table'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { getRowItemDraggables } from '../tables/helpers'; + +import * as i18n from './translations'; +import { HostDetailsLink, NetworkDetailsLink, UserDetailsLink } from '../links'; +import { AuthenticationsEdges } from '../../../../common/search_strategy'; +import { AuthTableColumns } from './types'; + +export const getHostDetailsAuthenticationColumns = (usersEnabled: boolean): AuthTableColumns => [ + getUserColumn(usersEnabled), + SUCCESS_COLUMN, + FAILURES_COLUMN, + LAST_SUCCESSFUL_TIME_COLUMN, + LAST_SUCCESSFUL_SOURCE_COLUMN, + LAST_FAILED_TIME_COLUMN, + LAST_FAILED_SOURCE_COLUMN, +]; + +export const getHostsPageAuthenticationColumns = (usersEnabled: boolean): AuthTableColumns => [ + getUserColumn(usersEnabled), + SUCCESS_COLUMN, + FAILURES_COLUMN, + LAST_SUCCESSFUL_TIME_COLUMN, + LAST_SUCCESSFUL_SOURCE_COLUMN, + LAST_SUCCESSFUL_DESTINATION_COLUMN, + LAST_FAILED_TIME_COLUMN, + LAST_FAILED_SOURCE_COLUMN, + LAST_FAILED_DESTINATION_COLUMN, +]; + +export const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +const FAILURES_COLUMN: Columns = { + name: i18n.FAILURES, + truncateText: false, + mobileOptions: { show: true }, + render: ({ node }) => { + const id = escapeDataProviderId(`authentications-table-${node._id}-failures-${node.failures}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + node.failures + ) + } + /> + ); + }, + width: '8%', +}; +const LAST_SUCCESSFUL_TIME_COLUMN: Columns = { + name: i18n.LAST_SUCCESSFUL_TIME, + truncateText: false, + mobileOptions: { show: true }, + render: ({ node }) => + has('lastSuccess.timestamp', node) && node.lastSuccess?.timestamp != null ? ( + + ) : ( + getEmptyTagValue() + ), +}; +const LAST_SUCCESSFUL_SOURCE_COLUMN: Columns = { + name: i18n.LAST_SUCCESSFUL_SOURCE, + truncateText: false, + mobileOptions: { show: true }, + render: ({ node }) => + getRowItemDraggables({ + rowItems: node.lastSuccess?.source?.ip || null, + attrName: 'source.ip', + idPrefix: `authentications-table-${node._id}-lastSuccessSource`, + render: (item) => , + }), +}; +const LAST_SUCCESSFUL_DESTINATION_COLUMN: Columns = { + name: i18n.LAST_SUCCESSFUL_DESTINATION, + truncateText: false, + mobileOptions: { show: true }, + render: ({ node }) => + getRowItemDraggables({ + rowItems: node.lastSuccess?.host?.name ?? null, + attrName: 'host.name', + idPrefix: `authentications-table-${node._id}-lastSuccessfulDestination`, + render: (item) => , + }), +}; +const LAST_FAILED_TIME_COLUMN: Columns = { + name: i18n.LAST_FAILED_TIME, + truncateText: false, + mobileOptions: { show: true }, + render: ({ node }) => + has('lastFailure.timestamp', node) && node.lastFailure?.timestamp != null ? ( + + ) : ( + getEmptyTagValue() + ), +}; +const LAST_FAILED_SOURCE_COLUMN: Columns = { + name: i18n.LAST_FAILED_SOURCE, + truncateText: false, + mobileOptions: { show: true }, + render: ({ node }) => + getRowItemDraggables({ + rowItems: node.lastFailure?.source?.ip || null, + attrName: 'source.ip', + idPrefix: `authentications-table-${node._id}-lastFailureSource`, + render: (item) => , + }), +}; +const LAST_FAILED_DESTINATION_COLUMN: Columns = { + name: i18n.LAST_FAILED_DESTINATION, + truncateText: false, + mobileOptions: { show: true }, + render: ({ node }) => + getRowItemDraggables({ + rowItems: node.lastFailure?.host?.name || null, + attrName: 'host.name', + idPrefix: `authentications-table-${node._id}-lastFailureDestination`, + render: (item) => , + }), +}; + +const getUserColumn = ( + usersEnabled: boolean +): Columns => ({ + name: i18n.USER, + truncateText: false, + mobileOptions: { show: true }, + render: ({ node }) => + getRowItemDraggables({ + rowItems: node.stackedValue, + attrName: 'user.name', + idPrefix: `authentications-table-${node._id}-userName`, + render: (item) => (usersEnabled ? : <>{item}), + }), +}); + +const SUCCESS_COLUMN: Columns = { + name: i18n.SUCCESSES, + truncateText: false, + mobileOptions: { show: true }, + render: ({ node }) => { + const id = escapeDataProviderId( + `authentications-table-${node._id}-node-successes-${node.successes}` + ); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + node.successes + ) + } + /> + ); + }, + width: '8%', +}; diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/translations.ts b/x-pack/plugins/security_solution/public/common/components/authentication/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/hosts/components/authentications_table/translations.ts rename to x-pack/plugins/security_solution/public/common/components/authentication/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/authentication/types.ts b/x-pack/plugins/security_solution/public/common/components/authentication/types.ts new file mode 100644 index 0000000000000..c686263fa8d12 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/authentication/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AuthenticationsEdges } from '../../../../common/search_strategy'; +import { Columns } from '../paginated_table'; + +export type AuthTableColumns = + | [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns + ] + | [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns + ]; diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 3b27dc0fcfac6..fc2fe1ac5b361 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -25,7 +25,6 @@ import styled from 'styled-components'; import { Direction } from '../../../../common/search_strategy'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; -import { AuthTableColumns } from '../../../hosts/components/authentications_table'; import { HostsTableColumns } from '../../../hosts/components/hosts_table'; import { NetworkDnsColumns } from '../../../network/components/network_dns_table/columns'; import { NetworkHttpColumns } from '../../../network/components/network_http_table/columns'; @@ -51,6 +50,7 @@ import { Panel } from '../panel'; import { InspectButtonContainer } from '../inspect'; import { useQueryToggle } from '../../containers/query_toggle'; import { UsersTableColumns } from '../../../users/components/all_users'; +import { AuthTableColumns } from '../authentication/types'; const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/authentications/index.test.tsx similarity index 82% rename from x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx rename to x-pack/plugins/security_solution/public/common/containers/authentications/index.test.tsx index 1f6ee4cb276ec..cf7953bb8922c 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/authentications/index.test.tsx @@ -6,9 +6,10 @@ */ import { act, renderHook } from '@testing-library/react-hooks'; +import { AuthStackByField } from '../../../../common/search_strategy'; import { TestProviders } from '../../../common/mock'; +import { HostsType } from '../../../hosts/store/model'; import { useAuthentications } from './index'; -import { HostsType } from '../../store/model'; describe('authentications', () => { it('skip = true will cancel any running request', () => { @@ -19,6 +20,9 @@ describe('authentications', () => { indexNames: ['cool'], type: HostsType.page, skip: false, + stackByField: AuthStackByField.hostName, + activePage: 0, + limit: 10, }; const { rerender } = renderHook(() => useAuthentications(localProps), { wrapper: TestProviders, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/common/containers/authentications/index.tsx similarity index 85% rename from x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx rename to x-pack/plugins/security_solution/public/common/containers/authentications/index.tsx index 1ff27e4b29917..593d5e4186e29 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/authentications/index.tsx @@ -5,24 +5,22 @@ * 2.0. */ -import { noop, pick } from 'lodash/fp'; +import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; import { Subscription } from 'rxjs'; import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; -import { HostsQueries } from '../../../../common/search_strategy/security_solution'; import { - HostAuthenticationsRequestOptions, - HostAuthenticationsStrategyResponse, AuthenticationsEdges, - PageInfoPaginated, - DocValueFields, - SortField, -} from '../../../../common/search_strategy'; + AuthStackByField, + UserAuthenticationsRequestOptions, + UserAuthenticationsStrategyResponse, + UsersQueries, +} from '../../../../common/search_strategy/security_solution'; +import { PageInfoPaginated, DocValueFields, SortField } from '../../../../common/search_strategy'; import { ESTermQuery } from '../../../../common/typed_json'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { inputsModel } from '../../../common/store'; import { createFilter } from '../../../common/containers/helpers'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; @@ -30,17 +28,12 @@ import { useKibana } from '../../../common/lib/kibana'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; -import { hostsModel, hostsSelectors } from '../../store'; - import * as i18n from './translations'; import { useTransforms } from '../../../transforms/containers/use_transforms'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -export const ID = 'hostsAuthenticationsQuery'; - export interface AuthenticationArgs { authentications: AuthenticationsEdges[]; - id: string; inspect: InspectResponse; isInspected: boolean; loading: boolean; @@ -56,8 +49,10 @@ interface UseAuthentications { endDate: string; indexNames: string[]; startDate: string; - type: hostsModel.HostsType; skip: boolean; + stackByField: AuthStackByField; + activePage: number; + limit: number; } export const useAuthentications = ({ @@ -66,20 +61,18 @@ export const useAuthentications = ({ endDate, indexNames, startDate, - type, + activePage, + limit, skip, + stackByField, }: UseAuthentications): [boolean, AuthenticationArgs] => { - const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); - const { activePage, limit } = useDeepEqualSelector((state) => - pick(['activePage', 'limit'], getAuthenticationsSelector(state, type)) - ); const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const searchSubscription$ = useRef(new Subscription()); const [loading, setLoading] = useState(false); const [authenticationsRequest, setAuthenticationsRequest] = - useState(null); + useState(null); const { getTransformChangesIfTheyExist } = useTransforms(); const { addError, addWarning } = useAppToasts(); @@ -101,7 +94,6 @@ export const useAuthentications = ({ const [authenticationsResponse, setAuthenticationsResponse] = useState({ authentications: [], - id: ID, inspect: { dsl: [], response: [], @@ -119,7 +111,7 @@ export const useAuthentications = ({ }); const authenticationsSearch = useCallback( - (request: HostAuthenticationsRequestOptions | null) => { + (request: UserAuthenticationsRequestOptions | null) => { if (request == null || skip) { return; } @@ -128,7 +120,7 @@ export const useAuthentications = ({ setLoading(true); searchSubscription$.current = data.search - .search(request, { + .search(request, { strategy: 'securitySolutionSearchStrategy', abortSignal: abortCtrl.current.signal, }) @@ -171,7 +163,7 @@ export const useAuthentications = ({ useEffect(() => { setAuthenticationsRequest((prevRequest) => { const { indices, factoryQueryType, timerange } = getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: UsersQueries.authentications, indices: indexNames, filterQuery, timerange: { @@ -187,6 +179,7 @@ export const useAuthentications = ({ docValueFields: docValueFields ?? [], factoryQueryType, filterQuery: createFilter(filterQuery), + stackByField, pagination: generateTablePaginationOptions(activePage, limit), timerange, sort: {} as SortField, @@ -202,6 +195,7 @@ export const useAuthentications = ({ endDate, filterQuery, indexNames, + stackByField, limit, startDate, getTransformChangesIfTheyExist, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/translations.ts b/x-pack/plugins/security_solution/public/common/containers/authentications/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/hosts/containers/authentications/translations.ts rename to x-pack/plugins/security_solution/public/common/containers/authentications/translations.ts diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap deleted file mode 100644 index bffd5e2261ad9..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,113 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Authentication Table Component rendering it renders the authentication table 1`] = ` - -`; diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx deleted file mode 100644 index 2ec333e335639..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import '../../../common/mock/match_media'; -import { - mockGlobalState, - SUB_PLUGINS_REDUCER, - kibanaObservable, - createSecuritySolutionStorageMock, -} from '../../../common/mock'; -import { createStore, State } from '../../../common/store'; -import { hostsModel } from '../../store'; -import { mockData } from './mock'; -import * as i18n from './translations'; -import { AuthenticationTable, getAuthenticationColumnsCurated } from '.'; - -describe('Authentication Table Component', () => { - const loadPage = jest.fn(); - const state: State = mockGlobalState; - - const { storage } = createSecuritySolutionStorageMock(); - let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - }); - - describe('rendering', () => { - test('it renders the authentication table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Memo(AuthenticationTableComponent)')).toMatchSnapshot(); - }); - }); - - describe('columns', () => { - test('on hosts page, we expect to get all columns', () => { - expect(getAuthenticationColumnsCurated(hostsModel.HostsType.page, false).length).toEqual(9); - }); - - test('on host details page, we expect to remove two columns', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details, false); - expect(columns.length).toEqual(7); - }); - - test('on host details page, we should have Last Failed Destination column', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.page, false); - expect(columns.some((col) => col.name === i18n.LAST_FAILED_DESTINATION)).toEqual(true); - }); - - test('on host details page, we should not have Last Failed Destination column', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details, false); - expect(columns.some((col) => col.name === i18n.LAST_FAILED_DESTINATION)).toEqual(false); - }); - - test('on host page, we should have Last Successful Destination column', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.page, false); - expect(columns.some((col) => col.name === i18n.LAST_SUCCESSFUL_DESTINATION)).toEqual(true); - }); - - test('on host details page, we should not have Last Successful Destination column', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details, false); - expect(columns.some((col) => col.name === i18n.LAST_SUCCESSFUL_DESTINATION)).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx deleted file mode 100644 index 2bbda82e15315..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { has } from 'lodash/fp'; -import React, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; - -import { AuthenticationsEdges } from '../../../../common/search_strategy/security_solution/hosts/authentications'; - -import { - DragEffects, - DraggableWrapper, -} from '../../../common/components/drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; -import { getEmptyTagValue } from '../../../common/components/empty_value'; -import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; -import { - HostDetailsLink, - NetworkDetailsLink, - UserDetailsLink, -} from '../../../common/components/links'; -import { Columns, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; -import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; -import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; -import { getRowItemDraggables } from '../../../common/components/tables/helpers'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; - -import { hostsActions, hostsModel, hostsSelectors } from '../../store'; - -import * as i18n from './translations'; - -const tableType = hostsModel.HostsTableType.authentications; - -interface AuthenticationTableProps { - data: AuthenticationsEdges[]; - fakeTotalCount: number; - loading: boolean; - loadPage: (newActivePage: number) => void; - id: string; - isInspect: boolean; - setQuerySkip: (skip: boolean) => void; - showMorePagesIndicator: boolean; - totalCount: number; - type: hostsModel.HostsType; -} - -export type AuthTableColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns -]; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; - -const AuthenticationTableComponent: React.FC = ({ - data, - fakeTotalCount, - id, - isInspect, - loading, - loadPage, - setQuerySkip, - showMorePagesIndicator, - totalCount, - type, -}) => { - const dispatch = useDispatch(); - const getAuthenticationsSelector = useMemo(() => hostsSelectors.authenticationsSelector(), []); - const { activePage, limit } = useDeepEqualSelector((state) => - getAuthenticationsSelector(state, type) - ); - - const updateLimitPagination = useCallback( - (newLimit) => - dispatch( - hostsActions.updateTableLimit({ - hostsType: type, - limit: newLimit, - tableType, - }) - ), - [type, dispatch] - ); - - const updateActivePage = useCallback( - (newPage) => - dispatch( - hostsActions.updateTableActivePage({ - activePage: newPage, - hostsType: type, - tableType, - }) - ), - [type, dispatch] - ); - - const usersEnabled = useIsExperimentalFeatureEnabled('usersEnabled'); - const columns = useMemo( - () => getAuthenticationColumnsCurated(type, usersEnabled), - [type, usersEnabled] - ); - - return ( - - ); -}; - -AuthenticationTableComponent.displayName = 'AuthenticationTableComponent'; - -export const AuthenticationTable = React.memo(AuthenticationTableComponent); - -const getAuthenticationColumns = (usersEnabled: boolean): AuthTableColumns => [ - { - name: i18n.USER, - truncateText: false, - mobileOptions: { show: true }, - render: ({ node }) => - getRowItemDraggables({ - rowItems: node.user.name, - attrName: 'user.name', - idPrefix: `authentications-table-${node._id}-userName`, - render: (item) => (usersEnabled ? : <>{item}), - }), - }, - { - name: i18n.SUCCESSES, - truncateText: false, - mobileOptions: { show: true }, - render: ({ node }) => { - const id = escapeDataProviderId( - `authentications-table-${node._id}-node-successes-${node.successes}` - ); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - node.successes - ) - } - /> - ); - }, - width: '8%', - }, - { - name: i18n.FAILURES, - truncateText: false, - mobileOptions: { show: true }, - render: ({ node }) => { - const id = escapeDataProviderId( - `authentications-table-${node._id}-failures-${node.failures}` - ); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - node.failures - ) - } - /> - ); - }, - width: '8%', - }, - { - name: i18n.LAST_SUCCESSFUL_TIME, - truncateText: false, - mobileOptions: { show: true }, - render: ({ node }) => - has('lastSuccess.timestamp', node) && node.lastSuccess?.timestamp != null ? ( - - ) : ( - getEmptyTagValue() - ), - }, - { - name: i18n.LAST_SUCCESSFUL_SOURCE, - truncateText: false, - mobileOptions: { show: true }, - render: ({ node }) => - getRowItemDraggables({ - rowItems: node.lastSuccess?.source?.ip || null, - attrName: 'source.ip', - idPrefix: `authentications-table-${node._id}-lastSuccessSource`, - render: (item) => , - }), - }, - { - name: i18n.LAST_SUCCESSFUL_DESTINATION, - truncateText: false, - mobileOptions: { show: true }, - render: ({ node }) => - getRowItemDraggables({ - rowItems: node.lastSuccess?.host?.name ?? null, - attrName: 'host.name', - idPrefix: `authentications-table-${node._id}-lastSuccessfulDestination`, - render: (item) => , - }), - }, - { - name: i18n.LAST_FAILED_TIME, - truncateText: false, - mobileOptions: { show: true }, - render: ({ node }) => - has('lastFailure.timestamp', node) && node.lastFailure?.timestamp != null ? ( - - ) : ( - getEmptyTagValue() - ), - }, - { - name: i18n.LAST_FAILED_SOURCE, - truncateText: false, - mobileOptions: { show: true }, - render: ({ node }) => - getRowItemDraggables({ - rowItems: node.lastFailure?.source?.ip || null, - attrName: 'source.ip', - idPrefix: `authentications-table-${node._id}-lastFailureSource`, - render: (item) => , - }), - }, - { - name: i18n.LAST_FAILED_DESTINATION, - truncateText: false, - mobileOptions: { show: true }, - render: ({ node }) => - getRowItemDraggables({ - rowItems: node.lastFailure?.host?.name || null, - attrName: 'host.name', - idPrefix: `authentications-table-${node._id}-lastFailureDestination`, - render: (item) => , - }), - }, -]; - -export const getAuthenticationColumnsCurated = ( - pageType: hostsModel.HostsType, - usersEnabled: boolean -): AuthTableColumns => { - const columns = getAuthenticationColumns(usersEnabled); - - // Columns to exclude from host details pages - if (pageType === hostsModel.HostsType.details) { - return [i18n.LAST_FAILED_DESTINATION, i18n.LAST_SUCCESSFUL_DESTINATION].reduce((acc, name) => { - acc.splice( - acc.findIndex((column) => column.name === name), - 1 - ); - return acc; - }, columns); - } - - return columns; -}; diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts deleted file mode 100644 index caf441b34ca90..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/mock.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { HostAuthenticationsStrategyResponse } from '../../../../common/search_strategy/security_solution/hosts/authentications'; - -export const mockData: { Authentications: HostAuthenticationsStrategyResponse } = { - Authentications: { - rawResponse: { - took: 880, - timed_out: false, - _shards: { - total: 26, - successful: 26, - skipped: 0, - failed: 0, - }, - hits: { - total: 2, - hits: [], - }, - aggregations: { - group_by_users: { - buckets: [ - { - key: 'SYSTEM', - doc_count: 4, - failures: { - doc_count: 0, - lastFailure: { hits: { total: 0, max_score: null, hits: [] } }, - hits: { total: 0, max_score: null, hits: [] }, - }, - successes: { - doc_count: 4, - lastSuccess: { hits: { total: 4, max_score: null } }, - }, - }, - ], - doc_count_error_upper_bound: -1, - sum_other_doc_count: 566, - }, - }, - } as estypes.SearchResponse, - totalCount: 54, - edges: [ - { - node: { - _id: 'cPsuhGcB0WOhS6qyTKC0', - failures: 10, - successes: 0, - user: { name: ['Evan Hassanabad'] }, - lastSuccess: { - timestamp: '2019-01-23T22:35:32.222Z', - source: { - ip: ['127.0.0.1'], - }, - host: { - id: ['host-id-1'], - name: ['host-1'], - }, - }, - lastFailure: { - timestamp: '2019-01-23T22:35:32.222Z', - source: { - ip: ['8.8.8.8'], - }, - host: { - id: ['host-id-1'], - name: ['host-2'], - }, - }, - }, - cursor: { - value: '98966fa2013c396155c460d35c0902be', - }, - }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - failures: 10, - successes: 0, - user: { name: ['Braden Hassanabad'] }, - lastSuccess: { - timestamp: '2019-01-23T22:35:32.222Z', - source: { - ip: ['127.0.0.1'], - }, - host: { - id: ['host-id-1'], - name: ['host-1'], - }, - }, - lastFailure: { - timestamp: '2019-01-23T22:35:32.222Z', - source: { - ip: ['8.8.8.8'], - }, - host: { - id: ['host-id-1'], - name: ['host-2'], - }, - }, - }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, - }, -}; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx index 42c8254ffd183..1326f24b5335f 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx @@ -8,7 +8,6 @@ import React, { useMemo, useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { assertUnreachable } from '../../../../common/utility_types'; import { Columns, Criteria, @@ -204,7 +203,6 @@ const getNodeField = (field: HostsFields): string => { case HostsFields.lastSeen: return 'node.lastSeen'; } - assertUnreachable(field); }; export const HostsTable = React.memo(HostsTableComponent); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx deleted file mode 100644 index 9d31b477a851a..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { TestProviders } from '../../../common/mock'; -import { useAuthentications } from '../../containers/authentications'; -import { useQueryToggle } from '../../../common/containers/query_toggle'; -import { AuthenticationsQueryTabBody } from './authentications_query_tab_body'; -import { HostsType } from '../../store/model'; - -jest.mock('../../containers/authentications'); -jest.mock('../../../common/containers/query_toggle'); -jest.mock('../../../common/lib/kibana'); - -describe('Authentications query tab body', () => { - const mockUseAuthentications = useAuthentications as jest.Mock; - const mockUseQueryToggle = useQueryToggle as jest.Mock; - const defaultProps = { - indexNames: [], - setQuery: jest.fn(), - skip: false, - startDate: '2019-06-25T04:31:59.345Z', - endDate: '2019-06-25T06:31:59.345Z', - type: HostsType.page, - }; - beforeEach(() => { - jest.clearAllMocks(); - mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); - mockUseAuthentications.mockReturnValue([ - false, - { - authentications: [], - id: '123', - inspect: { - dsl: [], - response: [], - }, - isInspected: false, - totalCount: 0, - pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, - loadPage: jest.fn(), - refetch: jest.fn(), - }, - ]); - }); - it('toggleStatus=true, do not skip', () => { - render( - - - - ); - expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(false); - }); - it('toggleStatus=false, skip', () => { - mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); - render( - - - - ); - expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(true); - }); -}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 1096085b93016..0f6de80343b11 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -5,11 +5,7 @@ * 2.0. */ -import { getOr } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; -import { AuthenticationTable } from '../../components/authentications_table'; -import { manageQuery } from '../../../common/components/page/manage_query'; -import { useAuthentications } from '../../containers/authentications'; +import React from 'react'; import { HostsComponentsQueryProps } from './types'; import { MatrixHistogramOption, @@ -22,11 +18,9 @@ import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { authenticationLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/hosts/authentication'; import { LensAttributes } from '../../../common/components/visualization_actions/types'; -import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { AuthenticationsHostTable } from '../../../common/components/authentication/authentications_host_table'; -const AuthenticationTableManage = manageQuery(AuthenticationTable); - -const ID = 'authenticationsHistogramQuery'; +const HISTOGRAM_QUERY_ID = 'authenticationsHistogramQuery'; const authenticationsStackByOptions: MatrixHistogramOption[] = [ { @@ -77,59 +71,28 @@ const AuthenticationsQueryTabBodyComponent: React.FC startDate, type, }) => { - const { toggleStatus } = useQueryToggle(ID); - const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); - useEffect(() => { - setQuerySkip(skip || !toggleStatus); - }, [skip, toggleStatus]); - const [ - loading, - { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, - ] = useAuthentications({ - docValueFields, - endDate, - filterQuery, - indexNames, - skip: querySkip, - startDate, - type, - }); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, [deleteQuery]); - return ( <> - ); diff --git a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes.test.ts b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes.test.ts index 5c6e10ae8f7ce..702d259f98615 100644 --- a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes.test.ts +++ b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes.test.ts @@ -15,6 +15,7 @@ import { MatrixHistogramType, NetworkKpiQueries, NetworkQueries, + UsersQueries, } from '../../../common/search_strategy'; /** Get the return type of createIndicesFromPrefix for TypeScript checks against expected */ @@ -75,11 +76,11 @@ describe('get_transform_changes', () => { test('it gets a transform change for authentications', () => { expect( getTransformChanges({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: UsersQueries.authentications, settings: getTransformConfigSchemaMock().settings[0], }) ).toEqual({ - factoryQueryType: HostsQueries.authenticationsEntities, + factoryQueryType: UsersQueries.authenticationsEntities, indices: ['.estc_all_user_ent*'], }); }); diff --git a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes.ts b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes.ts index 6e327457a683d..1a72c556490a5 100644 --- a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes.ts +++ b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes.ts @@ -9,6 +9,7 @@ import { getTransformChangesForHosts } from './get_transform_changes_for_hosts'; import { getTransformChangesForKpi } from './get_transform_changes_for_kpi'; import { getTransformChangesForMatrixHistogram } from './get_transform_changes_for_matrix_histogram'; import { getTransformChangesForNetwork } from './get_transform_changes_for_network'; +import { getTransformChangesForUsers } from './get_transform_changes_for_users'; import { GetTransformChanges } from './types'; export const getTransformChanges: GetTransformChanges = ({ @@ -26,6 +27,11 @@ export const getTransformChanges: GetTransformChanges = ({ return hostTransform; } + const userTransform = getTransformChangesForUsers({ factoryQueryType, settings }); + if (userTransform != null) { + return userTransform; + } + const networkTransform = getTransformChangesForNetwork({ factoryQueryType, settings, diff --git a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_hosts.test.ts b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_hosts.test.ts index e76c2ee2575ff..8223e3a9cd6e6 100644 --- a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_hosts.test.ts +++ b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_hosts.test.ts @@ -25,18 +25,6 @@ describe('get_transform_changes_for_host', () => { }); }); - test('it gets a transform change for authentications', () => { - expect( - getTransformChangesForHosts({ - factoryQueryType: HostsQueries.authentications, - settings: getTransformConfigSchemaMock().settings[0], - }) - ).toEqual({ - factoryQueryType: HostsQueries.authenticationsEntities, - indices: ['.estc_all_user_ent*'], - }); - }); - test('it returns an "undefined" for another value', () => { expect( getTransformChangesForHosts({ diff --git a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_hosts.ts b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_hosts.ts index 95a5e04bd9e51..265b2857d5397 100644 --- a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_hosts.ts +++ b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_hosts.ts @@ -30,15 +30,6 @@ export const getTransformChangesForHosts: GetTransformChanges = ({ factoryQueryType: HostsQueries.hostsEntities, }; } - case HostsQueries.authentications: { - return { - indices: createIndicesFromPrefix({ - prefix: settings.prefix, - transformIndices: ['user_ent*'], - }), - factoryQueryType: HostsQueries.authenticationsEntities, - }; - } default: { return undefined; } diff --git a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_users.test.ts b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_users.test.ts new file mode 100644 index 0000000000000..ae3690d72baba --- /dev/null +++ b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_users.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UsersQueries } from '../../../common/search_strategy'; +import { getTransformChangesForUsers } from './get_transform_changes_for_users'; +import { getTransformConfigSchemaMock } from './transform_config_schema.mock'; + +/** Get the return type of getTransformChangesForUsers for TypeScript checks against expected */ +type ReturnTypeGetTransformChangesForUsers = ReturnType; + +describe('get_transform_changes_for_user', () => { + test('it gets a transform change for authentications', () => { + expect( + getTransformChangesForUsers({ + factoryQueryType: UsersQueries.authentications, + settings: getTransformConfigSchemaMock().settings[0], + }) + ).toEqual({ + factoryQueryType: UsersQueries.authenticationsEntities, + indices: ['.estc_all_user_ent*'], + }); + }); + + test('it returns an "undefined" for another value', () => { + expect( + getTransformChangesForUsers({ + factoryQueryType: UsersQueries.details, + settings: getTransformConfigSchemaMock().settings[0], + }) + ).toEqual(undefined); + }); +}); diff --git a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_users.ts b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_users.ts new file mode 100644 index 0000000000000..a91bc05bb7383 --- /dev/null +++ b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_for_users.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UsersQueries } from '../../../common/search_strategy'; +import { createIndicesFromPrefix } from './create_indices_from_prefix'; +import { GetTransformChanges } from './types'; + +/** + * Given a factory query type this will return the transform changes such as the transform indices if it matches + * the correct type, otherwise it will return "undefined" + * @param factoryQueryType The query type to check if we have a transform for it and are capable of rendering one or not + * @param settings The settings configuration to get the prefix from + * @returns The transform type if we have one, otherwise undefined + */ +export const getTransformChangesForUsers: GetTransformChanges = ({ + factoryQueryType, + settings, +}) => { + switch (factoryQueryType) { + case UsersQueries.authentications: { + return { + indices: createIndicesFromPrefix({ + prefix: settings.prefix, + transformIndices: ['user_ent*'], + }), + factoryQueryType: UsersQueries.authenticationsEntities, + }; + } + default: { + return undefined; + } + } +}; diff --git a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_if_they_exist.test.ts b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_if_they_exist.test.ts index 7a4e11526d83e..19c9ba5596027 100644 --- a/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_if_they_exist.test.ts +++ b/x-pack/plugins/security_solution/public/transforms/utils/get_transform_changes_if_they_exist.test.ts @@ -35,7 +35,7 @@ describe('get_transform_changes_if_they_exist', () => { test('returns transformed settings if our settings is enabled', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: ['auditbeat-*'], transformSettings: { ...getTransformConfigSchemaMock(), enabled: true }, // sets enabled to true filterQuery: undefined, @@ -47,15 +47,15 @@ describe('get_transform_changes_if_they_exist', () => { }, }) ).toMatchObject>({ - indices: ['.estc_all_user_ent*'], - factoryQueryType: HostsQueries.authenticationsEntities, + indices: ['.estc_all_host_ent*'], + factoryQueryType: HostsQueries.hostsEntities, }); }); test('returns regular settings if our settings is disabled', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: ['auditbeat-*'], transformSettings: { ...getTransformConfigSchemaMock(), enabled: false }, // sets enabled to false filterQuery: undefined, @@ -68,7 +68,7 @@ describe('get_transform_changes_if_they_exist', () => { }) ).toMatchObject>({ indices: ['auditbeat-*'], - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, }); }); }); @@ -77,7 +77,7 @@ describe('get_transform_changes_if_they_exist', () => { test('returns regular settings if filter is set to something other than match_all', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: ['auditbeat-*'], transformSettings: getTransformConfigSchemaMock(), filterQuery: { @@ -97,14 +97,14 @@ describe('get_transform_changes_if_they_exist', () => { }) ).toMatchObject>({ indices: ['auditbeat-*'], - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, }); }); test('returns transformed settings if filter is set to something such as match_all', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: ['auditbeat-*'], transformSettings: getTransformConfigSchemaMock(), filterQuery: { @@ -123,15 +123,15 @@ describe('get_transform_changes_if_they_exist', () => { }, }) ).toMatchObject>({ - indices: ['.estc_all_user_ent*'], - factoryQueryType: HostsQueries.authenticationsEntities, + indices: ['.estc_all_host_ent*'], + factoryQueryType: HostsQueries.hostsEntities, }); }); test('returns transformed settings if filter is set to undefined', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: ['auditbeat-*'], transformSettings: getTransformConfigSchemaMock(), filterQuery: undefined, // undefined should return transform @@ -143,8 +143,8 @@ describe('get_transform_changes_if_they_exist', () => { }, }) ).toMatchObject>({ - indices: ['.estc_all_user_ent*'], - factoryQueryType: HostsQueries.authenticationsEntities, + indices: ['.estc_all_host_ent*'], + factoryQueryType: HostsQueries.hostsEntities, }); }); }); @@ -153,7 +153,7 @@ describe('get_transform_changes_if_they_exist', () => { test('returns regular settings if timerange is less than an hour', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: ['auditbeat-*'], transformSettings: getTransformConfigSchemaMock(), filterQuery: undefined, @@ -166,14 +166,14 @@ describe('get_transform_changes_if_they_exist', () => { }) ).toMatchObject>({ indices: ['auditbeat-*'], - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, }); }); test('returns regular settings if timerange is invalid', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: ['auditbeat-*'], transformSettings: getTransformConfigSchemaMock(), filterQuery: undefined, @@ -186,14 +186,14 @@ describe('get_transform_changes_if_they_exist', () => { }) ).toMatchObject>({ indices: ['auditbeat-*'], - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, }); }); test('returns transformed settings if timerange is greater than an hour', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: ['auditbeat-*'], transformSettings: getTransformConfigSchemaMock(), filterQuery: undefined, @@ -205,8 +205,8 @@ describe('get_transform_changes_if_they_exist', () => { }, }) ).toMatchObject>({ - indices: ['.estc_all_user_ent*'], - factoryQueryType: HostsQueries.authenticationsEntities, + indices: ['.estc_all_host_ent*'], + factoryQueryType: HostsQueries.hostsEntities, }); }); }); @@ -215,7 +215,7 @@ describe('get_transform_changes_if_they_exist', () => { test('it returns regular settings if settings do not match', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: ['should-not-match-*'], // index doesn't match anything transformSettings: getTransformConfigSchemaMock(), filterQuery: undefined, @@ -228,14 +228,14 @@ describe('get_transform_changes_if_they_exist', () => { }) ).toMatchObject>({ indices: ['should-not-match-*'], - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, }); }); test('it returns transformed settings if settings do match', () => { expect( getTransformChangesIfTheyExist({ - factoryQueryType: HostsQueries.authentications, + factoryQueryType: HostsQueries.hosts, indices: [ 'auditbeat-*', 'endgame-*', @@ -255,8 +255,8 @@ describe('get_transform_changes_if_they_exist', () => { }, }) ).toMatchObject>({ - indices: ['.estc_all_user_ent*'], - factoryQueryType: HostsQueries.authenticationsEntities, + indices: ['.estc_all_host_ent*'], + factoryQueryType: HostsQueries.hostsEntities, }); }); }); diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/authentications_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/authentications_query_tab_body.test.tsx index 83c2b0dfa6b72..b7ffb68e2fc2d 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/authentications_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/authentications_query_tab_body.test.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import { TestProviders } from '../../../common/mock'; -import { useAuthentications } from '../../../hosts/containers/authentications'; import { useQueryToggle } from '../../../common/containers/query_toggle'; import { AuthenticationsQueryTabBody } from './authentications_query_tab_body'; import { UsersType } from '../../store/model'; +import { useAuthentications } from '../../../common/containers/authentications'; -jest.mock('../../../hosts/containers/authentications'); +jest.mock('../../../common/containers/authentications'); jest.mock('../../../common/containers/query_toggle'); jest.mock('../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/authentications_query_tab_body.tsx index 6926a1e755f01..453ae6e9a8f3f 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/authentications_query_tab_body.tsx @@ -5,15 +5,11 @@ * 2.0. */ -import { getOr } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; -import { useAuthentications, ID } from '../../../hosts/containers/authentications'; +import React from 'react'; import { UsersComponentsQueryProps } from './types'; -import { AuthenticationTable } from '../../../hosts/components/authentications_table'; -import { manageQuery } from '../../../common/components/page/manage_query'; -import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { AuthenticationsUserTable } from '../../../common/components/authentication/authentications_user_table'; -const AuthenticationTableManage = manageQuery(AuthenticationTable); +export const ID = 'usersAuthenticationsQuery'; export const AuthenticationsQueryTabBody = ({ endDate, @@ -26,47 +22,17 @@ export const AuthenticationsQueryTabBody = ({ docValueFields, deleteQuery, }: UsersComponentsQueryProps) => { - const { toggleStatus } = useQueryToggle(ID); - const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); - useEffect(() => { - setQuerySkip(skip || !toggleStatus); - }, [skip, toggleStatus]); - - const [ - loading, - { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, - ] = useAuthentications({ - docValueFields, - endDate, - filterQuery, - indexNames, - skip: querySkip, - startDate, - // TODO Move authentication table and hook store to 'public/common' folder when 'usersEnabled' FF is removed - // @ts-ignore - type, - deleteQuery, - }); return ( - ); }; diff --git a/x-pack/plugins/security_solution/public/users/store/selectors.ts b/x-pack/plugins/security_solution/public/users/store/selectors.ts index bdeacef2bf774..1ccedf3b5da20 100644 --- a/x-pack/plugins/security_solution/public/users/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/users/store/selectors.ts @@ -21,3 +21,6 @@ export const userRiskScoreSelector = () => export const usersRiskScoreSeverityFilterSelector = () => createSelector(selectUserPage, (users) => users.queries[UsersTableType.risk].severitySelection); + +export const authenticationsSelector = () => + createSelector(selectUserPage, (users) => users.queries[UsersTableType.authentications]); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts index fda1e1c166ce3..901d577fdbf5a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts @@ -13,7 +13,6 @@ import { hostOverview } from './overview'; import { firstOrLastSeenHost } from './last_first_seen'; import { uncommonProcesses } from './uncommon_processes'; -import { authentications, authenticationsEntities } from './authentications'; import { hostsKpiAuthentications, hostsKpiAuthenticationsEntities } from './kpi/authentications'; import { hostsKpiHosts, hostsKpiHostsEntities } from './kpi/hosts'; import { hostsKpiUniqueIps, hostsKpiUniqueIpsEntities } from './kpi/unique_ips'; @@ -23,7 +22,6 @@ jest.mock('./details'); jest.mock('./overview'); jest.mock('./last_first_seen'); jest.mock('./uncommon_processes'); -jest.mock('./authentications'); jest.mock('./kpi/authentications'); jest.mock('./kpi/hosts'); jest.mock('./kpi/unique_ips'); @@ -36,8 +34,6 @@ describe('hostsFactory', () => { [HostsQueries.overview]: hostOverview, [HostsQueries.firstOrLastSeen]: firstOrLastSeenHost, [HostsQueries.uncommonProcesses]: uncommonProcesses, - [HostsQueries.authentications]: authentications, - [HostsQueries.authenticationsEntities]: authenticationsEntities, [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, [HostsKpiQueries.kpiAuthenticationsEntities]: hostsKpiAuthenticationsEntities, [HostsKpiQueries.kpiHosts]: hostsKpiHosts, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index cd95a38ec3092..5d40e5e153599 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -17,7 +17,6 @@ import { hostDetails } from './details'; import { hostOverview } from './overview'; import { firstOrLastSeenHost } from './last_first_seen'; import { uncommonProcesses } from './uncommon_processes'; -import { authentications, authenticationsEntities } from './authentications'; import { hostsKpiAuthentications, hostsKpiAuthenticationsEntities } from './kpi/authentications'; import { hostsKpiHosts, hostsKpiHostsEntities } from './kpi/hosts'; import { hostsKpiUniqueIps, hostsKpiUniqueIpsEntities } from './kpi/unique_ips'; @@ -32,8 +31,6 @@ export const hostsFactory: Record< [HostsQueries.overview]: hostOverview, [HostsQueries.firstOrLastSeen]: firstOrLastSeenHost, [HostsQueries.uncommonProcesses]: uncommonProcesses, - [HostsQueries.authentications]: authentications, - [HostsQueries.authenticationsEntities]: authenticationsEntities, [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, [HostsKpiQueries.kpiAuthenticationsEntities]: hostsKpiAuthenticationsEntities, [HostsKpiQueries.kpiHosts]: hostsKpiHosts, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/__mocks__/index.ts similarity index 99% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/__mocks__/index.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/__mocks__/index.ts index 6e43d771d1a02..6b57ff7fdb7cd 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/__mocks__/index.ts @@ -6,15 +6,15 @@ */ import type { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; - import { + UserAuthenticationsRequestOptions, AuthenticationHit, Direction, - HostAuthenticationsRequestOptions, - HostsQueries, + UsersQueries, + AuthStackByField, } from '../../../../../../../common/search_strategy'; -export const mockOptions: HostAuthenticationsRequestOptions = { +export const mockOptions: UserAuthenticationsRequestOptions = { defaultIndex: [ 'apm-*-transaction*', 'traces-apm*', @@ -25,6 +25,7 @@ export const mockOptions: HostAuthenticationsRequestOptions = { 'packetbeat-*', 'winlogbeat-*', ], + stackByField: AuthStackByField.userName, docValueFields: [ { field: '@timestamp', @@ -427,7 +428,7 @@ export const mockOptions: HostAuthenticationsRequestOptions = { format: 'date_time', }, ], - factoryQueryType: HostsQueries.authentications, + factoryQueryType: UsersQueries.authentications, filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', pagination: { activePage: 0, @@ -456,7 +457,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, hits: { total: -1, max_score: 0, hits: [] }, aggregations: { - group_by_users: { + stack_by: { doc_count_error_upper_bound: -1, sum_other_doc_count: 408, buckets: [ @@ -1290,7 +1291,7 @@ export const mockSearchStrategyResponse: IEsSearchResponse = { }, ], }, - user_count: { value: 188 }, + stack_by_count: { value: 188 }, }, }, total: 21, @@ -1306,7 +1307,7 @@ export const formattedSearchStrategyResponse = { _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, hits: { total: -1, max_score: 0, hits: [] }, aggregations: { - group_by_users: { + stack_by: { doc_count_error_upper_bound: -1, sum_other_doc_count: 408, buckets: [ @@ -2140,7 +2141,7 @@ export const formattedSearchStrategyResponse = { }, ], }, - user_count: { value: 188 }, + stack_by_count: { value: 188 }, }, }, total: 21, @@ -2164,8 +2165,8 @@ export const formattedSearchStrategyResponse = { body: { docvalue_fields: mockOptions.docValueFields, aggregations: { - user_count: { cardinality: { field: 'user.name' } }, - group_by_users: { + stack_by_count: { cardinality: { field: 'user.name' } }, + stack_by: { terms: { size: 10, field: 'user.name', @@ -2231,7 +2232,7 @@ export const formattedSearchStrategyResponse = { failures: 0, successes: 4, _id: 'SYSTEM+281', - user: { name: ['SYSTEM'] }, + stackedValue: ['SYSTEM'], lastSuccess: { timestamp: ['2020-09-04T13:08:02.532Z'], host: { id: ['ce1d3c9b-a815-4643-9641-ada0f2c00609'], name: ['siem-windows'] }, @@ -2244,7 +2245,7 @@ export const formattedSearchStrategyResponse = { failures: 0, successes: 1, _id: 'tsg+1', - user: { name: ['tsg'] }, + stackedValue: ['tsg'], lastSuccess: { timestamp: ['2020-09-04T11:49:21.000Z'], source: { ip: ['77.183.42.188'] }, @@ -2258,7 +2259,7 @@ export const formattedSearchStrategyResponse = { failures: 23, successes: 0, _id: 'admin+23', - user: { name: ['admin'] }, + stackedValue: ['admin'], lastFailure: { timestamp: ['2020-09-04T13:40:46.000Z'], source: { ip: ['59.15.3.197'] }, @@ -2272,7 +2273,7 @@ export const formattedSearchStrategyResponse = { failures: 21, successes: 0, _id: 'user+21', - user: { name: ['user'] }, + stackedValue: ['user'], lastFailure: { timestamp: ['2020-09-04T13:25:43.000Z'], source: { ip: ['64.227.88.245'] }, @@ -2286,7 +2287,7 @@ export const formattedSearchStrategyResponse = { failures: 18, successes: 0, _id: 'ubuntu+18', - user: { name: ['ubuntu'] }, + stackedValue: ['ubuntu'], lastFailure: { timestamp: ['2020-09-04T13:25:07.000Z'], source: { ip: ['64.227.88.245'] }, @@ -2300,7 +2301,7 @@ export const formattedSearchStrategyResponse = { failures: 17, successes: 0, _id: 'odoo+17', - user: { name: ['odoo'] }, + stackedValue: ['odoo'], lastFailure: { timestamp: ['2020-09-04T12:26:36.000Z'], source: { ip: ['180.151.228.166'] }, @@ -2314,7 +2315,7 @@ export const formattedSearchStrategyResponse = { failures: 17, successes: 0, _id: 'pi+17', - user: { name: ['pi'] }, + stackedValue: ['pi'], lastFailure: { timestamp: ['2020-09-04T11:37:22.000Z'], source: { ip: ['178.174.148.58'] }, @@ -2328,7 +2329,7 @@ export const formattedSearchStrategyResponse = { failures: 14, successes: 0, _id: 'demo+14', - user: { name: ['demo'] }, + stackedValue: ['demo'], lastFailure: { timestamp: ['2020-09-04T07:23:22.000Z'], source: { ip: ['45.95.168.157'] }, @@ -2342,7 +2343,7 @@ export const formattedSearchStrategyResponse = { failures: 13, successes: 0, _id: 'git+13', - user: { name: ['git'] }, + stackedValue: ['git'], lastFailure: { timestamp: ['2020-09-04T11:20:26.000Z'], source: { ip: ['123.206.30.76'] }, @@ -2356,7 +2357,7 @@ export const formattedSearchStrategyResponse = { failures: 13, successes: 0, _id: 'webadmin+13', - user: { name: ['webadmin'] }, + stackedValue: ['webadmin'], lastFailure: { timestamp: ['2020-09-04T07:25:28.000Z'], source: { ip: ['45.95.168.157'] }, @@ -2386,8 +2387,8 @@ export const expectedDsl = { body: { docvalue_fields: mockOptions.docValueFields, aggregations: { - user_count: { cardinality: { field: 'user.name' } }, - group_by_users: { + stack_by_count: { cardinality: { field: 'user.name' } }, + stack_by: { terms: { size: 10, field: 'user.name', @@ -2445,7 +2446,7 @@ export const mockHit: AuthenticationHit = { }, cursor: 'cursor-1', sort: [0], - user: 'Evan', + stackedValue: 'Evan', failures: 10, successes: 20, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/dsl/query.dsl.test.ts similarity index 100% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.test.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/dsl/query.dsl.test.ts diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/dsl/query.dsl.ts similarity index 90% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/dsl/query.dsl.ts index c88104745ba06..e018716d4c216 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/dsl/query.dsl.ts @@ -7,7 +7,7 @@ import { isEmpty } from 'lodash/fp'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { HostAuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts/authentications'; +import { UserAuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/users/authentications'; import { sourceFieldsMap, hostFieldsMap } from '../../../../../../../common/ecs/ecs_fields'; import { createQueryFilterClauses } from '../../../../../../utils/build_query'; @@ -28,11 +28,12 @@ export const auditdFieldsMap: Readonly> = { export const buildQuery = ({ filterQuery, + stackByField, timerange: { from, to }, pagination: { querySize }, defaultIndex, docValueFields, -}: HostAuthenticationsRequestOptions) => { +}: UserAuthenticationsRequestOptions) => { const esFields = reduceFields(authenticationsFields, { ...hostFieldsMap, ...sourceFieldsMap, @@ -52,14 +53,6 @@ export const buildQuery = ({ }, ]; - const agg = { - user_count: { - cardinality: { - field: 'user.name', - }, - }, - }; - const dslQuery = { allow_no_indices: true, index: defaultIndex, @@ -67,11 +60,15 @@ export const buildQuery = ({ body: { ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { - ...agg, - group_by_users: { + stack_by_count: { + cardinality: { + field: stackByField, + }, + }, + stack_by: { terms: { size: querySize, - field: 'user.name', + field: stackByField, order: [ { 'successes.doc_count': 'desc' as const }, { 'failures.doc_count': 'desc' as const }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query_entities.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/dsl/query_entities.dsl.ts similarity index 83% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query_entities.dsl.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/dsl/query_entities.dsl.ts index ab726b41ae01b..7c9c29a1efdc4 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query_entities.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/dsl/query_entities.dsl.ts @@ -8,9 +8,8 @@ import { isEmpty } from 'lodash/fp'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { HostAuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts/authentications'; - import { createQueryFilterClauses } from '../../../../../../utils/build_query'; +import { UserAuthenticationsRequestOptions } from '../../../../../../../common/search_strategy'; export const buildQueryEntities = ({ filterQuery, @@ -18,7 +17,8 @@ export const buildQueryEntities = ({ pagination: { querySize }, defaultIndex, docValueFields, -}: HostAuthenticationsRequestOptions) => { + stackByField, +}: UserAuthenticationsRequestOptions) => { const filter = [ ...createQueryFilterClauses(filterQuery), { @@ -32,14 +32,6 @@ export const buildQueryEntities = ({ }, ]; - const agg = { - user_count: { - cardinality: { - field: 'user.name', - }, - }, - }; - const dslQuery = { allow_no_indices: true, index: defaultIndex, @@ -47,11 +39,15 @@ export const buildQueryEntities = ({ body: { ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { - ...agg, - group_by_users: { + stack_by_count: { + cardinality: { + field: stackByField, + }, + }, + stack_by: { terms: { size: querySize, - field: 'user.name', + field: stackByField, order: [ { successes: 'desc' }, { failures: 'desc' }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.test.ts similarity index 90% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.test.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.test.ts index a8cce8a4c2bcf..1e745ffcbf2ed 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AuthenticationsEdges } from '../../../../../../common/search_strategy/security_solution/hosts/authentications'; +import { AuthenticationsEdges } from '../../../../../../common/search_strategy'; import { auditdFieldsMap } from './dsl/query.dsl'; import { formatAuthenticationData } from './helpers'; @@ -24,9 +24,7 @@ describe('#formatAuthenticationsData', () => { _id: 'id-123', failures: 10, successes: 20, - user: { - name: ['Evan'], - }, + stackedValue: ['Evan'], }, }; @@ -45,9 +43,7 @@ describe('#formatAuthenticationsData', () => { _id: 'id-123', failures: 10, successes: 20, - user: { - name: ['Evan'], - }, + stackedValue: ['Evan'], }, }; @@ -66,9 +62,7 @@ describe('#formatAuthenticationsData', () => { _id: 'id-123', failures: 10, successes: 20, - user: { - name: ['Evan'], - }, + stackedValue: ['Evan'], }, }; @@ -87,9 +81,7 @@ describe('#formatAuthenticationsData', () => { _id: 'id-123', failures: 10, successes: 20, - user: { - name: ['Evan'], - }, + stackedValue: ['Evan'], }, }; @@ -108,9 +100,7 @@ describe('#formatAuthenticationsData', () => { _id: 'id-123', failures: 10, successes: 20, - user: { - name: ['Evan'], - }, + stackedValue: ['Evan'], }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.ts similarity index 91% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.ts index 7517d112aebdc..02a89dd08a222 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/helpers.ts @@ -21,7 +21,7 @@ export const authenticationsFields = [ '_id', 'failures', 'successes', - 'user.name', + 'stackedValue', 'lastSuccess.timestamp', 'lastSuccess.source.ip', 'lastSuccess.host.id', @@ -46,7 +46,7 @@ export const formatAuthenticationData = ( ...flattenedFields.node, ...{ _id: hit._id, - user: { name: [hit.user] }, + stackedValue: [hit.stackedValue], failures: hit.failures, successes: hit.successes, }, @@ -69,9 +69,7 @@ export const formatAuthenticationData = ( failures: 0, successes: 0, _id: '', - user: { - name: [''], - }, + stackedValue: [''], }, cursor: { value: '', @@ -81,7 +79,7 @@ export const formatAuthenticationData = ( ); export const getHits = (response: StrategyResponseType) => - getOr([], 'aggregations.group_by_users.buckets', response.rawResponse).map( + getOr([], 'aggregations.stack_by.buckets', response.rawResponse).map( (bucket: AuthenticationBucket) => ({ _id: getOr( `${bucket.key}+${bucket.doc_count}`, @@ -92,14 +90,14 @@ export const getHits = (response: StrategyResponseT lastSuccess: getOr(null, 'successes.lastSuccess.hits.hits[0]._source', bucket), lastFailure: getOr(null, 'failures.lastFailure.hits.hits[0]._source', bucket), }, - user: bucket.key, + stackedValue: bucket.key, failures: bucket.failures.doc_count, successes: bucket.successes.doc_count, }) ); export const getHitsEntities = (response: StrategyResponseType) => - getOr([], 'aggregations.group_by_users.buckets', response.rawResponse).map( + getOr([], 'aggregations.stack_by.buckets', response.rawResponse).map( (bucket: AuthenticationBucket) => ({ _id: getOr( `${bucket.key}+${bucket.doc_count}`, @@ -110,7 +108,7 @@ export const getHitsEntities = (response: StrategyR lastSuccess: getOr(null, 'successes.lastSuccess.hits.hits[0]._source', bucket), lastFailure: getOr(null, 'failures.lastFailure.hits.hits[0]._source', bucket), }, - user: bucket.key, + stackedValue: bucket.key, failures: bucket.failures.value, successes: bucket.successes.value, }) @@ -130,7 +128,7 @@ export const formatAuthenticationEntitiesData = ( ...flattenedFields.node, ...{ _id: hit._id, - user: { name: [hit.user] }, + stackedValue: [hit.stackedValue], failures: hit.failures, successes: hit.successes, }, @@ -153,9 +151,7 @@ export const formatAuthenticationEntitiesData = ( failures: 0, successes: 0, _id: '', - user: { - name: [''], - }, + stackedValue: [''], }, cursor: { value: '', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/index.test.tsx similarity index 90% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.test.tsx rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/index.test.tsx index 960f1a516e814..e4342cf266474 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/index.test.tsx @@ -7,7 +7,6 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; -import { HostAuthenticationsRequestOptions } from '../../../../../../common/search_strategy/security_solution/hosts/authentications'; import * as buildQuery from './dsl/query.dsl'; import { authentications } from '.'; import { @@ -15,6 +14,7 @@ import { mockSearchStrategyResponse, formattedSearchStrategyResponse, } from './__mocks__'; +import { UserAuthenticationsRequestOptions } from '../../../../../../common/search_strategy'; describe('authentications search strategy', () => { const buildAuthenticationQuery = jest.spyOn(buildQuery, 'buildQuery'); @@ -36,7 +36,7 @@ describe('authentications search strategy', () => { ...mockOptions.pagination, querySize: DEFAULT_MAX_TABLE_QUERY_SIZE, }, - } as HostAuthenticationsRequestOptions; + } as UserAuthenticationsRequestOptions; expect(() => { authentications.buildDsl(overSizeOptions); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/index.tsx similarity index 78% rename from x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/index.tsx index e32d3592d3417..11400166e3344 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/authentications/index.tsx @@ -11,12 +11,12 @@ import type { IEsSearchResponse } from '../../../../../../../../../src/plugins/d import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; import { - HostsQueries, - AuthenticationsEdges, - HostAuthenticationsRequestOptions, - HostAuthenticationsStrategyResponse, AuthenticationHit, -} from '../../../../../../common/search_strategy/security_solution/hosts'; + AuthenticationsEdges, + UserAuthenticationsRequestOptions, + UserAuthenticationsStrategyResponse, +} from '../../../../../../common/search_strategy'; +import { UsersQueries } from '../../../../../../common/search_strategy/security_solution/users'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; @@ -32,8 +32,8 @@ import { getHitsEntities, } from './helpers'; -export const authentications: SecuritySolutionFactory = { - buildDsl: (options: HostAuthenticationsRequestOptions) => { +export const authentications: SecuritySolutionFactory = { + buildDsl: (options: UserAuthenticationsRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); } @@ -41,11 +41,11 @@ export const authentications: SecuritySolutionFactory - ): Promise => { + ): Promise => { const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.user_count.value', response.rawResponse); + const totalCount = getOr(0, 'aggregations.stack_by_count.value', response.rawResponse); const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; const hits: AuthenticationHit[] = getHits(response); @@ -72,8 +72,8 @@ export const authentications: SecuritySolutionFactory = { - buildDsl: (options: HostAuthenticationsRequestOptions) => { +export const authenticationsEntities: SecuritySolutionFactory = { + buildDsl: (options: UserAuthenticationsRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); } @@ -81,11 +81,11 @@ export const authenticationsEntities: SecuritySolutionFactory - ): Promise => { + ): Promise => { const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.user_count.value', response.rawResponse); + const totalCount = getOr(0, 'aggregations.stack_by_count.value', response.rawResponse); const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; const hits: AuthenticationHit[] = getHitsEntities(response); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/index.ts index dce2195867358..0b13319cc11c7 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/users/index.ts @@ -10,6 +10,7 @@ import { UsersQueries } from '../../../../../common/search_strategy/security_sol import { SecuritySolutionFactory } from '../types'; import { allUsers } from './all'; +import { authentications, authenticationsEntities } from './authentications'; import { userDetails } from './details'; import { totalUsersKpi } from './kpi/total_users'; @@ -17,4 +18,6 @@ export const usersFactory: Record { - const authentications = await bsearch.send({ - supertest, - options: { - factoryQueryType: HostsQueries.authentications, - timerange: { - interval: '12h', - to: TO, - from: FROM, - }, - pagination: { - activePage: 0, - cursorStart: 0, - fakePossibleCount: 3, - querySize: 1, - }, - defaultIndex: ['auditbeat-*'], - docValueFields: [], - inspect: false, + const requestOptions: UserAuthenticationsRequestOptions = { + factoryQueryType: UsersQueries.authentications, + timerange: { + interval: '12h', + to: TO, + from: FROM, + }, + pagination: { + activePage: 0, + cursorStart: 0, + fakePossibleCount: 3, + querySize: 1, }, + defaultIndex: ['auditbeat-*'], + docValueFields: [], + stackByField: AuthStackByField.userName, + sort: { field: 'timestamp', direction: Direction.asc }, + filterQuery: '', + }; + + const authentications = await bsearch.send({ + supertest, + options: requestOptions, strategy: 'securitySolutionSearchStrategy', }); @@ -63,25 +70,29 @@ export default function ({ getService }: FtrProviderContext) { }); it('Make sure that pagination is working in Authentications query', async () => { - const authentications = await bsearch.send({ - supertest, - options: { - factoryQueryType: HostsQueries.authentications, - timerange: { - interval: '12h', - to: TO, - from: FROM, - }, - pagination: { - activePage: 2, - cursorStart: 1, - fakePossibleCount: 5, - querySize: 2, - }, - defaultIndex: ['auditbeat-*'], - docValueFields: [], - inspect: false, + const requestOptions: UserAuthenticationsRequestOptions = { + factoryQueryType: UsersQueries.authentications, + timerange: { + interval: '12h', + to: TO, + from: FROM, + }, + pagination: { + activePage: 2, + cursorStart: 1, + fakePossibleCount: 5, + querySize: 2, }, + defaultIndex: ['auditbeat-*'], + docValueFields: [], + stackByField: AuthStackByField.userName, + sort: { field: 'timestamp', direction: Direction.asc }, + filterQuery: '', + }; + + const authentications = await bsearch.send({ + supertest, + options: requestOptions, strategy: 'securitySolutionSearchStrategy', });