Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update anomalies tab to display the same quantity of anomalies when navigating from entity analytics page #139910

Merged
merged 12 commits into from
Sep 7, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 })}</>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -122,6 +122,7 @@ describe('useNotableAnomaliesSearch', () => {
jobId,
name: jobId,
status: AnomalyJobStatus.enabled,
entity: AnomalyEntity.Host,
},
])
);
Expand Down Expand Up @@ -155,6 +156,7 @@ describe('useNotableAnomaliesSearch', () => {
jobId: undefined,
name: jobId,
status: AnomalyJobStatus.uninstalled,
entity: AnomalyEntity.Host,
},
])
);
Expand Down Expand Up @@ -197,6 +199,7 @@ describe('useNotableAnomaliesSearch', () => {
jobId: customJobId,
name: jobId,
status: AnomalyJobStatus.enabled,
entity: AnomalyEntity.Host,
},
])
);
Expand Down Expand Up @@ -254,6 +257,7 @@ describe('useNotableAnomaliesSearch', () => {
jobId: mostRecentJobId,
name: jobId,
status: AnomalyJobStatus.enabled,
entity: AnomalyEntity.Host,
},
])
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -142,6 +149,7 @@ const getMLJobStatus = (
? AnomalyJobStatus.disabled
: AnomalyJobStatus.uninstalled;
};

function formatResultData(
buckets: Array<{
key: string;
Expand All @@ -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 = !!get("entity.hits.hits[0]._source['user.name']", bucket);
machadoum marked this conversation as resolved.
Show resolved Hide resolved

return {
name: notableJobId,
jobId: job?.id,
count: bucket?.doc_count ?? 0,
status: getMLJobStatus(notableJobId, job, notableAnomaliesJobs),
entity: hasUserName ? AnomalyEntity.User : AnomalyEntity.Host,
};
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -24,6 +25,8 @@ interface Args {
skip?: boolean;
criteriaFields?: CriteriaFields[];
filterQuery?: estypes.QueryDslQueryContainer;
jobIds: string[];
aggregationInterval: string;
}

type Return = [boolean, Anomalies | null];
Expand Down Expand Up @@ -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]);

Expand All @@ -89,7 +95,7 @@ export const useAnomaliesTableData = ({
jobIds,
criteriaFields: criteriaFieldsInput,
influencersFilterQuery: filterQuery,
aggregationInterval: 'auto',
aggregationInterval,
threshold: getThreshold(anomalyScore, threshold),
earliestMs,
latestMs,
Expand Down Expand Up @@ -135,6 +141,7 @@ export const useAnomaliesTableData = ({
endDateMs,
skip,
isMlUser,
aggregationInterval,
// eslint-disable-next-line react-hooks/exhaustive-deps
jobIds.sort().join(),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 };
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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: {
Expand All @@ -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);
Expand All @@ -52,14 +59,60 @@ 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,
criteriaFields: getCriteriaFromHostType(type, hostName),
filterQuery: {
exists: { field: 'host.name' },
},
jobIds: selectedJobIds.length > 0 ? selectedJobIds : jobIds,
aggregationInterval: selectedInterval,
});

const hosts = convertAnomaliesToHosts(tableData, hostName);
Expand All @@ -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
Expand All @@ -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
Expand All @@ -99,15 +167,12 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
/>
)}

{loading && (
{(loadingTable || loadingJobs) && (
<Loader data-test-subj="anomalies-host-table-loading-panel" overlay size="xl" />
)}
</Panel>
);
}
};

export const AnomaliesHostTable = React.memo(
AnomaliesHostTableComponent,
anomaliesTableDefaultEquality
);
export const AnomaliesHostTable = React.memo(AnomaliesHostTableComponent);
Loading