mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Update anomalies tab to display the same quantity of anomalies when navigating from entity analytics page (#139910)
* Create Job id filter component * Add job filter to anomalies tab * Add interval selector to anomalies tab * Preselect anomalies table interval from entity analytics page link * Infer anomaly entity from top hits aggregation
This commit is contained in:
parent
6ed79f42db
commit
801600746d
39 changed files with 901 additions and 395 deletions
|
@ -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,15 @@ interface Props {
|
|||
|
||||
export const AnomalyTableProvider = React.memo<Props>(
|
||||
({ influencers, startDate, endDate, children, criteriaFields, skip }) => {
|
||||
const { jobIds } = useInstalledSecurityJobsIds();
|
||||
const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({
|
||||
criteriaFields,
|
||||
influencers,
|
||||
startDate,
|
||||
endDate,
|
||||
skip,
|
||||
jobIds,
|
||||
aggregationInterval: 'auto',
|
||||
});
|
||||
return <>{children({ isLoadingAnomaliesData, anomaliesData })}</>;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
|
|
@ -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, has } 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,6 +149,7 @@ const getMLJobStatus = (
|
|||
? AnomalyJobStatus.disabled
|
||||
: AnomalyJobStatus.uninstalled;
|
||||
};
|
||||
|
||||
function formatResultData(
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
|
@ -152,12 +160,14 @@ function formatResultData(
|
|||
return NOTABLE_ANOMALIES_IDS.map((notableJobId) => {
|
||||
const job = findJobWithId(notableJobId)(notableAnomaliesJobs);
|
||||
const bucket = buckets.find(({ key }) => key === job?.id);
|
||||
const hasUserName = has("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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,8 @@ interface Args {
|
|||
skip?: boolean;
|
||||
criteriaFields?: CriteriaFields[];
|
||||
filterQuery?: estypes.QueryDslQueryContainer;
|
||||
jobIds: string[];
|
||||
aggregationInterval: string;
|
||||
}
|
||||
|
||||
type Return = [boolean, Anomalies | null];
|
||||
|
@ -57,15 +60,18 @@ export const useAnomaliesTableData = ({
|
|||
threshold = -1,
|
||||
skip = false,
|
||||
filterQuery,
|
||||
jobIds,
|
||||
aggregationInterval,
|
||||
}: Args): Return => {
|
||||
const [tableData, setTableData] = useState<Anomalies | null>(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$<number>(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]);
|
||||
|
||||
|
@ -89,7 +95,7 @@ export const useAnomaliesTableData = ({
|
|||
jobIds,
|
||||
criteriaFields: criteriaFieldsInput,
|
||||
influencersFilterQuery: filterQuery,
|
||||
aggregationInterval: 'auto',
|
||||
aggregationInterval,
|
||||
threshold: getThreshold(anomalyScore, threshold),
|
||||
earliestMs,
|
||||
latestMs,
|
||||
|
@ -135,6 +141,7 @@ export const useAnomaliesTableData = ({
|
|||
endDateMs,
|
||||
skip,
|
||||
isMlUser,
|
||||
aggregationInterval,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
jobIds.sort().join(),
|
||||
]);
|
||||
|
|
|
@ -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, loading } = useInstalledSecurityJobs();
|
||||
const jobIds = useMemo(() => jobs.map((job) => job.id), [jobs]);
|
||||
|
||||
return { jobIds, loading };
|
||||
};
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
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';
|
||||
|
@ -20,8 +21,13 @@ 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 = {
|
||||
sort: {
|
||||
|
@ -37,6 +43,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
|||
skip,
|
||||
type,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const capabilities = useMlCapabilities();
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesHostTable`);
|
||||
const [querySkip, setQuerySkip] = useState(skip || !toggleStatus);
|
||||
|
@ -52,7 +59,51 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
|||
[setQuerySkip, setToggleStatus]
|
||||
);
|
||||
|
||||
const [loading, tableData] = useAnomaliesTableData({
|
||||
const { jobIds, loading: loadingJobs } = 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 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,
|
||||
|
@ -60,6 +111,8 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
|||
filterQuery: {
|
||||
exists: { field: 'host.name' },
|
||||
},
|
||||
jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds,
|
||||
aggregationInterval: selectedInterval,
|
||||
});
|
||||
|
||||
const hosts = convertAnomaliesToHosts(tableData, hostName);
|
||||
|
@ -77,7 +130,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
|||
return null;
|
||||
} else {
|
||||
return (
|
||||
<Panel loading={loading}>
|
||||
<Panel loading={loadingTable || loadingJobs}>
|
||||
<HeaderSection
|
||||
subtitle={`${i18n.SHOWING}: ${pagination.totalItemCount.toLocaleString()} ${i18n.UNIT(
|
||||
pagination.totalItemCount
|
||||
|
@ -87,6 +140,21 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
|||
toggleStatus={toggleStatus}
|
||||
tooltip={i18n.TOOLTIP}
|
||||
isInspectDisabled={skip}
|
||||
headerFilters={
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<SelectInterval interval={selectedInterval} onChange={onSelectInterval} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<JobIdFilter
|
||||
title={i18n.JOB_ID}
|
||||
onSelect={onSelectJobId}
|
||||
selectedJobIds={selectedJobIds}
|
||||
jobIds={jobIds}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
/>
|
||||
{toggleStatus && (
|
||||
<BasicTable
|
||||
|
@ -99,7 +167,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
{(loadingTable || loadingJobs) && (
|
||||
<Loader data-test-subj="anomalies-host-table-loading-panel" overlay size="xl" />
|
||||
)}
|
||||
</Panel>
|
||||
|
@ -107,7 +175,4 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
export const AnomaliesHostTable = React.memo(
|
||||
AnomaliesHostTableComponent,
|
||||
anomaliesTableDefaultEquality
|
||||
);
|
||||
export const AnomaliesHostTable = React.memo(AnomaliesHostTableComponent);
|
||||
|
|
|
@ -5,7 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
|
||||
import { HeaderSection } from '../../header_section';
|
||||
|
||||
|
@ -17,10 +19,15 @@ 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';
|
||||
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 { networkActions, networkSelectors } from '../../../../network/store';
|
||||
import { SelectInterval } from './select_interval';
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
|
@ -38,7 +45,7 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
|
|||
flowTarget,
|
||||
}) => {
|
||||
const capabilities = useMlCapabilities();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesNetwork-${flowTarget}`);
|
||||
const [querySkip, setQuerySkip] = useState(skip || !toggleStatus);
|
||||
useEffect(() => {
|
||||
|
@ -53,11 +60,57 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
|
|||
[setQuerySkip, setToggleStatus]
|
||||
);
|
||||
|
||||
const [loading, tableData] = useAnomaliesTableData({
|
||||
const { jobIds, loading: loadingJobs } = 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 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: selectedInterval,
|
||||
});
|
||||
|
||||
const networks = convertAnomaliesToNetwork(tableData, ip);
|
||||
|
@ -74,7 +127,7 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
|
|||
return null;
|
||||
} else {
|
||||
return (
|
||||
<Panel loading={loading}>
|
||||
<Panel loading={loadingTable || loadingJobs}>
|
||||
<HeaderSection
|
||||
subtitle={`${i18n.SHOWING}: ${pagination.totalItemCount.toLocaleString()} ${i18n.UNIT(
|
||||
pagination.totalItemCount
|
||||
|
@ -84,6 +137,21 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
|
|||
toggleQuery={toggleQuery}
|
||||
toggleStatus={toggleStatus}
|
||||
isInspectDisabled={skip}
|
||||
headerFilters={
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<SelectInterval interval={selectedInterval} onChange={onSelectInterval} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<JobIdFilter
|
||||
title={i18n.JOB_ID}
|
||||
onSelect={onSelectJobId}
|
||||
selectedJobIds={selectedJobIds}
|
||||
jobIds={jobIds}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
/>
|
||||
{toggleStatus && (
|
||||
<BasicTable
|
||||
|
@ -96,7 +164,7 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
{(loadingTable || loadingJobs) && (
|
||||
<Loader data-test-subj="anomalies-network-table-loading-panel" overlay size="xl" />
|
||||
)}
|
||||
</Panel>
|
||||
|
@ -104,4 +172,4 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
export const AnomaliesNetworkTable = React.memo(AnomaliesNetworkTableComponent, networkEquality);
|
||||
export const AnomaliesNetworkTable = React.memo(AnomaliesNetworkTableComponent);
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
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';
|
||||
|
||||
|
@ -20,10 +22,15 @@ 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';
|
||||
import { useInstalledSecurityJobsIds } from '../hooks/use_installed_security_jobs';
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
|
@ -39,6 +46,7 @@ const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
|
|||
skip,
|
||||
type,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const capabilities = useMlCapabilities();
|
||||
|
||||
const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesUserTable`);
|
||||
|
@ -55,7 +63,51 @@ const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
|
|||
[setQuerySkip, setToggleStatus]
|
||||
);
|
||||
|
||||
const [loading, tableData] = useAnomaliesTableData({
|
||||
const { jobIds, loading: loadingJobs } = 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 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,
|
||||
|
@ -63,6 +115,8 @@ const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
|
|||
filterQuery: {
|
||||
exists: { field: 'user.name' },
|
||||
},
|
||||
jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds,
|
||||
aggregationInterval: selectedInterval,
|
||||
});
|
||||
|
||||
const users = convertAnomaliesToUsers(tableData, userName);
|
||||
|
@ -80,7 +134,7 @@ const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
|
|||
return null;
|
||||
} else {
|
||||
return (
|
||||
<Panel loading={loading} data-test-subj="user-anomalies-tab">
|
||||
<Panel loading={loadingTable || loadingJobs} data-test-subj="user-anomalies-tab">
|
||||
<HeaderSection
|
||||
subtitle={`${i18n.SHOWING}: ${pagination.totalItemCount.toLocaleString()} ${i18n.UNIT(
|
||||
pagination.totalItemCount
|
||||
|
@ -90,6 +144,22 @@ const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
|
|||
toggleStatus={toggleStatus}
|
||||
tooltip={i18n.TOOLTIP}
|
||||
isInspectDisabled={skip}
|
||||
headerFilters={
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<SelectInterval interval={selectedInterval} onChange={onSelectInterval} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<JobIdFilter
|
||||
title={i18n.JOB_ID}
|
||||
onSelect={onSelectJobId}
|
||||
selectedJobIds={selectedJobIds}
|
||||
jobIds={jobIds}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
/>
|
||||
|
||||
{toggleStatus && (
|
||||
|
@ -103,15 +173,12 @@ const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<Loader data-test-subj="anomalies-host-table-loading-panel" overlay size="xl" />
|
||||
{(loadingTable || loadingJobs) && (
|
||||
<Loader data-test-subj="anomalies-user-table-loading-panel" overlay size="xl" />
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const AnomaliesUserTable = React.memo(
|
||||
AnomaliesUserTableComponent,
|
||||
anomaliesTableDefaultEquality
|
||||
);
|
||||
export const AnomaliesUserTable = React.memo(AnomaliesUserTableComponent);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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) => (
|
||||
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: true })}>{storyFn()}</ThemeProvider>
|
||||
);
|
||||
|
||||
storiesOf('JobIdFilter', module)
|
||||
.addDecorator(withTheme)
|
||||
.add('empty', () => (
|
||||
<JobIdFilter title="Job id" selectedJobIds={[]} jobIds={[]} onSelect={action('onSelect')} />
|
||||
))
|
||||
.add('one selected item', () => (
|
||||
<JobIdFilter
|
||||
title="Job id"
|
||||
selectedJobIds={['test_job_1']}
|
||||
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
|
||||
onSelect={action('onSelect')}
|
||||
/>
|
||||
))
|
||||
.add('multiple selected item', () => (
|
||||
<JobIdFilter
|
||||
title="Job id"
|
||||
selectedJobIds={['test_job_2', 'test_job_3']}
|
||||
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
|
||||
onSelect={action('onSelect')}
|
||||
/>
|
||||
))
|
||||
.add('no selected item', () => (
|
||||
<JobIdFilter
|
||||
title="Job id"
|
||||
selectedJobIds={[]}
|
||||
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
|
||||
onSelect={action('onSelect')}
|
||||
/>
|
||||
));
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { 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(
|
||||
<JobIdFilter title="Job id" selectedJobIds={[]} jobIds={[]} onSelect={jest.fn()} />
|
||||
);
|
||||
expect(getByTestId('job-id-filter-button')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('calls onSelect when clicked', () => {
|
||||
const onSelectCb = jest.fn();
|
||||
const { getByText, getByTestId } = render(
|
||||
<JobIdFilter
|
||||
title="Job id"
|
||||
selectedJobIds={[]}
|
||||
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
|
||||
onSelect={onSelectCb}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<JobIdFilter
|
||||
title="Job id"
|
||||
selectedJobIds={['test_job_2']}
|
||||
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
|
||||
onSelect={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('job-id-filter-button'));
|
||||
|
||||
expect(
|
||||
getByTestId('job-id-filter-item-test_job_2').querySelector('span[data-euiicon-type=check]')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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(
|
||||
() => (
|
||||
<EuiFilterButton
|
||||
disabled={jobIds.length === 0}
|
||||
data-test-subj="job-id-filter-button"
|
||||
hasActiveFilters={selectedJobIds.length > 0}
|
||||
iconType="arrowDown"
|
||||
isSelected={isPopoverOpen}
|
||||
numActiveFilters={selectedJobIds.length}
|
||||
onClick={onButtonClick}
|
||||
contentProps={{ style: { minWidth: 112 } }} // avoid resizing when selecting job id
|
||||
>
|
||||
{title}
|
||||
</EuiFilterButton>
|
||||
),
|
||||
[isPopoverOpen, onButtonClick, title, selectedJobIds.length, jobIds]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFilterGroup>
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<div className="euiFilterSelect__items">
|
||||
{jobIds.map((id) => (
|
||||
<EuiFilterSelectItem
|
||||
data-test-subj={`job-id-filter-item-${id}`}
|
||||
checked={selectedJobIds.includes(id) ? 'on' : undefined}
|
||||
key={id}
|
||||
onClick={() => updateSelection(id)}
|
||||
>
|
||||
{id}
|
||||
</EuiFilterSelectItem>
|
||||
))}
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { 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(<SelectInterval interval={'day'} onChange={jest.fn()} />);
|
||||
expect((getByText('1 day') as HTMLOptionElement).selected).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls onChange when clicked', () => {
|
||||
const onChangeCb = jest.fn();
|
||||
const { getByText, getByTestId } = render(
|
||||
<SelectInterval interval={'day'} onChange={onChangeCb} />
|
||||
);
|
||||
|
||||
userEvent.selectOptions(getByTestId('selectInterval'), getByText('1 hour'));
|
||||
expect(onChangeCb).toBeCalledWith('hour');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 (
|
||||
<EuiSelect
|
||||
data-test-subj="selectInterval"
|
||||
prepend={i18n.INTERVAL}
|
||||
append={
|
||||
<EuiToolTip content={i18n.INTERVAL_TOOLTIP}>
|
||||
<EuiIcon type="questionInCircle" color="subdued" />
|
||||
</EuiToolTip>
|
||||
}
|
||||
options={OPTIONS}
|
||||
value={interval}
|
||||
onChange={onChangeCb}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -65,3 +65,35 @@ 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',
|
||||
});
|
||||
|
||||
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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
},
|
||||
|
|
|
@ -44,3 +44,13 @@ 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');
|
||||
|
||||
export const updateHostsAnomaliesInterval = actionCreator<{
|
||||
interval: string;
|
||||
hostsType: HostsType;
|
||||
}>('UPDATE_HOSTS_ANOMALIES_INTERVAL');
|
||||
|
|
|
@ -32,7 +32,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,
|
||||
|
@ -68,7 +71,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,
|
||||
|
@ -96,7 +102,10 @@ describe('Hosts redux store', () => {
|
|||
limit: 10,
|
||||
sortField: 'lastSeen',
|
||||
},
|
||||
[HostsTableType.anomalies]: null,
|
||||
[HostsTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[HostsTableType.authentications]: {
|
||||
activePage: 0,
|
||||
limit: 10,
|
||||
|
@ -133,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,
|
||||
|
|
|
@ -42,12 +42,17 @@ export interface HostRiskScoreQuery extends BasicQueryPaginated {
|
|||
severitySelection: RiskSeverity[];
|
||||
}
|
||||
|
||||
export interface HostsAnomaliesQuery {
|
||||
jobIdSelection: string[];
|
||||
intervalSelection: 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;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ import {
|
|||
updateHostRiskScoreSort,
|
||||
updateTableActivePage,
|
||||
updateTableLimit,
|
||||
updateHostsAnomaliesJobIdFilter,
|
||||
updateHostsAnomaliesInterval,
|
||||
} from './actions';
|
||||
import {
|
||||
setHostPageQueriesActivePageToZero,
|
||||
|
@ -49,7 +51,10 @@ export const initialHostsState: HostsState = {
|
|||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
},
|
||||
[HostsTableType.anomalies]: null,
|
||||
[HostsTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[HostsTableType.risk]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
|
@ -85,7 +90,10 @@ export const initialHostsState: HostsState = {
|
|||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
},
|
||||
[HostsTableType.anomalies]: null,
|
||||
[HostsTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[HostsTableType.risk]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
|
@ -190,4 +198,30 @@ 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.case(updateHostsAnomaliesInterval, (state, { interval, hostsType }) => ({
|
||||
...state,
|
||||
[hostsType]: {
|
||||
...state[hostsType],
|
||||
queries: {
|
||||
...state[hostsType].queries,
|
||||
[HostsTableType.anomalies]: {
|
||||
...state[hostsType].queries[HostsTableType.anomalies],
|
||||
intervalSelection: interval,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.build();
|
||||
|
|
|
@ -30,3 +30,9 @@ export const hostRiskScoreSeverityFilterSelector = () =>
|
|||
|
||||
export const uncommonProcessesSelector = () =>
|
||||
createSelector(selectHosts, (hosts) => hosts.queries.uncommonProcesses);
|
||||
|
||||
export const hostsAnomaliesJobIdFilterSelector = () =>
|
||||
createSelector(selectHosts, (hosts) => hosts.queries[HostsTableType.anomalies].jobIdSelection);
|
||||
|
||||
export const hostsAnomaliesIntervalSelector = () =>
|
||||
createSelector(selectHosts, (hosts) => hosts.queries[HostsTableType.anomalies].intervalSelection);
|
||||
|
|
|
@ -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,14 @@ const NetworkDetailsComponent: React.FC = () => {
|
|||
ip,
|
||||
});
|
||||
|
||||
const { jobIds } = useInstalledSecurityJobsIds();
|
||||
const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({
|
||||
criteriaFields: networkToCriteria(detailName, flowTarget),
|
||||
startDate: from,
|
||||
endDate: to,
|
||||
skip: isInitializing,
|
||||
jobIds,
|
||||
aggregationInterval: 'auto',
|
||||
});
|
||||
|
||||
const headerDraggableArguments = useMemo(
|
||||
|
|
|
@ -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,13 @@ 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');
|
||||
|
||||
export const updateNetworkAnomaliesInterval = actionCreator<{
|
||||
interval: string;
|
||||
networkType: NetworkType;
|
||||
}>('UPDATE_NETWORK_ANOMALIES_INTERVAL');
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
@ -155,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,
|
||||
|
@ -227,6 +239,10 @@ describe('Network redux store', () => {
|
|||
field: 'bytes_out',
|
||||
},
|
||||
},
|
||||
[NetworkDetailsTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[NetworkDetailsTableType.http]: {
|
||||
activePage: 0,
|
||||
limit: 10,
|
||||
|
|
|
@ -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,11 @@ export interface NetworkUsersQuery extends BasicQueryPaginated {
|
|||
sort: SortField<NetworkUsersFields>;
|
||||
}
|
||||
|
||||
export interface NetworkAnomaliesQuery {
|
||||
jobIdSelection: string[];
|
||||
intervalSelection: string;
|
||||
}
|
||||
|
||||
export interface NetworkDetailsQueries {
|
||||
[NetworkDetailsTableType.http]: HttpQuery;
|
||||
[NetworkDetailsTableType.tls]: TlsQuery;
|
||||
|
@ -123,6 +131,7 @@ export interface NetworkDetailsQueries {
|
|||
[NetworkDetailsTableType.topNFlowDestination]: TopNFlowQuery;
|
||||
[NetworkDetailsTableType.topNFlowSource]: TopNFlowQuery;
|
||||
[NetworkDetailsTableType.users]: NetworkUsersQuery;
|
||||
[NetworkDetailsTableType.anomalies]: NetworkAnomaliesQuery;
|
||||
}
|
||||
|
||||
export interface NetworkDetailsModel {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { reducerWithInitialState } from 'typescript-fsa-reducers';
|
||||
import { get } from 'lodash/fp';
|
||||
import { get, set } from 'lodash/fp';
|
||||
import {
|
||||
Direction,
|
||||
FlowTarget,
|
||||
|
@ -21,13 +21,15 @@ import {
|
|||
setNetworkDetailsTablesActivePageToZero,
|
||||
setNetworkTablesActivePageToZero,
|
||||
updateNetworkTable,
|
||||
updateNetworkAnomaliesJobIdFilter,
|
||||
updateNetworkAnomaliesInterval,
|
||||
} 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 +96,10 @@ export const initialNetworkState: NetworkState = {
|
|||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
},
|
||||
[NetworkTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
},
|
||||
},
|
||||
details: {
|
||||
|
@ -153,6 +159,10 @@ export const initialNetworkState: NetworkState = {
|
|||
direction: Direction.asc,
|
||||
},
|
||||
},
|
||||
[NetworkDetailsTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
},
|
||||
flowTarget: FlowTarget.source,
|
||||
},
|
||||
|
@ -190,4 +200,18 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
|
|||
queries: setNetworkDetailsQueriesActivePageToZero(state),
|
||||
},
|
||||
}))
|
||||
.case(updateNetworkAnomaliesJobIdFilter, (state, { jobIds, networkType }) => {
|
||||
if (networkType === NetworkType.page) {
|
||||
return set('page.queries.anomalies.jobIdSelection', jobIds, state);
|
||||
} else {
|
||||
return set('details.queries.anomalies.jobIdSelection', jobIds, state);
|
||||
}
|
||||
})
|
||||
.case(updateNetworkAnomaliesInterval, (state, { interval, networkType }) => {
|
||||
if (networkType === NetworkType.page) {
|
||||
return set('page.queries.anomalies.intervalSelection', interval, state);
|
||||
} else {
|
||||
return set('details.queries.anomalies.intervalSelection', interval, state);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
|
|
@ -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,15 @@ 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
|
||||
);
|
||||
|
||||
export const networkAnomaliesIntervalSelector = () =>
|
||||
createSelector(
|
||||
selectNetwork,
|
||||
(network) => network.queries[NetworkTableType.anomalies].intervalSelection
|
||||
);
|
||||
|
|
|
@ -8,11 +8,20 @@ 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 {
|
||||
AnomalyJobStatus,
|
||||
AnomalyEntity,
|
||||
} 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 { usersActions } from '../../../../users/store';
|
||||
import { hostsActions } from '../../../../hosts/store';
|
||||
import { HostsType } from '../../../../hosts/store/model';
|
||||
import { UsersType } from '../../../../users/store/model';
|
||||
|
||||
type AnomaliesColumns = Array<EuiBasicTableColumn<AnomaliesCount>>;
|
||||
|
||||
|
@ -54,11 +63,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, entity }) => {
|
||||
if (loading) return '';
|
||||
|
||||
if (count > 0 || status === AnomalyJobStatus.enabled) {
|
||||
return count;
|
||||
return <AnomaliesTabLink count={count} jobId={jobId} entity={entity} />;
|
||||
} else {
|
||||
if (status === AnomalyJobStatus.disabled && jobId) {
|
||||
return <EnableJobLink jobId={jobId} />;
|
||||
|
@ -110,3 +119,60 @@ const EnableJobLink = ({ jobId }: { jobId: string }) => {
|
|||
</LinkAnchor>
|
||||
);
|
||||
};
|
||||
|
||||
const AnomaliesTabLink = ({
|
||||
count,
|
||||
jobId,
|
||||
entity,
|
||||
}: {
|
||||
count: number;
|
||||
jobId?: string;
|
||||
entity: AnomalyEntity;
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const deepLinkId =
|
||||
entity === AnomalyEntity.User
|
||||
? SecurityPageName.usersAnomalies
|
||||
: SecurityPageName.hostsAnomalies;
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (!jobId) return;
|
||||
|
||||
if (entity === AnomalyEntity.User) {
|
||||
dispatch(
|
||||
usersActions.updateUsersAnomaliesJobIdFilter({
|
||||
jobIds: [jobId],
|
||||
usersType: UsersType.page,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
usersActions.updateUsersAnomaliesInterval({
|
||||
interval: 'second',
|
||||
usersType: UsersType.page,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
hostsActions.updateHostsAnomaliesJobIdFilter({
|
||||
jobIds: [jobId],
|
||||
hostsType: HostsType.page,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
hostsActions.updateHostsAnomaliesInterval({
|
||||
interval: 'second',
|
||||
hostsType: HostsType.page,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [jobId, dispatch, entity]);
|
||||
|
||||
return (
|
||||
<SecuritySolutionLinkAnchor onClick={onClick} deepLinkId={deepLinkId}>
|
||||
{count}
|
||||
</SecuritySolutionLinkAnchor>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,34 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
interface AnomalyConfig {
|
||||
name: string;
|
||||
entity: 'User' | 'Host';
|
||||
}
|
||||
|
||||
export const NOTABLE_ANOMALIES_CONFIG = {
|
||||
auth_rare_source_ip_for_a_user: {
|
||||
entity: 'User',
|
||||
},
|
||||
packetbeat_dns_tunneling: {
|
||||
entity: 'Host',
|
||||
},
|
||||
packetbeat_rare_server_domain: {
|
||||
entity: 'Host',
|
||||
},
|
||||
packetbeat_rare_dns_question: {
|
||||
entity: 'Host',
|
||||
},
|
||||
suspicious_login_activity: {
|
||||
entity: 'User',
|
||||
},
|
||||
v3_windows_anomalous_script: {
|
||||
entity: '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<NotableAnomaliesJobId, AnomalyConfig>;
|
||||
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';
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -53,6 +53,16 @@ export const getAggregatedAnomaliesQuery = ({
|
|||
terms: {
|
||||
field: 'job_id',
|
||||
},
|
||||
aggs: {
|
||||
entity: {
|
||||
top_hits: {
|
||||
_source: {
|
||||
includes: ['host.name', 'user.name'],
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,14 @@ 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,
|
||||
aggregationInterval: 'auto',
|
||||
});
|
||||
|
||||
return indicesExist ? (
|
||||
|
|
|
@ -38,3 +38,13 @@ 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');
|
||||
|
||||
export const updateUsersAnomaliesInterval = actionCreator<{
|
||||
interval: string;
|
||||
usersType: usersModel.UsersType;
|
||||
}>('UPDATE_USERS_ANOMALIES_INTERVAL');
|
||||
|
|
|
@ -42,16 +42,21 @@ export interface UsersRiskScoreQuery extends BasicQueryPaginated {
|
|||
severitySelection: RiskSeverity[];
|
||||
}
|
||||
|
||||
export interface UsersAnomaliesQuery {
|
||||
jobIdSelection: string[];
|
||||
intervalSelection: 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
@ -14,6 +15,8 @@ import {
|
|||
updateTableLimit,
|
||||
updateTableSorting,
|
||||
updateUserRiskScoreSeverityFilter,
|
||||
updateUsersAnomaliesInterval,
|
||||
updateUsersAnomaliesJobIdFilter,
|
||||
} from './actions';
|
||||
import { setUsersPageQueriesActivePageToZero } from './helpers';
|
||||
import type { UsersModel } from './model';
|
||||
|
@ -46,7 +49,10 @@ export const initialUsersState: UsersModel = {
|
|||
},
|
||||
severitySelection: [],
|
||||
},
|
||||
[UsersTableType.anomalies]: null,
|
||||
[UsersTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[UsersTableType.events]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
|
@ -55,7 +61,10 @@ export const initialUsersState: UsersModel = {
|
|||
},
|
||||
details: {
|
||||
queries: {
|
||||
[UsersTableType.anomalies]: null,
|
||||
[UsersTableType.anomalies]: {
|
||||
jobIdSelection: [],
|
||||
intervalSelection: 'auto',
|
||||
},
|
||||
[UsersTableType.events]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
|
@ -126,4 +135,18 @@ export const usersReducer = reducerWithInitialState(initialUsersState)
|
|||
},
|
||||
},
|
||||
}))
|
||||
.case(updateUsersAnomaliesJobIdFilter, (state, { jobIds, usersType }) => {
|
||||
if (usersType === 'page') {
|
||||
return set('page.queries.anomalies.jobIdSelection', jobIds, state);
|
||||
} else {
|
||||
return set('details.queries.anomalies.jobIdSelection', jobIds, state);
|
||||
}
|
||||
})
|
||||
.case(updateUsersAnomaliesInterval, (state, { interval, usersType }) => {
|
||||
if (usersType === 'page') {
|
||||
return set('page.queries.anomalies.intervalSelection', interval, state);
|
||||
} else {
|
||||
return set('details.queries.anomalies.intervalSelection', interval, state);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
|
|
@ -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,9 @@ export const usersRiskScoreSeverityFilterSelector = () =>
|
|||
|
||||
export const authenticationsSelector = () =>
|
||||
createSelector(selectUserPage, (users) => users.queries[UsersTableType.authentications]);
|
||||
|
||||
export const usersAnomaliesJobIdFilterSelector = () =>
|
||||
createSelector(selectUsers, (users) => users.queries[UsersTableType.anomalies].jobIdSelection);
|
||||
|
||||
export const usersAnomaliesIntervalSelector = () =>
|
||||
createSelector(selectUsers, (users) => users.queries[UsersTableType.anomalies].intervalSelection);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue