From 41d1b8591167834c51530e72580d87d1c4d117fa Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Mon, 14 Feb 2022 10:01:10 +0100 Subject: [PATCH 1/3] Add anomalies tab to user page --- .../models/results_service/results_service.ts | 7 +- .../ml/server/routes/results_service.ts | 4 +- .../routes/schemas/results_service_schema.ts | 2 +- .../ml/anomaly/use_anomalies_table_data.ts | 5 +- .../components/ml/api/anomalies_table_data.ts | 2 + .../get_criteria_from_users_type.test.ts | 26 ++ .../criteria/get_criteria_from_users_type.ts | 20 ++ .../get_user_name_from_influencers.test.ts | 46 +++ .../get_user_name_from_influencers.ts | 31 ++ .../ml/tables/anomalies_host_table.tsx | 21 +- .../ml/tables/anomalies_user_table.tsx | 97 +++++++ .../tables/convert_anomalies_to_users.test.ts | 265 ++++++++++++++++++ .../ml/tables/convert_anomalies_to_users.ts | 41 +++ .../ml/tables/create_compound_key.test.ts | 23 +- .../ml/tables/create_compound_key.ts | 9 +- ...ality.test.ts => default_equality.test.ts} | 14 +- .../{host_equality.ts => default_equality.ts} | 8 +- .../get_anomalies_host_table_columns.test.tsx | 100 +------ .../get_anomalies_host_table_columns.tsx | 106 +------ ...t_anomalies_network_table_columns.test.tsx | 55 ---- .../get_anomalies_network_table_columns.tsx | 89 +----- .../get_anomalies_table_columns.test.tsx | 73 +++++ .../ml/tables/get_anomalies_table_columns.tsx | 111 ++++++++ .../get_anomalies_user_table_columns.test.tsx | 40 +++ .../get_anomalies_user_table_columns.tsx | 60 ++++ .../components/ml/tables/network_equality.ts | 5 +- .../components/ml/tables/translations.ts | 4 + .../public/common/components/ml/types.ts | 26 +- .../anomalies_query_tab_body/types.ts | 3 +- .../public/common/mock/global_state.ts | 2 + .../public/users/jest.config.js | 24 ++ .../public/users/pages/constants.ts | 2 +- .../public/users/pages/details/nav_tabs.tsx | 2 +- .../public/users/pages/details/utils.ts | 3 +- .../public/users/pages/nav_tabs.tsx | 8 +- .../public/users/pages/navigation/types.ts | 2 +- .../public/users/pages/translations.ts | 16 +- .../public/users/pages/users_tabs.test.tsx | 79 ++++++ .../public/users/pages/users_tabs.tsx | 64 ++++- .../public/users/store/model.ts | 2 + .../public/users/store/reducer.ts | 15 +- 41 files changed, 1085 insertions(+), 427 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/criteria/get_criteria_from_users_type.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/criteria/get_criteria_from_users_type.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.ts rename x-pack/plugins/security_solution/public/common/components/ml/tables/{host_equality.test.ts => default_equality.test.ts} (89%) rename x-pack/plugins/security_solution/public/common/components/ml/tables/{host_equality.ts => default_equality.ts} (68%) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx create mode 100644 x-pack/plugins/security_solution/public/users/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/users/pages/users_tabs.test.tsx diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index aa92ada043c29..1ef722422b69b 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -96,7 +96,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust dateFormatTz: string, maxRecords: number = ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, maxExamples: number = DEFAULT_MAX_EXAMPLES, - influencersFilterQuery?: any, + filterQuery?: any, functionDescription?: string ) { // Build the query to return the matching anomaly record results. @@ -153,8 +153,8 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }); } - if (influencersFilterQuery !== undefined) { - boolCriteria.push(influencersFilterQuery); + if (filterQuery !== undefined) { + boolCriteria.push(filterQuery); } // Add a nested query to filter for each of the specified influencers. @@ -224,6 +224,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust anomalies: [], interval: 'second', }; + // @ts-expect-error incorrect search response type if (body.hits.total.value > 0) { let records: AnomalyRecordDoc[] = []; diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 78f05f0d731aa..d8e11d181848f 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -37,7 +37,7 @@ function getAnomaliesTableData(mlClient: MlClient, payload: any) { dateFormatTz, maxRecords, maxExamples, - influencersFilterQuery, + filterQuery, functionDescription, } = payload; return rs.getAnomaliesTableData( @@ -51,7 +51,7 @@ function getAnomaliesTableData(mlClient: MlClient, payload: any) { dateFormatTz, maxRecords, maxExamples, - influencersFilterQuery, + filterQuery, functionDescription ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts index df4a56b06410b..02407b7ab091c 100644 --- a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts @@ -26,7 +26,7 @@ export const anomaliesTableDataSchema = schema.object({ dateFormatTz: schema.string(), maxRecords: schema.number(), maxExamples: schema.maybe(schema.number()), - influencersFilterQuery: schema.maybe(schema.any()), + filterQuery: schema.maybe(schema.any()), functionDescription: schema.maybe(schema.nullable(schema.string())), }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index 05a75a69909bd..bb6ecee9b3c86 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -6,7 +6,7 @@ */ import { useState, useEffect, useMemo } from 'react'; - +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; @@ -23,6 +23,7 @@ interface Args { threshold?: number; skip?: boolean; criteriaFields?: CriteriaFields[]; + filterQuery?: estypes.QueryDslQueryContainer; } type Return = [boolean, Anomalies | null]; @@ -55,6 +56,7 @@ export const useAnomaliesTableData = ({ endDate, threshold = -1, skip = false, + filterQuery, }: Args): Return => { const [tableData, setTableData] = useState(null); const { isMlUser, jobs } = useInstalledSecurityJobs(); @@ -84,6 +86,7 @@ export const useAnomaliesTableData = ({ { jobIds, criteriaFields: criteriaFieldsInput, + filterQuery, aggregationInterval: 'auto', threshold: getThreshold(anomalyScore, threshold), earliestMs, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/api/anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/api/anomalies_table_data.ts index 2c83cb2e2d5b9..09d0286a0c66b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/api/anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/api/anomalies_table_data.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { Anomalies, InfluencerInput, CriteriaFields } from '../types'; import { KibanaServices } from '../../../lib/kibana'; @@ -19,6 +20,7 @@ export interface Body { dateFormatTz: string; maxRecords: number; maxExamples: number; + filterQuery?: estypes.QueryDslQueryContainer; } export const anomaliesTableData = async (body: Body, signal: AbortSignal): Promise => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/criteria/get_criteria_from_users_type.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/criteria/get_criteria_from_users_type.test.ts new file mode 100644 index 0000000000000..89a38bf34f9b8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/criteria/get_criteria_from_users_type.test.ts @@ -0,0 +1,26 @@ +/* + * 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 { UsersType } from '../../../../users/store/model'; +import { getCriteriaFromUsersType } from './get_criteria_from_users_type'; + +describe('get_criteria_from_user_type', () => { + test('returns user name from criteria if the user type is details', () => { + const criteria = getCriteriaFromUsersType(UsersType.details, 'admin'); + expect(criteria).toEqual([{ fieldName: 'user.name', fieldValue: 'admin' }]); + }); + + test('returns empty array from criteria if the user type is page but rather an empty array', () => { + const criteria = getCriteriaFromUsersType(UsersType.page, 'admin'); + expect(criteria).toEqual([]); + }); + + test('returns empty array from criteria if the user name is undefined and user type is details', () => { + const criteria = getCriteriaFromUsersType(UsersType.details, undefined); + expect(criteria).toEqual([]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/criteria/get_criteria_from_users_type.ts b/x-pack/plugins/security_solution/public/common/components/ml/criteria/get_criteria_from_users_type.ts new file mode 100644 index 0000000000000..4f3fa93c8fe2f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/criteria/get_criteria_from_users_type.ts @@ -0,0 +1,20 @@ +/* + * 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 { UsersType } from '../../../../users/store/model'; +import { CriteriaFields } from '../types'; + +export const getCriteriaFromUsersType = ( + type: UsersType, + userName: string | undefined +): CriteriaFields[] => { + if (type === UsersType.details && userName != null) { + return [{ fieldName: 'user.name', fieldValue: userName }]; + } else { + return []; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.test.ts new file mode 100644 index 0000000000000..c49707748142f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import { getUserNameFromInfluencers } from './get_user_name_from_influencers'; +import { mockAnomalies } from '../mock'; + +describe('get_user_name_from_influencers', () => { + let anomalies = cloneDeep(mockAnomalies); + + beforeEach(() => { + anomalies = cloneDeep(mockAnomalies); + }); + + test('returns user names from influencers from the mock', () => { + const userName = getUserNameFromInfluencers(anomalies.anomalies[0].influencers); + expect(userName).toEqual('root'); + }); + + test('returns null if there are no influencers from the mock', () => { + anomalies.anomalies[0].influencers = []; + const userName = getUserNameFromInfluencers(anomalies.anomalies[0].influencers); + expect(userName).toEqual(null); + }); + + test('returns null if it is given undefined influencers', () => { + const userName = getUserNameFromInfluencers(); + expect(userName).toEqual(null); + }); + + test('returns null if there influencers is an empty object', () => { + anomalies.anomalies[0].influencers = [{}]; + const userName = getUserNameFromInfluencers(anomalies.anomalies[0].influencers); + expect(userName).toEqual(null); + }); + + test('returns user name mixed with other data', () => { + anomalies.anomalies[0].influencers = [{ 'user.name': 'root' }, { 'source.ip': '127.0.0.1' }]; + const userName = getUserNameFromInfluencers(anomalies.anomalies[0].influencers); + expect(userName).toEqual('root'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.ts b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.ts new file mode 100644 index 0000000000000..da27c10aeaf85 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.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 { getEntries } from '../get_entries'; + +export const getUserNameFromInfluencers = ( + influencers: Array> = [], + userName?: string +): string | null => { + const recordFound = influencers.find((influencer) => { + const [influencerName, influencerValue] = getEntries(influencer); + if (influencerName === 'user.name') { + if (userName == null) { + return true; + } else { + return influencerValue === userName; + } + } else { + return false; + } + }); + if (recordFound != null) { + return Object.values(recordFound)[0]; + } else { + return null; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx index b4db5f6798860..e8575e19575c1 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx @@ -15,13 +15,12 @@ import * as i18n from './translations'; import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns'; import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts'; import { Loader } from '../../loader'; -import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies'; import { AnomaliesHostTableProps } from '../types'; import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; -import { hostEquality } from './host_equality'; import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type'; import { Panel } from '../../panel'; +import { anomaliesTableDefaultEquality } from './default_equality'; const sorting = { sort: { @@ -33,7 +32,6 @@ const sorting = { const AnomaliesHostTableComponent: React.FC = ({ startDate, endDate, - narrowDateRange, hostName, skip, type, @@ -44,18 +42,14 @@ const AnomaliesHostTableComponent: React.FC = ({ endDate, skip, criteriaFields: getCriteriaFromHostType(type, hostName), + filterQuery: { + exists: { field: 'host.name' }, + }, }); const hosts = convertAnomaliesToHosts(tableData, hostName); - const interval = getIntervalFromAnomalies(tableData); - const columns = getAnomaliesHostTableColumnsCurated( - type, - startDate, - endDate, - interval, - narrowDateRange - ); + const columns = getAnomaliesHostTableColumnsCurated(type, startDate, endDate); const pagination = { initialPageIndex: 0, initialPageSize: 10, @@ -94,4 +88,7 @@ const AnomaliesHostTableComponent: React.FC = ({ } }; -export const AnomaliesHostTable = React.memo(AnomaliesHostTableComponent, hostEquality); +export const AnomaliesHostTable = React.memo( + AnomaliesHostTableComponent, + anomaliesTableDefaultEquality +); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx new file mode 100644 index 0000000000000..f3ee7bb89e4c8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx @@ -0,0 +1,97 @@ +/* + * 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 { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; +import { HeaderSection } from '../../header_section'; + +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import * as i18n from './translations'; + +import { Loader } from '../../loader'; +import { AnomaliesUserTableProps } from '../types'; +import { useMlCapabilities } from '../hooks/use_ml_capabilities'; +import { BasicTable } from './basic_table'; + +import { getCriteriaFromUsersType } from '../criteria/get_criteria_from_users_type'; +import { Panel } from '../../panel'; +import { anomaliesTableDefaultEquality } from './default_equality'; +import { convertAnomaliesToUsers } from './convert_anomalies_to_users'; +import { getAnomaliesUserTableColumnsCurated } from './get_anomalies_user_table_columns'; + +const sorting = { + sort: { + field: 'anomaly.severity', + direction: 'desc', + }, +} as const; + +const AnomaliesUserTableComponent: React.FC = ({ + startDate, + endDate, + userName, + skip, + type, +}) => { + const capabilities = useMlCapabilities(); + + const [loading, tableData] = useAnomaliesTableData({ + startDate, + endDate, + skip, + criteriaFields: getCriteriaFromUsersType(type, userName), + filterQuery: { + exists: { field: 'user.name' }, + }, + }); + + const users = convertAnomaliesToUsers(tableData, userName); + + const columns = getAnomaliesUserTableColumnsCurated(type, startDate, endDate); + const pagination = { + initialPageIndex: 0, + initialPageSize: 10, + totalItemCount: users.length, + pageSizeOptions: [5, 10, 20, 50], + hidePerPageOptions: false, + }; + + if (!hasMlUserPermissions(capabilities)) { + return null; + } else { + return ( + + + + type is not as specific as EUI's... + columns={columns} + items={users} + pagination={pagination} + sorting={sorting} + /> + + {loading && ( + + )} + + ); + } +}; + +export const AnomaliesUserTable = React.memo( + AnomaliesUserTableComponent, + anomaliesTableDefaultEquality +); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.test.ts new file mode 100644 index 0000000000000..f81b1e019c33a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.test.ts @@ -0,0 +1,265 @@ +/* + * 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 { mockAnomalies } from '../mock'; +import { cloneDeep } from 'lodash/fp'; +import { convertAnomaliesToUsers, getUserNameFromEntity } from './convert_anomalies_to_users'; +import { AnomaliesByUser } from '../types'; + +describe('convert_anomalies_to_users', () => { + let anomalies = cloneDeep(mockAnomalies); + + beforeEach(() => { + anomalies = cloneDeep(mockAnomalies); + }); + + test('it returns expected anomalies from a user', () => { + const entities = convertAnomaliesToUsers(anomalies); + const expected: AnomaliesByUser[] = [ + { + anomaly: { + detectorIndex: 0, + entityName: 'process.name', + entityValue: 'du', + influencers: [ + { 'host.name': 'zeek-iowa' }, + { 'process.name': 'du' }, + { 'user.name': 'root' }, + ], + jobId: 'job-1', + rowId: '1561157194802_0', + severity: 16.193669439507826, + source: { + actual: [1], + bucket_span: 900, + by_field_name: 'process.name', + by_field_value: 'du', + detector_index: 0, + function: 'rare', + function_description: 'rare', + influencers: [ + { influencer_field_name: 'user.name', influencer_field_values: ['root'] }, + { influencer_field_name: 'process.name', influencer_field_values: ['du'] }, + { influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] }, + ], + initial_record_score: 16.193669439507826, + is_interim: false, + job_id: 'job-1', + multi_bucket_impact: 0, + partition_field_name: 'host.name', + partition_field_value: 'zeek-iowa', + probability: 0.024041164411288146, + record_score: 16.193669439507826, + result_type: 'record', + timestamp: 1560664800000, + typical: [0.024041164411288146], + }, + time: 1560664800000, + }, + userName: 'root', + }, + { + anomaly: { + detectorIndex: 0, + entityName: 'process.name', + entityValue: 'ls', + influencers: [ + { 'host.name': 'zeek-iowa' }, + { 'process.name': 'ls' }, + { 'user.name': 'root' }, + ], + jobId: 'job-2', + rowId: '1561157194802_1', + severity: 16.193669439507826, + source: { + actual: [1], + bucket_span: 900, + by_field_name: 'process.name', + by_field_value: 'ls', + detector_index: 0, + function: 'rare', + function_description: 'rare', + influencers: [ + { influencer_field_name: 'user.name', influencer_field_values: ['root'] }, + { influencer_field_name: 'process.name', influencer_field_values: ['ls'] }, + { influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] }, + ], + initial_record_score: 16.193669439507826, + is_interim: false, + job_id: 'job-2', + multi_bucket_impact: 0, + partition_field_name: 'host.name', + partition_field_value: 'zeek-iowa', + probability: 0.024041164411288146, + record_score: 16.193669439507826, + result_type: 'record', + timestamp: 1560664800000, + typical: [0.024041164411288146], + }, + time: 1560664800000, + }, + userName: 'root', + }, + ]; + expect(entities).toEqual(expected); + }); + + test('it returns empty anomalies if sent in a null', () => { + const entities = convertAnomaliesToUsers(null); + const expected: AnomaliesByUser[] = []; + expect(entities).toEqual(expected); + }); + + test('it returns a specific anomaly if sent in the user name of an anomaly', () => { + anomalies.anomalies[0].entityName = 'something-else'; + anomalies.anomalies[0].entityValue = 'something-else'; + anomalies.anomalies[0].influencers = [ + { 'host.name': 'zeek-iowa' }, + { 'process.name': 'du' }, + { 'user.name': 'something-else' }, + ]; + + const entities = convertAnomaliesToUsers(anomalies, 'root'); + const expected: AnomaliesByUser[] = [ + { + anomaly: { + detectorIndex: 0, + entityName: 'process.name', + entityValue: 'ls', + influencers: [ + { 'host.name': 'zeek-iowa' }, + { 'process.name': 'ls' }, + { 'user.name': 'root' }, + ], + jobId: 'job-2', + rowId: '1561157194802_1', + severity: 16.193669439507826, + source: { + actual: [1], + bucket_span: 900, + by_field_name: 'process.name', + by_field_value: 'ls', + detector_index: 0, + function: 'rare', + function_description: 'rare', + influencers: [ + { influencer_field_name: 'user.name', influencer_field_values: ['root'] }, + { influencer_field_name: 'process.name', influencer_field_values: ['ls'] }, + { influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] }, + ], + initial_record_score: 16.193669439507826, + is_interim: false, + job_id: 'job-2', + multi_bucket_impact: 0, + partition_field_name: 'host.name', + partition_field_value: 'zeek-iowa', + probability: 0.024041164411288146, + record_score: 16.193669439507826, + result_type: 'record', + timestamp: 1560664800000, + typical: [0.024041164411288146], + }, + time: 1560664800000, + }, + userName: 'root', + }, + ]; + expect(entities).toEqual(expected); + }); + + test('it returns a specific anomaly if an influencer has the user name', () => { + anomalies.anomalies[0].entityName = 'something-else'; + anomalies.anomalies[0].entityValue = 'something-else'; + anomalies.anomalies[0].influencers = [ + { 'host.name': 'zeek-iowa' }, + { 'process.name': 'du' }, + { 'user.name': 'something-else' }, + ]; + + anomalies.anomalies[1].entityName = 'something-else'; + anomalies.anomalies[1].entityValue = 'something-else'; + const entities = convertAnomaliesToUsers(anomalies, 'root'); + const expected: AnomaliesByUser[] = [ + { + anomaly: { + detectorIndex: 0, + entityName: 'something-else', + entityValue: 'something-else', + influencers: [ + { 'host.name': 'zeek-iowa' }, + { 'process.name': 'ls' }, + { 'user.name': 'root' }, + ], + jobId: 'job-2', + rowId: '1561157194802_1', + severity: 16.193669439507826, + source: { + actual: [1], + bucket_span: 900, + by_field_name: 'process.name', + by_field_value: 'ls', + detector_index: 0, + function: 'rare', + function_description: 'rare', + influencers: [ + { influencer_field_name: 'user.name', influencer_field_values: ['root'] }, + { influencer_field_name: 'process.name', influencer_field_values: ['ls'] }, + { influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] }, + ], + initial_record_score: 16.193669439507826, + is_interim: false, + job_id: 'job-2', + multi_bucket_impact: 0, + partition_field_name: 'host.name', + partition_field_value: 'zeek-iowa', + probability: 0.024041164411288146, + record_score: 16.193669439507826, + result_type: 'record', + timestamp: 1560664800000, + typical: [0.024041164411288146], + }, + time: 1560664800000, + }, + userName: 'root', + }, + ]; + expect(entities).toEqual(expected); + }); + + test('it returns empty anomalies if sent in the name of one that does not exist', () => { + const entities = convertAnomaliesToUsers(anomalies, 'some-made-up-name-here-for-you'); + const expected: AnomaliesByUser[] = []; + expect(entities).toEqual(expected); + }); + + test('it returns true for a found entity name passed in', () => { + anomalies.anomalies[0].entityName = 'user.name'; + anomalies.anomalies[0].entityValue = 'admin'; + const found = getUserNameFromEntity(anomalies.anomalies[0], 'admin'); + expect(found).toEqual(true); + }); + + test('it returns false for an entity name that does not exist', () => { + anomalies.anomalies[0].entityName = 'user.name'; + anomalies.anomalies[0].entityValue = 'admin'; + const found = getUserNameFromEntity(anomalies.anomalies[0], 'some-made-up-entity-name'); + expect(found).toEqual(false); + }); + + test('it returns true for an entity that has user.name within it if no name is passed in', () => { + anomalies.anomalies[0].entityName = 'user.name'; + anomalies.anomalies[0].entityValue = 'something-made-up'; + const found = getUserNameFromEntity(anomalies.anomalies[0]); + expect(found).toEqual(true); + }); + + test('it returns false for an entity that is not user.name and no name is passed in', () => { + anomalies.anomalies[0].entityName = 'made-up'; + const found = getUserNameFromEntity(anomalies.anomalies[0]); + expect(found).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.ts new file mode 100644 index 0000000000000..8f69604fc5510 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.ts @@ -0,0 +1,41 @@ +/* + * 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 { Anomalies, AnomaliesByUser, Anomaly } from '../types'; +import { getUserNameFromInfluencers } from '../influencers/get_user_name_from_influencers'; + +export const convertAnomaliesToUsers = ( + anomalies: Anomalies | null, + userName?: string +): AnomaliesByUser[] => { + if (anomalies == null) { + return []; + } else { + return anomalies.anomalies.reduce((accum, item) => { + if (getUserNameFromEntity(item, userName)) { + return [...accum, { userName: item.entityValue, anomaly: item }]; + } else { + const userNameFromInfluencers = getUserNameFromInfluencers(item.influencers, userName); + if (userNameFromInfluencers != null) { + return [...accum, { userName: userNameFromInfluencers, anomaly: item }]; + } else { + return accum; + } + } + }, []); + } +}; + +export const getUserNameFromEntity = (anomaly: Anomaly, userName?: string): boolean => { + if (anomaly.entityName !== 'user.name') { + return false; + } else if (userName == null) { + return true; + } else { + return anomaly.entityValue === userName; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/create_compound_key.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/create_compound_key.test.ts index 8c80cf7901960..c70d38ca51b12 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/create_compound_key.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/create_compound_key.test.ts @@ -7,8 +7,7 @@ import { mockAnomalies } from '../mock'; import { cloneDeep } from 'lodash/fp'; -import { createCompoundHostKey, createCompoundNetworkKey } from './create_compound_key'; -import { AnomaliesByHost, AnomaliesByNetwork } from '../types'; +import { createCompoundAnomalyKey } from './create_compound_key'; describe('create_explorer_link', () => { let anomalies = cloneDeep(mockAnomalies); @@ -17,22 +16,8 @@ describe('create_explorer_link', () => { anomalies = cloneDeep(mockAnomalies); }); - test('it creates a compound host key', () => { - const anomaliesByHost: AnomaliesByHost = { - hostName: 'some-host-name', - anomaly: anomalies.anomalies[0], - }; - const key = createCompoundHostKey(anomaliesByHost); - expect(key).toEqual('some-host-name-process.name-du-16.193669439507826-job-1'); - }); - - test('it creates a compound network key', () => { - const anomaliesByNetwork: AnomaliesByNetwork = { - type: 'destination.ip', - ip: '127.0.0.1', - anomaly: anomalies.anomalies[0], - }; - const key = createCompoundNetworkKey(anomaliesByNetwork); - expect(key).toEqual('127.0.0.1-process.name-du-16.193669439507826-job-1'); + test('it creates a compound anomaly key', () => { + const key = createCompoundAnomalyKey(anomalies.anomalies[0]); + expect(key).toEqual('process.name-du-16.193669439507826-job-1'); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/create_compound_key.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/create_compound_key.ts index c6e0773f0ab0c..f9ef0ff9285fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/create_compound_key.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/create_compound_key.ts @@ -5,10 +5,7 @@ * 2.0. */ -import { AnomaliesByHost, AnomaliesByNetwork } from '../types'; +import { Anomaly } from '../types'; -export const createCompoundHostKey = (anomaliesByHost: AnomaliesByHost): string => - `${anomaliesByHost.hostName}-${anomaliesByHost.anomaly.entityName}-${anomaliesByHost.anomaly.entityValue}-${anomaliesByHost.anomaly.severity}-${anomaliesByHost.anomaly.jobId}`; - -export const createCompoundNetworkKey = (anomaliesByNetwork: AnomaliesByNetwork): string => - `${anomaliesByNetwork.ip}-${anomaliesByNetwork.anomaly.entityName}-${anomaliesByNetwork.anomaly.entityValue}-${anomaliesByNetwork.anomaly.severity}-${anomaliesByNetwork.anomaly.jobId}`; +export const createCompoundAnomalyKey = (anomaly: Anomaly): string => + `${anomaly.entityName}-${anomaly.entityValue}-${anomaly.severity}-${anomaly.jobId}`; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.test.ts similarity index 89% rename from x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts rename to x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.test.ts index 475ed4f579c4d..bbc2a9251c41d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { hostEquality } from './host_equality'; +import { anomaliesTableDefaultEquality } from './default_equality'; import { AnomaliesHostTableProps } from '../types'; import { HostsType } from '../../../../hosts/store/model'; @@ -25,7 +25,7 @@ describe('host_equality', () => { skip: false, type: HostsType.details, }; - const equal = hostEquality(prev, next); + const equal = anomaliesTableDefaultEquality(prev, next); expect(equal).toEqual(true); }); @@ -44,7 +44,7 @@ describe('host_equality', () => { skip: false, type: HostsType.details, }; - const equal = hostEquality(prev, next); + const equal = anomaliesTableDefaultEquality(prev, next); expect(equal).toEqual(false); }); @@ -63,7 +63,7 @@ describe('host_equality', () => { skip: false, type: HostsType.details, }; - const equal = hostEquality(prev, next); + const equal = anomaliesTableDefaultEquality(prev, next); expect(equal).toEqual(false); }); @@ -82,7 +82,7 @@ describe('host_equality', () => { skip: false, type: HostsType.details, }; - const equal = hostEquality(prev, next); + const equal = anomaliesTableDefaultEquality(prev, next); expect(equal).toEqual(false); }); @@ -101,7 +101,7 @@ describe('host_equality', () => { skip: false, type: HostsType.details, }; - const equal = hostEquality(prev, next); + const equal = anomaliesTableDefaultEquality(prev, next); expect(equal).toEqual(false); }); @@ -120,7 +120,7 @@ describe('host_equality', () => { skip: false, type: HostsType.details, }; - const equal = hostEquality(prev, next); + const equal = anomaliesTableDefaultEquality(prev, next); expect(equal).toEqual(false); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.ts similarity index 68% rename from x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.ts rename to x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.ts index 69c67fc4ca2c1..213bd922b51f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/host_equality.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { AnomaliesHostTableProps } from '../types'; +import { AnomaliesTableCommonProps } from '../types'; -export const hostEquality = ( - prevProps: AnomaliesHostTableProps, - nextProps: AnomaliesHostTableProps +export const anomaliesTableDefaultEquality = ( + prevProps: AnomaliesTableCommonProps, + nextProps: AnomaliesTableCommonProps ): boolean => prevProps.startDate === nextProps.startDate && prevProps.endDate === nextProps.endDate && diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 45883019b9ff8..364c24f000905 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -5,121 +5,35 @@ * 2.0. */ -import React from 'react'; - import '../../../mock/match_media'; import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns'; import { HostsType } from '../../../../hosts/store/model'; import * as i18n from './translations'; -import { AnomaliesByHost, Anomaly } from '../types'; -import { Columns } from '../../paginated_table'; -import { TestProviders } from '../../../mock'; -import { useMountAppended } from '../../../utils/use_mount_appended'; jest.mock('../../../lib/kibana'); const startDate = new Date(2001).toISOString(); const endDate = new Date(3000).toISOString(); -const interval = 'days'; -const narrowDateRange = jest.fn(); -describe('get_anomalies_host_table_columns', () => { - const mount = useMountAppended(); +describe('get_anomalies_host_table_columns', () => { test('on hosts page, we expect to get all columns', () => { - expect( - getAnomaliesHostTableColumnsCurated( - HostsType.page, - startDate, - endDate, - interval, - narrowDateRange - ).length - ).toEqual(6); + expect(getAnomaliesHostTableColumnsCurated(HostsType.page, startDate, endDate).length).toEqual( + 6 + ); }); test('on host details page, we expect to remove one columns', () => { - const columns = getAnomaliesHostTableColumnsCurated( - HostsType.details, - startDate, - endDate, - interval, - narrowDateRange - ); + const columns = getAnomaliesHostTableColumnsCurated(HostsType.details, startDate, endDate); expect(columns.length).toEqual(5); }); test('on host page, we should have Host Name', () => { - const columns = getAnomaliesHostTableColumnsCurated( - HostsType.page, - startDate, - endDate, - interval, - narrowDateRange - ); + const columns = getAnomaliesHostTableColumnsCurated(HostsType.page, startDate, endDate); expect(columns.some((col) => col.name === i18n.HOST_NAME)).toEqual(true); }); test('on host details page, we should not have Host Name', () => { - const columns = getAnomaliesHostTableColumnsCurated( - HostsType.details, - startDate, - endDate, - interval, - narrowDateRange - ); + const columns = getAnomaliesHostTableColumnsCurated(HostsType.details, startDate, endDate); expect(columns.some((col) => col.name === i18n.HOST_NAME)).toEqual(false); }); - - test('on host page, undefined influencers should turn into an empty column string', () => { - const columns = getAnomaliesHostTableColumnsCurated( - HostsType.page, - startDate, - endDate, - interval, - narrowDateRange - ); - const column = columns.find((col) => col.name === i18n.INFLUENCED_BY) as Columns< - Anomaly['influencers'], - AnomaliesByHost - >; - const anomaly: AnomaliesByHost = { - hostName: 'host.name', - anomaly: { - detectorIndex: 0, - entityName: 'entity-name-1', - entityValue: 'entity-value-1', - jobId: 'job-1', - rowId: 'row-1', - severity: 100, - time: new Date('01/01/2000').valueOf(), - source: { - job_id: 'job-1', - result_type: 'result-1', - probability: 50, - multi_bucket_impact: 0, - record_score: 0, - initial_record_score: 0, - bucket_span: 0, - detector_index: 0, - is_interim: true, - timestamp: new Date('01/01/2000').valueOf(), - by_field_name: 'some field name', - by_field_value: 'some field value', - partition_field_name: 'partition field name', - partition_field_value: 'partition field value', - function: 'function-1', - function_description: 'description-1', - typical: [5, 3], - actual: [7, 4], - influencers: [], - }, - }, - }; - if (column != null && column.render != null) { - const wrapper = mount({column.render(undefined, anomaly)}); - expect(wrapper.text()).toEqual(''); - } else { - expect(column).not.toBe(null); - } - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx index 468cb416a2f9b..7ee0b3ca88469 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx @@ -6,27 +6,18 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Columns } from '../../paginated_table'; -import { AnomaliesByHost, Anomaly, NarrowDateRange } from '../types'; +import { AnomaliesByHost, Anomaly } from '../types'; import { getRowItemDraggable } from '../../tables/helpers'; -import { EntityDraggable } from '../entity_draggable'; -import { createCompoundHostKey } from './create_compound_key'; +import { createCompoundAnomalyKey } from './create_compound_key'; import { HostDetailsLink } from '../../links'; - import * as i18n from './translations'; -import { getEntries } from '../get_entries'; -import { DraggableScore } from '../score/draggable_score'; -import { ExplorerLink } from '../links/create_explorer_link'; import { HostsType } from '../../../../hosts/store/model'; -import { escapeDataProviderId } from '../../drag_and_drop/helpers'; -import { FormattedRelativePreferenceDate } from '../../formatted_date'; +import { getAnomaliesDefaultTableColumns } from './get_anomalies_table_columns'; export const getAnomaliesHostTableColumns = ( startDate: string, - endDate: string, - interval: string, - narrowDateRange: NarrowDateRange + endDate: string ): [ Columns, Columns, @@ -43,100 +34,21 @@ export const getAnomaliesHostTableColumns = ( getRowItemDraggable({ rowItem: hostName, attrName: 'host.name', - idPrefix: `anomalies-host-table-hostName-${createCompoundHostKey( - anomaliesByHost + idPrefix: `anomalies-host-table-hostName-${createCompoundAnomalyKey( + anomaliesByHost.anomaly )}-hostName`, render: (item) => , }), }, - { - name: i18n.DETECTOR, - field: 'anomaly.jobId', - sortable: true, - render: (jobId, anomaliesByHost) => ( - - ), - }, - { - name: i18n.SCORE, - field: 'anomaly.severity', - sortable: true, - render: (_, anomaliesByHost) => ( - - ), - }, - { - name: i18n.ENTITY, - field: 'anomaly.entityValue', - sortable: true, - render: (entityValue, anomaliesByHost) => ( - - ), - }, - { - name: i18n.INFLUENCED_BY, - field: 'anomaly.influencers', - render: (influencers, anomaliesByHost) => ( - - {influencers && - influencers.map((influencer) => { - const [key, value] = getEntries(influencer); - const entityName = key != null ? key : ''; - const entityValue = value != null ? value : ''; - return ( - - - - - - - - ); - })} - - ), - }, - { - name: i18n.TIME_STAMP, - field: 'anomaly.time', - sortable: true, - render: (time) => , - }, + ...getAnomaliesDefaultTableColumns(startDate, endDate), ]; export const getAnomaliesHostTableColumnsCurated = ( pageType: HostsType, startDate: string, - endDate: string, - interval: string, - narrowDateRange: NarrowDateRange + endDate: string ) => { - const columns = getAnomaliesHostTableColumns(startDate, endDate, interval, narrowDateRange); + const columns = getAnomaliesHostTableColumns(startDate, endDate); // Columns to exclude from host details pages if (pageType === HostsType.details) { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx index 817205ce22808..96917bb8ed739 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -9,19 +9,12 @@ import '../../../mock/match_media'; import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns'; import { NetworkType } from '../../../../network/store/model'; import * as i18n from './translations'; -import { AnomaliesByNetwork, Anomaly } from '../types'; -import { Columns } from '../../paginated_table'; -import React from 'react'; -import { TestProviders } from '../../../mock'; -import { useMountAppended } from '../../../utils/use_mount_appended'; jest.mock('../../../../common/lib/kibana'); const startDate = new Date(2001).toISOString(); const endDate = new Date(3000).toISOString(); describe('get_anomalies_network_table_columns', () => { - const mount = useMountAppended(); - test('on network page, we expect to get all columns', () => { expect( getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate).length @@ -42,52 +35,4 @@ describe('get_anomalies_network_table_columns', () => { const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.details, startDate, endDate); expect(columns.some((col) => col.name === i18n.NETWORK_NAME)).toEqual(false); }); - - test('on network page, undefined influencers should turn into an empty column string', () => { - const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate); - const column = columns.find((col) => col.name === i18n.INFLUENCED_BY) as Columns< - Anomaly['influencers'], - AnomaliesByNetwork - >; - const anomaly: AnomaliesByNetwork = { - type: 'source.ip', - ip: '127.0.0.1', - anomaly: { - detectorIndex: 0, - entityName: 'entity-name-1', - entityValue: 'entity-value-1', - jobId: 'job-1', - rowId: 'row-1', - severity: 100, - time: new Date('01/01/2000').valueOf(), - source: { - job_id: 'job-1', - result_type: 'result-1', - probability: 50, - multi_bucket_impact: 0, - record_score: 0, - initial_record_score: 0, - bucket_span: 0, - detector_index: 0, - is_interim: true, - timestamp: new Date('01/01/2000').valueOf(), - by_field_name: 'some field name', - by_field_value: 'some field value', - partition_field_name: 'partition field name', - partition_field_value: 'partition field value', - function: 'function-1', - function_description: 'description-1', - typical: [5, 3], - actual: [7, 4], - influencers: [], - }, - }, - }; - if (column != null && column.render != null) { - const wrapper = mount({column.render(undefined, anomaly)}); - expect(wrapper.text()).toEqual(''); - } else { - expect(column).not.toBe(null); - } - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx index 4c4e131a9d467..44c6a193c30fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -6,23 +6,17 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Columns } from '../../paginated_table'; import { Anomaly, AnomaliesByNetwork } from '../types'; import { getRowItemDraggable } from '../../tables/helpers'; -import { EntityDraggable } from '../entity_draggable'; -import { createCompoundNetworkKey } from './create_compound_key'; +import { createCompoundAnomalyKey } from './create_compound_key'; import { NetworkDetailsLink } from '../../links'; import * as i18n from './translations'; -import { getEntries } from '../get_entries'; -import { DraggableScore } from '../score/draggable_score'; -import { ExplorerLink } from '../links/create_explorer_link'; -import { FormattedRelativePreferenceDate } from '../../formatted_date'; import { NetworkType } from '../../../../network/store/model'; -import { escapeDataProviderId } from '../../drag_and_drop/helpers'; import { FlowTarget } from '../../../../../common/search_strategy'; +import { getAnomaliesDefaultTableColumns } from './get_anomalies_table_columns'; export const getAnomaliesNetworkTableColumns = ( startDate: string, @@ -44,84 +38,13 @@ export const getAnomaliesNetworkTableColumns = ( getRowItemDraggable({ rowItem: ip, attrName: anomaliesByNetwork.type, - idPrefix: `anomalies-network-table-ip-${createCompoundNetworkKey(anomaliesByNetwork)}`, + idPrefix: `anomalies-network-table-ip-${createCompoundAnomalyKey( + anomaliesByNetwork.anomaly + )}`, render: (item) => , }), }, - { - name: i18n.DETECTOR, - field: 'anomaly.jobId', - sortable: true, - render: (jobId, anomaliesByHost) => ( - - ), - }, - { - name: i18n.SCORE, - field: 'anomaly.severity', - sortable: true, - render: (_, anomaliesByNetwork) => ( - - ), - }, - { - name: i18n.ENTITY, - field: 'anomaly.entityValue', - sortable: true, - render: (entityValue, anomaliesByNetwork) => ( - - ), - }, - { - name: i18n.INFLUENCED_BY, - field: 'anomaly.influencers', - render: (influencers, anomaliesByNetwork) => ( - - {influencers && - influencers.map((influencer) => { - const [key, value] = getEntries(influencer); - const entityName = key != null ? key : ''; - const entityValue = value != null ? value : ''; - return ( - - - - ); - })} - - ), - }, - { - name: i18n.TIME_STAMP, - field: 'anomaly.time', - sortable: true, - render: (time) => , - }, + ...getAnomaliesDefaultTableColumns(startDate, endDate), ]; export const getAnomaliesNetworkTableColumnsCurated = ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.test.tsx new file mode 100644 index 0000000000000..3df8d545d2871 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 '../../../mock/match_media'; +import * as i18n from './translations'; +import { AnomaliesBy, Anomaly } from '../types'; +import { Columns } from '../../paginated_table'; +import React from 'react'; +import { TestProviders } from '../../../mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; +import { getAnomaliesDefaultTableColumns } from './get_anomalies_table_columns'; + +jest.mock('../../../../common/lib/kibana'); + +const startDate = new Date(2001).toISOString(); +const endDate = new Date(3000).toISOString(); +describe('getAnomaliesDefaultTableColumns', () => { + const mount = useMountAppended(); + + test('it should return all columns', () => { + expect(getAnomaliesDefaultTableColumns(startDate, endDate).length).toEqual(5); + }); + + test('it should return an empty column string for undefined influencers', () => { + const columns = getAnomaliesDefaultTableColumns(startDate, endDate); + const column = columns.find((col) => col.name === i18n.INFLUENCED_BY) as Columns< + Anomaly['influencers'], + AnomaliesBy + >; + const anomaly: AnomaliesBy = { + anomaly: { + detectorIndex: 0, + entityName: 'entity-name-1', + entityValue: 'entity-value-1', + jobId: 'job-1', + rowId: 'row-1', + severity: 100, + time: new Date('01/01/2000').valueOf(), + source: { + job_id: 'job-1', + result_type: 'result-1', + probability: 50, + multi_bucket_impact: 0, + record_score: 0, + initial_record_score: 0, + bucket_span: 0, + detector_index: 0, + is_interim: true, + timestamp: new Date('01/01/2000').valueOf(), + by_field_name: 'some field name', + by_field_value: 'some field value', + partition_field_name: 'partition field name', + partition_field_value: 'partition field value', + function: 'function-1', + function_description: 'description-1', + typical: [5, 3], + actual: [7, 4], + influencers: [], + }, + }, + }; + if (column != null && column.render != null) { + const wrapper = mount({column.render(undefined, anomaly)}); + expect(wrapper.text()).toEqual(''); + } else { + expect(column).not.toBe(null); + } + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.tsx new file mode 100644 index 0000000000000..eb3d7aac2ae2f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_table_columns.tsx @@ -0,0 +1,111 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Columns } from '../../paginated_table'; +import { AnomaliesBy, Anomaly } from '../types'; + +import { EntityDraggable } from '../entity_draggable'; +import { createCompoundAnomalyKey } from './create_compound_key'; + +import * as i18n from './translations'; +import { getEntries } from '../get_entries'; +import { DraggableScore } from '../score/draggable_score'; +import { ExplorerLink } from '../links/create_explorer_link'; +import { escapeDataProviderId } from '../../drag_and_drop/helpers'; +import { FormattedRelativePreferenceDate } from '../../formatted_date'; + +export const getAnomaliesDefaultTableColumns = ( + startDate: string, + endDate: string +): [ + Columns, + Columns, + Columns, + Columns, + Columns +] => [ + { + name: i18n.DETECTOR, + field: 'anomaly.jobId', + sortable: true, + render: (jobId, anomalyBy) => ( + + ), + }, + { + name: i18n.SCORE, + field: 'anomaly.severity', + sortable: true, + render: (_, anomalyBy) => ( + + ), + }, + { + name: i18n.ENTITY, + field: 'anomaly.entityValue', + sortable: true, + render: (entityValue, anomalyBy) => ( + + ), + }, + { + name: i18n.INFLUENCED_BY, + field: 'anomaly.influencers', + render: (influencers, anomalyBy) => ( + + {influencers && + influencers.map((influencer) => { + const [key, value] = getEntries(influencer); + const entityName = key != null ? key : ''; + const entityValue = value != null ? value : ''; + return ( + + + + + + + + ); + })} + + ), + }, + { + name: i18n.TIME_STAMP, + field: 'anomaly.time', + sortable: true, + render: (time) => , + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.test.tsx new file mode 100644 index 0000000000000..5905a7ceaa114 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.test.tsx @@ -0,0 +1,40 @@ +/* + * 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 { UsersType } from '../../../../users/store/model'; +import '../../../mock/match_media'; +import { getAnomaliesUserTableColumnsCurated } from './get_anomalies_user_table_columns'; + +import * as i18n from './translations'; + +jest.mock('../../../lib/kibana'); + +const startDate = new Date(2001).toISOString(); +const endDate = new Date(3000).toISOString(); + +describe('get_anomalies_user_table_columns', () => { + test('on users page, we expect to get all columns', () => { + expect(getAnomaliesUserTableColumnsCurated(UsersType.page, startDate, endDate).length).toEqual( + 6 + ); + }); + + test('on user details page, we expect to remove one columns', () => { + const columns = getAnomaliesUserTableColumnsCurated(UsersType.details, startDate, endDate); + expect(columns.length).toEqual(5); + }); + + test('on users page, we should have User Name', () => { + const columns = getAnomaliesUserTableColumnsCurated(UsersType.page, startDate, endDate); + expect(columns.some((col) => col.name === i18n.USER_NAME)).toEqual(true); + }); + + test('on user details page, we should not have User Name', () => { + const columns = getAnomaliesUserTableColumnsCurated(UsersType.details, startDate, endDate); + expect(columns.some((col) => col.name === i18n.USER_NAME)).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx new file mode 100644 index 0000000000000..6bc9aefecfae1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_user_table_columns.tsx @@ -0,0 +1,60 @@ +/* + * 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 { Columns } from '../../paginated_table'; +import { AnomaliesByUser, Anomaly } from '../types'; +import { getRowItemDraggable } from '../../tables/helpers'; +import { createCompoundAnomalyKey } from './create_compound_key'; +import { UserDetailsLink } from '../../links'; + +import * as i18n from './translations'; +import { UsersType } from '../../../../users/store/model'; +import { getAnomaliesDefaultTableColumns } from './get_anomalies_table_columns'; + +export const getAnomaliesUserTableColumns = ( + startDate: string, + endDate: string +): [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +] => [ + { + name: i18n.USER_NAME, + field: 'userName', + sortable: true, + render: (userName, anomaliesByUser) => + getRowItemDraggable({ + rowItem: userName, + attrName: 'user.name', + idPrefix: `anomalies-user-table-userName-${createCompoundAnomalyKey( + anomaliesByUser.anomaly + )}-userName`, + render: (item) => , + }), + }, + ...getAnomaliesDefaultTableColumns(startDate, endDate), +]; + +export const getAnomaliesUserTableColumnsCurated = ( + pageType: UsersType, + startDate: string, + endDate: string +) => { + const columns = getAnomaliesUserTableColumns(startDate, endDate); + + // Columns to exclude from user details pages + if (pageType === UsersType.details) { + return columns.filter((column) => column.name !== i18n.USER_NAME); + } else { + return columns; + } +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.ts index 080720bd99808..4354fa75f5f91 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.ts @@ -6,12 +6,11 @@ */ import { AnomaliesNetworkTableProps } from '../types'; +import { anomaliesTableDefaultEquality } from './default_equality'; export const networkEquality = ( prevProps: AnomaliesNetworkTableProps, nextProps: AnomaliesNetworkTableProps ): boolean => - prevProps.startDate === nextProps.startDate && - prevProps.endDate === nextProps.endDate && - prevProps.skip === nextProps.skip && + anomaliesTableDefaultEquality(prevProps, nextProps) && prevProps.flowTarget === nextProps.flowTarget; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/translations.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/translations.ts index 20e2896170c5c..e0c7a169bf9e3 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/translations.ts @@ -42,6 +42,10 @@ export const HOST_NAME = i18n.translate('xpack.securitySolution.ml.table.hostNam defaultMessage: 'Host name', }); +export const USER_NAME = i18n.translate('xpack.securitySolution.ml.table.userNameTitle', { + defaultMessage: 'User name', +}); + export const INFLUENCED_BY = i18n.translate('xpack.securitySolution.ml.table.influencedByTitle', { defaultMessage: 'Influenced by', }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/types.ts b/x-pack/plugins/security_solution/public/common/components/ml/types.ts index 494c8a522ffac..c5be26fb57648 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/types.ts @@ -10,6 +10,7 @@ import { FlowTarget } from '../../../../common/search_strategy'; import { HostsType } from '../../../hosts/store/model'; import { NetworkType } from '../../../network/store/model'; +import { UsersType } from '../../../users/store/model'; export interface Source { job_id: string; @@ -62,37 +63,48 @@ export interface Anomalies { export type NarrowDateRange = (score: Anomaly, interval: string) => void; -export interface AnomaliesByHost { - hostName: string; +export interface AnomaliesBy { anomaly: Anomaly; } +export interface AnomaliesByHost extends AnomaliesBy { + hostName: string; +} + export type DestinationOrSource = 'source.ip' | 'destination.ip'; -export interface AnomaliesByNetwork { +export interface AnomaliesByNetwork extends AnomaliesBy { type: DestinationOrSource; ip: string; - anomaly: Anomaly; } -export interface HostOrNetworkProps { +export interface AnomaliesByUser extends AnomaliesBy { + userName: string; +} + +export interface AnomaliesTableCommonProps { startDate: string; endDate: string; narrowDateRange: NarrowDateRange; skip: boolean; } -export type AnomaliesHostTableProps = HostOrNetworkProps & { +export type AnomaliesHostTableProps = AnomaliesTableCommonProps & { hostName?: string; type: HostsType; }; -export type AnomaliesNetworkTableProps = HostOrNetworkProps & { +export type AnomaliesNetworkTableProps = AnomaliesTableCommonProps & { ip?: string; type: NetworkType; flowTarget?: FlowTarget; }; +export type AnomaliesUserTableProps = AnomaliesTableCommonProps & { + userName?: string; + type: UsersType; +}; + const sourceOrDestination = ['source.ip', 'destination.ip']; export const isDestinationOrSource = (value: string | null): value is DestinationOrSource => diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts index 2d3bb00501da5..ee3ace3819fd3 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts @@ -12,9 +12,10 @@ import { GlobalTimeArgs } from '../../use_global_time'; import { HostsType } from '../../../../hosts/store/model'; import { NetworkType } from '../../../../network/store//model'; import { FlowTarget } from '../../../../../common/search_strategy'; +import { UsersType } from '../../../../users/store/model'; interface QueryTabBodyProps { - type: HostsType | NetworkType; + type: HostsType | NetworkType | UsersType; filterQuery?: string | ESTermQuery; } diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 39e65a8c4e364..b65a8ece16c03 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -205,6 +205,7 @@ export const mockGlobalState: State = { limit: 10, // TODO sort: { field: RiskScoreFields.riskScore, direction: Direction.desc }, }, + [usersModel.UsersTableType.anomalies]: null, }, }, details: { @@ -214,6 +215,7 @@ export const mockGlobalState: State = { limit: 10, // TODO sort: { field: HostRulesFields.riskScore, direction: Direction.desc }, }, + [usersModel.UsersTableType.anomalies]: null, }, }, }, diff --git a/x-pack/plugins/security_solution/public/users/jest.config.js b/x-pack/plugins/security_solution/public/users/jest.config.js new file mode 100644 index 0000000000000..563491dd5befe --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/jest.config.js @@ -0,0 +1,24 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/users'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/users', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/security_solution/public/users/**/*.{ts,tsx}'], + // See: https://github.com/elastic/kibana/issues/117255, the moduleNameMapper creates mocks to avoid memory leaks from kibana core. + moduleNameMapper: { + 'core/server$': '/x-pack/plugins/security_solution/server/__mocks__/core.mock.ts', + 'task_manager/server$': + '/x-pack/plugins/security_solution/server/__mocks__/task_manager.mock.ts', + 'alerting/server$': '/x-pack/plugins/security_solution/server/__mocks__/alert.mock.ts', + 'actions/server$': '/x-pack/plugins/security_solution/server/__mocks__/action.mock.ts', + }, +}; diff --git a/x-pack/plugins/security_solution/public/users/pages/constants.ts b/x-pack/plugins/security_solution/public/users/pages/constants.ts index bb273bf479aec..48c2937657721 100644 --- a/x-pack/plugins/security_solution/public/users/pages/constants.ts +++ b/x-pack/plugins/security_solution/public/users/pages/constants.ts @@ -10,6 +10,6 @@ import { UsersTableType } from '../store/model'; export const usersDetailsPagePath = `${USERS_PATH}/:detailName`; -export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers})`; +export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers}|${UsersTableType.anomalies})`; export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.allUsers})`; diff --git a/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx index 2eff1efb85789..ddff8109daf24 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx @@ -17,7 +17,7 @@ export const navTabsUsersDetails = (hostName: string): UsersDetailsNavTab => { return { [UsersTableType.allUsers]: { id: UsersTableType.allUsers, - name: i18n.ALL_USERS_TITLE, + name: i18n.NAVIGATION_ALL_USERS_TITLE, href: getTabsOnUsersDetailsUrl(hostName, UsersTableType.allUsers), disabled: false, }, diff --git a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts index d69cac731df3e..f490e3e46914e 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts @@ -21,7 +21,8 @@ import { SecurityPageName } from '../../../app/types'; export const type = usersModel.UsersType.details; const TabNameMappedToI18nKey: Record = { - [UsersTableType.allUsers]: i18n.ALL_USERS_TITLE, + [UsersTableType.allUsers]: i18n.NAVIGATION_ALL_USERS_TITLE, + [UsersTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, }; export const getBreadcrumbs = ( diff --git a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx index efe8447b9b71b..beffcb879cea0 100644 --- a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx @@ -15,8 +15,14 @@ const getTabsOnUsersUrl = (tabName: UsersTableType) => `${USERS_PATH}/${tabName} export const navTabsUsers: UsersNavTab = { [UsersTableType.allUsers]: { id: UsersTableType.allUsers, - name: i18n.ALL_USERS_TITLE, + name: i18n.NAVIGATION_ALL_USERS_TITLE, href: getTabsOnUsersUrl(UsersTableType.allUsers), disabled: false, }, + [UsersTableType.anomalies]: { + id: UsersTableType.anomalies, + name: i18n.NAVIGATION_ANOMALIES_TITLE, + href: getTabsOnUsersUrl(UsersTableType.anomalies), + disabled: false, + }, }; diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts index b965dc914d513..53f7c74871084 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts @@ -10,7 +10,7 @@ import { ESTermQuery } from '../../../../common/typed_json'; import { DocValueFields } from '../../../../../timelines/common'; import { NavTab } from '../../../common/components/navigation/types'; -type KeyUsersNavTab = UsersTableType.allUsers; +type KeyUsersNavTab = UsersTableType.allUsers | UsersTableType.anomalies; export type UsersNavTab = Record; export interface QueryTabBodyProps { diff --git a/x-pack/plugins/security_solution/public/users/pages/translations.ts b/x-pack/plugins/security_solution/public/users/pages/translations.ts index bfcd29be4236c..4bcfc01e41706 100644 --- a/x-pack/plugins/security_solution/public/users/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/users/pages/translations.ts @@ -11,6 +11,16 @@ export const PAGE_TITLE = i18n.translate('xpack.securitySolution.users.pageTitle defaultMessage: 'Users', }); -export const ALL_USERS_TITLE = i18n.translate('xpack.securitySolution.users.allUsers', { - defaultMessage: 'All users', -}); +export const NAVIGATION_ALL_USERS_TITLE = i18n.translate( + 'xpack.securitySolution.users.navigation.allUsersTitle', + { + defaultMessage: 'All users', + } +); + +export const NAVIGATION_ANOMALIES_TITLE = i18n.translate( + 'xpack.securitySolution.users.navigation.anomaliesTitle', + { + defaultMessage: 'Anomalies', + } +); diff --git a/x-pack/plugins/security_solution/public/users/pages/users_tabs.test.tsx b/x-pack/plugins/security_solution/public/users/pages/users_tabs.test.tsx new file mode 100644 index 0000000000000..bd4ce3cbae1e3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/users_tabs.test.tsx @@ -0,0 +1,79 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { Router } from 'react-router-dom'; + +import '../../common/mock/match_media'; +import { TestProviders } from '../../common/mock'; +import { SecuritySolutionTabNavigation } from '../../common/components/navigation'; +import { Users } from './users'; +import { useSourcererDataView } from '../../common/containers/sourcerer'; + +jest.mock('../../common/containers/sourcerer'); +jest.mock('../../common/components/search_bar', () => ({ + SiemSearchBar: () => null, +})); +jest.mock('../../common/components/query_bar', () => ({ + QueryBar: () => null, +})); + +type Action = 'PUSH' | 'POP' | 'REPLACE'; +const pop: Action = 'POP'; +const location = { + pathname: '/network', + search: '', + state: '', + hash: '', +}; +const mockHistory = { + length: 2, + location, + action: pop, + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + block: jest.fn(), + createHref: jest.fn(), + listen: jest.fn(), +}; +const mockUseSourcererDataView = useSourcererDataView as jest.Mock; +describe('Users - rendering', () => { + test('it renders the Setup Instructions text when no index is available', async () => { + mockUseSourcererDataView.mockReturnValue({ + indicesExist: false, + }); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + }); + + test('it should render tab navigation', async () => { + mockUseSourcererDataView.mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find(SecuritySolutionTabNavigation).exists()).toBe(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx index a2de4d2f49ea7..2db83c5d75aea 100644 --- a/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx @@ -5,18 +5,22 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { Route, Switch } from 'react-router-dom'; import { UsersTabsProps } from './types'; import { UsersTableType } from '../store/model'; import { USERS_PATH } from '../../../common/constants'; import { AllUsersQueryTabBody } from './navigation'; +import { AnomaliesQueryTabBody } from '../../common/containers/anomalies/anomalies_query_tab_body'; +import { AnomaliesUserTable } from '../../common/components/ml/tables/anomalies_user_table'; +import { Anomaly } from '../../common/components/ml/types'; +import { scoreIntervalToDateTime } from '../../common/components/ml/score/score_interval_to_datetime'; +import { UpdateDateRange } from '../../common/components/charts/common'; export const UsersTabs = memo( ({ deleteQuery, - docValueFields, filterQuery, from, indexNames, @@ -24,21 +28,55 @@ export const UsersTabs = memo( setQuery, to, type, + setAbsoluteRangeDatePicker, }) => { + const narrowDateRange = useCallback( + (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const updateDateRange = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ + id: 'global', + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }); + }, + [setAbsoluteRangeDatePicker] + ); + + const tabProps = { + deleteQuery, + endDate: to, + filterQuery, + indexNames, + skip: isInitializing || filterQuery === undefined, + setQuery, + startDate: from, + type, + narrowDateRange, + updateDateRange, + }; + return ( - + + + + ); diff --git a/x-pack/plugins/security_solution/public/users/store/model.ts b/x-pack/plugins/security_solution/public/users/store/model.ts index 8a3e4b983dbb9..57d9c4b6c62f2 100644 --- a/x-pack/plugins/security_solution/public/users/store/model.ts +++ b/x-pack/plugins/security_solution/public/users/store/model.ts @@ -12,6 +12,7 @@ export enum UsersType { export enum UsersTableType { allUsers = 'allUsers', + anomalies = 'anomalies', } export type AllUsersTables = UsersTableType; @@ -32,6 +33,7 @@ export interface TableUpdates { export interface UsersQueries { [UsersTableType.allUsers]: AllUsersQuery; + [UsersTableType.anomalies]: null | undefined; } export interface UsersPageModel { diff --git a/x-pack/plugins/security_solution/public/users/store/reducer.ts b/x-pack/plugins/security_solution/public/users/store/reducer.ts index 1ca0cf5cabb8a..3f4cd69d7f9e8 100644 --- a/x-pack/plugins/security_solution/public/users/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/users/store/reducer.ts @@ -17,6 +17,7 @@ import { } from './actions'; import { setUsersPageQueriesActivePageToZero } from './helpers'; import { UsersTableType, UsersModel } from './model'; +import { HostsTableType } from '../../hosts/store/model'; export const initialUsersState: UsersModel = { page: { @@ -24,12 +25,8 @@ export const initialUsersState: UsersModel = { [UsersTableType.allUsers]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, - // TODO Fix me - // sort: { - // field: AllUsersFields.allUsers, - // direction: Direction.desc, - // }, }, + [HostsTableType.anomalies]: null, }, }, details: { @@ -37,12 +34,8 @@ export const initialUsersState: UsersModel = { [UsersTableType.allUsers]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, - // TODO Fix me - // sort: { - // field: HostRulesFields.riskScore, - // direction: Direction.desc, - // }, }, + [HostsTableType.anomalies]: null, }, }, }; @@ -75,7 +68,6 @@ export const usersReducer = reducerWithInitialState(initialUsersState) queries: { ...state[usersType].queries, [tableType]: { - // TODO: Steph/users fix active page/limit on users tables. is broken because multiple UsersTableType.userRules tables ...state[usersType].queries[tableType], activePage, }, @@ -89,7 +81,6 @@ export const usersReducer = reducerWithInitialState(initialUsersState) queries: { ...state[usersType].queries, [tableType]: { - // TODO: Steph/users fix active page/limit on users tables. is broken because multiple UsersTableType.userRules tables ...state[usersType].queries[tableType], limit, }, From 62e75d7d9df0f2267d98a59b960b51fa92220f6f Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Wed, 23 Feb 2022 15:43:43 +0100 Subject: [PATCH 2/3] Improve tests --- .../get_host_name_from_influencers.test.ts | 27 +- .../get_network_from_influencers.test.ts | 32 +- .../get_user_name_from_influencers.test.ts | 27 +- .../tables/convert_anomalies_to_users.test.ts | 274 ++++++------------ 4 files changed, 114 insertions(+), 246 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_host_name_from_influencers.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_host_name_from_influencers.test.ts index 31edb613d5b25..db15d725b31b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_host_name_from_influencers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_host_name_from_influencers.test.ts @@ -5,42 +5,31 @@ * 2.0. */ -import { cloneDeep } from 'lodash/fp'; import { getHostNameFromInfluencers } from './get_host_name_from_influencers'; import { mockAnomalies } from '../mock'; describe('get_host_name_from_influencers', () => { - let anomalies = cloneDeep(mockAnomalies); - - beforeEach(() => { - anomalies = cloneDeep(mockAnomalies); - }); - test('returns host names from influencers from the mock', () => { - const hostName = getHostNameFromInfluencers(anomalies.anomalies[0].influencers); - expect(hostName).toEqual('zeek-iowa'); + expect(getHostNameFromInfluencers(mockAnomalies.anomalies[0].influencers)).toEqual('zeek-iowa'); }); test('returns null if there are no influencers from the mock', () => { - anomalies.anomalies[0].influencers = []; - const hostName = getHostNameFromInfluencers(anomalies.anomalies[0].influencers); - expect(hostName).toEqual(null); + expect(getHostNameFromInfluencers([])).toEqual(null); }); test('returns null if it is given undefined influencers', () => { - const hostName = getHostNameFromInfluencers(); - expect(hostName).toEqual(null); + expect(getHostNameFromInfluencers()).toEqual(null); }); test('returns null if there influencers is an empty object', () => { - anomalies.anomalies[0].influencers = [{}]; - const hostName = getHostNameFromInfluencers(anomalies.anomalies[0].influencers); - expect(hostName).toEqual(null); + expect(getHostNameFromInfluencers([{}])).toEqual(null); }); test('returns host name mixed with other data', () => { - anomalies.anomalies[0].influencers = [{ 'host.name': 'name-1' }, { 'source.ip': '127.0.0.1' }]; - const hostName = getHostNameFromInfluencers(anomalies.anomalies[0].influencers); + const hostName = getHostNameFromInfluencers([ + { 'host.name': 'name-1' }, + { 'source.ip': '127.0.0.1' }, + ]); expect(hostName).toEqual('name-1'); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_network_from_influencers.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_network_from_influencers.test.ts index 16a7af42ad961..9160377b27e63 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_network_from_influencers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_network_from_influencers.test.ts @@ -5,38 +5,27 @@ * 2.0. */ -import { cloneDeep } from 'lodash/fp'; import { getNetworkFromInfluencers } from './get_network_from_influencers'; -import { mockAnomalies } from '../mock'; import { DestinationOrSource } from '../types'; describe('get_network_from_influencers', () => { - let anomalies = cloneDeep(mockAnomalies); - - beforeEach(() => { - anomalies = cloneDeep(mockAnomalies); - }); - - test('returns null if there are no influencers from the mock', () => { - anomalies.anomalies[0].influencers = []; - const network = getNetworkFromInfluencers(anomalies.anomalies[0].influencers); - expect(network).toEqual(null); + test('returns null if there are no influencers', () => { + expect(getNetworkFromInfluencers([])).toEqual(null); }); test('returns null if the influencers is an empty object', () => { - anomalies.anomalies[0].influencers = [{}]; - const network = getNetworkFromInfluencers(anomalies.anomalies[0].influencers); - expect(network).toEqual(null); + expect(getNetworkFromInfluencers([{}])).toEqual(null); }); test('returns null if the influencers are undefined', () => { - const network = getNetworkFromInfluencers(); - expect(network).toEqual(null); + expect(getNetworkFromInfluencers()).toEqual(null); }); test('returns network name of source mixed with other data', () => { - anomalies.anomalies[0].influencers = [{ 'host.name': 'name-1' }, { 'source.ip': '127.0.0.1' }]; - const network = getNetworkFromInfluencers(anomalies.anomalies[0].influencers); + const network = getNetworkFromInfluencers([ + { 'host.name': 'name-1' }, + { 'source.ip': '127.0.0.1' }, + ]); const expected: { ip: string; type: DestinationOrSource } = { ip: '127.0.0.1', type: 'source.ip', @@ -45,11 +34,10 @@ describe('get_network_from_influencers', () => { }); test('returns network name mixed with other data', () => { - anomalies.anomalies[0].influencers = [ + const network = getNetworkFromInfluencers([ { 'host.name': 'name-1' }, { 'destination.ip': '127.0.0.1' }, - ]; - const network = getNetworkFromInfluencers(anomalies.anomalies[0].influencers); + ]); const expected: { ip: string; type: DestinationOrSource } = { ip: '127.0.0.1', type: 'destination.ip', diff --git a/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.test.ts index c49707748142f..8e5042c8fd855 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/influencers/get_user_name_from_influencers.test.ts @@ -5,42 +5,31 @@ * 2.0. */ -import { cloneDeep } from 'lodash/fp'; import { getUserNameFromInfluencers } from './get_user_name_from_influencers'; import { mockAnomalies } from '../mock'; describe('get_user_name_from_influencers', () => { - let anomalies = cloneDeep(mockAnomalies); - - beforeEach(() => { - anomalies = cloneDeep(mockAnomalies); - }); - test('returns user names from influencers from the mock', () => { - const userName = getUserNameFromInfluencers(anomalies.anomalies[0].influencers); - expect(userName).toEqual('root'); + expect(getUserNameFromInfluencers(mockAnomalies.anomalies[0].influencers)).toEqual('root'); }); test('returns null if there are no influencers from the mock', () => { - anomalies.anomalies[0].influencers = []; - const userName = getUserNameFromInfluencers(anomalies.anomalies[0].influencers); - expect(userName).toEqual(null); + expect(getUserNameFromInfluencers([])).toEqual(null); }); test('returns null if it is given undefined influencers', () => { - const userName = getUserNameFromInfluencers(); - expect(userName).toEqual(null); + expect(getUserNameFromInfluencers()).toEqual(null); }); test('returns null if there influencers is an empty object', () => { - anomalies.anomalies[0].influencers = [{}]; - const userName = getUserNameFromInfluencers(anomalies.anomalies[0].influencers); - expect(userName).toEqual(null); + expect(getUserNameFromInfluencers([{}])).toEqual(null); }); test('returns user name mixed with other data', () => { - anomalies.anomalies[0].influencers = [{ 'user.name': 'root' }, { 'source.ip': '127.0.0.1' }]; - const userName = getUserNameFromInfluencers(anomalies.anomalies[0].influencers); + const userName = getUserNameFromInfluencers([ + { 'user.name': 'root' }, + { 'source.ip': '127.0.0.1' }, + ]); expect(userName).toEqual('root'); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.test.ts index f81b1e019c33a..b3c3aa64c9214 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/convert_anomalies_to_users.test.ts @@ -6,102 +6,20 @@ */ import { mockAnomalies } from '../mock'; -import { cloneDeep } from 'lodash/fp'; import { convertAnomaliesToUsers, getUserNameFromEntity } from './convert_anomalies_to_users'; import { AnomaliesByUser } from '../types'; describe('convert_anomalies_to_users', () => { - let anomalies = cloneDeep(mockAnomalies); - - beforeEach(() => { - anomalies = cloneDeep(mockAnomalies); - }); - test('it returns expected anomalies from a user', () => { - const entities = convertAnomaliesToUsers(anomalies); + const entities = convertAnomaliesToUsers(mockAnomalies); + const expected: AnomaliesByUser[] = [ { - anomaly: { - detectorIndex: 0, - entityName: 'process.name', - entityValue: 'du', - influencers: [ - { 'host.name': 'zeek-iowa' }, - { 'process.name': 'du' }, - { 'user.name': 'root' }, - ], - jobId: 'job-1', - rowId: '1561157194802_0', - severity: 16.193669439507826, - source: { - actual: [1], - bucket_span: 900, - by_field_name: 'process.name', - by_field_value: 'du', - detector_index: 0, - function: 'rare', - function_description: 'rare', - influencers: [ - { influencer_field_name: 'user.name', influencer_field_values: ['root'] }, - { influencer_field_name: 'process.name', influencer_field_values: ['du'] }, - { influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] }, - ], - initial_record_score: 16.193669439507826, - is_interim: false, - job_id: 'job-1', - multi_bucket_impact: 0, - partition_field_name: 'host.name', - partition_field_value: 'zeek-iowa', - probability: 0.024041164411288146, - record_score: 16.193669439507826, - result_type: 'record', - timestamp: 1560664800000, - typical: [0.024041164411288146], - }, - time: 1560664800000, - }, + anomaly: mockAnomalies.anomalies[0], userName: 'root', }, { - anomaly: { - detectorIndex: 0, - entityName: 'process.name', - entityValue: 'ls', - influencers: [ - { 'host.name': 'zeek-iowa' }, - { 'process.name': 'ls' }, - { 'user.name': 'root' }, - ], - jobId: 'job-2', - rowId: '1561157194802_1', - severity: 16.193669439507826, - source: { - actual: [1], - bucket_span: 900, - by_field_name: 'process.name', - by_field_value: 'ls', - detector_index: 0, - function: 'rare', - function_description: 'rare', - influencers: [ - { influencer_field_name: 'user.name', influencer_field_values: ['root'] }, - { influencer_field_name: 'process.name', influencer_field_values: ['ls'] }, - { influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] }, - ], - initial_record_score: 16.193669439507826, - is_interim: false, - job_id: 'job-2', - multi_bucket_impact: 0, - partition_field_name: 'host.name', - partition_field_value: 'zeek-iowa', - probability: 0.024041164411288146, - record_score: 16.193669439507826, - result_type: 'record', - timestamp: 1560664800000, - typical: [0.024041164411288146], - }, - time: 1560664800000, - }, + anomaly: mockAnomalies.anomalies[1], userName: 'root', }, ]; @@ -115,56 +33,27 @@ describe('convert_anomalies_to_users', () => { }); test('it returns a specific anomaly if sent in the user name of an anomaly', () => { - anomalies.anomalies[0].entityName = 'something-else'; - anomalies.anomalies[0].entityValue = 'something-else'; - anomalies.anomalies[0].influencers = [ - { 'host.name': 'zeek-iowa' }, - { 'process.name': 'du' }, - { 'user.name': 'something-else' }, - ]; - - const entities = convertAnomaliesToUsers(anomalies, 'root'); - const expected: AnomaliesByUser[] = [ - { - anomaly: { - detectorIndex: 0, - entityName: 'process.name', - entityValue: 'ls', + const anomalies = { + ...mockAnomalies, + anomalies: [ + { + ...mockAnomalies.anomalies[0], + entityName: 'something-else', + entityValue: 'something-else', influencers: [ { 'host.name': 'zeek-iowa' }, - { 'process.name': 'ls' }, - { 'user.name': 'root' }, + { 'process.name': 'du' }, + { 'user.name': 'something-else' }, ], - jobId: 'job-2', - rowId: '1561157194802_1', - severity: 16.193669439507826, - source: { - actual: [1], - bucket_span: 900, - by_field_name: 'process.name', - by_field_value: 'ls', - detector_index: 0, - function: 'rare', - function_description: 'rare', - influencers: [ - { influencer_field_name: 'user.name', influencer_field_values: ['root'] }, - { influencer_field_name: 'process.name', influencer_field_values: ['ls'] }, - { influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] }, - ], - initial_record_score: 16.193669439507826, - is_interim: false, - job_id: 'job-2', - multi_bucket_impact: 0, - partition_field_name: 'host.name', - partition_field_value: 'zeek-iowa', - probability: 0.024041164411288146, - record_score: 16.193669439507826, - result_type: 'record', - timestamp: 1560664800000, - typical: [0.024041164411288146], - }, - time: 1560664800000, }, + mockAnomalies.anomalies[1], + ], + }; + + const entities = convertAnomaliesToUsers(anomalies, 'root'); + const expected: AnomaliesByUser[] = [ + { + anomaly: anomalies.anomalies[1], userName: 'root', }, ]; @@ -172,58 +61,31 @@ describe('convert_anomalies_to_users', () => { }); test('it returns a specific anomaly if an influencer has the user name', () => { - anomalies.anomalies[0].entityName = 'something-else'; - anomalies.anomalies[0].entityValue = 'something-else'; - anomalies.anomalies[0].influencers = [ - { 'host.name': 'zeek-iowa' }, - { 'process.name': 'du' }, - { 'user.name': 'something-else' }, - ]; - - anomalies.anomalies[1].entityName = 'something-else'; - anomalies.anomalies[1].entityValue = 'something-else'; - const entities = convertAnomaliesToUsers(anomalies, 'root'); - const expected: AnomaliesByUser[] = [ - { - anomaly: { - detectorIndex: 0, + const anomalies = { + ...mockAnomalies, + anomalies: [ + { + ...mockAnomalies.anomalies[0], entityName: 'something-else', entityValue: 'something-else', influencers: [ { 'host.name': 'zeek-iowa' }, - { 'process.name': 'ls' }, - { 'user.name': 'root' }, + { 'process.name': 'du' }, + { 'user.name': 'something-else' }, ], - jobId: 'job-2', - rowId: '1561157194802_1', - severity: 16.193669439507826, - source: { - actual: [1], - bucket_span: 900, - by_field_name: 'process.name', - by_field_value: 'ls', - detector_index: 0, - function: 'rare', - function_description: 'rare', - influencers: [ - { influencer_field_name: 'user.name', influencer_field_values: ['root'] }, - { influencer_field_name: 'process.name', influencer_field_values: ['ls'] }, - { influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] }, - ], - initial_record_score: 16.193669439507826, - is_interim: false, - job_id: 'job-2', - multi_bucket_impact: 0, - partition_field_name: 'host.name', - partition_field_value: 'zeek-iowa', - probability: 0.024041164411288146, - record_score: 16.193669439507826, - result_type: 'record', - timestamp: 1560664800000, - typical: [0.024041164411288146], - }, - time: 1560664800000, }, + { + ...mockAnomalies.anomalies[1], + entityName: 'something-else', + entityValue: 'something-else', + }, + ], + }; + + const entities = convertAnomaliesToUsers(anomalies, 'root'); + const expected: AnomaliesByUser[] = [ + { + anomaly: anomalies.anomalies[1], userName: 'root', }, ]; @@ -231,34 +93,74 @@ describe('convert_anomalies_to_users', () => { }); test('it returns empty anomalies if sent in the name of one that does not exist', () => { - const entities = convertAnomaliesToUsers(anomalies, 'some-made-up-name-here-for-you'); + const entities = convertAnomaliesToUsers(mockAnomalies, 'some-made-up-name-here-for-you'); const expected: AnomaliesByUser[] = []; expect(entities).toEqual(expected); }); test('it returns true for a found entity name passed in', () => { - anomalies.anomalies[0].entityName = 'user.name'; - anomalies.anomalies[0].entityValue = 'admin'; + const anomalies = { + ...mockAnomalies, + anomalies: [ + { + ...mockAnomalies.anomalies[0], + entityName: 'user.name', + entityValue: 'admin', + }, + mockAnomalies.anomalies[1], + ], + }; + const found = getUserNameFromEntity(anomalies.anomalies[0], 'admin'); expect(found).toEqual(true); }); test('it returns false for an entity name that does not exist', () => { - anomalies.anomalies[0].entityName = 'user.name'; - anomalies.anomalies[0].entityValue = 'admin'; + const anomalies = { + ...mockAnomalies, + anomalies: [ + { + ...mockAnomalies.anomalies[0], + entityName: 'user.name', + entityValue: 'admin', + }, + mockAnomalies.anomalies[1], + ], + }; + const found = getUserNameFromEntity(anomalies.anomalies[0], 'some-made-up-entity-name'); expect(found).toEqual(false); }); test('it returns true for an entity that has user.name within it if no name is passed in', () => { - anomalies.anomalies[0].entityName = 'user.name'; - anomalies.anomalies[0].entityValue = 'something-made-up'; + const anomalies = { + ...mockAnomalies, + anomalies: [ + { + ...mockAnomalies.anomalies[0], + entityName: 'user.name', + entityValue: 'something-made-up', + }, + mockAnomalies.anomalies[1], + ], + }; + const found = getUserNameFromEntity(anomalies.anomalies[0]); expect(found).toEqual(true); }); test('it returns false for an entity that is not user.name and no name is passed in', () => { - anomalies.anomalies[0].entityName = 'made-up'; + const anomalies = { + ...mockAnomalies, + anomalies: [ + { + ...mockAnomalies.anomalies[0], + entityValue: 'made-up', + }, + mockAnomalies.anomalies[1], + ], + }; + const found = getUserNameFromEntity(anomalies.anomalies[0]); expect(found).toEqual(false); }); From 84a960addb38edadb861915e5df3de1884128be1 Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Mon, 28 Feb 2022 09:47:25 +0100 Subject: [PATCH 3/3] Revert influencersFilterQuery rename --- .../ml/server/models/results_service/results_service.ts | 7 +++---- x-pack/plugins/ml/server/routes/results_service.ts | 4 ++-- .../ml/server/routes/schemas/results_service_schema.ts | 2 +- .../components/ml/anomaly/use_anomalies_table_data.ts | 2 +- .../common/components/ml/api/anomalies_table_data.ts | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index 1ef722422b69b..aa92ada043c29 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -96,7 +96,7 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust dateFormatTz: string, maxRecords: number = ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, maxExamples: number = DEFAULT_MAX_EXAMPLES, - filterQuery?: any, + influencersFilterQuery?: any, functionDescription?: string ) { // Build the query to return the matching anomaly record results. @@ -153,8 +153,8 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust }); } - if (filterQuery !== undefined) { - boolCriteria.push(filterQuery); + if (influencersFilterQuery !== undefined) { + boolCriteria.push(influencersFilterQuery); } // Add a nested query to filter for each of the specified influencers. @@ -224,7 +224,6 @@ export function resultsServiceProvider(mlClient: MlClient, client?: IScopedClust anomalies: [], interval: 'second', }; - // @ts-expect-error incorrect search response type if (body.hits.total.value > 0) { let records: AnomalyRecordDoc[] = []; diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index d8e11d181848f..78f05f0d731aa 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -37,7 +37,7 @@ function getAnomaliesTableData(mlClient: MlClient, payload: any) { dateFormatTz, maxRecords, maxExamples, - filterQuery, + influencersFilterQuery, functionDescription, } = payload; return rs.getAnomaliesTableData( @@ -51,7 +51,7 @@ function getAnomaliesTableData(mlClient: MlClient, payload: any) { dateFormatTz, maxRecords, maxExamples, - filterQuery, + influencersFilterQuery, functionDescription ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts index 02407b7ab091c..df4a56b06410b 100644 --- a/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/results_service_schema.ts @@ -26,7 +26,7 @@ export const anomaliesTableDataSchema = schema.object({ dateFormatTz: schema.string(), maxRecords: schema.number(), maxExamples: schema.maybe(schema.number()), - filterQuery: schema.maybe(schema.any()), + influencersFilterQuery: schema.maybe(schema.any()), functionDescription: schema.maybe(schema.nullable(schema.string())), }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index bb6ecee9b3c86..f1cab9c2f441d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -86,7 +86,7 @@ export const useAnomaliesTableData = ({ { jobIds, criteriaFields: criteriaFieldsInput, - filterQuery, + influencersFilterQuery: filterQuery, aggregationInterval: 'auto', threshold: getThreshold(anomalyScore, threshold), earliestMs, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/api/anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/api/anomalies_table_data.ts index 09d0286a0c66b..01ed306d08318 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/api/anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/api/anomalies_table_data.ts @@ -20,7 +20,7 @@ export interface Body { dateFormatTz: string; maxRecords: number; maxExamples: number; - filterQuery?: estypes.QueryDslQueryContainer; + influencersFilterQuery?: estypes.QueryDslQueryContainer; } export const anomaliesTableData = async (body: Body, signal: AbortSignal): Promise => {