mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[SIEM] Adds Machine Learning table anomalies, a pop over UI for anomalies, and machine learning details score (#39483)
## Summary [SIEM] Adds Machine Learning table anomalies, a pop over UI for anomalies, and machine learning details score. Works on both the hosts page and the network page as well as the details sections of both those pages. Table <img width="760" alt="Screen Shot 2019-06-28 at 11 09 39 AM" src="https://user-images.githubusercontent.com/1151048/60359902-96c33e00-9997-11e9-8a0a-92fb8e7d808a.png"> Popover <img width="764" alt="Screen Shot 2019-06-28 at 11 09 50 AM" src="https://user-images.githubusercontent.com/1151048/60359909-9cb91f00-9997-11e9-96e0-f51ec33cee6f.png"> Details with Popover: <img width="800" alt="Screen Shot 2019-06-28 at 11 12 14 AM" src="https://user-images.githubusercontent.com/1151048/60359955-bce8de00-9997-11e9-8154-6165b21f25f7.png"> ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [x] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist) ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
This commit is contained in:
parent
af56c9b732
commit
2bf06185da
78 changed files with 4634 additions and 306 deletions
|
@ -4,8 +4,16 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export type Pick3<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]> = {
|
||||
[P1 in K1]: { [P2 in K2]: { [P3 in K3]: ((T[K1])[K2])[P3] } }
|
||||
};
|
||||
|
||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
// This type is for typing EuiDescriptionList
|
||||
export interface DescriptionList {
|
||||
title: NonNullable<ReactNode>;
|
||||
description: NonNullable<ReactNode>;
|
||||
}
|
||||
|
|
|
@ -99,14 +99,16 @@ interface BasicTableState {
|
|||
paginationLoading: boolean;
|
||||
}
|
||||
|
||||
export interface Columns<T> {
|
||||
type Func<T> = (arg: T) => string | number;
|
||||
|
||||
export interface Columns<T, U = T> {
|
||||
field?: string;
|
||||
name: string | React.ReactNode;
|
||||
isMobileHeader?: boolean;
|
||||
sortable?: boolean;
|
||||
sortable?: boolean | Func<T>;
|
||||
truncateText?: boolean;
|
||||
hideForMobile?: boolean;
|
||||
render?: (item: T) => void;
|
||||
render?: (item: T, node: U) => void;
|
||||
width?: string;
|
||||
}
|
||||
|
||||
|
@ -313,7 +315,7 @@ const FooterAction = styled.div`
|
|||
* The getOr is just there to simplify the test
|
||||
* So we do NOT need to wrap it around TestProvider
|
||||
*/
|
||||
const BackgroundRefetch = styled.div`
|
||||
export const BackgroundRefetch = styled.div`
|
||||
background-color: ${props => getOr('#ffffff', 'theme.eui.euiColorLightShade', props)};
|
||||
margin: -5px;
|
||||
height: calc(100% + 10px);
|
||||
|
|
23
x-pack/legacy/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap
generated
Normal file
23
x-pack/legacy/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`create_influencers renders correctly against snapshot 1`] = `
|
||||
<Connect(DraggableWrapperComponent)
|
||||
dataProvider={
|
||||
Object {
|
||||
"and": Array [],
|
||||
"enabled": true,
|
||||
"excluded": false,
|
||||
"id": "id-prefix-entity-name-entity-value",
|
||||
"kqlQuery": "",
|
||||
"name": "entity-value",
|
||||
"queryMatch": Object {
|
||||
"field": "entity-name",
|
||||
"operator": ":",
|
||||
"value": "entity-value",
|
||||
},
|
||||
}
|
||||
}
|
||||
key="id-prefix-entity-name-entity-value"
|
||||
render={[Function]}
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { InfluencerInput, Anomalies } from '../types';
|
||||
import { useAnomaliesTableData } from './use_anomalies_table_data';
|
||||
|
||||
interface ChildrenArgs {
|
||||
isLoadingAnomaliesData: boolean;
|
||||
anomaliesData: Anomalies | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
influencers: InfluencerInput[] | null;
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
children: (args: ChildrenArgs) => React.ReactNode;
|
||||
}
|
||||
|
||||
export const AnomalyTableProvider = React.memo<Props>(
|
||||
({ influencers, startDate, endDate, children }) => {
|
||||
const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({
|
||||
influencers,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
return <>{children({ isLoadingAnomaliesData, anomaliesData })}</>;
|
||||
}
|
||||
);
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { getIntervalFromAnomalies } from './get_interval_from_anomalies';
|
||||
import { mockAnomalies } from '../mock';
|
||||
|
||||
describe('get_interval_from_anomalies', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('returns "day" if anomalies is null', () => {
|
||||
const interval = getIntervalFromAnomalies(null);
|
||||
expect(interval).toEqual('day');
|
||||
});
|
||||
|
||||
test('returns normal interval from the mocks', () => {
|
||||
anomalies.interval = 'month';
|
||||
const interval = getIntervalFromAnomalies(anomalies);
|
||||
expect(interval).toEqual('month');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Anomalies } from '../types';
|
||||
|
||||
export const getIntervalFromAnomalies = (anomalies: Anomalies | null) => {
|
||||
if (anomalies == null) {
|
||||
return 'day';
|
||||
} else {
|
||||
return anomalies.interval;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { getSizeFromAnomalies } from './get_size_from_anomalies';
|
||||
import { mockAnomalies } from '../mock';
|
||||
|
||||
describe('get_size_from_anomalies', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('returns 0 if anomalies is null', () => {
|
||||
const size = getSizeFromAnomalies(null);
|
||||
expect(size).toEqual(0);
|
||||
});
|
||||
|
||||
test('returns anomalies length', () => {
|
||||
const size = getSizeFromAnomalies(anomalies);
|
||||
expect(size).toEqual(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Anomalies } from '../types';
|
||||
|
||||
export const getSizeFromAnomalies = (anomalies: Anomalies | null): number => {
|
||||
if (anomalies == null) {
|
||||
return 0;
|
||||
} else {
|
||||
return anomalies.anomalies.length;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { InfluencerInput } from '../types';
|
||||
import { influencersToString } from './use_anomalies_table_data';
|
||||
|
||||
describe('use_anomalies_table_data', () => {
|
||||
test('should return a reduced single influencer to string', () => {
|
||||
const influencers: InfluencerInput[] = [
|
||||
{
|
||||
fieldName: 'field-1',
|
||||
fieldValue: 'value-1',
|
||||
},
|
||||
];
|
||||
const influencerString = influencersToString(influencers);
|
||||
expect(influencerString).toEqual('field-1:value-1');
|
||||
});
|
||||
|
||||
test('should return a two single influencers together in a string', () => {
|
||||
const influencers: InfluencerInput[] = [
|
||||
{
|
||||
fieldName: 'field-1',
|
||||
fieldValue: 'value-1',
|
||||
},
|
||||
{
|
||||
fieldName: 'field-2',
|
||||
fieldValue: 'value-2',
|
||||
},
|
||||
];
|
||||
const influencerString = influencersToString(influencers);
|
||||
expect(influencerString).toEqual('field-1:value-1field-2:value-2');
|
||||
});
|
||||
|
||||
test('should return an empty string when the array is empty', () => {
|
||||
const influencers: InfluencerInput[] = [];
|
||||
const influencerString = influencersToString(influencers);
|
||||
expect(influencerString).toEqual('');
|
||||
});
|
||||
|
||||
test('should return an empty string when passed null', () => {
|
||||
const influencerString = influencersToString(null);
|
||||
expect(influencerString).toEqual('');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
import { anomaliesTableData } from '../api/anomalies_table_data';
|
||||
import { InfluencerInput, Anomalies } from '../types';
|
||||
import {
|
||||
KibanaConfigContext,
|
||||
AppKibanaFrameworkAdapter,
|
||||
} from '../../../lib/adapters/framework/kibana_framework_adapter';
|
||||
|
||||
interface Args {
|
||||
influencers: InfluencerInput[] | null;
|
||||
endDate: number;
|
||||
startDate: number;
|
||||
threshold?: number;
|
||||
skip?: boolean;
|
||||
}
|
||||
|
||||
type Return = [boolean, Anomalies | null];
|
||||
|
||||
export const influencersToString = (influencers: InfluencerInput[] | null): string =>
|
||||
influencers == null
|
||||
? ''
|
||||
: influencers.reduce((accum, item) => `${accum}${item.fieldName}:${item.fieldValue}`, '');
|
||||
|
||||
export const getTimeZone = (config: Partial<AppKibanaFrameworkAdapter>): string => {
|
||||
if (config.dateFormatTz !== 'Browser' && config.dateFormatTz != null) {
|
||||
return config.dateFormatTz;
|
||||
} else if (config.dateFormatTz === 'Browser' && config.timezone != null) {
|
||||
return config.timezone;
|
||||
} else {
|
||||
return moment.tz.guess();
|
||||
}
|
||||
};
|
||||
|
||||
export const useAnomaliesTableData = ({
|
||||
influencers,
|
||||
startDate,
|
||||
endDate,
|
||||
threshold = 0,
|
||||
skip = false,
|
||||
}: Args): Return => {
|
||||
const [tableData, setTableData] = useState<Anomalies | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const config = useContext(KibanaConfigContext);
|
||||
|
||||
const fetchFunc = async (
|
||||
influencersInput: InfluencerInput[] | null,
|
||||
earliestMs: number,
|
||||
latestMs: number
|
||||
) => {
|
||||
if (influencersInput != null && !skip) {
|
||||
const data = await anomaliesTableData(
|
||||
{
|
||||
jobIds: [],
|
||||
criteriaFields: [],
|
||||
aggregationInterval: 'auto',
|
||||
threshold,
|
||||
earliestMs,
|
||||
latestMs,
|
||||
influencers: influencersInput,
|
||||
dateFormatTz: getTimeZone(config),
|
||||
maxRecords: 500,
|
||||
maxExamples: 10,
|
||||
},
|
||||
{
|
||||
'kbn-version': config.kbnVersion,
|
||||
}
|
||||
);
|
||||
setTableData(data);
|
||||
setLoading(false);
|
||||
} else {
|
||||
setTableData(null);
|
||||
setLoading(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setLoading(true);
|
||||
fetchFunc(influencers, startDate, endDate);
|
||||
},
|
||||
[influencersToString(influencers), startDate, endDate, skip]
|
||||
);
|
||||
|
||||
return [loading, tableData];
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import chrome from 'ui/chrome';
|
||||
import { Anomalies, InfluencerInput } from '../types';
|
||||
|
||||
export interface Body {
|
||||
jobIds: string[];
|
||||
criteriaFields: string[];
|
||||
influencers: InfluencerInput[];
|
||||
aggregationInterval: string;
|
||||
threshold: number;
|
||||
earliestMs: number;
|
||||
latestMs: number;
|
||||
dateFormatTz: string;
|
||||
maxRecords: number;
|
||||
maxExamples: number;
|
||||
}
|
||||
|
||||
export const anomaliesTableData = async (
|
||||
body: Body,
|
||||
headers: Record<string, string | undefined>
|
||||
): Promise<Anomalies> => {
|
||||
try {
|
||||
const response = await fetch('/api/ml/results/anomalies_table_data', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'kbn-system-api': 'true',
|
||||
'content-Type': 'application/json',
|
||||
'kbn-xsrf': chrome.getXsrfToken(),
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
// TODO: Toaster error when this happens instead of returning empty data
|
||||
const empty: Anomalies = {
|
||||
anomalies: [],
|
||||
interval: 'second',
|
||||
};
|
||||
return empty;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { EntityDraggable } from './entity_draggable';
|
||||
import { TestProviders } from '../../mock/test_providers';
|
||||
|
||||
describe('create_influencers', () => {
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<EntityDraggable idPrefix="id-prefix" entityName="entity-name" entityValue="entity-value" />
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders with entity name with entity value as text', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EntityDraggable idPrefix="id-prefix" entityName="entity-name" entityValue="entity-value" />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual('entity-name: “entity-value”');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { DraggableWrapper, DragEffects } from '../drag_and_drop/draggable_wrapper';
|
||||
import { IS_OPERATOR } from '../timeline/data_providers/data_provider';
|
||||
import { Provider } from '../timeline/data_providers/provider';
|
||||
import { escapeDataProviderId } from '../drag_and_drop/helpers';
|
||||
|
||||
interface Props {
|
||||
idPrefix: string;
|
||||
entityName: string;
|
||||
entityValue: string;
|
||||
}
|
||||
|
||||
export const EntityDraggable = React.memo<Props>(
|
||||
({ idPrefix, entityName, entityValue }): JSX.Element => {
|
||||
const id = escapeDataProviderId(`${idPrefix}-${entityName}-${entityValue}`);
|
||||
return (
|
||||
<DraggableWrapper
|
||||
key={id}
|
||||
dataProvider={{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id,
|
||||
name: entityValue,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: entityName,
|
||||
value: entityValue,
|
||||
operator: IS_OPERATOR,
|
||||
},
|
||||
}}
|
||||
render={(dataProvider, _, snapshot) =>
|
||||
snapshot.isDragging ? (
|
||||
<DragEffects>
|
||||
<Provider dataProvider={dataProvider} />
|
||||
</DragEffects>
|
||||
) : (
|
||||
<>{`${entityName}: “${entityValue}”`}</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`create_influencers renders correctly against snapshot 1`] = `
|
||||
<span>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="host.name: “zeek-iowa”"
|
||||
>
|
||||
host.name: “zeek-iowa”
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="process.name: “du”"
|
||||
>
|
||||
process.name: “du”
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="user.name: “root”"
|
||||
>
|
||||
user.name: “root”
|
||||
</EuiFlexItem>
|
||||
</span>
|
||||
`;
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import { createInfluencers, createKeyAndValue } from './create_influencers';
|
||||
|
||||
describe('create_influencers', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(<span>{createInfluencers(anomalies.anomalies[0])}</span>);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it returns expected createKeyAndValue record with special left and right quotes', () => {
|
||||
const entities = createKeyAndValue({ 'name-1': 'value-1' });
|
||||
expect(entities).toEqual('name-1: “value-1”');
|
||||
});
|
||||
|
||||
test('it returns expected createKeyAndValue record when empty object is passed', () => {
|
||||
const entities = createKeyAndValue({});
|
||||
expect(entities).toEqual('');
|
||||
});
|
||||
|
||||
test('it creates the anomalies without filtering anything out since they are all well formed', () => {
|
||||
const wrapper = mount(<span>{createInfluencers(anomalies.anomalies[0])}</span>);
|
||||
expect(wrapper.text()).toEqual('host.name: “zeek-iowa”process.name: “du”user.name: “root”');
|
||||
});
|
||||
|
||||
test('it returns empty text when passed in empty objects of influencers', () => {
|
||||
anomalies.anomalies[0].influencers = [{}, {}, {}];
|
||||
const wrapper = mount(<span>{createInfluencers(anomalies.anomalies[0])}</span>);
|
||||
expect(wrapper.text()).toEqual('');
|
||||
});
|
||||
|
||||
test('it filters out empty anomalies but keeps the others', () => {
|
||||
anomalies.anomalies[0].influencers = [
|
||||
{ 'influencer-name-one': 'influencer-value-one' },
|
||||
{},
|
||||
{ 'influencer-name-two': 'influencer-value-two' },
|
||||
];
|
||||
const wrapper = mount(<span>{createInfluencers(anomalies.anomalies[0])}</span>);
|
||||
expect(wrapper.text()).toEqual(
|
||||
'influencer-name-one: “influencer-value-one”influencer-name-two: “influencer-value-two”'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { Anomaly } from '../types';
|
||||
|
||||
export const createKeyAndValue = (influencer: Record<string, string>): string => {
|
||||
if (Object.keys(influencer)[0] != null && Object.values(influencer)[0] != null) {
|
||||
return `${Object.keys(influencer)[0]}: “${Object.values(influencer)[0]}”`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const createInfluencers = (score: Anomaly): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{score.influencers
|
||||
.filter(influencer => !isEmpty(influencer))
|
||||
.map(influencer => {
|
||||
const keyAndValue = createKeyAndValue(influencer);
|
||||
return (
|
||||
<EuiFlexItem key={keyAndValue} grow={false}>
|
||||
{keyAndValue}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(hostName).toEqual('name-1');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const getHostNameFromInfluencers = (
|
||||
influencers: Array<Record<string, string>>
|
||||
): string | null => {
|
||||
const recordFound = influencers.find(influencer => {
|
||||
const influencerName = Object.keys(influencer)[0];
|
||||
if (influencerName === 'host.name') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (recordFound != null) {
|
||||
return Object.values(recordFound)[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
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 the influencers is an empty object', () => {
|
||||
anomalies.anomalies[0].influencers = [{}];
|
||||
const network = getNetworkFromInfluencers(anomalies.anomalies[0].influencers);
|
||||
expect(network).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 expected: { ip: string; type: DestinationOrSource } = {
|
||||
ip: '127.0.0.1',
|
||||
type: 'source.ip',
|
||||
};
|
||||
expect(network).toEqual(expected);
|
||||
});
|
||||
|
||||
test('returns network name mixed with other data', () => {
|
||||
anomalies.anomalies[0].influencers = [
|
||||
{ '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',
|
||||
};
|
||||
expect(network).toEqual(expected);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { DestinationOrSource } from '../types';
|
||||
|
||||
export const getNetworkFromInfluencers = (
|
||||
influencers: Array<Record<string, string>>
|
||||
): { ip: string; type: DestinationOrSource } | null => {
|
||||
const recordFound = influencers.find(influencer => {
|
||||
const influencerName = Object.keys(influencer)[0];
|
||||
if (influencerName === 'destination.ip' || influencerName === 'source.ip') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (recordFound != null) {
|
||||
const influencerName = Object.keys(recordFound)[0];
|
||||
if (influencerName === 'destination.ip' || influencerName === 'source.ip') {
|
||||
return { ip: Object.values(recordFound)[0], type: influencerName };
|
||||
} else {
|
||||
// default to destination.ip
|
||||
return { ip: Object.values(recordFound)[0], type: 'destination.ip' };
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { HostItem } from '../../../graphql/types';
|
||||
import { InfluencerInput } from '../types';
|
||||
import { hostToInfluencers } from './host_to_influencers';
|
||||
|
||||
describe('host_to_influencer', () => {
|
||||
test('converts a host to an influencer', () => {
|
||||
const hostItem: HostItem = {
|
||||
host: {
|
||||
name: ['host-name'],
|
||||
},
|
||||
};
|
||||
const expectedInfluencer: InfluencerInput[] = [
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
fieldValue: 'host-name',
|
||||
},
|
||||
];
|
||||
expect(hostToInfluencers(hostItem)).toEqual(expectedInfluencer);
|
||||
});
|
||||
|
||||
test('returns a null if the host.name is null', () => {
|
||||
const hostItem: HostItem = {
|
||||
host: {
|
||||
name: null,
|
||||
},
|
||||
};
|
||||
expect(hostToInfluencers(hostItem)).toEqual(null);
|
||||
});
|
||||
|
||||
test('returns a null if the host is null', () => {
|
||||
const hostItem: HostItem = {
|
||||
host: null,
|
||||
};
|
||||
expect(hostToInfluencers(hostItem)).toEqual(null);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { InfluencerInput } from '../types';
|
||||
import { HostItem } from '../../../graphql/types';
|
||||
|
||||
export const hostToInfluencers = (hostItem: HostItem): InfluencerInput[] | null => {
|
||||
if (hostItem.host != null && hostItem.host.name != null) {
|
||||
const influencers: InfluencerInput[] = [
|
||||
{
|
||||
fieldName: 'host.name',
|
||||
fieldValue: hostItem.host.name[0],
|
||||
},
|
||||
];
|
||||
return influencers;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { InfluencerInput } from '../types';
|
||||
import { networkToInfluencers } from './network_to_influencers';
|
||||
|
||||
describe('network_to_influencers', () => {
|
||||
test('converts a network to an influencer', () => {
|
||||
const expectedInfluencer: InfluencerInput[] = [
|
||||
{
|
||||
fieldName: 'source.ip',
|
||||
fieldValue: '127.0.0.1',
|
||||
},
|
||||
{
|
||||
fieldName: 'destination.ip',
|
||||
fieldValue: '127.0.0.1',
|
||||
},
|
||||
];
|
||||
expect(networkToInfluencers('127.0.0.1')).toEqual(expectedInfluencer);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { InfluencerInput } from '../types';
|
||||
|
||||
export const networkToInfluencers = (ip: string): InfluencerInput[] => {
|
||||
const influencers: InfluencerInput[] = [
|
||||
{
|
||||
fieldName: 'source.ip',
|
||||
fieldValue: ip,
|
||||
},
|
||||
{
|
||||
fieldName: 'destination.ip',
|
||||
fieldValue: ip,
|
||||
},
|
||||
];
|
||||
return influencers;
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { createExplorerLink } from './create_explorer_link';
|
||||
|
||||
describe('create_explorer_link', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('it returns expected link', () => {
|
||||
const entities = createExplorerLink(
|
||||
anomalies.anomalies[0],
|
||||
new Date('1970').valueOf(),
|
||||
new Date('3000').valueOf()
|
||||
);
|
||||
expect(entities).toEqual(
|
||||
"ml#/explorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Anomaly } from '../types';
|
||||
|
||||
export const createExplorerLink = (score: Anomaly, startDate: number, endDate: number): string => {
|
||||
const startDateIso = new Date(startDate).toISOString();
|
||||
const endDateIso = new Date(endDate).toISOString();
|
||||
|
||||
const JOB_PREFIX = `ml#/explorer?_g=(ml:(jobIds:!(${score.jobId}))`;
|
||||
const REFRESH_INTERVAL = `,refreshInterval:(display:Off,pause:!f,value:0),time:(from:'${startDateIso}',mode:absolute,to:'${endDateIso}'))`;
|
||||
const INTERVAL_SELECTION = `&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)`;
|
||||
|
||||
return `${JOB_PREFIX}${REFRESH_INTERVAL}${INTERVAL_SELECTION}`;
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { createSeriesLink } from './create_series_link';
|
||||
|
||||
describe('create_series_link', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('it returns expected createKeyAndValue record with special left and right quotes', () => {
|
||||
const entities = createSeriesLink(
|
||||
anomalies.anomalies[0],
|
||||
new Date('1970').valueOf(),
|
||||
new Date('3000').valueOf()
|
||||
);
|
||||
expect(entities).toEqual(
|
||||
"ml#/timeseriesexplorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlSelectInterval:(display:Auto,val:auto),mlSelectSeverity:(color:%23d2e9f7,display:warning,val:0),mlTimeSeriesExplorer:(detectorIndex:0,entities:(host.name:'zeek-iowa',process.name:'du',user.name:'root')))"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createEntitiesFromScore } from '../score/create_entities_from_score';
|
||||
import { Anomaly } from '../types';
|
||||
|
||||
export const createSeriesLink = (score: Anomaly, startDate: number, endDate: number): string => {
|
||||
const startDateIso = new Date(startDate).toISOString();
|
||||
const endDateIso = new Date(endDate).toISOString();
|
||||
|
||||
const JOB_PREFIX = `ml#/timeseriesexplorer?_g=(ml:(jobIds:!(${score.jobId}))`;
|
||||
const REFRESH_INTERVAL = `,refreshInterval:(display:Off,pause:!f,value:0),time:(from:'${startDateIso}',mode:absolute,to:'${endDateIso}'))`;
|
||||
const INTERVAL_SELECTION = `&_a=(mlSelectInterval:(display:Auto,val:auto),mlSelectSeverity:(color:%23d2e9f7,display:warning,val:0),mlTimeSeriesExplorer:(detectorIndex:0,`;
|
||||
const ENTITIES = `entities:(${createEntitiesFromScore(score)})))`;
|
||||
|
||||
return `${JOB_PREFIX}${REFRESH_INTERVAL}${INTERVAL_SELECTION}${ENTITIES}`;
|
||||
};
|
121
x-pack/legacy/plugins/siem/public/components/ml/mock.ts
Normal file
121
x-pack/legacy/plugins/siem/public/components/ml/mock.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Anomalies } from './types';
|
||||
|
||||
export const mockAnomalies: Anomalies = {
|
||||
anomalies: [
|
||||
{
|
||||
time: new Date('2019-06-16T06:00:00.000Z').valueOf(),
|
||||
source: {
|
||||
job_id: 'job-1',
|
||||
result_type: 'record',
|
||||
probability: 0.024041164411288146,
|
||||
multi_bucket_impact: 0,
|
||||
record_score: 16.193669439507826,
|
||||
initial_record_score: 16.193669439507826,
|
||||
bucket_span: 900,
|
||||
detector_index: 0,
|
||||
is_interim: false,
|
||||
timestamp: new Date('2019-06-16T06:00:00.000Z').valueOf(),
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'du',
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
typical: [0.024041164411288146],
|
||||
actual: [1],
|
||||
influencers: [
|
||||
{
|
||||
influencer_field_name: 'user.name',
|
||||
influencer_field_values: ['root'],
|
||||
},
|
||||
{
|
||||
influencer_field_name: 'process.name',
|
||||
influencer_field_values: ['du'],
|
||||
},
|
||||
{
|
||||
influencer_field_name: 'host.name',
|
||||
influencer_field_values: ['zeek-iowa'],
|
||||
},
|
||||
],
|
||||
},
|
||||
rowId: '1561157194802_0',
|
||||
jobId: 'job-1',
|
||||
detectorIndex: 0,
|
||||
severity: 16.193669439507826,
|
||||
entityName: 'process.name',
|
||||
entityValue: 'du',
|
||||
influencers: [
|
||||
{
|
||||
'host.name': 'zeek-iowa',
|
||||
},
|
||||
{
|
||||
'process.name': 'du',
|
||||
},
|
||||
{
|
||||
'user.name': 'root',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
time: new Date('2019-06-16T06:00:00.000Z').valueOf(),
|
||||
source: {
|
||||
job_id: 'job-2',
|
||||
result_type: 'record',
|
||||
probability: 0.024041164411288146,
|
||||
multi_bucket_impact: 0,
|
||||
record_score: 16.193669439507826,
|
||||
initial_record_score: 16.193669439507826,
|
||||
bucket_span: 900,
|
||||
detector_index: 0,
|
||||
is_interim: false,
|
||||
timestamp: new Date('2019-06-16T06:00:00.000Z').valueOf(),
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'ls',
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
typical: [0.024041164411288146],
|
||||
actual: [1],
|
||||
influencers: [
|
||||
{
|
||||
influencer_field_name: 'user.name',
|
||||
influencer_field_values: ['root'],
|
||||
},
|
||||
{
|
||||
influencer_field_name: 'process.name',
|
||||
influencer_field_values: ['ls'],
|
||||
},
|
||||
{
|
||||
influencer_field_name: 'host.name',
|
||||
influencer_field_values: ['zeek-iowa'],
|
||||
},
|
||||
],
|
||||
},
|
||||
rowId: '1561157194802_1',
|
||||
jobId: 'job-2',
|
||||
detectorIndex: 0,
|
||||
severity: 16.193669439507826,
|
||||
entityName: 'process.name',
|
||||
entityValue: 'ls',
|
||||
influencers: [
|
||||
{
|
||||
'host.name': 'zeek-iowa',
|
||||
},
|
||||
{
|
||||
'process.name': 'ls',
|
||||
},
|
||||
{
|
||||
'user.name': 'root',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
interval: 'day',
|
||||
};
|
219
x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap
generated
Normal file
219
x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,219 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`anomaly_scores renders correctly against snapshot 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<Memo()
|
||||
id="anomaly-scores-job-key-1"
|
||||
index={0}
|
||||
score={
|
||||
Object {
|
||||
"detectorIndex": 0,
|
||||
"entityName": "process.name",
|
||||
"entityValue": "du",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"host.name": "zeek-iowa",
|
||||
},
|
||||
Object {
|
||||
"process.name": "du",
|
||||
},
|
||||
Object {
|
||||
"user.name": "root",
|
||||
},
|
||||
],
|
||||
"jobId": "job-1",
|
||||
"rowId": "1561157194802_0",
|
||||
"severity": 16.193669439507826,
|
||||
"source": Object {
|
||||
"actual": Array [
|
||||
1,
|
||||
],
|
||||
"bucket_span": 900,
|
||||
"by_field_name": "process.name",
|
||||
"by_field_value": "du",
|
||||
"detector_index": 0,
|
||||
"function": "rare",
|
||||
"function_description": "rare",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"influencer_field_name": "user.name",
|
||||
"influencer_field_values": Array [
|
||||
"root",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "process.name",
|
||||
"influencer_field_values": Array [
|
||||
"du",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "host.name",
|
||||
"influencer_field_values": Array [
|
||||
"zeek-iowa",
|
||||
],
|
||||
},
|
||||
],
|
||||
"initial_record_score": 16.193669439507826,
|
||||
"is_interim": false,
|
||||
"job_id": "job-1",
|
||||
"multi_bucket_impact": 0,
|
||||
"partition_field_name": "host.name",
|
||||
"partition_field_value": "zeek-iowa",
|
||||
"probability": 0.024041164411288146,
|
||||
"record_score": 16.193669439507826,
|
||||
"result_type": "record",
|
||||
"timestamp": 1560664800000,
|
||||
"typical": Array [
|
||||
0.024041164411288146,
|
||||
],
|
||||
},
|
||||
"time": 1560664800000,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
<Styled(EuiIcon)
|
||||
type="iInCircle"
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
data-test-subj="anomaly-score-popover"
|
||||
hasArrow={true}
|
||||
id="anomaly-score-popover"
|
||||
isOpen={false}
|
||||
onClick={[Function]}
|
||||
ownFocus={false}
|
||||
panelPaddingSize="m"
|
||||
>
|
||||
<EuiDescriptionList
|
||||
data-test-subj="anomaly-description-list"
|
||||
listItems={
|
||||
Array [
|
||||
Object {
|
||||
"description": <React.Fragment>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<Styled(EuiText)>
|
||||
17
|
||||
</Styled(EuiText)>
|
||||
</React.Fragment>,
|
||||
"title": "Max Anomaly Score",
|
||||
},
|
||||
Object {
|
||||
"description": <EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
job-1
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
href="ml#/explorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)"
|
||||
target="_blank"
|
||||
type="button"
|
||||
>
|
||||
View in Machine Learning
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
"title": <React.Fragment>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
Anomaly Job
|
||||
</React.Fragment>,
|
||||
},
|
||||
Object {
|
||||
"description": <EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<pure(Component)
|
||||
value={2019-06-16T06:00:00.000Z}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
data-test-subj="anomaly-description-narrow-range-link"
|
||||
onClick={[Function]}
|
||||
target="_blank"
|
||||
type="button"
|
||||
>
|
||||
Narrow to this date range
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
"title": "Detected",
|
||||
},
|
||||
Object {
|
||||
"description": <EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
process.name: “du”
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>,
|
||||
"title": "Top Anomaly Suspect",
|
||||
},
|
||||
Object {
|
||||
"description": <EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<React.Fragment>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
host.name: “zeek-iowa”
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
process.name: “du”
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
user.name: “root”
|
||||
</EuiFlexItem>
|
||||
</React.Fragment>
|
||||
</EuiFlexGroup>,
|
||||
"title": "Influenced By",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
`;
|
161
x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap
generated
Normal file
161
x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,161 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`anomaly_scores renders correctly against snapshot 1`] = `
|
||||
<Fragment>
|
||||
<EuiFlexGroup
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<Memo()
|
||||
endDate={32503680000000}
|
||||
index={0}
|
||||
interval="day"
|
||||
jobKey="job-1-16.193669439507826-process.name-du"
|
||||
key="job-1-16.193669439507826-process.name-du"
|
||||
narrowDateRange={[MockFunction]}
|
||||
score={
|
||||
Object {
|
||||
"detectorIndex": 0,
|
||||
"entityName": "process.name",
|
||||
"entityValue": "du",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"host.name": "zeek-iowa",
|
||||
},
|
||||
Object {
|
||||
"process.name": "du",
|
||||
},
|
||||
Object {
|
||||
"user.name": "root",
|
||||
},
|
||||
],
|
||||
"jobId": "job-1",
|
||||
"rowId": "1561157194802_0",
|
||||
"severity": 16.193669439507826,
|
||||
"source": Object {
|
||||
"actual": Array [
|
||||
1,
|
||||
],
|
||||
"bucket_span": 900,
|
||||
"by_field_name": "process.name",
|
||||
"by_field_value": "du",
|
||||
"detector_index": 0,
|
||||
"function": "rare",
|
||||
"function_description": "rare",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"influencer_field_name": "user.name",
|
||||
"influencer_field_values": Array [
|
||||
"root",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "process.name",
|
||||
"influencer_field_values": Array [
|
||||
"du",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "host.name",
|
||||
"influencer_field_values": Array [
|
||||
"zeek-iowa",
|
||||
],
|
||||
},
|
||||
],
|
||||
"initial_record_score": 16.193669439507826,
|
||||
"is_interim": false,
|
||||
"job_id": "job-1",
|
||||
"multi_bucket_impact": 0,
|
||||
"partition_field_name": "host.name",
|
||||
"partition_field_value": "zeek-iowa",
|
||||
"probability": 0.024041164411288146,
|
||||
"record_score": 16.193669439507826,
|
||||
"result_type": "record",
|
||||
"timestamp": 1560664800000,
|
||||
"typical": Array [
|
||||
0.024041164411288146,
|
||||
],
|
||||
},
|
||||
"time": 1560664800000,
|
||||
}
|
||||
}
|
||||
startDate={0}
|
||||
/>
|
||||
<Memo()
|
||||
endDate={32503680000000}
|
||||
index={1}
|
||||
interval="day"
|
||||
jobKey="job-2-16.193669439507826-process.name-ls"
|
||||
key="job-2-16.193669439507826-process.name-ls"
|
||||
narrowDateRange={[MockFunction]}
|
||||
score={
|
||||
Object {
|
||||
"detectorIndex": 0,
|
||||
"entityName": "process.name",
|
||||
"entityValue": "ls",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"host.name": "zeek-iowa",
|
||||
},
|
||||
Object {
|
||||
"process.name": "ls",
|
||||
},
|
||||
Object {
|
||||
"user.name": "root",
|
||||
},
|
||||
],
|
||||
"jobId": "job-2",
|
||||
"rowId": "1561157194802_1",
|
||||
"severity": 16.193669439507826,
|
||||
"source": Object {
|
||||
"actual": Array [
|
||||
1,
|
||||
],
|
||||
"bucket_span": 900,
|
||||
"by_field_name": "process.name",
|
||||
"by_field_value": "ls",
|
||||
"detector_index": 0,
|
||||
"function": "rare",
|
||||
"function_description": "rare",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"influencer_field_name": "user.name",
|
||||
"influencer_field_values": Array [
|
||||
"root",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "process.name",
|
||||
"influencer_field_values": Array [
|
||||
"ls",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "host.name",
|
||||
"influencer_field_values": Array [
|
||||
"zeek-iowa",
|
||||
],
|
||||
},
|
||||
],
|
||||
"initial_record_score": 16.193669439507826,
|
||||
"is_interim": false,
|
||||
"job_id": "job-2",
|
||||
"multi_bucket_impact": 0,
|
||||
"partition_field_name": "host.name",
|
||||
"partition_field_value": "zeek-iowa",
|
||||
"probability": 0.024041164411288146,
|
||||
"record_score": 16.193669439507826,
|
||||
"result_type": "record",
|
||||
"timestamp": 1560664800000,
|
||||
"typical": Array [
|
||||
0.024041164411288146,
|
||||
],
|
||||
},
|
||||
"time": 1560664800000,
|
||||
}
|
||||
}
|
||||
startDate={0}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
`;
|
|
@ -0,0 +1,146 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`create_description_list renders correctly against snapshot 1`] = `
|
||||
<dl
|
||||
className="euiDescriptionList euiDescriptionList--row"
|
||||
>
|
||||
<EuiDescriptionListTitle
|
||||
key="title-0"
|
||||
>
|
||||
Max Anomaly Score
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription
|
||||
key="description-0"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
<Styled(EuiText)>
|
||||
17
|
||||
</Styled(EuiText)>
|
||||
</EuiDescriptionListDescription>
|
||||
<EuiDescriptionListTitle
|
||||
key="title-1"
|
||||
>
|
||||
<EuiSpacer
|
||||
size="m"
|
||||
/>
|
||||
Anomaly Job
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription
|
||||
key="description-1"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
job-1
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
href="ml#/explorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)"
|
||||
target="_blank"
|
||||
type="button"
|
||||
>
|
||||
View in Machine Learning
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDescriptionListDescription>
|
||||
<EuiDescriptionListTitle
|
||||
key="title-2"
|
||||
>
|
||||
Detected
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription
|
||||
key="description-2"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<pure(Component)
|
||||
value={2019-06-16T06:00:00.000Z}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
data-test-subj="anomaly-description-narrow-range-link"
|
||||
onClick={[Function]}
|
||||
target="_blank"
|
||||
type="button"
|
||||
>
|
||||
Narrow to this date range
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDescriptionListDescription>
|
||||
<EuiDescriptionListTitle
|
||||
key="title-3"
|
||||
>
|
||||
Top Anomaly Suspect
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription
|
||||
key="description-3"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
process.name: “du”
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDescriptionListDescription>
|
||||
<EuiDescriptionListTitle
|
||||
key="title-4"
|
||||
>
|
||||
Influenced By
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription
|
||||
key="description-4"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="host.name: “zeek-iowa”"
|
||||
>
|
||||
host.name: “zeek-iowa”
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="process.name: “du”"
|
||||
>
|
||||
process.name: “du”
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
key="user.name: “root”"
|
||||
>
|
||||
user.name: “root”
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiDescriptionListDescription>
|
||||
</dl>
|
||||
`;
|
23
x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap
generated
Normal file
23
x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap
generated
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`draggable_score renders correctly against snapshot 1`] = `
|
||||
<Connect(DraggableWrapperComponent)
|
||||
dataProvider={
|
||||
Object {
|
||||
"and": Array [],
|
||||
"enabled": true,
|
||||
"excluded": false,
|
||||
"id": "some-id",
|
||||
"kqlQuery": "",
|
||||
"name": "process.name",
|
||||
"queryMatch": Object {
|
||||
"field": "process.name",
|
||||
"operator": ":",
|
||||
"value": "du",
|
||||
},
|
||||
}
|
||||
}
|
||||
key="some-id"
|
||||
render={[Function]}
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import * as React from 'react';
|
||||
import { AnomalyScore } from './anomaly_score';
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { TestProviders } from '../../../mock/test_providers';
|
||||
import { Anomalies } from '../types';
|
||||
|
||||
const endDate: number = new Date('3000-01-01T00:00:00.000Z').valueOf();
|
||||
const narrowDateRange = jest.fn();
|
||||
|
||||
describe('anomaly_scores', () => {
|
||||
let anomalies: Anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<AnomalyScore
|
||||
jobKey="job-key-1"
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
score={anomalies.anomalies[0]}
|
||||
interval="day"
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not show a popover on initial render', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AnomalyScore
|
||||
jobKey="job-key-1"
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
score={anomalies.anomalies[0]}
|
||||
interval="day"
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="anomaly-description-list"]').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('show a popover on a mouse click', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AnomalyScore
|
||||
jobKey="job-key-1"
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
score={anomalies.anomalies[0]}
|
||||
interval="day"
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test-subj="anomaly-score-popover"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="anomaly-description-list"]').exists()).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { EuiPopover, EuiDescriptionList, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { NarrowDateRange, Anomaly } from '../types';
|
||||
import { DraggableScore } from './draggable_score';
|
||||
import { escapeDataProviderId } from '../../drag_and_drop/helpers';
|
||||
import { createDescriptionList } from './create_description_list';
|
||||
|
||||
interface Args {
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
narrowDateRange: NarrowDateRange;
|
||||
jobKey: string;
|
||||
index?: number;
|
||||
score: Anomaly;
|
||||
interval: string;
|
||||
}
|
||||
|
||||
const Icon = styled(EuiIcon)`
|
||||
vertical-align: text-bottom;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const AnomalyScore = React.memo<Args>(
|
||||
({ jobKey, startDate, endDate, index = 0, score, interval, narrowDateRange }): JSX.Element => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<DraggableScore
|
||||
id={escapeDataProviderId(`anomaly-scores-${jobKey}`)}
|
||||
index={index}
|
||||
score={score}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
data-test-subj="anomaly-score-popover"
|
||||
id="anomaly-score-popover"
|
||||
isOpen={isOpen}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
closePopover={() => setIsOpen(!isOpen)}
|
||||
button={<Icon type="iInCircle" />}
|
||||
>
|
||||
<EuiDescriptionList
|
||||
data-test-subj="anomaly-description-list"
|
||||
listItems={createDescriptionList(
|
||||
score,
|
||||
startDate,
|
||||
endDate,
|
||||
interval,
|
||||
narrowDateRange
|
||||
)}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import * as React from 'react';
|
||||
import { AnomalyScores, createJobKey } from './anomaly_scores';
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { TestProviders } from '../../../mock/test_providers';
|
||||
import { getEmptyValue } from '../../empty_value';
|
||||
import { Anomalies } from '../types';
|
||||
|
||||
const endDate: number = new Date('3000-01-01T00:00:00.000Z').valueOf();
|
||||
const narrowDateRange = jest.fn();
|
||||
|
||||
describe('anomaly_scores', () => {
|
||||
let anomalies: Anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<AnomalyScores
|
||||
anomalies={anomalies}
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
isLoading={false}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders spinner when isLoading is true is passed', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AnomalyScores
|
||||
anomalies={anomalies}
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
isLoading={true}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="anomaly-score-spinner"]').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('does NOT render spinner when isLoading is false is passed', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AnomalyScores
|
||||
anomalies={anomalies}
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
isLoading={false}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="anomaly-score-spinner"]').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('renders an empty value if anomalies is null', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AnomalyScores
|
||||
anomalies={null}
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
isLoading={false}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
|
||||
test('renders an empty value if anomalies array is empty', () => {
|
||||
anomalies.anomalies = [];
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AnomalyScores
|
||||
anomalies={anomalies}
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
isLoading={false}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
|
||||
test('can create a job key', () => {
|
||||
const job = createJobKey(anomalies.anomalies[0]);
|
||||
expect(job).toEqual('job-1-16.193669439507826-process.name-du');
|
||||
});
|
||||
|
||||
test('should not show a popover on initial render', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AnomalyScores
|
||||
anomalies={anomalies}
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
isLoading={false}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="anomaly-description-list"]').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('showing a popover on a mouse click', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<AnomalyScores
|
||||
anomalies={anomalies}
|
||||
startDate={0}
|
||||
endDate={endDate}
|
||||
isLoading={false}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test-subj="anomaly-score-popover"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test-subj="anomaly-description-list"]').exists()).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiLoadingSpinner, EuiFlexGroup } from '@elastic/eui';
|
||||
import { getEmptyTagValue } from '../../empty_value';
|
||||
import { Anomalies, Anomaly, NarrowDateRange } from '../types';
|
||||
import { getTopSeverityJobs } from './get_top_severity';
|
||||
import { AnomalyScore } from './anomaly_score';
|
||||
|
||||
interface Args {
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
anomalies: Anomalies | null;
|
||||
isLoading: boolean;
|
||||
narrowDateRange: NarrowDateRange;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export const createJobKey = (score: Anomaly): string =>
|
||||
`${score.jobId}-${score.severity}-${score.entityName}-${score.entityValue}`;
|
||||
|
||||
export const AnomalyScores = React.memo<Args>(
|
||||
({ anomalies, startDate, endDate, isLoading, narrowDateRange, limit }): JSX.Element => {
|
||||
if (isLoading) {
|
||||
return <EuiLoadingSpinner data-test-subj="anomaly-score-spinner" size="m" />;
|
||||
} else if (anomalies == null || anomalies.anomalies.length === 0) {
|
||||
return getEmptyTagValue();
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="none" responsive={false}>
|
||||
{getTopSeverityJobs(anomalies.anomalies, limit).map((score, index) => {
|
||||
const jobKey = createJobKey(score);
|
||||
return (
|
||||
<AnomalyScore
|
||||
key={jobKey}
|
||||
jobKey={jobKey}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
index={index}
|
||||
score={score}
|
||||
interval={anomalies.interval}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Anomaly, NarrowDateRange } from '../types';
|
||||
import { getScoreString } from './get_score_string';
|
||||
import { PreferenceFormattedDate } from '../../formatted_date';
|
||||
import { createInfluencers } from './../influencers/create_influencers';
|
||||
import { DescriptionList } from '../../../../common/utility_types';
|
||||
import * as i18n from './translations';
|
||||
import { createExplorerLink } from '../links/create_explorer_link';
|
||||
|
||||
const LargeScore = styled(EuiText)`
|
||||
font-size: 45px;
|
||||
font-weight: lighter;
|
||||
`;
|
||||
|
||||
export const createDescriptionList = (
|
||||
score: Anomaly,
|
||||
startDate: number,
|
||||
endDate: number,
|
||||
interval: string,
|
||||
narrowDateRange: NarrowDateRange
|
||||
): DescriptionList[] => {
|
||||
const descriptionList: DescriptionList[] = [
|
||||
{
|
||||
title: i18n.MAX_ANOMALY_SCORE,
|
||||
description: (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<LargeScore>{getScoreString(score.severity)}</LargeScore>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
{i18n.ANOMALY_JOB}
|
||||
</>
|
||||
),
|
||||
description: (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem grow={false}>{score.jobId}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink href={createExplorerLink(score, startDate, endDate)} target="_blank">
|
||||
{i18n.VIEW_IN_MACHINE_LEARNING}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.DETECTED,
|
||||
description: (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PreferenceFormattedDate value={new Date(score.time)} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
data-test-subj="anomaly-description-narrow-range-link"
|
||||
onClick={() => {
|
||||
narrowDateRange(score, interval);
|
||||
}}
|
||||
target="_blank"
|
||||
>
|
||||
{i18n.NARROW_TO_THIS_DATE_RANGE}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.TOP_ANOMALY_SUSPECT,
|
||||
description: (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem grow={false}>{`${score.entityName}: “${score.entityValue}”`}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.INFLUENCED_BY,
|
||||
description: (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
|
||||
{createInfluencers(score)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
];
|
||||
return descriptionList;
|
||||
};
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import * as React from 'react';
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { createDescriptionList } from './create_description_list';
|
||||
import { EuiDescriptionList } from '@elastic/eui';
|
||||
import { Anomaly } from '../types';
|
||||
|
||||
const endDate: number = new Date('3000-01-01T00:00:00.000Z').valueOf();
|
||||
|
||||
describe('create_description_list', () => {
|
||||
let narrowDateRange = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
narrowDateRange = jest.fn();
|
||||
});
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<EuiDescriptionList
|
||||
listItems={createDescriptionList(
|
||||
mockAnomalies.anomalies[0],
|
||||
0,
|
||||
endDate,
|
||||
'hours',
|
||||
narrowDateRange
|
||||
)}
|
||||
/>
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it calls the narrow date range function on click', () => {
|
||||
const wrapper = mount(
|
||||
<EuiDescriptionList
|
||||
listItems={createDescriptionList(
|
||||
mockAnomalies.anomalies[0],
|
||||
0,
|
||||
endDate,
|
||||
'hours',
|
||||
narrowDateRange
|
||||
)}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test-subj="anomaly-description-narrow-range-link"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(narrowDateRange.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
test('it should the narrow date range with the score', () => {
|
||||
const wrapper = mount(
|
||||
<EuiDescriptionList
|
||||
listItems={createDescriptionList(
|
||||
mockAnomalies.anomalies[0],
|
||||
0,
|
||||
endDate,
|
||||
'hours',
|
||||
narrowDateRange
|
||||
)}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test-subj="anomaly-description-narrow-range-link"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
const expected: Anomaly = {
|
||||
detectorIndex: 0,
|
||||
entityName: 'process.name',
|
||||
entityValue: 'du',
|
||||
influencers: [
|
||||
{ 'host.name': 'zeek-iowa' },
|
||||
{ 'process.name': 'du' },
|
||||
{ 'user.name': 'root' },
|
||||
],
|
||||
jobId: 'job-1',
|
||||
rowId: '1561157194802_0',
|
||||
severity: 16.193669439507826,
|
||||
source: {
|
||||
actual: [1],
|
||||
bucket_span: 900,
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'du',
|
||||
detector_index: 0,
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
influencers: [
|
||||
{ influencer_field_name: 'user.name', influencer_field_values: ['root'] },
|
||||
{ influencer_field_name: 'process.name', influencer_field_values: ['du'] },
|
||||
{ influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] },
|
||||
],
|
||||
initial_record_score: 16.193669439507826,
|
||||
is_interim: false,
|
||||
job_id: 'job-1',
|
||||
multi_bucket_impact: 0,
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
probability: 0.024041164411288146,
|
||||
record_score: 16.193669439507826,
|
||||
result_type: 'record',
|
||||
timestamp: 1560664800000,
|
||||
typical: [0.024041164411288146],
|
||||
},
|
||||
time: 1560664800000,
|
||||
};
|
||||
expect(narrowDateRange.mock.calls[0][0]).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it should call the narrow date range with the interval', () => {
|
||||
const wrapper = mount(
|
||||
<EuiDescriptionList
|
||||
listItems={createDescriptionList(
|
||||
mockAnomalies.anomalies[0],
|
||||
0,
|
||||
endDate,
|
||||
'hours',
|
||||
narrowDateRange
|
||||
)}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test-subj="anomaly-description-narrow-range-link"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(narrowDateRange.mock.calls[0][1]).toEqual('hours');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mockAnomalies } from '../mock';
|
||||
import {
|
||||
createEntitiesFromScore,
|
||||
createEntity,
|
||||
createEntityFromRecord,
|
||||
} from './create_entities_from_score';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
|
||||
describe('create_entities_from_score', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('it returns expected entities from a typical score', () => {
|
||||
const entities = createEntitiesFromScore(anomalies.anomalies[0]);
|
||||
expect(entities).toEqual("host.name:'zeek-iowa',process.name:'du',user.name:'root'");
|
||||
});
|
||||
|
||||
test('it returns expected non-duplicate entities', () => {
|
||||
const anomaly = anomalies.anomalies[0];
|
||||
anomaly.entityName = 'name-1';
|
||||
anomaly.entityValue = 'value-1';
|
||||
anomaly.influencers = [
|
||||
{
|
||||
'name-1': 'value-1',
|
||||
},
|
||||
];
|
||||
const entities = createEntitiesFromScore(anomaly);
|
||||
expect(entities).toEqual("name-1:'value-1'");
|
||||
});
|
||||
|
||||
test('it returns multiple entities', () => {
|
||||
const anomaly = anomalies.anomalies[0];
|
||||
anomaly.entityName = 'name-1';
|
||||
anomaly.entityValue = 'value-1';
|
||||
anomaly.influencers = [
|
||||
{
|
||||
'name-2': 'value-2',
|
||||
},
|
||||
];
|
||||
const entities = createEntitiesFromScore(anomaly);
|
||||
expect(entities).toEqual("name-2:'value-2',name-1:'value-1'");
|
||||
});
|
||||
|
||||
test('it returns just a single entity with an empty set of influencers', () => {
|
||||
const anomaly = anomalies.anomalies[0];
|
||||
anomaly.entityName = 'name-1';
|
||||
anomaly.entityValue = 'value-1';
|
||||
anomaly.influencers = [];
|
||||
const entities = createEntitiesFromScore(anomaly);
|
||||
expect(entities).toEqual("name-1:'value-1'");
|
||||
});
|
||||
|
||||
test('it creates a simple string entity with quotes', () => {
|
||||
const entity = createEntity('name-1', 'value-1');
|
||||
expect(entity).toEqual("name-1:'value-1'");
|
||||
});
|
||||
|
||||
test('it creates a simple string entity from a record<string, string>', () => {
|
||||
const entity = createEntityFromRecord({ 'name-1': 'value-1' });
|
||||
expect(entity).toEqual("name-1:'value-1'");
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Anomaly } from '../types';
|
||||
|
||||
export const createEntityFromRecord = (entity: Record<string, string>): string =>
|
||||
createEntity(Object.keys(entity)[0], Object.values(entity)[0]);
|
||||
|
||||
export const createEntity = (entityName: string, entityValue: string): string =>
|
||||
`${entityName}:'${entityValue}'`;
|
||||
|
||||
export const createEntitiesFromScore = (score: Anomaly): string => {
|
||||
const influencers = score.influencers.reduce((accum, item, index) => {
|
||||
if (index === 0) {
|
||||
return createEntityFromRecord(item);
|
||||
} else {
|
||||
return `${accum},${createEntityFromRecord(item)}`;
|
||||
}
|
||||
}, '');
|
||||
|
||||
if (influencers.length === 0) {
|
||||
return createEntity(score.entityName, score.entityValue);
|
||||
} else if (!influencers.includes(score.entityName)) {
|
||||
return `${influencers},${createEntity(score.entityName, score.entityValue)}`;
|
||||
} else {
|
||||
return influencers;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DraggableScore } from './draggable_score';
|
||||
|
||||
describe('draggable_score', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<DraggableScore id="some-id" index={0} score={anomalies.anomalies[0]} />
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { DraggableWrapper, DragEffects } from '../../drag_and_drop/draggable_wrapper';
|
||||
import { Anomaly } from '../types';
|
||||
import { IS_OPERATOR } from '../../timeline/data_providers/data_provider';
|
||||
import { Provider } from '../../timeline/data_providers/provider';
|
||||
import { Spacer } from '../../page';
|
||||
import { getScoreString } from './get_score_string';
|
||||
|
||||
export const DraggableScore = React.memo<{
|
||||
id: string;
|
||||
index: number;
|
||||
score: Anomaly;
|
||||
}>(
|
||||
({ id, index, score }): JSX.Element => (
|
||||
<DraggableWrapper
|
||||
key={id}
|
||||
dataProvider={{
|
||||
and: [],
|
||||
enabled: true,
|
||||
id,
|
||||
name: score.entityName,
|
||||
excluded: false,
|
||||
kqlQuery: '',
|
||||
queryMatch: {
|
||||
field: score.entityName,
|
||||
value: score.entityValue,
|
||||
operator: IS_OPERATOR,
|
||||
},
|
||||
}}
|
||||
render={(dataProvider, _, snapshot) =>
|
||||
snapshot.isDragging ? (
|
||||
<DragEffects>
|
||||
<Provider dataProvider={dataProvider} />
|
||||
</DragEffects>
|
||||
) : (
|
||||
<>
|
||||
{index !== 0 && (
|
||||
<>
|
||||
{','}
|
||||
<Spacer />
|
||||
</>
|
||||
)}
|
||||
{getScoreString(score.severity)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
);
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getScoreString } from './get_score_string';
|
||||
|
||||
describe('create_influencers', () => {
|
||||
test('it rounds up to 1 from 0.3', () => {
|
||||
const score = getScoreString(0.3);
|
||||
expect(score).toEqual('1');
|
||||
});
|
||||
|
||||
test('it rounds up to 1 from 0.000000001', () => {
|
||||
const score = getScoreString(0.000000001);
|
||||
expect(score).toEqual('1');
|
||||
});
|
||||
|
||||
test('0 is 0', () => {
|
||||
const score = getScoreString(0);
|
||||
expect(score).toEqual('0');
|
||||
});
|
||||
|
||||
test('99.1 is 100', () => {
|
||||
const score = getScoreString(99.1);
|
||||
expect(score).toEqual('100');
|
||||
});
|
||||
|
||||
test('100 is 100', () => {
|
||||
const score = getScoreString(100);
|
||||
expect(score).toEqual('100');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const getScoreString = (score: number): string => String(Math.ceil(score));
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { getTopSeverityJobs } from './get_top_severity';
|
||||
|
||||
describe('get_top_severity', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('gets the top severity jobs correctly on mock data', () => {
|
||||
const topJobs = getTopSeverityJobs(anomalies.anomalies);
|
||||
expect(topJobs).toEqual([
|
||||
{
|
||||
time: 1560664800000,
|
||||
source: {
|
||||
job_id: 'job-1',
|
||||
result_type: 'record',
|
||||
probability: 0.024041164411288146,
|
||||
multi_bucket_impact: 0,
|
||||
record_score: 16.193669439507826,
|
||||
initial_record_score: 16.193669439507826,
|
||||
bucket_span: 900,
|
||||
detector_index: 0,
|
||||
is_interim: false,
|
||||
timestamp: 1560664800000,
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'du',
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
typical: [0.024041164411288146],
|
||||
actual: [1],
|
||||
influencers: [
|
||||
{
|
||||
influencer_field_name: 'user.name',
|
||||
influencer_field_values: ['root'],
|
||||
},
|
||||
{
|
||||
influencer_field_name: 'process.name',
|
||||
influencer_field_values: ['du'],
|
||||
},
|
||||
{
|
||||
influencer_field_name: 'host.name',
|
||||
influencer_field_values: ['zeek-iowa'],
|
||||
},
|
||||
],
|
||||
},
|
||||
rowId: '1561157194802_0',
|
||||
jobId: 'job-1',
|
||||
detectorIndex: 0,
|
||||
severity: 16.193669439507826,
|
||||
entityName: 'process.name',
|
||||
entityValue: 'du',
|
||||
influencers: [
|
||||
{
|
||||
'host.name': 'zeek-iowa',
|
||||
},
|
||||
{
|
||||
'process.name': 'du',
|
||||
},
|
||||
{
|
||||
'user.name': 'root',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
time: 1560664800000,
|
||||
source: {
|
||||
job_id: 'job-2',
|
||||
result_type: 'record',
|
||||
probability: 0.024041164411288146,
|
||||
multi_bucket_impact: 0,
|
||||
record_score: 16.193669439507826,
|
||||
initial_record_score: 16.193669439507826,
|
||||
bucket_span: 900,
|
||||
detector_index: 0,
|
||||
is_interim: false,
|
||||
timestamp: 1560664800000,
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'ls',
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
typical: [0.024041164411288146],
|
||||
actual: [1],
|
||||
influencers: [
|
||||
{
|
||||
influencer_field_name: 'user.name',
|
||||
influencer_field_values: ['root'],
|
||||
},
|
||||
{
|
||||
influencer_field_name: 'process.name',
|
||||
influencer_field_values: ['ls'],
|
||||
},
|
||||
{
|
||||
influencer_field_name: 'host.name',
|
||||
influencer_field_values: ['zeek-iowa'],
|
||||
},
|
||||
],
|
||||
},
|
||||
rowId: '1561157194802_1',
|
||||
jobId: 'job-2',
|
||||
detectorIndex: 0,
|
||||
severity: 16.193669439507826,
|
||||
entityName: 'process.name',
|
||||
entityValue: 'ls',
|
||||
influencers: [
|
||||
{
|
||||
'host.name': 'zeek-iowa',
|
||||
},
|
||||
{
|
||||
'process.name': 'ls',
|
||||
},
|
||||
{
|
||||
'user.name': 'root',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('gets the top severity jobs sorted by severity', () => {
|
||||
anomalies.anomalies[0].severity = 0;
|
||||
anomalies.anomalies[1].severity = 100;
|
||||
const topJobs = getTopSeverityJobs(anomalies.anomalies);
|
||||
expect(topJobs[0].severity).toEqual(100);
|
||||
expect(topJobs[1].severity).toEqual(0);
|
||||
});
|
||||
|
||||
test('removes duplicate job Ids', () => {
|
||||
const anomaly = anomalies.anomalies[0];
|
||||
anomalies.anomalies = [
|
||||
cloneDeep(anomaly),
|
||||
cloneDeep(anomaly),
|
||||
cloneDeep(anomaly),
|
||||
cloneDeep(anomaly),
|
||||
];
|
||||
const topJobs = getTopSeverityJobs(anomalies.anomalies);
|
||||
expect(topJobs.length).toEqual(1);
|
||||
});
|
||||
|
||||
test('preserves multiple job Ids', () => {
|
||||
const anomaly = anomalies.anomalies[0];
|
||||
const anomaly1 = cloneDeep(anomaly);
|
||||
anomaly1.jobId = 'job-1';
|
||||
const anomaly2 = cloneDeep(anomaly);
|
||||
anomaly2.jobId = 'job-2';
|
||||
const anomaly3 = cloneDeep(anomaly);
|
||||
anomaly3.jobId = 'job-3';
|
||||
const anomaly4 = cloneDeep(anomaly);
|
||||
anomaly4.jobId = 'job-4';
|
||||
|
||||
anomalies.anomalies = [anomaly1, anomaly2, anomaly3, anomaly4];
|
||||
const topJobs = getTopSeverityJobs(anomalies.anomalies);
|
||||
expect(topJobs.length).toEqual(4);
|
||||
});
|
||||
|
||||
test('will choose a job id which has a higher score', () => {
|
||||
const anomaly = anomalies.anomalies[0];
|
||||
|
||||
const anomaly1 = cloneDeep(anomaly);
|
||||
anomaly1.jobId = 'job-1';
|
||||
anomaly1.severity = 100;
|
||||
|
||||
const anomaly2 = cloneDeep(anomaly);
|
||||
anomaly2.jobId = 'job-2';
|
||||
anomaly2.severity = 20; // This should not show since job-2 (25 below replaces this)
|
||||
|
||||
const anomaly3 = cloneDeep(anomaly);
|
||||
anomaly3.jobId = 'job-3';
|
||||
anomaly3.severity = 30;
|
||||
|
||||
const anomaly4 = cloneDeep(anomaly);
|
||||
anomaly4.jobId = 'job-4';
|
||||
anomaly4.severity = 10;
|
||||
|
||||
const anomaly5 = cloneDeep(anomaly);
|
||||
anomaly5.jobId = 'job-2';
|
||||
anomaly5.severity = 25; // This will replace job-2 (20 above)
|
||||
|
||||
anomalies.anomalies = [anomaly1, anomaly2, anomaly3, anomaly4, anomaly5];
|
||||
const topJobs = getTopSeverityJobs(anomalies.anomalies);
|
||||
expect(topJobs[0].severity).toEqual(100);
|
||||
expect(topJobs[1].severity).toEqual(30);
|
||||
expect(topJobs[2].severity).toEqual(25);
|
||||
expect(topJobs[3].severity).toEqual(10);
|
||||
});
|
||||
|
||||
test('it returns a top severity of empty length with an empty array', () => {
|
||||
anomalies.anomalies = [];
|
||||
const topJobs = getTopSeverityJobs(anomalies.anomalies);
|
||||
expect(topJobs.length).toEqual(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { toArray } from 'lodash/fp';
|
||||
import { Anomaly } from '../types';
|
||||
|
||||
export const getTopSeverityJobs = (anomalies: Anomaly[], limit?: number): Anomaly[] => {
|
||||
const reduced = anomalies.reduce<Record<string, Anomaly>>((accum, item) => {
|
||||
const jobId = item.jobId;
|
||||
const severity = item.severity;
|
||||
if (accum[jobId] == null || accum[jobId].severity < severity) {
|
||||
accum[jobId] = item;
|
||||
}
|
||||
return accum;
|
||||
}, {});
|
||||
const sortedArray = toArray(reduced).sort(
|
||||
(anomalyA, anomalyB) => anomalyB.severity - anomalyA.severity
|
||||
);
|
||||
|
||||
if (limit == null) {
|
||||
return sortedArray;
|
||||
} else {
|
||||
return sortedArray.slice(0, limit);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { scoreIntervalToDateTime, FromTo } from './score_interval_to_datetime';
|
||||
|
||||
describe('score_interval_to_datetime', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('converts a second interval to plus or minus (+/-) one hour', () => {
|
||||
const expected: FromTo = {
|
||||
from: new Date('2019-06-25T04:31:59.345Z').valueOf(),
|
||||
to: new Date('2019-06-25T06:31:59.345Z').valueOf(),
|
||||
};
|
||||
anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf();
|
||||
expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'second')).toEqual(expected);
|
||||
});
|
||||
|
||||
test('converts a minute interval to plus or minus (+/-) one hour', () => {
|
||||
const expected: FromTo = {
|
||||
from: new Date('2019-06-25T04:31:59.345Z').valueOf(),
|
||||
to: new Date('2019-06-25T06:31:59.345Z').valueOf(),
|
||||
};
|
||||
anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf();
|
||||
expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'minute')).toEqual(expected);
|
||||
});
|
||||
|
||||
test('converts a hour interval to plus or minus (+/-) one hour', () => {
|
||||
const expected: FromTo = {
|
||||
from: new Date('2019-06-25T04:31:59.345Z').valueOf(),
|
||||
to: new Date('2019-06-25T06:31:59.345Z').valueOf(),
|
||||
};
|
||||
anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf();
|
||||
expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'hour')).toEqual(expected);
|
||||
});
|
||||
|
||||
test('converts a day interval to plus or minus (+/-) one day', () => {
|
||||
const expected: FromTo = {
|
||||
from: new Date('2019-06-24T05:31:59.345Z').valueOf(),
|
||||
to: new Date('2019-06-26T05:31:59.345Z').valueOf(),
|
||||
};
|
||||
anomalies.anomalies[0].time = new Date('2019-06-25T05:31:59.345Z').valueOf();
|
||||
expect(scoreIntervalToDateTime(anomalies.anomalies[0], 'day')).toEqual(expected);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { Anomaly } from '../types';
|
||||
|
||||
export interface FromTo {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
export const scoreIntervalToDateTime = (score: Anomaly, interval: string): FromTo => {
|
||||
if (interval === 'second' || interval === 'minute' || interval === 'hour') {
|
||||
return {
|
||||
from: moment(score.time)
|
||||
.subtract(1, 'hour')
|
||||
.valueOf(),
|
||||
to: moment(score.time)
|
||||
.add(1, 'hour')
|
||||
.valueOf(),
|
||||
};
|
||||
} else {
|
||||
// default should be a day
|
||||
return {
|
||||
from: moment(score.time)
|
||||
.subtract(1, 'day')
|
||||
.valueOf(),
|
||||
to: moment(score.time)
|
||||
.add(1, 'day')
|
||||
.valueOf(),
|
||||
};
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const INFLUENCED_BY = i18n.translate('xpack.siem.ml.score.influencedByTitle', {
|
||||
defaultMessage: 'Influenced By',
|
||||
});
|
||||
|
||||
export const MAX_ANOMALY_SCORE = i18n.translate('xpack.siem.ml.score.maxAnomalyScoreTitle', {
|
||||
defaultMessage: 'Max Anomaly Score',
|
||||
});
|
||||
|
||||
export const ANOMALY_JOB = i18n.translate('xpack.siem.ml.score.anomalyJobTitle', {
|
||||
defaultMessage: 'Anomaly Job',
|
||||
});
|
||||
|
||||
export const VIEW_IN_MACHINE_LEARNING = i18n.translate(
|
||||
'xpack.siem.ml.score.viewInMachineLearningLink',
|
||||
{
|
||||
defaultMessage: 'View in Machine Learning',
|
||||
}
|
||||
);
|
||||
|
||||
export const DETECTED = i18n.translate('xpack.siem.ml.score.detectedTitle', {
|
||||
defaultMessage: 'Detected',
|
||||
});
|
||||
|
||||
export const NARROW_TO_THIS_DATE_RANGE = i18n.translate(
|
||||
'xpack.siem.ml.score.narrowToThisDateRangeLink',
|
||||
{
|
||||
defaultMessage: 'Narrow to this date range',
|
||||
}
|
||||
);
|
||||
|
||||
export const TOP_ANOMALY_SUSPECT = i18n.translate('xpack.siem.ml.score.topAnomalySuspectTitle', {
|
||||
defaultMessage: 'Top Anomaly Suspect',
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiInMemoryTable, EuiPanel } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
|
||||
import { HeaderPanel } from '../../header_panel';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { getAnomaliesHostTableColumns } from './get_anomalies_host_table_columns';
|
||||
import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts';
|
||||
import { BackgroundRefetch } from '../../load_more_table';
|
||||
import { LoadingPanel } from '../../loading';
|
||||
import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies';
|
||||
import { getSizeFromAnomalies } from '../anomaly/get_size_from_anomalies';
|
||||
import { dateTimesAreEqual } from './date_time_equality';
|
||||
import { AnomaliesTableProps } from '../types';
|
||||
|
||||
const BasicTableContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'anomaly.severity',
|
||||
direction: 'desc',
|
||||
},
|
||||
};
|
||||
|
||||
export const AnomaliesHostTable = React.memo<AnomaliesTableProps>(
|
||||
({ startDate, endDate, narrowDateRange, skip }): JSX.Element => {
|
||||
const [loading, tableData] = useAnomaliesTableData({
|
||||
influencers: [],
|
||||
startDate,
|
||||
endDate,
|
||||
threshold: 0,
|
||||
skip,
|
||||
});
|
||||
|
||||
const hosts = convertAnomaliesToHosts(tableData);
|
||||
const interval = getIntervalFromAnomalies(tableData);
|
||||
const columns = getAnomaliesHostTableColumns(startDate, endDate, interval, narrowDateRange);
|
||||
const pagination = {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
totalItemCount: getSizeFromAnomalies(tableData),
|
||||
pageSizeOptions: [5, 10, 20, 50],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
return (
|
||||
<EuiPanel>
|
||||
<BasicTableContainer>
|
||||
{loading && (
|
||||
<>
|
||||
<BackgroundRefetch />
|
||||
<LoadingPanel
|
||||
height="100%"
|
||||
width="100%"
|
||||
text={`${i18n.LOADING} ${i18n.ANOMALIES}`}
|
||||
position="absolute"
|
||||
zIndex={3}
|
||||
data-test-subj="anomalies-host-table-loading-panel"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<HeaderPanel
|
||||
subtitle={`${i18n.SHOWING}: ${hosts.length.toLocaleString()} ${i18n.ANOMALIES}`}
|
||||
title={i18n.ANOMALIES}
|
||||
/>
|
||||
<EuiInMemoryTable
|
||||
items={hosts}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
/>
|
||||
</BasicTableContainer>
|
||||
</EuiPanel>
|
||||
);
|
||||
},
|
||||
dateTimesAreEqual
|
||||
);
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiInMemoryTable, EuiPanel } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
|
||||
import { HeaderPanel } from '../../header_panel';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { convertAnomaliesToNetwork } from './convert_anomalies_to_network';
|
||||
import { BackgroundRefetch } from '../../load_more_table';
|
||||
import { LoadingPanel } from '../../loading';
|
||||
import { AnomaliesTableProps } from '../types';
|
||||
import { getAnomaliesNetworkTableColumns } from './get_anomalies_network_table_columns';
|
||||
import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies';
|
||||
import { getSizeFromAnomalies } from '../anomaly/get_size_from_anomalies';
|
||||
import { dateTimesAreEqual } from './date_time_equality';
|
||||
|
||||
const BasicTableContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: 'anomaly.severity',
|
||||
direction: 'desc',
|
||||
},
|
||||
};
|
||||
|
||||
export const AnomaliesNetworkTable = React.memo<AnomaliesTableProps>(
|
||||
({ startDate, endDate, narrowDateRange, skip }): JSX.Element => {
|
||||
const [loading, tableData] = useAnomaliesTableData({
|
||||
influencers: [],
|
||||
startDate,
|
||||
endDate,
|
||||
threshold: 0,
|
||||
skip,
|
||||
});
|
||||
|
||||
const networks = convertAnomaliesToNetwork(tableData);
|
||||
const interval = getIntervalFromAnomalies(tableData);
|
||||
const columns = getAnomaliesNetworkTableColumns(startDate, endDate, interval, narrowDateRange);
|
||||
const pagination = {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
totalItemCount: getSizeFromAnomalies(tableData),
|
||||
pageSizeOptions: [5, 10, 20, 50],
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
return (
|
||||
<EuiPanel>
|
||||
<BasicTableContainer>
|
||||
{loading && (
|
||||
<>
|
||||
<BackgroundRefetch />
|
||||
<LoadingPanel
|
||||
height="100%"
|
||||
width="100%"
|
||||
text={`${i18n.LOADING} ${i18n.ANOMALIES}`}
|
||||
position="absolute"
|
||||
zIndex={3}
|
||||
data-test-subj="anomalies-network-table-loading-panel"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<HeaderPanel
|
||||
subtitle={`${i18n.SHOWING}: ${networks.length.toLocaleString()} ${i18n.ANOMALIES}`}
|
||||
title={i18n.ANOMALIES}
|
||||
/>
|
||||
<EuiInMemoryTable
|
||||
items={networks}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
/>
|
||||
</BasicTableContainer>
|
||||
</EuiPanel>
|
||||
);
|
||||
},
|
||||
dateTimesAreEqual
|
||||
);
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts';
|
||||
import { AnomaliesByHost } from '../types';
|
||||
|
||||
describe('convert_anomalies_to_hosts', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('it returns expected anomalies from a host', () => {
|
||||
const entities = convertAnomaliesToHosts(anomalies);
|
||||
const expected: AnomaliesByHost[] = [
|
||||
{
|
||||
anomaly: {
|
||||
detectorIndex: 0,
|
||||
entityName: 'process.name',
|
||||
entityValue: 'du',
|
||||
influencers: [
|
||||
{ 'host.name': 'zeek-iowa' },
|
||||
{ 'process.name': 'du' },
|
||||
{ 'user.name': 'root' },
|
||||
],
|
||||
jobId: 'job-1',
|
||||
rowId: '1561157194802_0',
|
||||
severity: 16.193669439507826,
|
||||
source: {
|
||||
actual: [1],
|
||||
bucket_span: 900,
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'du',
|
||||
detector_index: 0,
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
influencers: [
|
||||
{ influencer_field_name: 'user.name', influencer_field_values: ['root'] },
|
||||
{ influencer_field_name: 'process.name', influencer_field_values: ['du'] },
|
||||
{ influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] },
|
||||
],
|
||||
initial_record_score: 16.193669439507826,
|
||||
is_interim: false,
|
||||
job_id: 'job-1',
|
||||
multi_bucket_impact: 0,
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
probability: 0.024041164411288146,
|
||||
record_score: 16.193669439507826,
|
||||
result_type: 'record',
|
||||
timestamp: 1560664800000,
|
||||
typical: [0.024041164411288146],
|
||||
},
|
||||
time: 1560664800000,
|
||||
},
|
||||
hostName: 'zeek-iowa',
|
||||
},
|
||||
{
|
||||
anomaly: {
|
||||
detectorIndex: 0,
|
||||
entityName: 'process.name',
|
||||
entityValue: 'ls',
|
||||
influencers: [
|
||||
{ 'host.name': 'zeek-iowa' },
|
||||
{ 'process.name': 'ls' },
|
||||
{ 'user.name': 'root' },
|
||||
],
|
||||
jobId: 'job-2',
|
||||
rowId: '1561157194802_1',
|
||||
severity: 16.193669439507826,
|
||||
source: {
|
||||
actual: [1],
|
||||
bucket_span: 900,
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'ls',
|
||||
detector_index: 0,
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
influencers: [
|
||||
{ influencer_field_name: 'user.name', influencer_field_values: ['root'] },
|
||||
{ influencer_field_name: 'process.name', influencer_field_values: ['ls'] },
|
||||
{ influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] },
|
||||
],
|
||||
initial_record_score: 16.193669439507826,
|
||||
is_interim: false,
|
||||
job_id: 'job-2',
|
||||
multi_bucket_impact: 0,
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
probability: 0.024041164411288146,
|
||||
record_score: 16.193669439507826,
|
||||
result_type: 'record',
|
||||
timestamp: 1560664800000,
|
||||
typical: [0.024041164411288146],
|
||||
},
|
||||
time: 1560664800000,
|
||||
},
|
||||
hostName: 'zeek-iowa',
|
||||
},
|
||||
];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns empty anomalies if sent in a null', () => {
|
||||
const entities = convertAnomaliesToHosts(null);
|
||||
const expected: AnomaliesByHost[] = [];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Anomalies, AnomaliesByHost } from '../types';
|
||||
import { getHostNameFromInfluencers } from '../influencers/get_host_name_from_influencers';
|
||||
|
||||
export const convertAnomaliesToHosts = (anomalies: Anomalies | null): AnomaliesByHost[] => {
|
||||
if (anomalies == null) {
|
||||
return [];
|
||||
} else {
|
||||
return anomalies.anomalies.reduce<AnomaliesByHost[]>((accum, item) => {
|
||||
if (item.entityName === 'host.name') {
|
||||
return accum.concat({ hostName: item.entityValue, anomaly: item });
|
||||
} else {
|
||||
const hostName = getHostNameFromInfluencers(item.influencers);
|
||||
if (hostName != null) {
|
||||
return accum.concat({ hostName, anomaly: item });
|
||||
} else {
|
||||
return accum;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { convertAnomaliesToNetwork } from './convert_anomalies_to_network';
|
||||
import { AnomaliesByHost, AnomaliesByNetwork } from '../types';
|
||||
|
||||
describe('convert_anomalies_to_hosts', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
anomalies = cloneDeep(mockAnomalies);
|
||||
});
|
||||
|
||||
test('it returns expected anomalies from a network if is part of the entityName and is a source.ip', () => {
|
||||
anomalies.anomalies[0].entityName = 'source.ip';
|
||||
anomalies.anomalies[0].entityValue = '127.0.0.1';
|
||||
const entities = convertAnomaliesToNetwork(anomalies);
|
||||
const expected: AnomaliesByNetwork[] = [
|
||||
{
|
||||
anomaly: {
|
||||
detectorIndex: 0,
|
||||
entityName: 'source.ip',
|
||||
entityValue: '127.0.0.1',
|
||||
influencers: [
|
||||
{ 'host.name': 'zeek-iowa' },
|
||||
{ 'process.name': 'du' },
|
||||
{ 'user.name': 'root' },
|
||||
],
|
||||
jobId: 'job-1',
|
||||
rowId: '1561157194802_0',
|
||||
severity: 16.193669439507826,
|
||||
source: {
|
||||
actual: [1],
|
||||
bucket_span: 900,
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'du',
|
||||
detector_index: 0,
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
influencers: [
|
||||
{ influencer_field_name: 'user.name', influencer_field_values: ['root'] },
|
||||
{ influencer_field_name: 'process.name', influencer_field_values: ['du'] },
|
||||
{ influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] },
|
||||
],
|
||||
initial_record_score: 16.193669439507826,
|
||||
is_interim: false,
|
||||
job_id: 'job-1',
|
||||
multi_bucket_impact: 0,
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
probability: 0.024041164411288146,
|
||||
record_score: 16.193669439507826,
|
||||
result_type: 'record',
|
||||
timestamp: 1560664800000,
|
||||
typical: [0.024041164411288146],
|
||||
},
|
||||
time: 1560664800000,
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
type: 'source.ip',
|
||||
},
|
||||
];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns expected anomalies from a network if is part of the entityName and is a destination.ip', () => {
|
||||
anomalies.anomalies[0].entityName = 'destination.ip';
|
||||
anomalies.anomalies[0].entityValue = '127.0.0.1';
|
||||
const entities = convertAnomaliesToNetwork(anomalies);
|
||||
const expected: AnomaliesByNetwork[] = [
|
||||
{
|
||||
anomaly: {
|
||||
detectorIndex: 0,
|
||||
entityName: 'destination.ip',
|
||||
entityValue: '127.0.0.1',
|
||||
influencers: [
|
||||
{ 'host.name': 'zeek-iowa' },
|
||||
{ 'process.name': 'du' },
|
||||
{ 'user.name': 'root' },
|
||||
],
|
||||
jobId: 'job-1',
|
||||
rowId: '1561157194802_0',
|
||||
severity: 16.193669439507826,
|
||||
source: {
|
||||
actual: [1],
|
||||
bucket_span: 900,
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'du',
|
||||
detector_index: 0,
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
influencers: [
|
||||
{ influencer_field_name: 'user.name', influencer_field_values: ['root'] },
|
||||
{ influencer_field_name: 'process.name', influencer_field_values: ['du'] },
|
||||
{ influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] },
|
||||
],
|
||||
initial_record_score: 16.193669439507826,
|
||||
is_interim: false,
|
||||
job_id: 'job-1',
|
||||
multi_bucket_impact: 0,
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
probability: 0.024041164411288146,
|
||||
record_score: 16.193669439507826,
|
||||
result_type: 'record',
|
||||
timestamp: 1560664800000,
|
||||
typical: [0.024041164411288146],
|
||||
},
|
||||
time: 1560664800000,
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
type: 'destination.ip',
|
||||
},
|
||||
];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns expected anomalies from a network if is part of the influencers and is a source.ip', () => {
|
||||
anomalies.anomalies[0].entityName = 'not-an-ip';
|
||||
anomalies.anomalies[0].entityValue = 'not-an-ip';
|
||||
anomalies.anomalies[0].influencers = [{ 'source.ip': '127.0.0.1' }];
|
||||
const entities = convertAnomaliesToNetwork(anomalies);
|
||||
const expected: AnomaliesByNetwork[] = [
|
||||
{
|
||||
anomaly: {
|
||||
detectorIndex: 0,
|
||||
entityName: 'not-an-ip',
|
||||
entityValue: 'not-an-ip',
|
||||
influencers: [{ 'source.ip': '127.0.0.1' }],
|
||||
jobId: 'job-1',
|
||||
rowId: '1561157194802_0',
|
||||
severity: 16.193669439507826,
|
||||
source: {
|
||||
actual: [1],
|
||||
bucket_span: 900,
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'du',
|
||||
detector_index: 0,
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
influencers: [
|
||||
{ influencer_field_name: 'user.name', influencer_field_values: ['root'] },
|
||||
{ influencer_field_name: 'process.name', influencer_field_values: ['du'] },
|
||||
{ influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] },
|
||||
],
|
||||
initial_record_score: 16.193669439507826,
|
||||
is_interim: false,
|
||||
job_id: 'job-1',
|
||||
multi_bucket_impact: 0,
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
probability: 0.024041164411288146,
|
||||
record_score: 16.193669439507826,
|
||||
result_type: 'record',
|
||||
timestamp: 1560664800000,
|
||||
typical: [0.024041164411288146],
|
||||
},
|
||||
time: 1560664800000,
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
type: 'source.ip',
|
||||
},
|
||||
];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns expected anomalies from a network if is part of the influencers and is a destination.ip', () => {
|
||||
anomalies.anomalies[0].entityName = 'not-an-ip';
|
||||
anomalies.anomalies[0].entityValue = 'not-an-ip';
|
||||
anomalies.anomalies[0].influencers = [{ 'destination.ip': '127.0.0.1' }];
|
||||
const entities = convertAnomaliesToNetwork(anomalies);
|
||||
const expected: AnomaliesByNetwork[] = [
|
||||
{
|
||||
anomaly: {
|
||||
detectorIndex: 0,
|
||||
entityName: 'not-an-ip',
|
||||
entityValue: 'not-an-ip',
|
||||
influencers: [{ 'destination.ip': '127.0.0.1' }],
|
||||
jobId: 'job-1',
|
||||
rowId: '1561157194802_0',
|
||||
severity: 16.193669439507826,
|
||||
source: {
|
||||
actual: [1],
|
||||
bucket_span: 900,
|
||||
by_field_name: 'process.name',
|
||||
by_field_value: 'du',
|
||||
detector_index: 0,
|
||||
function: 'rare',
|
||||
function_description: 'rare',
|
||||
influencers: [
|
||||
{ influencer_field_name: 'user.name', influencer_field_values: ['root'] },
|
||||
{ influencer_field_name: 'process.name', influencer_field_values: ['du'] },
|
||||
{ influencer_field_name: 'host.name', influencer_field_values: ['zeek-iowa'] },
|
||||
],
|
||||
initial_record_score: 16.193669439507826,
|
||||
is_interim: false,
|
||||
job_id: 'job-1',
|
||||
multi_bucket_impact: 0,
|
||||
partition_field_name: 'host.name',
|
||||
partition_field_value: 'zeek-iowa',
|
||||
probability: 0.024041164411288146,
|
||||
record_score: 16.193669439507826,
|
||||
result_type: 'record',
|
||||
timestamp: 1560664800000,
|
||||
typical: [0.024041164411288146],
|
||||
},
|
||||
time: 1560664800000,
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
type: 'destination.ip',
|
||||
},
|
||||
];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
|
||||
test('it returns empty anomalies if sent in a null', () => {
|
||||
const entities = convertAnomaliesToNetwork(null);
|
||||
const expected: AnomaliesByHost[] = [];
|
||||
expect(entities).toEqual(expected);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Anomalies, AnomaliesByNetwork } from '../types';
|
||||
import { getNetworkFromInfluencers } from '../influencers/get_network_from_influencers';
|
||||
|
||||
export const convertAnomaliesToNetwork = (anomalies: Anomalies | null): AnomaliesByNetwork[] => {
|
||||
if (anomalies == null) {
|
||||
return [];
|
||||
} else {
|
||||
return anomalies.anomalies.reduce<AnomaliesByNetwork[]>((accum, item) => {
|
||||
if (item.entityName === 'source.ip' || item.entityName === 'destination.ip') {
|
||||
return accum.concat({ ip: item.entityValue, type: item.entityName, anomaly: item });
|
||||
} else {
|
||||
const network = getNetworkFromInfluencers(item.influencers);
|
||||
if (network != null) {
|
||||
return accum.concat({ ip: network.ip, type: network.type, anomaly: item });
|
||||
} else {
|
||||
return accum;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mockAnomalies } from '../mock';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { createCompoundHostKey, createCompoundNetworkKey } from './create_compound_key';
|
||||
import { AnomaliesByHost, AnomaliesByNetwork } from '../types';
|
||||
|
||||
describe('create_explorer_link', () => {
|
||||
let anomalies = cloneDeep(mockAnomalies);
|
||||
|
||||
beforeEach(() => {
|
||||
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');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AnomaliesByHost, AnomaliesByNetwork } from '../types';
|
||||
|
||||
export const createCompoundHostKey = (anomaliesByHost: AnomaliesByHost): string => {
|
||||
return `${anomaliesByHost.hostName}-${anomaliesByHost.anomaly.entityName}-${
|
||||
anomaliesByHost.anomaly.entityValue
|
||||
}-${anomaliesByHost.anomaly.severity}-${anomaliesByHost.anomaly.jobId}`;
|
||||
};
|
||||
|
||||
export const createCompoundNetworkKey = (anomaliesByNetwork: AnomaliesByNetwork): string => {
|
||||
return `${anomaliesByNetwork.ip}-${anomaliesByNetwork.anomaly.entityName}-${
|
||||
anomaliesByNetwork.anomaly.entityValue
|
||||
}-${anomaliesByNetwork.anomaly.severity}-${anomaliesByNetwork.anomaly.jobId}`;
|
||||
};
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { dateTimesAreEqual } from './date_time_equality';
|
||||
import { AnomaliesTableProps } from '../types';
|
||||
|
||||
describe('date_time_equality', () => {
|
||||
test('it returns true if start and end date are equal', () => {
|
||||
const prev: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2000').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const next: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2000').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const equal = dateTimesAreEqual(prev, next);
|
||||
expect(equal).toEqual(true);
|
||||
});
|
||||
|
||||
test('it returns false if starts are not equal', () => {
|
||||
const prev: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('1999').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const next: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2000').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const equal = dateTimesAreEqual(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
|
||||
test('it returns false if starts are not equal for next', () => {
|
||||
const prev: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2000').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const next: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('1999').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const equal = dateTimesAreEqual(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
|
||||
test('it returns false if ends are not equal', () => {
|
||||
const prev: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2001').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const next: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2000').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const equal = dateTimesAreEqual(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
|
||||
test('it returns false if ends are not equal for next', () => {
|
||||
const prev: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2000').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const next: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2001').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const equal = dateTimesAreEqual(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
|
||||
test('it returns false if skip is not equal', () => {
|
||||
const prev: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2000').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: true,
|
||||
};
|
||||
const next: AnomaliesTableProps = {
|
||||
startDate: new Date('2000').valueOf(),
|
||||
endDate: new Date('2000').valueOf(),
|
||||
narrowDateRange: jest.fn(),
|
||||
skip: false,
|
||||
};
|
||||
const equal = dateTimesAreEqual(prev, next);
|
||||
expect(equal).toEqual(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AnomaliesTableProps } from '../types';
|
||||
|
||||
export const dateTimesAreEqual = (
|
||||
prevProps: AnomaliesTableProps,
|
||||
nextProps: AnomaliesTableProps
|
||||
): boolean =>
|
||||
prevProps.startDate === nextProps.startDate &&
|
||||
prevProps.endDate === nextProps.endDate &&
|
||||
prevProps.skip === nextProps.skip;
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { Columns } from '../../load_more_table';
|
||||
import { AnomaliesByHost, Anomaly, NarrowDateRange } from '../types';
|
||||
import { getRowItemDraggable } from '../../tables/helpers';
|
||||
import { EntityDraggable } from '../entity_draggable';
|
||||
import { createCompoundHostKey } from './create_compound_key';
|
||||
import { HostDetailsLink } from '../../links';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { AnomalyScore } from '../score/anomaly_score';
|
||||
|
||||
export const getAnomaliesHostTableColumns = (
|
||||
startDate: number,
|
||||
endDate: number,
|
||||
interval: string,
|
||||
narrowDateRange: NarrowDateRange
|
||||
): [
|
||||
Columns<AnomaliesByHost['hostName'], AnomaliesByHost>,
|
||||
Columns<Anomaly['severity'], AnomaliesByHost>,
|
||||
Columns<Anomaly['entityValue'], AnomaliesByHost>,
|
||||
Columns<Anomaly['influencers'], AnomaliesByHost>,
|
||||
Columns<Anomaly['jobId']>
|
||||
] => [
|
||||
{
|
||||
name: i18n.HOST_NAME,
|
||||
field: 'hostName',
|
||||
sortable: true,
|
||||
render: (hostName, anomaliesByHost) =>
|
||||
getRowItemDraggable({
|
||||
rowItem: hostName,
|
||||
attrName: 'host.name',
|
||||
idPrefix: `anomalies-host-table-hostName-${createCompoundHostKey(
|
||||
anomaliesByHost
|
||||
)}-hostName`,
|
||||
render: item => <HostDetailsLink hostName={item} />,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: i18n.SCORE,
|
||||
field: 'anomaly.severity',
|
||||
sortable: true,
|
||||
render: (_, anomaliesByHost) => (
|
||||
<AnomalyScore
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
jobKey={`anomalies-host-table-severity-${createCompoundHostKey(anomaliesByHost)}`}
|
||||
narrowDateRange={narrowDateRange}
|
||||
interval={interval}
|
||||
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.map(influencer => {
|
||||
const entityName = Object.keys(influencer)[0];
|
||||
const entityValue = Object.values(influencer)[0];
|
||||
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.DETECTOR,
|
||||
field: 'anomaly.jobId',
|
||||
sortable: true,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { Columns } from '../../load_more_table';
|
||||
import { Anomaly, NarrowDateRange, AnomaliesByNetwork } from '../types';
|
||||
import { getRowItemDraggable } from '../../tables/helpers';
|
||||
import { EntityDraggable } from '../entity_draggable';
|
||||
import { createCompoundNetworkKey } from './create_compound_key';
|
||||
import { IPDetailsLink } from '../../links';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { AnomalyScore } from '../score/anomaly_score';
|
||||
|
||||
export const getAnomaliesNetworkTableColumns = (
|
||||
startDate: number,
|
||||
endDate: number,
|
||||
interval: string,
|
||||
narrowDateRange: NarrowDateRange
|
||||
): [
|
||||
Columns<AnomaliesByNetwork['ip'], AnomaliesByNetwork>,
|
||||
Columns<Anomaly['severity'], AnomaliesByNetwork>,
|
||||
Columns<Anomaly['entityValue'], AnomaliesByNetwork>,
|
||||
Columns<Anomaly['influencers'], AnomaliesByNetwork>,
|
||||
Columns<Anomaly['jobId']>
|
||||
] => [
|
||||
{
|
||||
name: i18n.NETWORK_NAME,
|
||||
field: 'ip',
|
||||
sortable: true,
|
||||
render: (ip, anomaliesByNetwork) =>
|
||||
getRowItemDraggable({
|
||||
rowItem: ip,
|
||||
attrName: anomaliesByNetwork.type,
|
||||
idPrefix: `anomalies-network-table-ip-${createCompoundNetworkKey(anomaliesByNetwork)}`,
|
||||
render: item => <IPDetailsLink ip={item} />,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: i18n.SCORE,
|
||||
field: 'anomaly.severity',
|
||||
sortable: true,
|
||||
render: (_, anomaliesByNetwork) => (
|
||||
<AnomalyScore
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
jobKey={`anomalies-network-table-severity-${createCompoundNetworkKey(anomaliesByNetwork)}`}
|
||||
narrowDateRange={narrowDateRange}
|
||||
interval={interval}
|
||||
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.map(influencer => {
|
||||
const entityName = Object.keys(influencer)[0];
|
||||
const entityValue = Object.values(influencer)[0];
|
||||
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.DETECTOR,
|
||||
field: 'anomaly.jobId',
|
||||
sortable: true,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SHOWING = i18n.translate('xpack.siem.anomaliesTable.table.showingDescription', {
|
||||
defaultMessage: 'Showing',
|
||||
});
|
||||
|
||||
export const ANOMALIES = i18n.translate('xpack.siem.anomaliesTable.table.anomaliesDescription', {
|
||||
defaultMessage: 'Anomalies',
|
||||
});
|
||||
|
||||
export const LOADING = i18n.translate('xpack.siem.ml.table.loadingDescription', {
|
||||
defaultMessage: 'Loading…',
|
||||
});
|
||||
|
||||
export const SCORE = i18n.translate('xpack.siem.ml.table.scoreTitle', {
|
||||
defaultMessage: 'Score',
|
||||
});
|
||||
|
||||
export const HOST_NAME = i18n.translate('xpack.siem.ml.table.hostNameTitle', {
|
||||
defaultMessage: 'Host Name',
|
||||
});
|
||||
|
||||
export const INFLUENCED_BY = i18n.translate('xpack.siem.ml.table.influencedByTitle', {
|
||||
defaultMessage: 'Influenced By',
|
||||
});
|
||||
|
||||
export const ENTITY = i18n.translate('xpack.siem.ml.table.entityTitle', {
|
||||
defaultMessage: 'Entity',
|
||||
});
|
||||
|
||||
export const DETECTOR = i18n.translate('xpack.siem.ml.table.detectorTitle', {
|
||||
defaultMessage: 'Detector',
|
||||
});
|
||||
|
||||
export const NETWORK_NAME = i18n.translate('xpack.siem.ml.table.networkNameTitle', {
|
||||
defaultMessage: 'Network IP',
|
||||
});
|
81
x-pack/legacy/plugins/siem/public/components/ml/types.ts
Normal file
81
x-pack/legacy/plugins/siem/public/components/ml/types.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export interface Influencer {
|
||||
influencer_field_name: string;
|
||||
influencer_field_values: string[];
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
job_id: string;
|
||||
result_type: string;
|
||||
probability: number;
|
||||
multi_bucket_impact: number;
|
||||
record_score: number;
|
||||
initial_record_score: number;
|
||||
bucket_span: number;
|
||||
detector_index: number;
|
||||
is_interim: boolean;
|
||||
timestamp: number;
|
||||
by_field_name: string;
|
||||
by_field_value: string;
|
||||
partition_field_name: string;
|
||||
partition_field_value: string;
|
||||
function: string;
|
||||
function_description: string;
|
||||
typical: number[];
|
||||
actual: number[];
|
||||
influencers: Influencer[];
|
||||
}
|
||||
|
||||
export interface Influencer {
|
||||
influencer_field_name: string;
|
||||
influencer_field_values: string[];
|
||||
}
|
||||
|
||||
export interface InfluencerInput {
|
||||
fieldName: string;
|
||||
fieldValue: string;
|
||||
}
|
||||
|
||||
export interface Anomaly {
|
||||
detectorIndex: number;
|
||||
entityName: string;
|
||||
entityValue: string;
|
||||
influencers: Array<Record<string, string>>;
|
||||
jobId: string;
|
||||
rowId: string;
|
||||
severity: number;
|
||||
time: number;
|
||||
source: Source;
|
||||
}
|
||||
|
||||
export interface Anomalies {
|
||||
anomalies: Anomaly[];
|
||||
interval: string;
|
||||
}
|
||||
|
||||
export type NarrowDateRange = (score: Anomaly, interval: string) => void;
|
||||
|
||||
export interface AnomaliesByHost {
|
||||
hostName: string;
|
||||
anomaly: Anomaly;
|
||||
}
|
||||
|
||||
export type DestinationOrSource = 'source.ip' | 'destination.ip';
|
||||
|
||||
export interface AnomaliesByNetwork {
|
||||
type: DestinationOrSource;
|
||||
ip: string;
|
||||
anomaly: Anomaly;
|
||||
}
|
||||
|
||||
export interface AnomaliesTableProps {
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
narrowDateRange: NarrowDateRange;
|
||||
skip: boolean;
|
||||
}
|
|
@ -2,7 +2,142 @@
|
|||
|
||||
exports[`Host Summary Component rendering it renders the default Host Summary 1`] = `
|
||||
<Component>
|
||||
<pure(Component)
|
||||
<Memo()
|
||||
anomaliesData={
|
||||
Object {
|
||||
"anomalies": Array [
|
||||
Object {
|
||||
"detectorIndex": 0,
|
||||
"entityName": "process.name",
|
||||
"entityValue": "du",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"host.name": "zeek-iowa",
|
||||
},
|
||||
Object {
|
||||
"process.name": "du",
|
||||
},
|
||||
Object {
|
||||
"user.name": "root",
|
||||
},
|
||||
],
|
||||
"jobId": "job-1",
|
||||
"rowId": "1561157194802_0",
|
||||
"severity": 16.193669439507826,
|
||||
"source": Object {
|
||||
"actual": Array [
|
||||
1,
|
||||
],
|
||||
"bucket_span": 900,
|
||||
"by_field_name": "process.name",
|
||||
"by_field_value": "du",
|
||||
"detector_index": 0,
|
||||
"function": "rare",
|
||||
"function_description": "rare",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"influencer_field_name": "user.name",
|
||||
"influencer_field_values": Array [
|
||||
"root",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "process.name",
|
||||
"influencer_field_values": Array [
|
||||
"du",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "host.name",
|
||||
"influencer_field_values": Array [
|
||||
"zeek-iowa",
|
||||
],
|
||||
},
|
||||
],
|
||||
"initial_record_score": 16.193669439507826,
|
||||
"is_interim": false,
|
||||
"job_id": "job-1",
|
||||
"multi_bucket_impact": 0,
|
||||
"partition_field_name": "host.name",
|
||||
"partition_field_value": "zeek-iowa",
|
||||
"probability": 0.024041164411288146,
|
||||
"record_score": 16.193669439507826,
|
||||
"result_type": "record",
|
||||
"timestamp": 1560664800000,
|
||||
"typical": Array [
|
||||
0.024041164411288146,
|
||||
],
|
||||
},
|
||||
"time": 1560664800000,
|
||||
},
|
||||
Object {
|
||||
"detectorIndex": 0,
|
||||
"entityName": "process.name",
|
||||
"entityValue": "ls",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"host.name": "zeek-iowa",
|
||||
},
|
||||
Object {
|
||||
"process.name": "ls",
|
||||
},
|
||||
Object {
|
||||
"user.name": "root",
|
||||
},
|
||||
],
|
||||
"jobId": "job-2",
|
||||
"rowId": "1561157194802_1",
|
||||
"severity": 16.193669439507826,
|
||||
"source": Object {
|
||||
"actual": Array [
|
||||
1,
|
||||
],
|
||||
"bucket_span": 900,
|
||||
"by_field_name": "process.name",
|
||||
"by_field_value": "ls",
|
||||
"detector_index": 0,
|
||||
"function": "rare",
|
||||
"function_description": "rare",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"influencer_field_name": "user.name",
|
||||
"influencer_field_values": Array [
|
||||
"root",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "process.name",
|
||||
"influencer_field_values": Array [
|
||||
"ls",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "host.name",
|
||||
"influencer_field_values": Array [
|
||||
"zeek-iowa",
|
||||
],
|
||||
},
|
||||
],
|
||||
"initial_record_score": 16.193669439507826,
|
||||
"is_interim": false,
|
||||
"job_id": "job-2",
|
||||
"multi_bucket_impact": 0,
|
||||
"partition_field_name": "host.name",
|
||||
"partition_field_value": "zeek-iowa",
|
||||
"probability": 0.024041164411288146,
|
||||
"record_score": 16.193669439507826,
|
||||
"result_type": "record",
|
||||
"timestamp": 1560664800000,
|
||||
"typical": Array [
|
||||
0.024041164411288146,
|
||||
],
|
||||
},
|
||||
"time": 1560664800000,
|
||||
},
|
||||
],
|
||||
"interval": "day",
|
||||
}
|
||||
}
|
||||
data={
|
||||
Object {
|
||||
"_id": "yneHlmgBjVl2VqDlAjPR",
|
||||
|
@ -58,7 +193,11 @@ exports[`Host Summary Component rendering it renders the default Host Summary 1`
|
|||
},
|
||||
}
|
||||
}
|
||||
endDate={1560837600000}
|
||||
isLoadingAnomaliesData={false}
|
||||
loading={false}
|
||||
narrowDateRange={[MockFunction]}
|
||||
startDate={1560578400000}
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
|
|
|
@ -11,6 +11,7 @@ import { TestProviders } from '../../../../mock';
|
|||
|
||||
import { HostOverview } from './index';
|
||||
import { mockData } from './mock';
|
||||
import { mockAnomalies } from '../../../ml/mock';
|
||||
|
||||
describe('Host Summary Component', () => {
|
||||
// this is just a little hack to silence a warning that we'll get until react
|
||||
|
@ -38,7 +39,15 @@ describe('Host Summary Component', () => {
|
|||
test('it renders the default Host Summary', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
<HostOverview loading={false} data={mockData.Hosts.edges[0].node} />
|
||||
<HostOverview
|
||||
startDate={new Date('2019-06-15T06:00:00.000Z').valueOf()}
|
||||
endDate={new Date('2019-06-18T06:00:00.000Z').valueOf()}
|
||||
loading={false}
|
||||
data={mockData.Hosts.edges[0].node}
|
||||
anomaliesData={mockAnomalies}
|
||||
isLoadingAnomaliesData={false}
|
||||
narrowDateRange={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
import { EuiDescriptionList, EuiFlexItem } from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DescriptionList } from '../../../../../common/utility_types';
|
||||
import { HostItem } from '../../../../graphql/types';
|
||||
import { getEmptyTagValue } from '../../../empty_value';
|
||||
|
||||
|
@ -19,20 +19,20 @@ import { DefaultFieldRenderer, hostIdRenderer } from '../../../field_renderers/f
|
|||
import { LoadingPanel } from '../../../loading';
|
||||
import { LoadingOverlay, OverviewWrapper } from '../../index';
|
||||
import { IPDetailsLink } from '../../../links';
|
||||
import { AnomalyScores } from '../../../ml/score/anomaly_scores';
|
||||
import { Anomalies, NarrowDateRange } from '../../../ml/types';
|
||||
|
||||
interface DescriptionList {
|
||||
title: string;
|
||||
description: JSX.Element;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
interface HostSummaryProps {
|
||||
data: HostItem;
|
||||
loading: boolean;
|
||||
isLoadingAnomaliesData: boolean;
|
||||
anomaliesData: Anomalies | null;
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
narrowDateRange: NarrowDateRange;
|
||||
}
|
||||
|
||||
type HostSummaryProps = OwnProps;
|
||||
|
||||
const DescriptionList = styled(EuiDescriptionList)`
|
||||
const DescriptionListStyled = styled(EuiDescriptionList)`
|
||||
${({ theme }) => `
|
||||
dt {
|
||||
font-size: ${theme.eui.euiFontSizeXS} !important;
|
||||
|
@ -42,112 +42,136 @@ const DescriptionList = styled(EuiDescriptionList)`
|
|||
|
||||
const getDescriptionList = (descriptionList: DescriptionList[], key: number) => (
|
||||
<EuiFlexItem key={key}>
|
||||
<DescriptionList listItems={descriptionList} />
|
||||
<DescriptionListStyled listItems={descriptionList} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
||||
export const HostOverview = pure<HostSummaryProps>(({ data, loading }) => {
|
||||
const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => (
|
||||
<DefaultFieldRenderer
|
||||
rowItems={getOr([], fieldName, fieldData)}
|
||||
attrName={fieldName}
|
||||
idPrefix="host-overview"
|
||||
/>
|
||||
);
|
||||
export const HostOverview = React.memo<HostSummaryProps>(
|
||||
({
|
||||
data,
|
||||
loading,
|
||||
startDate,
|
||||
endDate,
|
||||
isLoadingAnomaliesData,
|
||||
anomaliesData,
|
||||
narrowDateRange,
|
||||
}) => {
|
||||
const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => (
|
||||
<DefaultFieldRenderer
|
||||
rowItems={getOr([], fieldName, fieldData)}
|
||||
attrName={fieldName}
|
||||
idPrefix="host-overview"
|
||||
/>
|
||||
);
|
||||
|
||||
const descriptionLists: Readonly<DescriptionList[][]> = [
|
||||
[
|
||||
{
|
||||
title: i18n.HOST_ID,
|
||||
description: data.host
|
||||
? hostIdRenderer({ host: data.host, noLink: true })
|
||||
: getEmptyTagValue(),
|
||||
},
|
||||
{
|
||||
title: i18n.FIRST_SEEN,
|
||||
description:
|
||||
data.host != null && data.host.name && data.host.name.length ? (
|
||||
<FirstLastSeenHost
|
||||
hostname={data.host.name[0]}
|
||||
type={FirstLastSeenHostType.FIRST_SEEN}
|
||||
const descriptionLists: Readonly<DescriptionList[][]> = [
|
||||
[
|
||||
{
|
||||
title: i18n.HOST_ID,
|
||||
description: data.host
|
||||
? hostIdRenderer({ host: data.host, noLink: true })
|
||||
: getEmptyTagValue(),
|
||||
},
|
||||
{
|
||||
title: i18n.FIRST_SEEN,
|
||||
description:
|
||||
data.host != null && data.host.name && data.host.name.length ? (
|
||||
<FirstLastSeenHost
|
||||
hostname={data.host.name[0]}
|
||||
type={FirstLastSeenHostType.FIRST_SEEN}
|
||||
/>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.LAST_SEEN,
|
||||
description:
|
||||
data.host != null && data.host.name && data.host.name.length ? (
|
||||
<FirstLastSeenHost
|
||||
hostname={data.host.name[0]}
|
||||
type={FirstLastSeenHostType.LAST_SEEN}
|
||||
/>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.MAX_ANOMALY_SCORE_BY_JOB,
|
||||
description: (
|
||||
<AnomalyScores
|
||||
anomalies={anomaliesData}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
isLoading={isLoadingAnomaliesData}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.LAST_SEEN,
|
||||
description:
|
||||
data.host != null && data.host.name && data.host.name.length ? (
|
||||
<FirstLastSeenHost
|
||||
hostname={data.host.name[0]}
|
||||
type={FirstLastSeenHostType.LAST_SEEN}
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
title: i18n.IP_ADDRESSES,
|
||||
description: (
|
||||
<DefaultFieldRenderer
|
||||
rowItems={getOr([], 'host.ip', data)}
|
||||
attrName={'host.ip'}
|
||||
idPrefix="host-overview"
|
||||
render={ip => (ip != null ? <IPDetailsLink ip={ip} /> : getEmptyTagValue())}
|
||||
/>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
title: i18n.IP_ADDRESSES,
|
||||
description: (
|
||||
<DefaultFieldRenderer
|
||||
rowItems={getOr([], 'host.ip', data)}
|
||||
attrName={'host.ip'}
|
||||
idPrefix="host-overview"
|
||||
render={ip => (ip != null ? <IPDetailsLink ip={ip} /> : getEmptyTagValue())}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.MAC_ADDRESSES,
|
||||
description: getDefaultRenderer('host.mac', data),
|
||||
},
|
||||
{ title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) },
|
||||
],
|
||||
[
|
||||
{ title: i18n.OS, description: getDefaultRenderer('host.os.name', data) },
|
||||
{ title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) },
|
||||
{ title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) },
|
||||
{ title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) },
|
||||
],
|
||||
[
|
||||
{
|
||||
title: i18n.CLOUD_PROVIDER,
|
||||
description: getDefaultRenderer('cloud.provider', data),
|
||||
},
|
||||
{
|
||||
title: i18n.REGION,
|
||||
description: getDefaultRenderer('cloud.region', data),
|
||||
},
|
||||
{
|
||||
title: i18n.INSTANCE_ID,
|
||||
description: getDefaultRenderer('cloud.instance.id', data),
|
||||
},
|
||||
{
|
||||
title: i18n.MACHINE_TYPE,
|
||||
description: getDefaultRenderer('cloud.machine.type', data),
|
||||
},
|
||||
],
|
||||
];
|
||||
},
|
||||
{
|
||||
title: i18n.MAC_ADDRESSES,
|
||||
description: getDefaultRenderer('host.mac', data),
|
||||
},
|
||||
{ title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) },
|
||||
],
|
||||
[
|
||||
{ title: i18n.OS, description: getDefaultRenderer('host.os.name', data) },
|
||||
{ title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) },
|
||||
{ title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) },
|
||||
{ title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) },
|
||||
],
|
||||
[
|
||||
{
|
||||
title: i18n.CLOUD_PROVIDER,
|
||||
description: getDefaultRenderer('cloud.provider', data),
|
||||
},
|
||||
{
|
||||
title: i18n.REGION,
|
||||
description: getDefaultRenderer('cloud.region', data),
|
||||
},
|
||||
{
|
||||
title: i18n.INSTANCE_ID,
|
||||
description: getDefaultRenderer('cloud.instance.id', data),
|
||||
},
|
||||
{
|
||||
title: i18n.MACHINE_TYPE,
|
||||
description: getDefaultRenderer('cloud.machine.type', data),
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return (
|
||||
<OverviewWrapper>
|
||||
{loading && (
|
||||
<>
|
||||
<LoadingOverlay />
|
||||
<LoadingPanel
|
||||
height="100%"
|
||||
width="100%"
|
||||
text=""
|
||||
position="absolute"
|
||||
zIndex={3}
|
||||
data-test-subj="LoadingPanelLoadMoreTable"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{descriptionLists.map((descriptionList, index) => getDescriptionList(descriptionList, index))}
|
||||
</OverviewWrapper>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<OverviewWrapper>
|
||||
{loading && (
|
||||
<>
|
||||
<LoadingOverlay />
|
||||
<LoadingPanel
|
||||
height="100%"
|
||||
width="100%"
|
||||
text=""
|
||||
position="absolute"
|
||||
zIndex={3}
|
||||
data-test-subj="LoadingPanelLoadMoreTable"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{descriptionLists.map((descriptionList, index) =>
|
||||
getDescriptionList(descriptionList, index)
|
||||
)}
|
||||
</OverviewWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -18,6 +18,13 @@ export const LAST_SEEN = i18n.translate('xpack.siem.host.details.lastSeenTitle',
|
|||
defaultMessage: 'Last Seen',
|
||||
});
|
||||
|
||||
export const MAX_ANOMALY_SCORE_BY_JOB = i18n.translate(
|
||||
'xpack.siem.host.details.overview.maxAnomalyScoreByJobTitle',
|
||||
{
|
||||
defaultMessage: 'Max Anomaly Score By Job',
|
||||
}
|
||||
);
|
||||
|
||||
export const IP_ADDRESSES = i18n.translate('xpack.siem.host.details.overview.ipAddressesTitle', {
|
||||
defaultMessage: 'IP Addresses',
|
||||
});
|
||||
|
|
|
@ -13,9 +13,148 @@ exports[`IP Overview Component rendering it renders the default IP Overview 1`]
|
|||
}
|
||||
>
|
||||
<pure(Component)
|
||||
anomaliesData={
|
||||
Object {
|
||||
"anomalies": Array [
|
||||
Object {
|
||||
"detectorIndex": 0,
|
||||
"entityName": "process.name",
|
||||
"entityValue": "du",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"host.name": "zeek-iowa",
|
||||
},
|
||||
Object {
|
||||
"process.name": "du",
|
||||
},
|
||||
Object {
|
||||
"user.name": "root",
|
||||
},
|
||||
],
|
||||
"jobId": "job-1",
|
||||
"rowId": "1561157194802_0",
|
||||
"severity": 16.193669439507826,
|
||||
"source": Object {
|
||||
"actual": Array [
|
||||
1,
|
||||
],
|
||||
"bucket_span": 900,
|
||||
"by_field_name": "process.name",
|
||||
"by_field_value": "du",
|
||||
"detector_index": 0,
|
||||
"function": "rare",
|
||||
"function_description": "rare",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"influencer_field_name": "user.name",
|
||||
"influencer_field_values": Array [
|
||||
"root",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "process.name",
|
||||
"influencer_field_values": Array [
|
||||
"du",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "host.name",
|
||||
"influencer_field_values": Array [
|
||||
"zeek-iowa",
|
||||
],
|
||||
},
|
||||
],
|
||||
"initial_record_score": 16.193669439507826,
|
||||
"is_interim": false,
|
||||
"job_id": "job-1",
|
||||
"multi_bucket_impact": 0,
|
||||
"partition_field_name": "host.name",
|
||||
"partition_field_value": "zeek-iowa",
|
||||
"probability": 0.024041164411288146,
|
||||
"record_score": 16.193669439507826,
|
||||
"result_type": "record",
|
||||
"timestamp": 1560664800000,
|
||||
"typical": Array [
|
||||
0.024041164411288146,
|
||||
],
|
||||
},
|
||||
"time": 1560664800000,
|
||||
},
|
||||
Object {
|
||||
"detectorIndex": 0,
|
||||
"entityName": "process.name",
|
||||
"entityValue": "ls",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"host.name": "zeek-iowa",
|
||||
},
|
||||
Object {
|
||||
"process.name": "ls",
|
||||
},
|
||||
Object {
|
||||
"user.name": "root",
|
||||
},
|
||||
],
|
||||
"jobId": "job-2",
|
||||
"rowId": "1561157194802_1",
|
||||
"severity": 16.193669439507826,
|
||||
"source": Object {
|
||||
"actual": Array [
|
||||
1,
|
||||
],
|
||||
"bucket_span": 900,
|
||||
"by_field_name": "process.name",
|
||||
"by_field_value": "ls",
|
||||
"detector_index": 0,
|
||||
"function": "rare",
|
||||
"function_description": "rare",
|
||||
"influencers": Array [
|
||||
Object {
|
||||
"influencer_field_name": "user.name",
|
||||
"influencer_field_values": Array [
|
||||
"root",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "process.name",
|
||||
"influencer_field_values": Array [
|
||||
"ls",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"influencer_field_name": "host.name",
|
||||
"influencer_field_values": Array [
|
||||
"zeek-iowa",
|
||||
],
|
||||
},
|
||||
],
|
||||
"initial_record_score": 16.193669439507826,
|
||||
"is_interim": false,
|
||||
"job_id": "job-2",
|
||||
"multi_bucket_impact": 0,
|
||||
"partition_field_name": "host.name",
|
||||
"partition_field_value": "zeek-iowa",
|
||||
"probability": 0.024041164411288146,
|
||||
"record_score": 16.193669439507826,
|
||||
"result_type": "record",
|
||||
"timestamp": 1560664800000,
|
||||
"typical": Array [
|
||||
0.024041164411288146,
|
||||
],
|
||||
},
|
||||
"time": 1560664800000,
|
||||
},
|
||||
],
|
||||
"interval": "day",
|
||||
}
|
||||
}
|
||||
endDate={1560837600000}
|
||||
flowTarget="source"
|
||||
ip="10.10.10.10"
|
||||
isLoadingAnomaliesData={false}
|
||||
loading={false}
|
||||
narrowDateRange={[MockFunction]}
|
||||
startDate={1560578400000}
|
||||
type="details"
|
||||
updateFlowTargetAction={[MockFunction]}
|
||||
/>
|
||||
|
|
|
@ -15,6 +15,8 @@ import { createStore, networkModel, State } from '../../../../store';
|
|||
|
||||
import { IpOverview } from './index';
|
||||
import { mockData } from './mock';
|
||||
import { mockAnomalies } from '../../../ml/mock';
|
||||
import { NarrowDateRange } from '../../../ml/types';
|
||||
|
||||
describe('IP Overview Component', () => {
|
||||
const state: State = mockGlobalState;
|
||||
|
@ -35,6 +37,11 @@ describe('IP Overview Component', () => {
|
|||
updateFlowTargetAction: (jest.fn() as unknown) as ActionCreator<{
|
||||
flowTarget: FlowTarget;
|
||||
}>,
|
||||
startDate: new Date('2019-06-15T06:00:00.000Z').valueOf(),
|
||||
endDate: new Date('2019-06-18T06:00:00.000Z').valueOf(),
|
||||
anomaliesData: mockAnomalies,
|
||||
isLoadingAnomaliesData: false,
|
||||
narrowDateRange: (jest.fn() as unknown) as NarrowDateRange,
|
||||
};
|
||||
|
||||
test('it renders the default IP Overview', () => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import { pure } from 'recompose';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { DescriptionList } from '../../../../../common/utility_types';
|
||||
import { FlowTarget, IpOverviewData, Overview } from '../../../../graphql/types';
|
||||
import { networkModel } from '../../../../store';
|
||||
import { getEmptyTagValue } from '../../../empty_value';
|
||||
|
@ -25,23 +26,25 @@ import {
|
|||
import * as i18n from './translations';
|
||||
import { LoadingOverlay, OverviewWrapper } from '../../index';
|
||||
import { LoadingPanel } from '../../../loading';
|
||||
|
||||
interface DescriptionList {
|
||||
title: string;
|
||||
description: JSX.Element;
|
||||
}
|
||||
import { Anomalies, NarrowDateRange } from '../../../ml/types';
|
||||
import { AnomalyScores } from '../../../ml/score/anomaly_scores';
|
||||
|
||||
interface OwnProps {
|
||||
data: IpOverviewData;
|
||||
flowTarget: FlowTarget;
|
||||
ip: string;
|
||||
loading: boolean;
|
||||
isLoadingAnomaliesData: boolean;
|
||||
anomaliesData: Anomalies | null;
|
||||
startDate: number;
|
||||
endDate: number;
|
||||
type: networkModel.NetworkType;
|
||||
narrowDateRange: NarrowDateRange;
|
||||
}
|
||||
|
||||
export type IpOverviewProps = OwnProps;
|
||||
|
||||
const DescriptionList = styled(EuiDescriptionList)`
|
||||
const DescriptionListStyled = styled(EuiDescriptionList)`
|
||||
${({ theme }) => `
|
||||
dt {
|
||||
font-size: ${theme.eui.euiFontSizeXS} !important;
|
||||
|
@ -52,66 +55,92 @@ const DescriptionList = styled(EuiDescriptionList)`
|
|||
const getDescriptionList = (descriptionList: DescriptionList[], key: number) => {
|
||||
return (
|
||||
<EuiFlexItem key={key}>
|
||||
<DescriptionList listItems={descriptionList} />
|
||||
<DescriptionListStyled listItems={descriptionList} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const IpOverview = pure<IpOverviewProps>(({ ip, data, loading, flowTarget }) => {
|
||||
const typeData: Overview = data[flowTarget]!;
|
||||
const descriptionLists: Readonly<DescriptionList[][]> = [
|
||||
[
|
||||
{
|
||||
title: i18n.LOCATION,
|
||||
description: locationRenderer(
|
||||
[`${flowTarget}.geo.city_name`, `${flowTarget}.geo.region_name`],
|
||||
data
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.AUTONOMOUS_SYSTEM,
|
||||
description: typeData
|
||||
? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget)
|
||||
: getEmptyTagValue(),
|
||||
},
|
||||
],
|
||||
[
|
||||
{ title: i18n.FIRST_SEEN, description: dateRenderer('firstSeen', typeData) },
|
||||
{ title: i18n.LAST_SEEN, description: dateRenderer('lastSeen', typeData) },
|
||||
],
|
||||
[
|
||||
{
|
||||
title: i18n.HOST_ID,
|
||||
description: typeData
|
||||
? hostIdRenderer({ host: data.host, ipFilter: ip })
|
||||
: getEmptyTagValue(),
|
||||
},
|
||||
{
|
||||
title: i18n.HOST_NAME,
|
||||
description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(),
|
||||
},
|
||||
],
|
||||
[
|
||||
{ title: i18n.WHOIS, description: whoisRenderer(ip) },
|
||||
{ title: i18n.REPUTATION, description: reputationRenderer(ip) },
|
||||
],
|
||||
];
|
||||
return (
|
||||
<OverviewWrapper>
|
||||
{loading && (
|
||||
<>
|
||||
<LoadingOverlay />
|
||||
<LoadingPanel
|
||||
height="100%"
|
||||
width="100%"
|
||||
text=""
|
||||
position="absolute"
|
||||
zIndex={3}
|
||||
data-test-subj="LoadingPanelLoadMoreTable"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{descriptionLists.map((descriptionList, index) => getDescriptionList(descriptionList, index))}
|
||||
</OverviewWrapper>
|
||||
);
|
||||
});
|
||||
export const IpOverview = pure<IpOverviewProps>(
|
||||
({
|
||||
ip,
|
||||
data,
|
||||
loading,
|
||||
flowTarget,
|
||||
startDate,
|
||||
endDate,
|
||||
isLoadingAnomaliesData,
|
||||
anomaliesData,
|
||||
narrowDateRange,
|
||||
}) => {
|
||||
const typeData: Overview = data[flowTarget]!;
|
||||
const descriptionLists: Readonly<DescriptionList[][]> = [
|
||||
[
|
||||
{
|
||||
title: i18n.LOCATION,
|
||||
description: locationRenderer(
|
||||
[`${flowTarget}.geo.city_name`, `${flowTarget}.geo.region_name`],
|
||||
data
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.AUTONOMOUS_SYSTEM,
|
||||
description: typeData
|
||||
? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget)
|
||||
: getEmptyTagValue(),
|
||||
},
|
||||
{
|
||||
title: i18n.MAX_ANOMALY_SCORE_BY_JOB,
|
||||
description: (
|
||||
<AnomalyScores
|
||||
anomalies={anomaliesData}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
isLoading={isLoadingAnomaliesData}
|
||||
narrowDateRange={narrowDateRange}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
{ title: i18n.FIRST_SEEN, description: dateRenderer('firstSeen', typeData) },
|
||||
{ title: i18n.LAST_SEEN, description: dateRenderer('lastSeen', typeData) },
|
||||
],
|
||||
[
|
||||
{
|
||||
title: i18n.HOST_ID,
|
||||
description: typeData
|
||||
? hostIdRenderer({ host: data.host, ipFilter: ip })
|
||||
: getEmptyTagValue(),
|
||||
},
|
||||
{
|
||||
title: i18n.HOST_NAME,
|
||||
description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(),
|
||||
},
|
||||
],
|
||||
[
|
||||
{ title: i18n.WHOIS, description: whoisRenderer(ip) },
|
||||
{ title: i18n.REPUTATION, description: reputationRenderer(ip) },
|
||||
],
|
||||
];
|
||||
return (
|
||||
<OverviewWrapper>
|
||||
{loading && (
|
||||
<>
|
||||
<LoadingOverlay />
|
||||
<LoadingPanel
|
||||
height="100%"
|
||||
width="100%"
|
||||
text=""
|
||||
position="absolute"
|
||||
zIndex={3}
|
||||
data-test-subj="LoadingPanelLoadMoreTable"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{descriptionLists.map((descriptionList, index) =>
|
||||
getDescriptionList(descriptionList, index)
|
||||
)}
|
||||
</OverviewWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -21,6 +21,13 @@ export const AUTONOMOUS_SYSTEM = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const MAX_ANOMALY_SCORE_BY_JOB = i18n.translate(
|
||||
'xpack.siem.network.ipDetails.ipOverview.maxAnomalyScoreByJobTitle',
|
||||
{
|
||||
defaultMessage: 'Max Anomaly Score By Job',
|
||||
}
|
||||
);
|
||||
|
||||
export const FIRST_SEEN = i18n.translate('xpack.siem.network.ipDetails.ipOverview.firstSeenTitle', {
|
||||
defaultMessage: 'First Seen',
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import { pure } from 'recompose';
|
|||
import { Breadcrumb } from 'ui/chrome';
|
||||
import { StaticIndexPattern } from 'ui/index_patterns';
|
||||
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import { ESTermQuery } from '../../../common/typed_json';
|
||||
import { FiltersGlobal } from '../../components/filters_global';
|
||||
import { HeaderPage } from '../../components/header_page';
|
||||
|
@ -36,6 +37,11 @@ import { hostsModel, hostsSelectors, State } from '../../store';
|
|||
import { HostsEmptyPage } from './hosts_empty_page';
|
||||
import { HostsKql } from './kql';
|
||||
import * as i18n from './translations';
|
||||
import { AnomalyTableProvider } from '../../components/ml/anomaly/anomaly_table_provider';
|
||||
import { hostToInfluencers } from '../../components/ml/influencers/host_to_influencers';
|
||||
import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../store/inputs/actions';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime';
|
||||
import { KpiHostDetailsQuery } from '../../containers/kpi_host_details';
|
||||
|
||||
const type = hostsModel.HostsType.details;
|
||||
|
@ -48,6 +54,11 @@ const KpiHostDetailsManage = manageQuery(KpiHostsComponent);
|
|||
|
||||
interface HostDetailsComponentReduxProps {
|
||||
filterQueryExpression: string;
|
||||
setAbsoluteRangeDatePicker: ActionCreator<{
|
||||
id: InputsModelId;
|
||||
from: number;
|
||||
to: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
type HostDetailsComponentProps = HostDetailsComponentReduxProps & HostComponentProps;
|
||||
|
@ -58,6 +69,7 @@ const HostDetailsComponent = pure<HostDetailsComponentProps>(
|
|||
params: { hostName },
|
||||
},
|
||||
filterQueryExpression,
|
||||
setAbsoluteRangeDatePicker,
|
||||
}) => (
|
||||
<WithSource sourceId="default">
|
||||
{({ indicesExist, indexPattern }) =>
|
||||
|
@ -87,13 +99,33 @@ const HostDetailsComponent = pure<HostDetailsComponentProps>(
|
|||
endDate={to}
|
||||
>
|
||||
{({ hostOverview, loading, id, refetch }) => (
|
||||
<HostOverviewManage
|
||||
id={id}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
data={hostOverview}
|
||||
loading={loading}
|
||||
/>
|
||||
<AnomalyTableProvider
|
||||
influencers={hostToInfluencers(hostOverview)}
|
||||
startDate={from}
|
||||
endDate={to}
|
||||
>
|
||||
{({ isLoadingAnomaliesData, anomaliesData }) => (
|
||||
<HostOverviewManage
|
||||
id={id}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
data={hostOverview}
|
||||
anomaliesData={anomaliesData}
|
||||
isLoadingAnomaliesData={isLoadingAnomaliesData}
|
||||
loading={loading}
|
||||
startDate={from}
|
||||
endDate={to}
|
||||
narrowDateRange={(score, interval) => {
|
||||
const fromTo = scoreIntervalToDateTime(score, interval);
|
||||
setAbsoluteRangeDatePicker({
|
||||
id: 'global',
|
||||
from: fromTo.from,
|
||||
to: fromTo.to,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnomalyTableProvider>
|
||||
)}
|
||||
</HostOverviewByNameQuery>
|
||||
|
||||
|
@ -236,7 +268,12 @@ const makeMapStateToProps = () => {
|
|||
});
|
||||
};
|
||||
|
||||
export const HostDetails = connect(makeMapStateToProps)(HostDetailsComponent);
|
||||
export const HostDetails = connect(
|
||||
makeMapStateToProps,
|
||||
{
|
||||
setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker,
|
||||
}
|
||||
)(HostDetailsComponent);
|
||||
|
||||
export const getBreadcrumbs = (hostId: string): Breadcrumb[] => [
|
||||
{
|
||||
|
|
|
@ -11,6 +11,7 @@ import { connect } from 'react-redux';
|
|||
import { StickyContainer } from 'react-sticky';
|
||||
import { pure } from 'recompose';
|
||||
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import { FiltersGlobal } from '../../components/filters_global';
|
||||
import { HeaderPage } from '../../components/header_page';
|
||||
import { LastEventTime } from '../../components/last_event_time';
|
||||
|
@ -36,19 +37,29 @@ import { hostsModel, hostsSelectors, State } from '../../store';
|
|||
import { HostsEmptyPage } from './hosts_empty_page';
|
||||
import { HostsKql } from './kql';
|
||||
import * as i18n from './translations';
|
||||
import { AnomaliesHostTable } from '../../components/ml/tables/anomalies_host_table';
|
||||
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime';
|
||||
|
||||
const AuthenticationTableManage = manageQuery(AuthenticationTable);
|
||||
const HostsTableManage = manageQuery(HostsTable);
|
||||
const EventsTableManage = manageQuery(EventsTable);
|
||||
const UncommonProcessTableManage = manageQuery(UncommonProcessTable);
|
||||
const KpiHostsComponentManage = manageQuery(KpiHostsComponent);
|
||||
|
||||
interface HostsComponentReduxProps {
|
||||
filterQuery: string;
|
||||
setAbsoluteRangeDatePicker: ActionCreator<{
|
||||
id: InputsModelId;
|
||||
from: number;
|
||||
to: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
type HostsComponentProps = HostsComponentReduxProps;
|
||||
|
||||
const HostsComponent = pure<HostsComponentProps>(({ filterQuery }) => (
|
||||
const HostsComponent = pure<HostsComponentProps>(({ filterQuery, setAbsoluteRangeDatePicker }) => (
|
||||
<WithSource sourceId="default">
|
||||
{({ indicesExist, indexPattern }) =>
|
||||
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
|
||||
|
@ -182,6 +193,22 @@ const HostsComponent = pure<HostsComponentProps>(({ filterQuery }) => (
|
|||
|
||||
<EuiSpacer />
|
||||
|
||||
<AnomaliesHostTable
|
||||
startDate={from}
|
||||
endDate={to}
|
||||
skip={isInitializing}
|
||||
narrowDateRange={(score, interval) => {
|
||||
const fromTo = scoreIntervalToDateTime(score, interval);
|
||||
setAbsoluteRangeDatePicker({
|
||||
id: 'global',
|
||||
from: fromTo.from,
|
||||
to: fromTo.to,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EventsQuery
|
||||
endDate={to}
|
||||
filterQuery={filterQuery}
|
||||
|
@ -230,4 +257,9 @@ const makeMapStateToProps = () => {
|
|||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export const Hosts = connect(makeMapStateToProps)(HostsComponent);
|
||||
export const Hosts = connect(
|
||||
makeMapStateToProps,
|
||||
{
|
||||
setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker,
|
||||
}
|
||||
)(HostsComponent);
|
||||
|
|
|
@ -42,5 +42,6 @@ exports[`Ip Details it matches the snapshot 1`] = `
|
|||
"url": "",
|
||||
}
|
||||
}
|
||||
setAbsoluteRangeDatePicker={[MockFunction]}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -18,10 +18,14 @@ import { FlowTarget } from '../../graphql/types';
|
|||
import { createStore, State } from '../../store';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { mocksSource } from '../../containers/source/mock';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
|
||||
type Action = 'PUSH' | 'POP' | 'REPLACE';
|
||||
const pop: Action = 'POP';
|
||||
|
||||
type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock };
|
||||
|
||||
let localSource: Array<{
|
||||
request: {};
|
||||
result: {
|
||||
|
@ -65,6 +69,11 @@ const getMockProps = (ip: string) => ({
|
|||
hash: '',
|
||||
},
|
||||
match: { params: { ip }, isExact: true, path: '', url: '' },
|
||||
setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{
|
||||
id: InputsModelId;
|
||||
from: number;
|
||||
to: number;
|
||||
}>,
|
||||
});
|
||||
|
||||
jest.mock('ui/documentation_links', () => ({
|
||||
|
@ -72,6 +81,7 @@ jest.mock('ui/documentation_links', () => ({
|
|||
siem: 'http://www.example.com',
|
||||
},
|
||||
}));
|
||||
|
||||
// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
|
||||
/* eslint-disable no-console */
|
||||
const originalError = console.error;
|
||||
|
@ -79,10 +89,19 @@ const originalError = console.error;
|
|||
describe('Ip Details', () => {
|
||||
beforeAll(() => {
|
||||
console.error = jest.fn();
|
||||
(global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => {
|
||||
return null;
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.error = originalError;
|
||||
delete (global as GlobalWithFetch).fetch;
|
||||
});
|
||||
const state: State = mockGlobalState;
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { StickyContainer } from 'react-sticky';
|
|||
import { pure } from 'recompose';
|
||||
import { Breadcrumb } from 'ui/chrome';
|
||||
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import { FiltersGlobal } from '../../components/filters_global';
|
||||
import { HeaderPage } from '../../components/header_page';
|
||||
import { LastEventTime } from '../../components/last_event_time';
|
||||
|
@ -32,10 +33,15 @@ import { UsersQuery } from '../../containers/users';
|
|||
import { FlowTarget, LastEventIndexKey } from '../../graphql/types';
|
||||
import { decodeIpv6 } from '../../lib/helpers';
|
||||
import { networkModel, networkSelectors, State } from '../../store';
|
||||
import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../store/inputs/actions';
|
||||
|
||||
import { NetworkKql } from './kql';
|
||||
import { NetworkEmptyPage } from './network_empty_page';
|
||||
import * as i18n from './translations';
|
||||
import { AnomalyTableProvider } from '../../components/ml/anomaly/anomaly_table_provider';
|
||||
import { networkToInfluencers } from '../../components/ml/influencers/network_to_influencers';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime';
|
||||
|
||||
const DomainsTableManage = manageQuery(DomainsTable);
|
||||
const TlsTableManage = manageQuery(TlsTable);
|
||||
|
@ -44,6 +50,11 @@ const UsersTableManage = manageQuery(UsersTable);
|
|||
interface IPDetailsComponentReduxProps {
|
||||
filterQuery: string;
|
||||
flowTarget: FlowTarget;
|
||||
setAbsoluteRangeDatePicker: ActionCreator<{
|
||||
id: InputsModelId;
|
||||
from: number;
|
||||
to: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
type IPDetailsComponentProps = IPDetailsComponentReduxProps & NetworkComponentProps;
|
||||
|
@ -55,6 +66,7 @@ export const IPDetailsComponent = pure<IPDetailsComponentProps>(
|
|||
},
|
||||
filterQuery,
|
||||
flowTarget,
|
||||
setAbsoluteRangeDatePicker,
|
||||
}) => (
|
||||
<WithSource sourceId="default" data-test-subj="ip-details-page">
|
||||
{({ indicesExist, indexPattern }) =>
|
||||
|
@ -87,13 +99,33 @@ export const IPDetailsComponent = pure<IPDetailsComponentProps>(
|
|||
ip={decodeIpv6(ip)}
|
||||
>
|
||||
{({ ipOverviewData, loading }) => (
|
||||
<IpOverview
|
||||
ip={decodeIpv6(ip)}
|
||||
data={ipOverviewData}
|
||||
loading={loading}
|
||||
type={networkModel.NetworkType.details}
|
||||
flowTarget={flowTarget}
|
||||
/>
|
||||
<AnomalyTableProvider
|
||||
influencers={networkToInfluencers(ip)}
|
||||
startDate={from}
|
||||
endDate={to}
|
||||
>
|
||||
{({ isLoadingAnomaliesData, anomaliesData }) => (
|
||||
<IpOverview
|
||||
ip={decodeIpv6(ip)}
|
||||
data={ipOverviewData}
|
||||
anomaliesData={anomaliesData}
|
||||
loading={loading}
|
||||
isLoadingAnomaliesData={isLoadingAnomaliesData}
|
||||
type={networkModel.NetworkType.details}
|
||||
flowTarget={flowTarget}
|
||||
startDate={from}
|
||||
endDate={to}
|
||||
narrowDateRange={(score, interval) => {
|
||||
const fromTo = scoreIntervalToDateTime(score, interval);
|
||||
setAbsoluteRangeDatePicker({
|
||||
id: 'global',
|
||||
from: fromTo.from,
|
||||
to: fromTo.to,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnomalyTableProvider>
|
||||
)}
|
||||
</IpOverviewQuery>
|
||||
|
||||
|
@ -211,7 +243,12 @@ const makeMapStateToProps = () => {
|
|||
});
|
||||
};
|
||||
|
||||
export const IPDetails = connect(makeMapStateToProps)(IPDetailsComponent);
|
||||
export const IPDetails = connect(
|
||||
makeMapStateToProps,
|
||||
{
|
||||
setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker,
|
||||
}
|
||||
)(IPDetailsComponent);
|
||||
|
||||
export const getBreadcrumbs = (ip: string): Breadcrumb[] => [
|
||||
{
|
||||
|
|
|
@ -11,6 +11,7 @@ import { connect } from 'react-redux';
|
|||
import { StickyContainer } from 'react-sticky';
|
||||
import { pure } from 'recompose';
|
||||
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
import { FiltersGlobal } from '../../components/filters_global';
|
||||
import { HeaderPage } from '../../components/header_page';
|
||||
import { LastEventTime } from '../../components/last_event_time';
|
||||
|
@ -29,128 +30,155 @@ import { networkModel, networkSelectors, State } from '../../store';
|
|||
import { NetworkKql } from './kql';
|
||||
import { NetworkEmptyPage } from './network_empty_page';
|
||||
import * as i18n from './translations';
|
||||
import { AnomaliesNetworkTable } from '../../components/ml/tables/anomalies_network_table';
|
||||
import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime';
|
||||
import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
|
||||
const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable);
|
||||
const NetworkDnsTableManage = manageQuery(NetworkDnsTable);
|
||||
const KpiNetworkComponentManage = manageQuery(KpiNetworkComponent);
|
||||
interface NetworkComponentReduxProps {
|
||||
filterQuery: string;
|
||||
setAbsoluteRangeDatePicker: ActionCreator<{
|
||||
id: InputsModelId;
|
||||
from: number;
|
||||
to: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
type NetworkComponentProps = NetworkComponentReduxProps;
|
||||
const NetworkComponent = pure<NetworkComponentProps>(({ filterQuery }) => (
|
||||
<WithSource sourceId="default">
|
||||
{({ indicesExist, indexPattern }) =>
|
||||
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
|
||||
<StickyContainer>
|
||||
<FiltersGlobal>
|
||||
<NetworkKql indexPattern={indexPattern} type={networkModel.NetworkType.page} />
|
||||
</FiltersGlobal>
|
||||
const NetworkComponent = pure<NetworkComponentProps>(
|
||||
({ filterQuery, setAbsoluteRangeDatePicker }) => (
|
||||
<WithSource sourceId="default">
|
||||
{({ indicesExist, indexPattern }) =>
|
||||
indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
|
||||
<StickyContainer>
|
||||
<FiltersGlobal>
|
||||
<NetworkKql indexPattern={indexPattern} type={networkModel.NetworkType.page} />
|
||||
</FiltersGlobal>
|
||||
|
||||
<HeaderPage
|
||||
subtitle={<LastEventTime indexKey={LastEventIndexKey.network} />}
|
||||
title={i18n.PAGE_TITLE}
|
||||
/>
|
||||
<HeaderPage
|
||||
subtitle={<LastEventTime indexKey={LastEventIndexKey.network} />}
|
||||
title={i18n.PAGE_TITLE}
|
||||
/>
|
||||
|
||||
<GlobalTime>
|
||||
{({ to, from, setQuery }) => (
|
||||
<UseUrlState indexPattern={indexPattern}>
|
||||
{({ isInitializing }) => (
|
||||
<>
|
||||
<KpiNetworkQuery
|
||||
endDate={to}
|
||||
filterQuery={filterQuery}
|
||||
skip={isInitializing}
|
||||
sourceId="default"
|
||||
startDate={from}
|
||||
>
|
||||
{({ kpiNetwork, loading, id, refetch }) => (
|
||||
<KpiNetworkComponentManage
|
||||
id={id}
|
||||
setQuery={setQuery}
|
||||
refetch={refetch}
|
||||
data={kpiNetwork}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</KpiNetworkQuery>
|
||||
<GlobalTime>
|
||||
{({ to, from, setQuery }) => (
|
||||
<UseUrlState indexPattern={indexPattern}>
|
||||
{({ isInitializing }) => (
|
||||
<>
|
||||
<KpiNetworkQuery
|
||||
endDate={to}
|
||||
filterQuery={filterQuery}
|
||||
skip={isInitializing}
|
||||
sourceId="default"
|
||||
startDate={from}
|
||||
>
|
||||
{({ kpiNetwork, loading, id, refetch }) => (
|
||||
<KpiNetworkComponentManage
|
||||
id={id}
|
||||
setQuery={setQuery}
|
||||
refetch={refetch}
|
||||
data={kpiNetwork}
|
||||
loading={loading}
|
||||
/>
|
||||
)}
|
||||
</KpiNetworkQuery>
|
||||
|
||||
<EuiSpacer />
|
||||
<EuiSpacer />
|
||||
|
||||
<NetworkTopNFlowQuery
|
||||
endDate={to}
|
||||
filterQuery={filterQuery}
|
||||
skip={isInitializing}
|
||||
sourceId="default"
|
||||
startDate={from}
|
||||
type={networkModel.NetworkType.page}
|
||||
>
|
||||
{({
|
||||
totalCount,
|
||||
loading,
|
||||
networkTopNFlow,
|
||||
pageInfo,
|
||||
loadMore,
|
||||
id,
|
||||
refetch,
|
||||
}) => (
|
||||
<NetworkTopNFlowTableManage
|
||||
data={networkTopNFlow}
|
||||
indexPattern={indexPattern}
|
||||
id={id}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
loading={loading}
|
||||
loadMore={loadMore}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
totalCount={totalCount}
|
||||
type={networkModel.NetworkType.page}
|
||||
/>
|
||||
)}
|
||||
</NetworkTopNFlowQuery>
|
||||
<NetworkTopNFlowQuery
|
||||
endDate={to}
|
||||
filterQuery={filterQuery}
|
||||
skip={isInitializing}
|
||||
sourceId="default"
|
||||
startDate={from}
|
||||
type={networkModel.NetworkType.page}
|
||||
>
|
||||
{({
|
||||
totalCount,
|
||||
loading,
|
||||
networkTopNFlow,
|
||||
pageInfo,
|
||||
loadMore,
|
||||
id,
|
||||
refetch,
|
||||
}) => (
|
||||
<NetworkTopNFlowTableManage
|
||||
data={networkTopNFlow}
|
||||
indexPattern={indexPattern}
|
||||
id={id}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
loading={loading}
|
||||
loadMore={loadMore}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
totalCount={totalCount}
|
||||
type={networkModel.NetworkType.page}
|
||||
/>
|
||||
)}
|
||||
</NetworkTopNFlowQuery>
|
||||
|
||||
<EuiSpacer />
|
||||
<EuiSpacer />
|
||||
|
||||
<NetworkDnsQuery
|
||||
endDate={to}
|
||||
filterQuery={filterQuery}
|
||||
skip={isInitializing}
|
||||
sourceId="default"
|
||||
startDate={from}
|
||||
type={networkModel.NetworkType.page}
|
||||
>
|
||||
{({ totalCount, loading, networkDns, pageInfo, loadMore, id, refetch }) => (
|
||||
<NetworkDnsTableManage
|
||||
data={networkDns}
|
||||
id={id}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
loading={loading}
|
||||
loadMore={loadMore}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
totalCount={totalCount}
|
||||
type={networkModel.NetworkType.page}
|
||||
/>
|
||||
)}
|
||||
</NetworkDnsQuery>
|
||||
</>
|
||||
)}
|
||||
</UseUrlState>
|
||||
)}
|
||||
</GlobalTime>
|
||||
</StickyContainer>
|
||||
) : (
|
||||
<>
|
||||
<HeaderPage title={i18n.PAGE_TITLE} />
|
||||
<NetworkDnsQuery
|
||||
endDate={to}
|
||||
filterQuery={filterQuery}
|
||||
skip={isInitializing}
|
||||
sourceId="default"
|
||||
startDate={from}
|
||||
type={networkModel.NetworkType.page}
|
||||
>
|
||||
{({ totalCount, loading, networkDns, pageInfo, loadMore, id, refetch }) => (
|
||||
<NetworkDnsTableManage
|
||||
data={networkDns}
|
||||
id={id}
|
||||
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
|
||||
loading={loading}
|
||||
loadMore={loadMore}
|
||||
nextCursor={getOr(null, 'endCursor.value', pageInfo)}
|
||||
refetch={refetch}
|
||||
setQuery={setQuery}
|
||||
totalCount={totalCount}
|
||||
type={networkModel.NetworkType.page}
|
||||
/>
|
||||
)}
|
||||
</NetworkDnsQuery>
|
||||
|
||||
<NetworkEmptyPage />
|
||||
</>
|
||||
)
|
||||
}
|
||||
</WithSource>
|
||||
));
|
||||
<EuiSpacer />
|
||||
|
||||
<AnomaliesNetworkTable
|
||||
startDate={from}
|
||||
endDate={to}
|
||||
skip={isInitializing}
|
||||
narrowDateRange={(score, interval) => {
|
||||
const fromTo = scoreIntervalToDateTime(score, interval);
|
||||
setAbsoluteRangeDatePicker({
|
||||
id: 'global',
|
||||
from: fromTo.from,
|
||||
to: fromTo.to,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</UseUrlState>
|
||||
)}
|
||||
</GlobalTime>
|
||||
</StickyContainer>
|
||||
) : (
|
||||
<>
|
||||
<HeaderPage title={i18n.PAGE_TITLE} />
|
||||
|
||||
<NetworkEmptyPage />
|
||||
</>
|
||||
)
|
||||
}
|
||||
</WithSource>
|
||||
)
|
||||
);
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getNetworkFilterQueryAsJson = networkSelectors.networkFilterQueryAsJson();
|
||||
|
@ -160,4 +188,9 @@ const makeMapStateToProps = () => {
|
|||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export const Network = connect(makeMapStateToProps)(NetworkComponent);
|
||||
export const Network = connect(
|
||||
makeMapStateToProps,
|
||||
{
|
||||
setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker,
|
||||
}
|
||||
)(NetworkComponent);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue