[SIEM] Adds support for apm-* to the network map (#54876) (#55142)

## Summary

Resolves https://github.com/elastic/kibana/issues/52297, https://github.com/elastic/kibana/issues/52565

To improve the display of APM data within SIEM (specifically the `HTTP Table` and `Network Map`), this PR adds `apm-*-transcation*` to `siem:defaultIndex`, and additional support for showing `client`/`server` layers on the `Network Map` when a matching `apm-*` index pattern is present.

The map now supports pattern matching when checking for available Kibana Index Patterns, and so matches `apm-*-transcation*` -> `apm-*` (if exists). Additionally, the map config was updated to generate layers for client/server geo fields (instead of the usual source/dest) since these are the fields Transactions use.

![image](https://user-images.githubusercontent.com/2946766/72573225-2a038880-3882-11ea-9590-a545d726dbf9.png)

<img width="1214" alt="Screen Shot 2020-01-14 at 18 22 11" src="https://user-images.githubusercontent.com/2946766/72407120-bcd5e300-371b-11ea-90cc-a0714320a59c.png">


### 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)~
- [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
  - Will work with @benskelker on updating the maps docs
- [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)~
- [ ] ~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 2020-01-16 18:52:52 -07:00 committed by GitHub
parent 62a1a4a144
commit f7dad8961d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 661 additions and 34 deletions

View file

@ -6,6 +6,7 @@
/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */
export const defaultIndexPattern = [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',

View file

@ -365,6 +365,7 @@ exports[`DragDropContextWrapper rendering it renders against the snapshot 1`] =
"example": null,
"format": "",
"indexes": Array [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",

View file

@ -5,11 +5,16 @@
*/
import { IndexPatternMapping } from '../types';
import { IndexPatternSavedObject } from '../../ml_popover/types';
export const mockIndexPatternIds: IndexPatternMapping[] = [
{ title: 'filebeat-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' },
];
export const mockAPMIndexPatternIds: IndexPatternMapping[] = [
{ title: 'apm-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' },
];
export const mockSourceLayer = {
sourceDescriptor: {
id: 'uuid.v4()',
@ -113,6 +118,109 @@ export const mockDestinationLayer = {
query: { query: '', language: 'kuery' },
};
export const mockClientLayer = {
sourceDescriptor: {
id: 'uuid.v4()',
type: 'ES_SEARCH',
applyGlobalQuery: true,
geoField: 'client.geo.location',
filterByMapBounds: false,
tooltipProperties: [
'host.name',
'client.ip',
'client.domain',
'client.geo.country_iso_code',
'client.as.organization.name',
],
useTopHits: false,
topHitsTimeField: '@timestamp',
topHitsSize: 1,
indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918',
},
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: { color: '#6092C0' },
},
lineColor: {
type: 'STATIC',
options: { color: '#FFFFFF' },
},
lineWidth: { type: 'STATIC', options: { size: 2 } },
iconSize: { type: 'STATIC', options: { size: 8 } },
iconOrientation: {
type: 'STATIC',
options: { orientation: 0 },
},
symbol: {
options: { symbolizeAs: 'icon', symbolId: 'home' },
},
},
},
id: 'uuid.v4()',
label: `apm-* | Client Point`,
minZoom: 0,
maxZoom: 24,
alpha: 1,
visible: true,
type: 'VECTOR',
query: { query: '', language: 'kuery' },
joins: [],
};
export const mockServerLayer = {
sourceDescriptor: {
id: 'uuid.v4()',
type: 'ES_SEARCH',
applyGlobalQuery: true,
geoField: 'server.geo.location',
filterByMapBounds: true,
tooltipProperties: [
'host.name',
'server.ip',
'server.domain',
'server.geo.country_iso_code',
'server.as.organization.name',
],
useTopHits: false,
topHitsTimeField: '@timestamp',
topHitsSize: 1,
indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918',
},
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: { color: '#D36086' },
},
lineColor: {
type: 'STATIC',
options: { color: '#FFFFFF' },
},
lineWidth: { type: 'STATIC', options: { size: 2 } },
iconSize: { type: 'STATIC', options: { size: 8 } },
iconOrientation: {
type: 'STATIC',
options: { orientation: 0 },
},
symbol: {
options: { symbolizeAs: 'icon', symbolId: 'marker' },
},
},
},
id: 'uuid.v4()',
label: `apm-* | Server Point`,
minZoom: 0,
maxZoom: 24,
alpha: 1,
visible: true,
type: 'VECTOR',
query: { query: '', language: 'kuery' },
};
export const mockLineLayer = {
sourceDescriptor: {
type: 'ES_PEW_PEW',
@ -173,6 +281,66 @@ export const mockLineLayer = {
query: { query: '', language: 'kuery' },
};
export const mockClientServerLineLayer = {
sourceDescriptor: {
type: 'ES_PEW_PEW',
applyGlobalQuery: true,
id: 'uuid.v4()',
indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918',
sourceGeoField: 'client.geo.location',
destGeoField: 'server.geo.location',
metrics: [
{ type: 'sum', field: 'client.bytes', label: 'client.bytes' },
{ type: 'sum', field: 'server.bytes', label: 'server.bytes' },
],
},
style: {
type: 'VECTOR',
properties: {
fillColor: {
type: 'STATIC',
options: { color: '#1EA593' },
},
lineColor: {
type: 'STATIC',
options: { color: '#6092C0' },
},
lineWidth: {
type: 'DYNAMIC',
options: {
field: {
label: 'count',
name: 'doc_count',
origin: 'source',
},
minSize: 1,
maxSize: 8,
fieldMetaOptions: {
isEnabled: true,
sigma: 3,
},
},
},
iconSize: { type: 'STATIC', options: { size: 10 } },
iconOrientation: {
type: 'STATIC',
options: { orientation: 0 },
},
symbol: {
options: { symbolizeAs: 'circle', symbolId: 'airfield' },
},
},
},
id: 'uuid.v4()',
label: `apm-* | Line`,
minZoom: 0,
maxZoom: 24,
alpha: 0.5,
visible: true,
type: 'VECTOR',
query: { query: '', language: 'kuery' },
};
export const mockLayerList = [
{
sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true },
@ -209,3 +377,83 @@ export const mockLayerListDouble = [
mockDestinationLayer,
mockSourceLayer,
];
export const mockLayerListMixed = [
{
sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true },
id: 'uuid.v4()',
label: null,
minZoom: 0,
maxZoom: 24,
alpha: 1,
visible: true,
style: null,
type: 'VECTOR_TILE',
},
mockLineLayer,
mockDestinationLayer,
mockSourceLayer,
mockClientServerLineLayer,
mockServerLayer,
mockClientLayer,
];
export const mockAPMIndexPattern: IndexPatternSavedObject = {
id: 'apm-*',
type: 'index-pattern',
updated_at: '',
version: 'abc',
attributes: {
title: 'apm-*',
},
};
export const mockAPMRegexIndexPattern: IndexPatternSavedObject = {
id: 'apm-7.*',
type: 'index-pattern',
updated_at: '',
version: 'abc',
attributes: {
title: 'apm-7.*',
},
};
export const mockFilebeatIndexPattern: IndexPatternSavedObject = {
id: 'filebeat-*',
type: 'index-pattern',
updated_at: '',
version: 'abc',
attributes: {
title: 'filebeat-*',
},
};
export const mockAuditbeatIndexPattern: IndexPatternSavedObject = {
id: 'auditbeat-*',
type: 'index-pattern',
updated_at: '',
version: 'abc',
attributes: {
title: 'auditbeat-*',
},
};
export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = {
id: 'apm-*-transaction*',
type: 'index-pattern',
updated_at: '',
version: 'abc',
attributes: {
title: 'apm-*-transaction*',
},
};
export const mockGlobIndexPattern: IndexPatternSavedObject = {
id: '*',
type: 'index-pattern',
updated_at: '',
version: 'abc',
attributes: {
title: '*',
},
};

View file

@ -18,7 +18,7 @@ import { Loader } from '../loader';
import { displayErrorToast, useStateToaster } from '../toasters';
import { Embeddable } from './embeddable';
import { EmbeddableHeader } from './embeddable_header';
import { createEmbeddable } from './embedded_map_helpers';
import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers';
import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt';
import { MapToolTip } from './map_tool_tip/map_tool_tip';
import * as i18n from './translations';
@ -107,10 +107,12 @@ export const EmbeddedMapComponent = ({
useEffect(() => {
let isSubscribed = true;
async function setupEmbeddable() {
// Ensure at least one `siem:defaultIndex` index pattern exists before trying to import
const matchingIndexPatterns = kibanaIndexPatterns.filter(ip =>
siemDefaultIndices.includes(ip.attributes.title)
);
// Ensure at least one `siem:defaultIndex` kibana index pattern exists before creating embeddable
const matchingIndexPatterns = findMatchingIndexPatterns({
kibanaIndexPatterns,
siemDefaultIndices,
});
if (matchingIndexPatterns.length === 0 && isSubscribed) {
setIsLoading(false);
setIsIndexError(true);

View file

@ -4,9 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { createEmbeddable } from './embedded_map_helpers';
import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers';
import { createUiNewPlatformMock } from 'ui/new_platform/__mocks__/helpers';
import { createPortalNode } from 'react-reverse-portal';
import {
mockAPMIndexPattern,
mockAPMRegexIndexPattern,
mockAPMTransactionIndexPattern,
mockAuditbeatIndexPattern,
mockFilebeatIndexPattern,
mockGlobIndexPattern,
} from './__mocks__/mock';
jest.mock('ui/new_platform');
@ -51,4 +59,58 @@ describe('embedded_map_helpers', () => {
expect(embeddable.reload).toHaveBeenCalledTimes(1);
});
});
describe('findMatchingIndexPatterns', () => {
const siemDefaultIndices = [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'winlogbeat-*',
];
test('finds exact matching index patterns ', () => {
const matchingIndexPatterns = findMatchingIndexPatterns({
kibanaIndexPatterns: [mockFilebeatIndexPattern, mockAuditbeatIndexPattern],
siemDefaultIndices,
});
expect(matchingIndexPatterns).toEqual([mockFilebeatIndexPattern, mockAuditbeatIndexPattern]);
});
test('finds glob-matched index patterns ', () => {
const matchingIndexPatterns = findMatchingIndexPatterns({
kibanaIndexPatterns: [mockAPMIndexPattern, mockFilebeatIndexPattern],
siemDefaultIndices,
});
expect(matchingIndexPatterns).toEqual([mockAPMIndexPattern, mockFilebeatIndexPattern]);
});
test('does not find glob-matched index pattern containing regex', () => {
const matchingIndexPatterns = findMatchingIndexPatterns({
kibanaIndexPatterns: [mockAPMRegexIndexPattern, mockFilebeatIndexPattern],
siemDefaultIndices,
});
expect(matchingIndexPatterns).toEqual([mockFilebeatIndexPattern]);
});
test('finds exact glob-matched index patterns ', () => {
const matchingIndexPatterns = findMatchingIndexPatterns({
kibanaIndexPatterns: [mockAPMTransactionIndexPattern, mockFilebeatIndexPattern],
siemDefaultIndices,
});
expect(matchingIndexPatterns).toEqual([
mockAPMTransactionIndexPattern,
mockFilebeatIndexPattern,
]);
});
test('finds glob-only index patterns ', () => {
const matchingIndexPatterns = findMatchingIndexPatterns({
kibanaIndexPatterns: [mockGlobIndexPattern, mockFilebeatIndexPattern],
siemDefaultIndices,
});
expect(matchingIndexPatterns).toEqual([mockGlobIndexPattern, mockFilebeatIndexPattern]);
});
});
});

View file

@ -7,6 +7,7 @@
import uuid from 'uuid';
import React from 'react';
import { OutPortal, PortalNode } from 'react-reverse-portal';
import minimatch from 'minimatch';
import { ViewMode } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import {
IndexPatternMapping,
@ -20,6 +21,7 @@ import { getLayerList } from './map_config';
import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants';
import * as i18n from './translations';
import { Query, esFilters } from '../../../../../../../src/plugins/data/public';
import { IndexPatternSavedObject } from '../ml_popover/types';
/**
* Creates MapEmbeddable with provided initial configuration
@ -108,3 +110,25 @@ export const createEmbeddable = async (
return embeddableObject;
};
/**
* Returns kibanaIndexPatterns that wildcard match at least one of siemDefaultIndices
*
* @param kibanaIndexPatterns
* @param siemDefaultIndices
*/
export const findMatchingIndexPatterns = ({
kibanaIndexPatterns,
siemDefaultIndices,
}: {
kibanaIndexPatterns: IndexPatternSavedObject[];
siemDefaultIndices: string[];
}): IndexPatternSavedObject[] => {
try {
return kibanaIndexPatterns.filter(kip =>
siemDefaultIndices.some(sdi => minimatch(sdi, kip.attributes.title))
);
} catch {
return [];
}
};

View file

@ -4,13 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getDestinationLayer, getLayerList, getLineLayer, getSourceLayer } from './map_config';
import { getDestinationLayer, getLayerList, getLineLayer, getSourceLayer, lmc } from './map_config';
import {
mockAPMIndexPatternIds,
mockClientLayer,
mockClientServerLineLayer,
mockDestinationLayer,
mockIndexPatternIds,
mockLayerList,
mockLayerListDouble,
mockLayerListMixed,
mockLineLayer,
mockServerLayer,
mockSourceLayer,
} from './__mocks__/mock';
@ -32,29 +37,70 @@ describe('map_config', () => {
const layerList = getLayerList([...mockIndexPatternIds, ...mockIndexPatternIds]);
expect(layerList).toStrictEqual(mockLayerListDouble);
});
test('it returns the complete layerList for multiple indices with custom layer mapping', () => {
const layerList = getLayerList([...mockIndexPatternIds, ...mockAPMIndexPatternIds]);
expect(layerList).toStrictEqual(mockLayerListMixed);
});
});
describe('#getSourceLayer', () => {
test('it returns a source layer', () => {
const layerList = getSourceLayer(mockIndexPatternIds[0].title, mockIndexPatternIds[0].id);
const layerList = getSourceLayer(
mockIndexPatternIds[0].title,
mockIndexPatternIds[0].id,
lmc.default.source
);
expect(layerList).toStrictEqual(mockSourceLayer);
});
test('it returns a source layer for custom layer mapping', () => {
const layerList = getSourceLayer(
mockAPMIndexPatternIds[0].title,
mockAPMIndexPatternIds[0].id,
lmc[mockAPMIndexPatternIds[0].title].source
);
expect(layerList).toStrictEqual(mockClientLayer);
});
});
describe('#getDestinationLayer', () => {
test('it returns a destination layer', () => {
const layerList = getDestinationLayer(
mockIndexPatternIds[0].title,
mockIndexPatternIds[0].id
mockIndexPatternIds[0].id,
lmc.default.destination
);
expect(layerList).toStrictEqual(mockDestinationLayer);
});
test('it returns a destination layer for custom layer mapping', () => {
const layerList = getDestinationLayer(
mockAPMIndexPatternIds[0].title,
mockAPMIndexPatternIds[0].id,
lmc[mockAPMIndexPatternIds[0].title].destination
);
expect(layerList).toStrictEqual(mockServerLayer);
});
});
describe('#getLineLayer', () => {
test('it returns a line layer', () => {
const layerList = getLineLayer(mockIndexPatternIds[0].title, mockIndexPatternIds[0].id);
const layerList = getLineLayer(
mockIndexPatternIds[0].title,
mockIndexPatternIds[0].id,
lmc.default
);
expect(layerList).toStrictEqual(mockLineLayer);
});
test('it returns a line layer for custom layer mapping', () => {
const layerList = getLineLayer(
mockAPMIndexPatternIds[0].title,
mockAPMIndexPatternIds[0].id,
lmc[mockAPMIndexPatternIds[0].title]
);
expect(layerList).toStrictEqual(mockClientServerLineLayer);
});
});
});

View file

@ -6,11 +6,16 @@
import uuid from 'uuid';
import { euiPaletteColorBlind } from '@elastic/eui';
import { IndexPatternMapping } from './types';
import {
IndexPatternMapping,
LayerMapping,
LayerMappingCollection,
LayerMappingDetails,
} from './types';
import * as i18n from './translations';
const euiVisColorPalette = euiPaletteColorBlind();
// Update source/destination field mappings to modify what fields will be returned to map tooltip
// Update field mappings to modify what fields will be returned to map tooltip
const sourceFieldMappings: Record<string, string> = {
'host.name': i18n.HOST,
'source.ip': i18n.SOURCE_IP,
@ -25,16 +30,67 @@ const destinationFieldMappings: Record<string, string> = {
'destination.geo.country_iso_code': i18n.LOCATION,
'destination.as.organization.name': i18n.ASN,
};
const clientFieldMappings: Record<string, string> = {
'host.name': i18n.HOST,
'client.ip': i18n.CLIENT_IP,
'client.domain': i18n.CLIENT_DOMAIN,
'client.geo.country_iso_code': i18n.LOCATION,
'client.as.organization.name': i18n.ASN,
};
const serverFieldMappings: Record<string, string> = {
'host.name': i18n.HOST,
'server.ip': i18n.SERVER_IP,
'server.domain': i18n.SERVER_DOMAIN,
'server.geo.country_iso_code': i18n.LOCATION,
'server.as.organization.name': i18n.ASN,
};
// Mapping of field -> display name for use within map tooltip
export const sourceDestinationFieldMappings: Record<string, string> = {
...sourceFieldMappings,
...destinationFieldMappings,
...clientFieldMappings,
...serverFieldMappings,
};
// Field names of LineLayer props returned from Maps API
export const SUM_OF_SOURCE_BYTES = 'sum_of_source.bytes';
export const SUM_OF_DESTINATION_BYTES = 'sum_of_destination.bytes';
export const SUM_OF_CLIENT_BYTES = 'sum_of_client.bytes';
export const SUM_OF_SERVER_BYTES = 'sum_of_server.bytes';
// Mapping to fields for creating specific layers for a given index pattern
// e.g. The apm-* index pattern needs layers for client/server instead of source/destination
export const lmc: LayerMappingCollection = {
default: {
source: {
metricField: 'source.bytes',
geoField: 'source.geo.location',
tooltipProperties: Object.keys(sourceFieldMappings),
label: i18n.SOURCE_LAYER,
},
destination: {
metricField: 'destination.bytes',
geoField: 'destination.geo.location',
tooltipProperties: Object.keys(destinationFieldMappings),
label: i18n.DESTINATION_LAYER,
},
},
'apm-*': {
source: {
metricField: 'client.bytes',
geoField: 'client.geo.location',
tooltipProperties: Object.keys(clientFieldMappings),
label: i18n.CLIENT_LAYER,
},
destination: {
metricField: 'server.bytes',
geoField: 'server.geo.location',
tooltipProperties: Object.keys(serverFieldMappings),
label: i18n.SERVER_LAYER,
},
},
};
/**
* Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source,
@ -58,9 +114,9 @@ export const getLayerList = (indexPatternIds: IndexPatternMapping[]) => {
...indexPatternIds.reduce((acc: object[], { title, id }) => {
return [
...acc,
getLineLayer(title, id),
getDestinationLayer(title, id),
getSourceLayer(title, id),
getLineLayer(title, id, lmc[title] ?? lmc.default),
getDestinationLayer(title, id, lmc[title]?.destination ?? lmc.default.destination),
getSourceLayer(title, id, lmc[title]?.source ?? lmc.default.source),
];
}, []),
];
@ -72,15 +128,20 @@ export const getLayerList = (indexPatternIds: IndexPatternMapping[]) => {
*
* @param indexPatternTitle used as layer name in LayerToC UI: "${indexPatternTitle} | Source point"
* @param indexPatternId used as layer's indexPattern to query for data
* @param layerDetails layer-specific field details
*/
export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string) => ({
export const getSourceLayer = (
indexPatternTitle: string,
indexPatternId: string,
layerDetails: LayerMappingDetails
) => ({
sourceDescriptor: {
id: uuid.v4(),
type: 'ES_SEARCH',
applyGlobalQuery: true,
geoField: 'source.geo.location',
geoField: layerDetails.geoField,
filterByMapBounds: false,
tooltipProperties: Object.keys(sourceFieldMappings),
tooltipProperties: layerDetails.tooltipProperties,
useTopHits: false,
topHitsTimeField: '@timestamp',
topHitsSize: 1,
@ -109,7 +170,7 @@ export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string
},
},
id: uuid.v4(),
label: `${indexPatternTitle} | ${i18n.SOURCE_LAYER}`,
label: `${indexPatternTitle} | ${layerDetails.label}`,
minZoom: 0,
maxZoom: 24,
alpha: 1,
@ -125,15 +186,21 @@ export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string
*
* @param indexPatternTitle used as layer name in LayerToC UI: "${indexPatternTitle} | Destination point"
* @param indexPatternId used as layer's indexPattern to query for data
* @param layerDetails layer-specific field details
*
*/
export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: string) => ({
export const getDestinationLayer = (
indexPatternTitle: string,
indexPatternId: string,
layerDetails: LayerMappingDetails
) => ({
sourceDescriptor: {
id: uuid.v4(),
type: 'ES_SEARCH',
applyGlobalQuery: true,
geoField: 'destination.geo.location',
geoField: layerDetails.geoField,
filterByMapBounds: true,
tooltipProperties: Object.keys(destinationFieldMappings),
tooltipProperties: layerDetails.tooltipProperties,
useTopHits: false,
topHitsTimeField: '@timestamp',
topHitsSize: 1,
@ -162,7 +229,7 @@ export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: s
},
},
id: uuid.v4(),
label: `${indexPatternTitle} | ${i18n.DESTINATION_LAYER}`,
label: `${indexPatternTitle} | ${layerDetails.label}`,
minZoom: 0,
maxZoom: 24,
alpha: 1,
@ -177,18 +244,31 @@ export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: s
*
* @param indexPatternTitle used as layer name in LayerToC UI: "${indexPatternTitle} | Line"
* @param indexPatternId used as layer's indexPattern to query for data
* @param layerDetails layer-specific field details
*/
export const getLineLayer = (indexPatternTitle: string, indexPatternId: string) => ({
export const getLineLayer = (
indexPatternTitle: string,
indexPatternId: string,
layerDetails: LayerMapping
) => ({
sourceDescriptor: {
type: 'ES_PEW_PEW',
applyGlobalQuery: true,
id: uuid.v4(),
indexPatternId,
sourceGeoField: 'source.geo.location',
destGeoField: 'destination.geo.location',
sourceGeoField: layerDetails.source.geoField,
destGeoField: layerDetails.destination.geoField,
metrics: [
{ type: 'sum', field: 'source.bytes', label: 'source.bytes' },
{ type: 'sum', field: 'destination.bytes', label: 'destination.bytes' },
{
type: 'sum',
field: layerDetails.source.metricField,
label: layerDetails.source.metricField,
},
{
type: 'sum',
field: layerDetails.destination.metricField,
label: layerDetails.destination.metricField,
},
],
},
style: {

View file

@ -49,3 +49,53 @@ exports[`LineToolTipContent renders correctly against snapshot 1`] = `
</EuiFlexItem>
</EuiFlexGroup>
`;
exports[`LineToolTipContent renders correctly against snapshot when rendering client & server 1`] = `
<EuiFlexGroup
gutterSize="none"
justifyContent="center"
>
<EuiFlexItem>
<Styled(EuiBadge)
color="hollow"
>
<Styled(EuiFlexGroup)
direction="column"
>
<EuiFlexItem
grow={false}
>
Client
</EuiFlexItem>
</Styled(EuiFlexGroup)>
</Styled(EuiBadge)>
</EuiFlexItem>
<SourceDestinationArrows
contextId="contextId"
destinationBytes={
Array [
"testPropValue",
]
}
eventId="map-line-tooltip-contextId"
sourceBytes={
Array [
"testPropValue",
]
}
/>
<EuiFlexItem>
<Styled(EuiBadge)
color="hollow"
>
<Styled(EuiFlexGroup)>
<EuiFlexItem
grow={false}
>
Server
</EuiFlexItem>
</Styled(EuiFlexGroup)>
</Styled(EuiBadge)>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -8,7 +8,12 @@ import { shallow } from 'enzyme';
import React from 'react';
import { LineToolTipContentComponent } from './line_tool_tip_content';
import { FeatureProperty } from '../types';
import { SUM_OF_DESTINATION_BYTES, SUM_OF_SOURCE_BYTES } from '../map_config';
import {
SUM_OF_CLIENT_BYTES,
SUM_OF_DESTINATION_BYTES,
SUM_OF_SERVER_BYTES,
SUM_OF_SOURCE_BYTES,
} from '../map_config';
describe('LineToolTipContent', () => {
const mockFeatureProps: FeatureProperty[] = [
@ -22,10 +27,31 @@ describe('LineToolTipContent', () => {
},
];
const mockClientServerFeatureProps: FeatureProperty[] = [
{
_propertyKey: SUM_OF_SERVER_BYTES,
_rawValue: 'testPropValue',
},
{
_propertyKey: SUM_OF_CLIENT_BYTES,
_rawValue: 'testPropValue',
},
];
test('renders correctly against snapshot', () => {
const wrapper = shallow(
<LineToolTipContentComponent contextId={'contextId'} featureProps={mockFeatureProps} />
);
expect(wrapper).toMatchSnapshot();
});
test('renders correctly against snapshot when rendering client & server', () => {
const wrapper = shallow(
<LineToolTipContentComponent
contextId={'contextId'}
featureProps={mockClientServerFeatureProps}
/>
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -8,7 +8,12 @@ import React from 'react';
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { SourceDestinationArrows } from '../../source_destination/source_destination_arrows';
import { SUM_OF_DESTINATION_BYTES, SUM_OF_SOURCE_BYTES } from '../map_config';
import {
SUM_OF_CLIENT_BYTES,
SUM_OF_DESTINATION_BYTES,
SUM_OF_SERVER_BYTES,
SUM_OF_SOURCE_BYTES,
} from '../map_config';
import { FeatureProperty } from '../types';
import * as i18n from '../translations';
@ -38,25 +43,29 @@ export const LineToolTipContentComponent = ({
{}
);
const isSrcDest = Object.keys(lineProps).includes(SUM_OF_SOURCE_BYTES);
return (
<EuiFlexGroup justifyContent="center" gutterSize="none">
<EuiFlexItem>
<FlowBadge color="hollow">
<EuiFlexGroupStyled direction="column">
<EuiFlexItem grow={false}>{i18n.SOURCE}</EuiFlexItem>
<EuiFlexItem grow={false}>{isSrcDest ? i18n.SOURCE : i18n.CLIENT}</EuiFlexItem>
</EuiFlexGroupStyled>
</FlowBadge>
</EuiFlexItem>
<SourceDestinationArrows
contextId={contextId}
destinationBytes={lineProps[SUM_OF_DESTINATION_BYTES]}
destinationBytes={
isSrcDest ? lineProps[SUM_OF_DESTINATION_BYTES] : lineProps[SUM_OF_SERVER_BYTES]
}
eventId={`map-line-tooltip-${contextId}`}
sourceBytes={lineProps[SUM_OF_SOURCE_BYTES]}
sourceBytes={isSrcDest ? lineProps[SUM_OF_SOURCE_BYTES] : lineProps[SUM_OF_CLIENT_BYTES]}
/>
<EuiFlexItem>
<FlowBadge color="hollow">
<EuiFlexGroupStyled>
<EuiFlexItem grow={false}>{i18n.DESTINATION}</EuiFlexItem>
<EuiFlexItem grow={false}>{isSrcDest ? i18n.DESTINATION : i18n.SERVER}</EuiFlexItem>
</EuiFlexGroupStyled>
</FlowBadge>
</EuiFlexItem>

View file

@ -41,6 +41,20 @@ export const DESTINATION_LAYER = i18n.translate(
}
);
export const CLIENT_LAYER = i18n.translate(
'xpack.siem.components.embeddables.embeddedMap.clientLayerLabel',
{
defaultMessage: 'Client Point',
}
);
export const SERVER_LAYER = i18n.translate(
'xpack.siem.components.embeddables.embeddedMap.serverLayerLabel',
{
defaultMessage: 'Server Point',
}
);
export const LINE_LAYER = i18n.translate(
'xpack.siem.components.embeddables.embeddedMap.lineLayerLabel',
{
@ -118,6 +132,20 @@ export const DESTINATION_IP = i18n.translate(
}
);
export const CLIENT_IP = i18n.translate(
'xpack.siem.components.embeddables.mapToolTip.pointContent.clientIPTitle',
{
defaultMessage: 'Client IP',
}
);
export const SERVER_IP = i18n.translate(
'xpack.siem.components.embeddables.mapToolTip.pointContent.serverIPTitle',
{
defaultMessage: 'Server IP',
}
);
export const SOURCE_DOMAIN = i18n.translate(
'xpack.siem.components.embeddables.mapToolTip.pointContent.sourceDomainTitle',
{
@ -132,6 +160,20 @@ export const DESTINATION_DOMAIN = i18n.translate(
}
);
export const CLIENT_DOMAIN = i18n.translate(
'xpack.siem.components.embeddables.mapToolTip.pointContent.clientDomainTitle',
{
defaultMessage: 'Client domain',
}
);
export const SERVER_DOMAIN = i18n.translate(
'xpack.siem.components.embeddables.mapToolTip.pointContent.serverDomainTitle',
{
defaultMessage: 'Server domain',
}
);
export const LOCATION = i18n.translate(
'xpack.siem.components.embeddables.mapToolTip.pointContent.locationTitle',
{
@ -159,3 +201,17 @@ export const DESTINATION = i18n.translate(
defaultMessage: 'Destination',
}
);
export const CLIENT = i18n.translate(
'xpack.siem.components.embeddables.mapToolTip.lineContent.clientLabel',
{
defaultMessage: 'Client',
}
);
export const SERVER = i18n.translate(
'xpack.siem.components.embeddables.mapToolTip.lineContent.serverLabel',
{
defaultMessage: 'Server',
}
);

View file

@ -31,6 +31,22 @@ export interface IndexPatternMapping {
id: string;
}
export interface LayerMappingDetails {
metricField: string;
geoField: string;
tooltipProperties: string[];
label: string;
}
export interface LayerMapping {
source: LayerMappingDetails;
destination: LayerMappingDetails;
}
export interface LayerMappingCollection {
[indexPatternTitle: string]: LayerMapping;
}
export type SetQuery = (params: {
id: string;
inspect: inputsModel.InspectQuery | null;

View file

@ -373,6 +373,7 @@ exports[`EventDetails rendering should match snapshot 1`] = `
"example": null,
"format": "",
"indexes": Array [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",
@ -1064,6 +1065,7 @@ In other use cases the message field can be used to concatenate different values
"example": null,
"format": "",
"indexes": Array [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",

View file

@ -378,6 +378,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
"example": null,
"format": "",
"indexes": Array [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",

View file

@ -370,6 +370,7 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = `
"example": null,
"format": "",
"indexes": Array [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",

View file

@ -365,6 +365,7 @@ exports[`ZeekDetails rendering it renders the default ZeekDetails 1`] = `
"example": null,
"format": "",
"indexes": Array [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",

View file

@ -370,6 +370,7 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = `
"example": null,
"format": "",
"indexes": Array [
"apm-*-transaction*",
"auditbeat-*",
"endgame-*",
"filebeat-*",