[8.16] [SecuritySolution] Update entity store source field (#197186) (#197373)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[SecuritySolution] Update entity store source field
(#197186)](https://github.com/elastic/kibana/pull/197186)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Pablo
Machado","email":"pablo.nevesmachado@elastic.co"},"sourceCommit":{"committedDate":"2024-10-23T08:05:05Z","message":"[SecuritySolution]
Update entity store source field (#197186)\n\n## Summary\r\n\r\nIn this
PR the source field will only store the first identified index\r\nfor an
entity.\r\nThe PR also updates the entities list panel to display a
textual\r\ndescription of the source index and adds a new source field
filter.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/c7aad254-f871-4035-9dac-89decce31a55\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Mark Hopkin
<mark.hopkin@elastic.co>\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"0bafb9632c2e1b09dd56586f15dca83d8ad5b358","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","v9.0.0","Team:
SecuritySolution","Theme: entity_analytics","Feature:Entity
Analytics","v8.9.0","Team:Entity Analytics","8.16
candidate","v8.16.0","backport:version","v8.17.0"],"title":"[SecuritySolution]
Update entity store source
field","number":197186,"url":"https://github.com/elastic/kibana/pull/197186","mergeCommit":{"message":"[SecuritySolution]
Update entity store source field (#197186)\n\n## Summary\r\n\r\nIn this
PR the source field will only store the first identified index\r\nfor an
entity.\r\nThe PR also updates the entities list panel to display a
textual\r\ndescription of the source index and adds a new source field
filter.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/c7aad254-f871-4035-9dac-89decce31a55\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Mark Hopkin
<mark.hopkin@elastic.co>\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"0bafb9632c2e1b09dd56586f15dca83d8ad5b358"}},"sourceBranch":"main","suggestedTargetBranches":["8.9","8.16","8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/197186","number":197186,"mergeCommit":{"message":"[SecuritySolution]
Update entity store source field (#197186)\n\n## Summary\r\n\r\nIn this
PR the source field will only store the first identified index\r\nfor an
entity.\r\nThe PR also updates the entities list panel to display a
textual\r\ndescription of the source index and adds a new source field
filter.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/c7aad254-f871-4035-9dac-89decce31a55\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Mark Hopkin
<mark.hopkin@elastic.co>\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"0bafb9632c2e1b09dd56586f15dca83d8ad5b358"}},{"branch":"8.9","label":"v8.9.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>
This commit is contained in:
Kibana Machine 2024-10-23 20:52:47 +11:00 committed by GitHub
parent 5daf58c563
commit f25094f779
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 246 additions and 126 deletions

View file

@ -30472,9 +30472,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source
@ -30626,9 +30624,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source

View file

@ -30472,9 +30472,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source
@ -30626,9 +30624,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source

View file

@ -39302,9 +39302,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source
@ -39456,9 +39454,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source

View file

@ -39302,9 +39302,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source
@ -39456,9 +39454,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source

View file

@ -24,7 +24,7 @@ export const UserEntity = z.object({
'@timestamp': z.string().datetime(),
entity: z.object({
name: z.string(),
source: z.array(z.string()),
source: z.string(),
}),
user: z.object({
full_name: z.array(z.string()).optional(),
@ -48,7 +48,7 @@ export const HostEntity = z.object({
'@timestamp': z.string().datetime(),
entity: z.object({
name: z.string(),
source: z.array(z.string()),
source: z.string(),
}),
host: z.object({
hostname: z.array(z.string()).optional(),

View file

@ -22,12 +22,10 @@ components:
- name
- source
properties:
name:
name:
type: string
source:
type: array
items:
type: string
type: string
user:
type: object
properties:
@ -84,12 +82,10 @@ components:
- name
- source
properties:
name:
name:
type: string
source:
type: array
items:
type: string
type: string
host:
type: object
properties:

View file

@ -910,9 +910,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source
@ -1062,9 +1060,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source

View file

@ -910,9 +910,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source
@ -1062,9 +1060,7 @@ components:
name:
type: string
source:
items:
type: string
type: array
type: string
required:
- name
- source

View file

@ -8,30 +8,26 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { MultiselectFilter } from '../../../../common/components/multiselect_filter';
import { EntitySourceTag } from '../types';
interface SourceFilterProps {
selectedItems: EntitySource[];
onChange: (selectedItems: EntitySource[]) => void;
selectedItems: EntitySourceTag[];
onChange: (selectedItems: EntitySourceTag[]) => void;
}
export enum EntitySource {
CSV_UPLOAD = 'CSV upload',
EVENTS = 'Events',
}
// TODO Fix the Entity Source field before using it
export const EntitySourceFilter: React.FC<SourceFilterProps> = ({ selectedItems, onChange }) => {
return (
<MultiselectFilter<EntitySource>
<MultiselectFilter<EntitySourceTag>
title={i18n.translate(
'xpack.securitySolution.entityAnalytics.entityStore.entitySource.filterTitle',
{
defaultMessage: 'Source',
}
)}
items={Object.values(EntitySource)}
items={Object.values(EntitySourceTag)}
selectedItems={selectedItems}
onSelectionChange={onChange}
width={140}
width={190}
/>
);
};

View file

@ -33,7 +33,7 @@ const responseData: ListEntitiesResponse = {
user: { name: entityName },
entity: {
name: entityName,
source: ['source'],
source: 'test-index',
},
},
],

View file

@ -21,12 +21,13 @@ import { EntityType } from '../../../../common/api/entity_analytics/entity_store
import type { Criteria } from '../../../explore/components/paginated_table';
import { PaginatedTable } from '../../../explore/components/paginated_table';
import { SeverityFilter } from '../severity/severity_filter';
import type { EntitySource } from './components/entity_source_filter';
import { EntitySourceFilter } from './components/entity_source_filter';
import { useEntitiesListFilters } from './hooks/use_entities_list_filters';
import { AssetCriticalityFilter } from '../asset_criticality/asset_criticality_filter';
import { useEntitiesListQuery } from './hooks/use_entities_list_query';
import { ENTITIES_LIST_TABLE_ID, rowItems } from './constants';
import { useEntitiesListColumns } from './hooks/use_entities_list_columns';
import type { EntitySourceTag } from './types';
export const EntitiesList: React.FC = () => {
const { deleteQuery, setQuery, isInitializing, from, to } = useGlobalTime();
@ -40,7 +41,7 @@ export const EntitiesList: React.FC = () => {
const [selectedSeverities, setSelectedSeverities] = useState<RiskSeverity[]>([]);
const [selectedCriticalities, setSelectedCriticalities] = useState<CriticalityLevels[]>([]);
const [selectedSources, _] = useState<EntitySource[]>([]);
const [selectedSources, setSelectedSources] = useState<EntitySourceTag[]>([]);
const filter = useEntitiesListFilters({
selectedSeverities,
@ -147,6 +148,7 @@ export const EntitiesList: React.FC = () => {
selectedItems={selectedCriticalities}
onChange={setSelectedCriticalities}
/>
<EntitySourceFilter selectedItems={selectedSources} onChange={setSelectedSources} />
</EuiFilterGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -5,40 +5,70 @@
* 2.0.
*/
import { isUserEntity } from './helpers';
import { isUserEntity, sourceFieldToText } from './helpers';
import type {
Entity,
UserEntity,
} from '../../../../common/api/entity_analytics/entity_store/entities/common.gen';
import { render } from '@testing-library/react';
import { TestProviders } from '@kbn/timelines-plugin/public/mock';
describe('isUserEntity', () => {
it('should return true if the record is a UserEntity', () => {
const userEntity: UserEntity = {
'@timestamp': '2021-08-02T14:00:00.000Z',
user: {
name: 'test_user',
},
entity: {
name: 'test_user',
source: ['logs-test'],
},
};
describe('helpers', () => {
describe('isUserEntity', () => {
it('should return true if the record is a UserEntity', () => {
const userEntity: UserEntity = {
'@timestamp': '2021-08-02T14:00:00.000Z',
user: {
name: 'test_user',
},
entity: {
name: 'test_user',
source: 'logs-test',
},
};
expect(isUserEntity(userEntity)).toBe(true);
expect(isUserEntity(userEntity)).toBe(true);
});
it('should return false if the record is not a UserEntity', () => {
const nonUserEntity: Entity = {
'@timestamp': '2021-08-02T14:00:00.000Z',
host: {
name: 'test_host',
},
entity: {
name: 'test_host',
source: 'logs-test',
},
};
expect(isUserEntity(nonUserEntity)).toBe(false);
});
});
it('should return false if the record is not a UserEntity', () => {
const nonUserEntity: Entity = {
'@timestamp': '2021-08-02T14:00:00.000Z',
host: {
name: 'test_host',
},
entity: {
name: 'test_host',
source: ['logs-test'],
},
};
describe('sourceFieldToText', () => {
it("should return 'Events' if the value isn't risk or asset", () => {
const { container } = render(sourceFieldToText('anything'), {
wrapper: TestProviders,
});
expect(isUserEntity(nonUserEntity)).toBe(false);
expect(container).toHaveTextContent('Events');
});
it("should return 'Risk' if the value is a risk index", () => {
const { container } = render(sourceFieldToText('risk-score.risk-score-default'), {
wrapper: TestProviders,
});
expect(container).toHaveTextContent('Risk');
});
it("should return 'Asset Criticality' if the value is a asset criticality index", () => {
const { container } = render(sourceFieldToText('.asset-criticality.asset-criticality-*'), {
wrapper: TestProviders,
});
expect(container).toHaveTextContent('Asset Criticality');
});
});
});

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
ASSET_CRITICALITY_INDEX_PATTERN,
RISK_SCORE_INDEX_PATTERN,
} from '../../../../common/constants';
import type {
Entity,
UserEntity,
} from '../../../../common/api/entity_analytics/entity_store/entities/common.gen';
export const isUserEntity = (record: Entity): record is UserEntity =>
!!(record as UserEntity)?.user;
export const sourceFieldToText = (source: string) => {
if (source.match(`^${RISK_SCORE_INDEX_PATTERN}`)) {
return (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.helpers.sourceField.riskDescription"
defaultMessage="Risk"
/>
);
}
if (source.match(`^${ASSET_CRITICALITY_INDEX_PATTERN}`)) {
return (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.helpers.sourceField.criticalityDescription"
defaultMessage="Asset Criticality"
/>
);
}
return (
<FormattedMessage
id="xpack.securitySolution.entityAnalytics.entityStore.helpers.sourceField.eventDescription"
defaultMessage="Events"
/>
);
};

View file

@ -17,9 +17,9 @@ import { RiskScoreLevel } from '../../severity/common';
import { getEmptyTagValue } from '../../../../common/components/empty_value';
import type { Columns } from '../../../../explore/components/paginated_table';
import type { Entity } from '../../../../../common/api/entity_analytics/entity_store/entities/common.gen';
import type { CriticalityLevels } from '../../../../../common/constants';
import { type CriticalityLevels } from '../../../../../common/constants';
import { ENTITIES_LIST_TABLE_ID } from '../constants';
import { isUserEntity } from '../helpers';
import { isUserEntity, sourceFieldToText } from '../helpers';
import { CRITICALITY_LEVEL_TITLE } from '../../asset_criticality/translations';
export type EntitiesListColumns = [
@ -110,7 +110,7 @@ export const useEntitiesListColumns = (): EntitiesListColumns => {
truncateText: { lines: 2 },
render: (source: string | undefined) => {
if (source != null) {
return <span>{source}</span>;
return sourceFieldToText(source);
}
return getEmptyTagValue();

View file

@ -11,7 +11,7 @@ import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { CriticalityLevels } from '../../../../../common/constants';
import { RiskSeverity } from '../../../../../common/search_strategy';
import { EntitySource } from '../components/entity_source_filter';
import { EntitySourceTag } from '../types';
jest.mock('../../../../common/hooks/use_global_filter_query');
@ -52,7 +52,6 @@ describe('useEntitiesListFilters', () => {
{ term: { 'host.risk.calculated_level': RiskSeverity.High } },
{ term: { 'user.risk.calculated_level': RiskSeverity.High } },
],
minimum_should_match: 1,
},
},
];
@ -72,7 +71,6 @@ describe('useEntitiesListFilters', () => {
const expectedFilters: QueryDslQueryContainer[] = [
{
bool: {
minimum_should_match: 1,
should: [
{
term: {
@ -97,13 +95,48 @@ describe('useEntitiesListFilters', () => {
useEntitiesListFilters({
selectedSeverities: [],
selectedCriticalities: [],
selectedSources: [EntitySource.CSV_UPLOAD, EntitySource.EVENTS],
selectedSources: [EntitySourceTag.criticality, EntitySourceTag.risk],
})
);
const expectedFilters: QueryDslQueryContainer[] = [
{ term: { 'entity.source': EntitySource.CSV_UPLOAD } },
{ term: { 'entity.source': EntitySource.EVENTS } },
{
bool: {
should: [
{ wildcard: { 'entity.source': '.asset-criticality.asset-criticality-*' } },
{ wildcard: { 'entity.source': 'risk-score.risk-score-*' } },
],
},
},
];
expect(result.current).toEqual(expectedFilters);
});
it('should return source events filters when events is selected', () => {
const { result } = renderHook(() =>
useEntitiesListFilters({
selectedSeverities: [],
selectedCriticalities: [],
selectedSources: [EntitySourceTag.events],
})
);
const expectedFilters: QueryDslQueryContainer[] = [
{
bool: {
should: [
{
bool: {
must_not: [
{ wildcard: { 'entity.source': '.asset-criticality.asset-criticality-*' } },
{ wildcard: { 'entity.source': 'risk-score.risk-score-*' } },
],
},
},
],
},
},
];
expect(result.current).toEqual(expectedFilters);
@ -132,7 +165,7 @@ describe('useEntitiesListFilters', () => {
useEntitiesListFilters({
selectedSeverities: [RiskSeverity.Low],
selectedCriticalities: [CriticalityLevels.HIGH_IMPACT],
selectedSources: [EntitySource.CSV_UPLOAD],
selectedSources: [EntitySourceTag.risk],
})
);
@ -143,16 +176,18 @@ describe('useEntitiesListFilters', () => {
{ term: { 'host.risk.calculated_level': RiskSeverity.Low } },
{ term: { 'user.risk.calculated_level': RiskSeverity.Low } },
],
minimum_should_match: 1,
},
},
{
bool: {
should: [{ term: { 'asset.criticality': CriticalityLevels.HIGH_IMPACT } }],
minimum_should_match: 1,
},
},
{ term: { 'entity.source': EntitySource.CSV_UPLOAD } },
{
bool: {
should: [{ wildcard: { 'entity.source': 'risk-score.risk-score-*' } }],
},
},
globalQuery,
];

View file

@ -7,15 +7,19 @@
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { useMemo } from 'react';
import type { CriticalityLevels } from '../../../../../common/constants';
import {
ASSET_CRITICALITY_INDEX_PATTERN,
RISK_SCORE_INDEX_PATTERN,
type CriticalityLevels,
} from '../../../../../common/constants';
import type { RiskSeverity } from '../../../../../common/search_strategy';
import { useGlobalFilterQuery } from '../../../../common/hooks/use_global_filter_query';
import type { EntitySource } from '../components/entity_source_filter';
import { EntitySourceTag } from '../types';
interface UseEntitiesListFiltersParams {
selectedSeverities: RiskSeverity[];
selectedCriticalities: CriticalityLevels[];
selectedSources: EntitySource[];
selectedSources: EntitySourceTag[];
}
export const useEntitiesListFilters = ({
@ -35,17 +39,20 @@ export const useEntitiesListFilters = ({
'asset.criticality': value,
},
})),
minimum_should_match: 1,
},
},
]
: [];
const sourceFilter: QueryDslQueryContainer[] = selectedSources.map((value) => ({
term: {
'entity.source': value,
},
}));
const sourceFilter: QueryDslQueryContainer[] = selectedSources.length
? [
{
bool: {
should: selectedSources.map((tag) => getSourceTagFilterQuery(tag)),
},
},
]
: [];
const severityFilter: QueryDslQueryContainer[] = selectedSeverities.length
? [
@ -63,7 +70,6 @@ export const useEntitiesListFilters = ({
},
},
]),
minimum_should_match: 1,
},
},
]
@ -80,3 +86,37 @@ export const useEntitiesListFilters = ({
return filterList;
}, [globalQuery, selectedCriticalities, selectedSeverities, selectedSources]);
};
const getSourceTagFilterQuery = (tag: EntitySourceTag): QueryDslQueryContainer => {
if (tag === EntitySourceTag.risk) {
return {
wildcard: {
'entity.source': RISK_SCORE_INDEX_PATTERN,
},
};
}
if (tag === EntitySourceTag.criticality) {
return {
wildcard: {
'entity.source': ASSET_CRITICALITY_INDEX_PATTERN,
},
};
}
return {
bool: {
must_not: [
{
wildcard: {
'entity.source': ASSET_CRITICALITY_INDEX_PATTERN,
},
},
{
wildcard: {
'entity.source': RISK_SCORE_INDEX_PATTERN,
},
},
],
},
};
};

View file

@ -5,10 +5,8 @@
* 2.0.
*/
import type {
Entity,
UserEntity,
} from '../../../../common/api/entity_analytics/entity_store/entities/common.gen';
export const isUserEntity = (record: Entity): record is UserEntity =>
!!(record as UserEntity)?.user;
export enum EntitySourceTag {
'risk' = 'Risk',
'criticality' = 'Asset Criticality',
'events' = 'Events',
}

View file

@ -7,7 +7,7 @@
import type { EntityType } from '../../../../../../common/api/entity_analytics/entity_store';
import { getIdentityFieldForEntityType } from '../../utils';
import { collectValues, newestValue } from '../definition_utils';
import { oldestValue, newestValue } from '../definition_utils';
import type { UnitedDefinitionField } from '../types';
export const getCommonUnitedFieldDefinitions = ({
@ -19,10 +19,9 @@ export const getCommonUnitedFieldDefinitions = ({
}): UnitedDefinitionField[] => {
const identityField = getIdentityFieldForEntityType(entityType);
return [
collectValues({
oldestValue({
sourceField: '_index',
field: 'entity.source',
fieldHistoryLength,
}),
newestValue({ field: 'asset.criticality' }),
newestValue({

View file

@ -117,8 +117,7 @@ describe('getUnitedEntityDefinition', () => {
},
Object {
"field": "entity.source",
"maxLength": 10,
"operation": "collect_values",
"operation": "prefer_oldest_value",
},
Object {
"field": "asset.criticality",
@ -219,8 +218,10 @@ describe('getUnitedEntityDefinition', () => {
},
Object {
"aggregation": Object {
"limit": 10,
"type": "terms",
"sort": Object {
"@timestamp": "asc",
},
"type": "top_value",
},
"destination": "entity.source",
"source": "_index",
@ -373,8 +374,7 @@ describe('getUnitedEntityDefinition', () => {
},
Object {
"field": "entity.source",
"maxLength": 10,
"operation": "collect_values",
"operation": "prefer_oldest_value",
},
Object {
"field": "asset.criticality",
@ -467,8 +467,10 @@ describe('getUnitedEntityDefinition', () => {
},
Object {
"aggregation": Object {
"limit": 10,
"type": "terms",
"sort": Object {
"@timestamp": "asc",
},
"type": "top_value",
},
"destination": "entity.source",
"source": "_index",