[SIEM] Fixes a crash when Machine Learning influencers is an undefined value (#42198) (#42464)

## Summary

Fixes a crashing bug when Machine Learning influencers is an undefined value:

https://github.com/elastic/ingest-dev/issues/611

```ts
TypeError: Cannot read property 'find' of undefined
    at getNetworkFromInfluencers (https://siem-dev-kibana.app.elstc.co/bundles/siem.bundle.js:41744:33)
    at https://siem-dev-kibana.app.elstc.co/bundles/siem.bundle.js:43012:83
    at Array.reduce (<anonymous>)
    at convertAnomaliesToNetwork (https://siem-dev-kibana.app.elstc.co/bundles/siem.bundle.js:43004:32)
    at https://siem-dev-kibana.app.elstc.co/bundles/siem.bundle.js:42830:78
    at renderWithHooks (webpack://%5Bname%5D/./node_modules/react-dom/cjs/react-dom.development.js?:12839:18)
    at updateFunctionComponent (webpack://%5Bname%5D/./node_modules/react-dom/cjs/react-dom.development.js?:14421:20)
    at beginWork (webpack://%5Bname%5D/./node_modules/react-dom/cjs/react-dom.development.js?:15431:16)
    at performUnitOfWork (webpack://%5Bname%5D/./node_modules/react-dom/cjs/react-dom.development.js?:19106:12)
    at workLoop (webpack://%5Bname%5D/./node_modules/react-dom/cjs/react-dom.development.js?:19146:24)
```

### Checklist

Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR.

~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~

~~- [ ] 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

~~- [ ] 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:
Garrett Spong 2019-08-02 07:34:33 -06:00 committed by GitHub
parent e555582a4c
commit 7dcf815049
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 249 additions and 90 deletions

View file

@ -19,10 +19,15 @@ describe('create_influencers', () => {
});
test('renders correctly against snapshot', () => {
const wrapper = shallow(<span>{createInfluencers(anomalies.anomalies[0])}</span>);
const wrapper = shallow(<span>{createInfluencers(anomalies.anomalies[0].influencers)}</span>);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it returns an empty string when influencers is undefined', () => {
const wrapper = mount(<span>{createInfluencers()}</span>);
expect(wrapper.text()).toEqual('');
});
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"');
@ -34,13 +39,13 @@ 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>);
const wrapper = mount(<span>{createInfluencers(anomalies.anomalies[0].influencers)}</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>);
const wrapper = mount(<span>{createInfluencers(anomalies.anomalies[0].influencers)}</span>);
expect(wrapper.text()).toEqual('');
});
@ -50,7 +55,7 @@ describe('create_influencers', () => {
{},
{ 'influencer-name-two': 'influencer-value-two' },
];
const wrapper = mount(<span>{createInfluencers(anomalies.anomalies[0])}</span>);
const wrapper = mount(<span>{createInfluencers(anomalies.anomalies[0].influencers)}</span>);
expect(wrapper.text()).toEqual(
'influencer-name-one: "influencer-value-one"influencer-name-two: "influencer-value-two"'
);

View file

@ -7,7 +7,6 @@
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 => {
@ -19,19 +18,14 @@ export const createKeyAndValue = (influencer: Record<string, string>): string =>
}
};
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>
);
})}
</>
);
};
export const createInfluencers = (influencers: Array<Record<string, string>> = []): JSX.Element[] =>
influencers
.filter(influencer => !isEmpty(influencer))
.map(influencer => {
const keyAndValue = createKeyAndValue(influencer);
return (
<EuiFlexItem key={keyAndValue} grow={false}>
{keyAndValue}
</EuiFlexItem>
);
});

View file

@ -26,6 +26,11 @@ describe('get_host_name_from_influencers', () => {
expect(hostName).toEqual(null);
});
test('returns null if it is given undefined influencers', () => {
const hostName = getHostNameFromInfluencers();
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);

View file

@ -7,7 +7,7 @@
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 => {

View file

@ -28,6 +28,11 @@ describe('get_network_from_influencers', () => {
expect(network).toEqual(null);
});
test('returns null if the influencers are undefined', () => {
const network = getNetworkFromInfluencers();
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);

View file

@ -8,7 +8,7 @@ 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 => {

View file

@ -190,23 +190,21 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = `
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>
<EuiFlexItem
grow={false}
>
host.name: "zeek-iowa"
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
process.name: "du"
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
user.name: "root"
</EuiFlexItem>
</EuiFlexGroup>,
"title": "Influenced By",
},

View file

@ -88,7 +88,7 @@ export const createDescriptionList = (
title: i18n.INFLUENCED_BY,
description: (
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
{createInfluencers(score)}
{createInfluencers(score.influencers)}
</EuiFlexGroup>
),
},

View file

@ -9,6 +9,7 @@ import {
createEntitiesFromScore,
createEntity,
createEntityFromRecord,
createInfluencersFromScore,
} from './create_entities_from_score';
import { cloneDeep } from 'lodash/fp';
@ -68,4 +69,41 @@ describe('create_entities_from_score', () => {
const entity = createEntityFromRecord({ 'name-1': 'value-1' });
expect(entity).toEqual("name-1:'value-1'");
});
test('it returns expected entities from a typical score for influencers', () => {
const influencers = createInfluencersFromScore(anomalies.anomalies[0].influencers);
expect(influencers).toEqual("host.name:'zeek-iowa',process.name:'du',user.name:'root'");
});
test('it returns empty string for empty influencers', () => {
const influencers = createInfluencersFromScore([]);
expect(influencers).toEqual('');
});
test('it returns empty string for undefined influencers', () => {
const influencers = createInfluencersFromScore();
expect(influencers).toEqual('');
});
test('it returns single influencer', () => {
const influencers = createInfluencersFromScore([{ 'influencer-1': 'value-1' }]);
expect(influencers).toEqual("influencer-1:'value-1'");
});
test('it returns two influencers', () => {
const influencers = createInfluencersFromScore([
{ 'influencer-1': 'value-1' },
{ 'influencer-2': 'value-2' },
]);
expect(influencers).toEqual("influencer-1:'value-1',influencer-2:'value-2'");
});
test('it creates a simple string entity with undefined influencers', () => {
const anomaly = anomalies.anomalies[0];
anomaly.entityName = 'name-1';
anomaly.entityValue = 'value-1';
delete anomaly.influencers;
const entities = createEntitiesFromScore(anomaly);
expect(entities).toEqual("name-1:'value-1'");
});
});

View file

@ -12,8 +12,10 @@ export const createEntityFromRecord = (entity: Record<string, string>): string =
export const createEntity = (entityName: string, entityValue: string): string =>
`${entityName}:'${entityValue}'`;
export const createEntitiesFromScore = (score: Anomaly): string => {
const influencers = score.influencers.reduce((accum, item, index) => {
export const createInfluencersFromScore = (
influencers: Array<Record<string, string>> = []
): string =>
influencers.reduce((accum, item, index) => {
if (index === 0) {
return createEntityFromRecord(item);
} else {
@ -21,6 +23,9 @@ export const createEntitiesFromScore = (score: Anomaly): string => {
}
}, '');
export const createEntitiesFromScore = (score: Anomaly): string => {
const influencers = createInfluencersFromScore(score.influencers);
if (influencers.length === 0) {
return createEntity(score.entityName, score.entityValue);
} else if (!influencers.includes(score.entityName)) {

View file

@ -7,7 +7,7 @@
import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns';
import { HostsType } from '../../../store/hosts/model';
import * as i18n from './translations';
import { AnomaliesByHost } from '../types';
import { AnomaliesByHost, Anomaly } from '../types';
import { Columns } from '../../load_more_table';
import { TestProviders } from '../../../mock';
import { mount } from 'enzyme';
@ -96,7 +96,7 @@ describe('get_anomalies_host_table_columns', () => {
is_interim: true,
timestamp: new Date('01/01/2000').valueOf(),
by_field_name: 'some field name',
by_field_value: 'some field valuke',
by_field_value: 'some field value',
partition_field_name: 'partition field name',
partition_field_value: 'partition field value',
function: 'function-1',
@ -121,4 +121,57 @@ describe('get_anomalies_host_table_columns', () => {
expect(column).not.toBe(null);
}
});
test('on host page, undefined influencers should turn into an empty column string', () => {
const columns = getAnomaliesHostTableColumnsCurated(
HostsType.page,
startDate,
endDate,
interval,
narrowDateRange
);
const column = columns.find(col => col.name === i18n.INFLUENCED_BY) as Columns<
Anomaly['influencers'],
AnomaliesByHost
>;
const anomaly: AnomaliesByHost = {
hostName: 'host.name',
anomaly: {
detectorIndex: 0,
entityName: 'entity-name-1',
entityValue: 'entity-value-1',
jobId: 'job-1',
rowId: 'row-1',
severity: 100,
time: new Date('01/01/2000').valueOf(),
source: {
job_id: 'job-1',
result_type: 'result-1',
probability: 50,
multi_bucket_impact: 0,
record_score: 0,
initial_record_score: 0,
bucket_span: 0,
detector_index: 0,
is_interim: true,
timestamp: new Date('01/01/2000').valueOf(),
by_field_name: 'some field name',
by_field_value: 'some field value',
partition_field_name: 'partition field name',
partition_field_value: 'partition field value',
function: 'function-1',
function_description: 'description-1',
typical: [5, 3],
actual: [7, 4],
influencers: [],
},
},
};
if (column != null && column.render != null) {
const wrapper = mount(<TestProviders>{column.render(undefined, anomaly)}</TestProviders>);
expect(wrapper.text()).toEqual('');
} else {
expect(column).not.toBe(null);
}
});
});

View file

@ -95,29 +95,30 @@ export const getAnomaliesHostTableColumns = (
field: 'anomaly.influencers',
render: (influencers, anomaliesByHost) => (
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
{influencers.map(influencer => {
const [key, value] = getEntries(influencer);
const entityName = key != null ? key : '';
const entityValue = value != null ? value : '';
return (
<EuiFlexItem
key={`${entityName}-${entityValue}-${createCompoundHostKey(anomaliesByHost)}`}
grow={false}
>
<EuiFlexGroup gutterSize="none" responsive={false}>
<EuiFlexItem grow={false}>
<EntityDraggable
idPrefix={`anomalies-host-table-influencers-${entityName}-${entityValue}-${createCompoundHostKey(
anomaliesByHost
)}`}
entityName={entityName}
entityValue={entityValue}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
})}
{influencers &&
influencers.map(influencer => {
const [key, value] = getEntries(influencer);
const entityName = key != null ? key : '';
const entityValue = value != null ? value : '';
return (
<EuiFlexItem
key={`${entityName}-${entityValue}-${createCompoundHostKey(anomaliesByHost)}`}
grow={false}
>
<EuiFlexGroup gutterSize="none" responsive={false}>
<EuiFlexItem grow={false}>
<EntityDraggable
idPrefix={`anomalies-host-table-influencers-${entityName}-${entityValue}-${createCompoundHostKey(
anomaliesByHost
)}`}
entityName={entityName}
entityValue={entityValue}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
),
},

View file

@ -7,7 +7,7 @@
import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns';
import { NetworkType } from '../../../store/network/model';
import * as i18n from './translations';
import { AnomaliesByNetwork } from '../types';
import { AnomaliesByNetwork, Anomaly } from '../types';
import { Columns } from '../../load_more_table';
import { mount } from 'enzyme';
import React from 'react';
@ -100,7 +100,7 @@ describe('get_anomalies_network_table_columns', () => {
is_interim: true,
timestamp: new Date('01/01/2000').valueOf(),
by_field_name: 'some field name',
by_field_value: 'some field valuke',
by_field_value: 'some field value',
partition_field_name: 'partition field name',
partition_field_value: 'partition field value',
function: 'function-1',
@ -125,4 +125,58 @@ describe('get_anomalies_network_table_columns', () => {
expect(column).not.toBe(null);
}
});
test('on network page, undefined influencers should turn into an empty column string', () => {
const columns = getAnomaliesNetworkTableColumnsCurated(
NetworkType.page,
startDate,
endDate,
interval,
narrowDateRange
);
const column = columns.find(col => col.name === i18n.INFLUENCED_BY) as Columns<
Anomaly['influencers'],
AnomaliesByNetwork
>;
const anomaly: AnomaliesByNetwork = {
type: 'source.ip',
ip: '127.0.0.1',
anomaly: {
detectorIndex: 0,
entityName: 'entity-name-1',
entityValue: 'entity-value-1',
jobId: 'job-1',
rowId: 'row-1',
severity: 100,
time: new Date('01/01/2000').valueOf(),
source: {
job_id: 'job-1',
result_type: 'result-1',
probability: 50,
multi_bucket_impact: 0,
record_score: 0,
initial_record_score: 0,
bucket_span: 0,
detector_index: 0,
is_interim: true,
timestamp: new Date('01/01/2000').valueOf(),
by_field_name: 'some field name',
by_field_value: 'some field value',
partition_field_name: 'partition field name',
partition_field_value: 'partition field value',
function: 'function-1',
function_description: 'description-1',
typical: [5, 3],
actual: [7, 4],
influencers: [],
},
},
};
if (column != null && column.render != null) {
const wrapper = mount(<TestProviders>{column.render(undefined, anomaly)}</TestProviders>);
expect(wrapper.text()).toEqual('');
} else {
expect(column).not.toBe(null);
}
});
});

View file

@ -93,25 +93,26 @@ export const getAnomaliesNetworkTableColumns = (
field: 'anomaly.influencers',
render: (influencers, anomaliesByNetwork) => (
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
{influencers.map(influencer => {
const [key, value] = getEntries(influencer);
const entityName = key != null ? key : '';
const entityValue = value != null ? value : '';
return (
<EuiFlexItem
key={`${entityName}-${entityValue}-${createCompoundNetworkKey(anomaliesByNetwork)}`}
grow={false}
>
<EntityDraggable
idPrefix={`anomalies-network-table-influencers-${entityName}-${entityValue}-${createCompoundNetworkKey(
anomaliesByNetwork
)}`}
entityName={entityName}
entityValue={entityValue}
/>
</EuiFlexItem>
);
})}
{influencers &&
influencers.map(influencer => {
const [key, value] = getEntries(influencer);
const entityName = key != null ? key : '';
const entityValue = value != null ? value : '';
return (
<EuiFlexItem
key={`${entityName}-${entityValue}-${createCompoundNetworkKey(anomaliesByNetwork)}`}
grow={false}
>
<EntityDraggable
idPrefix={`anomalies-network-table-influencers-${entityName}-${entityValue}-${createCompoundNetworkKey(
anomaliesByNetwork
)}`}
entityName={entityName}
entityValue={entityValue}
/>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
),
},

View file

@ -54,7 +54,7 @@ export interface Anomaly {
detectorIndex: number;
entityName: string;
entityValue: string;
influencers: Array<Record<string, string>>;
influencers?: Array<Record<string, string>>;
jobId: string;
rowId: string;
severity: number;