Skip to content

Commit

Permalink
Add anomalies tab to user page (elastic#126079)
Browse files Browse the repository at this point in the history
* Add anomalies tab to the users page
  • Loading branch information
machadoum authored Mar 1, 2022
1 parent 25b97bb commit 1bc178f
Show file tree
Hide file tree
Showing 40 changed files with 987 additions and 462 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { useState, useEffect, useMemo } from 'react';

import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants';
import { anomaliesTableData } from '../api/anomalies_table_data';
import { InfluencerInput, Anomalies, CriteriaFields } from '../types';
Expand All @@ -23,6 +23,7 @@ interface Args {
threshold?: number;
skip?: boolean;
criteriaFields?: CriteriaFields[];
filterQuery?: estypes.QueryDslQueryContainer;
}

type Return = [boolean, Anomalies | null];
Expand Down Expand Up @@ -55,6 +56,7 @@ export const useAnomaliesTableData = ({
endDate,
threshold = -1,
skip = false,
filterQuery,
}: Args): Return => {
const [tableData, setTableData] = useState<Anomalies | null>(null);
const { isMlUser, jobs } = useInstalledSecurityJobs();
Expand Down Expand Up @@ -84,6 +86,7 @@ export const useAnomaliesTableData = ({
{
jobIds,
criteriaFields: criteriaFieldsInput,
influencersFilterQuery: filterQuery,
aggregationInterval: 'auto',
threshold: getThreshold(anomalyScore, threshold),
earliestMs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { Anomalies, InfluencerInput, CriteriaFields } from '../types';
import { KibanaServices } from '../../../lib/kibana';

Expand All @@ -19,6 +20,7 @@ export interface Body {
dateFormatTz: string;
maxRecords: number;
maxExamples: number;
influencersFilterQuery?: estypes.QueryDslQueryContainer;
}

export const anomaliesTableData = async (body: Body, signal: AbortSignal): Promise<Anomalies> => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { UsersType } from '../../../../users/store/model';
import { getCriteriaFromUsersType } from './get_criteria_from_users_type';

describe('get_criteria_from_user_type', () => {
test('returns user name from criteria if the user type is details', () => {
const criteria = getCriteriaFromUsersType(UsersType.details, 'admin');
expect(criteria).toEqual([{ fieldName: 'user.name', fieldValue: 'admin' }]);
});

test('returns empty array from criteria if the user type is page but rather an empty array', () => {
const criteria = getCriteriaFromUsersType(UsersType.page, 'admin');
expect(criteria).toEqual([]);
});

test('returns empty array from criteria if the user name is undefined and user type is details', () => {
const criteria = getCriteriaFromUsersType(UsersType.details, undefined);
expect(criteria).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { UsersType } from '../../../../users/store/model';
import { CriteriaFields } from '../types';

export const getCriteriaFromUsersType = (
type: UsersType,
userName: string | undefined
): CriteriaFields[] => {
if (type === UsersType.details && userName != null) {
return [{ fieldName: 'user.name', fieldValue: userName }];
} else {
return [];
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,31 @@
* 2.0.
*/

import { cloneDeep } from 'lodash/fp';
import { getHostNameFromInfluencers } from './get_host_name_from_influencers';
import { mockAnomalies } from '../mock';

describe('get_host_name_from_influencers', () => {
let anomalies = cloneDeep(mockAnomalies);

beforeEach(() => {
anomalies = cloneDeep(mockAnomalies);
});

test('returns host names from influencers from the mock', () => {
const hostName = getHostNameFromInfluencers(anomalies.anomalies[0].influencers);
expect(hostName).toEqual('zeek-iowa');
expect(getHostNameFromInfluencers(mockAnomalies.anomalies[0].influencers)).toEqual('zeek-iowa');
});

test('returns null if there are no influencers from the mock', () => {
anomalies.anomalies[0].influencers = [];
const hostName = getHostNameFromInfluencers(anomalies.anomalies[0].influencers);
expect(hostName).toEqual(null);
expect(getHostNameFromInfluencers([])).toEqual(null);
});

test('returns null if it is given undefined influencers', () => {
const hostName = getHostNameFromInfluencers();
expect(hostName).toEqual(null);
expect(getHostNameFromInfluencers()).toEqual(null);
});

test('returns null if there influencers is an empty object', () => {
anomalies.anomalies[0].influencers = [{}];
const hostName = getHostNameFromInfluencers(anomalies.anomalies[0].influencers);
expect(hostName).toEqual(null);
expect(getHostNameFromInfluencers([{}])).toEqual(null);
});

test('returns host name mixed with other data', () => {
anomalies.anomalies[0].influencers = [{ 'host.name': 'name-1' }, { 'source.ip': '127.0.0.1' }];
const hostName = getHostNameFromInfluencers(anomalies.anomalies[0].influencers);
const hostName = getHostNameFromInfluencers([
{ 'host.name': 'name-1' },
{ 'source.ip': '127.0.0.1' },
]);
expect(hostName).toEqual('name-1');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,27 @@
* 2.0.
*/

import { cloneDeep } from 'lodash/fp';
import { getNetworkFromInfluencers } from './get_network_from_influencers';
import { mockAnomalies } from '../mock';
import { DestinationOrSource } from '../types';

describe('get_network_from_influencers', () => {
let anomalies = cloneDeep(mockAnomalies);

beforeEach(() => {
anomalies = cloneDeep(mockAnomalies);
});

test('returns null if there are no influencers from the mock', () => {
anomalies.anomalies[0].influencers = [];
const network = getNetworkFromInfluencers(anomalies.anomalies[0].influencers);
expect(network).toEqual(null);
test('returns null if there are no influencers', () => {
expect(getNetworkFromInfluencers([])).toEqual(null);
});

test('returns null if the influencers is an empty object', () => {
anomalies.anomalies[0].influencers = [{}];
const network = getNetworkFromInfluencers(anomalies.anomalies[0].influencers);
expect(network).toEqual(null);
expect(getNetworkFromInfluencers([{}])).toEqual(null);
});

test('returns null if the influencers are undefined', () => {
const network = getNetworkFromInfluencers();
expect(network).toEqual(null);
expect(getNetworkFromInfluencers()).toEqual(null);
});

test('returns network name of source mixed with other data', () => {
anomalies.anomalies[0].influencers = [{ 'host.name': 'name-1' }, { 'source.ip': '127.0.0.1' }];
const network = getNetworkFromInfluencers(anomalies.anomalies[0].influencers);
const network = getNetworkFromInfluencers([
{ 'host.name': 'name-1' },
{ 'source.ip': '127.0.0.1' },
]);
const expected: { ip: string; type: DestinationOrSource } = {
ip: '127.0.0.1',
type: 'source.ip',
Expand All @@ -45,11 +34,10 @@ describe('get_network_from_influencers', () => {
});

test('returns network name mixed with other data', () => {
anomalies.anomalies[0].influencers = [
const network = getNetworkFromInfluencers([
{ 'host.name': 'name-1' },
{ 'destination.ip': '127.0.0.1' },
];
const network = getNetworkFromInfluencers(anomalies.anomalies[0].influencers);
]);
const expected: { ip: string; type: DestinationOrSource } = {
ip: '127.0.0.1',
type: 'destination.ip',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { getUserNameFromInfluencers } from './get_user_name_from_influencers';
import { mockAnomalies } from '../mock';

describe('get_user_name_from_influencers', () => {
test('returns user names from influencers from the mock', () => {
expect(getUserNameFromInfluencers(mockAnomalies.anomalies[0].influencers)).toEqual('root');
});

test('returns null if there are no influencers from the mock', () => {
expect(getUserNameFromInfluencers([])).toEqual(null);
});

test('returns null if it is given undefined influencers', () => {
expect(getUserNameFromInfluencers()).toEqual(null);
});

test('returns null if there influencers is an empty object', () => {
expect(getUserNameFromInfluencers([{}])).toEqual(null);
});

test('returns user name mixed with other data', () => {
const userName = getUserNameFromInfluencers([
{ 'user.name': 'root' },
{ 'source.ip': '127.0.0.1' },
]);
expect(userName).toEqual('root');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { getEntries } from '../get_entries';

export const getUserNameFromInfluencers = (
influencers: Array<Record<string, string>> = [],
userName?: string
): string | null => {
const recordFound = influencers.find((influencer) => {
const [influencerName, influencerValue] = getEntries(influencer);
if (influencerName === 'user.name') {
if (userName == null) {
return true;
} else {
return influencerValue === userName;
}
} else {
return false;
}
});
if (recordFound != null) {
return Object.values(recordFound)[0];
} else {
return null;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ import * as i18n from './translations';
import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns';
import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts';
import { Loader } from '../../loader';
import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies';
import { AnomaliesHostTableProps } from '../types';
import { useMlCapabilities } from '../hooks/use_ml_capabilities';
import { BasicTable } from './basic_table';
import { hostEquality } from './host_equality';
import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type';
import { Panel } from '../../panel';
import { anomaliesTableDefaultEquality } from './default_equality';

const sorting = {
sort: {
Expand All @@ -33,7 +32,6 @@ const sorting = {
const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
startDate,
endDate,
narrowDateRange,
hostName,
skip,
type,
Expand All @@ -44,18 +42,14 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
endDate,
skip,
criteriaFields: getCriteriaFromHostType(type, hostName),
filterQuery: {
exists: { field: 'host.name' },
},
});

const hosts = convertAnomaliesToHosts(tableData, hostName);

const interval = getIntervalFromAnomalies(tableData);
const columns = getAnomaliesHostTableColumnsCurated(
type,
startDate,
endDate,
interval,
narrowDateRange
);
const columns = getAnomaliesHostTableColumnsCurated(type, startDate, endDate);
const pagination = {
initialPageIndex: 0,
initialPageSize: 10,
Expand Down Expand Up @@ -94,4 +88,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
}
};

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

0 comments on commit 1bc178f

Please sign in to comment.