Skip to content

Commit

Permalink
Update anomalies tab to display the same quantity of anomalies when n…
Browse files Browse the repository at this point in the history
…avigating 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
  • Loading branch information
machadoum authored Sep 7, 2022
1 parent 6ed79f4 commit 8016007
Show file tree
Hide file tree
Showing 39 changed files with 901 additions and 395 deletions.
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, 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';
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 = 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,
};
});
}
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

0 comments on commit 8016007

Please sign in to comment.