From 22a790163a31014518dacd494f26270ae22a175c Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Tue, 30 Aug 2022 15:28:36 +0200 Subject: [PATCH 01/11] Create Job id filter component --- .../ml/tables/job_id_filter.stories.tsx | 45 +++++++++++ .../ml/tables/job_id_filter.test.tsx | 8 ++ .../components/ml/tables/job_id_filter.tsx | 79 +++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.stories.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.stories.tsx new file mode 100644 index 0000000000000..88619573cb179 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.stories.tsx @@ -0,0 +1,45 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import type { ReactNode } from 'react'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { euiLightVars } from '@kbn/ui-theme'; +import { action } from '@storybook/addon-actions'; +import { JobIdFilter } from './job_id_filter'; + +const withTheme = (storyFn: () => ReactNode) => ( + ({ eui: euiLightVars, darkMode: true })}>{storyFn()} +); + +storiesOf('JobIdFilter', module) + .addDecorator(withTheme) + .add('one selected item', () => ( + + )) + .add('multiple selected item', () => ( + + )) + .add('no selected item', () => ( + + )); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.test.tsx new file mode 100644 index 0000000000000..53c3add276bed --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.test.tsx @@ -0,0 +1,8 @@ +/* + * 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. + */ + +// TODO diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx new file mode 100644 index 0000000000000..8ae28dc1c4bcd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.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 React, { useCallback, useMemo, useState } from 'react'; +import { EuiFilterButton, EuiFilterGroup, EuiFilterSelectItem, EuiPopover } from '@elastic/eui'; + +export const JobIdFilter: React.FC<{ + selectedJobIds: string[]; + jobIds: string[]; + onSelect: (jobIds: string[]) => void; + title: string; +}> = ({ selectedJobIds, onSelect, title, jobIds }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [isPopoverOpen]); + + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const updateSelection = useCallback( + (selectedJobId: string) => { + const currentSelection = selectedJobIds ?? []; + const newSelection = currentSelection.includes(selectedJobId) + ? currentSelection.filter((s) => s !== selectedJobId) + : [...currentSelection, selectedJobId]; + + onSelect(newSelection); + }, + [selectedJobIds, onSelect] + ); + + const button = useMemo( + () => ( + 0} + iconType="arrowDown" + isSelected={isPopoverOpen} + numActiveFilters={selectedJobIds.length} + onClick={onButtonClick} + contentProps={{ style: { minWidth: '120px' } }} // avoid resizing when selecting job id + > + {title} + + ), + [isPopoverOpen, onButtonClick, title, selectedJobIds.length] + ); + + return ( + + +
+ {jobIds.map((id) => ( + updateSelection(id)} + > + {id} + + ))} +
+
+
+ ); +}; From 97076aeb59b92c594ed75cda1b9927c5b23ad62d Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Tue, 30 Aug 2022 17:10:56 +0200 Subject: [PATCH 02/11] Add job filter to anomalies tab --- .../ml/anomaly/anomaly_table_provider.tsx | 3 ++ .../ml/anomaly/use_anomalies_table_data.ts | 10 +++-- .../ml/hooks/use_installed_security_jobs.ts | 9 +++- .../ml/tables/anomalies_host_table.tsx | 43 ++++++++++++++++++- .../ml/tables/anomalies_network_table.tsx | 43 ++++++++++++++++++- .../ml/tables/anomalies_user_table.tsx | 43 ++++++++++++++++++- .../components/ml/tables/job_id_filter.tsx | 2 +- .../components/ml/tables/translations.ts | 4 ++ .../public/hosts/store/actions.ts | 5 +++ .../public/hosts/store/model.ts | 6 ++- .../public/hosts/store/reducer.ts | 22 +++++++++- .../public/hosts/store/selectors.ts | 3 ++ .../public/network/pages/details/index.tsx | 3 ++ .../public/network/store/actions.ts | 6 +++ .../public/network/store/model.ts | 8 ++++ .../public/network/store/reducer.ts | 38 +++++++++++++++- .../public/network/store/selectors.ts | 11 +++++ .../network_details/expandable_network.tsx | 4 +- .../public/users/store/actions.ts | 5 +++ .../public/users/store/model.ts | 8 +++- .../public/users/store/reducer.ts | 40 ++++++++++++++++- .../public/users/store/selectors.ts | 8 +++- 22 files changed, 305 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx index f8d6a1e4ca983..beabeb8c60f2b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { useInstalledSecurityJobsIds } from '../hooks/use_installed_security_jobs'; import type { InfluencerInput, Anomalies, CriteriaFields } from '../types'; import { useAnomaliesTableData } from './use_anomalies_table_data'; @@ -25,12 +26,14 @@ interface Props { export const AnomalyTableProvider = React.memo( ({ influencers, startDate, endDate, children, criteriaFields, skip }) => { + const jobIds = useInstalledSecurityJobsIds(); const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({ criteriaFields, influencers, startDate, endDate, skip, + jobIds, }); return <>{children({ isLoadingAnomaliesData, anomaliesData })}; } 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 9cc55b449bc9f..aa89e79d4eec0 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 @@ -14,7 +14,8 @@ import type { InfluencerInput, Anomalies, CriteriaFields } from '../types'; import * as i18n from './translations'; import { useTimeZone, useUiSetting$ } from '../../../lib/kibana'; import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { useInstalledSecurityJobs } from '../hooks/use_installed_security_jobs'; +import { useMlCapabilities } from '../hooks/use_ml_capabilities'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; interface Args { influencers?: InfluencerInput[]; @@ -24,6 +25,7 @@ interface Args { skip?: boolean; criteriaFields?: CriteriaFields[]; filterQuery?: estypes.QueryDslQueryContainer; + jobIds: string[]; } type Return = [boolean, Anomalies | null]; @@ -57,15 +59,17 @@ export const useAnomaliesTableData = ({ threshold = -1, skip = false, filterQuery, + jobIds, }: Args): Return => { const [tableData, setTableData] = useState(null); - const { isMlUser, jobs } = useInstalledSecurityJobs(); + const mlCapabilities = useMlCapabilities(); + const isMlUser = hasMlUserPermissions(mlCapabilities); + const [loading, setLoading] = useState(true); const { addError } = useAppToasts(); const timeZone = useTimeZone(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); - const jobIds = jobs.map((job) => job.id); const startDateMs = useMemo(() => new Date(startDate).getTime(), [startDate]); const endDateMs = useMemo(() => new Date(endDate).getTime(), [endDate]); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts index a536b0a9252cc..81371e07ae1d5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import type { MlSummaryJob } from '@kbn/ml-plugin/public'; import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; @@ -65,3 +65,10 @@ export const useInstalledSecurityJobs = (): UseInstalledSecurityJobsReturn => { return { isLicensed, isMlUser, jobs, loading }; }; + +export const useInstalledSecurityJobsIds = () => { + const { jobs } = useInstalledSecurityJobs(); + const jobIds = useMemo(() => jobs.map((job) => job.id), [jobs]); + + return jobIds; +}; 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 bb896a4623040..675c2c43d317f 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 @@ -5,8 +5,9 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; @@ -22,6 +23,11 @@ import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type import { Panel } from '../../panel'; import { anomaliesTableDefaultEquality } from './default_equality'; import { useQueryToggle } from '../../../containers/query_toggle'; +import { useInstalledSecurityJobsIds } from '../hooks/use_installed_security_jobs'; +import { useDeepEqualSelector } from '../../../hooks/use_selector'; +import type { State } from '../../../store'; +import { JobIdFilter } from './job_id_filter'; +import { hostsActions, hostsSelectors } from '../../../../hosts/store'; const sorting = { sort: { @@ -37,6 +43,7 @@ const AnomaliesHostTableComponent: React.FC = ({ skip, type, }) => { + const dispatch = useDispatch(); const capabilities = useMlCapabilities(); const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesHostTable`); const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); @@ -52,6 +59,29 @@ const AnomaliesHostTableComponent: React.FC = ({ [setQuerySkip, setToggleStatus] ); + const jobIds = useInstalledSecurityJobsIds(); + + const getAnomaliesHostsTableFilterQuerySelector = useMemo( + () => hostsSelectors.hostsAnomaliesJobIdFilterSelector(), + [] + ); + + const selectedJobIds = useDeepEqualSelector((state: State) => + getAnomaliesHostsTableFilterQuerySelector(state, type) + ); + + const onSelectJobId = useCallback( + (newSelection: string[]) => { + dispatch( + hostsActions.updateHostsAnomaliesJobIdFilter({ + jobIds: newSelection, + hostsType: type, + }) + ); + }, + [dispatch, type] + ); + const [loading, tableData] = useAnomaliesTableData({ startDate, endDate, @@ -60,6 +90,7 @@ const AnomaliesHostTableComponent: React.FC = ({ filterQuery: { exists: { field: 'host.name' }, }, + jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds, }); const hosts = convertAnomaliesToHosts(tableData, hostName); @@ -87,6 +118,16 @@ const AnomaliesHostTableComponent: React.FC = ({ toggleStatus={toggleStatus} tooltip={i18n.TOOLTIP} isInspectDisabled={skip} + headerFilters={ + jobIds.length > 0 && ( + + ) + } /> {toggleStatus && ( = ({ flowTarget, }) => { const capabilities = useMlCapabilities(); - + const dispatch = useDispatch(); const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesNetwork-${flowTarget}`); const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); useEffect(() => { @@ -53,11 +59,34 @@ const AnomaliesNetworkTableComponent: React.FC = ({ [setQuerySkip, setToggleStatus] ); + const jobIds = useInstalledSecurityJobsIds(); + + const getAnomaliesUserTableFilterQuerySelector = useMemo( + () => networkSelectors.networkAnomaliesJobIdFilterSelector(), + [] + ); + + const selectedJobIds = useDeepEqualSelector((state: State) => + getAnomaliesUserTableFilterQuerySelector(state, type) + ); + + const onSelectJobId = useCallback( + (newSelection: string[]) => { + dispatch( + networkActions.updateNetworkAnomaliesJobIdFilter({ + jobIds: newSelection, + networkType: type, + }) + ); + }, + [dispatch, type] + ); const [loading, tableData] = useAnomaliesTableData({ startDate, endDate, skip: querySkip, criteriaFields: getCriteriaFromNetworkType(type, ip, flowTarget), + jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds, }); const networks = convertAnomaliesToNetwork(tableData, ip); @@ -84,6 +113,16 @@ const AnomaliesNetworkTableComponent: React.FC = ({ toggleQuery={toggleQuery} toggleStatus={toggleStatus} isInspectDisabled={skip} + headerFilters={ + jobIds.length > 0 && ( + + ) + } /> {toggleStatus && ( = ({ skip, type, }) => { + const dispatch = useDispatch(); const capabilities = useMlCapabilities(); const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesUserTable`); @@ -55,6 +62,29 @@ const AnomaliesUserTableComponent: React.FC = ({ [setQuerySkip, setToggleStatus] ); + const jobIds = useInstalledSecurityJobsIds(); + + const getAnomaliesUserTableFilterQuerySelector = useMemo( + () => usersSelectors.usersAnomaliesJobIdFilterSelector(), + [] + ); + + const selectedJobIds = useDeepEqualSelector((state: State) => + getAnomaliesUserTableFilterQuerySelector(state, type) + ); + + const onSelectJobId = useCallback( + (newSelection: string[]) => { + dispatch( + usersActions.updateUsersAnomaliesJobIdFilter({ + jobIds: newSelection, + usersType: type, + }) + ); + }, + [dispatch, type] + ); + const [loading, tableData] = useAnomaliesTableData({ startDate, endDate, @@ -63,6 +93,7 @@ const AnomaliesUserTableComponent: React.FC = ({ filterQuery: { exists: { field: 'user.name' }, }, + jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds, }); const users = convertAnomaliesToUsers(tableData, userName); @@ -90,6 +121,16 @@ const AnomaliesUserTableComponent: React.FC = ({ toggleStatus={toggleStatus} tooltip={i18n.TOOLTIP} isInspectDisabled={skip} + headerFilters={ + jobIds.length > 0 && ( + + ) + } /> {toggleStatus && ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx index 8ae28dc1c4bcd..672c35f5bd1cc 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx @@ -44,7 +44,7 @@ export const JobIdFilter: React.FC<{ isSelected={isPopoverOpen} numActiveFilters={selectedJobIds.length} onClick={onButtonClick} - contentProps={{ style: { minWidth: '120px' } }} // avoid resizing when selecting job id + contentProps={{ style: { minWidth: 120 } }} // avoid resizing when selecting job id > {title} 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 e0c7a169bf9e3..bb385fa415f52 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 @@ -65,3 +65,7 @@ export const NETWORK_NAME = i18n.translate('xpack.securitySolution.ml.table.netw export const TIME_STAMP = i18n.translate('xpack.securitySolution.ml.table.timestampTitle', { defaultMessage: 'Timestamp', }); + +export const JOB_ID = i18n.translate('xpack.securitySolution.ml.table.jobIdFilter', { + defaultMessage: 'Job', +}); diff --git a/x-pack/plugins/security_solution/public/hosts/store/actions.ts b/x-pack/plugins/security_solution/public/hosts/store/actions.ts index f8d657b9afd0b..76d43c030e994 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/actions.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/actions.ts @@ -44,3 +44,8 @@ export const updateHostRiskScoreSeverityFilter = actionCreator<{ severitySelection: RiskSeverity[]; hostsType: HostsType; }>('UPDATE_HOST_RISK_SCORE_SEVERITY'); + +export const updateHostsAnomaliesJobIdFilter = actionCreator<{ + jobIds: string[]; + hostsType: HostsType; +}>('UPDATE_HOSTS_ANOMALIES_JOB_ID_FILTER'); diff --git a/x-pack/plugins/security_solution/public/hosts/store/model.ts b/x-pack/plugins/security_solution/public/hosts/store/model.ts index bd62e7cb62c2f..d3660370aed65 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/model.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/model.ts @@ -42,12 +42,16 @@ export interface HostRiskScoreQuery extends BasicQueryPaginated { severitySelection: RiskSeverity[]; } +export interface HostsAnomaliesQuery { + jobIdSelection: string[]; +} + export interface Queries { [HostsTableType.authentications]: BasicQueryPaginated; [HostsTableType.hosts]: HostsQuery; [HostsTableType.events]: BasicQueryPaginated; [HostsTableType.uncommonProcesses]: BasicQueryPaginated; - [HostsTableType.anomalies]: null | undefined; + [HostsTableType.anomalies]: HostsAnomaliesQuery; [HostsTableType.risk]: HostRiskScoreQuery; [HostsTableType.sessions]: BasicQueryPaginated; } diff --git a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts index cf323795fa431..302dea2875ace 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts @@ -18,6 +18,7 @@ import { updateHostRiskScoreSort, updateTableActivePage, updateTableLimit, + updateHostsAnomaliesJobIdFilter, } from './actions'; import { setHostPageQueriesActivePageToZero, @@ -49,7 +50,9 @@ export const initialHostsState: HostsState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, }, - [HostsTableType.anomalies]: null, + [HostsTableType.anomalies]: { + jobIdSelection: [], + }, [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, @@ -85,7 +88,9 @@ export const initialHostsState: HostsState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, }, - [HostsTableType.anomalies]: null, + [HostsTableType.anomalies]: { + jobIdSelection: [], + }, [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, @@ -190,4 +195,17 @@ export const hostsReducer = reducerWithInitialState(initialHostsState) }, }, })) + .case(updateHostsAnomaliesJobIdFilter, (state, { jobIds, hostsType }) => ({ + ...state, + [hostsType]: { + ...state[hostsType], + queries: { + ...state[hostsType].queries, + [HostsTableType.anomalies]: { + ...state[hostsType].queries[HostsTableType.anomalies], + jobIdSelection: jobIds, + }, + }, + }, + })) .build(); diff --git a/x-pack/plugins/security_solution/public/hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/hosts/store/selectors.ts index 5addc9c2ccf4d..f922028c8a815 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/selectors.ts @@ -30,3 +30,6 @@ export const hostRiskScoreSeverityFilterSelector = () => export const uncommonProcessesSelector = () => createSelector(selectHosts, (hosts) => hosts.queries.uncommonProcesses); + +export const hostsAnomaliesJobIdFilterSelector = () => + createSelector(selectHosts, (hosts) => hosts.queries[HostsTableType.anomalies].jobIdSelection); diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx index a0eafe906fdfd..382d90cb35610 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx @@ -47,6 +47,7 @@ import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { navTabsNetworkDetails } from './nav_tabs'; import { NetworkDetailsTabs } from './details_tabs'; +import { useInstalledSecurityJobsIds } from '../../../common/components/ml/hooks/use_installed_security_jobs'; export { getTrailingBreadcrumbs } from './utils'; @@ -115,11 +116,13 @@ const NetworkDetailsComponent: React.FC = () => { ip, }); + const jobIds = useInstalledSecurityJobsIds(); const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({ criteriaFields: networkToCriteria(detailName, flowTarget), startDate: from, endDate: to, skip: isInitializing, + jobIds, }); const headerDraggableArguments = useMemo( diff --git a/x-pack/plugins/security_solution/public/network/store/actions.ts b/x-pack/plugins/security_solution/public/network/store/actions.ts index 81bae080c982f..c472012fbd7a0 100644 --- a/x-pack/plugins/security_solution/public/network/store/actions.ts +++ b/x-pack/plugins/security_solution/public/network/store/actions.ts @@ -7,6 +7,7 @@ import actionCreatorFactory from 'typescript-fsa'; import type { networkModel } from '.'; +import type { NetworkType } from './model'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/network'); @@ -23,3 +24,8 @@ export const setNetworkDetailsTablesActivePageToZero = actionCreator( export const setNetworkTablesActivePageToZero = actionCreator( 'SET_NETWORK_TABLES_ACTIVE_PAGE_TO_ZERO' ); + +export const updateNetworkAnomaliesJobIdFilter = actionCreator<{ + jobIds: string[]; + networkType: NetworkType; +}>('UPDATE_NETWORK_ANOMALIES_JOB_ID_FILTER'); diff --git a/x-pack/plugins/security_solution/public/network/store/model.ts b/x-pack/plugins/security_solution/public/network/store/model.ts index b1bf415e3cb08..7560e09d6d34f 100644 --- a/x-pack/plugins/security_solution/public/network/store/model.ts +++ b/x-pack/plugins/security_solution/public/network/store/model.ts @@ -29,6 +29,7 @@ export enum NetworkTableType { topNFlowDestination = 'topNFlowDestination', topNFlowSource = 'topNFlowSource', tls = 'tls', + anomalies = 'anomalies', } export type TopNTableType = @@ -53,6 +54,7 @@ export enum NetworkDetailsTableType { topNFlowDestination = 'topNFlowDestination', topNFlowSource = 'topNFlowSource', users = 'users', + anomalies = 'anomalies', } export interface BasicQueryPaginated { @@ -105,6 +107,7 @@ export interface NetworkQueries { [NetworkTableType.topNFlowSource]: TopNFlowQuery; [NetworkTableType.tls]: TlsQuery; [NetworkTableType.alerts]: BasicQueryPaginated; + [NetworkTableType.anomalies]: NetworkAnomaliesQuery; } export interface NetworkPageModel { @@ -115,6 +118,10 @@ export interface NetworkUsersQuery extends BasicQueryPaginated { sort: SortField; } +export interface NetworkAnomaliesQuery { + jobIdSelection: string[]; +} + export interface NetworkDetailsQueries { [NetworkDetailsTableType.http]: HttpQuery; [NetworkDetailsTableType.tls]: TlsQuery; @@ -123,6 +130,7 @@ export interface NetworkDetailsQueries { [NetworkDetailsTableType.topNFlowDestination]: TopNFlowQuery; [NetworkDetailsTableType.topNFlowSource]: TopNFlowQuery; [NetworkDetailsTableType.users]: NetworkUsersQuery; + [NetworkDetailsTableType.anomalies]: NetworkAnomaliesQuery; } export interface NetworkDetailsModel { diff --git a/x-pack/plugins/security_solution/public/network/store/reducer.ts b/x-pack/plugins/security_solution/public/network/store/reducer.ts index b60fae0725dc4..87844f80d15f7 100644 --- a/x-pack/plugins/security_solution/public/network/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/network/store/reducer.ts @@ -21,13 +21,14 @@ import { setNetworkDetailsTablesActivePageToZero, setNetworkTablesActivePageToZero, updateNetworkTable, + updateNetworkAnomaliesJobIdFilter, } from './actions'; import { setNetworkDetailsQueriesActivePageToZero, setNetworkPageQueriesActivePageToZero, } from './helpers'; import type { NetworkModel } from './model'; -import { NetworkDetailsTableType, NetworkTableType } from './model'; +import { NetworkType, NetworkDetailsTableType, NetworkTableType } from './model'; export type NetworkState = NetworkModel; @@ -94,6 +95,9 @@ export const initialNetworkState: NetworkState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, }, + [NetworkTableType.anomalies]: { + jobIdSelection: [], + }, }, }, details: { @@ -153,6 +157,9 @@ export const initialNetworkState: NetworkState = { direction: Direction.asc, }, }, + [NetworkDetailsTableType.anomalies]: { + jobIdSelection: [], + }, }, flowTarget: FlowTarget.source, }, @@ -190,4 +197,33 @@ export const networkReducer = reducerWithInitialState(initialNetworkState) queries: setNetworkDetailsQueriesActivePageToZero(state), }, })) + .case(updateNetworkAnomaliesJobIdFilter, (state, { jobIds, networkType }) => { + if (networkType === NetworkType.page) { + return { + ...state, + page: { + ...state.page, + queries: { + ...state.page.queries, + anomalies: { + jobIdSelection: jobIds, + }, + }, + }, + }; + } else { + return { + ...state, + details: { + ...state.details, + queries: { + ...state.details.queries, + anomalies: { + jobIdSelection: jobIds, + }, + }, + }, + }; + } + }) .build(); diff --git a/x-pack/plugins/security_solution/public/network/store/selectors.ts b/x-pack/plugins/security_solution/public/network/store/selectors.ts index 6566181312eff..9ec59c94f3871 100644 --- a/x-pack/plugins/security_solution/public/network/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/network/store/selectors.ts @@ -20,6 +20,11 @@ import type { } from './model'; import { NetworkDetailsTableType, NetworkTableType, NetworkType } from './model'; +const selectNetwork = ( + state: State, + networkType: NetworkType +): NetworkPageModel | NetworkDetailsModel => get(networkType, state.network); + const selectNetworkPage = (state: State): NetworkPageModel => state.network.page; const selectNetworkDetails = (state: State): NetworkDetailsModel => state.network.details; @@ -87,3 +92,9 @@ export const httpSelector = () => createSelector(selectHttpByType, (httpQueries) export const usersSelector = () => createSelector(selectNetworkDetails, (network) => network.queries.users); + +export const networkAnomaliesJobIdFilterSelector = () => + createSelector( + selectNetwork, + (network) => network.queries[NetworkTableType.anomalies].jobIdSelection + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx index 9abbc96680e64..a794666d13aaa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx @@ -29,6 +29,7 @@ import { useNetworkDetails } from '../../../../network/containers/details'; import { networkModel } from '../../../../network/store'; import { useAnomaliesTableData } from '../../../../common/components/ml/anomaly/use_anomalies_table_data'; import { LandingCards } from '../../../../common/components/landing_cards'; +import { useInstalledSecurityJobsIds } from '../../../../common/components/ml/hooks/use_installed_security_jobs'; interface ExpandableNetworkProps { expandedNetwork: { ip: string; flowTarget: FlowTargetSourceDest }; @@ -115,12 +116,13 @@ export const ExpandableNetworkDetails = ({ }); useInvalidFilterQuery({ id, filterQuery, kqlError, query, startDate: from, endDate: to }); - + const jobIds = useInstalledSecurityJobsIds(); const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({ criteriaFields: networkToCriteria(ip, flowTarget), startDate: from, endDate: to, skip: isInitializing, + jobIds, }); return indicesExist ? ( diff --git a/x-pack/plugins/security_solution/public/users/store/actions.ts b/x-pack/plugins/security_solution/public/users/store/actions.ts index 8465ae4b73927..59474c93d6346 100644 --- a/x-pack/plugins/security_solution/public/users/store/actions.ts +++ b/x-pack/plugins/security_solution/public/users/store/actions.ts @@ -38,3 +38,8 @@ export const updateTableSorting = actionCreator<{ export const updateUserRiskScoreSeverityFilter = actionCreator<{ severitySelection: RiskSeverity[]; }>('UPDATE_USERS_RISK_SEVERITY_FILTER'); + +export const updateUsersAnomaliesJobIdFilter = actionCreator<{ + jobIds: string[]; + usersType: usersModel.UsersType; +}>('UPDATE_USERS_ANOMALIES_JOB_ID_FILTER'); 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 8588790513874..edd728af25e87 100644 --- a/x-pack/plugins/security_solution/public/users/store/model.ts +++ b/x-pack/plugins/security_solution/public/users/store/model.ts @@ -42,16 +42,20 @@ export interface UsersRiskScoreQuery extends BasicQueryPaginated { severitySelection: RiskSeverity[]; } +export interface UsersAnomaliesQuery { + jobIdSelection: string[]; +} + export interface UsersQueries { [UsersTableType.allUsers]: AllUsersQuery; [UsersTableType.authentications]: BasicQueryPaginated; - [UsersTableType.anomalies]: null | undefined; + [UsersTableType.anomalies]: UsersAnomaliesQuery; [UsersTableType.risk]: UsersRiskScoreQuery; [UsersTableType.events]: BasicQueryPaginated; } export interface UserDetailsQueries { - [UsersTableType.anomalies]: null | undefined; + [UsersTableType.anomalies]: UsersAnomaliesQuery; [UsersTableType.events]: BasicQueryPaginated; } 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 c2eae888188b1..f3b012b570f2d 100644 --- a/x-pack/plugins/security_solution/public/users/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/users/store/reducer.ts @@ -14,6 +14,7 @@ import { updateTableLimit, updateTableSorting, updateUserRiskScoreSeverityFilter, + updateUsersAnomaliesJobIdFilter, } from './actions'; import { setUsersPageQueriesActivePageToZero } from './helpers'; import type { UsersModel } from './model'; @@ -46,7 +47,9 @@ export const initialUsersState: UsersModel = { }, severitySelection: [], }, - [UsersTableType.anomalies]: null, + [UsersTableType.anomalies]: { + jobIdSelection: [], + }, [UsersTableType.events]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, @@ -55,7 +58,9 @@ export const initialUsersState: UsersModel = { }, details: { queries: { - [UsersTableType.anomalies]: null, + [UsersTableType.anomalies]: { + jobIdSelection: [], + }, [UsersTableType.events]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, @@ -126,4 +131,35 @@ export const usersReducer = reducerWithInitialState(initialUsersState) }, }, })) + .case(updateUsersAnomaliesJobIdFilter, (state, { jobIds, usersType }) => { + if (usersType === 'page') { + return { + ...state, + page: { + ...state.page, + queries: { + ...state.page.queries, + [UsersTableType.anomalies]: { + ...state[usersType].queries[UsersTableType.anomalies], + jobIdSelection: jobIds, + }, + }, + }, + }; + } else { + return { + ...state, + details: { + ...state.details, + queries: { + ...state.details.queries, + [UsersTableType.anomalies]: { + ...state[usersType].queries[UsersTableType.anomalies], + jobIdSelection: jobIds, + }, + }, + }, + }; + } + }) .build(); diff --git a/x-pack/plugins/security_solution/public/users/store/selectors.ts b/x-pack/plugins/security_solution/public/users/store/selectors.ts index 0e3cf3fef6b2f..844c0ee3a6c06 100644 --- a/x-pack/plugins/security_solution/public/users/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/users/store/selectors.ts @@ -9,11 +9,14 @@ import { createSelector } from 'reselect'; import type { State } from '../../common/store/types'; -import type { UsersPageModel } from './model'; +import type { UserDetailsPageModel, UsersPageModel, UsersType } from './model'; import { UsersTableType } from './model'; const selectUserPage = (state: State): UsersPageModel => state.users.page; +const selectUsers = (state: State, usersType: UsersType): UsersPageModel | UserDetailsPageModel => + state.users[usersType]; + export const allUsersSelector = () => createSelector(selectUserPage, (users) => users.queries[UsersTableType.allUsers]); @@ -25,3 +28,6 @@ export const usersRiskScoreSeverityFilterSelector = () => export const authenticationsSelector = () => createSelector(selectUserPage, (users) => users.queries[UsersTableType.authentications]); + +export const usersAnomaliesJobIdFilterSelector = () => + createSelector(selectUsers, (users) => users.queries[UsersTableType.anomalies].jobIdSelection); From cd2a6a59a5a4d246ef1898251005aee5ec9cee73 Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Thu, 1 Sep 2022 10:48:09 +0200 Subject: [PATCH 03/11] Add interval selector to anomalies tab --- .../ml/anomaly/anomaly_table_provider.tsx | 3 +- .../ml/anomaly/use_anomalies_table_data.ts | 5 +- .../ml/hooks/use_installed_security_jobs.ts | 4 +- .../ml/tables/anomalies_host_table.tsx | 60 ++++--- .../ml/tables/anomalies_network_table.tsx | 26 ++- .../ml/tables/anomalies_user_table.tsx | 62 +++++--- .../components/ml/tables/default_equality.ts | 16 -- .../ml/tables/network_equality.test.ts | 148 ------------------ .../components/ml/tables/network_equality.ts | 16 -- .../ml/tables/select_interval.test.tsx | 9 ++ .../components/ml/tables/select_interval.tsx | 53 +++++++ .../components/ml/tables/translations.ts | 28 ++++ .../public/hosts/store/actions.ts | 5 + .../public/hosts/store/model.ts | 1 + .../public/hosts/store/reducer.ts | 16 ++ .../public/hosts/store/selectors.ts | 3 + .../entity_analytics/anomalies/columns.tsx | 57 ++++++- .../entity_analytics/anomalies/config.ts | 18 ++- .../public/users/store/actions.ts | 5 + .../public/users/store/model.ts | 1 + .../public/users/store/reducer.ts | 34 ++++ .../public/users/store/selectors.ts | 3 + 22 files changed, 329 insertions(+), 244 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx index beabeb8c60f2b..8516655032620 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/anomaly_table_provider.tsx @@ -26,7 +26,7 @@ interface Props { export const AnomalyTableProvider = React.memo( ({ influencers, startDate, endDate, children, criteriaFields, skip }) => { - const jobIds = useInstalledSecurityJobsIds(); + const { jobIds } = useInstalledSecurityJobsIds(); const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({ criteriaFields, influencers, @@ -34,6 +34,7 @@ export const AnomalyTableProvider = React.memo( endDate, skip, jobIds, + aggregationInterval: 'auto', }); return <>{children({ isLoadingAnomaliesData, anomaliesData })}; } 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 aa89e79d4eec0..06732f83f2af7 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 @@ -26,6 +26,7 @@ interface Args { criteriaFields?: CriteriaFields[]; filterQuery?: estypes.QueryDslQueryContainer; jobIds: string[]; + aggregationInterval: string; } type Return = [boolean, Anomalies | null]; @@ -60,6 +61,7 @@ export const useAnomaliesTableData = ({ skip = false, filterQuery, jobIds, + aggregationInterval, }: Args): Return => { const [tableData, setTableData] = useState(null); const mlCapabilities = useMlCapabilities(); @@ -93,7 +95,7 @@ export const useAnomaliesTableData = ({ jobIds, criteriaFields: criteriaFieldsInput, influencersFilterQuery: filterQuery, - aggregationInterval: 'auto', + aggregationInterval, threshold: getThreshold(anomalyScore, threshold), earliestMs, latestMs, @@ -139,6 +141,7 @@ export const useAnomaliesTableData = ({ endDateMs, skip, isMlUser, + aggregationInterval, // eslint-disable-next-line react-hooks/exhaustive-deps jobIds.sort().join(), ]); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts index 81371e07ae1d5..1834db13e7fb2 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts @@ -67,8 +67,8 @@ export const useInstalledSecurityJobs = (): UseInstalledSecurityJobsReturn => { }; export const useInstalledSecurityJobsIds = () => { - const { jobs } = useInstalledSecurityJobs(); + const { jobs, loading } = useInstalledSecurityJobs(); const jobIds = useMemo(() => jobs.map((job) => job.id), [jobs]); - return jobIds; + return { jobIds, loading }; }; 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 675c2c43d317f..1060d47375195 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 @@ -8,9 +8,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; 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 { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns'; @@ -21,12 +21,12 @@ import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type'; import { Panel } from '../../panel'; -import { anomaliesTableDefaultEquality } from './default_equality'; import { useQueryToggle } from '../../../containers/query_toggle'; import { useInstalledSecurityJobsIds } from '../hooks/use_installed_security_jobs'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import type { State } from '../../../store'; import { JobIdFilter } from './job_id_filter'; +import { SelectInterval } from './select_interval'; import { hostsActions, hostsSelectors } from '../../../../hosts/store'; const sorting = { @@ -59,7 +59,7 @@ const AnomaliesHostTableComponent: React.FC = ({ [setQuerySkip, setToggleStatus] ); - const jobIds = useInstalledSecurityJobsIds(); + const { jobIds, loading: loadingJobs } = useInstalledSecurityJobsIds(); const getAnomaliesHostsTableFilterQuerySelector = useMemo( () => hostsSelectors.hostsAnomaliesJobIdFilterSelector(), @@ -82,7 +82,28 @@ const AnomaliesHostTableComponent: React.FC = ({ [dispatch, type] ); - const [loading, tableData] = useAnomaliesTableData({ + const getAnomaliesHostTableIntervalQuerySelector = useMemo( + () => hostsSelectors.hostsAnomaliesIntervalSelector(), + [] + ); + + const selectedInterval = useDeepEqualSelector((state: State) => + getAnomaliesHostTableIntervalQuerySelector(state, type) + ); + + const onSelectInterval = useCallback( + (newInterval: string) => { + dispatch( + hostsActions.updateHostsAnomaliesInterval({ + interval: newInterval, + hostsType: type, + }) + ); + }, + [dispatch, type] + ); + + const [loadingTable, tableData] = useAnomaliesTableData({ startDate, endDate, skip: querySkip, @@ -91,6 +112,7 @@ const AnomaliesHostTableComponent: React.FC = ({ exists: { field: 'host.name' }, }, jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds, + aggregationInterval: selectedInterval, }); const hosts = convertAnomaliesToHosts(tableData, hostName); @@ -108,7 +130,7 @@ const AnomaliesHostTableComponent: React.FC = ({ return null; } else { return ( - + = ({ tooltip={i18n.TOOLTIP} isInspectDisabled={skip} headerFilters={ - jobIds.length > 0 && ( - - ) + + + + + + + + } /> {toggleStatus && ( @@ -140,7 +167,7 @@ const AnomaliesHostTableComponent: React.FC = ({ /> )} - {loading && ( + {(loadingTable || loadingJobs) && ( )} @@ -148,7 +175,4 @@ const AnomaliesHostTableComponent: React.FC = ({ } }; -export const AnomaliesHostTable = React.memo( - AnomaliesHostTableComponent, - anomaliesTableDefaultEquality -); +export const AnomaliesHostTable = React.memo(AnomaliesHostTableComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index f99ec69b2e0a6..839b55d613c8e 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -18,7 +18,6 @@ import type { AnomaliesNetworkTableProps } from '../types'; import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns'; import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; -import { networkEquality } from './network_equality'; import { getCriteriaFromNetworkType } from '../criteria/get_criteria_from_network_type'; import { Panel } from '../../panel'; import { useQueryToggle } from '../../../containers/query_toggle'; @@ -59,7 +58,7 @@ const AnomaliesNetworkTableComponent: React.FC = ({ [setQuerySkip, setToggleStatus] ); - const jobIds = useInstalledSecurityJobsIds(); + const { jobIds, loading: loadingJobs } = useInstalledSecurityJobsIds(); const getAnomaliesUserTableFilterQuerySelector = useMemo( () => networkSelectors.networkAnomaliesJobIdFilterSelector(), @@ -81,12 +80,13 @@ const AnomaliesNetworkTableComponent: React.FC = ({ }, [dispatch, type] ); - const [loading, tableData] = useAnomaliesTableData({ + const [loadingTable, tableData] = useAnomaliesTableData({ startDate, endDate, skip: querySkip, criteriaFields: getCriteriaFromNetworkType(type, ip, flowTarget), jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds, + aggregationInterval: 'second', }); const networks = convertAnomaliesToNetwork(tableData, ip); @@ -103,7 +103,7 @@ const AnomaliesNetworkTableComponent: React.FC = ({ return null; } else { return ( - + = ({ toggleStatus={toggleStatus} isInspectDisabled={skip} headerFilters={ - jobIds.length > 0 && ( - - ) + } /> {toggleStatus && ( @@ -135,7 +133,7 @@ const AnomaliesNetworkTableComponent: React.FC = ({ /> )} - {loading && ( + {(loadingTable || loadingJobs) && ( )} @@ -143,4 +141,4 @@ const AnomaliesNetworkTableComponent: React.FC = ({ } }; -export const AnomaliesNetworkTable = React.memo(AnomaliesNetworkTableComponent, networkEquality); +export const AnomaliesNetworkTable = React.memo(AnomaliesNetworkTableComponent); 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 index 8c47426b69555..f1d8eb8daf328 100644 --- 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 @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { useDispatch } from 'react-redux'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; @@ -21,11 +22,11 @@ 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'; import { useQueryToggle } from '../../../containers/query_toggle'; import { JobIdFilter } from './job_id_filter'; +import { SelectInterval } from './select_interval'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { usersActions, usersSelectors } from '../../../../users/store'; import type { State } from '../../../store/types'; @@ -62,7 +63,7 @@ const AnomaliesUserTableComponent: React.FC = ({ [setQuerySkip, setToggleStatus] ); - const jobIds = useInstalledSecurityJobsIds(); + const { jobIds, loading: loadingJobs } = useInstalledSecurityJobsIds(); const getAnomaliesUserTableFilterQuerySelector = useMemo( () => usersSelectors.usersAnomaliesJobIdFilterSelector(), @@ -85,7 +86,28 @@ const AnomaliesUserTableComponent: React.FC = ({ [dispatch, type] ); - const [loading, tableData] = useAnomaliesTableData({ + const getAnomaliesUserTableIntervalQuerySelector = useMemo( + () => usersSelectors.usersAnomaliesIntervalSelector(), + [] + ); + + const selectedInterval = useDeepEqualSelector((state: State) => + getAnomaliesUserTableIntervalQuerySelector(state, type) + ); + + const onSelectInterval = useCallback( + (newInterval: string) => { + dispatch( + usersActions.updateUsersAnomaliesInterval({ + interval: newInterval, + usersType: type, + }) + ); + }, + [dispatch, type] + ); + + const [loadingTable, tableData] = useAnomaliesTableData({ startDate, endDate, skip: querySkip, @@ -94,6 +116,7 @@ const AnomaliesUserTableComponent: React.FC = ({ exists: { field: 'user.name' }, }, jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds, + aggregationInterval: selectedInterval, }); const users = convertAnomaliesToUsers(tableData, userName); @@ -111,7 +134,7 @@ const AnomaliesUserTableComponent: React.FC = ({ return null; } else { return ( - + = ({ tooltip={i18n.TOOLTIP} isInspectDisabled={skip} headerFilters={ - jobIds.length > 0 && ( - - ) + + + + + + + + + } /> @@ -144,15 +173,12 @@ const AnomaliesUserTableComponent: React.FC = ({ /> )} - {loading && ( - + {(loadingTable || loadingJobs) && ( + )} ); } }; -export const AnomaliesUserTable = React.memo( - AnomaliesUserTableComponent, - anomaliesTableDefaultEquality -); +export const AnomaliesUserTable = React.memo(AnomaliesUserTableComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.ts deleted file mode 100644 index 6285a7dbe6744..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AnomaliesTableCommonProps } from '../types'; - -export const anomaliesTableDefaultEquality = ( - prevProps: AnomaliesTableCommonProps, - nextProps: AnomaliesTableCommonProps -): boolean => - prevProps.startDate === nextProps.startDate && - prevProps.endDate === nextProps.endDate && - prevProps.skip === nextProps.skip; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts deleted file mode 100644 index e2b9ba44d0e13..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { networkEquality } from './network_equality'; -import type { AnomaliesNetworkTableProps } from '../types'; -import { NetworkType } from '../../../../network/store/model'; -import { FlowTarget } from '../../../../../common/search_strategy'; - -describe('network_equality', () => { - test('it returns true if start and end date are equal', () => { - const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const equal = networkEquality(prev, next); - expect(equal).toEqual(true); - }); - - test('it returns false if starts are not equal', () => { - const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2001').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const equal = networkEquality(prev, next); - expect(equal).toEqual(false); - }); - - test('it returns false if starts are not equal for next', () => { - const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const next: AnomaliesNetworkTableProps = { - startDate: new Date('2001').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const equal = networkEquality(prev, next); - expect(equal).toEqual(false); - }); - - test('it returns false if ends are not equal', () => { - const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2001').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const equal = networkEquality(prev, next); - expect(equal).toEqual(false); - }); - - test('it returns false if ends are not equal for next', () => { - const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2001').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const equal = networkEquality(prev, next); - expect(equal).toEqual(false); - }); - - test('it returns false if skip is not equal', () => { - const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: true, - type: NetworkType.details, - }; - const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - }; - const equal = networkEquality(prev, next); - expect(equal).toEqual(false); - }); - - test('it returns false if flowType is not equal', () => { - const prev: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: true, - type: NetworkType.details, - flowTarget: FlowTarget.source, - }; - const next: AnomaliesNetworkTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: NetworkType.details, - flowTarget: FlowTarget.destination, - }; - const equal = networkEquality(prev, next); - expect(equal).toEqual(false); - }); -}); 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 deleted file mode 100644 index a62a41cb95c36..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/network_equality.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { AnomaliesNetworkTableProps } from '../types'; -import { anomaliesTableDefaultEquality } from './default_equality'; - -export const networkEquality = ( - prevProps: AnomaliesNetworkTableProps, - nextProps: AnomaliesNetworkTableProps -): boolean => - anomaliesTableDefaultEquality(prevProps, nextProps) && - prevProps.flowTarget === nextProps.flowTarget; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.test.tsx new file mode 100644 index 0000000000000..6e245e971de2d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.test.tsx @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// TODO +// it renders diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.tsx new file mode 100644 index 0000000000000..603bfa1172eb1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback } from 'react'; +import { EuiSelect, EuiIcon, EuiToolTip } from '@elastic/eui'; +import * as i18n from './translations'; + +const OPTIONS = [ + { + value: 'auto', + text: i18n.INTERVAL_AUTO, + }, + { + value: 'hour', + text: i18n.INTERVAL_HOUR, + }, + { + value: 'day', + text: i18n.INTERVAL_DAY, + }, + { + value: 'second', + text: i18n.INTERVAL_SHOW_ALL, + }, +]; + +export const SelectInterval: React.FC<{ + interval: string; + onChange: (interval: string) => void; +}> = ({ interval, onChange }) => { + const onChangeCb = useCallback( + (e) => { + onChange(e.target.value); + }, + [onChange] + ); + return ( + + + + } + options={OPTIONS} + value={interval} + onChange={onChangeCb} + /> + ); +}; 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 bb385fa415f52..1a5b627f5007a 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 @@ -69,3 +69,31 @@ export const TIME_STAMP = i18n.translate('xpack.securitySolution.ml.table.timest export const JOB_ID = i18n.translate('xpack.securitySolution.ml.table.jobIdFilter', { defaultMessage: 'Job', }); + +export const INTERVAL_TOOLTIP = i18n.translate('xpack.securitySolution.ml.table.intervalTooltip', { + defaultMessage: + 'Show only the highest severity anomaly for each interval (such as hour or day) or show all anomalies in the selected time period.', +}); + +export const INTERVAL = i18n.translate('xpack.securitySolution.ml.table.intervalLabel', { + defaultMessage: 'Interval', +}); + +export const INTERVAL_AUTO = i18n.translate('xpack.securitySolution.ml.table.intervalAutoOption', { + defaultMessage: 'Auto', +}); + +export const INTERVAL_HOUR = i18n.translate('xpack.securitySolution.ml.table.intervalHourOption', { + defaultMessage: '1 hour', +}); + +export const INTERVAL_DAY = i18n.translate('xpack.securitySolution.ml.table.intervalDayOption', { + defaultMessage: '1 day', +}); + +export const INTERVAL_SHOW_ALL = i18n.translate( + 'xpack.securitySolution.ml.table.intervalshowAllOption', + { + defaultMessage: 'Show all', + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/store/actions.ts b/x-pack/plugins/security_solution/public/hosts/store/actions.ts index 76d43c030e994..ed5f29342e827 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/actions.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/actions.ts @@ -49,3 +49,8 @@ export const updateHostsAnomaliesJobIdFilter = actionCreator<{ jobIds: string[]; hostsType: HostsType; }>('UPDATE_HOSTS_ANOMALIES_JOB_ID_FILTER'); + +export const updateHostsAnomaliesInterval = actionCreator<{ + interval: string; + hostsType: HostsType; +}>('UPDATE_HOSTS_ANOMALIES_INTERVAL'); diff --git a/x-pack/plugins/security_solution/public/hosts/store/model.ts b/x-pack/plugins/security_solution/public/hosts/store/model.ts index d3660370aed65..07c6803f77e2f 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/model.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/model.ts @@ -44,6 +44,7 @@ export interface HostRiskScoreQuery extends BasicQueryPaginated { export interface HostsAnomaliesQuery { jobIdSelection: string[]; + intervalSelection: string; } export interface Queries { diff --git a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts index 302dea2875ace..15f4d979a7267 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts @@ -19,6 +19,7 @@ import { updateTableActivePage, updateTableLimit, updateHostsAnomaliesJobIdFilter, + updateHostsAnomaliesInterval, } from './actions'; import { setHostPageQueriesActivePageToZero, @@ -52,6 +53,7 @@ export const initialHostsState: HostsState = { }, [HostsTableType.anomalies]: { jobIdSelection: [], + intervalSelection: 'auto', }, [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, @@ -90,6 +92,7 @@ export const initialHostsState: HostsState = { }, [HostsTableType.anomalies]: { jobIdSelection: [], + intervalSelection: 'auto', }, [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, @@ -208,4 +211,17 @@ export const hostsReducer = reducerWithInitialState(initialHostsState) }, }, })) + .case(updateHostsAnomaliesInterval, (state, { interval, hostsType }) => ({ + ...state, + [hostsType]: { + ...state[hostsType], + queries: { + ...state[hostsType].queries, + [HostsTableType.anomalies]: { + ...state[hostsType].queries[HostsTableType.anomalies], + intervalSelection: interval, + }, + }, + }, + })) .build(); diff --git a/x-pack/plugins/security_solution/public/hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/hosts/store/selectors.ts index f922028c8a815..72b039df120c6 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/selectors.ts @@ -33,3 +33,6 @@ export const uncommonProcessesSelector = () => export const hostsAnomaliesJobIdFilterSelector = () => createSelector(selectHosts, (hosts) => hosts.queries[HostsTableType.anomalies].jobIdSelection); + +export const hostsAnomaliesIntervalSelector = () => + createSelector(selectHosts, (hosts) => hosts.queries[HostsTableType.anomalies].intervalSelection); diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx index c29c596897d9a..4c2527d0798d2 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx @@ -8,11 +8,19 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import type { EuiBasicTableColumn } from '@elastic/eui'; import { ML_PAGES, useMlHref } from '@kbn/ml-plugin/public'; +import { useDispatch } from 'react-redux'; import * as i18n from './translations'; import type { AnomaliesCount } from '../../../../common/components/ml/anomaly/use_anomalies_search'; import { AnomalyJobStatus } from '../../../../common/components/ml/anomaly/use_anomalies_search'; import { useKibana } from '../../../../common/lib/kibana'; -import { LinkAnchor } from '../../../../common/components/links'; +import { LinkAnchor, SecuritySolutionLinkAnchor } from '../../../../common/components/links'; +import { SecurityPageName } from '../../../../app/types'; +import type { NotableAnomaliesJobId, NotableAnomaliesJobId } from './config'; +import { AnomalyConfigEntity, NOTABLE_ANOMALIES_CONFIG } from './config'; +import { usersActions } from '../../../../users/store'; +import { hostsActions } from '../../../../hosts/store'; +import { HostsType } from '../../../../hosts/store/model'; +import { UsersType } from '../../../../users/store/model'; type AnomaliesColumns = Array>; @@ -54,11 +62,11 @@ export const useAnomaliesColumns = (loading: boolean): AnomaliesColumns => { mobileOptions: { show: true }, width: '15%', 'data-test-subj': 'anomalies-table-column-count', - render: (count, { status, jobId }) => { + render: (count, { status, jobId, name }) => { if (loading) return ''; if (count > 0 || status === AnomalyJobStatus.enabled) { - return count; + return ; } else { if (status === AnomalyJobStatus.disabled && jobId) { return ; @@ -110,3 +118,46 @@ const EnableJobLink = ({ jobId }: { jobId: string }) => { ); }; + +const AnomaliesTabLink = ({ + count, + jobId, + jobName, +}: { + count: number; + jobId?: string; + jobName: NotableAnomaliesJobId; +}) => { + const dispatch = useDispatch(); + const entity = NOTABLE_ANOMALIES_CONFIG[jobName].entity; + const deepLinkId = + entity === AnomalyConfigEntity.User + ? SecurityPageName.usersAnomalies + : SecurityPageName.hostsAnomalies; + + const onClick = useCallback(() => { + if (!jobId) return; + + if (entity === AnomalyConfigEntity.User) { + dispatch( + usersActions.updateUsersAnomaliesJobIdFilter({ + jobIds: [jobId], + usersType: UsersType.page, + }) + ); + } else { + dispatch( + hostsActions.updateHostsAnomaliesJobIdFilter({ + jobIds: [jobId], + hostsType: HostsType.page, + }) + ); + } + }, [jobId, dispatch, entity]); + + return ( + + {count} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/config.ts b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/config.ts index 3f010158aaa2a..16e0d006cdae3 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/config.ts +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/config.ts @@ -5,29 +5,33 @@ * 2.0. */ +export const enum AnomalyConfigEntity { + User, + Host, +} interface AnomalyConfig { name: string; - entity: 'User' | 'Host'; + entity: AnomalyConfigEntity; } export const NOTABLE_ANOMALIES_CONFIG = { auth_rare_source_ip_for_a_user: { - entity: 'User', + entity: AnomalyConfigEntity.User, }, packetbeat_dns_tunneling: { - entity: 'Host', + entity: AnomalyConfigEntity.Host, }, packetbeat_rare_server_domain: { - entity: 'Host', + entity: AnomalyConfigEntity.Host, }, packetbeat_rare_dns_question: { - entity: 'Host', + entity: AnomalyConfigEntity.Host, }, suspicious_login_activity: { - entity: 'User', + entity: AnomalyConfigEntity.User, }, v3_windows_anomalous_script: { - entity: 'User', + entity: AnomalyConfigEntity.User, }, }; diff --git a/x-pack/plugins/security_solution/public/users/store/actions.ts b/x-pack/plugins/security_solution/public/users/store/actions.ts index 59474c93d6346..fa47107959293 100644 --- a/x-pack/plugins/security_solution/public/users/store/actions.ts +++ b/x-pack/plugins/security_solution/public/users/store/actions.ts @@ -43,3 +43,8 @@ export const updateUsersAnomaliesJobIdFilter = actionCreator<{ jobIds: string[]; usersType: usersModel.UsersType; }>('UPDATE_USERS_ANOMALIES_JOB_ID_FILTER'); + +export const updateUsersAnomaliesInterval = actionCreator<{ + interval: string; + usersType: usersModel.UsersType; +}>('UPDATE_USERS_ANOMALIES_INTERVAL'); 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 edd728af25e87..de9606d163944 100644 --- a/x-pack/plugins/security_solution/public/users/store/model.ts +++ b/x-pack/plugins/security_solution/public/users/store/model.ts @@ -44,6 +44,7 @@ export interface UsersRiskScoreQuery extends BasicQueryPaginated { export interface UsersAnomaliesQuery { jobIdSelection: string[]; + intervalSelection: string; } export interface UsersQueries { 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 f3b012b570f2d..7a9a224c49e08 100644 --- a/x-pack/plugins/security_solution/public/users/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/users/store/reducer.ts @@ -14,6 +14,7 @@ import { updateTableLimit, updateTableSorting, updateUserRiskScoreSeverityFilter, + updateUsersAnomaliesInterval, updateUsersAnomaliesJobIdFilter, } from './actions'; import { setUsersPageQueriesActivePageToZero } from './helpers'; @@ -49,6 +50,7 @@ export const initialUsersState: UsersModel = { }, [UsersTableType.anomalies]: { jobIdSelection: [], + intervalSelection: 'auto', }, [UsersTableType.events]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, @@ -60,6 +62,7 @@ export const initialUsersState: UsersModel = { queries: { [UsersTableType.anomalies]: { jobIdSelection: [], + intervalSelection: 'auto', }, [UsersTableType.events]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, @@ -162,4 +165,35 @@ export const usersReducer = reducerWithInitialState(initialUsersState) }; } }) + .case(updateUsersAnomaliesInterval, (state, { interval, usersType }) => { + if (usersType === 'page') { + return { + ...state, + page: { + ...state.page, + queries: { + ...state.page.queries, + [UsersTableType.anomalies]: { + ...state[usersType].queries[UsersTableType.anomalies], + intervalSelection: interval, + }, + }, + }, + }; + } else { + return { + ...state, + details: { + ...state.details, + queries: { + ...state.details.queries, + [UsersTableType.anomalies]: { + ...state[usersType].queries[UsersTableType.anomalies], + intervalSelection: interval, + }, + }, + }, + }; + } + }) .build(); diff --git a/x-pack/plugins/security_solution/public/users/store/selectors.ts b/x-pack/plugins/security_solution/public/users/store/selectors.ts index 844c0ee3a6c06..db054c88cf3ad 100644 --- a/x-pack/plugins/security_solution/public/users/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/users/store/selectors.ts @@ -31,3 +31,6 @@ export const authenticationsSelector = () => export const usersAnomaliesJobIdFilterSelector = () => createSelector(selectUsers, (users) => users.queries[UsersTableType.anomalies].jobIdSelection); + +export const usersAnomaliesIntervalSelector = () => + createSelector(selectUsers, (users) => users.queries[UsersTableType.anomalies].intervalSelection); From f2beebf2ebb8d2b39aaab4f6267cba181b787eac Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Thu, 1 Sep 2022 11:08:07 +0200 Subject: [PATCH 04/11] Preselect anomalies table interval from entity analytics page link --- .../entity_analytics/anomalies/columns.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx index 4c2527d0798d2..cce4cd9dcbbad 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx @@ -145,6 +145,13 @@ const AnomaliesTabLink = ({ usersType: UsersType.page, }) ); + + dispatch( + usersActions.updateUsersAnomaliesInterval({ + interval: 'second', + usersType: UsersType.page, + }) + ); } else { dispatch( hostsActions.updateHostsAnomaliesJobIdFilter({ @@ -152,6 +159,13 @@ const AnomaliesTabLink = ({ hostsType: HostsType.page, }) ); + + dispatch( + hostsActions.updateHostsAnomaliesInterval({ + interval: 'second', + hostsType: HostsType.page, + }) + ); } }, [jobId, dispatch, entity]); From 4afa0bc77824f35e794ac9360bb257f3719a4cc3 Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Thu, 1 Sep 2022 13:18:47 +0200 Subject: [PATCH 05/11] Infer anomaly entity from top hits aggregation --- .../ml/anomaly/use_anomalies_search.ts | 13 ++++- .../entity_analytics/anomalies/columns.tsx | 20 +++++--- .../entity_analytics/anomalies/config.ts | 50 ++++++------------- .../entity_analytics/anomalies/query/index.ts | 10 ++++ 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts index 74dc65a7b4797..931f4d9707c2e 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts @@ -6,8 +6,9 @@ */ import { useState, useEffect, useMemo, useRef } from 'react'; -import { filter, head, noop, orderBy, pipe } from 'lodash/fp'; +import { filter, head, noop, orderBy, pipe, get } from 'lodash/fp'; import type { MlSummaryJob } from '@kbn/ml-plugin/common'; + import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import * as i18n from './translations'; import { useUiSetting$ } from '../../../lib/kibana'; @@ -27,11 +28,17 @@ export enum AnomalyJobStatus { 'failed', } +export const enum AnomalyEntity { + User, + Host, +} + export interface AnomaliesCount { name: NotableAnomaliesJobId; jobId?: string; count: number; status: AnomalyJobStatus; + entity: AnomalyEntity; } interface UseNotableAnomaliesSearchProps { @@ -142,22 +149,26 @@ const getMLJobStatus = ( ? AnomalyJobStatus.disabled : AnomalyJobStatus.uninstalled; }; + function formatResultData( buckets: Array<{ key: string; doc_count: number; + // entity: EntityHits; }>, notableAnomaliesJobs: MlSummaryJob[] ): AnomaliesCount[] { return NOTABLE_ANOMALIES_IDS.map((notableJobId) => { const job = findJobWithId(notableJobId)(notableAnomaliesJobs); const bucket = buckets.find(({ key }) => key === job?.id); + const hasUserName = !!get("entity.hits.hits[0]._source['user.name']", bucket); return { name: notableJobId, jobId: job?.id, count: bucket?.doc_count ?? 0, status: getMLJobStatus(notableJobId, job, notableAnomaliesJobs), + entity: hasUserName ? AnomalyEntity.User : AnomalyEntity.Host, }; }); } diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx index cce4cd9dcbbad..c9526db88d3bd 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx @@ -11,12 +11,14 @@ import { ML_PAGES, useMlHref } from '@kbn/ml-plugin/public'; import { useDispatch } from 'react-redux'; import * as i18n from './translations'; import type { AnomaliesCount } from '../../../../common/components/ml/anomaly/use_anomalies_search'; -import { AnomalyJobStatus } from '../../../../common/components/ml/anomaly/use_anomalies_search'; +import { + AnomalyJobStatus, + AnomalyEntity, +} from '../../../../common/components/ml/anomaly/use_anomalies_search'; import { useKibana } from '../../../../common/lib/kibana'; import { LinkAnchor, SecuritySolutionLinkAnchor } from '../../../../common/components/links'; import { SecurityPageName } from '../../../../app/types'; -import type { NotableAnomaliesJobId, NotableAnomaliesJobId } from './config'; -import { AnomalyConfigEntity, NOTABLE_ANOMALIES_CONFIG } from './config'; +import type { NotableAnomaliesJobId } from './config'; import { usersActions } from '../../../../users/store'; import { hostsActions } from '../../../../hosts/store'; import { HostsType } from '../../../../hosts/store/model'; @@ -62,11 +64,11 @@ export const useAnomaliesColumns = (loading: boolean): AnomaliesColumns => { mobileOptions: { show: true }, width: '15%', 'data-test-subj': 'anomalies-table-column-count', - render: (count, { status, jobId, name }) => { + render: (count, { status, jobId, name, entity }) => { if (loading) return ''; if (count > 0 || status === AnomalyJobStatus.enabled) { - return ; + return ; } else { if (status === AnomalyJobStatus.disabled && jobId) { return ; @@ -123,22 +125,24 @@ const AnomaliesTabLink = ({ count, jobId, jobName, + entity, }: { count: number; jobId?: string; jobName: NotableAnomaliesJobId; + entity: AnomalyEntity; }) => { const dispatch = useDispatch(); - const entity = NOTABLE_ANOMALIES_CONFIG[jobName].entity; + const deepLinkId = - entity === AnomalyConfigEntity.User + entity === AnomalyEntity.User ? SecurityPageName.usersAnomalies : SecurityPageName.hostsAnomalies; const onClick = useCallback(() => { if (!jobId) return; - if (entity === AnomalyConfigEntity.User) { + if (entity === AnomalyEntity.User) { dispatch( usersActions.updateUsersAnomaliesJobIdFilter({ jobIds: [jobId], diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/config.ts b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/config.ts index 16e0d006cdae3..29f227e800f7a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/config.ts +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/config.ts @@ -5,38 +5,18 @@ * 2.0. */ -export const enum AnomalyConfigEntity { - User, - Host, -} -interface AnomalyConfig { - name: string; - entity: AnomalyConfigEntity; -} - -export const NOTABLE_ANOMALIES_CONFIG = { - auth_rare_source_ip_for_a_user: { - entity: AnomalyConfigEntity.User, - }, - packetbeat_dns_tunneling: { - entity: AnomalyConfigEntity.Host, - }, - packetbeat_rare_server_domain: { - entity: AnomalyConfigEntity.Host, - }, - packetbeat_rare_dns_question: { - entity: AnomalyConfigEntity.Host, - }, - suspicious_login_activity: { - entity: AnomalyConfigEntity.User, - }, - v3_windows_anomalous_script: { - entity: AnomalyConfigEntity.User, - }, -}; - -export const NOTABLE_ANOMALIES_IDS = Object.keys( - NOTABLE_ANOMALIES_CONFIG -) as NotableAnomaliesJobId[]; -export type NotableAnomaliesJobId = keyof typeof NOTABLE_ANOMALIES_CONFIG; -export type NotableAnomaliesConfig = Record; +export const NOTABLE_ANOMALIES_IDS: NotableAnomaliesJobId[] = [ + 'auth_rare_source_ip_for_a_user', + 'packetbeat_dns_tunneling', + 'packetbeat_rare_server_domain', + 'packetbeat_rare_dns_question', + 'suspicious_login_activity', + 'v3_windows_anomalous_script', +]; +export type NotableAnomaliesJobId = + | 'auth_rare_source_ip_for_a_user' + | 'packetbeat_dns_tunneling' + | 'packetbeat_rare_server_domain' + | 'packetbeat_rare_dns_question' + | 'suspicious_login_activity' + | 'v3_windows_anomalous_script'; diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/query/index.ts b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/query/index.ts index c3727a0f893ae..4537bda9c39b7 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/query/index.ts +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/query/index.ts @@ -53,6 +53,16 @@ export const getAggregatedAnomaliesQuery = ({ terms: { field: 'job_id', }, + aggs: { + entity: { + top_hits: { + _source: { + includes: ['host.name', 'user.name'], + }, + size: 1, + }, + }, + }, }, }, }); From 9bd6582d725678f8965523d53ac542b9adcdcfbf Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Thu, 1 Sep 2022 14:11:21 +0200 Subject: [PATCH 06/11] Delete unused field --- .../components/entity_analytics/anomalies/columns.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx index c9526db88d3bd..088875d541efe 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx @@ -18,7 +18,6 @@ import { import { useKibana } from '../../../../common/lib/kibana'; import { LinkAnchor, SecuritySolutionLinkAnchor } from '../../../../common/components/links'; import { SecurityPageName } from '../../../../app/types'; -import type { NotableAnomaliesJobId } from './config'; import { usersActions } from '../../../../users/store'; import { hostsActions } from '../../../../hosts/store'; import { HostsType } from '../../../../hosts/store/model'; @@ -68,7 +67,7 @@ export const useAnomaliesColumns = (loading: boolean): AnomaliesColumns => { if (loading) return ''; if (count > 0 || status === AnomalyJobStatus.enabled) { - return ; + return ; } else { if (status === AnomalyJobStatus.disabled && jobId) { return ; @@ -124,12 +123,10 @@ const EnableJobLink = ({ jobId }: { jobId: string }) => { const AnomaliesTabLink = ({ count, jobId, - jobName, entity, }: { count: number; jobId?: string; - jobName: NotableAnomaliesJobId; entity: AnomalyEntity; }) => { const dispatch = useDispatch(); From dc03721e13a3c43dfab9bf9fca65cebaf52aa025 Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Thu, 1 Sep 2022 14:39:19 +0200 Subject: [PATCH 07/11] Didable job id filter when there are no job ids to display --- .../public/common/components/ml/tables/job_id_filter.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx index 672c35f5bd1cc..cb2aba4a6ae2b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx @@ -38,18 +38,19 @@ export const JobIdFilter: React.FC<{ const button = useMemo( () => ( 0} iconType="arrowDown" isSelected={isPopoverOpen} numActiveFilters={selectedJobIds.length} onClick={onButtonClick} - contentProps={{ style: { minWidth: 120 } }} // avoid resizing when selecting job id + contentProps={{ style: { minWidth: 112 } }} // avoid resizing when selecting job id > {title} ), - [isPopoverOpen, onButtonClick, title, selectedJobIds.length] + [isPopoverOpen, onButtonClick, title, selectedJobIds.length, jobIds] ); return ( From eb28fed4edf714a1898876d8f05f6f3b5730322c Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Thu, 1 Sep 2022 15:52:42 +0200 Subject: [PATCH 08/11] Fix many TS issue and broken tests --- .../ml/tables/anomalies_network_table.tsx | 45 ++++++- .../ml/tables/default_equality.test.ts | 126 ------------------ .../public/common/mock/global_state.ts | 28 +++- .../public/hosts/store/helpers.test.ts | 11 +- .../public/network/pages/details/index.tsx | 3 +- .../public/network/store/actions.ts | 5 + .../public/network/store/helpers.test.ts | 8 ++ .../public/network/store/model.ts | 1 + .../public/network/store/reducer.ts | 36 +++++ .../public/network/store/selectors.ts | 6 + .../entity_analytics/anomalies/index.test.tsx | 10 +- .../network_details/expandable_network.tsx | 3 +- .../public/users/store/reducer.ts | 16 +-- 13 files changed, 148 insertions(+), 150 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.test.ts diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index 839b55d613c8e..7fdafcfc2ea8d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; @@ -26,6 +27,7 @@ import { useDeepEqualSelector } from '../../../hooks/use_selector'; import type { State } from '../../../store'; import { JobIdFilter } from './job_id_filter'; import { networkActions, networkSelectors } from '../../../../network/store'; +import { SelectInterval } from './select_interval'; const sorting = { sort: { @@ -80,13 +82,35 @@ const AnomaliesNetworkTableComponent: React.FC = ({ }, [dispatch, type] ); + + const getAnomaliesNetworkTableIntervalQuerySelector = useMemo( + () => networkSelectors.networkAnomaliesIntervalSelector(), + [] + ); + + const selectedInterval = useDeepEqualSelector((state: State) => + getAnomaliesNetworkTableIntervalQuerySelector(state, type) + ); + + const onSelectInterval = useCallback( + (newInterval: string) => { + dispatch( + networkActions.updateNetworkAnomaliesInterval({ + interval: newInterval, + networkType: type, + }) + ); + }, + [dispatch, type] + ); + const [loadingTable, tableData] = useAnomaliesTableData({ startDate, endDate, skip: querySkip, criteriaFields: getCriteriaFromNetworkType(type, ip, flowTarget), jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds, - aggregationInterval: 'second', + aggregationInterval: selectedInterval, }); const networks = convertAnomaliesToNetwork(tableData, ip); @@ -114,12 +138,19 @@ const AnomaliesNetworkTableComponent: React.FC = ({ toggleStatus={toggleStatus} isInspectDisabled={skip} headerFilters={ - + + + + + + + + } /> {toggleStatus && ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.test.ts deleted file mode 100644 index ef644f621c1e1..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/default_equality.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { anomaliesTableDefaultEquality } from './default_equality'; -import type { AnomaliesHostTableProps } from '../types'; -import { HostsType } from '../../../../hosts/store/model'; - -describe('host_equality', () => { - test('it returns true if start and end date are equal', () => { - const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const next: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const equal = anomaliesTableDefaultEquality(prev, next); - expect(equal).toEqual(true); - }); - - test('it returns false if starts are not equal', () => { - const prev: AnomaliesHostTableProps = { - startDate: new Date('2001').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const next: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const equal = anomaliesTableDefaultEquality(prev, next); - expect(equal).toEqual(false); - }); - - test('it returns false if starts are not equal for next', () => { - const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const next: AnomaliesHostTableProps = { - startDate: new Date('2001').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const equal = anomaliesTableDefaultEquality(prev, next); - expect(equal).toEqual(false); - }); - - test('it returns false if ends are not equal', () => { - const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2001').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const next: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const equal = anomaliesTableDefaultEquality(prev, next); - expect(equal).toEqual(false); - }); - - test('it returns false if ends are not equal for next', () => { - const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const next: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2001').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const equal = anomaliesTableDefaultEquality(prev, next); - expect(equal).toEqual(false); - }); - - test('it returns false if skip is not equal', () => { - const prev: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: true, - type: HostsType.details, - }; - const next: AnomaliesHostTableProps = { - startDate: new Date('2000').toISOString(), - endDate: new Date('2000').toISOString(), - narrowDateRange: jest.fn(), - skip: false, - type: HostsType.details, - }; - const equal = anomaliesTableDefaultEquality(prev, next); - expect(equal).toEqual(false); - }); -}); 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 8e1b707147b66..00ca0e0a5852c 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 @@ -75,7 +75,10 @@ export const mockGlobalState: State = { }, events: { activePage: 0, limit: 10 }, uncommonProcesses: { activePage: 0, limit: 10 }, - anomalies: null, + anomalies: { + jobIdSelection: [], + intervalSelection: 'auto', + }, hostRisk: { activePage: 0, limit: 10, @@ -96,7 +99,10 @@ export const mockGlobalState: State = { }, events: { activePage: 0, limit: 10 }, uncommonProcesses: { activePage: 0, limit: 10 }, - anomalies: null, + anomalies: { + jobIdSelection: [], + intervalSelection: 'auto', + }, hostRisk: { activePage: 0, limit: 10, @@ -150,6 +156,10 @@ export const mockGlobalState: State = { activePage: 0, limit: 10, }, + [networkModel.NetworkTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, }, }, details: { @@ -190,6 +200,10 @@ export const mockGlobalState: State = { limit: 10, sort: { direction: Direction.desc }, }, + [networkModel.NetworkTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, }, }, }, @@ -205,7 +219,10 @@ export const mockGlobalState: State = { activePage: 0, limit: 10, }, - [usersModel.UsersTableType.anomalies]: null, + [usersModel.UsersTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, [usersModel.UsersTableType.risk]: { activePage: 0, limit: 10, @@ -220,7 +237,10 @@ export const mockGlobalState: State = { }, details: { queries: { - [usersModel.UsersTableType.anomalies]: null, + [usersModel.UsersTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, [usersModel.UsersTableType.events]: { activePage: 0, limit: 10 }, }, }, diff --git a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts index 186b48f64e40e..78f58e703f09b 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts @@ -32,7 +32,11 @@ export const mockHostsState: HostsModel = { activePage: 8, limit: DEFAULT_TABLE_LIMIT, }, - [HostsTableType.anomalies]: null, + [HostsTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, + [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, @@ -68,7 +72,10 @@ export const mockHostsState: HostsModel = { activePage: 8, limit: DEFAULT_TABLE_LIMIT, }, - [HostsTableType.anomalies]: null, + [HostsTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx index 382d90cb35610..b0dda455293d9 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx @@ -116,13 +116,14 @@ const NetworkDetailsComponent: React.FC = () => { ip, }); - const jobIds = useInstalledSecurityJobsIds(); + const { jobIds } = useInstalledSecurityJobsIds(); const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({ criteriaFields: networkToCriteria(detailName, flowTarget), startDate: from, endDate: to, skip: isInitializing, jobIds, + aggregationInterval: 'auto', }); const headerDraggableArguments = useMemo( diff --git a/x-pack/plugins/security_solution/public/network/store/actions.ts b/x-pack/plugins/security_solution/public/network/store/actions.ts index c472012fbd7a0..2ce6f5397d865 100644 --- a/x-pack/plugins/security_solution/public/network/store/actions.ts +++ b/x-pack/plugins/security_solution/public/network/store/actions.ts @@ -29,3 +29,8 @@ export const updateNetworkAnomaliesJobIdFilter = actionCreator<{ jobIds: string[]; networkType: NetworkType; }>('UPDATE_NETWORK_ANOMALIES_JOB_ID_FILTER'); + +export const updateNetworkAnomaliesInterval = actionCreator<{ + interval: string; + networkType: NetworkType; +}>('UPDATE_NETWORK_ANOMALIES_INTERVAL'); diff --git a/x-pack/plugins/security_solution/public/network/store/helpers.test.ts b/x-pack/plugins/security_solution/public/network/store/helpers.test.ts index 08436c262356a..6b24a44b26b38 100644 --- a/x-pack/plugins/security_solution/public/network/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/network/store/helpers.test.ts @@ -79,6 +79,10 @@ export const mockNetworkState: NetworkModel = { activePage: 0, limit: DEFAULT_TABLE_LIMIT, }, + [NetworkDetailsTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, }, }, details: { @@ -136,6 +140,10 @@ export const mockNetworkState: NetworkModel = { limit: DEFAULT_TABLE_LIMIT, sort: { direction: Direction.desc }, }, + [NetworkDetailsTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, }, flowTarget: FlowTarget.source, }, diff --git a/x-pack/plugins/security_solution/public/network/store/model.ts b/x-pack/plugins/security_solution/public/network/store/model.ts index 7560e09d6d34f..dd3459ac891cd 100644 --- a/x-pack/plugins/security_solution/public/network/store/model.ts +++ b/x-pack/plugins/security_solution/public/network/store/model.ts @@ -120,6 +120,7 @@ export interface NetworkUsersQuery extends BasicQueryPaginated { export interface NetworkAnomaliesQuery { jobIdSelection: string[]; + intervalSelection: string; } export interface NetworkDetailsQueries { diff --git a/x-pack/plugins/security_solution/public/network/store/reducer.ts b/x-pack/plugins/security_solution/public/network/store/reducer.ts index 87844f80d15f7..9ef2325601d59 100644 --- a/x-pack/plugins/security_solution/public/network/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/network/store/reducer.ts @@ -22,6 +22,7 @@ import { setNetworkTablesActivePageToZero, updateNetworkTable, updateNetworkAnomaliesJobIdFilter, + updateNetworkAnomaliesInterval, } from './actions'; import { setNetworkDetailsQueriesActivePageToZero, @@ -97,6 +98,7 @@ export const initialNetworkState: NetworkState = { }, [NetworkTableType.anomalies]: { jobIdSelection: [], + intervalSelection: 'auto', }, }, }, @@ -159,6 +161,7 @@ export const initialNetworkState: NetworkState = { }, [NetworkDetailsTableType.anomalies]: { jobIdSelection: [], + intervalSelection: 'auto', }, }, flowTarget: FlowTarget.source, @@ -206,6 +209,7 @@ export const networkReducer = reducerWithInitialState(initialNetworkState) queries: { ...state.page.queries, anomalies: { + ...state.page.queries.anomalies, jobIdSelection: jobIds, }, }, @@ -219,6 +223,7 @@ export const networkReducer = reducerWithInitialState(initialNetworkState) queries: { ...state.details.queries, anomalies: { + ...state.details.queries.anomalies, jobIdSelection: jobIds, }, }, @@ -226,4 +231,35 @@ export const networkReducer = reducerWithInitialState(initialNetworkState) }; } }) + .case(updateNetworkAnomaliesInterval, (state, { interval, networkType }) => { + if (networkType === NetworkType.page) { + return { + ...state, + page: { + ...state.page, + queries: { + ...state.page.queries, + anomalies: { + ...state.page.queries.anomalies, + intervalSelection: interval, + }, + }, + }, + }; + } else { + return { + ...state, + details: { + ...state.details, + queries: { + ...state.details.queries, + anomalies: { + ...state.details.queries.anomalies, + intervalSelection: interval, + }, + }, + }, + }; + } + }) .build(); diff --git a/x-pack/plugins/security_solution/public/network/store/selectors.ts b/x-pack/plugins/security_solution/public/network/store/selectors.ts index 9ec59c94f3871..56b3fd8335618 100644 --- a/x-pack/plugins/security_solution/public/network/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/network/store/selectors.ts @@ -98,3 +98,9 @@ export const networkAnomaliesJobIdFilterSelector = () => selectNetwork, (network) => network.queries[NetworkTableType.anomalies].jobIdSelection ); + +export const networkAnomaliesIntervalSelector = () => + createSelector( + selectNetwork, + (network) => network.queries[NetworkTableType.anomalies].intervalSelection + ); diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/index.test.tsx index 60f921d1a753d..642e4ae207362 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/index.test.tsx @@ -9,7 +9,10 @@ import { render } from '@testing-library/react'; import React from 'react'; import { EntityAnalyticsAnomalies } from '.'; import type { AnomaliesCount } from '../../../../common/components/ml/anomaly/use_anomalies_search'; -import { AnomalyJobStatus } from '../../../../common/components/ml/anomaly/use_anomalies_search'; +import { + AnomalyJobStatus, + AnomalyEntity, +} from '../../../../common/components/ml/anomaly/use_anomalies_search'; import { TestProviders } from '../../../../common/mock'; @@ -67,6 +70,7 @@ describe('EntityAnalyticsAnomalies', () => { name: 'v3_windows_anomalous_script', count: 9999, status: AnomalyJobStatus.enabled, + entity: AnomalyEntity.User, }; mockUseNotableAnomaliesSearch.mockReturnValue({ @@ -93,6 +97,7 @@ describe('EntityAnalyticsAnomalies', () => { name: 'v3_windows_anomalous_script', count: 0, status: AnomalyJobStatus.disabled, + entity: AnomalyEntity.User, }; mockUseNotableAnomaliesSearch.mockReturnValue({ @@ -118,6 +123,7 @@ describe('EntityAnalyticsAnomalies', () => { name: 'v3_windows_anomalous_script', count: 0, status: AnomalyJobStatus.uninstalled, + entity: AnomalyEntity.User, }; mockUseNotableAnomaliesSearch.mockReturnValue({ @@ -142,6 +148,7 @@ describe('EntityAnalyticsAnomalies', () => { name: 'v3_windows_anomalous_script', count: 0, status: AnomalyJobStatus.failed, + entity: AnomalyEntity.User, }; mockUseNotableAnomaliesSearch.mockReturnValue({ @@ -166,6 +173,7 @@ describe('EntityAnalyticsAnomalies', () => { name: 'v3_windows_anomalous_script', count: 0, status: AnomalyJobStatus.failed, + entity: AnomalyEntity.User, }; mockUseNotableAnomaliesSearch.mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx index a794666d13aaa..05421e9891e51 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx @@ -116,13 +116,14 @@ export const ExpandableNetworkDetails = ({ }); useInvalidFilterQuery({ id, filterQuery, kqlError, query, startDate: from, endDate: to }); - const jobIds = useInstalledSecurityJobsIds(); + const { jobIds } = useInstalledSecurityJobsIds(); const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({ criteriaFields: networkToCriteria(ip, flowTarget), startDate: from, endDate: to, skip: isInitializing, jobIds, + aggregationInterval: 'auto', }); return indicesExist ? ( 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 7a9a224c49e08..1e5c1e813eea3 100644 --- a/x-pack/plugins/security_solution/public/users/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/users/store/reducer.ts @@ -142,8 +142,8 @@ export const usersReducer = reducerWithInitialState(initialUsersState) ...state.page, queries: { ...state.page.queries, - [UsersTableType.anomalies]: { - ...state[usersType].queries[UsersTableType.anomalies], + anomalies: { + ...state.page.queries.anomalies, jobIdSelection: jobIds, }, }, @@ -156,8 +156,8 @@ export const usersReducer = reducerWithInitialState(initialUsersState) ...state.details, queries: { ...state.details.queries, - [UsersTableType.anomalies]: { - ...state[usersType].queries[UsersTableType.anomalies], + anomalies: { + ...state.details.queries.anomalies, jobIdSelection: jobIds, }, }, @@ -173,8 +173,8 @@ export const usersReducer = reducerWithInitialState(initialUsersState) ...state.page, queries: { ...state.page.queries, - [UsersTableType.anomalies]: { - ...state[usersType].queries[UsersTableType.anomalies], + anomalies: { + ...state.page.queries.anomalies, intervalSelection: interval, }, }, @@ -187,8 +187,8 @@ export const usersReducer = reducerWithInitialState(initialUsersState) ...state.details, queries: { ...state.details.queries, - [UsersTableType.anomalies]: { - ...state[usersType].queries[UsersTableType.anomalies], + anomalies: { + ...state.details.queries.anomalies, intervalSelection: interval, }, }, From 23788c7902e3736067849ec1c37934775da9ace5 Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Mon, 5 Sep 2022 10:20:27 +0200 Subject: [PATCH 09/11] Fix borken unit tests --- .../ml/anomaly/use_anomalies_search.test.ts | 6 +++++- .../components/ml/anomaly/use_anomalies_search.ts | 1 - .../components/ml/tables/job_id_filter.stories.tsx | 3 +++ .../common/components/ml/tables/job_id_filter.tsx | 1 - .../public/hosts/store/helpers.test.ts | 11 ++++++++--- .../public/network/store/helpers.test.ts | 8 ++++++++ .../components/entity_analytics/anomalies/columns.tsx | 2 +- 7 files changed, 25 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.test.ts index c53bdbc2bc4bc..49be1077da393 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.test.ts @@ -10,7 +10,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { TestProviders } from '../../../mock'; import type { Refetch } from '../../../store/inputs/model'; import type { AnomaliesCount } from './use_anomalies_search'; -import { useNotableAnomaliesSearch, AnomalyJobStatus } from './use_anomalies_search'; +import { useNotableAnomaliesSearch, AnomalyJobStatus, AnomalyEntity } from './use_anomalies_search'; const jobId = 'auth_rare_source_ip_for_a_user'; const from = 'now-24h'; @@ -122,6 +122,7 @@ describe('useNotableAnomaliesSearch', () => { jobId, name: jobId, status: AnomalyJobStatus.enabled, + entity: AnomalyEntity.Host, }, ]) ); @@ -155,6 +156,7 @@ describe('useNotableAnomaliesSearch', () => { jobId: undefined, name: jobId, status: AnomalyJobStatus.uninstalled, + entity: AnomalyEntity.Host, }, ]) ); @@ -197,6 +199,7 @@ describe('useNotableAnomaliesSearch', () => { jobId: customJobId, name: jobId, status: AnomalyJobStatus.enabled, + entity: AnomalyEntity.Host, }, ]) ); @@ -254,6 +257,7 @@ describe('useNotableAnomaliesSearch', () => { jobId: mostRecentJobId, name: jobId, status: AnomalyJobStatus.enabled, + entity: AnomalyEntity.Host, }, ]) ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts index 931f4d9707c2e..2af5a4ddabd7b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts @@ -154,7 +154,6 @@ function formatResultData( buckets: Array<{ key: string; doc_count: number; - // entity: EntityHits; }>, notableAnomaliesJobs: MlSummaryJob[] ): AnomaliesCount[] { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.stories.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.stories.tsx index 88619573cb179..5e0e886ec138a 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.stories.tsx @@ -19,6 +19,9 @@ const withTheme = (storyFn: () => ReactNode) => ( storiesOf('JobIdFilter', module) .addDecorator(withTheme) + .add('empty', () => ( + + )) .add('one selected item', () => ( { limit: 10, sortField: 'lastSeen', }, - [HostsTableType.anomalies]: null, + [HostsTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, [HostsTableType.authentications]: { activePage: 0, limit: 10, @@ -140,7 +142,10 @@ describe('Hosts redux store', () => { limit: 10, sortField: 'lastSeen', }, - [HostsTableType.anomalies]: null, + [HostsTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, [HostsTableType.authentications]: { activePage: 0, limit: 10, diff --git a/x-pack/plugins/security_solution/public/network/store/helpers.test.ts b/x-pack/plugins/security_solution/public/network/store/helpers.test.ts index 6b24a44b26b38..14d2ca1c05f57 100644 --- a/x-pack/plugins/security_solution/public/network/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/network/store/helpers.test.ts @@ -163,6 +163,10 @@ describe('Network redux store', () => { limit: 10, sort: { field: 'bytes_out', direction: 'desc' }, }, + [NetworkDetailsTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, [NetworkTableType.dns]: { activePage: 0, limit: 10, @@ -235,6 +239,10 @@ describe('Network redux store', () => { field: 'bytes_out', }, }, + [NetworkDetailsTableType.anomalies]: { + jobIdSelection: [], + intervalSelection: 'auto', + }, [NetworkDetailsTableType.http]: { activePage: 0, limit: 10, diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx index 088875d541efe..8f37f8af1ac3f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/columns.tsx @@ -63,7 +63,7 @@ export const useAnomaliesColumns = (loading: boolean): AnomaliesColumns => { mobileOptions: { show: true }, width: '15%', 'data-test-subj': 'anomalies-table-column-count', - render: (count, { status, jobId, name, entity }) => { + render: (count, { status, jobId, entity }) => { if (loading) return ''; if (count > 0 || status === AnomalyJobStatus.enabled) { From 7c9c0d3572ea2a40e9b852c71df2d4ff17f6ed41 Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Mon, 5 Sep 2022 11:44:23 +0200 Subject: [PATCH 10/11] Add unit tests --- .../ml/tables/job_id_filter.test.tsx | 46 ++++++++++++++++++- .../components/ml/tables/job_id_filter.tsx | 4 +- .../ml/tables/select_interval.test.tsx | 23 +++++++++- .../components/ml/tables/select_interval.tsx | 2 + 4 files changed, 70 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.test.tsx index 53c3add276bed..8a345f9a0f3ae 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.test.tsx @@ -5,4 +5,48 @@ * 2.0. */ -// TODO +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { JobIdFilter } from './job_id_filter'; + +describe('JobIdFilter', () => { + it('is disabled when job id is empty', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('job-id-filter-button')).toBeDisabled(); + }); + + it('calls onSelect when clicked', () => { + const onSelectCb = jest.fn(); + const { getByText, getByTestId } = render( + + ); + fireEvent.click(getByTestId('job-id-filter-button')); + fireEvent.click(getByText('test_job_2')); + + expect(onSelectCb).toBeCalledWith(['test_job_2']); + }); + + it('displays job id as selected when it is present in selectedJobIds', () => { + const { getByTestId } = render( + + ); + + fireEvent.click(getByTestId('job-id-filter-button')); + + expect( + getByTestId('job-id-filter-item-test_job_2').querySelector('span[data-euiicon-type=check]') + ).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx index 95f0136298c9d..379c9b3e0f2af 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/job_id_filter.tsx @@ -39,7 +39,7 @@ export const JobIdFilter: React.FC<{ () => ( 0} iconType="arrowDown" isSelected={isPopoverOpen} @@ -64,7 +64,7 @@ export const JobIdFilter: React.FC<{
{jobIds.map((id) => ( updateSelection(id)} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.test.tsx index 6e245e971de2d..7cba71adae9fc 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.test.tsx @@ -5,5 +5,24 @@ * 2.0. */ -// TODO -// it renders +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { SelectInterval } from './select_interval'; + +describe('SelectInterval', () => { + it('selects the given interval', () => { + const { getByText } = render(); + expect((getByText('1 day') as HTMLOptionElement).selected).toBeTruthy(); + }); + + it('calls onChange when clicked', () => { + const onChangeCb = jest.fn(); + const { getByText, getByTestId } = render( + + ); + + userEvent.selectOptions(getByTestId('selectInterval'), getByText('1 hour')); + expect(onChangeCb).toBeCalledWith('hour'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.tsx index 603bfa1172eb1..717c0d0a48409 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/select_interval.tsx @@ -37,8 +37,10 @@ export const SelectInterval: React.FC<{ }, [onChange] ); + return ( From 240fe738b43602e33a6de7008206d57b2be4165b Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Tue, 6 Sep 2022 11:09:39 +0200 Subject: [PATCH 11/11] Please code review --- .../ml/anomaly/use_anomalies_search.ts | 4 +- .../public/network/store/reducer.ts | 58 ++----------------- .../public/users/store/reducer.ts | 57 ++---------------- 3 files changed, 12 insertions(+), 107 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts index 2af5a4ddabd7b..1a95f56465ba8 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_search.ts @@ -6,7 +6,7 @@ */ import { useState, useEffect, useMemo, useRef } from 'react'; -import { filter, head, noop, orderBy, pipe, get } from 'lodash/fp'; +import { filter, head, noop, orderBy, pipe, has } from 'lodash/fp'; import type { MlSummaryJob } from '@kbn/ml-plugin/common'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; @@ -160,7 +160,7 @@ function formatResultData( return NOTABLE_ANOMALIES_IDS.map((notableJobId) => { const job = findJobWithId(notableJobId)(notableAnomaliesJobs); const bucket = buckets.find(({ key }) => key === job?.id); - const hasUserName = !!get("entity.hits.hits[0]._source['user.name']", bucket); + const hasUserName = has("entity.hits.hits[0]._source['user.name']", bucket); return { name: notableJobId, diff --git a/x-pack/plugins/security_solution/public/network/store/reducer.ts b/x-pack/plugins/security_solution/public/network/store/reducer.ts index 9ef2325601d59..4b4d2e847ee1c 100644 --- a/x-pack/plugins/security_solution/public/network/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/network/store/reducer.ts @@ -6,7 +6,7 @@ */ import { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { get } from 'lodash/fp'; +import { get, set } from 'lodash/fp'; import { Direction, FlowTarget, @@ -202,64 +202,16 @@ export const networkReducer = reducerWithInitialState(initialNetworkState) })) .case(updateNetworkAnomaliesJobIdFilter, (state, { jobIds, networkType }) => { if (networkType === NetworkType.page) { - return { - ...state, - page: { - ...state.page, - queries: { - ...state.page.queries, - anomalies: { - ...state.page.queries.anomalies, - jobIdSelection: jobIds, - }, - }, - }, - }; + return set('page.queries.anomalies.jobIdSelection', jobIds, state); } else { - return { - ...state, - details: { - ...state.details, - queries: { - ...state.details.queries, - anomalies: { - ...state.details.queries.anomalies, - jobIdSelection: jobIds, - }, - }, - }, - }; + return set('details.queries.anomalies.jobIdSelection', jobIds, state); } }) .case(updateNetworkAnomaliesInterval, (state, { interval, networkType }) => { if (networkType === NetworkType.page) { - return { - ...state, - page: { - ...state.page, - queries: { - ...state.page.queries, - anomalies: { - ...state.page.queries.anomalies, - intervalSelection: interval, - }, - }, - }, - }; + return set('page.queries.anomalies.intervalSelection', interval, state); } else { - return { - ...state, - details: { - ...state.details, - queries: { - ...state.details.queries, - anomalies: { - ...state.details.queries.anomalies, - intervalSelection: interval, - }, - }, - }, - }; + return set('details.queries.anomalies.intervalSelection', interval, state); } }) .build(); 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 1e5c1e813eea3..0699f3d3c3acc 100644 --- a/x-pack/plugins/security_solution/public/users/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/users/store/reducer.ts @@ -6,6 +6,7 @@ */ import { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { set } from 'lodash/fp'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; import { @@ -136,64 +137,16 @@ export const usersReducer = reducerWithInitialState(initialUsersState) })) .case(updateUsersAnomaliesJobIdFilter, (state, { jobIds, usersType }) => { if (usersType === 'page') { - return { - ...state, - page: { - ...state.page, - queries: { - ...state.page.queries, - anomalies: { - ...state.page.queries.anomalies, - jobIdSelection: jobIds, - }, - }, - }, - }; + return set('page.queries.anomalies.jobIdSelection', jobIds, state); } else { - return { - ...state, - details: { - ...state.details, - queries: { - ...state.details.queries, - anomalies: { - ...state.details.queries.anomalies, - jobIdSelection: jobIds, - }, - }, - }, - }; + return set('details.queries.anomalies.jobIdSelection', jobIds, state); } }) .case(updateUsersAnomaliesInterval, (state, { interval, usersType }) => { if (usersType === 'page') { - return { - ...state, - page: { - ...state.page, - queries: { - ...state.page.queries, - anomalies: { - ...state.page.queries.anomalies, - intervalSelection: interval, - }, - }, - }, - }; + return set('page.queries.anomalies.intervalSelection', interval, state); } else { - return { - ...state, - details: { - ...state.details, - queries: { - ...state.details.queries, - anomalies: { - ...state.details.queries.anomalies, - intervalSelection: interval, - }, - }, - }, - }; + return set('details.queries.anomalies.intervalSelection', interval, state); } }) .build();