diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx index a79ebdbc5ed60..615e83d208dd6 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx @@ -19,10 +19,15 @@ describe('create_influencers', () => { }); test('renders correctly against snapshot', () => { - const wrapper = shallow({createInfluencers(anomalies.anomalies[0])}); + const wrapper = shallow({createInfluencers(anomalies.anomalies[0].influencers)}); expect(toJson(wrapper)).toMatchSnapshot(); }); + test('it returns an empty string when influencers is undefined', () => { + const wrapper = mount({createInfluencers()}); + 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({createInfluencers(anomalies.anomalies[0])}); + const wrapper = mount({createInfluencers(anomalies.anomalies[0].influencers)}); 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({createInfluencers(anomalies.anomalies[0])}); + const wrapper = mount({createInfluencers(anomalies.anomalies[0].influencers)}); expect(wrapper.text()).toEqual(''); }); @@ -50,7 +55,7 @@ describe('create_influencers', () => { {}, { 'influencer-name-two': 'influencer-value-two' }, ]; - const wrapper = mount({createInfluencers(anomalies.anomalies[0])}); + const wrapper = mount({createInfluencers(anomalies.anomalies[0].influencers)}); expect(wrapper.text()).toEqual( 'influencer-name-one: "influencer-value-one"influencer-name-two: "influencer-value-two"' ); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.tsx b/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.tsx index 010353f2c1e12..3418cad8d5d71 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.tsx @@ -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 => { @@ -19,19 +18,14 @@ export const createKeyAndValue = (influencer: Record): string => } }; -export const createInfluencers = (score: Anomaly): JSX.Element => { - return ( - <> - {score.influencers - .filter(influencer => !isEmpty(influencer)) - .map(influencer => { - const keyAndValue = createKeyAndValue(influencer); - return ( - - {keyAndValue} - - ); - })} - - ); -}; +export const createInfluencers = (influencers: Array> = []): JSX.Element[] => + influencers + .filter(influencer => !isEmpty(influencer)) + .map(influencer => { + const keyAndValue = createKeyAndValue(influencer); + return ( + + {keyAndValue} + + ); + }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts b/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts index 5913dec4277af..727be435d712a 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts @@ -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); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts b/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts index 6507c15d4e407..54b825d4b9426 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts @@ -7,7 +7,7 @@ import { getEntries } from '../get_entries'; export const getHostNameFromInfluencers = ( - influencers: Array>, + influencers: Array> = [], hostName?: string ): string | null => { const recordFound = influencers.find(influencer => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts b/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts index fa3b5e2c4e326..0ed3c3844a5b8 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts @@ -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); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts b/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts index 2ea5cad63b1bf..16e1be05d0299 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts @@ -8,7 +8,7 @@ import { DestinationOrSource, isDestinationOrSource } from '../types'; import { getEntries } from '../get_entries'; export const getNetworkFromInfluencers = ( - influencers: Array>, + influencers: Array> = [], ip?: string ): { ip: string; type: DestinationOrSource } | null => { const recordFound = influencers.find(influencer => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap index e0b5714d74c59..e620507d2001d 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap @@ -191,23 +191,21 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` gutterSize="none" responsive={false} > - - - host.name: "zeek-iowa" - - - process.name: "du" - - - user.name: "root" - - + + host.name: "zeek-iowa" + + + process.name: "du" + + + user.name: "root" + , "title": "Influenced By", }, diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx index 81aa58a0d0368..da07337653dc0 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx @@ -88,7 +88,7 @@ export const createDescriptionList = ( title: i18n.INFLUENCED_BY, description: ( - {createInfluencers(score)} + {createInfluencers(score.influencers)} ), }, diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts b/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts index 4340bcaf274d4..09ce472fe8a7b 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts @@ -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'"); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.ts b/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.ts index 219d408598a6b..f2db3b8a6d57b 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.ts @@ -12,8 +12,10 @@ export const createEntityFromRecord = (entity: Record): 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> = [] +): 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)) { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 5aeeee6d58304..3a1fcbb653efe 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -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({column.render(undefined, anomaly)}); + expect(wrapper.text()).toEqual(''); + } else { + expect(column).not.toBe(null); + } + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx index ef829cadb9fb6..97d09c3e74b06 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx @@ -95,29 +95,30 @@ export const getAnomaliesHostTableColumns = ( field: 'anomaly.influencers', render: (influencers, anomaliesByHost) => ( - {influencers.map(influencer => { - const [key, value] = getEntries(influencer); - const entityName = key != null ? key : ''; - const entityValue = value != null ? value : ''; - return ( - - - - - - - - ); - })} + {influencers && + influencers.map(influencer => { + const [key, value] = getEntries(influencer); + const entityName = key != null ? key : ''; + const entityValue = value != null ? value : ''; + return ( + + + + + + + + ); + })} ), }, diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx index 706c4feaa0f37..d063ed023bca6 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -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({column.render(undefined, anomaly)}); + expect(wrapper.text()).toEqual(''); + } else { + expect(column).not.toBe(null); + } + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx index 771f6f50857ae..4792c9343eae6 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -93,25 +93,26 @@ export const getAnomaliesNetworkTableColumns = ( field: 'anomaly.influencers', render: (influencers, anomaliesByNetwork) => ( - {influencers.map(influencer => { - const [key, value] = getEntries(influencer); - const entityName = key != null ? key : ''; - const entityValue = value != null ? value : ''; - return ( - - - - ); - })} + {influencers && + influencers.map(influencer => { + const [key, value] = getEntries(influencer); + const entityName = key != null ? key : ''; + const entityValue = value != null ? value : ''; + return ( + + + + ); + })} ), }, diff --git a/x-pack/legacy/plugins/siem/public/components/ml/types.ts b/x-pack/legacy/plugins/siem/public/components/ml/types.ts index d76b2258d088c..75780a659e245 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/types.ts @@ -54,7 +54,7 @@ export interface Anomaly { detectorIndex: number; entityName: string; entityValue: string; - influencers: Array>; + influencers?: Array>; jobId: string; rowId: string; severity: number;