mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Add anomalies tab to user page (#126079)
* Add anomalies tab to the users page
This commit is contained in:
parent
25b97bbac1
commit
1bc178fe76
40 changed files with 987 additions and 462 deletions
|
@ -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';
|
||||
|
@ -23,6 +23,7 @@ interface Args {
|
|||
threshold?: number;
|
||||
skip?: boolean;
|
||||
criteriaFields?: CriteriaFields[];
|
||||
filterQuery?: estypes.QueryDslQueryContainer;
|
||||
}
|
||||
|
||||
type Return = [boolean, Anomalies | null];
|
||||
|
@ -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();
|
||||
|
@ -84,6 +86,7 @@ export const useAnomaliesTableData = ({
|
|||
{
|
||||
jobIds,
|
||||
criteriaFields: criteriaFieldsInput,
|
||||
influencersFilterQuery: filterQuery,
|
||||
aggregationInterval: 'auto',
|
||||
threshold: getThreshold(anomalyScore, threshold),
|
||||
earliestMs,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -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> => {
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
|
@ -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 [];
|
||||
}
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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: {
|
||||
|
@ -33,7 +32,6 @@ const sorting = {
|
|||
const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
||||
startDate,
|
||||
endDate,
|
||||
narrowDateRange,
|
||||
hostName,
|
||||
skip,
|
||||
type,
|
||||
|
@ -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,
|
||||
|
@ -94,4 +88,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
export const AnomaliesHostTable = React.memo(AnomaliesHostTableComponent, hostEquality);
|
||||
export const AnomaliesHostTable = React.memo(
|
||||
AnomaliesHostTableComponent,
|
||||
anomaliesTableDefaultEquality
|
||||
);
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
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 { Loader } from '../../loader';
|
||||
import { AnomaliesUserTableProps } from '../types';
|
||||
import { useMlCapabilities } from '../hooks/use_ml_capabilities';
|
||||
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';
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'anomaly.severity',
|
||||
direction: 'desc',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
|
||||
startDate,
|
||||
endDate,
|
||||
userName,
|
||||
skip,
|
||||
type,
|
||||
}) => {
|
||||
const capabilities = useMlCapabilities();
|
||||
|
||||
const [loading, tableData] = useAnomaliesTableData({
|
||||
startDate,
|
||||
endDate,
|
||||
skip,
|
||||
criteriaFields: getCriteriaFromUsersType(type, userName),
|
||||
filterQuery: {
|
||||
exists: { field: 'user.name' },
|
||||
},
|
||||
});
|
||||
|
||||
const users = convertAnomaliesToUsers(tableData, userName);
|
||||
|
||||
const columns = getAnomaliesUserTableColumnsCurated(type, startDate, endDate);
|
||||
const pagination = {
|
||||
initialPageIndex: 0,
|
||||
initialPageSize: 10,
|
||||
totalItemCount: users.length,
|
||||
pageSizeOptions: [5, 10, 20, 50],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
|
||||
if (!hasMlUserPermissions(capabilities)) {
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
<Panel loading={loading}>
|
||||
<HeaderSection
|
||||
subtitle={`${i18n.SHOWING}: ${pagination.totalItemCount.toLocaleString()} ${i18n.UNIT(
|
||||
pagination.totalItemCount
|
||||
)}`}
|
||||
title={i18n.ANOMALIES}
|
||||
tooltip={i18n.TOOLTIP}
|
||||
isInspectDisabled={skip}
|
||||
/>
|
||||
|
||||
<BasicTable
|
||||
// @ts-expect-error the Columns<T, U> type is not as specific as EUI's...
|
||||
columns={columns}
|
||||
items={users}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
/>
|
||||
|
||||
{loading && (
|
||||
<Loader data-test-subj="anomalies-host-table-loading-panel" overlay size="xl" />
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const AnomaliesUserTable = React.memo(
|
||||
AnomaliesUserTableComponent,
|
||||
anomaliesTableDefaultEquality
|
||||
);
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 { mockAnomalies } from '../mock';
|
||||
import { convertAnomaliesToUsers, getUserNameFromEntity } from './convert_anomalies_to_users';
|
||||
import { AnomaliesByUser } from '../types';
|
||||
|
||||
describe('convert_anomalies_to_users', () => {
|
||||
test('it returns expected anomalies from a user', () => {
|
||||
const entities = convertAnomaliesToUsers(mockAnomalies);
|
||||
|
||||
const expected: AnomaliesByUser[] = [
|
||||
{
|
||||
anomaly: mockAnomalies.anomalies[0],
|
||||
userName: 'root',
|
||||
},
|
||||
{
|
||||
anomaly: mockAnomalies.anomalies[1],
|
||||
userName: 'root',
|
||||
},
|
||||
];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns empty anomalies if sent in a null', () => {
|
||||
const entities = convertAnomaliesToUsers(null);
|
||||
const expected: AnomaliesByUser[] = [];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns a specific anomaly if sent in the user name of an anomaly', () => {
|
||||
const anomalies = {
|
||||
...mockAnomalies,
|
||||
anomalies: [
|
||||
{
|
||||
...mockAnomalies.anomalies[0],
|
||||
entityName: 'something-else',
|
||||
entityValue: 'something-else',
|
||||
influencers: [
|
||||
{ 'host.name': 'zeek-iowa' },
|
||||
{ 'process.name': 'du' },
|
||||
{ 'user.name': 'something-else' },
|
||||
],
|
||||
},
|
||||
mockAnomalies.anomalies[1],
|
||||
],
|
||||
};
|
||||
|
||||
const entities = convertAnomaliesToUsers(anomalies, 'root');
|
||||
const expected: AnomaliesByUser[] = [
|
||||
{
|
||||
anomaly: anomalies.anomalies[1],
|
||||
userName: 'root',
|
||||
},
|
||||
];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns a specific anomaly if an influencer has the user name', () => {
|
||||
const anomalies = {
|
||||
...mockAnomalies,
|
||||
anomalies: [
|
||||
{
|
||||
...mockAnomalies.anomalies[0],
|
||||
entityName: 'something-else',
|
||||
entityValue: 'something-else',
|
||||
influencers: [
|
||||
{ 'host.name': 'zeek-iowa' },
|
||||
{ 'process.name': 'du' },
|
||||
{ 'user.name': 'something-else' },
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockAnomalies.anomalies[1],
|
||||
entityName: 'something-else',
|
||||
entityValue: 'something-else',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const entities = convertAnomaliesToUsers(anomalies, 'root');
|
||||
const expected: AnomaliesByUser[] = [
|
||||
{
|
||||
anomaly: anomalies.anomalies[1],
|
||||
userName: 'root',
|
||||
},
|
||||
];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns empty anomalies if sent in the name of one that does not exist', () => {
|
||||
const entities = convertAnomaliesToUsers(mockAnomalies, 'some-made-up-name-here-for-you');
|
||||
const expected: AnomaliesByUser[] = [];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns true for a found entity name passed in', () => {
|
||||
const anomalies = {
|
||||
...mockAnomalies,
|
||||
anomalies: [
|
||||
{
|
||||
...mockAnomalies.anomalies[0],
|
||||
entityName: 'user.name',
|
||||
entityValue: 'admin',
|
||||
},
|
||||
mockAnomalies.anomalies[1],
|
||||
],
|
||||
};
|
||||
|
||||
const found = getUserNameFromEntity(anomalies.anomalies[0], 'admin');
|
||||
expect(found).toEqual(true);
|
||||
});
|
||||
|
||||
test('it returns false for an entity name that does not exist', () => {
|
||||
const anomalies = {
|
||||
...mockAnomalies,
|
||||
anomalies: [
|
||||
{
|
||||
...mockAnomalies.anomalies[0],
|
||||
entityName: 'user.name',
|
||||
entityValue: 'admin',
|
||||
},
|
||||
mockAnomalies.anomalies[1],
|
||||
],
|
||||
};
|
||||
|
||||
const found = getUserNameFromEntity(anomalies.anomalies[0], 'some-made-up-entity-name');
|
||||
expect(found).toEqual(false);
|
||||
});
|
||||
|
||||
test('it returns true for an entity that has user.name within it if no name is passed in', () => {
|
||||
const anomalies = {
|
||||
...mockAnomalies,
|
||||
anomalies: [
|
||||
{
|
||||
...mockAnomalies.anomalies[0],
|
||||
entityName: 'user.name',
|
||||
entityValue: 'something-made-up',
|
||||
},
|
||||
mockAnomalies.anomalies[1],
|
||||
],
|
||||
};
|
||||
|
||||
const found = getUserNameFromEntity(anomalies.anomalies[0]);
|
||||
expect(found).toEqual(true);
|
||||
});
|
||||
|
||||
test('it returns false for an entity that is not user.name and no name is passed in', () => {
|
||||
const anomalies = {
|
||||
...mockAnomalies,
|
||||
anomalies: [
|
||||
{
|
||||
...mockAnomalies.anomalies[0],
|
||||
entityValue: 'made-up',
|
||||
},
|
||||
mockAnomalies.anomalies[1],
|
||||
],
|
||||
};
|
||||
|
||||
const found = getUserNameFromEntity(anomalies.anomalies[0]);
|
||||
expect(found).toEqual(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { Anomalies, AnomaliesByUser, Anomaly } from '../types';
|
||||
import { getUserNameFromInfluencers } from '../influencers/get_user_name_from_influencers';
|
||||
|
||||
export const convertAnomaliesToUsers = (
|
||||
anomalies: Anomalies | null,
|
||||
userName?: string
|
||||
): AnomaliesByUser[] => {
|
||||
if (anomalies == null) {
|
||||
return [];
|
||||
} else {
|
||||
return anomalies.anomalies.reduce<AnomaliesByUser[]>((accum, item) => {
|
||||
if (getUserNameFromEntity(item, userName)) {
|
||||
return [...accum, { userName: item.entityValue, anomaly: item }];
|
||||
} else {
|
||||
const userNameFromInfluencers = getUserNameFromInfluencers(item.influencers, userName);
|
||||
if (userNameFromInfluencers != null) {
|
||||
return [...accum, { userName: userNameFromInfluencers, anomaly: item }];
|
||||
} else {
|
||||
return accum;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserNameFromEntity = (anomaly: Anomaly, userName?: string): boolean => {
|
||||
if (anomaly.entityName !== 'user.name') {
|
||||
return false;
|
||||
} else if (userName == null) {
|
||||
return true;
|
||||
} else {
|
||||
return anomaly.entityValue === userName;
|
||||
}
|
||||
};
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { createCompoundHostKey, createCompoundNetworkKey } from './create_compound_key';
|
||||
import { AnomaliesByHost, AnomaliesByNetwork } from '../types';
|
||||
import { createCompoundAnomalyKey } from './create_compound_key';
|
||||
|
||||
describe('create_explorer_link', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
@ -17,22 +16,8 @@ describe('create_explorer_link', () => {
|
|||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('it creates a compound host key', () => {
|
||||
const anomaliesByHost: AnomaliesByHost = {
|
||||
hostName: 'some-host-name',
|
||||
anomaly: anomalies.anomalies[0],
|
||||
};
|
||||
const key = createCompoundHostKey(anomaliesByHost);
|
||||
expect(key).toEqual('some-host-name-process.name-du-16.193669439507826-job-1');
|
||||
});
|
||||
|
||||
test('it creates a compound network key', () => {
|
||||
const anomaliesByNetwork: AnomaliesByNetwork = {
|
||||
type: 'destination.ip',
|
||||
ip: '127.0.0.1',
|
||||
anomaly: anomalies.anomalies[0],
|
||||
};
|
||||
const key = createCompoundNetworkKey(anomaliesByNetwork);
|
||||
expect(key).toEqual('127.0.0.1-process.name-du-16.193669439507826-job-1');
|
||||
test('it creates a compound anomaly key', () => {
|
||||
const key = createCompoundAnomalyKey(anomalies.anomalies[0]);
|
||||
expect(key).toEqual('process.name-du-16.193669439507826-job-1');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AnomaliesByHost, AnomaliesByNetwork } from '../types';
|
||||
import { Anomaly } from '../types';
|
||||
|
||||
export const createCompoundHostKey = (anomaliesByHost: AnomaliesByHost): string =>
|
||||
`${anomaliesByHost.hostName}-${anomaliesByHost.anomaly.entityName}-${anomaliesByHost.anomaly.entityValue}-${anomaliesByHost.anomaly.severity}-${anomaliesByHost.anomaly.jobId}`;
|
||||
|
||||
export const createCompoundNetworkKey = (anomaliesByNetwork: AnomaliesByNetwork): string =>
|
||||
`${anomaliesByNetwork.ip}-${anomaliesByNetwork.anomaly.entityName}-${anomaliesByNetwork.anomaly.entityValue}-${anomaliesByNetwork.anomaly.severity}-${anomaliesByNetwork.anomaly.jobId}`;
|
||||
export const createCompoundAnomalyKey = (anomaly: Anomaly): string =>
|
||||
`${anomaly.entityName}-${anomaly.entityValue}-${anomaly.severity}-${anomaly.jobId}`;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { hostEquality } from './host_equality';
|
||||
import { anomaliesTableDefaultEquality } from './default_equality';
|
||||
import { AnomaliesHostTableProps } from '../types';
|
||||
import { HostsType } from '../../../../hosts/store/model';
|
||||
|
||||
|
@ -25,7 +25,7 @@ describe('host_equality', () => {
|
|||
skip: false,
|
||||
type: HostsType.details,
|
||||
};
|
||||
const equal = hostEquality(prev, next);
|
||||
const equal = anomaliesTableDefaultEquality(prev, next);
|
||||
expect(equal).toEqual(true);
|
||||
});
|
||||
|
||||
|
@ -44,7 +44,7 @@ describe('host_equality', () => {
|
|||
skip: false,
|
||||
type: HostsType.details,
|
||||
};
|
||||
const equal = hostEquality(prev, next);
|
||||
const equal = anomaliesTableDefaultEquality(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
|
||||
|
@ -63,7 +63,7 @@ describe('host_equality', () => {
|
|||
skip: false,
|
||||
type: HostsType.details,
|
||||
};
|
||||
const equal = hostEquality(prev, next);
|
||||
const equal = anomaliesTableDefaultEquality(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
|
||||
|
@ -82,7 +82,7 @@ describe('host_equality', () => {
|
|||
skip: false,
|
||||
type: HostsType.details,
|
||||
};
|
||||
const equal = hostEquality(prev, next);
|
||||
const equal = anomaliesTableDefaultEquality(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
|
||||
|
@ -101,7 +101,7 @@ describe('host_equality', () => {
|
|||
skip: false,
|
||||
type: HostsType.details,
|
||||
};
|
||||
const equal = hostEquality(prev, next);
|
||||
const equal = anomaliesTableDefaultEquality(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
|
||||
|
@ -120,7 +120,7 @@ describe('host_equality', () => {
|
|||
skip: false,
|
||||
type: HostsType.details,
|
||||
};
|
||||
const equal = hostEquality(prev, next);
|
||||
const equal = anomaliesTableDefaultEquality(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
});
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AnomaliesHostTableProps } from '../types';
|
||||
import { AnomaliesTableCommonProps } from '../types';
|
||||
|
||||
export const hostEquality = (
|
||||
prevProps: AnomaliesHostTableProps,
|
||||
nextProps: AnomaliesHostTableProps
|
||||
export const anomaliesTableDefaultEquality = (
|
||||
prevProps: AnomaliesTableCommonProps,
|
||||
nextProps: AnomaliesTableCommonProps
|
||||
): boolean =>
|
||||
prevProps.startDate === nextProps.startDate &&
|
||||
prevProps.endDate === nextProps.endDate &&
|
|
@ -5,121 +5,35 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import '../../../mock/match_media';
|
||||
import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns';
|
||||
import { HostsType } from '../../../../hosts/store/model';
|
||||
import * as i18n from './translations';
|
||||
import { AnomaliesByHost, Anomaly } from '../types';
|
||||
import { Columns } from '../../paginated_table';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { useMountAppended } from '../../../utils/use_mount_appended';
|
||||
|
||||
jest.mock('../../../lib/kibana');
|
||||
|
||||
const startDate = new Date(2001).toISOString();
|
||||
const endDate = new Date(3000).toISOString();
|
||||
const interval = 'days';
|
||||
const narrowDateRange = jest.fn();
|
||||
describe('get_anomalies_host_table_columns', () => {
|
||||
const mount = useMountAppended();
|
||||
|
||||
describe('get_anomalies_host_table_columns', () => {
|
||||
test('on hosts page, we expect to get all columns', () => {
|
||||
expect(
|
||||
getAnomaliesHostTableColumnsCurated(
|
||||
HostsType.page,
|
||||
startDate,
|
||||
endDate,
|
||||
interval,
|
||||
narrowDateRange
|
||||
).length
|
||||
).toEqual(6);
|
||||
expect(getAnomaliesHostTableColumnsCurated(HostsType.page, startDate, endDate).length).toEqual(
|
||||
6
|
||||
);
|
||||
});
|
||||
|
||||
test('on host details page, we expect to remove one columns', () => {
|
||||
const columns = getAnomaliesHostTableColumnsCurated(
|
||||
HostsType.details,
|
||||
startDate,
|
||||
endDate,
|
||||
interval,
|
||||
narrowDateRange
|
||||
);
|
||||
const columns = getAnomaliesHostTableColumnsCurated(HostsType.details, startDate, endDate);
|
||||
expect(columns.length).toEqual(5);
|
||||
});
|
||||
|
||||
test('on host page, we should have Host Name', () => {
|
||||
const columns = getAnomaliesHostTableColumnsCurated(
|
||||
HostsType.page,
|
||||
startDate,
|
||||
endDate,
|
||||
interval,
|
||||
narrowDateRange
|
||||
);
|
||||
const columns = getAnomaliesHostTableColumnsCurated(HostsType.page, startDate, endDate);
|
||||
expect(columns.some((col) => col.name === i18n.HOST_NAME)).toEqual(true);
|
||||
});
|
||||
|
||||
test('on host details page, we should not have Host Name', () => {
|
||||
const columns = getAnomaliesHostTableColumnsCurated(
|
||||
HostsType.details,
|
||||
startDate,
|
||||
endDate,
|
||||
interval,
|
||||
narrowDateRange
|
||||
);
|
||||
const columns = getAnomaliesHostTableColumnsCurated(HostsType.details, startDate, endDate);
|
||||
expect(columns.some((col) => col.name === i18n.HOST_NAME)).toEqual(false);
|
||||
});
|
||||
|
||||
test('on host page, undefined influencers should turn into an empty column string', () => {
|
||||
const columns = getAnomaliesHostTableColumnsCurated(
|
||||
HostsType.page,
|
||||
startDate,
|
||||
endDate,
|
||||
interval,
|
||||
narrowDateRange
|
||||
);
|
||||
const column = columns.find((col) => col.name === i18n.INFLUENCED_BY) as Columns<
|
||||
Anomaly['influencers'],
|
||||
AnomaliesByHost
|
||||
>;
|
||||
const anomaly: AnomaliesByHost = {
|
||||
hostName: 'host.name',
|
||||
anomaly: {
|
||||
detectorIndex: 0,
|
||||
entityName: 'entity-name-1',
|
||||
entityValue: 'entity-value-1',
|
||||
jobId: 'job-1',
|
||||
rowId: 'row-1',
|
||||
severity: 100,
|
||||
time: new Date('01/01/2000').valueOf(),
|
||||
source: {
|
||||
job_id: 'job-1',
|
||||
result_type: 'result-1',
|
||||
probability: 50,
|
||||
multi_bucket_impact: 0,
|
||||
record_score: 0,
|
||||
initial_record_score: 0,
|
||||
bucket_span: 0,
|
||||
detector_index: 0,
|
||||
is_interim: true,
|
||||
timestamp: new Date('01/01/2000').valueOf(),
|
||||
by_field_name: 'some field name',
|
||||
by_field_value: 'some field value',
|
||||
partition_field_name: 'partition field name',
|
||||
partition_field_value: 'partition field value',
|
||||
function: 'function-1',
|
||||
function_description: 'description-1',
|
||||
typical: [5, 3],
|
||||
actual: [7, 4],
|
||||
influencers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
if (column != null && column.render != null) {
|
||||
const wrapper = mount(<TestProviders>{column.render(undefined, anomaly)}</TestProviders>);
|
||||
expect(wrapper.text()).toEqual('');
|
||||
} else {
|
||||
expect(column).not.toBe(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,27 +6,18 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { Columns } from '../../paginated_table';
|
||||
import { AnomaliesByHost, Anomaly, NarrowDateRange } from '../types';
|
||||
import { AnomaliesByHost, Anomaly } from '../types';
|
||||
import { getRowItemDraggable } from '../../tables/helpers';
|
||||
import { EntityDraggable } from '../entity_draggable';
|
||||
import { createCompoundHostKey } from './create_compound_key';
|
||||
import { createCompoundAnomalyKey } from './create_compound_key';
|
||||
import { HostDetailsLink } from '../../links';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { getEntries } from '../get_entries';
|
||||
import { DraggableScore } from '../score/draggable_score';
|
||||
import { ExplorerLink } from '../links/create_explorer_link';
|
||||
import { HostsType } from '../../../../hosts/store/model';
|
||||
import { escapeDataProviderId } from '../../drag_and_drop/helpers';
|
||||
import { FormattedRelativePreferenceDate } from '../../formatted_date';
|
||||
import { getAnomaliesDefaultTableColumns } from './get_anomalies_table_columns';
|
||||
|
||||
export const getAnomaliesHostTableColumns = (
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
interval: string,
|
||||
narrowDateRange: NarrowDateRange
|
||||
endDate: string
|
||||
): [
|
||||
Columns<AnomaliesByHost['hostName'], AnomaliesByHost>,
|
||||
Columns<Anomaly['severity'], AnomaliesByHost>,
|
||||
|
@ -43,100 +34,21 @@ export const getAnomaliesHostTableColumns = (
|
|||
getRowItemDraggable({
|
||||
rowItem: hostName,
|
||||
attrName: 'host.name',
|
||||
idPrefix: `anomalies-host-table-hostName-${createCompoundHostKey(
|
||||
anomaliesByHost
|
||||
idPrefix: `anomalies-host-table-hostName-${createCompoundAnomalyKey(
|
||||
anomaliesByHost.anomaly
|
||||
)}-hostName`,
|
||||
render: (item) => <HostDetailsLink hostName={item} />,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: i18n.DETECTOR,
|
||||
field: 'anomaly.jobId',
|
||||
sortable: true,
|
||||
render: (jobId, anomaliesByHost) => (
|
||||
<ExplorerLink
|
||||
score={anomaliesByHost.anomaly}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
linkName={jobId}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.SCORE,
|
||||
field: 'anomaly.severity',
|
||||
sortable: true,
|
||||
render: (_, anomaliesByHost) => (
|
||||
<DraggableScore
|
||||
id={escapeDataProviderId(
|
||||
`anomalies-host-table-severity-${createCompoundHostKey(anomaliesByHost)}`
|
||||
)}
|
||||
score={anomaliesByHost.anomaly}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.ENTITY,
|
||||
field: 'anomaly.entityValue',
|
||||
sortable: true,
|
||||
render: (entityValue, anomaliesByHost) => (
|
||||
<EntityDraggable
|
||||
idPrefix={`anomalies-host-table-entityValue${createCompoundHostKey(
|
||||
anomaliesByHost
|
||||
)}-entity`}
|
||||
entityName={anomaliesByHost.anomaly.entityName}
|
||||
entityValue={entityValue}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.INFLUENCED_BY,
|
||||
field: 'anomaly.influencers',
|
||||
render: (influencers, anomaliesByHost) => (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
{influencers &&
|
||||
influencers.map((influencer) => {
|
||||
const [key, value] = getEntries(influencer);
|
||||
const entityName = key != null ? key : '';
|
||||
const entityValue = value != null ? value : '';
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={`${entityName}-${entityValue}-${createCompoundHostKey(anomaliesByHost)}`}
|
||||
grow={false}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EntityDraggable
|
||||
idPrefix={`anomalies-host-table-influencers-${entityName}-${entityValue}-${createCompoundHostKey(
|
||||
anomaliesByHost
|
||||
)}`}
|
||||
entityName={entityName}
|
||||
entityValue={entityValue}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.TIME_STAMP,
|
||||
field: 'anomaly.time',
|
||||
sortable: true,
|
||||
render: (time) => <FormattedRelativePreferenceDate value={time} />,
|
||||
},
|
||||
...getAnomaliesDefaultTableColumns(startDate, endDate),
|
||||
];
|
||||
|
||||
export const getAnomaliesHostTableColumnsCurated = (
|
||||
pageType: HostsType,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
interval: string,
|
||||
narrowDateRange: NarrowDateRange
|
||||
endDate: string
|
||||
) => {
|
||||
const columns = getAnomaliesHostTableColumns(startDate, endDate, interval, narrowDateRange);
|
||||
const columns = getAnomaliesHostTableColumns(startDate, endDate);
|
||||
|
||||
// Columns to exclude from host details pages
|
||||
if (pageType === HostsType.details) {
|
||||
|
|
|
@ -9,19 +9,12 @@ import '../../../mock/match_media';
|
|||
import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns';
|
||||
import { NetworkType } from '../../../../network/store/model';
|
||||
import * as i18n from './translations';
|
||||
import { AnomaliesByNetwork, Anomaly } from '../types';
|
||||
import { Columns } from '../../paginated_table';
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { useMountAppended } from '../../../utils/use_mount_appended';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
const startDate = new Date(2001).toISOString();
|
||||
const endDate = new Date(3000).toISOString();
|
||||
describe('get_anomalies_network_table_columns', () => {
|
||||
const mount = useMountAppended();
|
||||
|
||||
test('on network page, we expect to get all columns', () => {
|
||||
expect(
|
||||
getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate).length
|
||||
|
@ -42,52 +35,4 @@ describe('get_anomalies_network_table_columns', () => {
|
|||
const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.details, startDate, endDate);
|
||||
expect(columns.some((col) => col.name === i18n.NETWORK_NAME)).toEqual(false);
|
||||
});
|
||||
|
||||
test('on network page, undefined influencers should turn into an empty column string', () => {
|
||||
const columns = getAnomaliesNetworkTableColumnsCurated(NetworkType.page, startDate, endDate);
|
||||
const column = columns.find((col) => col.name === i18n.INFLUENCED_BY) as Columns<
|
||||
Anomaly['influencers'],
|
||||
AnomaliesByNetwork
|
||||
>;
|
||||
const anomaly: AnomaliesByNetwork = {
|
||||
type: 'source.ip',
|
||||
ip: '127.0.0.1',
|
||||
anomaly: {
|
||||
detectorIndex: 0,
|
||||
entityName: 'entity-name-1',
|
||||
entityValue: 'entity-value-1',
|
||||
jobId: 'job-1',
|
||||
rowId: 'row-1',
|
||||
severity: 100,
|
||||
time: new Date('01/01/2000').valueOf(),
|
||||
source: {
|
||||
job_id: 'job-1',
|
||||
result_type: 'result-1',
|
||||
probability: 50,
|
||||
multi_bucket_impact: 0,
|
||||
record_score: 0,
|
||||
initial_record_score: 0,
|
||||
bucket_span: 0,
|
||||
detector_index: 0,
|
||||
is_interim: true,
|
||||
timestamp: new Date('01/01/2000').valueOf(),
|
||||
by_field_name: 'some field name',
|
||||
by_field_value: 'some field value',
|
||||
partition_field_name: 'partition field name',
|
||||
partition_field_value: 'partition field value',
|
||||
function: 'function-1',
|
||||
function_description: 'description-1',
|
||||
typical: [5, 3],
|
||||
actual: [7, 4],
|
||||
influencers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
if (column != null && column.render != null) {
|
||||
const wrapper = mount(<TestProviders>{column.render(undefined, anomaly)}</TestProviders>);
|
||||
expect(wrapper.text()).toEqual('');
|
||||
} else {
|
||||
expect(column).not.toBe(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,23 +6,17 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { Columns } from '../../paginated_table';
|
||||
import { Anomaly, AnomaliesByNetwork } from '../types';
|
||||
import { getRowItemDraggable } from '../../tables/helpers';
|
||||
import { EntityDraggable } from '../entity_draggable';
|
||||
import { createCompoundNetworkKey } from './create_compound_key';
|
||||
import { createCompoundAnomalyKey } from './create_compound_key';
|
||||
import { NetworkDetailsLink } from '../../links';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { getEntries } from '../get_entries';
|
||||
import { DraggableScore } from '../score/draggable_score';
|
||||
import { ExplorerLink } from '../links/create_explorer_link';
|
||||
import { FormattedRelativePreferenceDate } from '../../formatted_date';
|
||||
import { NetworkType } from '../../../../network/store/model';
|
||||
import { escapeDataProviderId } from '../../drag_and_drop/helpers';
|
||||
import { FlowTarget } from '../../../../../common/search_strategy';
|
||||
import { getAnomaliesDefaultTableColumns } from './get_anomalies_table_columns';
|
||||
|
||||
export const getAnomaliesNetworkTableColumns = (
|
||||
startDate: string,
|
||||
|
@ -44,84 +38,13 @@ export const getAnomaliesNetworkTableColumns = (
|
|||
getRowItemDraggable({
|
||||
rowItem: ip,
|
||||
attrName: anomaliesByNetwork.type,
|
||||
idPrefix: `anomalies-network-table-ip-${createCompoundNetworkKey(anomaliesByNetwork)}`,
|
||||
idPrefix: `anomalies-network-table-ip-${createCompoundAnomalyKey(
|
||||
anomaliesByNetwork.anomaly
|
||||
)}`,
|
||||
render: (item) => <NetworkDetailsLink ip={item} flowTarget={flowTarget} />,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: i18n.DETECTOR,
|
||||
field: 'anomaly.jobId',
|
||||
sortable: true,
|
||||
render: (jobId, anomaliesByHost) => (
|
||||
<ExplorerLink
|
||||
score={anomaliesByHost.anomaly}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
linkName={jobId}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.SCORE,
|
||||
field: 'anomaly.severity',
|
||||
sortable: true,
|
||||
render: (_, anomaliesByNetwork) => (
|
||||
<DraggableScore
|
||||
id={escapeDataProviderId(
|
||||
`anomalies-network-table-severity-${createCompoundNetworkKey(anomaliesByNetwork)}`
|
||||
)}
|
||||
score={anomaliesByNetwork.anomaly}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.ENTITY,
|
||||
field: 'anomaly.entityValue',
|
||||
sortable: true,
|
||||
render: (entityValue, anomaliesByNetwork) => (
|
||||
<EntityDraggable
|
||||
idPrefix={`anomalies-network-table-entityValue-${createCompoundNetworkKey(
|
||||
anomaliesByNetwork
|
||||
)}`}
|
||||
entityName={anomaliesByNetwork.anomaly.entityName}
|
||||
entityValue={entityValue}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.INFLUENCED_BY,
|
||||
field: 'anomaly.influencers',
|
||||
render: (influencers, anomaliesByNetwork) => (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
{influencers &&
|
||||
influencers.map((influencer) => {
|
||||
const [key, value] = getEntries(influencer);
|
||||
const entityName = key != null ? key : '';
|
||||
const entityValue = value != null ? value : '';
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={`${entityName}-${entityValue}-${createCompoundNetworkKey(anomaliesByNetwork)}`}
|
||||
grow={false}
|
||||
>
|
||||
<EntityDraggable
|
||||
idPrefix={`anomalies-network-table-influencers-${entityName}-${entityValue}-${createCompoundNetworkKey(
|
||||
anomaliesByNetwork
|
||||
)}`}
|
||||
entityName={entityName}
|
||||
entityValue={entityValue}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.TIME_STAMP,
|
||||
field: 'anomaly.time',
|
||||
sortable: true,
|
||||
render: (time) => <FormattedRelativePreferenceDate value={time} />,
|
||||
},
|
||||
...getAnomaliesDefaultTableColumns(startDate, endDate),
|
||||
];
|
||||
|
||||
export const getAnomaliesNetworkTableColumnsCurated = (
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 '../../../mock/match_media';
|
||||
import * as i18n from './translations';
|
||||
import { AnomaliesBy, Anomaly } from '../types';
|
||||
import { Columns } from '../../paginated_table';
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../../mock';
|
||||
import { useMountAppended } from '../../../utils/use_mount_appended';
|
||||
import { getAnomaliesDefaultTableColumns } from './get_anomalies_table_columns';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
|
||||
const startDate = new Date(2001).toISOString();
|
||||
const endDate = new Date(3000).toISOString();
|
||||
describe('getAnomaliesDefaultTableColumns', () => {
|
||||
const mount = useMountAppended();
|
||||
|
||||
test('it should return all columns', () => {
|
||||
expect(getAnomaliesDefaultTableColumns(startDate, endDate).length).toEqual(5);
|
||||
});
|
||||
|
||||
test('it should return an empty column string for undefined influencers', () => {
|
||||
const columns = getAnomaliesDefaultTableColumns(startDate, endDate);
|
||||
const column = columns.find((col) => col.name === i18n.INFLUENCED_BY) as Columns<
|
||||
Anomaly['influencers'],
|
||||
AnomaliesBy
|
||||
>;
|
||||
const anomaly: AnomaliesBy = {
|
||||
anomaly: {
|
||||
detectorIndex: 0,
|
||||
entityName: 'entity-name-1',
|
||||
entityValue: 'entity-value-1',
|
||||
jobId: 'job-1',
|
||||
rowId: 'row-1',
|
||||
severity: 100,
|
||||
time: new Date('01/01/2000').valueOf(),
|
||||
source: {
|
||||
job_id: 'job-1',
|
||||
result_type: 'result-1',
|
||||
probability: 50,
|
||||
multi_bucket_impact: 0,
|
||||
record_score: 0,
|
||||
initial_record_score: 0,
|
||||
bucket_span: 0,
|
||||
detector_index: 0,
|
||||
is_interim: true,
|
||||
timestamp: new Date('01/01/2000').valueOf(),
|
||||
by_field_name: 'some field name',
|
||||
by_field_value: 'some field value',
|
||||
partition_field_name: 'partition field name',
|
||||
partition_field_value: 'partition field value',
|
||||
function: 'function-1',
|
||||
function_description: 'description-1',
|
||||
typical: [5, 3],
|
||||
actual: [7, 4],
|
||||
influencers: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
if (column != null && column.render != null) {
|
||||
const wrapper = mount(<TestProviders>{column.render(undefined, anomaly)}</TestProviders>);
|
||||
expect(wrapper.text()).toEqual('');
|
||||
} else {
|
||||
expect(column).not.toBe(null);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { Columns } from '../../paginated_table';
|
||||
import { AnomaliesBy, Anomaly } from '../types';
|
||||
|
||||
import { EntityDraggable } from '../entity_draggable';
|
||||
import { createCompoundAnomalyKey } from './create_compound_key';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { getEntries } from '../get_entries';
|
||||
import { DraggableScore } from '../score/draggable_score';
|
||||
import { ExplorerLink } from '../links/create_explorer_link';
|
||||
import { escapeDataProviderId } from '../../drag_and_drop/helpers';
|
||||
import { FormattedRelativePreferenceDate } from '../../formatted_date';
|
||||
|
||||
export const getAnomaliesDefaultTableColumns = (
|
||||
startDate: string,
|
||||
endDate: string
|
||||
): [
|
||||
Columns<Anomaly['severity'], AnomaliesBy>,
|
||||
Columns<Anomaly['jobId'], AnomaliesBy>,
|
||||
Columns<Anomaly['entityValue'], AnomaliesBy>,
|
||||
Columns<Anomaly['influencers'], AnomaliesBy>,
|
||||
Columns<Anomaly['time'], AnomaliesBy>
|
||||
] => [
|
||||
{
|
||||
name: i18n.DETECTOR,
|
||||
field: 'anomaly.jobId',
|
||||
sortable: true,
|
||||
render: (jobId, anomalyBy) => (
|
||||
<ExplorerLink
|
||||
score={anomalyBy.anomaly}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
linkName={jobId}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.SCORE,
|
||||
field: 'anomaly.severity',
|
||||
sortable: true,
|
||||
render: (_, anomalyBy) => (
|
||||
<DraggableScore
|
||||
id={escapeDataProviderId(
|
||||
`anomalies-table-severity-${createCompoundAnomalyKey(anomalyBy.anomaly)}`
|
||||
)}
|
||||
score={anomalyBy.anomaly}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.ENTITY,
|
||||
field: 'anomaly.entityValue',
|
||||
sortable: true,
|
||||
render: (entityValue, anomalyBy) => (
|
||||
<EntityDraggable
|
||||
idPrefix={`anomalies-table-entityValue${createCompoundAnomalyKey(
|
||||
anomalyBy.anomaly
|
||||
)}-entity`}
|
||||
entityName={anomalyBy.anomaly.entityName}
|
||||
entityValue={entityValue}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.INFLUENCED_BY,
|
||||
field: 'anomaly.influencers',
|
||||
render: (influencers, anomalyBy) => (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
{influencers &&
|
||||
influencers.map((influencer) => {
|
||||
const [key, value] = getEntries(influencer);
|
||||
const entityName = key != null ? key : '';
|
||||
const entityValue = value != null ? value : '';
|
||||
return (
|
||||
<EuiFlexItem
|
||||
key={`${entityName}-${entityValue}-${createCompoundAnomalyKey(anomalyBy.anomaly)}`}
|
||||
grow={false}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EntityDraggable
|
||||
idPrefix={`anomalies-table-influencers-${entityName}-${entityValue}-${createCompoundAnomalyKey(
|
||||
anomalyBy.anomaly
|
||||
)}`}
|
||||
entityName={entityName}
|
||||
entityValue={entityValue}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.TIME_STAMP,
|
||||
field: 'anomaly.time',
|
||||
sortable: true,
|
||||
render: (time) => <FormattedRelativePreferenceDate value={time} />,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 '../../../mock/match_media';
|
||||
import { getAnomaliesUserTableColumnsCurated } from './get_anomalies_user_table_columns';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
jest.mock('../../../lib/kibana');
|
||||
|
||||
const startDate = new Date(2001).toISOString();
|
||||
const endDate = new Date(3000).toISOString();
|
||||
|
||||
describe('get_anomalies_user_table_columns', () => {
|
||||
test('on users page, we expect to get all columns', () => {
|
||||
expect(getAnomaliesUserTableColumnsCurated(UsersType.page, startDate, endDate).length).toEqual(
|
||||
6
|
||||
);
|
||||
});
|
||||
|
||||
test('on user details page, we expect to remove one columns', () => {
|
||||
const columns = getAnomaliesUserTableColumnsCurated(UsersType.details, startDate, endDate);
|
||||
expect(columns.length).toEqual(5);
|
||||
});
|
||||
|
||||
test('on users page, we should have User Name', () => {
|
||||
const columns = getAnomaliesUserTableColumnsCurated(UsersType.page, startDate, endDate);
|
||||
expect(columns.some((col) => col.name === i18n.USER_NAME)).toEqual(true);
|
||||
});
|
||||
|
||||
test('on user details page, we should not have User Name', () => {
|
||||
const columns = getAnomaliesUserTableColumnsCurated(UsersType.details, startDate, endDate);
|
||||
expect(columns.some((col) => col.name === i18n.USER_NAME)).toEqual(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { Columns } from '../../paginated_table';
|
||||
import { AnomaliesByUser, Anomaly } from '../types';
|
||||
import { getRowItemDraggable } from '../../tables/helpers';
|
||||
import { createCompoundAnomalyKey } from './create_compound_key';
|
||||
import { UserDetailsLink } from '../../links';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { UsersType } from '../../../../users/store/model';
|
||||
import { getAnomaliesDefaultTableColumns } from './get_anomalies_table_columns';
|
||||
|
||||
export const getAnomaliesUserTableColumns = (
|
||||
startDate: string,
|
||||
endDate: string
|
||||
): [
|
||||
Columns<AnomaliesByUser['userName'], AnomaliesByUser>,
|
||||
Columns<Anomaly['severity'], AnomaliesByUser>,
|
||||
Columns<Anomaly['jobId'], AnomaliesByUser>,
|
||||
Columns<Anomaly['entityValue'], AnomaliesByUser>,
|
||||
Columns<Anomaly['influencers'], AnomaliesByUser>,
|
||||
Columns<Anomaly['time'], AnomaliesByUser>
|
||||
] => [
|
||||
{
|
||||
name: i18n.USER_NAME,
|
||||
field: 'userName',
|
||||
sortable: true,
|
||||
render: (userName, anomaliesByUser) =>
|
||||
getRowItemDraggable({
|
||||
rowItem: userName,
|
||||
attrName: 'user.name',
|
||||
idPrefix: `anomalies-user-table-userName-${createCompoundAnomalyKey(
|
||||
anomaliesByUser.anomaly
|
||||
)}-userName`,
|
||||
render: (item) => <UserDetailsLink userName={item} />,
|
||||
}),
|
||||
},
|
||||
...getAnomaliesDefaultTableColumns(startDate, endDate),
|
||||
];
|
||||
|
||||
export const getAnomaliesUserTableColumnsCurated = (
|
||||
pageType: UsersType,
|
||||
startDate: string,
|
||||
endDate: string
|
||||
) => {
|
||||
const columns = getAnomaliesUserTableColumns(startDate, endDate);
|
||||
|
||||
// Columns to exclude from user details pages
|
||||
if (pageType === UsersType.details) {
|
||||
return columns.filter((column) => column.name !== i18n.USER_NAME);
|
||||
} else {
|
||||
return columns;
|
||||
}
|
||||
};
|
|
@ -6,12 +6,11 @@
|
|||
*/
|
||||
|
||||
import { AnomaliesNetworkTableProps } from '../types';
|
||||
import { anomaliesTableDefaultEquality } from './default_equality';
|
||||
|
||||
export const networkEquality = (
|
||||
prevProps: AnomaliesNetworkTableProps,
|
||||
nextProps: AnomaliesNetworkTableProps
|
||||
): boolean =>
|
||||
prevProps.startDate === nextProps.startDate &&
|
||||
prevProps.endDate === nextProps.endDate &&
|
||||
prevProps.skip === nextProps.skip &&
|
||||
anomaliesTableDefaultEquality(prevProps, nextProps) &&
|
||||
prevProps.flowTarget === nextProps.flowTarget;
|
||||
|
|
|
@ -42,6 +42,10 @@ export const HOST_NAME = i18n.translate('xpack.securitySolution.ml.table.hostNam
|
|||
defaultMessage: 'Host name',
|
||||
});
|
||||
|
||||
export const USER_NAME = i18n.translate('xpack.securitySolution.ml.table.userNameTitle', {
|
||||
defaultMessage: 'User name',
|
||||
});
|
||||
|
||||
export const INFLUENCED_BY = i18n.translate('xpack.securitySolution.ml.table.influencedByTitle', {
|
||||
defaultMessage: 'Influenced by',
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { FlowTarget } from '../../../../common/search_strategy';
|
|||
|
||||
import { HostsType } from '../../../hosts/store/model';
|
||||
import { NetworkType } from '../../../network/store/model';
|
||||
import { UsersType } from '../../../users/store/model';
|
||||
|
||||
export interface Source {
|
||||
job_id: string;
|
||||
|
@ -62,37 +63,48 @@ export interface Anomalies {
|
|||
|
||||
export type NarrowDateRange = (score: Anomaly, interval: string) => void;
|
||||
|
||||
export interface AnomaliesByHost {
|
||||
hostName: string;
|
||||
export interface AnomaliesBy {
|
||||
anomaly: Anomaly;
|
||||
}
|
||||
|
||||
export interface AnomaliesByHost extends AnomaliesBy {
|
||||
hostName: string;
|
||||
}
|
||||
|
||||
export type DestinationOrSource = 'source.ip' | 'destination.ip';
|
||||
|
||||
export interface AnomaliesByNetwork {
|
||||
export interface AnomaliesByNetwork extends AnomaliesBy {
|
||||
type: DestinationOrSource;
|
||||
ip: string;
|
||||
anomaly: Anomaly;
|
||||
}
|
||||
|
||||
export interface HostOrNetworkProps {
|
||||
export interface AnomaliesByUser extends AnomaliesBy {
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export interface AnomaliesTableCommonProps {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
narrowDateRange: NarrowDateRange;
|
||||
skip: boolean;
|
||||
}
|
||||
|
||||
export type AnomaliesHostTableProps = HostOrNetworkProps & {
|
||||
export type AnomaliesHostTableProps = AnomaliesTableCommonProps & {
|
||||
hostName?: string;
|
||||
type: HostsType;
|
||||
};
|
||||
|
||||
export type AnomaliesNetworkTableProps = HostOrNetworkProps & {
|
||||
export type AnomaliesNetworkTableProps = AnomaliesTableCommonProps & {
|
||||
ip?: string;
|
||||
type: NetworkType;
|
||||
flowTarget?: FlowTarget;
|
||||
};
|
||||
|
||||
export type AnomaliesUserTableProps = AnomaliesTableCommonProps & {
|
||||
userName?: string;
|
||||
type: UsersType;
|
||||
};
|
||||
|
||||
const sourceOrDestination = ['source.ip', 'destination.ip'];
|
||||
|
||||
export const isDestinationOrSource = (value: string | null): value is DestinationOrSource =>
|
||||
|
|
|
@ -12,9 +12,10 @@ import { GlobalTimeArgs } from '../../use_global_time';
|
|||
import { HostsType } from '../../../../hosts/store/model';
|
||||
import { NetworkType } from '../../../../network/store//model';
|
||||
import { FlowTarget } from '../../../../../common/search_strategy';
|
||||
import { UsersType } from '../../../../users/store/model';
|
||||
|
||||
interface QueryTabBodyProps {
|
||||
type: HostsType | NetworkType;
|
||||
type: HostsType | NetworkType | UsersType;
|
||||
filterQuery?: string | ESTermQuery;
|
||||
}
|
||||
|
||||
|
|
|
@ -205,6 +205,7 @@ export const mockGlobalState: State = {
|
|||
limit: 10,
|
||||
// TODO sort: { field: RiskScoreFields.riskScore, direction: Direction.desc },
|
||||
},
|
||||
[usersModel.UsersTableType.anomalies]: null,
|
||||
},
|
||||
},
|
||||
details: {
|
||||
|
@ -214,6 +215,7 @@ export const mockGlobalState: State = {
|
|||
limit: 10,
|
||||
// TODO sort: { field: HostRulesFields.riskScore, direction: Direction.desc },
|
||||
},
|
||||
[usersModel.UsersTableType.anomalies]: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
24
x-pack/plugins/security_solution/public/users/jest.config.js
Normal file
24
x-pack/plugins/security_solution/public/users/jest.config.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/security_solution/public/users'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/users',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: ['<rootDir>/x-pack/plugins/security_solution/public/users/**/*.{ts,tsx}'],
|
||||
// See: https://github.com/elastic/kibana/issues/117255, the moduleNameMapper creates mocks to avoid memory leaks from kibana core.
|
||||
moduleNameMapper: {
|
||||
'core/server$': '<rootDir>/x-pack/plugins/security_solution/server/__mocks__/core.mock.ts',
|
||||
'task_manager/server$':
|
||||
'<rootDir>/x-pack/plugins/security_solution/server/__mocks__/task_manager.mock.ts',
|
||||
'alerting/server$': '<rootDir>/x-pack/plugins/security_solution/server/__mocks__/alert.mock.ts',
|
||||
'actions/server$': '<rootDir>/x-pack/plugins/security_solution/server/__mocks__/action.mock.ts',
|
||||
},
|
||||
};
|
|
@ -10,6 +10,6 @@ import { UsersTableType } from '../store/model';
|
|||
|
||||
export const usersDetailsPagePath = `${USERS_PATH}/:detailName`;
|
||||
|
||||
export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers})`;
|
||||
export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers}|${UsersTableType.anomalies})`;
|
||||
|
||||
export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.allUsers})`;
|
||||
|
|
|
@ -17,7 +17,7 @@ export const navTabsUsersDetails = (hostName: string): UsersDetailsNavTab => {
|
|||
return {
|
||||
[UsersTableType.allUsers]: {
|
||||
id: UsersTableType.allUsers,
|
||||
name: i18n.ALL_USERS_TITLE,
|
||||
name: i18n.NAVIGATION_ALL_USERS_TITLE,
|
||||
href: getTabsOnUsersDetailsUrl(hostName, UsersTableType.allUsers),
|
||||
disabled: false,
|
||||
},
|
||||
|
|
|
@ -21,7 +21,8 @@ import { SecurityPageName } from '../../../app/types';
|
|||
export const type = usersModel.UsersType.details;
|
||||
|
||||
const TabNameMappedToI18nKey: Record<UsersTableType, string> = {
|
||||
[UsersTableType.allUsers]: i18n.ALL_USERS_TITLE,
|
||||
[UsersTableType.allUsers]: i18n.NAVIGATION_ALL_USERS_TITLE,
|
||||
[UsersTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE,
|
||||
};
|
||||
|
||||
export const getBreadcrumbs = (
|
||||
|
|
|
@ -15,8 +15,14 @@ const getTabsOnUsersUrl = (tabName: UsersTableType) => `${USERS_PATH}/${tabName}
|
|||
export const navTabsUsers: UsersNavTab = {
|
||||
[UsersTableType.allUsers]: {
|
||||
id: UsersTableType.allUsers,
|
||||
name: i18n.ALL_USERS_TITLE,
|
||||
name: i18n.NAVIGATION_ALL_USERS_TITLE,
|
||||
href: getTabsOnUsersUrl(UsersTableType.allUsers),
|
||||
disabled: false,
|
||||
},
|
||||
[UsersTableType.anomalies]: {
|
||||
id: UsersTableType.anomalies,
|
||||
name: i18n.NAVIGATION_ANOMALIES_TITLE,
|
||||
href: getTabsOnUsersUrl(UsersTableType.anomalies),
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import { ESTermQuery } from '../../../../common/typed_json';
|
|||
import { DocValueFields } from '../../../../../timelines/common';
|
||||
import { NavTab } from '../../../common/components/navigation/types';
|
||||
|
||||
type KeyUsersNavTab = UsersTableType.allUsers;
|
||||
type KeyUsersNavTab = UsersTableType.allUsers | UsersTableType.anomalies;
|
||||
|
||||
export type UsersNavTab = Record<KeyUsersNavTab, NavTab>;
|
||||
export interface QueryTabBodyProps {
|
||||
|
|
|
@ -11,6 +11,16 @@ export const PAGE_TITLE = i18n.translate('xpack.securitySolution.users.pageTitle
|
|||
defaultMessage: 'Users',
|
||||
});
|
||||
|
||||
export const ALL_USERS_TITLE = i18n.translate('xpack.securitySolution.users.allUsers', {
|
||||
defaultMessage: 'All users',
|
||||
});
|
||||
export const NAVIGATION_ALL_USERS_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.users.navigation.allUsersTitle',
|
||||
{
|
||||
defaultMessage: 'All users',
|
||||
}
|
||||
);
|
||||
|
||||
export const NAVIGATION_ANOMALIES_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.users.navigation.anomaliesTitle',
|
||||
{
|
||||
defaultMessage: 'Anomalies',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import '../../common/mock/match_media';
|
||||
import { TestProviders } from '../../common/mock';
|
||||
import { SecuritySolutionTabNavigation } from '../../common/components/navigation';
|
||||
import { Users } from './users';
|
||||
import { useSourcererDataView } from '../../common/containers/sourcerer';
|
||||
|
||||
jest.mock('../../common/containers/sourcerer');
|
||||
jest.mock('../../common/components/search_bar', () => ({
|
||||
SiemSearchBar: () => null,
|
||||
}));
|
||||
jest.mock('../../common/components/query_bar', () => ({
|
||||
QueryBar: () => null,
|
||||
}));
|
||||
|
||||
type Action = 'PUSH' | 'POP' | 'REPLACE';
|
||||
const pop: Action = 'POP';
|
||||
const location = {
|
||||
pathname: '/network',
|
||||
search: '',
|
||||
state: '',
|
||||
hash: '',
|
||||
};
|
||||
const mockHistory = {
|
||||
length: 2,
|
||||
location,
|
||||
action: pop,
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
go: jest.fn(),
|
||||
goBack: jest.fn(),
|
||||
goForward: jest.fn(),
|
||||
block: jest.fn(),
|
||||
createHref: jest.fn(),
|
||||
listen: jest.fn(),
|
||||
};
|
||||
const mockUseSourcererDataView = useSourcererDataView as jest.Mock;
|
||||
describe('Users - rendering', () => {
|
||||
test('it renders the Setup Instructions text when no index is available', async () => {
|
||||
mockUseSourcererDataView.mockReturnValue({
|
||||
indicesExist: false,
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Users />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it should render tab navigation', async () => {
|
||||
mockUseSourcererDataView.mockReturnValue({
|
||||
indicesExist: true,
|
||||
indexPattern: {},
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Router history={mockHistory}>
|
||||
<Users />
|
||||
</Router>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find(SecuritySolutionTabNavigation).exists()).toBe(true);
|
||||
});
|
||||
});
|
|
@ -5,18 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { UsersTabsProps } from './types';
|
||||
import { UsersTableType } from '../store/model';
|
||||
import { USERS_PATH } from '../../../common/constants';
|
||||
import { AllUsersQueryTabBody } from './navigation';
|
||||
import { AnomaliesQueryTabBody } from '../../common/containers/anomalies/anomalies_query_tab_body';
|
||||
import { AnomaliesUserTable } from '../../common/components/ml/tables/anomalies_user_table';
|
||||
import { Anomaly } from '../../common/components/ml/types';
|
||||
import { scoreIntervalToDateTime } from '../../common/components/ml/score/score_interval_to_datetime';
|
||||
import { UpdateDateRange } from '../../common/components/charts/common';
|
||||
|
||||
export const UsersTabs = memo<UsersTabsProps>(
|
||||
({
|
||||
deleteQuery,
|
||||
docValueFields,
|
||||
filterQuery,
|
||||
from,
|
||||
indexNames,
|
||||
|
@ -24,21 +28,55 @@ export const UsersTabs = memo<UsersTabsProps>(
|
|||
setQuery,
|
||||
to,
|
||||
type,
|
||||
setAbsoluteRangeDatePicker,
|
||||
}) => {
|
||||
const narrowDateRange = useCallback(
|
||||
(score: Anomaly, interval: string) => {
|
||||
const fromTo = scoreIntervalToDateTime(score, interval);
|
||||
setAbsoluteRangeDatePicker({
|
||||
id: 'global',
|
||||
from: fromTo.from,
|
||||
to: fromTo.to,
|
||||
});
|
||||
},
|
||||
[setAbsoluteRangeDatePicker]
|
||||
);
|
||||
|
||||
const updateDateRange = useCallback<UpdateDateRange>(
|
||||
({ x }) => {
|
||||
if (!x) {
|
||||
return;
|
||||
}
|
||||
const [min, max] = x;
|
||||
setAbsoluteRangeDatePicker({
|
||||
id: 'global',
|
||||
from: new Date(min).toISOString(),
|
||||
to: new Date(max).toISOString(),
|
||||
});
|
||||
},
|
||||
[setAbsoluteRangeDatePicker]
|
||||
);
|
||||
|
||||
const tabProps = {
|
||||
deleteQuery,
|
||||
endDate: to,
|
||||
filterQuery,
|
||||
indexNames,
|
||||
skip: isInitializing || filterQuery === undefined,
|
||||
setQuery,
|
||||
startDate: from,
|
||||
type,
|
||||
narrowDateRange,
|
||||
updateDateRange,
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`${USERS_PATH}/:tabName(${UsersTableType.allUsers})`}>
|
||||
<AllUsersQueryTabBody
|
||||
deleteQuery={deleteQuery}
|
||||
endDate={to}
|
||||
filterQuery={filterQuery}
|
||||
indexNames={indexNames}
|
||||
skip={isInitializing || filterQuery === undefined}
|
||||
setQuery={setQuery}
|
||||
startDate={from}
|
||||
type={type}
|
||||
docValueFields={docValueFields}
|
||||
/>
|
||||
<AllUsersQueryTabBody {...tabProps} />
|
||||
</Route>
|
||||
<Route path={`${USERS_PATH}/:tabName(${UsersTableType.anomalies})`}>
|
||||
<AnomaliesQueryTabBody {...tabProps} AnomaliesTableComponent={AnomaliesUserTable} />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ export enum UsersType {
|
|||
|
||||
export enum UsersTableType {
|
||||
allUsers = 'allUsers',
|
||||
anomalies = 'anomalies',
|
||||
}
|
||||
|
||||
export type AllUsersTables = UsersTableType;
|
||||
|
@ -32,6 +33,7 @@ export interface TableUpdates {
|
|||
|
||||
export interface UsersQueries {
|
||||
[UsersTableType.allUsers]: AllUsersQuery;
|
||||
[UsersTableType.anomalies]: null | undefined;
|
||||
}
|
||||
|
||||
export interface UsersPageModel {
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from './actions';
|
||||
import { setUsersPageQueriesActivePageToZero } from './helpers';
|
||||
import { UsersTableType, UsersModel } from './model';
|
||||
import { HostsTableType } from '../../hosts/store/model';
|
||||
|
||||
export const initialUsersState: UsersModel = {
|
||||
page: {
|
||||
|
@ -24,12 +25,8 @@ export const initialUsersState: UsersModel = {
|
|||
[UsersTableType.allUsers]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
// TODO Fix me
|
||||
// sort: {
|
||||
// field: AllUsersFields.allUsers,
|
||||
// direction: Direction.desc,
|
||||
// },
|
||||
},
|
||||
[HostsTableType.anomalies]: null,
|
||||
},
|
||||
},
|
||||
details: {
|
||||
|
@ -37,12 +34,8 @@ export const initialUsersState: UsersModel = {
|
|||
[UsersTableType.allUsers]: {
|
||||
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
|
||||
limit: DEFAULT_TABLE_LIMIT,
|
||||
// TODO Fix me
|
||||
// sort: {
|
||||
// field: HostRulesFields.riskScore,
|
||||
// direction: Direction.desc,
|
||||
// },
|
||||
},
|
||||
[HostsTableType.anomalies]: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -75,7 +68,6 @@ export const usersReducer = reducerWithInitialState(initialUsersState)
|
|||
queries: {
|
||||
...state[usersType].queries,
|
||||
[tableType]: {
|
||||
// TODO: Steph/users fix active page/limit on users tables. is broken because multiple UsersTableType.userRules tables
|
||||
...state[usersType].queries[tableType],
|
||||
activePage,
|
||||
},
|
||||
|
@ -89,7 +81,6 @@ export const usersReducer = reducerWithInitialState(initialUsersState)
|
|||
queries: {
|
||||
...state[usersType].queries,
|
||||
[tableType]: {
|
||||
// TODO: Steph/users fix active page/limit on users tables. is broken because multiple UsersTableType.userRules tables
|
||||
...state[usersType].queries[tableType],
|
||||
limit,
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue