[SIEM] Adds Machine Learning Anomaly Table to Host Details and Network Details (#40223) (#40328)

## Summary

Adds Machine Learning Anomaly Table to Host Details and Network Details. Filters them down to the specific host name and specific network ip of the details

<img width="1677" alt="Screen Shot 2019-07-02 at 6 40 42 PM" src="https://user-images.githubusercontent.com/1151048/60555277-3d417300-9cf9-11e9-83b7-7c78ed65cd27.png">

<img width="1024" alt="Screen Shot 2019-07-02 at 6 40 27 PM" src="https://user-images.githubusercontent.com/1151048/60555308-5d713200-9cf9-11e9-88b5-f629169e47ce.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:
Frank Hassanabad 2019-07-04 06:56:38 -06:00 committed by GitHub
parent 7eabd2a069
commit d5246141c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 644 additions and 59 deletions

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`create_influencers renders correctly against snapshot 1`] = `
exports[`entity_draggable renders correctly against snapshot 1`] = `
<Connect(DraggableWrapperComponent)
dataProvider={
Object {

View file

@ -10,7 +10,7 @@ import { shallow, mount } from 'enzyme';
import { EntityDraggable } from './entity_draggable';
import { TestProviders } from '../../mock/test_providers';
describe('create_influencers', () => {
describe('entity_draggable', () => {
test('renders correctly against snapshot', () => {
const wrapper = shallow(
<EntityDraggable idPrefix="id-prefix" entityName="entity-name" entityValue="entity-value" />
@ -24,6 +24,6 @@ describe('create_influencers', () => {
<EntityDraggable idPrefix="id-prefix" entityName="entity-name" entityValue="entity-value" />
</TestProviders>
);
expect(wrapper.text()).toEqual('entity-name: “entity-value”');
expect(wrapper.text()).toEqual('entity-name: "entity-value"');
});
});

View file

@ -41,7 +41,7 @@ export const EntityDraggable = React.memo<Props>(
<Provider dataProvider={dataProvider} />
</DragEffects>
) : (
<>{`${entityName}: ${entityValue}`}</>
<>{`${entityName}: "${entityValue}"`}</>
)
}
/>

View file

@ -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 { getEntries } from './get_entries';
describe('get_entries', () => {
test('returns null if the entries is an empty object', () => {
const [key, value] = getEntries({});
expect(key).toEqual(null);
expect(value).toEqual(null);
});
test('returns key value if the entries as a key value', () => {
const [key, value] = getEntries({ 'host.name': 'some-host-value' });
expect(key).toEqual('host.name');
expect(value).toEqual('some-host-value');
});
});

View file

@ -0,0 +1,17 @@
/*
* 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 getEntries = (
entityOrInfluencer: Record<string, string>
): [string, string] | [null, null] => {
const entries = Object.entries(entityOrInfluencer);
if (Array.isArray(entries[0])) {
const [[key, value]] = entries;
return [key, value];
} else {
return [null, null];
}
};

View file

@ -4,21 +4,21 @@ exports[`create_influencers renders correctly against snapshot 1`] = `
<span>
<EuiFlexItem
grow={false}
key="host.name: “zeek-iowa”"
key="host.name: \\"zeek-iowa\\""
>
host.name: “zeek-iowa”
host.name: "zeek-iowa"
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="process.name: “du”"
key="process.name: \\"du\\""
>
process.name: “du”
process.name: "du"
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="user.name: “root”"
key="user.name: \\"root\\""
>
user.name: “root”
user.name: "root"
</EuiFlexItem>
</span>
`;

View file

@ -25,7 +25,7 @@ describe('create_influencers', () => {
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”');
expect(entities).toEqual('name-1: "value-1"');
});
test('it returns expected createKeyAndValue record when empty object is passed', () => {
@ -35,7 +35,7 @@ describe('create_influencers', () => {
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”');
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', () => {
@ -52,7 +52,7 @@ describe('create_influencers', () => {
];
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”'
'influencer-name-one: "influencer-value-one"influencer-name-two: "influencer-value-two"'
);
});
});

View file

@ -8,10 +8,12 @@ import { EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { isEmpty } from 'lodash/fp';
import { Anomaly } from '../types';
import { getEntries } from '../get_entries';
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]}`;
const [key, value] = getEntries(influencer);
if (key != null && value != null) {
return `${key}: "${value}"`;
} else {
return '';
}

View file

@ -4,13 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getEntries } from '../get_entries';
export const getHostNameFromInfluencers = (
influencers: Array<Record<string, string>>
influencers: Array<Record<string, string>>,
hostName?: string
): string | null => {
const recordFound = influencers.find(influencer => {
const influencerName = Object.keys(influencer)[0];
const [influencerName, influencerValue] = getEntries(influencer);
if (influencerName === 'host.name') {
return true;
if (hostName == null) {
return true;
} else {
return influencerValue === hostName;
}
} else {
return false;
}

View file

@ -4,22 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { DestinationOrSource } from '../types';
import { DestinationOrSource, isDestinationOrSource } from '../types';
import { getEntries } from '../get_entries';
export const getNetworkFromInfluencers = (
influencers: Array<Record<string, string>>
influencers: Array<Record<string, string>>,
ip?: 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;
const [influencerName, influencerValue] = getEntries(influencer);
if (isDestinationOrSource(influencerName)) {
if (ip == null) {
return true;
} else {
return influencerValue === ip;
}
} else {
return false;
}
});
if (recordFound != null) {
const influencerName = Object.keys(recordFound)[0];
if (influencerName === 'destination.ip' || influencerName === 'source.ip') {
const [influencerName] = getEntries(recordFound);
if (isDestinationOrSource(influencerName)) {
return { ip: Object.values(recordFound)[0], type: influencerName };
} else {
// default to destination.ip

View file

@ -179,7 +179,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = `
<EuiFlexItem
grow={false}
>
process.name: “du”
process.name: "du"
</EuiFlexItem>
</EuiFlexGroup>,
"title": "Top Anomaly Suspect",
@ -194,17 +194,17 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = `
<EuiFlexItem
grow={false}
>
host.name: “zeek-iowa”
host.name: "zeek-iowa"
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
process.name: “du”
process.name: "du"
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
user.name: “root”
user.name: "root"
</EuiFlexItem>
</React.Fragment>
</EuiFlexGroup>,

View file

@ -105,7 +105,7 @@ exports[`create_description_list renders correctly against snapshot 1`] = `
<EuiFlexItem
grow={false}
>
process.name: “du”
process.name: "du"
</EuiFlexItem>
</EuiFlexGroup>
</EuiDescriptionListDescription>
@ -124,21 +124,21 @@ exports[`create_description_list renders correctly against snapshot 1`] = `
>
<EuiFlexItem
grow={false}
key="host.name: “zeek-iowa”"
key="host.name: \\"zeek-iowa\\""
>
host.name: “zeek-iowa”
host.name: "zeek-iowa"
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="process.name: “du”"
key="process.name: \\"du\\""
>
process.name: “du”
process.name: "du"
</EuiFlexItem>
<EuiFlexItem
grow={false}
key="user.name: “root”"
key="user.name: \\"root\\""
>
user.name: “root”
user.name: "root"
</EuiFlexItem>
</EuiFlexGroup>
</EuiDescriptionListDescription>

View file

@ -80,7 +80,7 @@ export const createDescriptionList = (
title: i18n.TOP_ANOMALY_SUSPECT,
description: (
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false}>{`${score.entityName}: ${score.entityValue}`}</EuiFlexItem>
<EuiFlexItem grow={false}>{`${score.entityName}: "${score.entityValue}"`}</EuiFlexItem>
</EuiFlexGroup>
),
},

View file

@ -34,7 +34,7 @@ const sorting = {
};
export const AnomaliesHostTable = React.memo<AnomaliesTableProps>(
({ startDate, endDate, narrowDateRange, skip }): JSX.Element | null => {
({ startDate, endDate, narrowDateRange, hostName, skip }): JSX.Element | null => {
const capabilities = useContext(MlCapabilitiesContext);
const [loading, tableData] = useAnomaliesTableData({
influencers: [],
@ -44,7 +44,7 @@ export const AnomaliesHostTable = React.memo<AnomaliesTableProps>(
skip,
});
const hosts = convertAnomaliesToHosts(tableData);
const hosts = convertAnomaliesToHosts(tableData, hostName);
const interval = getIntervalFromAnomalies(tableData);
const columns = getAnomaliesHostTableColumns(startDate, endDate, interval, narrowDateRange);
const pagination = {

View file

@ -34,7 +34,7 @@ const sorting = {
};
export const AnomaliesNetworkTable = React.memo<AnomaliesTableProps>(
({ startDate, endDate, narrowDateRange, skip }): JSX.Element | null => {
({ startDate, endDate, narrowDateRange, skip, ip }): JSX.Element | null => {
const capabilities = useContext(MlCapabilitiesContext);
const [loading, tableData] = useAnomaliesTableData({
influencers: [],
@ -44,7 +44,7 @@ export const AnomaliesNetworkTable = React.memo<AnomaliesTableProps>(
skip,
});
const networks = convertAnomaliesToNetwork(tableData);
const networks = convertAnomaliesToNetwork(tableData, ip);
const interval = getIntervalFromAnomalies(tableData);
const columns = getAnomaliesNetworkTableColumns(startDate, endDate, interval, narrowDateRange);
const pagination = {

View file

@ -11,7 +11,7 @@
import { mockAnomalies } from '../mock';
import { cloneDeep } from 'lodash/fp';
import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts';
import { convertAnomaliesToHosts, getHostNameFromEntity } from './convert_anomalies_to_hosts';
import { AnomaliesByHost } from '../types';
describe('convert_anomalies_to_hosts', () => {
@ -117,4 +117,153 @@ describe('convert_anomalies_to_hosts', () => {
const expected: AnomaliesByHost[] = [];
expect(entities).toEqual(expected);
});
test('it returns a specific anomaly if sent in the name of an anomaly', () => {
anomalies.anomalies[0].entityName = 'something-else';
anomalies.anomalies[0].entityValue = 'something-else';
anomalies.anomalies[0].influencers = [
{ 'host.name': 'something-else' },
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToHosts(anomalies, 'zeek-iowa');
const expected: AnomaliesByHost[] = [
{
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 a specific anomaly if an influencer has the name', () => {
anomalies.anomalies[0].entityName = 'something-else';
anomalies.anomalies[0].entityValue = 'something-else';
anomalies.anomalies[0].influencers = [
{ 'host.name': 'something-else' },
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
anomalies.anomalies[1].entityName = 'something-else';
anomalies.anomalies[1].entityValue = 'something-else';
const entities = convertAnomaliesToHosts(anomalies, 'zeek-iowa');
const expected: AnomaliesByHost[] = [
{
anomaly: {
detectorIndex: 0,
entityName: 'something-else',
entityValue: 'something-else',
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 the name of one that does not exist', () => {
const entities = convertAnomaliesToHosts(anomalies, 'some-made-up-name-here-for-you');
const expected: AnomaliesByHost[] = [];
expect(entities).toEqual(expected);
});
test('it returns true for a found entity name passed in', () => {
anomalies.anomalies[0].entityName = 'host.name';
anomalies.anomalies[0].entityValue = 'zeek-iowa';
const found = getHostNameFromEntity(anomalies.anomalies[0], 'zeek-iowa');
expect(found).toEqual(true);
});
test('it returns false for an entity name that does not exist', () => {
anomalies.anomalies[0].entityName = 'host.name';
anomalies.anomalies[0].entityValue = 'zeek-iowa';
const found = getHostNameFromEntity(anomalies.anomalies[0], 'some-made-up-entity-name');
expect(found).toEqual(false);
});
test('it returns true for an entity that has host.name within it if no name is passed in', () => {
anomalies.anomalies[0].entityName = 'host.name';
anomalies.anomalies[0].entityValue = 'something-made-up';
const found = getHostNameFromEntity(anomalies.anomalies[0]);
expect(found).toEqual(true);
});
test('it returns false for an entity that is not host.name and no name is passed in', () => {
anomalies.anomalies[0].entityName = 'made-up';
const found = getHostNameFromEntity(anomalies.anomalies[0]);
expect(found).toEqual(false);
});
});

View file

@ -4,20 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Anomalies, AnomaliesByHost } from '../types';
import { Anomalies, AnomaliesByHost, Anomaly } from '../types';
import { getHostNameFromInfluencers } from '../influencers/get_host_name_from_influencers';
export const convertAnomaliesToHosts = (anomalies: Anomalies | null): AnomaliesByHost[] => {
export const convertAnomaliesToHosts = (
anomalies: Anomalies | null,
hostName?: string
): AnomaliesByHost[] => {
if (anomalies == null) {
return [];
} else {
return anomalies.anomalies.reduce<AnomaliesByHost[]>((accum, item) => {
if (item.entityName === 'host.name') {
if (getHostNameFromEntity(item, hostName)) {
return [...accum, { hostName: item.entityValue, anomaly: item }];
} else {
const hostName = getHostNameFromInfluencers(item.influencers);
if (hostName != null) {
return [...accum, { hostName, anomaly: item }];
const hostNameFromInfluencers = getHostNameFromInfluencers(item.influencers, hostName);
if (hostNameFromInfluencers != null) {
return [...accum, { hostName: hostNameFromInfluencers, anomaly: item }];
} else {
return accum;
}
@ -25,3 +28,13 @@ export const convertAnomaliesToHosts = (anomalies: Anomalies | null): AnomaliesB
}, []);
}
};
export const getHostNameFromEntity = (anomaly: Anomaly, hostName?: string): boolean => {
if (anomaly.entityName !== 'host.name') {
return false;
} else if (hostName == null) {
return true;
} else {
return anomaly.entityValue === hostName;
}
};

View file

@ -11,8 +11,8 @@
import { mockAnomalies } from '../mock';
import { cloneDeep } from 'lodash/fp';
import { convertAnomaliesToNetwork } from './convert_anomalies_to_network';
import { AnomaliesByHost, AnomaliesByNetwork } from '../types';
import { convertAnomaliesToNetwork, getNetworkFromEntity } from './convert_anomalies_to_network';
import { AnomaliesByNetwork } from '../types';
describe('convert_anomalies_to_hosts', () => {
let anomalies = cloneDeep(mockAnomalies);
@ -225,7 +225,289 @@ describe('convert_anomalies_to_hosts', () => {
test('it returns empty anomalies if sent in a null', () => {
const entities = convertAnomaliesToNetwork(null);
const expected: AnomaliesByHost[] = [];
const expected: AnomaliesByNetwork[] = [];
expect(entities).toEqual(expected);
});
test('it returns a specific anomaly if sent in the name of an anomaly by source.ip', () => {
anomalies.anomalies[0].entityName = 'source.ip';
anomalies.anomalies[0].entityValue = '127.0.0.1';
anomalies.anomalies[0].influencers = [
{ 'host.name': 'something-else' },
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToNetwork(anomalies, '127.0.0.1');
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
detectorIndex: 0,
entityName: 'source.ip',
entityValue: '127.0.0.1',
influencers: [
{ 'host.name': 'something-else' },
{ '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 a specific anomaly if sent in the name of an anomaly by destination.ip', () => {
anomalies.anomalies[0].entityName = 'destination.ip';
anomalies.anomalies[0].entityValue = '127.0.0.1';
anomalies.anomalies[0].influencers = [
{ 'host.name': 'something-else' },
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToNetwork(anomalies, '127.0.0.1');
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
detectorIndex: 0,
entityName: 'destination.ip',
entityValue: '127.0.0.1',
influencers: [
{ 'host.name': 'something-else' },
{ '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 a specific anomaly if an influencer has the name by source.ip', () => {
anomalies.anomalies[0].entityName = 'something-else';
anomalies.anomalies[0].entityValue = 'something-else';
anomalies.anomalies[0].influencers = [
{ 'source.ip': '127.0.0.1' },
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToNetwork(anomalies, '127.0.0.1');
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
detectorIndex: 0,
entityName: 'something-else',
entityValue: 'something-else',
influencers: [
{ 'source.ip': '127.0.0.1' },
{ '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 a specific anomaly if an influencer has the name by destination.ip', () => {
anomalies.anomalies[0].entityName = 'something-else';
anomalies.anomalies[0].entityValue = 'something-else';
anomalies.anomalies[0].influencers = [
{ 'destination.ip': '127.0.0.1' },
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToNetwork(anomalies, '127.0.0.1');
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
detectorIndex: 0,
entityName: 'something-else',
entityValue: 'something-else',
influencers: [
{ 'destination.ip': '127.0.0.1' },
{ '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 empty anomalies if sent in the name of one that does not exist', () => {
const entities = convertAnomaliesToNetwork(anomalies, 'some-made-up-name-here-for-you');
const expected: AnomaliesByNetwork[] = [];
expect(entities).toEqual(expected);
});
test('it returns true for a found entity name passed in by source.ip', () => {
anomalies.anomalies[0].entityName = 'source.ip';
anomalies.anomalies[0].entityValue = '255.255.255.255';
const found = getNetworkFromEntity(anomalies.anomalies[0], '255.255.255.255');
expect(found).toEqual(true);
});
test('it returns true for a found entity name passed in by destination.ip', () => {
anomalies.anomalies[0].entityName = 'destination.ip';
anomalies.anomalies[0].entityValue = '255.255.255.255';
const found = getNetworkFromEntity(anomalies.anomalies[0], '255.255.255.255');
expect(found).toEqual(true);
});
test('it returns false for an entity name that does not exist by source.ip', () => {
anomalies.anomalies[0].entityName = 'source.ip';
anomalies.anomalies[0].entityValue = '255.255.255.255';
const found = getNetworkFromEntity(anomalies.anomalies[0], 'some-made-up-entity-name');
expect(found).toEqual(false);
});
test('it returns false for an entity name that does not exist by destination.ip', () => {
anomalies.anomalies[0].entityName = 'destination.ip';
anomalies.anomalies[0].entityValue = '255.255.255.255';
const found = getNetworkFromEntity(anomalies.anomalies[0], 'some-made-up-entity-name');
expect(found).toEqual(false);
});
test('it returns true for an entity that has source.ip within it if no name is passed in', () => {
anomalies.anomalies[0].entityName = 'source.ip';
anomalies.anomalies[0].entityValue = 'something-made-up';
const found = getNetworkFromEntity(anomalies.anomalies[0]);
expect(found).toEqual(true);
});
test('it returns true for an entity that has destination.ip within it if no name is passed in', () => {
anomalies.anomalies[0].entityName = 'destination.ip';
anomalies.anomalies[0].entityValue = 'something-made-up';
const found = getNetworkFromEntity(anomalies.anomalies[0]);
expect(found).toEqual(true);
});
test('it returns false for an entity that not source.ip or destination.ip and no name is passed in', () => {
anomalies.anomalies[0].entityName = 'made-up';
const found = getNetworkFromEntity(anomalies.anomalies[0]);
expect(found).toEqual(false);
});
});

View file

@ -4,18 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Anomalies, AnomaliesByNetwork } from '../types';
import { Anomalies, AnomaliesByNetwork, Anomaly, isDestinationOrSource } from '../types';
import { getNetworkFromInfluencers } from '../influencers/get_network_from_influencers';
export const convertAnomaliesToNetwork = (anomalies: Anomalies | null): AnomaliesByNetwork[] => {
export const convertAnomaliesToNetwork = (
anomalies: Anomalies | null,
ip?: string
): AnomaliesByNetwork[] => {
if (anomalies == null) {
return [];
} else {
return anomalies.anomalies.reduce<AnomaliesByNetwork[]>((accum, item) => {
if (item.entityName === 'source.ip' || item.entityName === 'destination.ip') {
if (isDestinationOrSource(item.entityName) && getNetworkFromEntity(item, ip)) {
return [...accum, { ip: item.entityValue, type: item.entityName, anomaly: item }];
} else {
const network = getNetworkFromInfluencers(item.influencers);
const network = getNetworkFromInfluencers(item.influencers, ip);
if (network != null) {
return [...accum, { ip: network.ip, type: network.type, anomaly: item }];
} else {
@ -25,3 +28,15 @@ export const convertAnomaliesToNetwork = (anomalies: Anomalies | null): Anomalie
}, []);
}
};
export const getNetworkFromEntity = (anomaly: Anomaly, ip?: string): boolean => {
if (isDestinationOrSource(anomaly.entityName)) {
if (ip == null) {
return true;
} else {
return anomaly.entityValue === ip;
}
} else {
return false;
}
};

View file

@ -15,6 +15,7 @@ import { HostDetailsLink } from '../../links';
import * as i18n from './translations';
import { AnomalyScore } from '../score/anomaly_score';
import { getEntries } from '../get_entries';
export const getAnomaliesHostTableColumns = (
startDate: number,
@ -77,8 +78,9 @@ export const getAnomaliesHostTableColumns = (
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];
const [key, value] = getEntries(influencer);
const entityName = key != null ? key : '';
const entityValue = value != null ? value : '';
return (
<EuiFlexItem
key={`${entityName}-${entityValue}-${createCompoundHostKey(anomaliesByHost)}`}

View file

@ -15,6 +15,7 @@ import { IPDetailsLink } from '../../links';
import * as i18n from './translations';
import { AnomalyScore } from '../score/anomaly_score';
import { getEntries } from '../get_entries';
export const getAnomaliesNetworkTableColumns = (
startDate: number,
@ -75,8 +76,9 @@ export const getAnomaliesNetworkTableColumns = (
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];
const [key, value] = getEntries(influencer);
const entityName = key != null ? key : '';
const entityValue = value != null ? value : '';
return (
<EuiFlexItem
key={`${entityName}-${entityValue}-${createCompoundNetworkKey(anomaliesByNetwork)}`}

View file

@ -0,0 +1,25 @@
/*
* 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 { isDestinationOrSource } from './types';
describe('types', () => {
test('it returns that something is a source.ip type and value', () => {
expect(isDestinationOrSource('source.ip')).toEqual(true);
});
test('it returns that something is a destination.ip type and value', () => {
expect(isDestinationOrSource('destination.ip')).toEqual(true);
});
test('it returns that something else is not is a destination.ip type and value', () => {
expect(isDestinationOrSource('something-else.ip')).toEqual(false);
});
test('it returns that null is not is a destination.ip type and value', () => {
expect(isDestinationOrSource(null)).toEqual(false);
});
});

View file

@ -78,6 +78,8 @@ export interface AnomaliesTableProps {
endDate: number;
narrowDateRange: NarrowDateRange;
skip: boolean;
hostName?: string;
ip?: string;
}
export interface MlCapabilities {
@ -110,3 +112,8 @@ export interface MlCapabilities {
mlFeatureEnabledInSpace: boolean;
upgradeInProgress: boolean;
}
const sourceOrDestination = ['source.ip', 'destination.ip'];
export const isDestinationOrSource = (value: string | null): value is DestinationOrSource =>
value != null && sourceOrDestination.includes(value);

View file

@ -43,6 +43,7 @@ import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '.
import { InputsModelId } from '../../store/inputs/constants';
import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime';
import { KpiHostDetailsQuery } from '../../containers/kpi_host_details';
import { AnomaliesHostTable } from '../../components/ml/tables/anomalies_host_table';
const type = hostsModel.HostsType.details;
@ -227,6 +228,23 @@ const HostDetailsComponent = pure<HostDetailsComponentProps>(
<EuiSpacer />
<AnomaliesHostTable
startDate={from}
endDate={to}
skip={isInitializing}
hostName={hostName}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({
id: 'global',
from: fromTo.from,
to: fromTo.to,
});
}}
/>
<EuiSpacer />
<EventsQuery
endDate={to}
filterQuery={getFilterQuery(hostName, filterQueryExpression, indexPattern)}

View file

@ -42,6 +42,7 @@ import { AnomalyTableProvider } from '../../components/ml/anomaly/anomaly_table_
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';
import { AnomaliesNetworkTable } from '../../components/ml/tables/anomalies_network_table';
const DomainsTableManage = manageQuery(DomainsTable);
const TlsTableManage = manageQuery(TlsTable);
@ -251,6 +252,23 @@ export const IPDetailsComponent = pure<IPDetailsComponentProps>(
/>
)}
</TlsQuery>
<EuiSpacer />
<AnomaliesNetworkTable
startDate={from}
endDate={to}
skip={isInitializing}
ip={ip}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({
id: 'global',
from: fromTo.from,
to: fromTo.to,
});
}}
/>
</>
)}
</UseUrlState>