## 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:
parent
62a1a4a144
commit
f7dad8961d
|
@ -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-*',
|
||||
|
|
|
@ -365,6 +365,7 @@ exports[`DragDropContextWrapper rendering it renders against the snapshot 1`] =
|
|||
"example": null,
|
||||
"format": "",
|
||||
"indexes": Array [
|
||||
"apm-*-transaction*",
|
||||
"auditbeat-*",
|
||||
"endgame-*",
|
||||
"filebeat-*",
|
||||
|
|
|
@ -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: '*',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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-*",
|
||||
|
|
|
@ -378,6 +378,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
|
|||
"example": null,
|
||||
"format": "",
|
||||
"indexes": Array [
|
||||
"apm-*-transaction*",
|
||||
"auditbeat-*",
|
||||
"endgame-*",
|
||||
"filebeat-*",
|
||||
|
|
|
@ -370,6 +370,7 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = `
|
|||
"example": null,
|
||||
"format": "",
|
||||
"indexes": Array [
|
||||
"apm-*-transaction*",
|
||||
"auditbeat-*",
|
||||
"endgame-*",
|
||||
"filebeat-*",
|
||||
|
|
|
@ -365,6 +365,7 @@ exports[`ZeekDetails rendering it renders the default ZeekDetails 1`] = `
|
|||
"example": null,
|
||||
"format": "",
|
||||
"indexes": Array [
|
||||
"apm-*-transaction*",
|
||||
"auditbeat-*",
|
||||
"endgame-*",
|
||||
"filebeat-*",
|
||||
|
|
|
@ -370,6 +370,7 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = `
|
|||
"example": null,
|
||||
"format": "",
|
||||
"indexes": Array [
|
||||
"apm-*-transaction*",
|
||||
"auditbeat-*",
|
||||
"endgame-*",
|
||||
"filebeat-*",
|
||||
|
|
Loading…
Reference in a new issue