diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md new file mode 100644 index 00000000000000..64805eefbfea1f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.getcustomnavlink_.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [getCustomNavLink$](./kibana-plugin-core-public.chromestart.getcustomnavlink_.md) + +## ChromeStart.getCustomNavLink$() method + +Get an observable of the current custom nav link + +Signature: + +```typescript +getCustomNavLink$(): Observable | undefined>; +``` +Returns: + +`Observable | undefined>` + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.md index b4eadc93fe78d3..e983ad50d2afe5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.md @@ -55,6 +55,7 @@ core.chrome.setHelpExtension(elem => { | [getBadge$()](./kibana-plugin-core-public.chromestart.getbadge_.md) | Get an observable of the current badge | | [getBrand$()](./kibana-plugin-core-public.chromestart.getbrand_.md) | Get an observable of the current brand information. | | [getBreadcrumbs$()](./kibana-plugin-core-public.chromestart.getbreadcrumbs_.md) | Get an observable of the current list of breadcrumbs | +| [getCustomNavLink$()](./kibana-plugin-core-public.chromestart.getcustomnavlink_.md) | Get an observable of the current custom nav link | | [getHelpExtension$()](./kibana-plugin-core-public.chromestart.gethelpextension_.md) | Get an observable of the current custom help conttent | | [getIsNavDrawerLocked$()](./kibana-plugin-core-public.chromestart.getisnavdrawerlocked_.md) | Get an observable of the current locked state of the nav drawer. | | [getIsVisible$()](./kibana-plugin-core-public.chromestart.getisvisible_.md) | Get an observable of the current visibility state of the chrome. | @@ -64,6 +65,7 @@ core.chrome.setHelpExtension(elem => { | [setBadge(badge)](./kibana-plugin-core-public.chromestart.setbadge.md) | Override the current badge | | [setBrand(brand)](./kibana-plugin-core-public.chromestart.setbrand.md) | Set the brand configuration. | | [setBreadcrumbs(newBreadcrumbs)](./kibana-plugin-core-public.chromestart.setbreadcrumbs.md) | Override the current set of breadcrumbs | +| [setCustomNavLink(newCustomNavLink)](./kibana-plugin-core-public.chromestart.setcustomnavlink.md) | Override the current set of custom nav link | | [setHelpExtension(helpExtension)](./kibana-plugin-core-public.chromestart.sethelpextension.md) | Override the current set of custom help content | | [setHelpSupportUrl(url)](./kibana-plugin-core-public.chromestart.sethelpsupporturl.md) | Override the default support URL shown in the help menu | | [setIsVisible(isVisible)](./kibana-plugin-core-public.chromestart.setisvisible.md) | Set the temporary visibility for the chrome. This does nothing if the chrome is hidden by default and should be used to hide the chrome for things like full-screen modes with an exit button. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md new file mode 100644 index 00000000000000..adfb57f9c5ff25 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromestart.setcustomnavlink.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeStart](./kibana-plugin-core-public.chromestart.md) > [setCustomNavLink](./kibana-plugin-core-public.chromestart.setcustomnavlink.md) + +## ChromeStart.setCustomNavLink() method + +Override the current set of custom nav link + +Signature: + +```typescript +setCustomNavLink(newCustomNavLink?: Partial): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newCustomNavLink | Partial<ChromeNavLink> | | + +Returns: + +`void` + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 1a03ac5ee3d1ad..29c340bc390f28 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -150,7 +150,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsBulkUpdateResponse](./kibana-plugin-core-server.savedobjectsbulkupdateresponse.md) | | | [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. | | [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | -| [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | +| [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation.Note: this type intentially doesn't include a type definition for defining the dynamic mapping parameter. Saved Object fields should always inherit the dynamic: 'strict' paramater. If you are unsure of the shape of your data use type: 'object', enabled: false instead. | | [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. | | [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | | | [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md deleted file mode 100644 index e63e543e68d517..00000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) > [dynamic](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md) - -## SavedObjectsComplexFieldMapping.dynamic property - -Signature: - -```typescript -dynamic?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md index 60e62212609d9f..a7d13b0015e3fd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md @@ -6,6 +6,8 @@ See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. +Note: this type intentially doesn't include a type definition for defining the `dynamic` mapping parameter. Saved Object fields should always inherit the `dynamic: 'strict'` paramater. If you are unsure of the shape of your data use `type: 'object', enabled: false` instead. + Signature: ```typescript @@ -16,7 +18,6 @@ export interface SavedObjectsComplexFieldMapping | Property | Type | Description | | --- | --- | --- | -| [dynamic](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md) | string | | | [properties](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.properties.md) | SavedObjectsMappingProperties | | | [type](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.type.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index be6af335f20cd5..6f42fb32fdb7be 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -28,6 +28,7 @@ export interface IFieldType | [searchable](./kibana-plugin-plugins-data-public.ifieldtype.searchable.md) | boolean | | | [sortable](./kibana-plugin-plugins-data-public.ifieldtype.sortable.md) | boolean | | | [subType](./kibana-plugin-plugins-data-public.ifieldtype.subtype.md) | IFieldSubType | | +| [toSpec](./kibana-plugin-plugins-data-public.ifieldtype.tospec.md) | () => FieldSpec | | | [type](./kibana-plugin-plugins-data-public.ifieldtype.type.md) | string | | | [visualizable](./kibana-plugin-plugins-data-public.ifieldtype.visualizable.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md new file mode 100644 index 00000000000000..1fb4084c25d343 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) > [toSpec](./kibana-plugin-plugins-data-public.ifieldtype.tospec.md) + +## IFieldType.toSpec property + +Signature: + +```typescript +toSpec?: () => FieldSpec; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md index 9a93148e4a466f..d4dca48c7cd7b4 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md @@ -7,5 +7,7 @@ Signature: ```typescript -fields: IIndexPatternFieldList; +fields: IIndexPatternFieldList & { + toSpec: () => FieldSpec[]; + }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md new file mode 100644 index 00000000000000..764dd116382217 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [initFromSpec](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) + +## IndexPattern.initFromSpec() method + +Signature: + +```typescript +initFromSpec(spec: IndexPatternSpec): this; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | + +Returns: + +`this` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 8ffa7b6b36f56b..d39b384c538f1b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -21,7 +21,7 @@ export declare class IndexPattern implements IIndexPattern | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | any | | -| [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList | | +| [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => FieldSpec[];
} | | | [fieldsFetcher](./kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md) | | any | | | [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | any | | | [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | any | | @@ -30,7 +30,6 @@ export declare class IndexPattern implements IIndexPattern | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string | | -| [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) | | string | | | [typeMeta](./kibana-plugin-plugins-data-public.indexpattern.typemeta.md) | | TypeMeta | | ## Methods @@ -49,6 +48,7 @@ export declare class IndexPattern implements IIndexPattern | [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | | | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | | [init(forceFieldRefresh)](./kibana-plugin-plugins-data-public.indexpattern.init.md) | | | +| [initFromSpec(spec)](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeBasedWildcard()](./kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | @@ -59,5 +59,6 @@ export declare class IndexPattern implements IIndexPattern | [removeScriptedField(field)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | | | [save(saveAttempts)](./kibana-plugin-plugins-data-public.indexpattern.save.md) | | | | [toJSON()](./kibana-plugin-plugins-data-public.indexpattern.tojson.md) | | | +| [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | | | [toString()](./kibana-plugin-plugins-data-public.indexpattern.tostring.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md similarity index 53% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md index 58047d9e27ac63..d1a78eea660cea 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md @@ -1,11 +1,15 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [toSpec](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) -## IndexPattern.type property +## IndexPattern.toSpec() method Signature: ```typescript -type?: string; +toSpec(): IndexPatternSpec; ``` +Returns: + +`IndexPatternSpec` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md index e1e0d58ce38c10..7a195702b6f13f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `Field` class Signature: ```typescript -constructor(indexPattern: IIndexPattern, spec: FieldSpec | Field, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); +constructor(indexPattern: IIndexPattern, spec: FieldSpecExportFmt | FieldSpec | Field, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); ``` ## Parameters @@ -17,7 +17,7 @@ constructor(indexPattern: IIndexPattern, spec: FieldSpec | Field, shortDotsEnabl | Parameter | Type | Description | | --- | --- | --- | | indexPattern | IIndexPattern | | -| spec | FieldSpec | Field | | +| spec | FieldSpecExportFmt | FieldSpec | Field | | | shortDotsEnable | boolean | | | { fieldFormats, onNotification } | FieldDependencies | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md index ca2552aeb1b425..ec19a4854bf0e6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md @@ -7,5 +7,5 @@ Signature: ```typescript -conflictDescriptions?: Record; +conflictDescriptions?: FieldSpecConflictDescriptions; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 8fa1ee0d72e54d..d82999e7a96af4 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -22,7 +22,7 @@ export declare class Field implements IFieldType | --- | --- | --- | --- | | [$$spec](./kibana-plugin-plugins-data-public.indexpatternfield.__spec.md) | | FieldSpec | | | [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | -| [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | | +| [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | FieldSpecConflictDescriptions | | | [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | | | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | | @@ -37,6 +37,7 @@ export declare class Field implements IFieldType | [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | | [sortable](./kibana-plugin-plugins-data-public.indexpatternfield.sortable.md) | | boolean | | | [subType](./kibana-plugin-plugins-data-public.indexpatternfield.subtype.md) | | IFieldSubType | | +| [toSpec](./kibana-plugin-plugins-data-public.indexpatternfield.tospec.md) | | () => FieldSpecExportFmt | | | [type](./kibana-plugin-plugins-data-public.indexpatternfield.type.md) | | string | | | [visualizable](./kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md new file mode 100644 index 00000000000000..35714faa03bc9a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [toSpec](./kibana-plugin-plugins-data-public.indexpatternfield.tospec.md) + +## IndexPatternField.toSpec property + +Signature: + +```typescript +toSpec: () => FieldSpecExportFmt; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index 5375cf2a2ef431..77a2954428f8d4 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -28,6 +28,7 @@ export interface IFieldType | [searchable](./kibana-plugin-plugins-data-server.ifieldtype.searchable.md) | boolean | | | [sortable](./kibana-plugin-plugins-data-server.ifieldtype.sortable.md) | boolean | | | [subType](./kibana-plugin-plugins-data-server.ifieldtype.subtype.md) | IFieldSubType | | +| [toSpec](./kibana-plugin-plugins-data-server.ifieldtype.tospec.md) | () => FieldSpec | | | [type](./kibana-plugin-plugins-data-server.ifieldtype.type.md) | string | | | [visualizable](./kibana-plugin-plugins-data-server.ifieldtype.visualizable.md) | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md new file mode 100644 index 00000000000000..d1863bebce4f00 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) > [toSpec](./kibana-plugin-plugins-data-server.ifieldtype.tospec.md) + +## IFieldType.toSpec property + +Signature: + +```typescript +toSpec?: () => FieldSpec; +``` diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 07541b1adff16c..5abd14312f4a66 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -38,7 +38,7 @@ import { AppCategory } from '../../../../types'; import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { createEuiListItem, createRecentNavLink } from './nav_link'; +import { createEuiListItem, createRecentNavLink, isModifiedOrPrevented } from './nav_link'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -184,17 +184,13 @@ export function CollapsibleNav({ label: 'Home', iconType: 'home', href: homeHref, - onClick: (event: React.MouseEvent) => { - closeNav(); - if ( - event.isDefaultPrevented() || - event.altKey || - event.metaKey || - event.ctrlKey - ) { + onClick: (event) => { + if (isModifiedOrPrevented(event)) { return; } + event.preventDefault(); + closeNav(); navigateToApp('home'); }, }, @@ -230,7 +226,13 @@ export function CollapsibleNav({ return { ...hydratedLink, 'data-test-subj': 'collapsibleNavAppLink--recent', - onClick: closeNav, + onClick: (event) => { + if (isModifiedOrPrevented(event)) { + return; + } + + closeNav(); + }, }; })} maxWidth="none" diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index 6b5cecd138376b..c70a40f49643e0 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -17,20 +17,15 @@ * under the License. */ -import { EuiImage } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; import { HttpStart } from '../../../http'; import { relativeToAbsolute } from '../../nav_links/to_nav_link'; -function isModifiedEvent(event: React.MouseEvent) { - return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); -} - -function LinkIcon({ url }: { url: string }) { - return ; -} +export const isModifiedOrPrevented = (event: React.MouseEvent) => + event.metaKey || event.altKey || event.ctrlKey || event.shiftKey || event.defaultPrevented; interface Props { link: ChromeNavLink; @@ -69,14 +64,16 @@ export function createEuiListItem({ href, /* Use href and onClick to support "open in new tab" and SPA navigation in the same link */ onClick(event: React.MouseEvent) { - onClick(); + if (!isModifiedOrPrevented(event)) { + onClick(); + } + if ( !externalLink && // ignore external links !legacyMode && // ignore when in legacy mode !legacy && // ignore links to legacy apps - !event.defaultPrevented && // onClick prevented default event.button === 0 && // ignore everything but left clicks - !isModifiedEvent(event) // ignore clicks with modifier keys + !isModifiedOrPrevented(event) ) { event.preventDefault(); navigateToApp(id); @@ -88,7 +85,8 @@ export function createEuiListItem({ 'data-test-subj': dataTestSubj, ...(basePath && { iconType: euiIconType, - icon: !euiIconType && icon ? : undefined, + icon: + !euiIconType && icon ? : undefined, }), }; } diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 0770e8843e2f63..2ac5bd98f7ed45 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -46,6 +46,7 @@ export { httpServiceMock } from './http/http_service.mock'; export { loggingSystemMock } from './logging/logging_system.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; +export { migrationMocks } from './saved_objects/migrations/mocks'; export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index 8362d1f16bd2a0..c037ed733549e6 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -145,10 +145,14 @@ export interface SavedObjectsCoreFieldMapping { /** * See {@link SavedObjectsFieldMapping} for documentation. * + * Note: this type intentially doesn't include a type definition for defining + * the `dynamic` mapping parameter. Saved Object fields should always inherit + * the `dynamic: 'strict'` paramater. If you are unsure of the shape of your + * data use `type: 'object', enabled: false` instead. + * * @public */ export interface SavedObjectsComplexFieldMapping { - dynamic?: string; type?: string; properties: SavedObjectsMappingProperties; } diff --git a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts index c2a7b11e057cd3..4561f4d30e104f 100644 --- a/src/core/server/saved_objects/migrations/core/build_active_mappings.ts +++ b/src/core/server/saved_objects/migrations/core/build_active_mappings.ts @@ -130,6 +130,8 @@ function defaultMapping(): IndexMapping { dynamic: 'strict', properties: { migrationVersion: { + // Saved Objects can't redefine dynamic, but we cheat here to support migrations + // @ts-expect-error dynamic: 'true', type: 'object', }, diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index b2ffe2ad04a880..e588eb7877322a 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -195,7 +195,7 @@ async function migrateSourceToDest(context: Context) { await Index.write( callCluster, dest.indexName, - migrateRawDocs(serializer, documentMigrator.migrate, docs, log) + await migrateRawDocs(serializer, documentMigrator.migrate, docs, log) ); } } diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index e55b72be2436d9..6e4dd9615d4230 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -26,7 +26,7 @@ import { createSavedObjectsMigrationLoggerMock } from '../../migrations/mocks'; describe('migrateRawDocs', () => { test('converts raw docs to saved objects', async () => { const transform = jest.fn((doc: any) => _.set(doc, 'attributes.name', 'HOI!')); - const result = migrateRawDocs( + const result = await migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), transform, [ @@ -55,7 +55,7 @@ describe('migrateRawDocs', () => { const transform = jest.fn((doc: any) => _.set(_.cloneDeep(doc), 'attributes.name', 'TADA') ); - const result = migrateRawDocs( + const result = await migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), transform, [ diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index a2b72ea76c1a28..2bdf59d25dc74d 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -21,7 +21,11 @@ * This file provides logic for migrating raw documents. */ -import { SavedObjectsRawDoc, SavedObjectsSerializer } from '../../serialization'; +import { + SavedObjectsRawDoc, + SavedObjectsSerializer, + SavedObjectUnsanitizedDoc, +} from '../../serialization'; import { TransformFn } from './document_migrator'; import { SavedObjectsMigrationLogger } from '.'; @@ -33,26 +37,51 @@ import { SavedObjectsMigrationLogger } from '.'; * @param {SavedObjectsRawDoc[]} rawDocs * @returns {SavedObjectsRawDoc[]} */ -export function migrateRawDocs( +export async function migrateRawDocs( serializer: SavedObjectsSerializer, migrateDoc: TransformFn, rawDocs: SavedObjectsRawDoc[], log: SavedObjectsMigrationLogger -): SavedObjectsRawDoc[] { - return rawDocs.map((raw) => { +): Promise { + const migrateDocWithoutBlocking = transformNonBlocking(migrateDoc); + const processedDocs = []; + for (const raw of rawDocs) { if (serializer.isRawSavedObject(raw)) { const savedObject = serializer.rawToSavedObject(raw); savedObject.migrationVersion = savedObject.migrationVersion || {}; - return serializer.savedObjectToRaw({ - references: [], - ...migrateDoc(savedObject), - }); + processedDocs.push( + serializer.savedObjectToRaw({ + references: [], + ...(await migrateDocWithoutBlocking(savedObject)), + }) + ); + } else { + log.error( + `Error: Unable to migrate the corrupt Saved Object document ${raw._id}. To prevent Kibana from performing a migration on every restart, please delete or fix this document by ensuring that the namespace and type in the document's id matches the values in the namespace and type fields.`, + { rawDocument: raw } + ); + processedDocs.push(raw); } + } + return processedDocs; +} - log.error( - `Error: Unable to migrate the corrupt Saved Object document ${raw._id}. To prevent Kibana from performing a migration on every restart, please delete or fix this document by ensuring that the namespace and type in the document's id matches the values in the namespace and type fields.`, - { rawDocument: raw } - ); - return raw; - }); +/** + * Migration transform functions are potentially CPU heavy e.g. doing decryption/encryption + * or (de)/serializing large JSON payloads. + * Executing all transforms for a batch in a synchronous loop can block the event-loop for a long time. + * To prevent this we use setImmediate to ensure that the event-loop can process other parallel + * work in between each transform. + */ +function transformNonBlocking( + transform: TransformFn +): (doc: SavedObjectUnsanitizedDoc) => Promise { + // promises aren't enough to unblock the event loop + return (doc: SavedObjectUnsanitizedDoc) => + new Promise((resolve) => { + // set immediate is though + setImmediate(() => { + resolve(transform(doc)); + }); + }); } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4d6316fceb5682..00ec217bc85862 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1970,8 +1970,6 @@ export interface SavedObjectsClientWrapperOptions { // @public export interface SavedObjectsComplexFieldMapping { - // (undocumented) - dynamic?: string; // (undocumented) properties: SavedObjectsMappingProperties; // (undocumented) diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js index ddc6d000bca194..32eb7bf8712e31 100644 --- a/src/dev/build/tasks/copy_source_task.js +++ b/src/dev/build/tasks/copy_source_task.js @@ -34,9 +34,7 @@ export const CopySourceTask = { '!src/test_utils/**', '!src/fixtures/**', '!src/legacy/core_plugins/tests_bundle/**', - '!src/legacy/core_plugins/testbed/**', '!src/legacy/core_plugins/console/public/tests/**', - '!src/plugins/testbed/**', '!src/cli/cluster/**', '!src/cli/repl/**', '!src/es_archiver/**', diff --git a/src/fixtures/stubbed_saved_object_index_pattern.js b/src/fixtures/stubbed_saved_object_index_pattern.js index 15e47b40eb203f..8e0e230ef33dd8 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.js +++ b/src/fixtures/stubbed_saved_object_index_pattern.js @@ -27,6 +27,7 @@ export function stubbedSavedObjectIndexPattern(id) { id, type: 'index-pattern', attributes: { + timeFieldName: 'timestamp', customFormats: '{}', fields: mockLogstashFields, }, diff --git a/src/legacy/core_plugins/testbed/README.md b/src/legacy/core_plugins/testbed/README.md deleted file mode 100644 index ac50ffbb804b5c..00000000000000 --- a/src/legacy/core_plugins/testbed/README.md +++ /dev/null @@ -1,8 +0,0 @@ -## Kibana Testbed - -Sometimes when developing for Kibana, it is useful to have an isolated routable space to demonstrate new functionality. This Testbed provides such a space. - -To make use of the testbed, edit the testbed.js, testbed.html, and testbed.less files as necessary. When you are done demonstrating -your new functionality, remember to cleanup your changes and restore the testbed to its pristine state for the next person. - -To access the testbed, visit `http://localhost:5601/app/kibana#/testbed` diff --git a/src/legacy/core_plugins/testbed/index.js b/src/legacy/core_plugins/testbed/index.js deleted file mode 100644 index f0b61ea0c3de77..00000000000000 --- a/src/legacy/core_plugins/testbed/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; - -export default function (kibana) { - return new kibana.Plugin({ - id: 'testbed', - publicDir: resolve(__dirname, 'public'), - uiExports: { - hacks: ['plugins/testbed'], - }, - }); -} diff --git a/src/legacy/core_plugins/testbed/package.json b/src/legacy/core_plugins/testbed/package.json deleted file mode 100644 index 98fcaf7eda95da..00000000000000 --- a/src/legacy/core_plugins/testbed/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "testbed", - "version": "kibana" -} \ No newline at end of file diff --git a/src/legacy/core_plugins/testbed/public/index.js b/src/legacy/core_plugins/testbed/public/index.js deleted file mode 100644 index c6687de249cf2a..00000000000000 --- a/src/legacy/core_plugins/testbed/public/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './testbed'; diff --git a/src/legacy/core_plugins/testbed/public/testbed.html b/src/legacy/core_plugins/testbed/public/testbed.html deleted file mode 100644 index 52455beb02360d..00000000000000 --- a/src/legacy/core_plugins/testbed/public/testbed.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
- -
{{ testbed.data }}
- - - - - - -
-
diff --git a/src/legacy/core_plugins/testbed/public/testbed.js b/src/legacy/core_plugins/testbed/public/testbed.js deleted file mode 100644 index 13005a6106ca4e..00000000000000 --- a/src/legacy/core_plugins/testbed/public/testbed.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import uiRoutes from 'ui/routes'; -import template from './testbed.html'; - -uiRoutes.when('/testbed', { - template: template, - controllerAs: 'testbed', - controller: class TestbedController { - constructor() {} - }, -}); diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap index 4593349a408a7d..e61593f6bfb27b 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap @@ -33,3 +33,43 @@ Object { "type": "type", } `; + +exports[`Field spec snapshot 1`] = ` +Object { + "aggregatable": true, + "conflictDescriptions": Object { + "a": Array [ + "b", + "c", + ], + "d": Array [ + "e", + ], + }, + "count": 1, + "esTypes": Array [ + "type", + ], + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, + "lang": "lang", + "name": "name", + "readFromDocValues": false, + "script": "script", + "scripted": true, + "searchable": true, + "subType": Object { + "multi": Object { + "parent": "parent", + }, + "nested": Object { + "path": "path", + }, + }, + "type": "type", +} +`; diff --git a/src/plugins/data/common/index_patterns/fields/field.test.ts b/src/plugins/data/common/index_patterns/fields/field.test.ts index 711c176fed9ccb..910f22088f43a8 100644 --- a/src/plugins/data/common/index_patterns/fields/field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/field.test.ts @@ -20,7 +20,7 @@ import { Field } from './field'; import { IndexPattern } from '../index_patterns'; import { FieldFormatsStartCommon } from '../..'; -import { KBN_FIELD_TYPES } from '../../../common'; +import { KBN_FIELD_TYPES, FieldSpec, FieldSpecExportFmt } from '../../../common'; describe('Field', function () { function flatten(obj: Record) { @@ -59,8 +59,9 @@ describe('Field', function () { fieldFormatMap: { name: {}, _source: {}, _score: {}, _id: {} }, } as unknown) as IndexPattern, format: { name: 'formatName' }, - $$spec: {}, + $$spec: ({} as unknown) as FieldSpec, conflictDescriptions: { a: ['b', 'c'], d: ['e'] }, + toSpec: () => (({} as unknown) as FieldSpecExportFmt), } as Field; it('the correct properties are writable', () => { @@ -145,7 +146,7 @@ describe('Field', function () { }).toThrow(); expect(() => { - field.$$spec = { a: 'b' }; + field.$$spec = ({ a: 'b' } as unknown) as FieldSpec; }).toThrow(); }); @@ -219,4 +220,21 @@ describe('Field', function () { }); expect(flatten(field)).toMatchSnapshot(); }); + + it('spec snapshot', () => { + const field = new Field( + { + fieldFormatMap: { + name: { toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }) }, + }, + } as IndexPattern, + fieldValues, + false, + { + fieldFormats: {} as FieldFormatsStartCommon, + onNotification: () => {}, + } + ); + expect(field.toSpec()).toMatchSnapshot(); + }); }); diff --git a/src/plugins/data/common/index_patterns/fields/field.ts b/src/plugins/data/common/index_patterns/fields/field.ts index c53e3f2b1f621f..81c7aff8a0faaa 100644 --- a/src/plugins/data/common/index_patterns/fields/field.ts +++ b/src/plugins/data/common/index_patterns/fields/field.ts @@ -28,11 +28,14 @@ import { FieldFormat, shortenDottedString, } from '../../../common'; -import { OnNotification } from '../types'; +import { + OnNotification, + FieldSpec, + FieldSpecConflictDescriptions, + FieldSpecExportFmt, +} from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; -export type FieldSpec = Record; - interface FieldDependencies { fieldFormats: FieldFormatsStartCommon; onNotification: OnNotification; @@ -59,11 +62,11 @@ export class Field implements IFieldType { readFromDocValues?: boolean; format: any; $$spec: FieldSpec; - conflictDescriptions?: Record; + conflictDescriptions?: FieldSpecConflictDescriptions; constructor( indexPattern: IIndexPattern, - spec: FieldSpec | Field, + spec: FieldSpecExportFmt | FieldSpec | Field, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies ) { @@ -95,7 +98,7 @@ export class Field implements IFieldType { if (!type) type = getKbnFieldType('unknown'); - let format = spec.format; + let format: any = spec.format; if (!FieldFormat.isInstanceOfFieldFormat(format)) { format = @@ -148,6 +151,26 @@ export class Field implements IFieldType { // multi info obj.fact('subType'); - return obj.create(); + const newObj = obj.create(); + newObj.toSpec = function () { + return { + count: this.count, + script: this.script, + lang: this.lang, + conflictDescriptions: this.conflictDescriptions, + name: this.name, + type: this.type, + esTypes: this.esTypes, + scripted: this.scripted, + searchable: this.searchable, + aggregatable: this.aggregatable, + readFromDocValues: this.readFromDocValues, + subType: this.subType, + format: this.indexPattern?.fieldFormatMap[this.name]?.toJSON() || undefined, + }; + }; + return newObj; } + // only providing type info as constructor returns new object instead of `this` + toSpec = () => (({} as unknown) as FieldSpecExportFmt); } diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index 173a629863a716..c1ca5341328ce1 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -20,8 +20,8 @@ import { findIndex } from 'lodash'; import { IIndexPattern } from '../../types'; import { IFieldType } from '../../../common'; -import { Field, FieldSpec } from './field'; -import { OnNotification } from '../types'; +import { Field } from './field'; +import { OnNotification, FieldSpec } from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; type FieldMap = Map; @@ -102,6 +102,10 @@ export const getIndexPatternFieldListCreator = ({ this.removeByGroup(newField); this.setByGroup(newField); }; + + toSpec = () => { + return [...this.map((field) => field.toSpec())]; + }; } return new FieldList(...fieldListParams); diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index c336472a1e7d6f..558b5b57dce40a 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -17,10 +17,7 @@ * under the License. */ -export interface IFieldSubType { - multi?: { parent: string }; - nested?: { path: string }; -} +import { FieldSpec, IFieldSubType } from '../types'; export interface IFieldType { name: string; @@ -41,4 +38,5 @@ export interface IFieldType { subType?: IFieldSubType; displayName?: string; format?: any; + toSpec?: () => FieldSpec; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap new file mode 100644 index 00000000000000..047ac836a87d1f --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -0,0 +1,503 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IndexPattern toSpec should match snapshot 1`] = ` +Object { + "fields": Array [ + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 10, + "esTypes": Array [ + "long", + ], + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, + "lang": undefined, + "name": "bytes", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "number", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 20, + "esTypes": Array [ + "boolean", + ], + "format": undefined, + "lang": undefined, + "name": "ssl", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "boolean", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 30, + "esTypes": Array [ + "date", + ], + "format": undefined, + "lang": undefined, + "name": "@timestamp", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "date", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 30, + "esTypes": Array [ + "date", + ], + "format": undefined, + "lang": undefined, + "name": "time", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "date", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": undefined, + "lang": undefined, + "name": "@tags", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "date", + ], + "format": undefined, + "lang": undefined, + "name": "utc_time", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "date", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "integer", + ], + "format": undefined, + "lang": undefined, + "name": "phpmemory", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "number", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "ip", + ], + "format": undefined, + "lang": undefined, + "name": "ip", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "ip", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "attachment", + ], + "format": undefined, + "lang": undefined, + "name": "request_body", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "attachment", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "geo_point", + ], + "format": undefined, + "lang": undefined, + "name": "point", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "geo_point", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "geo_shape", + ], + "format": undefined, + "lang": undefined, + "name": "area", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "geo_shape", + }, + Object { + "aggregatable": false, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "murmur3", + ], + "format": undefined, + "lang": undefined, + "name": "hashed", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "murmur3", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "geo_point", + ], + "format": undefined, + "lang": undefined, + "name": "geo.coordinates", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "geo_point", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": undefined, + "name": "extension", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": undefined, + "lang": undefined, + "name": "extension.keyword", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": Object { + "multi": Object { + "parent": "extension", + }, + }, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": undefined, + "name": "machine.os", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": undefined, + "lang": undefined, + "name": "machine.os.raw", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": Object { + "multi": Object { + "parent": "machine.os", + }, + }, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": undefined, + "lang": undefined, + "name": "geo.src", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "_id", + ], + "format": undefined, + "lang": undefined, + "name": "_id", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "_type", + ], + "format": undefined, + "lang": undefined, + "name": "_type", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "_source", + ], + "format": undefined, + "lang": undefined, + "name": "_source", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "_source", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": undefined, + "name": "non-filterable", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": false, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": false, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": undefined, + "name": "non-sortable", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": false, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "conflict", + ], + "format": undefined, + "lang": undefined, + "name": "custom_user_field", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "conflict", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": "expression", + "name": "script string", + "readFromDocValues": false, + "script": "'i am a string'", + "scripted": true, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "long", + ], + "format": undefined, + "lang": "expression", + "name": "script number", + "readFromDocValues": false, + "script": "1234", + "scripted": true, + "searchable": true, + "subType": undefined, + "type": "number", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "date", + ], + "format": undefined, + "lang": "painless", + "name": "script date", + "readFromDocValues": false, + "script": "1234", + "scripted": true, + "searchable": true, + "subType": undefined, + "type": "date", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "murmur3", + ], + "format": undefined, + "lang": "expression", + "name": "script murmur3", + "readFromDocValues": false, + "script": "1234", + "scripted": true, + "searchable": true, + "subType": undefined, + "type": "murmur3", + }, + ], + "id": "test-pattern", + "sourceFilters": undefined, + "timeFieldName": "timestamp", + "title": "test-pattern", + "typeMeta": undefined, + "version": 2, +} +`; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index_patterns/index.ts index 5fae08f3bb7755..77527857ed0caa 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index.ts @@ -18,7 +18,6 @@ */ export * from './index_patterns_api_client'; -export * from './types'; export * from './_pattern_cache'; export * from './flatten_hit'; export * from './format_hit'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index cea476781ad3bf..ba8e4f6fb36955 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -30,6 +30,10 @@ import { Field } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; +class MockFieldFormatter {} + +fieldFormatsMock.getType = jest.fn().mockImplementation(() => MockFieldFormatter); + jest.mock('../../field_mapping', () => { const originalModule = jest.requireActual('../../field_mapping'); @@ -303,6 +307,29 @@ describe('IndexPattern', () => { }); }); + describe('toSpec', () => { + test('should match snapshot', () => { + indexPattern.fieldFormatMap.bytes = { + toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }), + }; + expect(indexPattern.toSpec()).toMatchSnapshot(); + }); + + test('can restore from spec', async () => { + indexPattern.fieldFormatMap.bytes = { + toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }), + }; + const spec = indexPattern.toSpec(); + const restoredPattern = await create(spec.id as string); + restoredPattern.initFromSpec(spec); + expect(restoredPattern.id).toEqual(indexPattern.id); + expect(restoredPattern.title).toEqual(indexPattern.title); + expect(restoredPattern.timeFieldName).toEqual(indexPattern.timeFieldName); + expect(restoredPattern.fields.length).toEqual(indexPattern.fields.length); + expect(restoredPattern.fieldFormatMap.bytes instanceof MockFieldFormatter).toEqual(true); + }); + }); + describe('popularizeField', () => { test('should increment the popularity count by default', () => { // const saveSpy = sinon.stub(indexPattern, 'save'); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index cd39a965ae6fce..e9ac5a09b9db3a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -20,6 +20,7 @@ import _, { each, reject } from 'lodash'; import { i18n } from '@kbn/i18n'; import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObjectAttributes } from 'src/core/public'; import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common'; import { @@ -36,11 +37,12 @@ import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; import { IIndexPatternsApiClient } from '.'; -import { TypeMeta } from '.'; import { OnNotification, OnError } from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; import { PatternCache } from './_pattern_cache'; import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; +import { IndexPatternSpec, TypeMeta, FieldSpec, SourceFilter } from '../types'; +import { SerializedFieldFormat } from '../../../../expressions/common'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const type = 'index-pattern'; @@ -60,10 +62,9 @@ export class IndexPattern implements IIndexPattern { public id?: string; public title: string = ''; - public type?: string; public fieldFormatMap: any; public typeMeta?: TypeMeta; - public fields: IIndexPatternFieldList; + public fields: IIndexPatternFieldList & { toSpec: () => FieldSpec[] }; public timeFieldName: string | undefined; public formatHit: any; public formatField: any; @@ -74,7 +75,7 @@ export class IndexPattern implements IIndexPattern { private savedObjectsClient: SavedObjectsClientContract; private patternCache: PatternCache; private getConfig: any; - private sourceFilters?: []; + private sourceFilters?: SourceFilter[]; private originalBody: { [key: string]: any } = {}; public fieldsFetcher: any; // probably want to factor out any direct usage and change to private private shortDotsEnable: boolean = false; @@ -196,6 +197,35 @@ export class IndexPattern implements IIndexPattern { this.initFields(); } + public initFromSpec(spec: IndexPatternSpec) { + // create fieldFormatMap from field list + const fieldFormatMap: Record = {}; + if (_.isArray(spec.fields)) { + spec.fields.forEach((field: FieldSpec) => { + if (field.format) { + fieldFormatMap[field.name as string] = { ...field.format }; + } + }); + } + + this.version = spec.version; + + this.title = spec.title || ''; + this.timeFieldName = spec.timeFieldName; + this.sourceFilters = spec.sourceFilters; + + // ignoring this because the same thing happens elsewhere but via _.assign + // @ts-ignore + this.fields = spec.fields || []; + this.typeMeta = spec.typeMeta; + this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => { + return this.deserializeFieldFormatMap(mapping); + }); + + this.initFields(); + return this; + } + private updateFromElasticSearch(response: any, forceFieldRefresh: boolean = false) { if (!response.found) { throw new SavedObjectNotFound(type, this.id, 'management/kibana/indexPatterns'); @@ -206,15 +236,16 @@ export class IndexPattern implements IIndexPattern { return; } - response._source[name] = fieldMapping._deserialize(response._source[name]); + response[name] = fieldMapping._deserialize(response[name]); }); - // give index pattern all of the values in _source - _.assign(this, response._source); + // give index pattern all of the values + _.assign(this, response); if (!this.title && this.id) { this.title = this.id; } + this.version = response.version; return this.indexFields(forceFieldRefresh); } @@ -266,13 +297,11 @@ export class IndexPattern implements IIndexPattern { } const savedObject = await this.savedObjectsClient.get(type, this.id); - this.version = savedObject._version; const response = { - _id: savedObject.id, - _type: savedObject.type, - _source: _.cloneDeep(savedObject.attributes), + version: savedObject._version, found: savedObject._version ? true : false, + ...(_.cloneDeep(savedObject.attributes) as SavedObjectAttributes), }; // Do this before we attempt to update from ES since that call can potentially perform a save this.originalBody = this.prepBody(); @@ -283,6 +312,19 @@ export class IndexPattern implements IIndexPattern { return this; } + public toSpec(): IndexPatternSpec { + return { + id: this.id, + version: this.version, + + title: this.title, + timeFieldName: this.timeFieldName, + sourceFilters: this.sourceFilters, + fields: this.fields.toSpec(), + typeMeta: this.typeMeta, + }; + } + // Get the source filtering configuration for that index. getSourceFiltering() { return { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 22d1765d793488..5e51897d133727 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -32,12 +32,8 @@ import { createEnsureDefaultIndexPattern, EnsureDefaultIndexPattern, } from './ensure_default_index_pattern'; -import { - getIndexPatternFieldListCreator, - CreateIndexPatternFieldList, - Field, - FieldSpec, -} from '../fields'; +import { getIndexPatternFieldListCreator, CreateIndexPatternFieldList, Field } from '../fields'; +import { IndexPatternSpec, FieldSpec } from '../types'; import { OnNotification, OnError } from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; @@ -195,6 +191,21 @@ export class IndexPatternsService { return indexPatternCache.set(id, indexPattern); }; + specToIndexPattern(spec: IndexPatternSpec) { + const indexPattern = new IndexPattern(spec.id, { + getConfig: (cfg: any) => this.config.get(cfg), + savedObjectsClient: this.savedObjectsClient, + apiClient: this.apiClient, + patternCache: indexPatternCache, + fieldFormats: this.fieldFormats, + onNotification: this.onNotification, + onError: this.onError, + }); + + indexPattern.initFromSpec(spec); + return indexPattern; + } + make = (id?: string): Promise => { const indexPattern = new IndexPattern(id, { getConfig: (cfg: any) => this.config.get(cfg), diff --git a/src/plugins/data/common/index_patterns/index_patterns/types.ts b/src/plugins/data/common/index_patterns/index_patterns/types.ts deleted file mode 100644 index b2060dd1d48bac..00000000000000 --- a/src/plugins/data/common/index_patterns/index_patterns/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export type AggregationRestrictions = Record< - string, - { - agg?: string; - interval?: number; - fixed_interval?: string; - calendar_interval?: string; - delay?: string; - time_zone?: string; - } ->; - -export interface TypeMeta { - aggs?: Record; - [key: string]: any; -} diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 7399bbbc10a7e1..94121a274d686e 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -19,6 +19,8 @@ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications'; import { IFieldType } from './fields'; +import { SerializedFieldFormat } from '../../../expressions/common'; +import { KBN_FIELD_TYPES } from '..'; export interface IIndexPattern { [key: string]: any; @@ -51,3 +53,65 @@ export interface IndexPatternAttributes { export type OnNotification = (toastInputFields: ToastInputFields) => void; export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void; + +export type AggregationRestrictions = Record< + string, + { + agg?: string; + interval?: number; + fixed_interval?: string; + calendar_interval?: string; + delay?: string; + time_zone?: string; + } +>; + +export interface IFieldSubType { + multi?: { parent: string }; + nested?: { path: string }; +} + +export interface TypeMeta { + aggs?: Record; + [key: string]: any; +} + +export type FieldSpecConflictDescriptions = Record; + +// This should become FieldSpec once types are cleaned up +export interface FieldSpecExportFmt { + count?: number; + script?: string; + lang?: string; + conflictDescriptions?: FieldSpecConflictDescriptions; + name: string; + type: KBN_FIELD_TYPES; + esTypes?: string[]; + scripted: boolean; + searchable: boolean; + aggregatable: boolean; + readFromDocValues?: boolean; + subType?: IFieldSubType; + format?: SerializedFieldFormat; + indexed?: boolean; +} + +export interface FieldSpec { + [key: string]: any; + format?: SerializedFieldFormat; +} + +export interface IndexPatternSpec { + id?: string; + version?: string; + + title: string; + timeFieldName?: string; + sourceFilters?: SourceFilter[]; + fields?: FieldSpec[]; + typeMeta?: TypeMeta; +} + +export interface SourceFilter { + value: string; +} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 984ce18aa4d839..3665d9dc2b46e7 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -249,8 +249,6 @@ export { IndexPattern, IIndexPatternFieldList, Field as IndexPatternField, - TypeMeta as IndexPatternTypeMeta, - AggregationRestrictions as IndexPatternAggRestrictions, // TODO: exported only in stub_index_pattern test. Move into data plugin and remove export. getIndexPatternFieldListCreator, } from './index_patterns'; @@ -263,6 +261,8 @@ export { KBN_FIELD_TYPES, IndexPatternAttributes, UI_SETTINGS, + TypeMeta as IndexPatternTypeMeta, + AggregationRestrictions as IndexPatternAggRestrictions, } from '../common'; /* diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 0a8397467807c6..2c540527f468d2 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -34,11 +34,4 @@ export { IIndexPatternFieldList, } from '../../common/index_patterns'; -// TODO: figure out how to replace IndexPatterns in get_inner_angular. -export { - IndexPatternsService, - IndexPatternsContract, - IndexPattern, - TypeMeta, - AggregationRestrictions, -} from './index_patterns'; +export { IndexPatternsService, IndexPatternsContract, IndexPattern } from './index_patterns'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 31dc5b51a06f56..25c9b0718050ac 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -902,6 +902,10 @@ export interface IFieldType { sortable?: boolean; // (undocumented) subType?: IFieldSubType; + // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + toSpec?: () => FieldSpec; // (undocumented) type: string; // (undocumented) @@ -937,8 +941,6 @@ export interface IIndexPattern { // // @public (undocumented) export interface IIndexPatternFieldList extends Array { - // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts - // // (undocumented) add(field: FieldSpec): void; // (undocumented) @@ -993,7 +995,9 @@ export class IndexPattern implements IIndexPattern { // (undocumented) fieldFormatMap: any; // (undocumented) - fields: IIndexPatternFieldList; + fields: IIndexPatternFieldList & { + toSpec: () => FieldSpec[]; + }; // (undocumented) fieldsFetcher: any; // (undocumented) @@ -1036,6 +1040,10 @@ export class IndexPattern implements IIndexPattern { id?: string; // (undocumented) init(forceFieldRefresh?: boolean): Promise; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + initFromSpec(spec: IndexPatternSpec): this; // (undocumented) isTimeBased(): boolean; // (undocumented) @@ -1065,9 +1073,9 @@ export class IndexPattern implements IIndexPattern { // (undocumented) toJSON(): string | undefined; // (undocumented) - toString(): string; + toSpec(): IndexPatternSpec; // (undocumented) - type?: string; + toString(): string; // (undocumented) typeMeta?: IndexPatternTypeMeta; } @@ -1106,12 +1114,15 @@ export interface IndexPatternAttributes { export class IndexPatternField implements IFieldType { // (undocumented) $$spec: FieldSpec; + // Warning: (ae-forgotten-export) The symbol "FieldSpecExportFmt" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FieldDependencies" needs to be exported by the entry point index.d.ts - constructor(indexPattern: IIndexPattern, spec: FieldSpec | IndexPatternField, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); + constructor(indexPattern: IIndexPattern, spec: FieldSpecExportFmt | FieldSpec | IndexPatternField, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); // (undocumented) aggregatable?: boolean; + // Warning: (ae-forgotten-export) The symbol "FieldSpecConflictDescriptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - conflictDescriptions?: Record; + conflictDescriptions?: FieldSpecConflictDescriptions; // (undocumented) count?: number; // (undocumented) @@ -1141,6 +1152,8 @@ export class IndexPatternField implements IFieldType { // (undocumented) subType?: IFieldSubType; // (undocumented) + toSpec: () => FieldSpecExportFmt; + // (undocumented) type: string; // (undocumented) visualizable?: boolean; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 2ab0644f7237b7..136d960b52c347 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -392,6 +392,10 @@ export interface IFieldType { sortable?: boolean; // (undocumented) subType?: IFieldSubType; + // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + toSpec?: () => FieldSpec; // (undocumented) type: string; // (undocumented) diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 8c527475b7480f..099ec2e5b1ffcb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -28,6 +28,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DiscoverField } from './discover_field'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternField } from '../../../../../data/public'; +import { FieldSpecExportFmt } from '../../../../../data/common'; jest.mock('../../../kibana_services', () => ({ getServices: () => ({ @@ -74,6 +75,7 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals format: null, routes: {}, $$spec: {}, + toSpec: () => (({} as unknown) as FieldSpecExportFmt), } as IndexPatternField; const props = { diff --git a/src/plugins/testbed/kibana.json b/src/plugins/testbed/kibana.json deleted file mode 100644 index 9afe357b7a0104..00000000000000 --- a/src/plugins/testbed/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "testbed", - "version": "0.0.1", - "kibanaVersion": "kibana", - "configPath": ["core", "testbed"], - "server": true, - "ui": true -} diff --git a/src/plugins/testbed/public/index.ts b/src/plugins/testbed/public/index.ts deleted file mode 100644 index 601db10f6f8bb0..00000000000000 --- a/src/plugins/testbed/public/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; -import { TestbedPlugin, TestbedPluginSetup, TestbedPluginStart } from './plugin'; - -export const plugin: PluginInitializer = ( - initializerContext: PluginInitializerContext -) => new TestbedPlugin(initializerContext); diff --git a/src/plugins/testbed/public/plugin.ts b/src/plugins/testbed/public/plugin.ts deleted file mode 100644 index 8c70485d9ee8b3..00000000000000 --- a/src/plugins/testbed/public/plugin.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/public'; - -interface ConfigType { - uiProp: string; -} - -export class TestbedPlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - - public async setup(core: CoreSetup, deps: {}) { - const config = this.initializerContext.config.get(); - - // eslint-disable-next-line no-console - console.log(`Testbed plugin set up. uiProp: '${config.uiProp}'`); - return { - foo: 'bar', - }; - } - - public start() { - // eslint-disable-next-line no-console - console.log(`Testbed plugin started`); - } - - public stop() {} -} - -export type TestbedPluginSetup = ReturnType; -export type TestbedPluginStart = ReturnType; diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts deleted file mode 100644 index 21f97259c97f44..00000000000000 --- a/src/plugins/testbed/server/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { map } from 'rxjs/operators'; -import { schema, TypeOf } from '@kbn/config-schema'; - -import { - CoreSetup, - CoreStart, - Logger, - PluginInitializerContext, - PluginConfigDescriptor, - PluginName, -} from 'kibana/server'; - -const configSchema = schema.object({ - secret: schema.string({ defaultValue: 'Not really a secret :/' }), - uiProp: schema.string({ defaultValue: 'Accessible from client' }), -}); - -type ConfigType = TypeOf; - -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - uiProp: true, - }, - schema: configSchema, - deprecations: ({ rename, unused, renameFromRoot }) => [ - rename('securityKey', 'secret'), - renameFromRoot('oldtestbed.uiProp', 'testbed.uiProp'), - unused('deprecatedProperty'), - ], -}; - -class Plugin { - private readonly log: Logger; - - constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get(); - } - - public setup(core: CoreSetup, deps: Record) { - this.log.debug( - `Setting up TestBed with core contract [${Object.keys(core)}] and deps [${Object.keys(deps)}]` - ); - - const router = core.http.createRouter(); - router.get( - { path: '/requestcontext/elasticsearch', validate: false }, - async (context, req, res) => { - const response = await context.core.elasticsearch.legacy.client.callAsInternalUser('ping'); - return res.ok({ body: `Elasticsearch: ${response}` }); - } - ); - - router.get( - { path: '/requestcontext/savedobjectsclient', validate: false }, - async (context, req, res) => { - const response = await context.core.savedObjects.client.find({ type: 'TYPE' }); - return res.ok({ body: `SavedObjects client: ${JSON.stringify(response)}` }); - } - ); - - return { - data$: this.initializerContext.config.create().pipe( - map((configValue) => { - this.log.debug(`I've got value from my config: ${configValue.secret}`); - return `Some exposed data derived from config: ${configValue.secret}`; - }) - ), - pingElasticsearch: async () => { - const [coreStart] = await core.getStartServices(); - return coreStart.elasticsearch.legacy.client.callAsInternalUser('ping'); - }, - }; - } - - public start(core: CoreStart, deps: Record) { - this.log.debug( - `Starting up TestBed testbed with core contract [${Object.keys( - core - )}] and deps [${Object.keys(deps)}]` - ); - - return { - getStartContext() { - return core; - }, - }; - } - - public stop() { - this.log.debug(`Stopping TestBed`); - } -} - -export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); diff --git a/test/api_integration/apis/core/index.js b/test/api_integration/apis/core/index.js index c522acaea25a31..ab9bb8d33c2dc3 100644 --- a/test/api_integration/apis/core/index.js +++ b/test/api_integration/apis/core/index.js @@ -22,19 +22,6 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('core', () => { - describe('request context', () => { - it('provides access to elasticsearch', async () => - await supertest.get('/requestcontext/elasticsearch').expect(200, 'Elasticsearch: true')); - - it('provides access to SavedObjects client', async () => - await supertest - .get('/requestcontext/savedobjectsclient') - .expect( - 200, - 'SavedObjects client: {"page":1,"per_page":20,"total":0,"saved_objects":[]}' - )); - }); - describe('compression', () => { it(`uses compression when there isn't a referer`, async () => { await supertest diff --git a/test/functional/apps/dashboard/dashboard_state.js b/test/functional/apps/dashboard/dashboard_state.js index 5bba2447cde286..3656c824394f46 100644 --- a/test/functional/apps/dashboard/dashboard_state.js +++ b/test/functional/apps/dashboard/dashboard_state.js @@ -251,8 +251,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // Unskip once https://github.com/elastic/kibana/issues/15736 is fixed. - it.skip('and updates the pie slice legend color', async function () { + it('and updates the pie slice legend color', async function () { await retry.try(async () => { const colorExists = await PageObjects.visChart.doesSelectedLegendColorExist('#FFFFFF'); expect(colorExists).to.be(true); @@ -272,8 +271,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // Unskip once https://github.com/elastic/kibana/issues/15736 is fixed. - it.skip('resets the legend color as well', async function () { + it('resets the legend color as well', async function () { await retry.try(async () => { const colorExists = await PageObjects.visChart.doesSelectedLegendColorExist('#57c17b'); expect(colorExists).to.be(true); diff --git a/test/functional/apps/dashboard/empty_dashboard.js b/test/functional/apps/dashboard/empty_dashboard.js index e7ebbcf09e828b..7f13aca438842a 100644 --- a/test/functional/apps/dashboard/empty_dashboard.js +++ b/test/functional/apps/dashboard/empty_dashboard.js @@ -49,10 +49,11 @@ export default function ({ getService, getPageObjects }) { expect(emptyWidgetExists).to.be(true); }); - it.skip('should open add panel when add button is clicked', async () => { + it('should open add panel when add button is clicked', async () => { await testSubjects.click('dashboardAddPanelButton'); const isAddPanelOpen = await dashboardAddPanel.isAddPanelOpen(); expect(isAddPanelOpen).to.be(true); + await testSubjects.click('euiFlyoutCloseButton'); }); it('should add new visualization from dashboard', async () => { diff --git a/test/functional/apps/discover/_errors.js b/test/functional/apps/discover/_errors.js index 5113fc8568d526..f3936d06bb6dfc 100644 --- a/test/functional/apps/discover/_errors.js +++ b/test/functional/apps/discover/_errors.js @@ -35,7 +35,7 @@ export default function ({ getService, getPageObjects }) { await esArchiver.unload('invalid_scripted_field'); }); - // https://github.com/elastic/kibana/issues/61366 + // ES issue https://github.com/elastic/elasticsearch/issues/54235 describe.skip('invalid scripted field error', () => { it('is rendered', async () => { const isFetchErrorVisible = await testSubjects.exists('discoverFetchError'); diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.js b/test/functional/apps/visualize/_data_table_nontimeindex.js index 4ae66d14ec30da..d64629a65c2c30 100644 --- a/test/functional/apps/visualize/_data_table_nontimeindex.js +++ b/test/functional/apps/visualize/_data_table_nontimeindex.js @@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }) { const renderable = getService('renderable'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'header', 'visChart']); - describe.skip('data table with index without time filter', function indexPatternCreation() { + describe('data table with index without time filter', function indexPatternCreation() { const vizName1 = 'Visualization DataTable without time filter'; before(async function () { @@ -112,65 +112,49 @@ export default function ({ getService, getPageObjects }) { expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); }); - it('should show correct data for a data table with date histogram', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Daily'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); + // bug https://github.com/elastic/kibana/issues/68977 + describe.skip('data table with date histogram', async () => { + before(async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickDataTable(); + await PageObjects.visualize.clickNewSearch( + PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED + ); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.setInterval('Daily'); + await PageObjects.visEditor.clickGo(); + }); - it('should show correct data for a data table with date histogram', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Daily'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); + it('should show correct data', async () => { + const data = await PageObjects.visChart.getTableVisData(); + log.debug(data.split('\n')); + expect(data.trim().split('\n')).to.be.eql([ + '2015-09-20', + '4,757', + '2015-09-21', + '4,614', + '2015-09-22', + '4,633', + ]); + }); - it('should correctly filter for applied time filter on the main timefield', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); + it('should correctly filter for applied time filter on the main timefield', async () => { + await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); + const data = await PageObjects.visChart.getTableVisData(); + expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); + }); - it('should correctly filter for pinned filters', async () => { - await filterBar.toggleFilterPinned('@timestamp'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); + it('should correctly filter for pinned filters', async () => { + await filterBar.toggleFilterPinned('@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); + const data = await PageObjects.visChart.getTableVisData(); + expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); + }); }); }); } diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index f1c5c916a89bf0..7e22f543bc7dbf 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -28,8 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); - // FLAKY: https://github.com/elastic/kibana/issues/43150 - describe.skip('visual builder', function describeIndexTests() { + describe('visual builder', function describeIndexTests() { this.tags('includeFirefox'); beforeEach(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); @@ -74,7 +73,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/46677 describe('gauge', () => { beforeEach(async () => { await PageObjects.visualBuilder.resetPage(); @@ -107,7 +105,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('switch index patterns', () => { + // FLAKY: https://github.com/elastic/kibana/issues/43150 + describe.skip('switch index patterns', () => { beforeEach(async () => { log.debug('Load kibana_sample_data_flights data'); await esArchiver.loadIfNeeded('kibana_sample_data_flights'); diff --git a/x-pack/package.json b/x-pack/package.json index ad8c12d41000c5..ac5b77c4f78dbb 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -198,7 +198,7 @@ "@elastic/eui": "24.1.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.3.0", - "@elastic/node-crypto": "1.1.1", + "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index cb6003c58e90d1..cdebb3aac129b6 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -12,11 +12,19 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { PROJECTION } from '../../../../common/projections/typings'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; + +type TracesAPIResponse = APIReturnType<'/api/apm/traces'>; +const DEFAULT_RESPONSE: TracesAPIResponse = { + items: [], + isAggregationAccurate: true, + bucketSize: 0, +}; export function TraceOverview() { const { urlParams, uiFilters } = useUrlParams(); const { start, end } = urlParams; - const { status, data = [] } = useFetcher( + const { status, data = DEFAULT_RESPONSE } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -56,7 +64,7 @@ export function TraceOverview() { diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index fc5347d081316a..a1e01b61d5c1b5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -11,16 +11,21 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, + EuiCallOut, + EuiCode, } from '@elastic/eui'; import { Location } from 'history'; +import { FormattedMessage } from '@kbn/i18n/react'; import { first } from 'lodash'; import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { useTransactionList } from '../../../hooks/useTransactionList'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; import { TransactionCharts } from '../../shared/charts/TransactionCharts'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { TransactionList } from './List'; +import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { useRedirect } from './useRedirect'; import { history } from '../../../utils/history'; import { useLocation } from '../../../hooks/useLocation'; @@ -140,9 +145,48 @@ export function TransactionOverview() {

Transactions

+ {!transactionListData.isAggregationAccurate && ( + +

+ + xpack.apm.ui.transactionGroupBucketSize + + ), + }} + /> + + + {i18n.translate( + 'xpack.apm.transactionCardinalityWarning.docsLink', + { defaultMessage: 'Learn more in the docs' } + )} + +

+
+ )} +
diff --git a/x-pack/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts index 202437ae722572..ed6bb9309a557c 100644 --- a/x-pack/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts @@ -8,8 +8,7 @@ import { useMemo } from 'react'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useUiFilters } from '../context/UrlParamsContext'; import { useFetcher } from './useFetcher'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroupListAPIResponse } from '../../server/lib/transaction_groups'; +import { APIReturnType } from '../services/rest/createCallApmApi'; const getRelativeImpact = ( impact: number, @@ -21,7 +20,11 @@ const getRelativeImpact = ( 1 ); -function getWithRelativeImpact(items: TransactionGroupListAPIResponse) { +type TransactionsAPIResponse = APIReturnType< + '/api/apm/services/{serviceName}/transaction_groups' +>; + +function getWithRelativeImpact(items: TransactionsAPIResponse['items']) { const impacts = items .map(({ impact }) => impact) .filter((impact) => impact !== null) as number[]; @@ -40,10 +43,16 @@ function getWithRelativeImpact(items: TransactionGroupListAPIResponse) { }); } +const DEFAULT_RESPONSE: TransactionsAPIResponse = { + items: [], + isAggregationAccurate: true, + bucketSize: 0, +}; + export function useTransactionList(urlParams: IUrlParams) { const { serviceName, transactionType, start, end } = urlParams; const uiFilters = useUiFilters(urlParams); - const { data = [], error, status } = useFetcher( + const { data = DEFAULT_RESPONSE, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && transactionType) { return callApmApi({ @@ -63,7 +72,14 @@ export function useTransactionList(urlParams: IUrlParams) { [serviceName, start, end, transactionType, uiFilters] ); - const memoizedData = useMemo(() => getWithRelativeImpact(data), [data]); + const memoizedData = useMemo( + () => ({ + items: getWithRelativeImpact(data.items), + isAggregationAccurate: data.isAggregationAccurate, + bucketSize: data.bucketSize, + }), + [data] + ); return { data: memoizedData, status, diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index 44768c94f3b1df..8babc72ef129ce 100644 --- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -8,7 +8,7 @@ import { callApi, FetchOptions } from './callApi'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMAPI } from '../../../server/routes/create_apm_api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Client } from '../../../server/routes/typings'; +import { Client, HttpMethod } from '../../../server/routes/typings'; export type APMClient = Client; export type APMClientOptions = Omit & { @@ -43,3 +43,11 @@ export function createCallApmApi(http: HttpSetup) { }); }) as APMClient; } + +// infer return type from API +export type APIReturnType< + TPath extends keyof APMAPI['_S'], + TMethod extends HttpMethod = 'GET' +> = APMAPI['_S'][TPath] extends { [key in TMethod]: { ret: any } } + ? APMAPI['_S'][TPath][TMethod]['ret'] + : unknown; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 2409da59d66ae2..e77307a3f9db13 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller } from 'kibana/server'; +import { APICaller, Logger } from 'kibana/server'; import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ESSearchResponse } from '../../../../typings/elasticsearch'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; @@ -19,12 +19,14 @@ export async function getStoredAnnotations({ environment, apiCaller, annotationsClient, + logger, }: { setup: Setup & SetupTimeRange; serviceName: string; environment?: string; apiCaller: APICaller; annotationsClient: ScopedAnnotationsClient; + logger: Logger; }): Promise { try { const environmentFilter = getEnvironmentUiFilterES(environment); @@ -71,6 +73,14 @@ export async function getStoredAnnotations({ if (error.body?.error?.type === 'index_not_found_exception') { return []; } + + if (error.body?.error?.type === 'security_exception') { + logger.warn( + `Unable to get stored annotations due to a security exception. Please make sure that the user has 'indices:data/read/search' permissions for ${annotationsClient.index}` + ); + return []; + } + throw error; } } diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.ts index 9365213a87f6ef..e2b6e74d4d65a5 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { APICaller } from 'kibana/server'; +import { APICaller, Logger } from 'kibana/server'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { getDerivedServiceAnnotations } from './get_derived_service_annotations'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -15,12 +15,14 @@ export async function getServiceAnnotations({ environment, annotationsClient, apiCaller, + logger, }: { serviceName: string; environment?: string; setup: Setup & SetupTimeRange; annotationsClient?: ScopedAnnotationsClient; apiCaller: APICaller; + logger: Logger; }) { // start fetching derived annotations (based on transactions), but don't wait on it // it will likely be significantly slower than the stored annotations @@ -37,6 +39,7 @@ export async function getServiceAnnotations({ environment, annotationsClient, apiCaller, + logger, }) : []; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap index 64f06ad0a81cd0..087dc6afc9a587 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap @@ -46,7 +46,7 @@ Array [ }, }, "composite": Object { - "size": 10000, + "size": 101, "sources": Array [ Object { "service": Object { @@ -159,7 +159,7 @@ Array [ }, }, "composite": Object { - "size": 10000, + "size": 101, "sources": Array [ Object { "transaction": Object { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index b93f842b878cb0..496533cf97e65d 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -44,7 +44,7 @@ Object { }, }, "composite": Object { - "size": 10000, + "size": 101, "sources": Array [ Object { "service": Object { @@ -153,7 +153,7 @@ Object { }, }, "composite": Object { - "size": 10000, + "size": 101, "sources": Array [ Object { "transaction": Object { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts index 00702be6744ec8..a26c3d85a3fc47 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.test.ts @@ -39,7 +39,8 @@ describe('transactionGroupsFetcher', () => { describe('type: top_traces', () => { it('should call client.search with correct query', async () => { const setup = getSetup(); - await transactionGroupsFetcher({ type: 'top_traces' }, setup); + const bucketSize = 100; + await transactionGroupsFetcher({ type: 'top_traces' }, setup, bucketSize); expect(setup.client.search.mock.calls).toMatchSnapshot(); }); }); @@ -47,13 +48,15 @@ describe('transactionGroupsFetcher', () => { describe('type: top_transactions', () => { it('should call client.search with correct query', async () => { const setup = getSetup(); + const bucketSize = 100; await transactionGroupsFetcher( { type: 'top_transactions', serviceName: 'opbeans-node', transactionType: 'request', }, - setup + setup, + bucketSize ); expect(setup.client.search.mock.calls).toMatchSnapshot(); }); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index d10c45ecbdbfb1..595ee9d8da2dcf 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -36,9 +36,10 @@ interface TopTraceOptions { export type Options = TopTransactionOptions | TopTraceOptions; export type ESResponse = PromiseReturnType; -export function transactionGroupsFetcher( +export async function transactionGroupsFetcher( options: Options, - setup: Setup & SetupTimeRange & SetupUIFilters + setup: Setup & SetupTimeRange & SetupUIFilters, + bucketSize: number ) { const { client } = setup; @@ -71,7 +72,7 @@ export function transactionGroupsFetcher( aggs: { transaction_groups: { composite: { - size: 10000, + size: bucketSize + 1, // 1 extra bucket is added to check whether the total number of buckets exceed the specified bucket size. sources: [ ...(isTopTraces ? [{ service: { terms: { field: SERVICE_NAME } } }] diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/index.ts b/x-pack/plugins/apm/server/lib/transaction_groups/index.ts index 30c4975120483e..893e586b351a80 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/index.ts @@ -11,20 +11,18 @@ import { } from '../helpers/setup_request'; import { transactionGroupsFetcher, Options } from './fetcher'; import { transactionGroupsTransformer } from './transform'; -import { PromiseReturnType } from '../../../../observability/typings/common'; -export type TransactionGroupListAPIResponse = PromiseReturnType< - typeof getTransactionGroupList ->; export async function getTransactionGroupList( options: Options, setup: Setup & SetupTimeRange & SetupUIFilters ) { const { start, end } = setup; - const response = await transactionGroupsFetcher(options, setup); + const bucketSize = setup.config['xpack.apm.ui.transactionGroupBucketSize']; + const response = await transactionGroupsFetcher(options, setup, bucketSize); return transactionGroupsTransformer({ response, start, end, + bucketSize, }); } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts index 58d770bebce979..2c5aa79bb3483c 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -18,6 +18,7 @@ describe('transaction group queries', () => { }); it('fetches top transactions', async () => { + const bucketSize = 100; mock = await inspectSearchParams((setup) => transactionGroupsFetcher( { @@ -25,7 +26,8 @@ describe('transaction group queries', () => { serviceName: 'foo', transactionType: 'bar', }, - setup + setup, + bucketSize ) ); @@ -33,12 +35,14 @@ describe('transaction group queries', () => { }); it('fetches top traces', async () => { + const bucketSize = 100; mock = await inspectSearchParams((setup) => transactionGroupsFetcher( { type: 'top_traces', }, - setup + setup, + bucketSize ) ); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts index e5ec9a8eae782b..0bb29e27f0219e 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/transform.test.ts @@ -10,13 +10,20 @@ import { transactionGroupsTransformer } from './transform'; describe('transactionGroupsTransformer', () => { it('should match snapshot', () => { - expect( - transactionGroupsTransformer({ - response: transactionGroupsResponse, - start: 100, - end: 2000, - }) - ).toMatchSnapshot(); + const { + bucketSize, + isAggregationAccurate, + items, + } = transactionGroupsTransformer({ + response: transactionGroupsResponse, + start: 100, + end: 2000, + bucketSize: 100, + }); + + expect(bucketSize).toBe(100); + expect(isAggregationAccurate).toBe(true); + expect(items).toMatchSnapshot(); }); it('should transform response correctly', () => { @@ -43,17 +50,59 @@ describe('transactionGroupsTransformer', () => { } as unknown) as ESResponse; expect( - transactionGroupsTransformer({ response, start: 100, end: 20000 }) - ).toEqual([ - { - averageResponseTime: 255966.30555555556, - impact: 0, - name: 'POST /api/orders', - p95: 320238.5, - sample: 'sample source', - transactionsPerMinute: 542.713567839196, + transactionGroupsTransformer({ + response, + start: 100, + end: 20000, + bucketSize: 100, + }) + ).toEqual({ + bucketSize: 100, + isAggregationAccurate: true, + items: [ + { + averageResponseTime: 255966.30555555556, + impact: 0, + name: 'POST /api/orders', + p95: 320238.5, + sample: 'sample source', + transactionsPerMinute: 542.713567839196, + }, + ], + }); + }); + + it('`isAggregationAccurate` should be false if number of bucket is higher than `bucketSize`', () => { + const bucket = { + key: { transaction: 'POST /api/orders' }, + doc_count: 180, + avg: { value: 255966.30555555556 }, + p95: { values: { '95.0': 320238.5 } }, + sum: { value: 3000000000 }, + sample: { + hits: { + total: 180, + hits: [{ _source: 'sample source' }], + }, }, - ]); + }; + + const response = ({ + aggregations: { + transaction_groups: { + buckets: [bucket, bucket, bucket, bucket], // four buckets returned + }, + }, + } as unknown) as ESResponse; + + const { isAggregationAccurate } = transactionGroupsTransformer({ + response, + start: 100, + end: 20000, + bucketSize: 3, // bucket size of three + }); + + expect(isAggregationAccurate).toEqual(false); }); it('should calculate impact from sum', () => { @@ -74,10 +123,13 @@ describe('transactionGroupsTransformer', () => { }, } as unknown) as ESResponse; - expect( - transactionGroupsTransformer({ response, start: 100, end: 20000 }).map( - (bucket) => bucket.impact - ) - ).toEqual([100, 25, 0]); + const { items } = transactionGroupsTransformer({ + response, + start: 100, + end: 20000, + bucketSize: 100, + }); + + expect(items.map((bucket) => bucket.impact)).toEqual([100, 25, 0]); }); }); diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts index 2f34d365e5be9a..81dba39e9d7126 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/transform.ts @@ -8,15 +8,15 @@ import moment from 'moment'; import { sortByOrder } from 'lodash'; import { ESResponse } from './fetcher'; -function calculateRelativeImpacts(transactionGroups: ITransactionGroup[]) { - const values = transactionGroups +function calculateRelativeImpacts(items: ITransactionGroup[]) { + const values = items .map(({ impact }) => impact) .filter((value) => value !== null) as number[]; const max = Math.max(...values); const min = Math.min(...values); - return transactionGroups.map((bucket) => ({ + return items.map((bucket) => ({ ...bucket, impact: bucket.impact !== null @@ -60,17 +60,30 @@ export function transactionGroupsTransformer({ response, start, end, + bucketSize, }: { response: ESResponse; start: number; end: number; -}): ITransactionGroup[] { + bucketSize: number; +}): { + items: ITransactionGroup[]; + isAggregationAccurate: boolean; + bucketSize: number; +} { const buckets = getBuckets(response); const duration = moment.duration(end - start); const minutes = duration.asMinutes(); - const transactionGroups = buckets.map((bucket) => - getTransactionGroup(bucket, minutes) - ); + const items = buckets.map((bucket) => getTransactionGroup(bucket, minutes)); - return calculateRelativeImpacts(transactionGroups); + const itemsWithRelativeImpact = calculateRelativeImpacts(items); + + return { + items: itemsWithRelativeImpact, + + // The aggregation is considered accurate if the configured bucket size is larger or equal to the number of buckets returned + // the actual number of buckets retrieved are `bucketsize + 1` to detect whether it's above the limit + isAggregationAccurate: bucketSize >= buckets.length, + bucketSize, + }; } diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 8672c6c108c4cf..08eba00251e264 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -105,6 +105,7 @@ export const serviceAnnotationsRoute = createRoute(() => ({ environment, annotationsClient, apiCaller: context.core.elasticsearch.legacy.client.callAsCurrentUser, + logger: context.logger, }); }, })); diff --git a/x-pack/plugins/encrypted_saved_objects/README.md b/x-pack/plugins/encrypted_saved_objects/README.md index 2f0af9e8667971..0a5e79a96f02a7 100644 --- a/x-pack/plugins/encrypted_saved_objects/README.md +++ b/x-pack/plugins/encrypted_saved_objects/README.md @@ -99,6 +99,138 @@ const savedObjectWithDecryptedContent = await esoClient.getDecryptedAsInternalU one would pass to `SavedObjectsClient.get`. These argument allows to specify `namespace` property that, for example, is required if Saved Object was created within a non-default space. +### Defining migrations +EncryptedSavedObjects rely on standard SavedObject migrations, but due to the additional complexity introduced by the need to decrypt and reencrypt the migrated document, there are some caveats to how we support this. +The good news is, most of this complexity is abstracted away by the plugin and all you need to do is leverage our api. + +The `EncryptedSavedObjects` Plugin _SetupContract_ exposes an `createMigration` api which facilitates defining a migration for your EncryptedSavedObject type. + +The `createMigration` function takes four arguments: + +|Argument|Description|Type| +|---|---|---| +|isMigrationNeededPredicate|A predicate which is called for each document, prior to being decrypted, which confirms whether a document requires migration or not. This predicate is important as the decryption step is costly and we would rather not decrypt and re-encrypt a document if we can avoid it.|function| +|migration|A migration function which will migrate each decrypted document from the old shape to the new one.|function| +|inputType|Optional. An `EncryptedSavedObjectTypeRegistration` which describes the ESOType of the input (the document prior to migration). If this type isn't provided, we'll assume the input doc follows the registered type. |object| +|migratedType| Optional. An `EncryptedSavedObjectTypeRegistration` which describes the ESOType of the output (the document after migration). If this type isn't provided, we'll assume the migrated doc follows the registered type.|object| + +### Example: Migrating a Value + +```typescript +encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), +}); + +const migration790 = encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return doc.consumer === 'alerting' || doc.consumer === undefined; + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + const { + attributes: { consumer }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + consumer: consumer === 'alerting' || !consumer ? 'alerts' : consumer, + }, + }; + } +); +``` + +In the above example you can see thwe following: +1. In `shouldBeMigrated` we limit the migrated alerts to those whose `consumer` field equals `alerting` or is undefined. +2. In the migration function we then migrate the value of `consumer` to the value we want (`alerts` or `unknown`, depending on the current value). In this function we can assume that only documents with a `consumer` of `alerting` or `undefined` will be passed in, but it's still safest not to, and so we use the current `consumer` as the default when needed. +3. Note that we haven't passed in any type definitions. This is because we can rely on the registered type, as the migration is changing a value and not the shape of the object. + +As we said above, an EncryptedSavedObject migration is a normal SavedObjects migration, and so we can plug it into the underlying SavedObject just like any other kind of migration: + +```typescript +savedObjects.registerType({ + name: 'alert', + hidden: true, + namespaceType: 'single', + migrations: { + // apply this migration in 7.9.0 + '7.9.0': migration790, + }, + mappings: { + //... + }, +}); +``` + +### Example: Migating a Type +If your migration needs to change the type by, for example, removing an encrypted field, you will have to specify the legacy type for the input. + +```typescript +encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), +}); + +const migration790 = encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return doc.consumer === 'alerting' || doc.consumer === undefined; + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + const { + attributes: { legacyEncryptedField, ...attributes }, + } = doc; + return { + ...doc, + attributes: { + ...attributes + }, + }; + }, + { + type: 'alert', + attributesToEncrypt: new Set(['apiKey', 'legacyEncryptedField']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), + } +); +``` + +As you can see in this example we provide a legacy type which describes the _input_ which needs to be decrypted. +The migration function will default to using the registered type to encrypt the migrated document after the migration is applied. + +If you need to migrate between two legacy types, you can specify both types at once: + +```typescript +encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), +}); + +const migration780 = encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + // ... + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + // ... + }, + // legacy input type + { + type: 'alert', + attributesToEncrypt: new Set(['apiKey', 'legacyEncryptedField']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), + }, + // legacy migration type + { + type: 'alert', + attributesToEncrypt: new Set(['apiKey', 'legacyEncryptedField']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy', 'legacyEncryptedField']), + } +); +``` + ## Testing ### Unit tests diff --git a/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts b/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts new file mode 100644 index 00000000000000..620e0016775949 --- /dev/null +++ b/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts @@ -0,0 +1,296 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { migrationMocks } from 'src/core/server/mocks'; +import { encryptedSavedObjectsServiceMock } from './crypto/index.mock'; +import { getCreateMigration } from './create_migration'; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('createMigration()', () => { + const { log } = migrationMocks.createContext(); + const inputType = { type: 'known-type-1', attributesToEncrypt: new Set(['firstAttr']) }; + const migrationType = { + type: 'known-type-1', + attributesToEncrypt: new Set(['firstAttr', 'secondAttr']), + }; + + interface InputType { + firstAttr: string; + nonEncryptedAttr?: string; + } + interface MigrationType { + firstAttr: string; + encryptedAttr?: string; + } + + const encryptionSavedObjectService = encryptedSavedObjectsServiceMock.create(); + + it('throws if the types arent compatible', async () => { + const migrationCreator = getCreateMigration(encryptionSavedObjectService, () => + encryptedSavedObjectsServiceMock.create() + ); + expect(() => + migrationCreator( + function (doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + (doc) => doc, + { + type: 'known-type-1', + attributesToEncrypt: new Set(), + }, + { + type: 'known-type-2', + attributesToEncrypt: new Set(), + } + ) + ).toThrowErrorMatchingInlineSnapshot( + `"An Invalid Encrypted Saved Objects migration is trying to migrate across types (\\"known-type-1\\" => \\"known-type-2\\"), which isn't permitted"` + ); + }); + + describe('migration of an existing type', () => { + it('uses the type in the current service for both input and migration types when none are specified', async () => { + const instantiateServiceWithLegacyType = jest.fn(() => + encryptedSavedObjectsServiceMock.create() + ); + + const migrationCreator = getCreateMigration( + encryptionSavedObjectService, + instantiateServiceWithLegacyType + ); + const noopMigration = migrationCreator( + function (doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + (doc) => doc + ); + + const attributes = { + firstAttr: 'first_attr', + }; + + encryptionSavedObjectService.decryptAttributesSync.mockReturnValueOnce(attributes); + encryptionSavedObjectService.encryptAttributesSync.mockReturnValueOnce(attributes); + + noopMigration( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes, + }, + { log } + ); + + expect(encryptionSavedObjectService.decryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + attributes + ); + + expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + attributes + ); + }); + }); + + describe('migration of a single legacy type', () => { + it('uses the input type as the mirgation type when omitted', async () => { + const serviceWithLegacyType = encryptedSavedObjectsServiceMock.create(); + const instantiateServiceWithLegacyType = jest.fn(() => serviceWithLegacyType); + + const migrationCreator = getCreateMigration( + encryptionSavedObjectService, + instantiateServiceWithLegacyType + ); + const noopMigration = migrationCreator( + function (doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + (doc) => doc, + inputType + ); + + const attributes = { + firstAttr: 'first_attr', + }; + + serviceWithLegacyType.decryptAttributesSync.mockReturnValueOnce(attributes); + encryptionSavedObjectService.encryptAttributesSync.mockReturnValueOnce(attributes); + + noopMigration( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes, + }, + { log } + ); + + expect(serviceWithLegacyType.decryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + attributes + ); + + expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + attributes + ); + }); + }); + + describe('migration across two legacy types', () => { + const serviceWithInputLegacyType = encryptedSavedObjectsServiceMock.create(); + const serviceWithMigrationLegacyType = encryptedSavedObjectsServiceMock.create(); + const instantiateServiceWithLegacyType = jest.fn(); + + function createMigration() { + instantiateServiceWithLegacyType + .mockImplementationOnce(() => serviceWithInputLegacyType) + .mockImplementationOnce(() => serviceWithMigrationLegacyType); + + const migrationCreator = getCreateMigration( + encryptionSavedObjectService, + instantiateServiceWithLegacyType + ); + return migrationCreator( + function (doc): doc is SavedObjectUnsanitizedDoc { + // migrate doc that have the second field + return ( + typeof (doc as SavedObjectUnsanitizedDoc).attributes.nonEncryptedAttr === + 'string' + ); + }, + ({ attributes: { firstAttr, nonEncryptedAttr }, ...doc }) => ({ + attributes: { + // modify an encrypted field + firstAttr: `~~${firstAttr}~~`, + // encrypt a non encrypted field if it's there + ...(nonEncryptedAttr ? { encryptedAttr: `${nonEncryptedAttr}` } : {}), + }, + ...doc, + }), + inputType, + migrationType + ); + } + + it('doesnt decrypt saved objects that dont need to be migrated', async () => { + const migration = createMigration(); + expect(instantiateServiceWithLegacyType).toHaveBeenCalledWith(inputType); + expect(instantiateServiceWithLegacyType).toHaveBeenCalledWith(migrationType); + + expect( + migration( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes: { + firstAttr: '#####', + }, + }, + { log } + ) + ).toMatchObject({ + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes: { + firstAttr: '#####', + }, + }); + + expect(serviceWithInputLegacyType.decryptAttributesSync).not.toHaveBeenCalled(); + expect(serviceWithMigrationLegacyType.encryptAttributesSync).not.toHaveBeenCalled(); + }); + + it('decrypt, migrates and reencrypts saved objects that need to be migrated', async () => { + const migration = createMigration(); + expect(instantiateServiceWithLegacyType).toHaveBeenCalledWith(inputType); + expect(instantiateServiceWithLegacyType).toHaveBeenCalledWith(migrationType); + + serviceWithInputLegacyType.decryptAttributesSync.mockReturnValueOnce({ + firstAttr: 'first_attr', + nonEncryptedAttr: 'non encrypted', + }); + + serviceWithMigrationLegacyType.encryptAttributesSync.mockReturnValueOnce({ + firstAttr: `#####`, + encryptedAttr: `#####`, + }); + + expect( + migration( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes: { + firstAttr: '#####', + nonEncryptedAttr: 'non encrypted', + }, + }, + { log } + ) + ).toMatchObject({ + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes: { + firstAttr: '#####', + encryptedAttr: `#####`, + }, + }); + + expect(serviceWithInputLegacyType.decryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + { + firstAttr: '#####', + nonEncryptedAttr: 'non encrypted', + } + ); + + expect(serviceWithMigrationLegacyType.encryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + { + firstAttr: `~~first_attr~~`, + encryptedAttr: 'non encrypted', + } + ); + }); + }); +}); diff --git a/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts b/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts new file mode 100644 index 00000000000000..8e9dc1c1389660 --- /dev/null +++ b/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SavedObjectUnsanitizedDoc, + SavedObjectMigrationFn, + SavedObjectMigrationContext, +} from 'src/core/server'; +import { EncryptedSavedObjectTypeRegistration, EncryptedSavedObjectsService } from './crypto'; + +type SavedObjectOptionalMigrationFn = ( + doc: SavedObjectUnsanitizedDoc | SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext +) => SavedObjectUnsanitizedDoc; + +type IsMigrationNeededPredicate = ( + encryptedDoc: + | SavedObjectUnsanitizedDoc + | SavedObjectUnsanitizedDoc +) => encryptedDoc is SavedObjectUnsanitizedDoc; + +export type CreateEncryptedSavedObjectsMigrationFn = < + InputAttributes = unknown, + MigratedAttributes = InputAttributes +>( + isMigrationNeededPredicate: IsMigrationNeededPredicate, + migration: SavedObjectMigrationFn, + inputType?: EncryptedSavedObjectTypeRegistration, + migratedType?: EncryptedSavedObjectTypeRegistration +) => SavedObjectOptionalMigrationFn; + +export const getCreateMigration = ( + encryptedSavedObjectsService: Readonly, + instantiateServiceWithLegacyType: ( + typeRegistration: EncryptedSavedObjectTypeRegistration + ) => EncryptedSavedObjectsService +): CreateEncryptedSavedObjectsMigrationFn => ( + isMigrationNeededPredicate, + migration, + inputType, + migratedType +) => { + if (inputType && migratedType && inputType.type !== migratedType.type) { + throw new Error( + `An Invalid Encrypted Saved Objects migration is trying to migrate across types ("${inputType.type}" => "${migratedType.type}"), which isn't permitted` + ); + } + + const inputService = inputType + ? instantiateServiceWithLegacyType(inputType) + : encryptedSavedObjectsService; + + const migratedService = migratedType + ? instantiateServiceWithLegacyType(migratedType) + : encryptedSavedObjectsService; + + return (encryptedDoc, context) => { + if (!isMigrationNeededPredicate(encryptedDoc)) { + return encryptedDoc; + } + + const descriptor = { + id: encryptedDoc.id!, + type: encryptedDoc.type, + namespace: encryptedDoc.namespace, + }; + + // decrypt the attributes using the input type definition + // then migrate the document + // then encrypt the attributes using the migration type definition + return mapAttributes( + migration( + mapAttributes(encryptedDoc, (inputAttributes) => + inputService.decryptAttributesSync(descriptor, inputAttributes) + ), + context + ), + (migratedAttributes) => + migratedService.encryptAttributesSync(descriptor, migratedAttributes) + ); + }; +}; + +function mapAttributes(obj: SavedObjectUnsanitizedDoc, mapper: (attributes: T) => T) { + return Object.assign(obj, { + attributes: mapper(obj.attributes), + }); +} diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts new file mode 100644 index 00000000000000..c692d8698771fe --- /dev/null +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EncryptedSavedObjectsService, + EncryptedSavedObjectTypeRegistration, + SavedObjectDescriptor, +} from './encrypted_saved_objects_service'; + +function createEncryptedSavedObjectsServiceMock() { + return ({ + isRegistered: jest.fn(), + stripOrDecryptAttributes: jest.fn(), + encryptAttributes: jest.fn(), + decryptAttributes: jest.fn(), + encryptAttributesSync: jest.fn(), + decryptAttributesSync: jest.fn(), + } as unknown) as jest.Mocked; +} + +export const encryptedSavedObjectsServiceMock = { + create: createEncryptedSavedObjectsServiceMock, + createWithTypes(registrations: EncryptedSavedObjectTypeRegistration[] = []) { + const mock = createEncryptedSavedObjectsServiceMock(); + + function processAttributes>( + descriptor: Pick, + attrs: T, + action: (attrs: T, attrName: string, shouldExpose: boolean) => void + ) { + const registration = registrations.find((r) => r.type === descriptor.type); + if (!registration) { + return attrs; + } + + const clonedAttrs = { ...attrs }; + for (const attr of registration.attributesToEncrypt) { + const [attrName, shouldExpose] = + typeof attr === 'string' + ? [attr, false] + : [attr.key, attr.dangerouslyExposeValue === true]; + if (attrName in clonedAttrs) { + action(clonedAttrs, attrName, shouldExpose); + } + } + return clonedAttrs; + } + + mock.isRegistered.mockImplementation( + (type) => registrations.findIndex((r) => r.type === type) >= 0 + ); + mock.encryptAttributes.mockImplementation(async (descriptor, attrs) => + processAttributes( + descriptor, + attrs, + (clonedAttrs, attrName) => (clonedAttrs[attrName] = `*${clonedAttrs[attrName]}*`) + ) + ); + mock.decryptAttributes.mockImplementation(async (descriptor, attrs) => + processAttributes( + descriptor, + attrs, + (clonedAttrs, attrName) => + (clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1)) + ) + ); + mock.stripOrDecryptAttributes.mockImplementation((descriptor, attrs) => + Promise.resolve({ + attributes: processAttributes(descriptor, attrs, (clonedAttrs, attrName, shouldExpose) => { + if (shouldExpose) { + clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1); + } else { + delete clonedAttrs[attrName]; + } + }), + }) + ); + + return mock; + }, +}; diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts index db7c96f83dff25..42d2e2ffd15163 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockAuthenticatedUser } from '../../../security/common/model/authenticated_user.mock'; - -jest.mock('@elastic/node-crypto', () => jest.fn()); +import nodeCrypto, { Crypto } from '@elastic/node-crypto'; +import { mockAuthenticatedUser } from '../../../security/common/model/authenticated_user.mock'; import { EncryptedSavedObjectsAuditLogger } from '../audit'; import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service'; import { EncryptionError } from './encryption_error'; @@ -15,19 +14,37 @@ import { EncryptionError } from './encryption_error'; import { loggingSystemMock } from 'src/core/server/mocks'; import { encryptedSavedObjectsAuditLoggerMock } from '../audit/index.mock'; +const crypto = nodeCrypto({ encryptionKey: 'encryption-key-abc' }); + +const mockNodeCrypto: jest.Mocked = { + encrypt: jest.fn(), + decrypt: jest.fn(), + encryptSync: jest.fn(), + decryptSync: jest.fn(), +}; + let service: EncryptedSavedObjectsService; let mockAuditLogger: jest.Mocked; -beforeEach(() => { - mockAuditLogger = encryptedSavedObjectsAuditLoggerMock.create(); +beforeEach(() => { // Call actual `@elastic/node-crypto` by default, but allow to override implementation in tests. - jest.requireMock('@elastic/node-crypto').mockImplementation((...args: any[]) => { - const { default: nodeCrypto } = jest.requireActual('@elastic/node-crypto'); - return nodeCrypto(...args); - }); + mockNodeCrypto.encrypt.mockImplementation(async (input: any, aad?: string) => + crypto.encrypt(input, aad) + ); + mockNodeCrypto.decrypt.mockImplementation( + async (encryptedOutput: string | Buffer, aad?: string) => crypto.decrypt(encryptedOutput, aad) + ); + mockNodeCrypto.encryptSync.mockImplementation((input: any, aad?: string) => + crypto.encryptSync(input, aad) + ); + mockNodeCrypto.decryptSync.mockImplementation((encryptedOutput: string | Buffer, aad?: string) => + crypto.decryptSync(encryptedOutput, aad) + ); + + mockAuditLogger = encryptedSavedObjectsAuditLoggerMock.create(); service = new EncryptedSavedObjectsService( - 'encryption-key-abc', + mockNodeCrypto, loggingSystemMock.create().get(), mockAuditLogger ); @@ -35,12 +52,6 @@ beforeEach(() => { afterEach(() => jest.resetAllMocks()); -it('correctly initializes crypto', () => { - const mockNodeCrypto = jest.requireMock('@elastic/node-crypto'); - expect(mockNodeCrypto).toHaveBeenCalledTimes(1); - expect(mockNodeCrypto).toHaveBeenCalledWith({ encryptionKey: 'encryption-key-abc' }); -}); - describe('#registerType', () => { it('throws if `attributesToEncrypt` is empty', () => { expect(() => @@ -213,15 +224,13 @@ describe('#stripOrDecryptAttributes', () => { }); describe('#encryptAttributes', () => { - let mockEncrypt: jest.Mock; beforeEach(() => { - mockEncrypt = jest - .fn() - .mockImplementation(async (valueToEncrypt, aad) => `|${valueToEncrypt}|${aad}|`); - jest.requireMock('@elastic/node-crypto').mockReturnValue({ encrypt: mockEncrypt }); + mockNodeCrypto.encrypt.mockImplementation( + async (valueToEncrypt, aad) => `|${valueToEncrypt}|${aad}|` + ); service = new EncryptedSavedObjectsService( - 'encryption-key-abc', + mockNodeCrypto, loggingSystemMock.create().get(), mockAuditLogger ); @@ -399,7 +408,7 @@ describe('#encryptAttributes', () => { attributesToEncrypt: new Set(['attrOne', 'attrThree']), }); - mockEncrypt + mockNodeCrypto.encrypt .mockResolvedValueOnce('Successfully encrypted attrOne') .mockRejectedValueOnce(new Error('Something went wrong with attrThree...')); @@ -915,7 +924,7 @@ describe('#decryptAttributes', () => { it('fails if encrypted with another encryption key', async () => { service = new EncryptedSavedObjectsService( - 'encryption-key-abc*', + nodeCrypto({ encryptionKey: 'encryption-key-abc*' }), loggingSystemMock.create().get(), mockAuditLogger ); @@ -941,3 +950,532 @@ describe('#decryptAttributes', () => { }); }); }); + +describe('#encryptAttributesSync', () => { + beforeEach(() => { + mockNodeCrypto.encryptSync.mockImplementation( + (valueToEncrypt, aad) => `|${valueToEncrypt}|${aad}|` + ); + + service = new EncryptedSavedObjectsService( + mockNodeCrypto, + loggingSystemMock.create().get(), + mockAuditLogger + ); + }); + + it('does not encrypt attributes that are not supposed to be encrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrFour']), + }); + expect( + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('encrypts only attributes that are supposed to be encrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three', attrFour: null }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']), + }); + + expect( + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrOne: '|one|["known-type-1","object-id",{"attrTwo":"two"}]|', + attrTwo: 'two', + attrThree: '|three|["known-type-1","object-id",{"attrTwo":"two"}]|', + attrFour: null, + }); + }); + + it('encrypts only attributes that are supposed to be encrypted even if not all provided', () => { + const attributes = { attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + expect( + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrTwo: 'two', + attrThree: '|three|["known-type-1","object-id",{"attrTwo":"two"}]|', + }); + }); + + it('includes `namespace` into AAD if provided', () => { + const attributes = { attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + expect( + service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + attributes + ) + ).toEqual({ + attrTwo: 'two', + attrThree: '|three|["object-ns","known-type-1","object-id",{"attrTwo":"two"}]|', + }); + }); + + it('does not include specified attributes to AAD', () => { + const knownType1attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const knownType2attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-2', + attributesToEncrypt: new Set(['attrThree']), + attributesToExcludeFromAAD: new Set(['attrTwo']), + }); + + expect( + service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id-1' }, + knownType1attributes + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: '|three|["known-type-1","object-id-1",{"attrOne":"one","attrTwo":"two"}]|', + }); + expect( + service.encryptAttributesSync( + { type: 'known-type-2', id: 'object-id-2' }, + knownType2attributes + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: '|three|["known-type-2","object-id-2",{"attrOne":"one"}]|', + }); + }); + + it('encrypts even if no attributes are included into AAD', () => { + const attributes = { attrOne: 'one', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + expect( + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id-1' }, attributes) + ).toEqual({ + attrOne: '|one|["known-type-1","object-id-1",{}]|', + attrThree: '|three|["known-type-1","object-id-1",{}]|', + }); + }); + + it('fails if encryption of any attribute fails', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + mockNodeCrypto.encryptSync + .mockImplementationOnce(() => 'Successfully encrypted attrOne') + .mockImplementationOnce(() => { + throw new Error('Something went wrong with attrThree...'); + }); + + expect(() => + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toThrowError(EncryptionError); + + expect(attributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); +}); + +describe('#decryptAttributesSync', () => { + it('does not decrypt attributes that are not supposed to be decrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrFour']), + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts only attributes that are supposed to be decrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three', attrFour: null }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: expect.not.stringMatching(/^one$/), + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + attrFour: null, + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + attrFour: null, + }); + }); + + it('decrypts only attributes that are supposed to be encrypted even if not all provided', () => { + const attributes = { attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes) + ).toEqual({ + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts if all attributes that contribute to AAD are present', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + attributesToExcludeFromAAD: new Set(['attrOne']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const attributesWithoutAttr = { attrTwo: 'two', attrThree: encryptedAttributes.attrThree }; + + expect( + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributesWithoutAttr + ) + ).toEqual({ + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts even if attributes in AAD are defined in a different order', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const attributesInDifferentOrder = { + attrThree: encryptedAttributes.attrThree, + attrTwo: 'two', + attrOne: 'one', + }; + + expect( + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributesInDifferentOrder + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts if correct namespace is provided', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + expect( + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + encryptedAttributes + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts even if no attributes are included into AAD', () => { + const attributes = { attrOne: 'one', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: expect.not.stringMatching(/^one$/), + attrThree: expect.not.stringMatching(/^three$/), + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes) + ).toEqual({ + attrOne: 'one', + attrThree: 'three', + }); + }); + + it('decrypts non-string attributes and restores their original type', () => { + const attributes = { + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + attrFour: null, + attrFive: { nested: 'five' }, + attrSix: 6, + }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour', 'attrFive', 'attrSix']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: expect.not.stringMatching(/^one$/), + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + attrFour: null, + attrFive: expect.any(String), + attrSix: expect.any(String), + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + attrFour: null, + attrFive: { nested: 'five' }, + attrSix: 6, + }); + }); + + describe('decryption failures', () => { + let encryptedAttributes: Record; + + const type1 = { + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }; + + const type2 = { + type: 'known-type-2', + attributesToEncrypt: new Set(['attrThree']), + }; + + beforeEach(() => { + service.registerType(type1); + service.registerType(type2); + + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + }); + + it('fails to decrypt if not all attributes that contribute to AAD are present', () => { + const attributesWithoutAttr = { attrTwo: 'two', attrThree: encryptedAttributes.attrThree }; + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributesWithoutAttr + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if ID does not match', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id*' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if type does not match', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-2', id: 'object-id' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if namespace does not match', () => { + encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + { attrOne: 'one', attrTwo: 'two', attrThree: 'three' } + ); + + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-NS' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if namespace is expected, but is not provided', () => { + encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + { attrOne: 'one', attrTwo: 'two', attrThree: 'three' } + ); + + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if encrypted attribute is defined, but not a string', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + { + ...encryptedAttributes, + attrThree: 2, + } + ) + ).toThrowError('Encrypted "attrThree" attribute should be a string, but found number'); + }); + + it('fails to decrypt if encrypted attribute is not correct', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + { + ...encryptedAttributes, + attrThree: 'some-unknown-string', + } + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if the AAD attribute has changed', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + { + ...encryptedAttributes, + attrOne: 'oNe', + } + ) + ).toThrowError(EncryptionError); + }); + + it('fails if encrypted with another encryption key', () => { + service = new EncryptedSavedObjectsService( + nodeCrypto({ encryptionKey: 'encryption-key-abc*' }), + loggingSystemMock.create().get(), + mockAuditLogger + ); + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + }); +}); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index 5cf3e1c2d65aec..99361107047c27 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import nodeCrypto, { Crypto } from '@elastic/node-crypto'; -import stringify from 'json-stable-stringify'; +import { Crypto, EncryptOutput } from '@elastic/node-crypto'; import typeDetect from 'type-detect'; +import stringify from 'json-stable-stringify'; import { Logger } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/common/model'; import { EncryptedSavedObjectsAuditLogger } from '../audit'; @@ -70,8 +70,6 @@ export function descriptorToArray(descriptor: SavedObjectDescriptor) { * attributes. */ export class EncryptedSavedObjectsService { - private readonly crypto: Readonly; - /** * Map of all registered saved object types where the `key` is saved object type and the `value` * is the definition (names of attributes that need to be encrypted etc.). @@ -82,17 +80,15 @@ export class EncryptedSavedObjectsService { > = new Map(); /** - * @param encryptionKey The key used to encrypt and decrypt saved objects attributes. + * @param crypto nodeCrypto instance. * @param logger Ordinary logger instance. * @param audit Audit logger instance. */ constructor( - encryptionKey: string, + private readonly crypto: Readonly, private readonly logger: Logger, private readonly audit: EncryptedSavedObjectsAuditLogger - ) { - this.crypto = nodeCrypto({ encryptionKey }); - } + ) {} /** * Registers saved object type as the one that contains attributes that should be encrypted. @@ -193,20 +189,11 @@ export class EncryptedSavedObjectsService { return { attributes: clonedAttributes as T, error: decryptionError }; } - /** - * Takes saved object attributes for the specified type and encrypts all of them that are supposed - * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the - * attributes were encrypted original attributes dictionary is returned. - * @param descriptor Descriptor of the saved object to encrypt attributes for. - * @param attributes Dictionary of __ALL__ saved object attributes. - * @param [params] Additional parameters. - * @throws Will throw if encryption fails for whatever reason. - */ - public async encryptAttributes>( + private *attributesToEncryptIterator>( descriptor: SavedObjectDescriptor, attributes: T, params?: CommonParameters - ): Promise { + ): Iterator<[unknown, string], T, string> { const typeDefinition = this.typeDefinitions.get(descriptor.type); if (typeDefinition === undefined) { return attributes; @@ -218,10 +205,7 @@ export class EncryptedSavedObjectsService { const attributeValue = attributes[attributeName]; if (attributeValue != null) { try { - encryptedAttributes[attributeName] = await this.crypto.encrypt( - attributeValue, - encryptionAAD - ); + encryptedAttributes[attributeName] = (yield [attributeValue, encryptionAAD])!; } catch (err) { this.logger.error( `Failed to encrypt "${attributeName}" attribute: ${err.message || err}` @@ -263,6 +247,64 @@ export class EncryptedSavedObjectsService { }; } + /** + * Takes saved object attributes for the specified type and encrypts all of them that are supposed + * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the + * attributes were encrypted original attributes dictionary is returned. + * @param descriptor Descriptor of the saved object to encrypt attributes for. + * @param attributes Dictionary of __ALL__ saved object attributes. + * @param [params] Additional parameters. + * @throws Will throw if encryption fails for whatever reason. + */ + public async encryptAttributes>( + descriptor: SavedObjectDescriptor, + attributes: T, + params?: CommonParameters + ): Promise { + const iterator = this.attributesToEncryptIterator(descriptor, attributes, params); + + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + const [attributeValue, encryptionAAD] = iteratorResult.value; + try { + iteratorResult = iterator.next(await this.crypto.encrypt(attributeValue, encryptionAAD)); + } catch (err) { + iterator.throw!(err); + } + } + + return iteratorResult.value; + } + + /** + * Takes saved object attributes for the specified type and encrypts all of them that are supposed + * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the + * attributes were encrypted original attributes dictionary is returned. + * @param descriptor Descriptor of the saved object to encrypt attributes for. + * @param attributes Dictionary of __ALL__ saved object attributes. + * @param [params] Additional parameters. + * @throws Will throw if encryption fails for whatever reason. + */ + public encryptAttributesSync>( + descriptor: SavedObjectDescriptor, + attributes: T, + params?: CommonParameters + ): T { + const iterator = this.attributesToEncryptIterator(descriptor, attributes, params); + + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + const [attributeValue, encryptionAAD] = iteratorResult.value; + try { + iteratorResult = iterator.next(this.crypto.encryptSync(attributeValue, encryptionAAD)); + } catch (err) { + iterator.throw!(err); + } + } + + return iteratorResult.value; + } + /** * Takes saved object attributes for the specified type and decrypts all of them that are supposed * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the @@ -278,13 +320,65 @@ export class EncryptedSavedObjectsService { attributes: T, params?: CommonParameters ): Promise { + const iterator = this.attributesToDecryptIterator(descriptor, attributes, params); + + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + const [attributeValue, encryptionAAD] = iteratorResult.value; + try { + iteratorResult = iterator.next( + (await this.crypto.decrypt(attributeValue, encryptionAAD)) as string + ); + } catch (err) { + iterator.throw!(err); + } + } + + return iteratorResult.value; + } + + /** + * Takes saved object attributes for the specified type and decrypts all of them that are supposed + * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the + * attributes were decrypted original attributes dictionary is returned. + * @param descriptor Descriptor of the saved object to decrypt attributes for. + * @param attributes Dictionary of __ALL__ saved object attributes. + * @param [params] Additional parameters. + * @throws Will throw if decryption fails for whatever reason. + * @throws Will throw if any of the attributes to decrypt is not a string. + */ + public decryptAttributesSync>( + descriptor: SavedObjectDescriptor, + attributes: T, + params?: CommonParameters + ): T { + const iterator = this.attributesToDecryptIterator(descriptor, attributes, params); + + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + const [attributeValue, encryptionAAD] = iteratorResult.value; + try { + iteratorResult = iterator.next(this.crypto.decryptSync(attributeValue, encryptionAAD)); + } catch (err) { + iterator.throw!(err); + } + } + + return iteratorResult.value; + } + + private *attributesToDecryptIterator>( + descriptor: SavedObjectDescriptor, + attributes: T, + params?: CommonParameters + ): Iterator<[string, string], T, EncryptOutput> { const typeDefinition = this.typeDefinitions.get(descriptor.type); if (typeDefinition === undefined) { return attributes; } const encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); - const decryptedAttributes: Record = {}; + const decryptedAttributes: Record = {}; for (const attributeName of typeDefinition.attributesToEncrypt) { const attributeValue = attributes[attributeName]; if (attributeValue == null) { @@ -301,10 +395,7 @@ export class EncryptedSavedObjectsService { } try { - decryptedAttributes[attributeName] = (await this.crypto.decrypt( - attributeValue, - encryptionAAD - )) as string; + decryptedAttributes[attributeName] = (yield [attributeValue, encryptionAAD])!; } catch (err) { this.logger.error(`Failed to decrypt "${attributeName}" attribute: ${err.message || err}`); this.audit.decryptAttributeFailure(attributeName, descriptor, params?.user); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/index.mock.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/index.mock.ts index 11a0cd6f33307d..3e4983deca6255 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/index.mock.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/index.mock.ts @@ -4,71 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EncryptedSavedObjectsService, - EncryptedSavedObjectTypeRegistration, - SavedObjectDescriptor, -} from '.'; - -export const encryptedSavedObjectsServiceMock = { - create(registrations: EncryptedSavedObjectTypeRegistration[] = []) { - const mock: jest.Mocked = new (jest.requireMock( - './encrypted_saved_objects_service' - ).EncryptedSavedObjectsService)(); - - function processAttributes>( - descriptor: Pick, - attrs: T, - action: (attrs: T, attrName: string, shouldExpose: boolean) => void - ) { - const registration = registrations.find((r) => r.type === descriptor.type); - if (!registration) { - return attrs; - } - - const clonedAttrs = { ...attrs }; - for (const attr of registration.attributesToEncrypt) { - const [attrName, shouldExpose] = - typeof attr === 'string' - ? [attr, false] - : [attr.key, attr.dangerouslyExposeValue === true]; - if (attrName in clonedAttrs) { - action(clonedAttrs, attrName, shouldExpose); - } - } - return clonedAttrs; - } - - mock.isRegistered.mockImplementation( - (type) => registrations.findIndex((r) => r.type === type) >= 0 - ); - mock.encryptAttributes.mockImplementation(async (descriptor, attrs) => - processAttributes( - descriptor, - attrs, - (clonedAttrs, attrName) => (clonedAttrs[attrName] = `*${clonedAttrs[attrName]}*`) - ) - ); - mock.decryptAttributes.mockImplementation(async (descriptor, attrs) => - processAttributes( - descriptor, - attrs, - (clonedAttrs, attrName) => - (clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1)) - ) - ); - mock.stripOrDecryptAttributes.mockImplementation((descriptor, attrs) => - Promise.resolve({ - attributes: processAttributes(descriptor, attrs, (clonedAttrs, attrName, shouldExpose) => { - if (shouldExpose) { - clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1); - } else { - delete clonedAttrs[attrName]; - } - }), - }) - ); - - return mock; - }, -}; +export { encryptedSavedObjectsServiceMock } from './encrypted_saved_objects_service.mocks'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/index.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/index.ts index 0849f0eb320dd3..75445bd24eba84 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/index.ts @@ -11,3 +11,4 @@ export { SavedObjectDescriptor, } from './encrypted_saved_objects_service'; export { EncryptionError } from './encryption_error'; +export { EncryptedSavedObjectAttributesDefinition } from './encrypted_saved_object_type_definition'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 38ac8f254315e1..adec3a3b9fbf40 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -12,6 +12,7 @@ function createEncryptedSavedObjectsSetupMock() { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, usingEphemeralEncryptionKey: true, + createMigration: jest.fn(), } as jest.Mocked; } diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 4afd74488f9fed..57108954f2568d 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -16,6 +16,7 @@ describe('EncryptedSavedObjects Plugin', () => { await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) .resolves.toMatchInlineSnapshot(` Object { + "createMigration": [Function], "registerType": [Function], "usingEphemeralEncryptionKey": true, } diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index cdbdd18b9d696c..69777798ddf192 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import nodeCrypto from '@elastic/node-crypto'; import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; import { first } from 'rxjs/operators'; import { SecurityPluginSetup } from '../../security/server'; @@ -15,6 +16,7 @@ import { } from './crypto'; import { EncryptedSavedObjectsAuditLogger } from './audit'; import { setupSavedObjects, ClientInstanciator } from './saved_objects'; +import { getCreateMigration, CreateEncryptedSavedObjectsMigrationFn } from './create_migration'; export interface PluginsSetup { security?: SecurityPluginSetup; @@ -23,6 +25,7 @@ export interface PluginsSetup { export interface EncryptedSavedObjectsPluginSetup { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; usingEphemeralEncryptionKey: boolean; + createMigration: CreateEncryptedSavedObjectsMigrationFn; } export interface EncryptedSavedObjectsPluginStart { @@ -45,18 +48,18 @@ export class Plugin { core: CoreSetup, deps: PluginsSetup ): Promise { - const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext) - .pipe(first()) - .toPromise(); + const { + config: { encryptionKey }, + usingEphemeralEncryptionKey, + } = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + + const crypto = nodeCrypto({ encryptionKey }); + const auditLogger = new EncryptedSavedObjectsAuditLogger( + deps.security?.audit.getLogger('encryptedSavedObjects') + ); const service = Object.freeze( - new EncryptedSavedObjectsService( - config.encryptionKey, - this.logger, - new EncryptedSavedObjectsAuditLogger( - deps.security?.audit.getLogger('encryptedSavedObjects') - ) - ) + new EncryptedSavedObjectsService(crypto, this.logger, auditLogger) ); this.savedObjectsSetup = setupSavedObjects({ @@ -70,6 +73,18 @@ export class Plugin { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => service.registerType(typeRegistration), usingEphemeralEncryptionKey, + createMigration: getCreateMigration( + service, + (typeRegistration: EncryptedSavedObjectTypeRegistration) => { + const serviceForMigration = new EncryptedSavedObjectsService( + crypto, + this.logger, + auditLogger + ); + serviceForMigration.registerType(typeRegistration); + return serviceForMigration; + } + ), }; } diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index ec5d81532e238f..eea19bb1aa7dd5 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -22,7 +22,7 @@ let encryptedSavedObjectsServiceMockInstance: jest.Mocked { mockBaseClient = savedObjectsClientMock.create(); mockBaseTypeRegistry = savedObjectsTypeRegistryMock.create(); - encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.create([ + encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.createWithTypes([ { type: 'known-type', attributesToEncrypt: new Set([ diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts index 8e9f12268cd7e2..ef9aed8706e2ce 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts @@ -42,7 +42,7 @@ describe('#setupSavedObjects', () => { coreSetupMock = coreMock.createSetup(); coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]); - mockEncryptedSavedObjectsService = encryptedSavedObjectsServiceMock.create([ + mockEncryptedSavedObjectsService = encryptedSavedObjectsServiceMock.createWithTypes([ { type: 'known-type', attributesToEncrypt: new Set(['attrSecret']) }, ]); setupContract = setupSavedObjects({ diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx index 47a0f037816bc1..04642a01c15b4d 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx @@ -7,6 +7,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; @@ -15,6 +16,9 @@ export const InventoryAlertDropdown = () => { const [flyoutVisible, setFlyoutVisible] = useState(false); const kibana = useKibana(); + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric, filterQuery } = inventoryPrefill; + const closePopover = useCallback(() => { setPopoverOpen(false); }, [setPopoverOpen]); @@ -57,7 +61,13 @@ export const InventoryAlertDropdown = () => { > - + ); }; diff --git a/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts new file mode 100644 index 00000000000000..d659057b95ed92 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; +import { SnapshotMetricInput } from '../../../../common/http_api/snapshot_api'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; + +export const useInventoryAlertPrefill = () => { + const [nodeType, setNodeType] = useState('host'); + const [filterQuery, setFilterQuery] = useState(); + const [metric, setMetric] = useState({ type: 'cpu' }); + + return { + nodeType, + filterQuery, + metric, + setNodeType, + setFilterQuery, + setMetric, + }; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx index d26575f65dfec5..384a93e796dbe3 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -7,14 +7,18 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { AlertFlyout } from './alert_flyout'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useAlertPrefillContext } from '../../use_alert_prefill'; +import { AlertFlyout } from './alert_flyout'; export const MetricsAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); const kibana = useKibana(); + const { metricThresholdPrefill } = useAlertPrefillContext(); + const { groupBy, filterQuery, metrics } = metricThresholdPrefill; + const closePopover = useCallback(() => { setPopoverOpen(false); }, [setPopoverOpen]); @@ -57,7 +61,11 @@ export const MetricsAlertDropdown = () => { > - + ); }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx new file mode 100644 index 00000000000000..fa535e28c0b770 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; +import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { AlertContextMeta } from '../types'; +import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; +import React from 'react'; +import { Expressions } from './expression'; +import { act } from 'react-dom/test-utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; + +jest.mock('../../../containers/source/use_source_via_http', () => ({ + useSourceViaHttp: () => ({ + source: { id: 'default' }, + createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), + }), +})); + +describe('Expression', () => { + async function setup(currentOptions: { + metrics?: MetricsExplorerMetric[]; + filterQuery?: string; + groupBy?: string; + }) { + const alertParams = { + criteria: [], + groupBy: undefined, + filterQueryText: '', + }; + + const mocks = coreMock.createSetup(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + const context: AlertsContextValue = { + http: mocks.http, + toastNotifications: mocks.notifications.toasts, + actionTypeRegistry: actionTypeRegistryMock.create() as any, + alertTypeRegistry: alertTypeRegistryMock.create() as any, + docLinks: mocks.docLinks, + capabilities: { + ...capabilities, + actions: { + delete: true, + save: true, + show: true, + }, + }, + metadata: { + currentOptions, + }, + }; + + const wrapper = mountWithIntl( + Reflect.set(alertParams, key, value)} + setAlertProperty={() => {}} + /> + ); + + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await update(); + + return { wrapper, update, alertParams }; + } + + it('should prefill the alert using the context metadata', async () => { + const currentOptions = { + groupBy: 'host.hostname', + filterQuery: 'foo', + metrics: [ + { aggregation: 'avg', field: 'system.load.1' }, + { aggregation: 'cardinality', field: 'system.cpu.user.pct' }, + ] as MetricsExplorerMetric[], + }; + const { alertParams } = await setup(currentOptions); + expect(alertParams.groupBy).toBe('host.hostname'); + expect(alertParams.filterQueryText).toBe('foo'); + expect(alertParams.criteria).toEqual([ + { + metric: 'system.load.1', + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', + aggType: 'avg', + }, + { + metric: 'system.cpu.user.pct', + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', + aggType: 'cardinality', + }, + ]); + }); +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 3c3351f4ddd76d..f45474f2844844 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { debounce, pick } from 'lodash'; +import { debounce, pick, omit } from 'lodash'; import { Unit } from '@elastic/datemath'; import * as rt from 'io-ts'; import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react'; @@ -52,7 +52,7 @@ import { useSourceViaHttp } from '../../../containers/source/use_source_via_http import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; import { ExpressionRow } from './expression_row'; -import { AlertContextMeta, TimeUnit, MetricExpression } from '../types'; +import { AlertContextMeta, TimeUnit, MetricExpression, AlertParams } from '../types'; import { ExpressionChart } from './expression_chart'; import { validateMetricThreshold } from './validation'; @@ -60,14 +60,7 @@ const FILTER_TYPING_DEBOUNCE_MS = 500; interface Props { errors: IErrorObject[]; - alertParams: { - criteria: MetricExpression[]; - groupBy?: string; - filterQuery?: string; - sourceId?: string; - filterQueryText?: string; - alertOnNoData?: boolean; - }; + alertParams: AlertParams; alertsContext: AlertsContextValue; alertInterval: string; setAlertParams(key: string, value: any): void; @@ -81,6 +74,7 @@ const defaultExpression = { timeSize: 1, timeUnit: 'm', } as MetricExpression; +export { defaultExpression }; export const Expressions: React.FC = (props) => { const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props; @@ -247,6 +241,13 @@ export const Expressions: React.FC = (props) => { } }, [alertsContext.metadata, derivedIndexPattern, setAlertParams]); + const preFillAlertGroupBy = useCallback(() => { + const md = alertsContext.metadata; + if (md && md.currentOptions?.groupBy && !md.series) { + setAlertParams('groupBy', md.currentOptions.groupBy); + } + }, [alertsContext.metadata, setAlertParams]); + const onSelectPreviewLookbackInterval = useCallback((e) => { setPreviewLookbackInterval(e.target.value); setPreviewResult(null); @@ -286,6 +287,10 @@ export const Expressions: React.FC = (props) => { preFillAlertFilter(); } + if (!alertParams.groupBy) { + preFillAlertGroupBy(); + } + if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } @@ -465,7 +470,7 @@ export const Expressions: React.FC = (props) => { id="selectPreviewLookbackInterval" value={previewLookbackInterval} onChange={onSelectPreviewLookbackInterval} - options={previewOptions} + options={previewDOMOptions} /> @@ -588,6 +593,10 @@ export const Expressions: React.FC = (props) => { ); }; +const previewDOMOptions: Array<{ text: string; value: string }> = previewOptions.map((o) => + omit(o, 'shortText') +); + // required for dynamic import // eslint-disable-next-line import/no-default-export export default Expressions; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx index da342f0a454203..2221d3cd4fe120 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx @@ -50,7 +50,7 @@ export function validateMetricThreshold({ if (!c.aggType) { errors[id].aggField.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', { - defaultMessage: 'Aggreation is required.', + defaultMessage: 'Aggregation is required.', }) ); } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts new file mode 100644 index 00000000000000..366d6aa7003e63 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +import { useState } from 'react'; +import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; + +interface MetricThresholdPrefillOptions { + groupBy: string | string[] | undefined; + filterQuery: string | undefined; + metrics: MetricsExplorerMetric[]; +} + +export const useMetricThresholdAlertPrefill = () => { + const [prefillOptionsState, setPrefillOptionsState] = useState({ + groupBy: undefined, + filterQuery: undefined, + metrics: [], + }); + + const { groupBy, filterQuery, metrics } = prefillOptionsState; + + return { + groupBy, + filterQuery, + metrics, + setPrefillOptions(newState: MetricThresholdPrefillOptions) { + if (!isEqual(newState, prefillOptionsState)) setPrefillOptionsState(newState); + }, + }; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index feeec4b0ce8bf0..2f8d7ec0ba6f48 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -51,3 +51,12 @@ export interface ExpressionChartData { id: string; series: ExpressionChartSeries; } + +export interface AlertParams { + criteria: MetricExpression[]; + groupBy?: string; + filterQuery?: string; + sourceId?: string; + filterQueryText?: string; + alertOnNoData?: boolean; +} diff --git a/x-pack/plugins/infra/public/alerting/use_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/use_alert_prefill.ts new file mode 100644 index 00000000000000..eff2fe462509f4 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/use_alert_prefill.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import createContainer from 'constate'; +import { useMetricThresholdAlertPrefill } from './metric_threshold/hooks/use_metric_threshold_alert_prefill'; +import { useInventoryAlertPrefill } from './inventory/hooks/use_inventory_alert_prefill'; + +const useAlertPrefill = () => { + const metricThresholdPrefill = useMetricThresholdAlertPrefill(); + const inventoryPrefill = useInventoryAlertPrefill(); + + return { metricThresholdPrefill, inventoryPrefill }; +}; + +export const [AlertPrefillProvider, useAlertPrefillContext] = createContainer(useAlertPrefill); diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx deleted file mode 100644 index e27de7fd6b5a89..00000000000000 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; -import { EuiButtonIcon, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { LogEntryColumnContent } from './log_entry_column'; -import { euiStyled } from '../../../../../observability/public'; - -interface LogEntryActionsColumnProps { - isHovered: boolean; - isMenuOpen: boolean; - onOpenMenu: () => void; - onCloseMenu: () => void; - onViewDetails?: () => void; - onViewLogInContext?: () => void; -} - -const MENU_LABEL = i18n.translate('xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', { - defaultMessage: 'View actions for line', -}); - -const LOG_DETAILS_LABEL = i18n.translate('xpack.infra.logs.logEntryActionsDetailsButton', { - defaultMessage: 'View details', -}); - -const LOG_VIEW_IN_CONTEXT_LABEL = i18n.translate( - 'xpack.infra.lobs.logEntryActionsViewInContextButton', - { - defaultMessage: 'View in context', - } -); - -export const LogEntryActionsColumn: React.FC = ({ - isHovered, - isMenuOpen, - onOpenMenu, - onCloseMenu, - onViewDetails, - onViewLogInContext, -}) => { - const handleClickViewDetails = useCallback(() => { - onCloseMenu(); - - // Function might be `undefined` and the linter doesn't like that. - // eslint-disable-next-line no-unused-expressions - onViewDetails?.(); - }, [onCloseMenu, onViewDetails]); - - const handleClickViewInContext = useCallback(() => { - onCloseMenu(); - - // Function might be `undefined` and the linter doesn't like that. - // eslint-disable-next-line no-unused-expressions - onViewLogInContext?.(); - }, [onCloseMenu, onViewLogInContext]); - - const button = ( - - - - ); - - const items = [ - - {LOG_DETAILS_LABEL} - , - ]; - - if (onViewLogInContext !== undefined) { - items.push( - - {LOG_VIEW_IN_CONTEXT_LABEL} - - ); - } - - return ( - - {isHovered || isMenuOpen ? ( - - - - - - ) : null} - - ); -}; - -const ActionsColumnContent = euiStyled(LogEntryColumnContent)` - overflow: hidden; - user-select: none; -`; - -const ButtonWrapper = euiStyled.div` - background: ${(props) => props.theme.eui.euiColorPrimary}; - border-radius: 50%; - padding: 4px; - transform: translateY(-6px); -`; - -// this prevents the button from influencing the line height -const AbsoluteWrapper = euiStyled.div` - position: absolute; -`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx new file mode 100644 index 00000000000000..4aa81846d90ef9 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; + +import { euiStyled } from '../../../../../observability/public'; +import { LogEntryColumnContent } from './log_entry_column'; + +interface LogEntryContextMenuItem { + label: string; + onClick: () => void; +} + +interface LogEntryContextMenuProps { + 'aria-label'?: string; + isOpen: boolean; + onOpen: () => void; + onClose: () => void; + items: LogEntryContextMenuItem[]; +} + +const DEFAULT_MENU_LABEL = i18n.translate( + 'xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', + { + defaultMessage: 'View actions for line', + } +); + +export const LogEntryContextMenu: React.FC = ({ + 'aria-label': ariaLabel, + isOpen, + onOpen, + onClose, + items, +}) => { + const closeMenuAndCall = useMemo(() => { + return (callback: LogEntryContextMenuItem['onClick']) => { + return () => { + onClose(); + callback(); + }; + }; + }, [onClose]); + + const button = ( + + + + ); + + const wrappedItems = useMemo(() => { + return items.map((item, i) => ( + + {item.label} + + )); + }, [items, closeMenuAndCall]); + + return ( + + + + + + + + ); +}; + +const LogEntryContextMenuContent = euiStyled(LogEntryColumnContent)` + overflow: hidden; + user-select: none; +`; + +const AbsoluteWrapper = euiStyled.div` + position: absolute; +`; + +const ButtonWrapper = euiStyled.div` + background: ${(props) => props.theme.eui.euiColorPrimary}; + border-radius: 50%; + padding: 4px; + transform: translateY(-6px); +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index 0d971151dd95c2..2d53203a60e4f0 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -5,6 +5,7 @@ */ import React, { memo, useState, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import { euiStyled } from '../../../../../observability/public'; @@ -18,11 +19,26 @@ import { import { TextScale } from '../../../../common/log_text_scale'; import { LogEntryColumn, LogEntryColumnWidths, iconColumnId } from './log_entry_column'; import { LogEntryFieldColumn } from './log_entry_field_column'; -import { LogEntryActionsColumn } from './log_entry_actions_column'; import { LogEntryMessageColumn } from './log_entry_message_column'; import { LogEntryTimestampColumn } from './log_entry_timestamp_column'; import { monospaceTextStyle, hoveredContentStyle, highlightedContentStyle } from './text_styles'; import { LogEntry, LogColumn } from '../../../../common/http_api'; +import { LogEntryContextMenu } from './log_entry_context_menu'; + +const MENU_LABEL = i18n.translate('xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', { + defaultMessage: 'View actions for line', +}); + +const LOG_DETAILS_LABEL = i18n.translate('xpack.infra.logs.logEntryActionsDetailsButton', { + defaultMessage: 'View details', +}); + +const LOG_VIEW_IN_CONTEXT_LABEL = i18n.translate( + 'xpack.infra.lobs.logEntryActionsViewInContextButton', + { + defaultMessage: 'View in context', + } +); interface LogEntryRowProps { boundingBoxRef?: React.Ref; @@ -76,6 +92,29 @@ export const LogEntryRow = memo( const hasActionViewLogInContext = hasContext && openViewLogInContext !== undefined; const hasActionsMenu = hasActionFlyoutWithItem || hasActionViewLogInContext; + const menuItems = useMemo(() => { + const items = []; + if (hasActionFlyoutWithItem) { + items.push({ + label: LOG_DETAILS_LABEL, + onClick: openFlyout, + }); + } + if (hasActionViewLogInContext) { + items.push({ + label: LOG_VIEW_IN_CONTEXT_LABEL, + onClick: handleOpenViewLogInContext, + }); + } + + return items; + }, [ + hasActionFlyoutWithItem, + hasActionViewLogInContext, + openFlyout, + handleOpenViewLogInContext, + ]); + const logEntryColumnsById = useMemo( () => logEntry.columns.reduce<{ @@ -183,16 +222,15 @@ export const LogEntryRow = memo( key="logColumn iconLogColumn iconLogColumn:details" {...columnWidths[iconColumnId]} > - + {isHovered || isMenuOpen ? ( + + ) : null} ) : null} diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index a04897d9c738d5..2c76b3bb925ee1 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -59,7 +59,7 @@ class WithKueryAutocompletionComponent extends React.Component< ) => { const { indexPattern } = this.props; const language = 'kuery'; - const hasQuerySuggestions = this.props.kibana.services.data.autocomplete.hasQuerySuggestions( + const hasQuerySuggestions = this.props.kibana.services.data?.autocomplete.hasQuerySuggestions( language ); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index ab7f41e3066b8c..121748f8e5220b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -31,6 +31,7 @@ import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown'; import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; +import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', { defaultMessage: 'Add data', @@ -44,114 +45,119 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { return ( - - - - - - - + + + + + + -
- - - - - - - - - - - - {ADD_DATA_LABEL} - - - - + - - - ( - - {({ configuration, createDerivedIndexPattern }) => ( - - - {configuration ? ( - - ) : ( - - )} - - )} - - )} +
- - - - - - + + + + + + + + + + + + {ADD_DATA_LABEL} + + + + + + + + ( + + {({ configuration, createDerivedIndexPattern }) => ( + + + {configuration ? ( + + ) : ( + + )} + + )} + + )} + /> + + + + + + + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts new file mode 100644 index 00000000000000..93b6b635183dda --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useWaffleFilters, WaffleFiltersState } from './use_waffle_filters'; + +// Mock useUrlState hook +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + location: '', + replace: () => {}, + }), +})); + +jest.mock('../../../../containers/source', () => ({ + useSourceContext: () => ({ + createDerivedIndexPattern: () => 'jestbeat-*', + }), +})); + +let PREFILL: Record = {}; +jest.mock('../../../../alerting/use_alert_prefill', () => ({ + useAlertPrefillContext: () => ({ + inventoryPrefill: { + setFilterQuery(filterQuery: string) { + PREFILL = { filterQuery }; + }, + }, + }), +})); + +const renderUseWaffleFiltersHook = () => renderHook(() => useWaffleFilters()); + +describe('useWaffleFilters', () => { + beforeEach(() => { + PREFILL = {}; + }); + + it('should sync the options to the inventory alert preview context', () => { + const { result, rerender } = renderUseWaffleFiltersHook(); + + const newQuery = { + expression: 'foo', + kind: 'kuery', + } as WaffleFiltersState; + act(() => { + result.current.applyFilterQuery(newQuery); + }); + rerender(); + expect(PREFILL.filterQuery).toEqual(newQuery.expression); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts index 63d9d08796f052..d4fb1356be77ef 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts @@ -10,6 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainter from 'constate'; +import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { useUrlState } from '../../../../utils/use_url_state'; import { useSourceContext } from '../../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery'; @@ -68,6 +69,10 @@ export const useWaffleFilters = () => { filterQueryDraft, ]); + const { inventoryPrefill } = useAlertPrefillContext(); + const prefillContext = useMemo(() => inventoryPrefill, [inventoryPrefill]); // For Jest compatibility + useEffect(() => prefillContext.setFilterQuery(state.expression), [prefillContext, state]); + return { filterQuery: urlState, filterQueryDraft, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts new file mode 100644 index 00000000000000..579073e9500d0c --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useWaffleOptions, WaffleOptionsState } from './use_waffle_options'; + +// Mock useUrlState hook +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + location: '', + replace: () => {}, + }), +})); + +// Jest can't access variables outside the scope of the mock factory function except to +// reassign them, so we can't make these both part of the same object +let PREFILL_NODETYPE: WaffleOptionsState['nodeType'] | undefined; +let PREFILL_METRIC: WaffleOptionsState['metric'] | undefined; +jest.mock('../../../../alerting/use_alert_prefill', () => ({ + useAlertPrefillContext: () => ({ + inventoryPrefill: { + setNodeType(nodeType: WaffleOptionsState['nodeType']) { + PREFILL_NODETYPE = nodeType; + }, + setMetric(metric: WaffleOptionsState['metric']) { + PREFILL_METRIC = metric; + }, + }, + }), +})); + +const renderUseWaffleOptionsHook = () => renderHook(() => useWaffleOptions()); + +describe('useWaffleOptions', () => { + beforeEach(() => { + PREFILL_NODETYPE = undefined; + PREFILL_METRIC = undefined; + }); + + it('should sync the options to the inventory alert preview context', () => { + const { result, rerender } = renderUseWaffleOptionsHook(); + + const newOptions = { + nodeType: 'pod', + metric: { type: 'memory' }, + } as WaffleOptionsState; + act(() => { + result.current.changeNodeType(newOptions.nodeType); + }); + rerender(); + expect(PREFILL_NODETYPE).toEqual(newOptions.nodeType); + act(() => { + result.current.changeMetric(newOptions.metric); + }); + rerender(); + expect(PREFILL_METRIC).toEqual(newOptions.metric); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts index 975e33cf2415fe..a3132c83849791 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts @@ -10,6 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainer from 'constate'; +import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { InventoryColorPaletteRT } from '../../../../lib/lib'; import { SnapshotMetricInput, @@ -121,6 +122,13 @@ export const useWaffleOptions = () => { [setState] ); + const { inventoryPrefill } = useAlertPrefillContext(); + useEffect(() => { + const { setNodeType, setMetric } = inventoryPrefill; + setNodeType(state.nodeType); + setMetric(state.metric); + }, [state, inventoryPrefill]); + return { ...DEFAULT_WAFFLE_OPTIONS_STATE, ...state, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx index 1381ed9da656a9..c35e9f17bdcc36 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx @@ -4,26 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useMetricsExplorerOptions, - MetricsExplorerOptionsContainer, MetricsExplorerOptions, MetricsExplorerTimeOptions, DEFAULT_OPTIONS, DEFAULT_TIMERANGE, } from './use_metrics_explorer_options'; -const renderUseMetricsExplorerOptionsHook = () => - renderHook(() => useMetricsExplorerOptions(), { - initialProps: {}, - wrapper: ({ children }) => ( - - {children} - - ), - }); +let PREFILL: Record = {}; +jest.mock('../../../../alerting/use_alert_prefill', () => ({ + useAlertPrefillContext: () => ({ + metricThresholdPrefill: { + setPrefillOptions(opts: Record) { + PREFILL = opts; + }, + }, + }), +})); + +const renderUseMetricsExplorerOptionsHook = () => renderHook(() => useMetricsExplorerOptions()); interface LocalStore { [key: string]: string; @@ -52,6 +53,7 @@ describe('useMetricExplorerOptions', () => { beforeEach(() => { delete STORE.MetricsExplorerOptions; delete STORE.MetricsExplorerTimeRange; + PREFILL = {}; }); it('should just work', () => { @@ -100,4 +102,22 @@ describe('useMetricExplorerOptions', () => { const { result } = renderUseMetricsExplorerOptionsHook(); expect(result.current.options).toEqual(newOptions); }); + + it('should sync the options to the threshold alert preview context', () => { + const { result, rerender } = renderUseMetricsExplorerOptionsHook(); + + const newOptions: MetricsExplorerOptions = { + ...DEFAULT_OPTIONS, + metrics: [{ aggregation: 'count' }], + filterQuery: 'foo', + groupBy: 'host.hostname', + }; + act(() => { + result.current.setOptions(newOptions); + }); + rerender(); + expect(PREFILL.metrics).toEqual(newOptions.metrics); + expect(PREFILL.groupBy).toEqual(newOptions.groupBy); + expect(PREFILL.filterQuery).toEqual(newOptions.filterQuery); + }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 56595c09aadded..8abdffd39ed3aa 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -5,7 +5,8 @@ */ import createContainer from 'constate'; -import { useState, useEffect, Dispatch, SetStateAction } from 'react'; +import { useState, useEffect, useMemo, Dispatch, SetStateAction } from 'react'; +import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { MetricsExplorerColor } from '../../../../../common/color_palette'; import { MetricsExplorerAggregation, @@ -122,6 +123,21 @@ export const useMetricsExplorerOptions = () => { DEFAULT_CHART_OPTIONS ); const [isAutoReloading, setAutoReloading] = useState(false); + + const { metricThresholdPrefill } = useAlertPrefillContext(); + // For Jest compatibility; including metricThresholdPrefill as a dep in useEffect causes an + // infinite loop in test environment + const prefillContext = useMemo(() => metricThresholdPrefill, [metricThresholdPrefill]); + + useEffect(() => { + if (prefillContext) { + const { setPrefillOptions } = prefillContext; + const { metrics, groupBy, filterQuery } = options; + + setPrefillOptions({ metrics, groupBy, filterQuery }); + } + }, [options, prefillContext]); + return { defaultViewState: { options: DEFAULT_OPTIONS, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 7939feed801430..6f23c0ce608509 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -23,14 +23,14 @@ import { CreateDatasourceFrom } from '../types'; export const CreateDatasourcePageLayout: React.FunctionComponent<{ from: CreateDatasourceFrom; cancelUrl: string; - cancelOnClick?: React.ReactEventHandler; + onCancel?: React.ReactEventHandler; agentConfig?: AgentConfig; packageInfo?: PackageInfo; 'data-test-subj'?: string; }> = ({ from, cancelUrl, - cancelOnClick, + onCancel, agentConfig, packageInfo, children, @@ -45,7 +45,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ iconType="arrowLeft" flush="left" href={cancelUrl} - onClick={cancelOnClick} + onClick={onCancel} data-test-subj={`${dataTestSubj}_cancelBackLink`} > { const layoutProps = { from, cancelUrl, - cancelOnClick: cancelClickHandler, + onCancel: cancelClickHandler, agentConfig, packageInfo, }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx index d47eea80da8b72..af39cb87f18c93 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -242,7 +242,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { }; return ( - + {isLoadingData ? ( ) : loadingError || !agentConfig || !packageInfo ? ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 281a8d3a9745c3..75d05567551491 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -489,6 +489,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { className="fleet__agentList__table" + data-test-subj="fleetAgentListTable" loading={isLoading && agentsRequest.isInitialRequest} hasActions={true} noItemsMessage={ diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 703ddb521c8317..482fe181e2b7e5 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -246,7 +246,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { internal: { type: 'boolean' }, removable: { type: 'boolean' }, es_index_patterns: { - dynamic: 'false', + enabled: false, type: 'object', }, installed: { diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 1d795c370dc00b..ea722c18e7005b 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -25,7 +25,6 @@ export const EMS_TILES_VECTOR_TILE_PATH = 'vector/tile'; export const MAP_SAVED_OBJECT_TYPE = 'map'; export const APP_ID = 'maps'; export const APP_ICON = 'gisApp'; -export const TELEMETRY_TYPE = APP_ID; export const MAP_APP_PATH = `app/${APP_ID}`; export const GIS_API_PATH = `api/${APP_ID}`; diff --git a/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts b/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts index 383d7773663c68..f54776f5ab629f 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts @@ -6,8 +6,6 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { getMapsTelemetry } from '../maps_telemetry'; -// @ts-ignore -import { TELEMETRY_TYPE } from '../../../common/constants'; import { MapsConfigType } from '../../../config'; export function registerMapsUsageCollector( @@ -19,7 +17,7 @@ export function registerMapsUsageCollector( } const mapsUsageCollector = usageCollection.makeUsageCollector({ - type: TELEMETRY_TYPE, + type: 'maps', isReady: () => true, fetch: async () => await getMapsTelemetry(config), }); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index f2331b9a1a9600..fe2b73df7978f5 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -15,7 +15,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, createMapPath } from '../common/constants'; -import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; +import { mapSavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore import { setInternalRepository } from './kibana_server_services'; @@ -191,7 +191,6 @@ export class MapsPlugin implements Plugin { }, }); - core.savedObjects.registerType(mapsTelemetrySavedObjects); core.savedObjects.registerType(mapSavedObjects); registerMapsUsageCollector(usageCollection, currentConfig); diff --git a/x-pack/plugins/maps/server/saved_objects/index.ts b/x-pack/plugins/maps/server/saved_objects/index.ts index c4b779183a2dee..804d720a13ab05 100644 --- a/x-pack/plugins/maps/server/saved_objects/index.ts +++ b/x-pack/plugins/maps/server/saved_objects/index.ts @@ -3,5 +3,4 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { mapsTelemetrySavedObjects } from './maps_telemetry'; export { mapSavedObjects } from './map'; diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts deleted file mode 100644 index ad0b17af36ddab..00000000000000 --- a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { SavedObjectsType } from 'src/core/server'; - -export const mapsTelemetrySavedObjects: SavedObjectsType = { - name: 'maps', - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - settings: { - properties: { - showMapVisualizationTypes: { type: 'boolean' }, - }, - }, - indexPatternsWithGeoFieldCount: { type: 'long' }, - indexPatternsWithGeoPointFieldCount: { type: 'long' }, - indexPatternsWithGeoShapeFieldCount: { type: 'long' }, - mapsTotalCount: { type: 'long' }, - timeCaptured: { type: 'date' }, - attributesPerMap: { - properties: { - dataSourcesCount: { - properties: { - min: { type: 'long' }, - max: { type: 'long' }, - avg: { type: 'long' }, - }, - }, - layersCount: { - properties: { - min: { type: 'long' }, - max: { type: 'long' }, - avg: { type: 'long' }, - }, - }, - layerTypesCount: { dynamic: 'true', properties: {} }, - emsVectorLayersCount: { dynamic: 'true', properties: {} }, - }, - }, - }, - }, -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index c0b7d63e623ce2..07442124959d0d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -48,7 +48,7 @@ export const Page: FC = () => {

  { + describe('APM', () => { + registerDataHandler({ + appName: 'apm', + fetchData: async () => { + return { + title: 'apm', + appLink: '/apm', + stats: { + services: { + label: 'services', + type: 'number', + value: 1, + }, + transactions: { + label: 'transactions', + type: 'number', + value: 1, + }, + }, + series: { + transactions: { + label: 'transactions', + coordinates: [{ x: 1 }], + }, + }, + }; + }, + hasData: async () => true, + }); + + it('registered data handler', () => { + const dataHandler = getDataHandler('apm'); + expect(dataHandler?.fetchData).toBeDefined(); + expect(dataHandler?.hasData).toBeDefined(); + }); + + it('returns data when fetchData is called', async () => { + const dataHandler = getDataHandler('apm'); + const response = await dataHandler?.fetchData(params); + expect(response).toEqual({ + title: 'apm', + appLink: '/apm', + stats: { + services: { + label: 'services', + type: 'number', + value: 1, + }, + transactions: { + label: 'transactions', + type: 'number', + value: 1, + }, + }, + series: { + transactions: { + label: 'transactions', + coordinates: [{ x: 1 }], + }, + }, + }); + }); + + it('returns true when hasData is called', async () => { + const dataHandler = getDataHandler('apm'); + const hasData = await dataHandler?.hasData(); + expect(hasData).toBeTruthy(); + }); + }); + describe('Logs', () => { + registerDataHandler({ + appName: 'infra_logs', + fetchData: async () => { + return { + title: 'logs', + appLink: '/logs', + stats: { + foo: { + label: 'Foo', + type: 'number', + value: 1, + }, + bar: { + label: 'bar', + type: 'number', + value: 1, + }, + }, + series: { + foo: { + label: 'Foo', + coordinates: [{ x: 1 }], + }, + bar: { + label: 'Bar', + coordinates: [{ x: 1 }], + }, + }, + }; + }, + hasData: async () => true, + }); + + it('registered data handler', () => { + const dataHandler = getDataHandler('infra_logs'); + expect(dataHandler?.fetchData).toBeDefined(); + expect(dataHandler?.hasData).toBeDefined(); + }); + + it('returns data when fetchData is called', async () => { + const dataHandler = getDataHandler('infra_logs'); + const response = await dataHandler?.fetchData(params); + expect(response).toEqual({ + title: 'logs', + appLink: '/logs', + stats: { + foo: { + label: 'Foo', + type: 'number', + value: 1, + }, + bar: { + label: 'bar', + type: 'number', + value: 1, + }, + }, + series: { + foo: { + label: 'Foo', + coordinates: [{ x: 1 }], + }, + bar: { + label: 'Bar', + coordinates: [{ x: 1 }], + }, + }, + }); + }); + + it('returns true when hasData is called', async () => { + const dataHandler = getDataHandler('apm'); + const hasData = await dataHandler?.hasData(); + expect(hasData).toBeTruthy(); + }); + }); + describe('Uptime', () => { + registerDataHandler({ + appName: 'uptime', + fetchData: async () => { + return { + title: 'uptime', + appLink: '/uptime', + stats: { + monitors: { + label: 'Monitors', + type: 'number', + value: 1, + }, + up: { + label: 'Up', + type: 'number', + value: 1, + }, + down: { + label: 'Down', + type: 'number', + value: 1, + }, + }, + series: { + down: { + label: 'Down', + coordinates: [{ x: 1 }], + }, + up: { + label: 'Up', + coordinates: [{ x: 1 }], + }, + }, + }; + }, + hasData: async () => true, + }); + + it('registered data handler', () => { + const dataHandler = getDataHandler('uptime'); + expect(dataHandler?.fetchData).toBeDefined(); + expect(dataHandler?.hasData).toBeDefined(); + }); + + it('returns data when fetchData is called', async () => { + const dataHandler = getDataHandler('uptime'); + const response = await dataHandler?.fetchData(params); + expect(response).toEqual({ + title: 'uptime', + appLink: '/uptime', + stats: { + monitors: { + label: 'Monitors', + type: 'number', + value: 1, + }, + up: { + label: 'Up', + type: 'number', + value: 1, + }, + down: { + label: 'Down', + type: 'number', + value: 1, + }, + }, + series: { + down: { + label: 'Down', + coordinates: [{ x: 1 }], + }, + up: { + label: 'Up', + coordinates: [{ x: 1 }], + }, + }, + }); + }); + + it('returns true when hasData is called', async () => { + const dataHandler = getDataHandler('apm'); + const hasData = await dataHandler?.hasData(); + expect(hasData).toBeTruthy(); + }); + }); + describe('Metrics', () => { + registerDataHandler({ + appName: 'infra_metrics', + fetchData: async () => { + return { + title: 'metrics', + appLink: '/metrics', + stats: { + hosts: { + label: 'hosts', + type: 'number', + value: 1, + }, + cpu: { + label: 'cpu', + type: 'number', + value: 1, + }, + memory: { + label: 'memory', + type: 'number', + value: 1, + }, + disk: { + label: 'disk', + type: 'number', + value: 1, + }, + inboundTraffic: { + label: 'inboundTraffic', + type: 'number', + value: 1, + }, + outboundTraffic: { + label: 'outboundTraffic', + type: 'number', + value: 1, + }, + }, + series: { + inboundTraffic: { + label: 'inbound Traffic', + coordinates: [{ x: 1 }], + }, + outboundTraffic: { + label: 'outbound Traffic', + coordinates: [{ x: 1 }], + }, + }, + }; + }, + hasData: async () => true, + }); + + it('registered data handler', () => { + const dataHandler = getDataHandler('infra_metrics'); + expect(dataHandler?.fetchData).toBeDefined(); + expect(dataHandler?.hasData).toBeDefined(); + }); + + it('returns data when fetchData is called', async () => { + const dataHandler = getDataHandler('infra_metrics'); + const response = await dataHandler?.fetchData(params); + expect(response).toEqual({ + title: 'metrics', + appLink: '/metrics', + stats: { + hosts: { + label: 'hosts', + type: 'number', + value: 1, + }, + cpu: { + label: 'cpu', + type: 'number', + value: 1, + }, + memory: { + label: 'memory', + type: 'number', + value: 1, + }, + disk: { + label: 'disk', + type: 'number', + value: 1, + }, + inboundTraffic: { + label: 'inboundTraffic', + type: 'number', + value: 1, + }, + outboundTraffic: { + label: 'outboundTraffic', + type: 'number', + value: 1, + }, + }, + series: { + inboundTraffic: { + label: 'inbound Traffic', + coordinates: [{ x: 1 }], + }, + outboundTraffic: { + label: 'outbound Traffic', + coordinates: [{ x: 1 }], + }, + }, + }); + }); + + it('returns true when hasData is called', async () => { + const dataHandler = getDataHandler('apm'); + const hasData = await dataHandler?.hasData(); + expect(hasData).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index 8f80f79b2e829a..288da3d78bf36b 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -19,25 +19,27 @@ interface FetchDataParams { export type FetchData = ( fetchDataParams: FetchDataParams ) => Promise; + export type HasData = () => Promise; -interface DataHandler { - fetchData: FetchData; +interface DataHandler { + fetchData: FetchData; hasData: HasData; } const dataHandlers: Partial> = {}; -export type RegisterDataHandler = (params: { - appName: T; - fetchData: FetchData; - hasData: HasData; -}) => void; - -export const registerDataHandler: RegisterDataHandler = ({ appName, fetchData, hasData }) => { +export function registerDataHandler({ + appName, + fetchData, + hasData, +}: { appName: T } & DataHandler) { dataHandlers[appName] = { fetchData, hasData }; -}; +} -export function getDataHandler(appName: ObservabilityApp): DataHandler | undefined { - return dataHandlers[appName]; +export function getDataHandler(appName: T) { + const dataHandler = dataHandlers[appName]; + if (dataHandler) { + return dataHandler as DataHandler; + } } diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index ade347c79728d2..fcb569f535d763 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -5,15 +5,15 @@ */ import { PluginInitializerContext, PluginInitializer } from 'kibana/public'; -import { Plugin, ObservabilityPluginSetup, ObservabilityPluginStart } from './plugin'; +import { Plugin, ObservabilityPluginSetup } from './plugin'; -export const plugin: PluginInitializer = ( +export const plugin: PluginInitializer = ( context: PluginInitializerContext ) => { return new Plugin(context); }; -export { ObservabilityPluginSetup, ObservabilityPluginStart }; +export { ObservabilityPluginSetup }; export * from './components/action_menu'; diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 16adf88d152c52..c20e8c7b75d49d 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -10,15 +10,13 @@ import { Plugin as PluginClass, PluginInitializerContext, } from '../../../../src/core/public'; -import { RegisterDataHandler, registerDataHandler } from './data_handler'; +import { registerDataHandler } from './data_handler'; export interface ObservabilityPluginSetup { - dashboard: { register: RegisterDataHandler }; + dashboard: { register: typeof registerDataHandler }; } -export type ObservabilityPluginStart = void; - -export class Plugin implements PluginClass { +export class Plugin implements PluginClass { constructor(context: PluginInitializerContext) {} public setup(core: CoreSetup) { diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index e311e358e61460..984cd7d2506a97 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -7,5 +7,6 @@ export const eventsIndexPattern = 'logs-endpoint.events.*'; export const alertsIndexPattern = 'logs-endpoint.alerts-*'; export const metadataIndexPattern = 'metrics-endpoint.metadata-*'; +export const metadataMirrorIndexPattern = 'metrics-endpoint.metadata_mirror-*'; export const policyIndexPattern = 'metrics-endpoint.policy-*'; export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index ef9e8376827a0c..5af34b6a694e83 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -14,6 +14,7 @@ import { HostPolicyResponse, HostPolicyResponseActionStatus, PolicyData, + EndpointStatus, } from './types'; import { factory as policyFactory } from './models/policy_config'; @@ -209,6 +210,7 @@ interface HostInfo { }; host: Host; Endpoint: { + status: EndpointStatus; policy: { applied: { id: string; @@ -305,7 +307,7 @@ export class EndpointDocGenerator { * Creates new random policy id for the host to simulate new policy application */ public updatePolicyId() { - this.commonInfo.Endpoint.policy.applied = this.randomChoice(APPLIED_POLICIES); + this.commonInfo.Endpoint.policy.applied.id = this.randomChoice(APPLIED_POLICIES).id; this.commonInfo.Endpoint.policy.applied.status = this.randomChoice([ HostPolicyResponseActionStatus.success, HostPolicyResponseActionStatus.failure, @@ -333,6 +335,7 @@ export class EndpointDocGenerator { os: this.randomChoice(OS), }, Endpoint: { + status: EndpointStatus.enrolled, policy: { applied: this.randomChoice(APPLIED_POLICIES), }, diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index f8cfb8f7c3bbce..42f5f4b220da95 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -74,7 +74,7 @@ export interface ResolverNodeStats { /** * A child node can also have additional children so we need to provide a pagination cursor. */ -export interface ChildNode extends LifecycleNode { +export interface ResolverChildNode extends ResolverLifecycleNode { /** * A child node's pagination cursor can be null for a couple reasons: * 1. At the time of querying it could have no children in ES, in which case it will be marked as @@ -89,7 +89,7 @@ export interface ChildNode extends LifecycleNode { * has an array of lifecycle events. */ export interface ResolverChildren { - childNodes: ChildNode[]; + childNodes: ResolverChildNode[]; /** * This is the children cursor for the origin of a tree. */ @@ -116,7 +116,7 @@ export interface ResolverTree { /** * The lifecycle events (start, end etc) for a node. */ -export interface LifecycleNode { +export interface ResolverLifecycleNode { entityID: string; lifecycle: ResolverEvent[]; /** @@ -132,7 +132,7 @@ export interface ResolverAncestry { /** * An array of ancestors with the lifecycle events grouped together */ - ancestors: LifecycleNode[]; + ancestors: ResolverLifecycleNode[]; /** * A cursor for retrieving additional ancestors for a particular node. `null` indicates that there were no additional * ancestors when the request returned. More could have been ingested by ES after the fact though. @@ -350,7 +350,23 @@ export interface AlertEvent { } /** - * The status of the host + * The status of the Endpoint Agent as reported by the Agent or the + * Security Solution app using events from Fleet. + */ +export enum EndpointStatus { + /** + * Agent is enrolled with Fleet + */ + enrolled = 'enrolled', + + /** + * Agent is unenrrolled from Fleet + */ + unenrolled = 'unenrolled', +} + +/** + * The status of the host, which is mapped to the Elastic Agent status in Fleet */ export enum HostStatus { /** @@ -386,6 +402,7 @@ export type HostMetadata = Immutable<{ }; }; Endpoint: { + status: EndpointStatus; policy: { applied: { id: string; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx index 62b942d03591c1..d033bc25e98013 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx @@ -12,9 +12,10 @@ import '../../../common/mock/match_media'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { DetectionEnginePageComponent } from './detection_engine'; import { useUserInfo } from '../../components/user_info'; +import { useWithSource } from '../../../common/containers/source'; jest.mock('../../components/user_info'); -jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/containers/source'); jest.mock('../../../common/components/link_to'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -30,7 +31,12 @@ describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); (useUserInfo as jest.Mock).mockReturnValue({}); + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); }); + it('renders correctly', () => { const wrapper = shallow( { /> ); - expect(wrapper.find('WithSource')).toHaveLength(1); + expect(wrapper.find('FiltersGlobal')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx index 05a0b4441bb3a6..dc0b22c82af3ea 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx @@ -13,10 +13,7 @@ import { useHistory } from 'react-router-dom'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { GlobalTime } from '../../../common/containers/global_time'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../../common/containers/source'; +import { useWithSource } from '../../../common/containers/source'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { FiltersGlobal } from '../../../common/components/filters_global'; import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine'; @@ -82,6 +79,7 @@ export const DetectionEnginePageComponent: React.FC = ({ const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ signalIndexName, ]); + const { indicesExist, indexPattern } = useWithSource('default', indexToAdd); if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( @@ -104,77 +102,73 @@ export const DetectionEnginePageComponent: React.FC = ({ <> {hasEncryptionKey != null && !hasEncryptionKey && } {hasIndexWrite != null && !hasIndexWrite && } - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - {i18n.LAST_ALERT} - {': '} - {lastAlerts} - - ) - } - title={i18n.PAGE_TITLE} - > - - {i18n.BUTTON_MANAGE_RULES} - - + {indicesExist ? ( + + + + + + + {i18n.LAST_ALERT} + {': '} + {lastAlerts} + + ) + } + title={i18n.PAGE_TITLE} + > + + {i18n.BUTTON_MANAGE_RULES} + + - - {({ to, from, deleteQuery, setQuery }) => ( - <> - <> - - - - - - )} - - - - ) : ( - - - - - ); - }} - + + {({ to, from, deleteQuery, setQuery }) => ( + <> + <> + + + + + + )} + + + + ) : ( + + + + + )} ); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx index df6ea65ba52ba5..0acb18082379ae 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx @@ -12,10 +12,12 @@ import { TestProviders } from '../../../../../common/mock'; import { RuleDetailsPageComponent } from './index'; import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; import { useUserInfo } from '../../../../components/user_info'; +import { useWithSource } from '../../../../../common/containers/source'; import { useParams } from 'react-router-dom'; jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); +jest.mock('../../../../../common/containers/source'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -30,6 +32,10 @@ describe('RuleDetailsPageComponent', () => { beforeAll(() => { (useUserInfo as jest.Mock).mockReturnValue({}); (useParams as jest.Mock).mockReturnValue({}); + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); }); it('renders correctly', () => { @@ -44,6 +50,6 @@ describe('RuleDetailsPageComponent', () => { } ); - expect(wrapper.find('WithSource')).toHaveLength(1); + expect(wrapper.find('GlobalTime')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx index 90fd4bb225ec5a..2ec603546983e4 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react-hooks/rules-of-hooks */ -/* eslint-disable complexity */ +/* eslint-disable react-hooks/rules-of-hooks, complexity */ // TODO: Disabling complexity is temporary till this component is refactored as part of lists UI integration import { @@ -36,10 +35,7 @@ import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { useRule } from '../../../../../alerts/containers/detection_engine/rules'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../../../../common/containers/source'; +import { useWithSource } from '../../../../../common/containers/source'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details'; @@ -255,6 +251,8 @@ export const RuleDetailsPageComponent: FC = ({ [history, ruleId] ); + const { indicesExist, indexPattern } = useWithSource('default', indexToAdd); + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { history.replace(getDetectionEngineUrl()); return null; @@ -264,187 +262,185 @@ export const RuleDetailsPageComponent: FC = ({ <> {hasIndexWrite != null && !hasIndexWrite && } {userHasNoPermissions(canUserCRUD) && } - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {({ to, from, deleteQuery, setQuery }) => ( - - - - - - - - {detectionI18n.LAST_ALERT} - {': '} - {lastAlerts} - , - ] - : []), - , - ]} - title={title} - > - + {indicesExist ? ( + + {({ to, from, deleteQuery, setQuery }) => ( + + + + + + + + {detectionI18n.LAST_ALERT} + {': '} + {lastAlerts} + , + ] + : []), + , + ]} + title={title} + > + + + + + + + + + - - - + {ruleI18n.EDIT_RULE_SETTINGS} + - - - - - {ruleI18n.EDIT_RULE_SETTINGS} - - - - - - + - - {ruleError} - - - - - + + + + {ruleError} + + + + + - - - - - {defineRuleData != null && ( - - )} - - - - - - {scheduleRuleData != null && ( - - )} - - - + + + + + {defineRuleData != null && ( + + )} + + + + + + {scheduleRuleData != null && ( + + )} + + + + + {tabs} + + {ruleDetailTab === RuleDetailTabs.alerts && ( + <> + - {tabs} - - {ruleDetailTab === RuleDetailTabs.alerts && ( - <> - - - {ruleId != null && ( - - )} - - )} - {ruleDetailTab === RuleDetailTabs.exceptions && ( - )} - {ruleDetailTab === RuleDetailTabs.failures && } - - - )} - - ) : ( - - + + )} + {ruleDetailTab === RuleDetailTabs.exceptions && ( + + )} + {ruleDetailTab === RuleDetailTabs.failures && } + + + )} + + ) : ( + + - - - ); - }} - + + + )} ); }; +RuleDetailsPageComponent.displayName = 'RuleDetailsPageComponent'; + const makeMapStateToProps = () => { const getGlobalInputs = inputsSelectors.globalSelector(); return (state: State) => { @@ -467,3 +463,5 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; export const RuleDetailsPage = connector(memo(RuleDetailsPageComponent)); + +RuleDetailsPage.displayName = 'RuleDetailsPage'; diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index d8bdbd6e7ef5fb..03e48282cb754d 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -14,10 +14,7 @@ import { HeaderGlobal } from '../../common/components/header_global'; import { HelpMenu } from '../../common/components/help_menu'; import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning'; import { UseUrlState } from '../../common/components/url_state'; -import { - WithSource, - indicesExistOrDataTemporarilyUnavailable, -} from '../../common/containers/source'; +import { useWithSource } from '../../common/containers/source'; import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; @@ -60,31 +57,28 @@ export const HomePage: React.FC = ({ children }) => { ); const [showTimeline] = useShowTimeline(); + const { browserFields, indexPattern, indicesExist } = useWithSource(); return (
- - {({ browserFields, indexPattern, indicesExist }) => ( - - - {indicesExistOrDataTemporarilyUnavailable(indicesExist) && showTimeline && ( - <> - - - - )} - - {children} - + + + {indicesExist && showTimeline && ( + <> + + + )} - + + {children} +
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index e60d876617dcaa..ee1dc73b27fe2b 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -6,10 +6,9 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { mocksSource } from '../../containers/source/mock'; -import { wait } from '../../lib/helpers'; +import { useWithSource } from '../../containers/source'; +import { mockBrowserFields } from '../../containers/source/mock'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; @@ -21,10 +20,19 @@ import { ManageGlobalTimeline, timelineDefaults, } from '../../../timelines/components/manage_timeline'; +import { TimelineId } from '../../../../common/types/timeline'; jest.mock('../link_to'); jest.mock('../../lib/kibana'); +jest.mock('../../containers/source', () => { + const original = jest.requireActual('../../containers/source'); + + return { + ...original, + useWithSource: jest.fn(), + }; +}); jest.mock('uuid', () => { return { @@ -34,9 +42,24 @@ jest.mock('uuid', () => { }); jest.mock('../../hooks/use_add_to_timeline'); +const mockAddFilters = jest.fn(); +const mockGetTimelineFilterManager = jest.fn().mockReturnValue({ + addFilters: mockAddFilters, +}); +jest.mock('../../../timelines/components/manage_timeline', () => { + const original = jest.requireActual('../../../timelines/components/manage_timeline'); + + return { + ...original, + useManageTimeline: () => ({ + getTimelineFilterManager: mockGetTimelineFilterManager, + isManagedTimeline: jest.fn().mockReturnValue(false), + }), + }; +}); const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; -const timelineId = 'cool-id'; +const timelineId = TimelineId.active; const field = 'process.name'; const value = 'nice'; const toggleTopN = jest.fn(); @@ -52,6 +75,9 @@ describe('DraggableWrapperHoverContent', () => { beforeAll(() => { // our mock implementation of the useAddToTimeline hook returns a mock startDragToTimeline function: (useAddToTimeline as jest.Mock).mockReturnValue(jest.fn()); + (useWithSource as jest.Mock).mockReturnValue({ + browserFields: mockBrowserFields, + }); }); // Suppress warnings about "react-beautiful-dnd" @@ -78,6 +104,9 @@ describe('DraggableWrapperHoverContent', () => { forOrOut.forEach((hoverAction) => { describe(`Filter ${hoverAction} value`, () => { + beforeEach(() => { + jest.clearAllMocks(); + }); test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => { const wrapper = mount( @@ -101,21 +130,16 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists() ).toBe(false); }); - describe('when run in the context of a timeline', () => { - let filterManager: FilterManager; let wrapper: ReactWrapper; let onFilterAdded: () => void; beforeEach(() => { - filterManager = new FilterManager(mockUiSettingsForFilterManager); - filterManager.addFilters = jest.fn(); onFilterAdded = jest.fn(); const manageTimelineForTesting = { [timelineId]: { ...timelineDefaults, id: timelineId, - filterManager, }, }; @@ -131,7 +155,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); - expect(filterManager.addFilters).toBeCalledWith({ + expect(mockAddFilters).toBeCalledWith({ meta: { alias: null, disabled: false, @@ -164,7 +188,9 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -253,7 +279,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); - expect(filterManager.addFilters).toBeCalledWith(expected); + expect(mockAddFilters).toBeCalledWith(expected); }); }); @@ -268,7 +294,14 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -323,17 +356,15 @@ describe('DraggableWrapperHoverContent', () => { test(`it ${assertion} the 'Add to timeline investigation' button when showTopN is ${showTopN}, value is ${maybeValue}, and a draggableId is ${maybeDraggableId}`, () => { const wrapper = mount( - - - + ); @@ -348,15 +379,13 @@ describe('DraggableWrapperHoverContent', () => { test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', () => { const wrapper = mount( - - - + ); @@ -380,18 +409,15 @@ describe('DraggableWrapperHoverContent', () => { const aggregatableStringField = 'cloud.account.id'; const wrapper = mount( - - - + ); - await wait(); // https://github.com/apollographql/react-apollo/issues/1711 wrapper.update(); expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); @@ -401,18 +427,15 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); @@ -422,18 +445,15 @@ describe('DraggableWrapperHoverContent', () => { const notKnownToBrowserFields = 'unknown.field'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); @@ -443,18 +463,15 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); wrapper.find('[data-test-subj="show-top-field"]').first().simulate('click'); @@ -467,18 +484,15 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect(wrapper.find('[data-test-subj="eventsByDatasetOverviewPanel"]').first().exists()).toBe( @@ -490,19 +504,16 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); @@ -512,19 +523,16 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect( @@ -559,4 +567,41 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false); }); }); + + describe('Filter Manager', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('filter manager, not active timeline', () => { + mount( + + + + ); + + expect(mockGetTimelineFilterManager).not.toBeCalled(); + }); + test('filter manager, active timeline', () => { + mount( + + + + ); + + expect(mockGetTimelineFilterManager).toBeCalled(); + }); + test('filter manager, active timeline in draggableId', () => { + mount( + + + + ); + + expect(mockGetTimelineFilterManager).toBeCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index f916f42fe41cdc..4efdea5eee43b6 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -8,16 +8,17 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { DraggableId } from 'react-beautiful-dnd'; -import { getAllFieldsByName, WithSource } from '../../containers/source'; +import { getAllFieldsByName, useWithSource } from '../../containers/source'; import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; import { createFilter } from '../add_filter_to_global_search_bar'; -import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '../top_n'; +import { StatefulTopN } from '../top_n'; import { allowTopN } from './helpers'; import * as i18n from './translations'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { TimelineId } from '../../../../common/types/timeline'; interface Props { draggableId?: DraggableId; @@ -34,7 +35,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ field, onFilterAdded, showTopN, - timelineId = ACTIVE_TIMELINE_REDUX_ID, + timelineId, toggleTopN, value, }) => { @@ -44,11 +45,16 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ kibana.services.data.query.filterManager, ]); const { getTimelineFilterManager } = useManageTimeline(); - const filterManager = useMemo(() => getTimelineFilterManager(timelineId) ?? filterManagerBackup, [ - timelineId, - getTimelineFilterManager, - filterManagerBackup, - ]); + + const filterManager = useMemo( + () => + timelineId === TimelineId.active || + (draggableId != null && draggableId?.includes(TimelineId.active)) + ? getTimelineFilterManager(TimelineId.active) + : filterManagerBackup, + [draggableId, timelineId, getTimelineFilterManager, filterManagerBackup] + ); + const filterForValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); @@ -79,6 +85,8 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [field, value, filterManager, onFilterAdded]); + const { browserFields } = useWithSource(); + return ( <> {!showTopN && value != null && ( @@ -117,40 +125,36 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ )} - - {({ browserFields }) => ( + <> + {allowTopN({ + browserField: getAllFieldsByName(browserFields)[field], + fieldName: field, + }) && ( <> - {allowTopN({ - browserField: getAllFieldsByName(browserFields)[field], - fieldName: field, - }) && ( - <> - {!showTopN && ( - - - - )} - - {showTopN && ( - - )} - + {!showTopN && ( + + + + )} + + {showTopN && ( + )} )} - + {!showTopN && ( diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index de19c1903586ae..17fdf2163b58ed 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -16,7 +16,7 @@ import { getAppOverviewUrl } from '../link_to'; import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; +import { useWithSource } from '../../containers/source'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { useKibana } from '../../lib/kibana'; import { APP_ID, ADD_DATA_PATH, APP_ALERTS_PATH } from '../../../../common/constants'; @@ -41,6 +41,7 @@ interface HeaderGlobalProps { hideDetectionEngine?: boolean; } export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { + const { indicesExist } = useWithSource(); const search = useGetUrlSearch(navTabs.overview); const { navigateToApp } = useKibana().services.application; const goToOverview = useCallback( @@ -54,60 +55,55 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine return ( - - {({ indicesExist }) => ( - <> - - - - - - - + <> + + + + + + + - - {indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - key !== SecurityPageName.alerts, navTabs) - : navTabs - } - /> - ) : ( - key === SecurityPageName.overview, navTabs)} - /> - )} - - + + {indicesExist ? ( + key !== SecurityPageName.alerts, navTabs) + : navTabs + } + /> + ) : ( + key === SecurityPageName.overview, navTabs)} + /> + )} + + - - - {indicesExistOrDataTemporarilyUnavailable(indicesExist) && - window.location.pathname.includes(APP_ALERTS_PATH) && ( - - - - )} + + + {indicesExist && window.location.pathname.includes(APP_ALERTS_PATH) && ( + + + + )} - - - {i18n.BUTTON_ADD_DATA} - - - + + + {i18n.BUTTON_ADD_DATA} + - - )} - + + + ); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index c28f5ab8aa44f0..09da027569c61a 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -8,7 +8,7 @@ import React, { useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { GlobalTime } from '../../containers/global_time'; -import { BrowserFields, WithSource } from '../../containers/source'; +import { BrowserFields, useWithSource } from '../../containers/source'; import { useKibana } from '../../lib/kibana'; import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/public'; import { inputsModel, inputsSelectors, State } from '../../store'; @@ -99,7 +99,7 @@ const StatefulTopNComponent: React.FC = ({ // * `id` (`timelineId`) may only be populated when we are rendered in the // context of the active timeline. // * `indexToAdd`, which enables the alerts index to be appended to - // the `indexPattern` returned by `WithSource`, may only be populated when + // the `indexPattern` returned by `useWithSource`, may only be populated when // this component is rendered in the context of the active timeline. This // behavior enables the 'All events' view by appending the alerts index // to the index pattern. @@ -117,54 +117,50 @@ const StatefulTopNComponent: React.FC = ({ timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined ); + const { indexPattern } = useWithSource('default', indexToAdd); + return ( {({ from, deleteQuery, setQuery, to }) => ( - - {({ indexPattern }) => ( - - )} - + )} ); diff --git a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx index 9b9b5c5d815b99..9c9778c7074ee1 100644 --- a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx @@ -94,3 +94,5 @@ export const connector = connect(mapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; export const GlobalTime = connector(React.memo(GlobalTimeComponent)); + +GlobalTime.displayName = 'GlobalTime'; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index d1a183a402e371..c30c3668638a3f 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -4,55 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; -import { mount } from 'enzyme'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; +import { act, renderHook } from '@testing-library/react-hooks'; -import { wait } from '../../lib/helpers'; - -import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '.'; +import { useWithSource, indicesExistOrDataTemporarilyUnavailable } from '.'; import { mockBrowserFields, mockIndexFields, mocksSource } from './mock'; jest.mock('../../lib/kibana'); +jest.mock('../../utils/apollo_context', () => ({ + useApolloClient: jest.fn().mockReturnValue({ + query: jest.fn().mockImplementation(() => Promise.resolve(mocksSource[0].result)), + }), +})); describe('Index Fields & Browser Fields', () => { - test('Index Fields', async () => { - mount( - - - {({ indexPattern }) => { - if (!isEqual(indexPattern.fields, [])) { - expect(indexPattern.fields).toEqual(mockIndexFields); - } + test('returns memoized value', async () => { + const { result, waitForNextUpdate, rerender } = renderHook(() => useWithSource()); + await waitForNextUpdate(); - return null; - }} - - - ); + const result1 = result.current; + act(() => rerender()); + const result2 = result.current; - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await wait(); + return expect(result1).toBe(result2); }); - test('Browser Fields', async () => { - mount( - - - {({ browserFields }) => { - if (!isEqual(browserFields, {})) { - expect(browserFields).toEqual(mockBrowserFields); - } + test('Index Fields', async () => { + const { result, waitForNextUpdate } = renderHook(() => useWithSource()); - return null; - }} - - - ); + await waitForNextUpdate(); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await wait(); + return expect(result).toEqual({ + current: { + indicesExist: true, + browserFields: mockBrowserFields, + indexPattern: { + fields: mockIndexFields, + title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + }, + loading: false, + errorMessage: null, + }, + error: undefined, + }); }); describe('indicesExistOrDataTemporarilyUnavailable', () => { diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index ad480ad2c496bf..34ac5f8f5d94fa 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -6,8 +6,7 @@ import { isUndefined } from 'lodash'; import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; -import { Query } from 'react-apollo'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import memoizeOne from 'memoize-one'; import { IIndexPattern } from 'src/plugins/data/public'; @@ -50,18 +49,6 @@ export const getAllFieldsByName = ( ): { [fieldName: string]: Partial } => keyBy('name', getAllBrowserFields(browserFields)); -interface WithSourceArgs { - indicesExist: boolean; - browserFields: BrowserFields; - indexPattern: IIndexPattern; -} - -interface WithSourceProps { - children: (args: WithSourceArgs) => React.ReactNode; - indexToAdd?: string[] | null; - sourceId: string; -} - export const getIndexFields = memoizeOne( (title: string, fields: IndexField[]): IIndexPattern => fields && fields.length > 0 @@ -71,7 +58,8 @@ export const getIndexFields = memoizeOne( ), title, } - : { fields: [], title } + : { fields: [], title }, + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length ); export const getBrowserFields = memoizeOne( @@ -82,10 +70,26 @@ export const getBrowserFields = memoizeOne( set([field.category, 'fields', field.name], field, accumulator), {} ) - : {} + : {}, + // Update the value only if _title has changed + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] ); -export const WithSource = React.memo(({ children, indexToAdd, sourceId }) => { +export const indicesExistOrDataTemporarilyUnavailable = ( + indicesExist: boolean | null | undefined +) => indicesExist || isUndefined(indicesExist); + +const EMPTY_BROWSER_FIELDS = {}; + +interface UseWithSourceState { + browserFields: BrowserFields; + errorMessage: string | null; + indexPattern: IIndexPattern; + indicesExist: boolean | undefined | null; + loading: boolean; +} + +export const useWithSource = (sourceId = 'default', indexToAdd?: string[] | null) => { const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const defaultIndex = useMemo(() => { if (indexToAdd != null && !isEmpty(indexToAdd)) { @@ -94,87 +98,84 @@ export const WithSource = React.memo(({ children, indexToAdd, s return configIndex; }, [configIndex, indexToAdd]); - return ( - - query={sourceQuery} - fetchPolicy="cache-first" - notifyOnNetworkStatusChange - variables={{ - sourceId, - defaultIndex, - }} - > - {({ data }) => - children({ - indicesExist: get('source.status.indicesExist', data), - browserFields: getBrowserFields( - defaultIndex.join(), - get('source.status.indexFields', data) - ), - indexPattern: getIndexFields(defaultIndex.join(), get('source.status.indexFields', data)), - }) - } - - ); -}); + const [state, setState] = useState({ + browserFields: EMPTY_BROWSER_FIELDS, + errorMessage: null, + indexPattern: getIndexFields(defaultIndex.join(), []), + indicesExist: undefined, + loading: false, + }); -WithSource.displayName = 'WithSource'; + const apolloClient = useApolloClient(); -export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => - indicesExist || isUndefined(indicesExist); + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); -export const useWithSource = (sourceId: string, indices: string[]) => { - const [loading, updateLoading] = useState(false); - const [indicesExist, setIndicesExist] = useState(undefined); - const [browserFields, setBrowserFields] = useState(null); - const [indexPattern, setIndexPattern] = useState(null); - const [errorMessage, updateErrorMessage] = useState(null); + async function fetchSource() { + if (!apolloClient) return; - const apolloClient = useApolloClient(); - async function fetchSource(signal: AbortSignal) { - updateLoading(true); - if (apolloClient) { - apolloClient - .query({ + setState((prevState) => ({ ...prevState, loading: true })); + + try { + const result = await apolloClient.query({ query: sourceQuery, fetchPolicy: 'cache-first', variables: { sourceId, - defaultIndex: indices, + defaultIndex, }, context: { fetchOptions: { - signal, + signal: abortCtrl.signal, }, }, - }) - .then( - (result) => { - updateLoading(false); - updateErrorMessage(null); - setIndicesExist(get('data.source.status.indicesExist', result)); - setBrowserFields( - getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) - ); - setIndexPattern( - getIndexFields(indices.join(), get('data.source.status.indexFields', result)) - ); - }, - (error) => { - updateLoading(false); - updateErrorMessage(error.message); - } - ); + }); + if (!isSubscribed) { + return setState((prevState) => ({ + ...prevState, + loading: false, + })); + } + + setState({ + loading: false, + indicesExist: indicesExistOrDataTemporarilyUnavailable( + get('data.source.status.indicesExist', result) + ), + browserFields: getBrowserFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), + indexPattern: getIndexFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), + errorMessage: null, + }); + } catch (error) { + if (!isSubscribed) { + return setState((prevState) => ({ + ...prevState, + loading: false, + })); + } + + setState((prevState) => ({ + ...prevState, + loading: false, + errorMessage: error.message, + })); + } } - } - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchSource(signal); - return () => abortCtrl.abort(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [apolloClient, sourceId, indices]); + fetchSource(); + + return () => { + isSubscribed = false; + return abortCtrl.abort(); + }; + }, [apolloClient, sourceId, defaultIndex]); - return { indicesExist, browserFields, indexPattern, loading, errorMessage }; + return state; }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index 936789625a4dde..e520facf285c2b 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; import { MemoryRouter } from 'react-router-dom'; import useResizeObserver from 'use-resize-observer/polyfilled'; @@ -19,12 +18,7 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { getHostDetailsPageFilters } from './helpers'; jest.mock('../../../common/containers/source', () => ({ - indicesExistOrDataTemporarilyUnavailable: () => true, - WithSource: ({ - children, - }: { - children: (args: { indicesExist: boolean; indexPattern: IIndexPattern }) => React.ReactNode; - }) => children({ indicesExist: true, indexPattern: mockIndexPattern }), + useWithSource: jest.fn().mockReturnValue({ indicesExist: true, indexPattern: mockIndexPattern }), })); // Test will fail because we will to need to mock some core services to make the test work diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index e3f00a377d2724..1c66a9edc19475 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -27,10 +27,7 @@ import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; import { HostOverviewByNameQuery } from '../../containers/hosts/overview'; import { KpiHostDetailsQuery } from '../../containers/kpi_host_details'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../../common/containers/source'; +import { useWithSource } from '../../../common/containers/source'; import { LastEventIndexKey } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; @@ -83,132 +80,126 @@ const HostDetailsComponent = React.memo( }, [setAbsoluteRangeDatePicker] ); + const { indicesExist, indexPattern } = useWithSource(); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: getFilters(), + }); return ( <> - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: getFilters(), - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - - } - title={detailName} - /> - - + + + + + + + } + title={detailName} + /> + + + {({ hostOverview, loading, id, inspect, refetch }) => ( + - {({ hostOverview, loading, id, inspect, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - - - - - - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} /> )} - - - - - - - - - + )} + + + + + + {({ kpiHostDetails, id, inspect, loading, refetch }) => ( + - - - ) : ( - - - - - - ); - }} - + )} + + + + + + + + + + + + ) : ( + + + + + + )} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index 85db3b4e159f12..ea0b32137eb395 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -5,15 +5,12 @@ */ import { mount } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; import React from 'react'; import { Router } from 'react-router-dom'; -import { MockedProvider } from 'react-apollo/test-utils'; import { Filter } from '../../../../../../src/plugins/data/common/es_query'; import '../../common/mock/match_media'; -import { mocksSource } from '../../common/containers/source/mock'; -import { wait } from '../../common/lib/helpers'; +import { useWithSource } from '../../common/containers/source'; import { apolloClientObservable, TestProviders, @@ -28,6 +25,8 @@ import { HostsComponentProps } from './types'; import { Hosts } from './hosts'; import { HostsTabs } from './hosts_tabs'; +jest.mock('../../common/containers/source'); + // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar jest.mock('../../common/components/search_bar', () => ({ @@ -37,19 +36,6 @@ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); -let localSource: Array<{ - request: {}; - result: { - data: { - source: { - status: { - indicesExist: boolean; - }; - }; - }; - }; -}>; - type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; const location = { @@ -84,57 +70,49 @@ describe('Hosts - rendering', () => { hostsPagePath: '', }; - beforeEach(() => { - localSource = cloneDeep(mocksSource); - }); - test('it renders the Setup Instructions text when no index is available', async () => { - localSource[0].result.data.source.status.indicesExist = false; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + }); + const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); }); test('it DOES NOT render the Setup Instructions text when an index is available', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); test('it should render tab navigation', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); + const wrapper = mount( - - - - - + + + ); - await wait(); - wrapper.update(); expect(wrapper.find(SiemNavigation).exists()).toBe(true); }); @@ -170,22 +148,21 @@ describe('Hosts - rendering', () => { }, }, ]; - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: { fields: [], title: 'title' }, + }); const myState: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const wrapper = mount( - - - - - + + + ); - await wait(); wrapper.update(); - myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); wrapper.update(); expect(wrapper.find(HostsTabs).props().filterQuery).toEqual( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index f6429544f855e0..f5cc651a30443d 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -22,10 +22,7 @@ import { manageQuery } from '../../common/components/page/manage_query'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { KpiHostsQuery } from '../containers/kpi_hosts'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../common/containers/source'; +import { useWithSource } from '../../common/containers/source'; import { LastEventIndexKey } from '../../graphql/types'; import { useKibana } from '../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../common/lib/keury'; @@ -77,87 +74,84 @@ export const HostsComponent = React.memo( }, [setAbsoluteRangeDatePicker] ); + const { indicesExist, indexPattern } = useWithSource(); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }); + const tabsFilterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: tabsFilters, + }); return ( <> - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); - const tabsFilterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: tabsFilters, - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - } - title={i18n.PAGE_TITLE} - /> - - - {({ kpiHosts, loading, id, inspect, refetch }) => ( - - )} - - - - - - - - - + + + + + + } + title={i18n.PAGE_TITLE} + /> + + + {({ kpiHosts, loading, id, inspect, refetch }) => ( + - - - ) : ( - - - - - - ); - }} - + )} + + + + + + + + + + + + ) : ( + + + + + + )} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/action.ts index e14e39bf45c931..b04b2f085689e7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/action.ts @@ -6,7 +6,10 @@ import { PolicyData } from '../../../../../../common/endpoint/types'; import { ServerApiError } from '../../../../../common/types'; -import { GetAgentStatusResponse } from '../../../../../../../ingest_manager/common/types/rest_spec'; +import { + GetAgentStatusResponse, + GetPackagesResponse, +} from '../../../../../../../ingest_manager/common'; interface ServerReturnedPolicyListData { type: 'serverReturnedPolicyListData'; @@ -53,6 +56,11 @@ interface ServerReturnedPolicyAgentsSummaryForDelete { payload: { agentStatusSummary: GetAgentStatusResponse['results'] }; } +interface ServerReturnedEndpointPackageInfo { + type: 'serverReturnedEndpointPackageInfo'; + payload: GetPackagesResponse['response'][0]; +} + export type PolicyListAction = | ServerReturnedPolicyListData | ServerFailedToReturnPolicyListData @@ -61,4 +69,5 @@ export type PolicyListAction = | ServerDeletedPolicy | UserOpenedPolicyListDeleteModal | ServerReturnedPolicyAgentsSummaryForDeleteFailure - | ServerReturnedPolicyAgentsSummaryForDelete; + | ServerReturnedPolicyAgentsSummaryForDelete + | ServerReturnedEndpointPackageInfo; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index c24c47becc0b53..f454061055e96d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -18,6 +18,7 @@ import { selectIsLoading, urlSearchParams, selectIsDeleting, + endpointPackageVersion, } from './selectors'; import { DepsStartMock, depsStartMock } from '../../../../../common/mock/endpoint'; import { setPolicyListApiMockImplementation } from './test_mock_utils'; @@ -254,5 +255,21 @@ describe('policy list store concerns', () => { page_size: 50, }); }); + + it('should load package information only if not already in state', async () => { + dispatchUserChangedUrl('?page_size=10&page_index=10'); + await waitForAction('serverReturnedEndpointPackageInfo'); + expect(endpointPackageVersion(store.getState())).toEqual('0.5.0'); + fakeCoreStart.http.get.mockClear(); + dispatchUserChangedUrl('?page_size=10&page_index=11'); + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 12, + perPage: 10, + }, + }); + expect(endpointPackageVersion(store.getState())).toEqual('0.5.0'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts index 39c685da3ec460..7d8620a5831d0d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts @@ -9,8 +9,9 @@ import { sendGetEndpointSpecificDatasources, sendDeleteDatasource, sendGetFleetAgentStatusForConfig, + sendGetEndpointSecurityPackage, } from './services/ingest'; -import { isOnPolicyListPage, urlSearchParams } from './selectors'; +import { endpointPackageInfo, isOnPolicyListPage, urlSearchParams } from './selectors'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; import { initialPolicyListState } from './reducer'; import { @@ -32,6 +33,25 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory { + dispatch({ + type: 'serverReturnedEndpointPackageInfo', + payload: packageInfo, + }); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error(error); + }); + } + const { page_index: pageIndex, page_size: pageSize } = urlSearchParams(state); let response: GetPolicyListResponse; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts index a8a2ad3e7cc268..52bed8d850ef42 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts @@ -16,6 +16,7 @@ import { PolicyListState } from '../../types'; */ export const initialPolicyListState: () => Immutable = () => ({ policyItems: [], + endpointPackageInfo: undefined, isLoading: false, isDeleting: false, deleteStatus: undefined, @@ -95,6 +96,13 @@ export const policyListReducer: ImmutableReducer = ( }; } + if (action.type === 'serverReturnedEndpointPackageInfo') { + return { + ...state, + endpointPackageInfo: action.payload, + }; + } + if (action.type === 'userChangedUrl') { const newState: Immutable = { ...state, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/selectors.ts index 089c97b5520a20..ce57d238d7581b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/selectors.ts @@ -84,3 +84,17 @@ export const urlSearchParams: ( return searchParams; }); + +/** + * Returns package information for Endpoint + * @param state + */ +export const endpointPackageInfo = (state: Immutable) => state.endpointPackageInfo; + +/** + * Returns the version number for the endpoint package. + */ +export const endpointPackageVersion = createSelector( + endpointPackageInfo, + (info) => info?.version ?? undefined +); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts index cbbc5c3c6fdbe8..2270c65fb149fc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sendGetDatasource, sendGetEndpointSpecificDatasources } from './ingest'; +import { + sendGetDatasource, + sendGetEndpointSecurityPackage, + sendGetEndpointSpecificDatasources, +} from './ingest'; import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common'; @@ -37,6 +41,7 @@ describe('ingest service', () => { }); }); }); + describe('sendGetDatasource()', () => { it('builds correct API path', async () => { await sendGetDatasource(http, '123'); @@ -51,4 +56,90 @@ describe('ingest service', () => { }); }); }); + + describe('sendGetEndpointSecurityPackage()', () => { + it('should query EPM with category=security', async () => { + http.get.mockResolvedValue({ + response: [ + { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + description: 'This is the Elastic Endpoint package.', + type: 'solution', + download: '/epr/endpoint/endpoint-0.5.0.tar.gz', + path: '/package/endpoint/0.5.0', + icons: [ + { + src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + size: '16x16', + type: 'image/svg+xml', + }, + ], + status: 'installed', + savedObject: { + type: 'epm-packages', + id: 'endpoint', + attributes: { + installed: [ + { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, + { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, + { id: 'logs-endpoint.alerts', type: 'index-template' }, + { id: 'events-endpoint', type: 'index-template' }, + { id: 'logs-endpoint.events.file', type: 'index-template' }, + { id: 'logs-endpoint.events.library', type: 'index-template' }, + { id: 'metrics-endpoint.metadata', type: 'index-template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index-template' }, + { id: 'logs-endpoint.events.network', type: 'index-template' }, + { id: 'metrics-endpoint.policy', type: 'index-template' }, + { id: 'logs-endpoint.events.process', type: 'index-template' }, + { id: 'logs-endpoint.events.registry', type: 'index-template' }, + { id: 'logs-endpoint.events.security', type: 'index-template' }, + { id: 'metrics-endpoint.telemetry', type: 'index-template' }, + ], + es_index_patterns: { + alerts: 'logs-endpoint.alerts-*', + events: 'events-endpoint-*', + file: 'logs-endpoint.events.file-*', + library: 'logs-endpoint.events.library-*', + metadata: 'metrics-endpoint.metadata-*', + metadata_mirror: 'metrics-endpoint.metadata_mirror-*', + network: 'logs-endpoint.events.network-*', + policy: 'metrics-endpoint.policy-*', + process: 'logs-endpoint.events.process-*', + registry: 'logs-endpoint.events.registry-*', + security: 'logs-endpoint.events.security-*', + telemetry: 'metrics-endpoint.telemetry-*', + }, + name: 'endpoint', + version: '0.5.0', + internal: false, + removable: false, + }, + references: [], + updated_at: '2020-06-24T14:41:23.098Z', + version: 'Wzc0LDFd', + score: 0, + }, + }, + ], + success: true, + }); + await sendGetEndpointSecurityPackage(http); + expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/epm/packages', { + query: { category: 'security' }, + }); + }); + + it('should throw if package is not found', async () => { + http.get.mockResolvedValue({ response: [], success: true }); + await expect(async () => { + await sendGetEndpointSecurityPackage(http); + }).rejects.toThrow(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index 66e98aa51601e7..cbdd67261739f8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -20,7 +20,7 @@ const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; -const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; +export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; const INGEST_API_DELETE_DATASOURCE = `${INGEST_API_DATASOURCES}/delete`; /** diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index 2c495202dc75b0..0f0d1cb1b559d8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -5,9 +5,14 @@ */ import { HttpStart } from 'kibana/public'; -import { INGEST_API_DATASOURCES } from './services/ingest'; +import { INGEST_API_DATASOURCES, INGEST_API_EPM_PACKAGES } from './services/ingest'; import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; +import { + AssetReference, + GetPackagesResponse, + InstallationStatus, +} from '../../../../../../../ingest_manager/common'; const generator = new EndpointDocGenerator('policy-list'); @@ -32,6 +37,78 @@ export const setPolicyListApiMockImplementation = ( success: true, }); } + + if (path === INGEST_API_EPM_PACKAGES) { + return Promise.resolve({ + response: [ + { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + description: 'This is the Elastic Endpoint package.', + type: 'solution', + download: '/epr/endpoint/endpoint-0.5.0.tar.gz', + path: '/package/endpoint/0.5.0', + icons: [ + { + src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + size: '16x16', + type: 'image/svg+xml', + }, + ], + status: 'installed' as InstallationStatus, + savedObject: { + type: 'epm-packages', + id: 'endpoint', + attributes: { + installed: [ + { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, + { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, + { id: 'logs-endpoint.alerts', type: 'index-template' }, + { id: 'events-endpoint', type: 'index-template' }, + { id: 'logs-endpoint.events.file', type: 'index-template' }, + { id: 'logs-endpoint.events.library', type: 'index-template' }, + { id: 'metrics-endpoint.metadata', type: 'index-template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index-template' }, + { id: 'logs-endpoint.events.network', type: 'index-template' }, + { id: 'metrics-endpoint.policy', type: 'index-template' }, + { id: 'logs-endpoint.events.process', type: 'index-template' }, + { id: 'logs-endpoint.events.registry', type: 'index-template' }, + { id: 'logs-endpoint.events.security', type: 'index-template' }, + { id: 'metrics-endpoint.telemetry', type: 'index-template' }, + ] as AssetReference[], + es_index_patterns: { + alerts: 'logs-endpoint.alerts-*', + events: 'events-endpoint-*', + file: 'logs-endpoint.events.file-*', + library: 'logs-endpoint.events.library-*', + metadata: 'metrics-endpoint.metadata-*', + metadata_mirror: 'metrics-endpoint.metadata_mirror-*', + network: 'logs-endpoint.events.network-*', + policy: 'metrics-endpoint.policy-*', + process: 'logs-endpoint.events.process-*', + registry: 'logs-endpoint.events.registry-*', + security: 'logs-endpoint.events.security-*', + telemetry: 'metrics-endpoint.telemetry-*', + }, + name: 'endpoint', + version: '0.5.0', + internal: false, + removable: false, + }, + references: [], + updated_at: '2020-06-24T14:41:23.098Z', + version: 'Wzc0LDFd', + }, + }, + ], + success: true, + }); + } } return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`)); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 4d798d3717ce4d..a3a0983331ac38 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -16,6 +16,7 @@ import { GetAgentStatusResponse, GetDatasourcesResponse, GetOneDatasourceResponse, + GetPackagesResponse, UpdateDatasourceResponse, } from '../../../../../ingest_manager/common'; @@ -25,6 +26,8 @@ import { export interface PolicyListState { /** Array of policy items */ policyItems: PolicyData[]; + /** Information about the latest endpoint package */ + endpointPackageInfo?: GetPackagesResponse['response'][0]; /** API error if loading data failed */ apiError?: ServerApiError; /** total number of policies */ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts deleted file mode 100644 index 75e1556ff0bb08..00000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useEffect, useState } from 'react'; -import { Immutable } from '../../../../../common/endpoint/types'; -import { GetPackagesResponse } from '../../../../../../ingest_manager/common/types/rest_spec'; -import { sendGetEndpointSecurityPackage } from '../store/policy_list/services/ingest'; -import { useKibana } from '../../../../common/lib/kibana'; - -type UseEndpointPackageInfo = [ - /** The Package Info. will be undefined while it is being fetched */ - Immutable | undefined, - /** Boolean indicating if fetching is underway */ - boolean, - /** Any error encountered during fetch */ - Error | undefined -]; - -/** - * Hook that fetches the endpoint package info - * - * @example - * const [packageInfo, isFetching, fetchError] = useEndpointPackageInfo(); - */ -export const useEndpointPackageInfo = (): UseEndpointPackageInfo => { - const { - services: { http }, - } = useKibana(); - const [endpointPackage, setEndpointPackage] = useState(); - const [isFetching, setIsFetching] = useState(true); - const [error, setError] = useState(); - - useEffect(() => { - sendGetEndpointSecurityPackage(http) - .then((packageInfo) => setEndpointPackage(packageInfo)) - .catch((apiError) => setError(apiError)) - .finally(() => setIsFetching(false)); - }, [http]); - - return [endpointPackage, isFetching, error]; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx index 20346cb720acbc..7b4dc36def1335 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx @@ -35,9 +35,13 @@ export const ConfigureEndpointDatasource = memo {from === 'edit' ? ( { const [policyIdToDelete, setPolicyIdToDelete] = useState(''); const dispatch = useDispatch<(action: PolicyListAction) => void>(); - const [packageInfo, isFetchingPackageInfo] = useEndpointPackageInfo(); const { selectPolicyItems: policyItems, selectPageIndex: pageIndex, @@ -146,6 +144,7 @@ export const PolicyList = React.memo(() => { selectIsDeleting: isDeleting, selectDeleteStatus: deleteStatus, selectAgentStatusSummary: agentStatusSummary, + endpointPackageVersion, } = usePolicyListSelector(selector); const handleCreatePolicyClick = useNavigateToAppEventHandler( @@ -156,7 +155,9 @@ export const PolicyList = React.memo(() => { // Also, // We pass along soem state information so that the Ingest page can change the behaviour // of the cancel and submit buttons and redirect the user back to endpoint policy - path: `#/integrations${packageInfo ? `/endpoint-${packageInfo.version}/add-datasource` : ''}`, + path: `#/integrations${ + endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-datasource` : '' + }`, state: { onCancelNavigateTo: ['securitySolution:management', { path: getPoliciesPath() }], onCancelUrl: formatUrl(getPoliciesPath()), @@ -401,7 +402,6 @@ export const PolicyList = React.memo(() => { { )} @@ -449,7 +449,6 @@ export const PolicyList = React.memo(() => { }, [ policyItems, loading, - isFetchingPackageInfo, columns, handleCreatePolicyClick, handleTableChange, diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap index 6e76ff00a8141c..d7af8d6910f45c 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap @@ -1,15 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Ip Details it matches the snapshot 1`] = ` - - - - +
+ + + + - +
`; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx index bbb964ae17b9f0..a87eb3d0574479 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx @@ -5,15 +5,13 @@ */ import { shallow } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; import React from 'react'; import { Router } from 'react-router-dom'; -import { MockedProvider } from 'react-apollo/test-utils'; import { ActionCreator } from 'typescript-fsa'; import '../../../common/mock/match_media'; -import { mocksSource } from '../../../common/containers/source/mock'; +import { useWithSource } from '../../../common/containers/source'; import { FlowTarget } from '../../../graphql/types'; import { apolloClientObservable, @@ -32,6 +30,9 @@ const pop: Action = 'POP'; type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; +jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/containers/source'); + // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../common/components/search_bar', () => ({ @@ -41,19 +42,6 @@ jest.mock('../../../common/components/query_bar', () => ({ QueryBar: () => null, })); -let localSource: Array<{ - request: {}; - result: { - data: { - source: { - status: { - indicesExist: boolean; - }; - }; - }; - }; -}>; - const getMockHistory = (ip: string) => ({ length: 2, location: { @@ -104,6 +92,10 @@ describe('Ip Details', () => { const mount = useMountAppended(); beforeAll(() => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + indexPattern: {}, + }); (global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() => Promise.resolve({ ok: true, @@ -124,7 +116,6 @@ describe('Ip Details', () => { beforeEach(() => { store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); - localSource = cloneDeep(mocksSource); }); test('it renders', () => { @@ -138,20 +129,18 @@ describe('Ip Details', () => { }); test('it renders ipv6 headline', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); const ip = 'fe80--24ce-f7ff-fede-a571'; const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect( wrapper .find('[data-test-subj="ip-details-headline"] [data-test-subj="header-page-title"]') diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx index face3f8904794e..162b3a7c158d5e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx @@ -22,10 +22,7 @@ import { IpOverview } from '../../components/ip_overview'; import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; import { IpOverviewQuery } from '../../containers/ip_overview'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../../common/containers/source'; +import { useWithSource } from '../../../common/containers/source'; import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; import { decodeIpv6 } from '../../../common/lib/helpers'; @@ -74,208 +71,207 @@ export const IPDetailsComponent: React.FC { setIpDetailsTablesActivePageToZero(); }, [detailName, setIpDetailsTablesActivePageToZero]); - return ( - <> - - {({ indicesExist, indexPattern }) => { - const ip = decodeIpv6(detailName); - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); + const { indicesExist, indexPattern } = useWithSource(); + const ip = decodeIpv6(detailName); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters, + }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - + return ( +
+ {indicesExist ? ( + + + + - - } - title={ip} - > - - + + } + title={ip} + > + + - + {({ id, inspect, ipOverviewData, loading, refetch }) => ( + - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - - )} - - )} - - - - - - - ( + - - - - - - - - - - - - - - - - - - + )} + + )} + - + - + + + - - - + + + - + - + + + - - - + - - - ) : ( - - + + + + + + + + + + + + + + + + + + + + + ) : ( + + - - - ); - }} - + + + )} - +
); }; IPDetailsComponent.displayName = 'IPDetailsComponent'; diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index e1078dee3eb0d7..7cdfdbf0af69a6 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -5,14 +5,12 @@ */ import { mount } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; import React from 'react'; import { Router } from 'react-router-dom'; -import { MockedProvider } from 'react-apollo/test-utils'; import '../../common/mock/match_media'; import { Filter } from '../../../../../../src/plugins/data/common/es_query'; -import { mocksSource } from '../../common/containers/source/mock'; +import { useWithSource } from '../../common/containers/source'; import { TestProviders, mockGlobalState, @@ -26,6 +24,8 @@ import { inputsActions } from '../../common/store/inputs'; import { Network } from './network'; import { NetworkRoutes } from './navigation'; +jest.mock('../../common/containers/source'); + // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar jest.mock('../../common/components/search_bar', () => ({ @@ -35,19 +35,6 @@ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); -let localSource: Array<{ - request: {}; - result: { - data: { - source: { - status: { - indicesExist: boolean; - }; - }; - }; - }; -}>; - type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; const location = { @@ -84,41 +71,33 @@ const getMockProps = () => ({ }); describe('rendering - rendering', () => { - beforeEach(() => { - localSource = cloneDeep(mocksSource); - }); - test('it renders the Setup Instructions text when no index is available', async () => { - localSource[0].result.data.source.status.indicesExist = false; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + }); + const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); }); test('it DOES NOT render the Setup Instructions text when an index is available', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); @@ -154,20 +133,20 @@ describe('rendering - rendering', () => { }, }, ]; - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: { fields: [], title: 'title' }, + }); const myState: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const wrapper = mount( - - - - - + + + ); - await new Promise((resolve) => setTimeout(resolve)); wrapper.update(); myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 845a6bbd95dd6d..4275c1641f5176 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -23,10 +23,7 @@ import { KpiNetworkComponent } from '..//components/kpi_network'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { KpiNetworkQuery } from '../../network/containers/kpi_network'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../common/containers/source'; +import { useWithSource } from '../../common/containers/source'; import { LastEventIndexKey } from '../../graphql/types'; import { useKibana } from '../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../common/lib/keury'; @@ -78,103 +75,100 @@ const NetworkComponent = React.memo( [setAbsoluteRangeDatePicker] ); + const { indicesExist, indexPattern } = useWithSource(sourceId); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }); + const tabsFilterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: tabsFilters, + }); + return ( <> - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); - const tabsFilterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: tabsFilters, - }); - - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - } - title={i18n.PAGE_TITLE} - /> - - + + + + + + } + title={i18n.PAGE_TITLE} + /> + + + + + + + {({ kpiNetwork, loading, id, inspect, refetch }) => ( + + )} + + {capabilitiesFetched && !isInitializing ? ( + <> - - {({ kpiNetwork, loading, id, inspect, refetch }) => ( - - )} - - - {capabilitiesFetched && !isInitializing ? ( - <> - - - - - - - - - ) : ( - - )} + - - - ) : ( - - - - - ); - }} - + + + + ) : ( + + )} + + + +
+ ) : ( + + + + + )} diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index a2010f1f64b718..d6e8fb984ac0ff 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -5,17 +5,16 @@ */ import { mount } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; import { MemoryRouter } from 'react-router-dom'; import '../../common/mock/match_media'; import { TestProviders } from '../../common/mock'; -import { mocksSource } from '../../common/containers/source/mock'; +import { useWithSource } from '../../common/containers/source'; import { Overview } from './index'; jest.mock('../../common/lib/kibana'); +jest.mock('../../common/containers/source'); // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -26,56 +25,36 @@ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); -let localSource: Array<{ - request: {}; - result: { - data: { - source: { - status: { - indicesExist: boolean; - }; - }; - }; - }; -}>; - describe('Overview', () => { describe('rendering', () => { - beforeEach(() => { - localSource = cloneDeep(mocksSource); - }); - test('it renders the Setup Instructions text when no index is available', async () => { - localSource[0].result.data.source.status.indicesExist = false; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + }); + const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); }); test('it DOES NOT render the Getting started text when an index is available', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 543dafd50c8e03..53cb32a16a9de1 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -16,10 +16,7 @@ import { FiltersGlobal } from '../../common/components/filters_global'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { GlobalTime } from '../../common/containers/global_time'; -import { - WithSource, - indicesExistOrDataTemporarilyUnavailable, -} from '../../common/containers/source'; +import { useWithSource } from '../../common/containers/source'; import { EventsByDataset } from '../components/events_by_dataset'; import { EventCounts } from '../components/event_counts'; import { OverviewEmpty } from '../components/overview_empty'; @@ -41,89 +38,89 @@ const OverviewComponent: React.FC = ({ filters = NO_FILTERS, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, -}) => ( - <> - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - - - - - - - - {({ from, deleteQuery, setQuery, to }) => ( - - - - - - - - - - - - - - - - - - - )} - - - - - - ) : ( - - ) - } - - - - -); +}) => { + const { indicesExist, indexPattern } = useWithSource(); + + return ( + <> + {indicesExist ? ( + + + + + + + + + + + + + + {({ from, deleteQuery, setQuery, to }) => ( + + + + + + + + + + + + + + + + + + + )} + + + + + + ) : ( + + )} + + + + ); +}; const makeMapStateToProps = () => { const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); diff --git a/x-pack/plugins/security_solution/public/resolver/embeddable.tsx b/x-pack/plugins/security_solution/public/resolver/embeddable.tsx deleted file mode 100644 index 5ec71e6b3041e0..00000000000000 --- a/x-pack/plugins/security_solution/public/resolver/embeddable.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import ReactDOM from 'react-dom'; -import React from 'react'; -import { Provider } from 'react-redux'; -import { Resolver } from './view'; -import { storeFactory } from './store'; -import { Embeddable } from '../../../../../src/plugins/embeddable/public'; - -export class ResolverEmbeddable extends Embeddable { - public readonly type = 'resolver'; - private lastRenderTarget?: Element; - - public render(node: HTMLElement) { - if (this.lastRenderTarget !== undefined) { - ReactDOM.unmountComponentAtNode(this.lastRenderTarget); - } - this.lastRenderTarget = node; - const { store } = storeFactory(); - ReactDOM.render( - - - , - node - ); - } - - public reload(): void { - throw new Error('Method not implemented.'); - } - - public destroy(): void { - if (this.lastRenderTarget !== undefined) { - ReactDOM.unmountComponentAtNode(this.lastRenderTarget); - } - } -} diff --git a/x-pack/plugins/security_solution/public/resolver/factory.ts b/x-pack/plugins/security_solution/public/resolver/factory.ts deleted file mode 100644 index 5168d2771e7235..00000000000000 --- a/x-pack/plugins/security_solution/public/resolver/factory.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { - IContainer, - EmbeddableInput, - EmbeddableFactoryDefinition, -} from '../../../../../src/plugins/embeddable/public'; -import { ResolverEmbeddable } from './embeddable'; - -export class ResolverEmbeddableFactory implements EmbeddableFactoryDefinition { - public readonly type = 'resolver'; - - public async isEditable() { - return true; - } - - public async create(initialInput: EmbeddableInput, parent?: IContainer) { - return new ResolverEmbeddable(initialInput, {}, parent); - } - - public getDisplayName() { - return i18n.translate('xpack.securitySolution.endpoint.resolver.displayNameTitle', { - defaultMessage: 'Resolver', - }); - } -} diff --git a/x-pack/plugins/security_solution/public/resolver/index.ts b/x-pack/plugins/security_solution/public/resolver/index.ts deleted file mode 100644 index e4f3cc90ae30aa..00000000000000 --- a/x-pack/plugins/security_solution/public/resolver/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { ResolverEmbeddableFactory } from './factory'; -export { ResolverEmbeddable } from './embeddable'; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/sample.ts b/x-pack/plugins/security_solution/public/resolver/store/data/sample.ts deleted file mode 100644 index b0ed9f3554c9bb..00000000000000 --- a/x-pack/plugins/security_solution/public/resolver/store/data/sample.ts +++ /dev/null @@ -1,1608 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ProcessEvent } from '../../types'; - -interface ProcessEventSampleData { - data: { - result: { - search_results: ProcessEvent[]; - }; - }; -} - -const rawData = { - data: { - code: 200, - result: { - alert_id: 'a9834bf5-42c1-4039-83be-08c3ad3232b3', - bulk_task_id: null, - correlation_id: '7022e509-087e-493d-b02c-d88a206cd993', - created_at: '2019-09-24T03:17:36Z', - endpoint: { - ad_distinguished_name: - 'CN=ENDPOINT-W-1-07,OU=Desktops,OU=Workstations,OU=Computers_DEMO,DC=demo,DC=endgamelabs,DC=net', - ad_hostname: 'demo.endgamelabs.net', - display_operating_system: 'Windows 7 (SP1)', - hostname: 'ENDPOINT-W-1-07', - id: '39153006-0064-424b-99e9-4e21dcc00c2e', - ip_address: '172.31.27.17', - mac_address: '00:50:56:b1:b7:7b', - name: 'ENDPOINT-W-1-07', - operating_system: 'Windows 6.1 Service Pack 1', - status: 'monitored', - updated_at: '2019-09-24T01:48:47.960649+00:00', - }, - event_logging_search_request_count: 3, - family: 'collection', - investigation_id: null, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - message_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - metadata: { - chunk_id: 0, - correlation_id: '7022e509-087e-493d-b02c-d88a206cd993', - final: true, - message_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - origination_task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', - os_type: 'windows', - priority: 50, - result: { - local_code: 0, - local_msg: 'Success', - }, - semantic_version: '3.52.8', - task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', - type: 'collection', - }, - origination_task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', - pagination: { - backwards: false, - eof: false, - page_number: 3, - page_offset: 31666, - params: - 'eyJhbGVydF9pZCI6ICJhOTgzNGJmNS00MmMxLTQwMzktODNiZS0wOGMzYWQzMjMyYjMiLCAidGVtcGxhdGVfZmlsZSI6ICJwcm9jZXNzLWNvbnRleHQubHVhIiwgImNyaXRlcmlhIjogeyJwaWQiOiAxODA4LCAidW5pcXVlX3BpZCI6IDE4OTQzfX0=', - remaining_events: 0, - }, - pending_event_logging_search_request: false, - results_count: 807, - search_results: [ - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 6, - command_line: '', - depth: -5, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'already_running', - event_type_full: 'process_event', - integrity_level: 'system', - node_id: 1002, - opcode: 3, - pid: 4, - ppid: 0, - process_name: '', - process_path: '', - serial_event_id: 1002, - timestamp: 132137632670000000, - timestamp_utc: '2019-09-24 01:47:47Z', - unique_pid: 1002, - unique_ppid: 1001, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137632670000000, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 5, - command_line: '\\SystemRoot\\System32\\smss.exe', - depth: -4, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'already_running', - event_type_full: 'process_event', - integrity_level: 'system', - md5: '1911a3356fa3f77ccc825ccbac038c2a', - node_id: 1003, - opcode: 3, - original_file_name: 'smss.exe', - pid: 244, - ppid: 4, - process_name: 'smss.exe', - process_path: 'C:\\Windows\\System32\\smss.exe', - serial_event_id: 1003, - sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2', - sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 1002, - timestamp: 132137632670000000, - timestamp_utc: '2019-09-24 01:47:47Z', - unique_pid: 1003, - unique_ppid: 1002, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137632670000000, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 4, - authentication_id: 999, - command_line: '\\SystemRoot\\System32\\smss.exe 00000000 00000048 ', - depth: -3, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'system', - md5: '1911a3356fa3f77ccc825ccbac038c2a', - node_id: 18643, - opcode: 1, - original_file_name: 'smss.exe', - parent_process_name: 'smss.exe', - parent_process_path: 'C:\\Windows\\System32\\smss.exe', - pid: 2364, - ppid: 244, - process_name: 'smss.exe', - process_path: 'C:\\Windows\\System32\\smss.exe', - serial_event_id: 18643, - sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2', - sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 1003, - timestamp: 132137681960227504, - timestamp_utc: '2019-09-24 03:09:56Z', - unique_pid: 18643, - unique_ppid: 1003, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137681960227504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 3, - authentication_id: 999, - command_line: 'winlogon.exe', - depth: -2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'system', - md5: '1151b1baa6f350b1db6598e0fea7c457', - node_id: 18645, - opcode: 1, - original_file_name: 'WINLOGON.EXE', - parent_process_name: 'smss.exe', - parent_process_path: 'C:\\Windows\\System32\\smss.exe', - pid: 3108, - ppid: 2364, - process_name: 'winlogon.exe', - process_path: 'C:\\Windows\\System32\\winlogon.exe', - serial_event_id: 18645, - sha1: '434856b834baf163c5ea4d26434eeae775a507fb', - sha256: 'b1506e0a7e826eff0f5252ef5026070c46e2235438403a9a24d73ee69c0b8a49', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18643, - timestamp: 132137681961163504, - timestamp_utc: '2019-09-24 03:09:56Z', - unique_pid: 18645, - unique_ppid: 18643, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137681961163504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - depth: -2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: '1911a3356fa3f77ccc825ccbac038c2a', - node_id: 18646, - opcode: 2, - original_file_name: 'smss.exe', - parent_process_name: 'smss.exe', - parent_process_path: 'C:\\Windows\\System32\\smss.exe', - pid: 2364, - ppid: 244, - process_name: 'smss.exe', - process_path: 'C:\\Windows\\System32\\smss.exe', - serial_event_id: 18646, - sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2', - sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18643, - timestamp: 132137681961787504, - timestamp_utc: '2019-09-24 03:09:56Z', - unique_pid: 18643, - unique_ppid: 1003, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137681961787504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 1, - authentication_id: 4904488, - command_line: 'C:\\Windows\\system32\\userinit.exe', - depth: -1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'bafe84e637bf7388c96ef48d4d3fdd53', - node_id: 18833, - opcode: 1, - original_file_name: 'USERINIT.EXE', - parent_process_name: 'winlogon.exe', - parent_process_path: 'C:\\Windows\\System32\\winlogon.exe', - pid: 3560, - ppid: 3108, - process_name: 'userinit.exe', - process_path: 'C:\\Windows\\System32\\userinit.exe', - serial_event_id: 18833, - sha1: '47267f943f060e36604d56c8895a6eece063d9a1', - sha256: '11c194d9adce90027272c627d7fbf3ba5025ff0f7b26a8333f764e11e1382cf9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18645, - timestamp: 132137681981287504, - timestamp_utc: '2019-09-24 03:09:58Z', - unique_pid: 18833, - unique_ppid: 18645, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137681981287504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 0, - authentication_id: 4904488, - command_line: 'C:\\Windows\\Explorer.EXE', - depth: 0, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 18943, - opcode: 1, - origin: true, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'userinit.exe', - parent_process_path: 'C:\\Windows\\System32\\userinit.exe', - pid: 1808, - ppid: 3560, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 18943, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18833, - timestamp: 132137681985655504, - timestamp_utc: '2019-09-24 03:09:58Z', - unique_pid: 18943, - unique_ppid: 18833, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137681985655504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe" -n vmusr', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '8dc5ad50587b936f7f616738112bfd2a', - node_id: 19545, - opcode: 1, - original_file_name: 'vmtoolsd.exe', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3596, - ppid: 1808, - process_name: 'vmtoolsd.exe', - process_path: 'C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe', - serial_event_id: 19545, - sha1: '04479ea30943ec471a6a5ca4c0dc74b5ff496e9f', - sha256: 'd6d9f041da6f724bf69f48bbee3bf41295a0ed4dca715b1908c5f35bc8034d53', - signature_signer: 'VMware, Inc.', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137681999539504, - timestamp_utc: '2019-09-24 03:09:59Z', - unique_pid: 19545, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137681999539504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - depth: 0, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'bafe84e637bf7388c96ef48d4d3fdd53', - node_id: 20261, - opcode: 2, - original_file_name: 'USERINIT.EXE', - parent_process_name: 'winlogon.exe', - parent_process_path: 'C:\\Windows\\System32\\winlogon.exe', - pid: 3560, - ppid: 3108, - process_name: 'userinit.exe', - process_path: 'C:\\Windows\\System32\\userinit.exe', - serial_event_id: 20261, - sha1: '47267f943f060e36604d56c8895a6eece063d9a1', - sha256: '11c194d9adce90027272c627d7fbf3ba5025ff0f7b26a8333f764e11e1382cf9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18833, - timestamp: 132137682277819504, - timestamp_utc: '2019-09-24 03:10:27Z', - unique_pid: 18833, - unique_ppid: 18645, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682277819504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\explorer.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 20303, - opcode: 1, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3124, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 20303, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137682603979504, - timestamp_utc: '2019-09-24 03:11:00Z', - unique_pid: 20303, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682603979504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 20310, - opcode: 2, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3124, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 20310, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 20303, - timestamp: 132137682604229504, - timestamp_utc: '2019-09-24 03:11:00Z', - unique_pid: 20303, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682604229504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\explorer.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 20455, - opcode: 1, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3084, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 20455, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137682773669504, - timestamp_utc: '2019-09-24 03:11:17Z', - unique_pid: 20455, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682773669504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 20462, - opcode: 2, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3084, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 20462, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 20455, - timestamp: 132137682774259504, - timestamp_utc: '2019-09-24 03:11:17Z', - unique_pid: 20455, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682774259504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\System32\\cmd.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '5746bd7e255dd6a8afa06f7c42c1ba41', - node_id: 21120, - opcode: 1, - original_file_name: 'Cmd.Exe', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3280, - ppid: 1808, - process_name: 'cmd.exe', - process_path: 'C:\\Windows\\System32\\cmd.exe', - serial_event_id: 21120, - sha1: '0f3c4ff28f354aede202d54e9d1c5529a3bf87d8', - sha256: 'db06c3534964e3fc79d2763144ba53742d7fa250ca336f4a0fe724b75aaff386', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137682997939504, - timestamp_utc: '2019-09-24 03:11:39Z', - unique_pid: 21120, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682997939504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\explorer.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 21166, - opcode: 1, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3548, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 21166, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137683166079504, - timestamp_utc: '2019-09-24 03:11:56Z', - unique_pid: 21166, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683166079504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 21173, - opcode: 2, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3548, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 21173, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 21166, - timestamp: 132137683166729504, - timestamp_utc: '2019-09-24 03:11:56Z', - unique_pid: 21166, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683166729504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Python27\\python.exe" "C:\\tmp\\dns.py" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '743b91619fbfee3c3e173ba5a17b1290', - node_id: 21480, - opcode: 1, - original_file_name: '', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 4060, - ppid: 1808, - process_name: 'python.exe', - process_path: 'C:\\Python27\\python.exe', - serial_event_id: 21480, - sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', - sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', - signature_signer: '', - signature_status: 'noSignature', - source_id: 18943, - timestamp: 132137683493349504, - timestamp_utc: '2019-09-24 03:12:29Z', - unique_pid: 21480, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683493349504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: '743b91619fbfee3c3e173ba5a17b1290', - node_id: 21500, - opcode: 2, - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 4060, - ppid: 1808, - process_name: 'python.exe', - process_path: 'C:\\Python27\\python.exe', - serial_event_id: 21500, - sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', - sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', - signature_status: 'noSignature', - source_id: 21480, - timestamp: 132137683493889504, - timestamp_utc: '2019-09-24 03:12:29Z', - unique_pid: 21480, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683493889504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Python27\\python.exe" "C:\\tmp\\dns.py" ', - depth: 2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '743b91619fbfee3c3e173ba5a17b1290', - node_id: 21539, - opcode: 1, - original_file_name: '', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 2888, - ppid: 3280, - process_name: 'python.exe', - process_path: 'C:\\Python27\\python.exe', - serial_event_id: 21539, - sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', - sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21120, - timestamp: 132137683555889504, - timestamp_utc: '2019-09-24 03:12:35Z', - unique_pid: 21539, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683555889504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 3, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: '743b91619fbfee3c3e173ba5a17b1290', - node_id: 21540, - opcode: 2, - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 2888, - ppid: 3280, - process_name: 'python.exe', - process_path: 'C:\\Python27\\python.exe', - serial_event_id: 21540, - sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', - sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', - signature_status: 'noSignature', - source_id: 21539, - timestamp: 132137683556159504, - timestamp_utc: '2019-09-24 03:12:35Z', - unique_pid: 21539, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683556159504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - depth: 2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21634, - opcode: 1, - original_file_name: '', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 3996, - ppid: 3280, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21634, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21120, - timestamp: 132137683921669504, - timestamp_utc: '2019-09-24 03:13:12Z', - unique_pid: 21634, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683921669504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - depth: 3, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21669, - opcode: 1, - original_file_name: '', - parent_process_name: 'fakenet.exe', - parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - pid: 184, - ppid: 3996, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21669, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21634, - timestamp: 132137683923819504, - timestamp_utc: '2019-09-24 03:13:12Z', - unique_pid: 21669, - unique_ppid: 21634, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683923819504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 4, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21679, - opcode: 2, - parent_process_name: 'fakenet.exe', - parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - pid: 184, - ppid: 3996, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21679, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_status: 'noSignature', - source_id: 21669, - timestamp: 132137683931089504, - timestamp_utc: '2019-09-24 03:13:13Z', - unique_pid: 21669, - unique_ppid: 21634, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683931089504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 3, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21694, - opcode: 2, - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 3996, - ppid: 3280, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21694, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_status: 'noSignature', - source_id: 21634, - timestamp: 132137683931569504, - timestamp_utc: '2019-09-24 03:13:13Z', - unique_pid: 21634, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683931569504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: - '"C:\\Windows\\system32\\NOTEPAD.EXE" C:\\tmp\\fakenet1.4.3\\configs\\default.ini', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'f2c7bb8acc97f92e987a2d4087d021b1', - node_id: 21769, - opcode: 1, - original_file_name: 'NOTEPAD.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 2492, - ppid: 1808, - process_name: 'notepad.exe', - process_path: 'C:\\Windows\\System32\\notepad.exe', - serial_event_id: 21769, - sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', - sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137684112851830, - timestamp_utc: '2019-09-24 03:13:31Z', - unique_pid: 21769, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684112851830, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'f2c7bb8acc97f92e987a2d4087d021b1', - node_id: 21794, - opcode: 2, - original_file_name: 'NOTEPAD.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 2492, - ppid: 1808, - process_name: 'notepad.exe', - process_path: 'C:\\Windows\\System32\\notepad.exe', - serial_event_id: 21794, - sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', - sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 21769, - timestamp: 132137684131573702, - timestamp_utc: '2019-09-24 03:13:33Z', - unique_pid: 21769, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684131573702, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: 'fakenet.exe', - depth: 2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21890, - opcode: 1, - original_file_name: '', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 1060, - ppid: 3280, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21890, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21120, - timestamp: 132137684579848525, - timestamp_utc: '2019-09-24 03:14:17Z', - unique_pid: 21890, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684579848525, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: 'fakenet.exe', - depth: 3, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21924, - opcode: 1, - original_file_name: '', - parent_process_name: 'fakenet.exe', - parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - pid: 4024, - ppid: 1060, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21924, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21890, - timestamp: 132137684580468587, - timestamp_utc: '2019-09-24 03:14:18Z', - unique_pid: 21924, - unique_ppid: 21890, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684580468587, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\System32\\cmd.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '5746bd7e255dd6a8afa06f7c42c1ba41', - node_id: 22238, - opcode: 1, - original_file_name: 'Cmd.Exe', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3328, - ppid: 1808, - process_name: 'cmd.exe', - process_path: 'C:\\Windows\\System32\\cmd.exe', - serial_event_id: 22238, - sha1: '0f3c4ff28f354aede202d54e9d1c5529a3bf87d8', - sha256: 'db06c3534964e3fc79d2763144ba53742d7fa250ca336f4a0fe724b75aaff386', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137684944024939, - timestamp_utc: '2019-09-24 03:14:54Z', - unique_pid: 22238, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684944024939, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - attack_references: [ - { - tactics: ['Privilege Escalation', 'Execution', 'Persistence'], - technique_id: 'T1053', - technique_name: 'Scheduled Task', - }, - ], - authentication_id: 4904488, - command_line: 'SCHTASKS /CREATE /SC MINUTE /TN "Windiws" /TR "C:\\tmp\\scheduler.bat"', - depth: 2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '97e0ec3d6d99e8cc2b17ef2d3760e8fc', - node_id: 22376, - opcode: 1, - original_file_name: 'sctasks.exe', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 2864, - ppid: 3328, - process_name: 'schtasks.exe', - process_path: 'C:\\Windows\\System32\\schtasks.exe', - serial_event_id: 22376, - sha1: 'bd9dceffbcbbc82bee5f2109bd73a57477fe1f92', - sha256: '6dce7d58ebb0d705fcb4179349c441b45e160c94e43934c5ed8fa1964e2cd031', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22238, - timestamp: 132137685249385472, - timestamp_utc: '2019-09-24 03:15:24Z', - unique_pid: 22376, - unique_ppid: 22238, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137685249385472, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 3, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: '97e0ec3d6d99e8cc2b17ef2d3760e8fc', - node_id: 22384, - opcode: 2, - original_file_name: 'sctasks.exe', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 2864, - ppid: 3328, - process_name: 'schtasks.exe', - process_path: 'C:\\Windows\\System32\\schtasks.exe', - serial_event_id: 22384, - sha1: 'bd9dceffbcbbc82bee5f2109bd73a57477fe1f92', - sha256: '6dce7d58ebb0d705fcb4179349c441b45e160c94e43934c5ed8fa1964e2cd031', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22376, - timestamp: 132137685251515685, - timestamp_utc: '2019-09-24 03:15:25Z', - unique_pid: 22376, - unique_ppid: 22238, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137685251515685, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\System32\\NOTEPAD.EXE" C:\\tmp\\scheduler.bat', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'f2c7bb8acc97f92e987a2d4087d021b1', - node_id: 22448, - opcode: 1, - original_file_name: 'NOTEPAD.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 4048, - ppid: 1808, - process_name: 'notepad.exe', - process_path: 'C:\\Windows\\System32\\notepad.exe', - serial_event_id: 22448, - sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', - sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137685448755407, - timestamp_utc: '2019-09-24 03:15:44Z', - unique_pid: 22448, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137685448755407, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'f2c7bb8acc97f92e987a2d4087d021b1', - node_id: 22464, - opcode: 2, - original_file_name: 'NOTEPAD.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 4048, - ppid: 1808, - process_name: 'notepad.exe', - process_path: 'C:\\Windows\\System32\\notepad.exe', - serial_event_id: 22464, - sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', - sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22448, - timestamp: 132137685516752206, - timestamp_utc: '2019-09-24 03:15:51Z', - unique_pid: 22448, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137685516752206, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - attack_references: [ - { - tactics: ['Execution'], - technique_id: 'T1085', - technique_name: 'Rundll32', - }, - ], - authentication_id: 4904488, - command_line: - '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\tmp\\XLS_no_email_Upcoming Events February 2018.xls\\cb85072e6ca66a29cb0b73659a0fe5ba2456d9ba0b52e3a4c89e86549bc6e2c7.xls', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 22799, - opcode: 1, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 2864, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 22799, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137686572217742, - timestamp_utc: '2019-09-24 03:17:37Z', - unique_pid: 22799, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686572217742, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 22805, - opcode: 2, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 2864, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 22805, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22799, - timestamp: 132137686585839104, - timestamp_utc: '2019-09-24 03:17:38Z', - unique_pid: 22799, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686585839104, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - attack_references: [ - { - tactics: ['Execution'], - technique_id: 'T1085', - technique_name: 'Rundll32', - }, - ], - authentication_id: 4904488, - command_line: - '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\tmp\\Upcoming Defense events February 2018.eml', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 22933, - opcode: 1, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 1864, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 22933, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137686702740793, - timestamp_utc: '2019-09-24 03:17:50Z', - unique_pid: 22933, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686702740793, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 22945, - opcode: 2, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 1864, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 22945, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22933, - timestamp: 132137686718432362, - timestamp_utc: '2019-09-24 03:17:51Z', - unique_pid: 22933, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686718432362, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - attack_references: [ - { - tactics: ['Execution'], - technique_id: 'T1085', - technique_name: 'Rundll32', - }, - ], - authentication_id: 4904488, - command_line: - '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\Users\\Administrator\\AppData\\Roaming\\Microsoft\\Windows\\SendTo\\Mail Recipient.MAPIMail', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 27050, - opcode: 1, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 568, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 27050, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137686926723189, - timestamp_utc: '2019-09-24 03:18:12Z', - unique_pid: 27050, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686926723189, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 27053, - opcode: 2, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 568, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 27053, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 27050, - timestamp: 132137686939784495, - timestamp_utc: '2019-09-24 03:18:13Z', - unique_pid: 27050, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686939784495, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - ], - status: 'success', - task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', - total_events_searched: 7730, - type: 'eventLoggingSearchResponse', - }, - }, - metadata: { - count: 39, - next: null, - next_url: null, - per_page: '4000', - previous_url: null, - timestamp: '2019-12-18T19:31:27.565110', - }, -}; - -export const sampleData: ProcessEventSampleData = rawData as ProcessEventSampleData; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts index a352a076e5a972..343b4e1a14478c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts @@ -12,7 +12,7 @@ import { ResolverEvent, ResolverChildren, ResolverAncestry, - LifecycleNode, + ResolverLifecycleNode, ResolverNodeStats, ResolverRelatedEvents, } from '../../../common/endpoint/types'; @@ -25,10 +25,10 @@ type MiddlewareFactory = ( ) => (next: Dispatch) => (action: ResolverAction) => unknown; function getLifecycleEventsAndStats( - nodes: LifecycleNode[], + nodes: ResolverLifecycleNode[], stats: Map ): ResolverEvent[] { - return nodes.reduce((flattenedEvents: ResolverEvent[], currentNode: LifecycleNode) => { + return nodes.reduce((flattenedEvents: ResolverEvent[], currentNode: ResolverLifecycleNode) => { if (currentNode.lifecycle && currentNode.lifecycle.length > 0) { flattenedEvents.push(...currentNode.lifecycle); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx index ae05d99b58ee08..a1392ad8b82707 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx @@ -10,7 +10,7 @@ import { rgba } from 'polished'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { WithSource } from '../../../../common/containers/source'; +import { useWithSource } from '../../../../common/containers/source'; import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_drop/helpers'; import { DataProvider } from '../../timeline/data_providers/data_provider'; import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers'; @@ -84,6 +84,7 @@ interface FlyoutButtonProps { export const FlyoutButton = React.memo( ({ onOpen, show, dataProviders, timelineId }) => { const badgeCount = useMemo(() => getBadgeCount(dataProviders), [dataProviders]); + const { browserFields } = useWithSource(); if (!show) { return null; @@ -121,19 +122,15 @@ export const FlyoutButton = React.memo( - - {({ browserFields }) => ( - - )} - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 51cfe8ae33b05e..df76eb350ace7f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -9,7 +9,7 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { NO_ALERT_INDEX } from '../../../../common/constants'; -import { WithSource } from '../../../common/containers/source'; +import { useWithSource } from '../../../common/containers/source'; import { useSignalIndex } from '../../../alerts/containers/detection_engine/alerts/use_signal_index'; import { inputsModel, inputsSelectors, State } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../store/timeline'; @@ -158,40 +158,38 @@ const StatefulTimelineComponent = React.memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const { indexPattern, browserFields } = useWithSource('default', indexToAdd); + return ( - - {({ indexPattern, browserFields }) => ( - - )} - + ); }, (prevProps, nextProps) => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 4037f1a7cbc464..7c50a10846f9a0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -8,6 +8,7 @@ import { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; +import Boom from 'boom'; import { metadataIndexPattern } from '../../../../common/endpoint/constants'; import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; import { @@ -18,6 +19,7 @@ import { } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; import { AgentStatus } from '../../../../../ingest_manager/common/types/models'; +import { findAllUnenrolledHostIds, findUnenrolledHostByHostId, HostId } from './support/unenroll'; interface HitSource { _source: HostMetadata; @@ -68,10 +70,17 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { + const unenrolledHostIds = await findAllUnenrolledHostIds( + context.core.elasticsearch.legacy.client + ); + const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, - metadataIndexPattern + metadataIndexPattern, + { + unenrolledHostIds: unenrolledHostIds.map((host: HostId) => host.host.id), + } ); const response = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', @@ -113,6 +122,12 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp return res.notFound({ body: 'Endpoint Not Found' }); } catch (err) { logger.warn(JSON.stringify(err, null, 2)); + if (err.isBoom) { + return res.customError({ + statusCode: err.output.statusCode, + body: { message: err.message }, + }); + } return res.internalError({ body: err }); } } @@ -123,6 +138,13 @@ export async function getHostData( metadataRequestContext: MetadataRequestContext, id: string ): Promise { + const unenrolledHostId = await findUnenrolledHostByHostId( + metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client, + id + ); + if (unenrolledHostId) { + throw Boom.badRequest('the requested endpoint is unenrolled'); + } const query = getESQueryHostMetadataByID(id, metadataIndexPattern); const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index c04975fa8b28e0..1ca205f669fa37 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -35,6 +35,7 @@ import Boom from 'boom'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { HostId } from './support/unenroll'; describe('test endpoint route', () => { let routerMock: jest.Mocked; @@ -50,6 +51,12 @@ describe('test endpoint route', () => { typeof createMockEndpointAppContextServiceStartContract >['agentService']; let endpointAppContextService: EndpointAppContextService; + const noUnenrolledEndpoint = () => + Promise.resolve(({ + hits: { + hits: [], + }, + } as unknown) as SearchResponse); beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< @@ -77,7 +84,9 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata()); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -88,7 +97,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; @@ -113,9 +122,11 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) - ); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => + Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -126,8 +137,8 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({ + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({ match_all: {}, }); expect(routeConfig.options).toEqual({ authRequired: true }); @@ -156,9 +167,11 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) - ); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => + Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -170,20 +183,26 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.callAsCurrentUser).toBeCalled(); - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({ + expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({ bool: { - must_not: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'host.ip': '10.140.73.246', + must: [ + { + bool: { + must_not: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'host.ip': '10.140.73.246', + }, + }, + ], }, }, - ], + }, }, - }, + ], }, }); expect(routeConfig.options).toEqual({ authRequired: true }); @@ -199,9 +218,10 @@ describe('test endpoint route', () => { it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse()) - ); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(createSearchResponse())); + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -212,7 +232,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.notFound).toBeCalled(); const message = mockResponse.notFound.mock.calls[0][0]?.body; @@ -224,8 +244,12 @@ describe('test endpoint route', () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: response.hits.hits[0]._id }, }); + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -236,7 +260,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; @@ -254,7 +278,11 @@ describe('test endpoint route', () => { mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => { throw Boom.notFound('Agent not found'); }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -265,7 +293,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; @@ -280,7 +308,11 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -291,12 +323,50 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result.host_status).toEqual(HostStatus.ERROR); }); + + it('should throw error when endpoint is unenrolled', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + params: { id: 'hostId' }, + }); + + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(({ + hits: { + hits: [ + { + _index: 'metrics-endpoint.metadata_mirror-default', + _id: 'S5M1yHIBLSMVtiLw6Wpr', + _score: 0.0, + _source: { + host: { + id: 'hostId', + }, + }, + }, + ], + }, + } as unknown) as SearchResponse) + ); + + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/metadata') + )!; + + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockResponse.customError).toBeCalled(); + }); }); }); @@ -319,7 +389,7 @@ function createSearchResponse(hostMetadata?: HostMetadata): SearchResponse { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record); }); + + it( + 'test default query params for all endpoints metadata when no params or body is provided ' + + 'with unenrolled host ids excluded', + async () => { + const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672'; + const mockRequest = httpServerMock.createKibanaRequest({ + body: {}, + }); + const query = await kibanaRequestToMetadataListESQuery( + mockRequest, + { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + }, + metadataIndexPattern, + { + unenrolledHostIds: [unenrolledHostId], + } + ); + + expect(query).toEqual({ + body: { + query: { + bool: { + must_not: { + terms: { + 'host.id': ['1fdca33f-799f-49f4-939c-ea4383c77672'], + }, + }, + }, + }, + collapse: { + field: 'host.id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ 'event.created': 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'host.id', + }, + }, + }, + sort: [ + { + 'event.created': { + order: 'desc', + }, + }, + ], + }, + from: 0, + size: 10, + index: metadataIndexPattern, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as Record); + } + ); }); describe('test query builder with kql filter', () => { @@ -76,22 +139,29 @@ describe('query builder', () => { }, metadataIndexPattern ); + expect(query).toEqual({ body: { query: { bool: { - must_not: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'host.ip': '10.140.73.246', + must: [ + { + bool: { + must_not: { + bool: { + should: [ + { + match: { + 'host.ip': '10.140.73.246', + }, + }, + ], + minimum_should_match: 1, }, }, - ], + }, }, - }, + ], }, }, collapse: { @@ -123,6 +193,93 @@ describe('query builder', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record); }); + + it( + 'test default query params for all endpoints endpoint metadata excluding unerolled endpoint ' + + 'and when body filter is provided', + async () => { + const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672'; + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + filter: 'not host.ip:10.140.73.246', + }, + }); + const query = await kibanaRequestToMetadataListESQuery( + mockRequest, + { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + }, + metadataIndexPattern, + { + unenrolledHostIds: [unenrolledHostId], + } + ); + + expect(query).toEqual({ + body: { + query: { + bool: { + must: [ + { + bool: { + must_not: { + terms: { + 'host.id': [unenrolledHostId], + }, + }, + }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match: { + 'host.ip': '10.140.73.246', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + }, + ], + }, + }, + collapse: { + field: 'host.id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ 'event.created': 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'host.id', + }, + }, + }, + sort: [ + { + 'event.created': { + order: 'desc', + }, + }, + ], + }, + from: 0, + size: 10, + index: metadataIndexPattern, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as Record); + } + ); }); describe('MetadataGetQuery', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index 075e4377f0b2a2..b6ec91675f2483 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -7,17 +7,22 @@ import { KibanaRequest } from 'kibana/server'; import { esKuery } from '../../../../../../../src/plugins/data/server'; import { EndpointAppContext } from '../../types'; -export const kibanaRequestToMetadataListESQuery = async ( +export interface QueryBuilderOptions { + unenrolledHostIds?: string[]; +} + +export async function kibanaRequestToMetadataListESQuery( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, endpointAppContext: EndpointAppContext, - index: string + index: string, + queryBuilderOptions?: QueryBuilderOptions // eslint-disable-next-line @typescript-eslint/no-explicit-any -): Promise> => { +): Promise> { const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { - query: buildQueryBody(request), + query: buildQueryBody(request, queryBuilderOptions?.unenrolledHostIds!), collapse: { field: 'host.id', inner_hits: { @@ -45,7 +50,7 @@ export const kibanaRequestToMetadataListESQuery = async ( size: pagingProperties.pageSize, index, }; -}; +} async function getPagingProperties( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -68,14 +73,53 @@ async function getPagingProperties( }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function buildQueryBody(request: KibanaRequest): Record { +function buildQueryBody( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: KibanaRequest, + unerolledHostIds: string[] | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): Record { + const filterUnenrolledHosts = unerolledHostIds && unerolledHostIds.length > 0; if (typeof request?.body?.filter === 'string') { - return esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); + const kqlQuery = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); + return { + bool: { + must: filterUnenrolledHosts + ? [ + { + bool: { + must_not: { + terms: { + 'host.id': unerolledHostIds, + }, + }, + }, + }, + { + ...kqlQuery, + }, + ] + : [ + { + ...kqlQuery, + }, + ], + }, + }; } - return { - match_all: {}, - }; + return filterUnenrolledHosts + ? { + bool: { + must_not: { + terms: { + 'host.id': unerolledHostIds, + }, + }, + }, + } + : { + match_all: {}, + }; } export function getESQueryHostMetadataByID(hostID: string, index: string) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts new file mode 100644 index 00000000000000..2e6bb2c976fef1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import { + findAllUnenrolledHostIds, + fetchAllUnenrolledHostIdsWithScroll, + HostId, + findUnenrolledHostByHostId, +} from './unenroll'; +import { elasticsearchServiceMock } from '../../../../../../../../src/core/server/mocks'; +import { SearchResponse } from 'elasticsearch'; +import { metadataMirrorIndexPattern } from '../../../../../common/endpoint/constants'; +import { EndpointStatus } from '../../../../../common/endpoint/types'; + +const noUnenrolledEndpoint = () => + Promise.resolve(({ + hits: { + hits: [], + }, + } as unknown) as SearchResponse); + +describe('test find all unenrolled HostId', () => { + let mockScopedClient: jest.Mocked; + + it('can find all hits with scroll', async () => { + const firstHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; + const secondHostId = '2fdca33f-799f-49f4-939c-ea4383c77672'; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(() => Promise.resolve(createSearchResponse(secondHostId, 'scrollId'))) + .mockImplementationOnce(noUnenrolledEndpoint); + + const initialResponse = createSearchResponse(firstHostId, 'initialScrollId'); + const hostIds = await fetchAllUnenrolledHostIdsWithScroll( + initialResponse, + mockScopedClient.callAsCurrentUser + ); + + expect(hostIds).toEqual([{ host: { id: firstHostId } }, { host: { id: secondHostId } }]); + }); + + it('can find all unerolled endpoint host ids', async () => { + const firstEndpointHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; + const secondEndpointHostId = '2fdca33f-799f-49f4-939c-ea4383c77672'; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(() => + Promise.resolve(createSearchResponse(firstEndpointHostId, 'initialScrollId')) + ) + .mockImplementationOnce(() => + Promise.resolve(createSearchResponse(secondEndpointHostId, 'scrollId')) + ) + .mockImplementationOnce(noUnenrolledEndpoint); + const hostIds = await findAllUnenrolledHostIds(mockScopedClient); + + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]).toEqual({ + index: metadataMirrorIndexPattern, + scroll: '30s', + body: { + size: 1000, + _source: ['host.id'], + query: { + bool: { + filter: { + term: { + 'Endpoint.status': EndpointStatus.unenrolled, + }, + }, + }, + }, + }, + }); + expect(hostIds).toEqual([ + { host: { id: firstEndpointHostId } }, + { host: { id: secondEndpointHostId } }, + ]); + }); +}); + +describe('test find unenrolled endpoint host id by hostId', () => { + let mockScopedClient: jest.Mocked; + + it('can find unenrolled endpoint by the host id when unenrolled', async () => { + const firstEndpointHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse(firstEndpointHostId, 'initialScrollId')) + ); + const endpointHostId = await findUnenrolledHostByHostId(mockScopedClient, firstEndpointHostId); + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.index).toEqual( + metadataMirrorIndexPattern + ); + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body).toEqual({ + size: 1, + _source: ['host.id'], + query: { + bool: { + filter: [ + { + term: { + 'Endpoint.status': EndpointStatus.unenrolled, + }, + }, + { + term: { + 'host.id': firstEndpointHostId, + }, + }, + ], + }, + }, + }); + expect(endpointHostId).toEqual({ host: { id: firstEndpointHostId } }); + }); + + it('find unenrolled endpoint host by the host id return undefined when no unenrolled host', async () => { + const firstHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(noUnenrolledEndpoint); + const hostId = await findUnenrolledHostByHostId(mockScopedClient, firstHostId); + expect(hostId).toBeFalsy(); + }); +}); + +function createSearchResponse(hostId: string, scrollId: string): SearchResponse { + return ({ + hits: { + hits: [ + { + _index: metadataMirrorIndexPattern, + _id: 'S5M1yHIBLSMVtiLw6Wpr', + _score: 0.0, + _source: { + host: { + id: hostId, + }, + }, + }, + ], + }, + _scroll_id: scrollId, + } as unknown) as SearchResponse; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts new file mode 100644 index 00000000000000..ef6898fad2807d --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APICaller, IScopedClusterClient } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; +import { metadataMirrorIndexPattern } from '../../../../../common/endpoint/constants'; +import { EndpointStatus } from '../../../../../common/endpoint/types'; + +const KEEPALIVE = '30s'; +const SIZE = 1000; + +export interface HostId { + host: { + id: string; + }; +} + +interface HitSource { + _source: HostId; +} + +export async function findUnenrolledHostByHostId( + client: IScopedClusterClient, + hostId: string +): Promise { + const queryParams = { + index: metadataMirrorIndexPattern, + body: { + size: 1, + _source: ['host.id'], + query: { + bool: { + filter: [ + { + term: { + 'Endpoint.status': EndpointStatus.unenrolled, + }, + }, + { + term: { + 'host.id': hostId, + }, + }, + ], + }, + }, + }, + }; + + const response = (await client.callAsCurrentUser('search', queryParams)) as SearchResponse< + HostId + >; + const newHits = response.hits?.hits || []; + + if (newHits.length > 0) { + const hostIds = newHits.map((hitSource: HitSource) => hitSource._source); + return hostIds[0]; + } else { + return undefined; + } +} + +export async function findAllUnenrolledHostIds(client: IScopedClusterClient): Promise { + const queryParams = { + index: metadataMirrorIndexPattern, + scroll: KEEPALIVE, + body: { + size: SIZE, + _source: ['host.id'], + query: { + bool: { + filter: { + term: { + 'Endpoint.status': EndpointStatus.unenrolled, + }, + }, + }, + }, + }, + }; + const response = (await client.callAsCurrentUser('search', queryParams)) as SearchResponse< + HostId + >; + + return fetchAllUnenrolledHostIdsWithScroll(response, client.callAsCurrentUser); +} + +export async function fetchAllUnenrolledHostIdsWithScroll( + response: SearchResponse, + client: APICaller, + hits: HostId[] = [] +): Promise { + let newHits = response.hits?.hits || []; + let scrollId = response._scroll_id; + + while (newHits.length > 0) { + const hostIds: HostId[] = newHits.map((hitSource: HitSource) => hitSource._source); + hits.push(...hostIds); + + const innerResponse = await client('scroll', { + body: { + scroll: KEEPALIVE, + scroll_id: scrollId, + }, + }); + + newHits = innerResponse.hits?.hits || []; + scrollId = innerResponse._scroll_id; + } + return hits; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index 7a3e1fc591e82e..e60e5087c30a9e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -9,7 +9,11 @@ import { parentEntityId, isProcessStart, } from '../../../../../common/endpoint/models/event'; -import { ChildNode, ResolverEvent, ResolverChildren } from '../../../../../common/endpoint/types'; +import { + ResolverChildNode, + ResolverEvent, + ResolverChildren, +} from '../../../../../common/endpoint/types'; import { PaginationBuilder } from './pagination'; import { createChild } from './node'; @@ -17,7 +21,7 @@ import { createChild } from './node'; * This class helps construct the children structure when building a resolver tree. */ export class ChildrenNodesHelper { - private readonly cache: Map = new Map(); + private readonly cache: Map = new Map(); constructor(private readonly rootID: string) { this.cache.set(rootID, createChild(rootID)); @@ -27,7 +31,7 @@ export class ChildrenNodesHelper { * Constructs a ResolverChildren response based on the children that were previously add. */ getNodes(): ResolverChildren { - const cacheCopy: Map = new Map(this.cache); + const cacheCopy: Map = new Map(this.cache); const rootNode = cacheCopy.get(this.rootID); let rootNextChild = null; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index d448649ae447bf..0af2fca7106bef 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -10,7 +10,7 @@ import { ResolverRelatedEvents, ResolverAncestry, ResolverRelatedAlerts, - LifecycleNode, + ResolverLifecycleNode, ResolverEvent, } from '../../../../../common/endpoint/types'; import { @@ -143,7 +143,7 @@ export class Fetcher { return tree; } - private async getNode(entityID: string): Promise { + private async getNode(entityID: string): Promise { const query = new LifecycleQuery(this.eventsIndexPattern, this.endpointID); const results = await query.search(this.client, entityID); if (results.length === 0) { @@ -186,7 +186,7 @@ export class Fetcher { // bucket the start and end events together for a single node const ancestryNodes = results.reduce( - (nodes: Map, ancestorEvent: ResolverEvent) => { + (nodes: Map, ancestorEvent: ResolverEvent) => { const nodeId = entityId(ancestorEvent); let node = nodes.get(nodeId); if (!node) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts index 58aa9efc1fc567..57a2ebfcc17929 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts @@ -7,10 +7,10 @@ import { ResolverEvent, ResolverAncestry, - LifecycleNode, + ResolverLifecycleNode, ResolverRelatedEvents, ResolverTree, - ChildNode, + ResolverChildNode, ResolverRelatedAlerts, } from '../../../../../common/endpoint/types'; @@ -49,7 +49,7 @@ export function createRelatedAlerts( * * @param entityID the entity_id of the child */ -export function createChild(entityID: string): ChildNode { +export function createChild(entityID: string): ResolverChildNode { const lifecycle = createLifecycle(entityID, []); return { ...lifecycle, @@ -70,7 +70,10 @@ export function createAncestry(): ResolverAncestry { * @param id the entity_id that these lifecycle nodes should have * @param lifecycle an array of lifecycle events */ -export function createLifecycle(entityID: string, lifecycle: ResolverEvent[]): LifecycleNode { +export function createLifecycle( + entityID: string, + lifecycle: ResolverEvent[] +): ResolverLifecycleNode { return { entityID, lifecycle }; } diff --git a/x-pack/plugins/security_solution/server/index.ts b/x-pack/plugins/security_solution/server/index.ts index 8a77137c20c115..06b35213b4713a 100644 --- a/x-pack/plugins/security_solution/server/index.ts +++ b/x-pack/plugins/security_solution/server/index.ts @@ -4,15 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { Plugin, PluginSetup, PluginStart } from './plugin'; import { configSchema, ConfigType } from './config'; +import { SIGNALS_INDEX_KEY } from '../common/constants'; export const plugin = (context: PluginInitializerContext) => { return new Plugin(context); }; -export const config = { schema: configSchema }; +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('xpack.siem.enabled', 'xpack.securitySolution.enabled'), + renameFromRoot( + 'xpack.siem.maxRuleImportExportSize', + 'xpack.securitySolution.maxRuleImportExportSize' + ), + renameFromRoot( + 'xpack.siem.maxRuleImportPayloadBytes', + 'xpack.securitySolution.maxRuleImportPayloadBytes' + ), + renameFromRoot( + 'xpack.siem.maxTimelineImportExportSize', + 'xpack.securitySolution.maxTimelineImportExportSize' + ), + renameFromRoot( + 'xpack.siem.maxTimelineImportPayloadBytes', + 'xpack.securitySolution.maxTimelineImportPayloadBytes' + ), + renameFromRoot( + `xpack.siem.${SIGNALS_INDEX_KEY}`, + `xpack.securitySolution.${SIGNALS_INDEX_KEY}` + ), + ], +}; export { ConfigType, Plugin, PluginSetup, PluginStart }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index 09acf4fe1ef68d..fe592aadb37a5d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -136,14 +136,14 @@ export const ThresholdExpression = ({ ) : null} 0 || !threshold[i]} + isInvalid={errors[`threshold${i}`]?.length > 0 || !threshold[i]} error={errors[`threshold${i}`]} > 0 || !threshold[i]} + isInvalid={errors[`threshold${i}`]?.length > 0 || !threshold[i]} onChange={(e) => { const { value } = e.target; const thresholdVal = value !== '' ? parseFloat(value) : undefined; diff --git a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts b/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts index b239ab41e41f12..d2e99a80ef8a13 100644 --- a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts +++ b/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts @@ -10,6 +10,7 @@ import { eventsIndexPattern, alertsIndexPattern, policyIndexPattern, + metadataMirrorIndexPattern, } from '../../../../plugins/security_solution/common/endpoint/constants'; export async function deleteDataStream(getService: (serviceName: 'es') => Client, index: string) { @@ -29,6 +30,10 @@ export async function deleteMetadataStream(getService: (serviceName: 'es') => Cl await deleteDataStream(getService, metadataIndexPattern); } +export async function deleteMetadataMirrorStream(getService: (serviceName: 'es') => Client) { + await deleteDataStream(getService, metadataMirrorIndexPattern); +} + export async function deleteEventsStream(getService: (serviceName: 'es') => Client) { await deleteDataStream(getService, eventsIndexPattern); } diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 41531269ddeb95..0d77486e07536c 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { deleteMetadataStream } from './data_stream_helper'; +import { deleteMetadataMirrorStream, deleteMetadataStream } from './data_stream_helper'; /** * The number of host documents in the es archive. @@ -33,6 +33,40 @@ export default function ({ getService }: FtrProviderContext) { }); }); + describe('POST /api/endpoint/metadata when metadata mirror index contains unenrolled host', () => { + before(async () => { + await esArchiver.load('endpoint/metadata/unenroll_feature/metadata', { useCreate: true }); + await esArchiver.load('endpoint/metadata/unenroll_feature/metadata_mirror', { + useCreate: true, + }); + }); + + after(async () => { + await deleteMetadataStream(getService); + await deleteMetadataMirrorStream(getService); + }); + + it('metadata api should return only enrolled host', async () => { + const { body } = await supertest + .post('/api/endpoint/metadata') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + expect(body.total).to.eql(1); + expect(body.hosts.length).to.eql(1); + expect(body.request_page_size).to.eql(10); + expect(body.request_page_index).to.eql(0); + }); + + it('metadata api should return 400 when an unenrolled host is retrieved', async () => { + const { body } = await supertest + .get('/api/endpoint/metadata/1fdca33f-799f-49f4-939c-ea4383c77671') + .send() + .expect(400); + expect(body.message).to.eql('the requested endpoint is unenrolled'); + }); + }); + describe('POST /api/endpoint/metadata when index is not empty', () => { before( async () => await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }) diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index 67b828b8df30ec..eeca8ee54e32f3 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -6,8 +6,8 @@ import _ from 'lodash'; import expect from '@kbn/expect'; import { - ChildNode, - LifecycleNode, + ResolverChildNode, + ResolverLifecycleNode, ResolverAncestry, ResolverEvent, ResolverRelatedEvents, @@ -35,7 +35,7 @@ import { Options, GeneratedTrees } from '../../services/resolver'; * @param node a lifecycle node containing the start and end events for a node * @param nodeMap a map of entity_ids to nodes to look for the passed in `node` */ -const expectLifecycleNodeInMap = (node: LifecycleNode, nodeMap: Map) => { +const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map) => { const genNode = nodeMap.get(node.entityID); expect(genNode).to.be.ok(); compareArrays(genNode!.lifecycle, node.lifecycle, true); @@ -49,7 +49,11 @@ const expectLifecycleNodeInMap = (node: LifecycleNode, nodeMap: Map { +const verifyAncestry = ( + ancestors: ResolverLifecycleNode[], + tree: Tree, + verifyLastParent: boolean +) => { // group the ancestors by their entity_id mapped to a lifecycle node const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); // group by parent entity_id @@ -97,7 +101,7 @@ const verifyAncestry = (ancestors: LifecycleNode[], tree: Tree, verifyLastParent * * @param ancestors an array of ancestor nodes */ -const retrieveDistantAncestor = (ancestors: LifecycleNode[]) => { +const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { // group the ancestors by their entity_id mapped to a lifecycle node const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); let node = ancestors[0]; @@ -124,7 +128,7 @@ const retrieveDistantAncestor = (ancestors: LifecycleNode[]) => { * @param childrenPerParent an optional number to compare that there are a certain number of children for each parent */ const verifyChildren = ( - children: ChildNode[], + children: ResolverChildNode[], tree: Tree, numberOfParents?: number, childrenPerParent?: number @@ -210,7 +214,7 @@ const verifyStats = ( * @param categories the related event info used when generating the resolver tree */ const verifyLifecycleStats = ( - nodes: LifecycleNode[], + nodes: ResolverLifecycleNode[], categories: RelatedEventInfo[], relatedAlerts: number ) => { diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index 2a0327ff57104e..d135c43e2302c2 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -26,7 +26,7 @@ import { import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration'; import { InfraLogSourceConfigurationProvider } from './infra_log_source_configuration'; import { MachineLearningProvider } from './ml'; -import { IngestManagerProvider } from './ingest_manager'; +import { IngestManagerProvider } from '../../common/services/ingest_manager'; import { ResolverGeneratorProvider } from './resolver'; import { TransformProvider } from './transform'; diff --git a/x-pack/test/common/services/index.ts b/x-pack/test/common/services/index.ts index 079687577c3e53..41b01bffbd8898 100644 --- a/x-pack/test/common/services/index.ts +++ b/x-pack/test/common/services/index.ts @@ -5,11 +5,13 @@ */ import { services as kibanaCommonServices } from '../../../../test/common/services'; +import { services as kibanaApiIntegrationServices } from '../../../../test/api_integration/services'; import { SpacesServiceProvider } from './spaces'; export const services = { ...kibanaCommonServices, + supertest: kibanaApiIntegrationServices.supertest, spaces: SpacesServiceProvider, }; diff --git a/x-pack/test/api_integration/services/ingest_manager.ts b/x-pack/test/common/services/ingest_manager.ts similarity index 100% rename from x-pack/test/api_integration/services/ingest_manager.ts rename to x-pack/test/common/services/ingest_manager.ts diff --git a/x-pack/test/encrypted_saved_objects_api_integration/config.ts b/x-pack/test/encrypted_saved_objects_api_integration/config.ts index fb643c2c5a901b..f061a38b72ce6d 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/config.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; +import path from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; @@ -18,12 +18,16 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { junit: { reportName: 'X-Pack Encrypted Saved Objects API Integration Tests', }, + esArchiver: { + directory: path.join(__dirname, 'fixtures', 'es_archiver'), + }, esTestCluster: xPackAPITestsConfig.get('esTestCluster'), kbnTestServer: { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), - `--plugin-path=${resolve(__dirname, './fixtures/api_consumer_plugin')}`, + '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', + `--plugin-path=${path.resolve(__dirname, './fixtures/api_consumer_plugin')}`, ], }, }; diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts index 7fb4de9ae4dc17..87bed7f4160191 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts @@ -9,6 +9,7 @@ import { CoreSetup, PluginInitializer, SavedObjectsNamespaceType, + SavedObjectUnsanitizedDoc, } from '../../../../../../src/core/server'; import { EncryptedSavedObjectsPluginSetup, @@ -23,6 +24,17 @@ const SAVED_OBJECT_WITH_SECRET_AND_MULTIPLE_SPACES_TYPE = 'saved-object-with-secret-and-multiple-spaces'; const SAVED_OBJECT_WITHOUT_SECRET_TYPE = 'saved-object-without-secret'; +const SAVED_OBJECT_WITH_MIGRATION_TYPE = 'saved-object-with-migration'; +interface MigratedTypePre790 { + nonEncryptedAttribute: string; + encryptedAttribute: string; +} +interface MigratedType { + nonEncryptedAttribute: string; + encryptedAttribute: string; + additionalEncryptedAttribute: string; +} + export interface PluginsSetup { encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; spaces: SpacesPluginSetup; @@ -34,7 +46,7 @@ export interface PluginsStart { } export const plugin: PluginInitializer = () => ({ - setup(core: CoreSetup, deps) { + setup(core: CoreSetup, deps: PluginsSetup) { for (const [name, namespaceType, hidden] of [ [SAVED_OBJECT_WITH_SECRET_TYPE, 'single', false], [HIDDEN_SAVED_OBJECT_WITH_SECRET_TYPE, 'single', true], @@ -71,6 +83,8 @@ export const plugin: PluginInitializer = mappings: deepFreeze({ properties: { publicProperty: { type: 'keyword' } } }), }); + defineTypeWithMigration(core, deps); + const router = core.http.createRouter(); router.get( { @@ -103,3 +117,83 @@ export const plugin: PluginInitializer = start() {}, stop() {}, }); + +function defineTypeWithMigration(core: CoreSetup, deps: PluginsSetup) { + const typePriorTo790 = { + type: SAVED_OBJECT_WITH_MIGRATION_TYPE, + attributesToEncrypt: new Set(['encryptedAttribute']), + }; + + // current type is registered + deps.encryptedSavedObjects.registerType({ + type: SAVED_OBJECT_WITH_MIGRATION_TYPE, + attributesToEncrypt: new Set(['encryptedAttribute', 'additionalEncryptedAttribute']), + }); + + core.savedObjects.registerType({ + name: SAVED_OBJECT_WITH_MIGRATION_TYPE, + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + nonEncryptedAttribute: { + type: 'keyword', + }, + encryptedAttribute: { + type: 'binary', + }, + additionalEncryptedAttribute: { + type: 'keyword', + }, + }, + }, + migrations: { + // in this version we migrated a non encrypted field and type didnt change + '7.8.0': deps.encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectUnsanitizedDoc => { + const { + attributes: { nonEncryptedAttribute }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + nonEncryptedAttribute: `${nonEncryptedAttribute}-migrated`, + }, + }; + }, + // type hasn't changed as the field we're updating is not an encrypted one + typePriorTo790, + typePriorTo790 + ), + // in this version we encrypted an existing non encrypted field + '7.9.0': deps.encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectUnsanitizedDoc => { + const { + attributes: { nonEncryptedAttribute }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + nonEncryptedAttribute, + // clone and modify the non encrypted field + additionalEncryptedAttribute: `${nonEncryptedAttribute}-encrypted`, + }, + }; + }, + typePriorTo790 + ), + }, + }); +} diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json new file mode 100644 index 00000000000000..88ec54cdf3a54e --- /dev/null +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json @@ -0,0 +1,370 @@ +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "buildNum": 9007199254740991 + }, + "migrationVersion": { + "config": "7.9.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2020-06-17T15:03:14.532Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [ + ], + "space": { + "_reserved": true, + "color": "#00bfb3", + "description": "This is your default space!", + "disabledFeatures": [ + ], + "name": "Default" + }, + "type": "space", + "updated_at": "2020-06-17T15:03:27.426Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "apm-telemetry:apm-telemetry", + "index": ".kibana_1", + "source": { + "apm-telemetry": { + "agents": { + }, + "cardinality": { + "transaction": { + "name": { + "all_agents": { + "1d": 0 + }, + "rum": { + "1d": 0 + } + } + }, + "user_agent": { + "original": { + "all_agents": { + "1d": 0 + }, + "rum": { + "1d": 0 + } + } + } + }, + "counts": { + "agent_configuration": { + "all": 0 + }, + "error": { + "1d": 0, + "all": 0 + }, + "max_error_groups_per_service": { + "1d": 0 + }, + "max_transaction_groups_per_service": { + "1d": 0 + }, + "metric": { + "1d": 0, + "all": 0 + }, + "onboarding": { + "1d": 0, + "all": 0 + }, + "services": { + "1d": 0 + }, + "sourcemap": { + "1d": 0, + "all": 0 + }, + "span": { + "1d": 0, + "all": 0 + }, + "traces": { + "1d": 0 + }, + "transaction": { + "1d": 0, + "all": 0 + } + }, + "has_any_services": false, + "indices": { + "all": { + "total": { + "docs": { + "count": 0 + }, + "store": { + "size_in_bytes": 416 + } + } + }, + "shards": { + "total": 2 + } + }, + "integrations": { + "ml": { + "all_jobs_count": 0 + } + }, + "services_per_agent": { + "dotnet": 0, + "go": 0, + "java": 0, + "js-base": 0, + "nodejs": 0, + "python": 0, + "ruby": 0, + "rum-js": 0 + }, + "tasks": { + "agent_configuration": { + "took": { + "ms": 21 + } + }, + "agents": { + "took": { + "ms": 65 + } + }, + "cardinality": { + "took": { + "ms": 80 + } + }, + "groupings": { + "took": { + "ms": 25 + } + }, + "indices_stats": { + "took": { + "ms": 65 + } + }, + "integrations": { + "took": { + "ms": 108 + } + }, + "processor_events": { + "took": { + "ms": 113 + } + }, + "services": { + "took": { + "ms": 98 + } + }, + "versions": { + "took": { + "ms": 6 + } + } + } + }, + "references": [ + ], + "type": "apm-telemetry", + "updated_at": "2020-06-17T15:03:47.184Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "saved-object-with-migration:74f3e6d7-b7bb-477d-ac28-92ee22728e6e", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "JuDwwSjflpKmPKUIfjgo04E0DW9iyhp8C94hwvflgkS0SUUPt+862FQ1eja4VEfEG7HVUt7xxj+BWeZv9vrf4olxgbr4/f5RrT8BVic0EOVS9nhspiDVEv12mV0uDWGtdneB/UWyaZg+0Qr0tPrwceSl8BS///U=", + "nonEncryptedAttribute": "elastic" + }, + "migrationVersion": { + "saved-object-with-migration": "7.7.0" + }, + "references": [ + ], + "type": "saved-object-with-migration", + "updated_at": "2020-06-17T15:35:39.839Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:5f01fd40-b0b0-11ea-9510-fdf248d5f2a4", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 1.60245, + "numberOfClicks": 6, + "timestamp": "2020-06-17T15:36:54.292Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-06-17T15:36:54.292Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:4ca5ac00-b0b0-11ea-9510-fdf248d5f2a4", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "home", + "minutesOnScreen": 0.4106666666666667, + "numberOfClicks": 3, + "timestamp": "2020-06-17T15:36:23.487Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-06-17T15:36:23.488Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:kibana-user_agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", + "index": ".kibana_1", + "source": { + "references": [ + ], + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2020-06-17T15:36:23.487Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:Kibana_home:sampleDataDecline", + "index": ".kibana_1", + "source": { + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2020-06-17T15:36:23.488Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:Kibana_home:welcomeScreenMount", + "index": ".kibana_1", + "source": { + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2020-06-17T15:36:23.488Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "telemetry:telemetry", + "index": ".kibana_1", + "source": { + "references": [ + ], + "telemetry": { + "lastReported": 1592408310031, + "reportFailureCount": 0, + "userHasSeenNotice": true + }, + "type": "telemetry", + "updated_at": "2020-06-17T15:38:30.031Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "maps-telemetry:maps-telemetry", + "index": ".kibana_1", + "source": { + "maps-telemetry": { + "attributesPerMap": { + "dataSourcesCount": { + "avg": 0, + "max": 0, + "min": 0 + }, + "emsVectorLayersCount": { + }, + "layerTypesCount": { + }, + "layersCount": { + "avg": 0, + "max": 0, + "min": 0 + } + }, + "indexPatternsWithGeoFieldCount": 0, + "indexPatternsWithGeoPointFieldCount": 0, + "indexPatternsWithGeoShapeFieldCount": 0, + "mapsTotalCount": 0, + "settings": { + "showMapVisualizationTypes": false + }, + "timeCaptured": "2020-06-17T16:29:27.563Z" + }, + "references": [ + ], + "type": "maps-telemetry", + "updated_at": "2020-06-17T16:29:27.563Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/mappings.json b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/mappings.json new file mode 100644 index 00000000000000..c025ad9da1a9cb --- /dev/null +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/mappings.json @@ -0,0 +1,2413 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "todo": "082a2cc96a590268344d5cd74c159ac4", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "saved-object-with-migration": { + "properties": { + "encryptedAttribute": { + "type": "binary" + }, + "nonEncryptedAttribute": { + "type": "keyword" + }, + "additionalEncryptedAttribute": { + "type": "binary" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoPointFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoShapeFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "alert": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "saved-object-with-migration": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "todo": { + "properties": { + "icon": { + "type": "keyword" + }, + "task": { + "type": "text" + }, + "title": { + "type": "keyword" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "type": "keyword" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "integer" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "type": "keyword" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "type": "keyword" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts index 6b3ae620117046..8bdc1715bf487b 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts @@ -12,6 +12,7 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const randomness = getService('randomness'); const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; const HIDDEN_SAVED_OBJECT_WITH_SECRET_TYPE = 'hidden-saved-object-with-secret'; @@ -501,5 +502,32 @@ export default function ({ getService }: FtrProviderContext) { ); }); }); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('encrypted_saved_objects'); + }); + + after(async () => { + await esArchiver.unload('encrypted_saved_objects'); + }); + + it('migrates unencrypted fields on saved objects', async () => { + const { body: decryptedResponse } = await supertest + .get( + `/api/saved_objects/get-decrypted-as-internal-user/saved-object-with-migration/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ) + .expect(200); + + expect(decryptedResponse.attributes).to.eql({ + // ensures the encrypted field can still be decrypted after the migration + encryptedAttribute: 'this is my secret api key', + // ensures the non-encrypted field has been migrated in 7.8.0 + nonEncryptedAttribute: 'elastic-migrated', + // ensures the non-encrypted field has been migrated into a new encrypted field in 7.9.0 + additionalEncryptedAttribute: 'elastic-migrated-encrypted', + }); + }); + }); }); } diff --git a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts index 3483ddf769e5fc..bf2a4192af5437 100644 --- a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts +++ b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts @@ -12,15 +12,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const aceEditor = getService('aceEditor'); const retry = getService('retry'); + const security = getService('security'); const editorTestSubjectSelector = 'searchProfilerEditor'; describe('Search Profiler Editor', () => { before(async () => { + await security.testUser.setRoles(['global_devtools_read']); await PageObjects.common.navigateToApp('searchProfiler'); expect(await testSubjects.exists('searchProfilerEditor')).to.be(true); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('correctly parses triple quotes in JSON', async () => { // The below inputs are written to work _with_ ace's autocomplete unlike console's unit test // counterparts in src/legacy/core_plugins/console/public/tests/src/editor.test.js diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index d5e3f82878d6b5..14e05d21b87535 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -231,6 +231,17 @@ export default async function ({ readConfigFile }) { ], }, + global_devtools_read: { + kibana: [ + { + feature: { + dev_tools: ['read'], + }, + spaces: ['*'], + }, + ], + }, + //Kibana feature privilege isn't specific to advancedSetting. It can be anything. https://github.com/elastic/kibana/issues/35965 test_api_keys: { elasticsearch: { diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz new file mode 100644 index 00000000000000..d7b130e4051569 Binary files /dev/null and b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz new file mode 100644 index 00000000000000..3b4da7c47d9f22 Binary files /dev/null and b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz differ diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index bbc78cf399f711..730beb9143406b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { deleteMetadataStream } from '../../../api_integration/apis/endpoint/data_stream_helper'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'endpoint', 'header', 'endpointPageUtils']); @@ -17,11 +18,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { this.tags('ciGroup7'); const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)); before(async () => { - await esArchiver.load('endpoint/metadata/api_feature'); + await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }); await pageObjects.endpoint.navigateToEndpointList(); }); - it('finds title', async () => { + it('finds page title', async () => { const title = await testSubjects.getVisibleText('pageViewHeaderLeftTitle'); expect(title).to.equal('Endpoints'); }); @@ -77,54 +78,61 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(tableData).to.eql(expectedData); }); - it('no details flyout when endpoint page displayed', async () => { + it('does not show the details flyout initially', async () => { await testSubjects.missingOrFail('hostDetailsFlyout'); }); - it('display details flyout when the hostname is clicked on', async () => { - await (await testSubjects.find('hostnameCellLink')).click(); - await testSubjects.existOrFail('hostDetailsUpperList'); - await testSubjects.existOrFail('hostDetailsLowerList'); - }); + describe('when the hostname is clicked on,', () => { + it('display the details flyout', async () => { + await (await testSubjects.find('hostnameCellLink')).click(); + await testSubjects.existOrFail('hostDetailsUpperList'); + await testSubjects.existOrFail('hostDetailsLowerList'); + }); - it('update details flyout when new hostname is clicked on', async () => { - // display flyout for the first host in the list - await (await testSubjects.findAll('hostnameCellLink'))[0].click(); - await testSubjects.existOrFail('hostDetailsFlyoutTitle'); - const hostDetailTitle0 = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); - // select the 2nd host in the host list - await (await testSubjects.findAll('hostnameCellLink'))[1].click(); - await pageObjects.endpoint.waitForVisibleTextToChange( - 'hostDetailsFlyoutTitle', - hostDetailTitle0 - ); - const hostDetailTitle1 = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); - expect(hostDetailTitle1).to.not.eql(hostDetailTitle0); - }); + it('updates the details flyout when a new hostname is selected from the list', async () => { + // display flyout for the first host in the list + await (await testSubjects.findAll('hostnameCellLink'))[0].click(); + await testSubjects.existOrFail('hostDetailsFlyoutTitle'); + const hostDetailTitle0 = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); + // select the 2nd host in the host list + await (await testSubjects.findAll('hostnameCellLink'))[1].click(); + await pageObjects.endpoint.waitForVisibleTextToChange( + 'hostDetailsFlyoutTitle', + hostDetailTitle0 + ); + const hostDetailTitle1 = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); + expect(hostDetailTitle1).to.not.eql(hostDetailTitle0); + }); + + it('has the same flyout info when the same hostname is selected', async () => { + // display flyout for the first host in the list + await (await testSubjects.findAll('hostnameCellLink'))[1].click(); + await testSubjects.existOrFail('hostDetailsFlyoutTitle'); + const hostDetailTitleInitial = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); + // select the same host in the host list + await (await testSubjects.findAll('hostnameCellLink'))[1].click(); + await sleep(500); // give page time to refresh and verify it did not change + const hostDetailTitleNew = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); + expect(hostDetailTitleNew).to.equal(hostDetailTitleInitial); + }); - it('details flyout remains the same when current hostname is clicked on', async () => { - // display flyout for the first host in the list - await (await testSubjects.findAll('hostnameCellLink'))[1].click(); - await testSubjects.existOrFail('hostDetailsFlyoutTitle'); - const hostDetailTitleInitial = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); - // select the same host in the host list - await (await testSubjects.findAll('hostnameCellLink'))[1].click(); - await sleep(500); // give page time to refresh and verify it did not change - const hostDetailTitleNew = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); - expect(hostDetailTitleNew).to.equal(hostDetailTitleInitial); + it('navigates to ingest fleet when the Reassign Policy link is clicked', async () => { + await (await testSubjects.find('hostDetailsLinkToIngest')).click(); + await testSubjects.existOrFail('fleetAgentListTable'); + }); }); - describe('no data', () => { + describe('when there is no data,', () => { before(async () => { // clear out the data and reload the page - await esArchiver.unload('endpoint/metadata/api_feature'); + await deleteMetadataStream(getService); await pageObjects.endpoint.navigateToEndpointList(); }); after(async () => { // reload the data so the other tests continue to pass - await esArchiver.load('endpoint/metadata/api_feature'); + await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }); }); - it('displays no items found when empty', async () => { + it('displays No items found when empty', async () => { // get the endpoint list table data and verify message const [, [noItemsFoundMessage]] = await pageObjects.endpointPageUtils.tableData( 'hostListTable' @@ -166,7 +174,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Windows 10', '', '0', - '00000000-0000-0000-0000-000000000000', + 'Default', 'Unknown', '10.101.149.262606:a000:ffc0:39:11ef:37b9:3371:578c', 'rezzani-7.example.com', @@ -175,7 +183,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); after(async () => { - await esArchiver.unload('endpoint/metadata/api_feature'); + await deleteMetadataStream(getService); }); }); }; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 036f82a591fb3f..b0c161ca1d0c24 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -9,7 +9,13 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const pageObjects = getPageObjects(['common', 'endpoint', 'policy', 'endpointPageUtils']); + const pageObjects = getPageObjects([ + 'common', + 'endpoint', + 'policy', + 'endpointPageUtils', + 'ingestManagerCreateDatasource', + ]); const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); @@ -185,5 +191,38 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); }); + + describe('when on Ingest Configurations Edit Datasource page', async () => { + let policyInfo: PolicyTestResourceInfo; + beforeEach(async () => { + // Create a policy and navigate to Ingest app + policyInfo = await policyTestResources.createPolicy(); + await pageObjects.ingestManagerCreateDatasource.navigateToAgentConfigEditDatasource( + policyInfo.agentConfig.id, + policyInfo.datasource.id + ); + }); + afterEach(async () => { + if (policyInfo) { + await policyInfo.cleanup(); + } + }); + it('should show a link to Policy Details', async () => { + await testSubjects.existOrFail('editLinkToPolicyDetails'); + }); + it('should navigate to Policy Details when the link is clicked', async () => { + const linkToPolicy = await testSubjects.find('editLinkToPolicyDetails'); + await linkToPolicy.click(); + await pageObjects.policy.ensureIsOnDetailsPage(); + }); + it('should allow the user to navigate, edit and save Policy Details', async () => { + await (await testSubjects.find('editLinkToPolicyDetails')).click(); + await pageObjects.policy.ensureIsOnDetailsPage(); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); + await pageObjects.policy.confirmAndSave(); + + await testSubjects.existOrFail('policyDetailsSuccessMessage'); + }); + }); }); } diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts index f50cde6285be72..e104b8701276c4 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts @@ -6,13 +6,14 @@ import { FtrProviderContext } from '../ftr_provider_context'; -export function IngestManagerCreateDatasource({ getService }: FtrProviderContext) { +export function IngestManagerCreateDatasource({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); + const pageObjects = getPageObjects(['common']); return { /** - * Validates that the page shown is the Datasource Craete Page + * Validates that the page shown is the Datasource Create Page */ async ensureOnCreatePageOrFail() { await testSubjects.existOrFail('createDataSource_header'); @@ -75,5 +76,22 @@ export function IngestManagerCreateDatasource({ getService }: FtrProviderContext async waitForSaveSuccessNotification() { await testSubjects.existOrFail('datasourceCreateSuccessToast'); }, + + /** + * Validates that the page shown is the Datasource Edit Page + */ + async ensureOnEditPageOrFail() { + await testSubjects.existOrFail('editDataSource_header'); + }, + + /** + * Navigates to the Ingest Agent configuration Edit Datasource page + */ + async navigateToAgentConfigEditDatasource(agentConfigId: string, datasourceId: string) { + await pageObjects.common.navigateToApp('ingestManager', { + hash: `/configs/${agentConfigId}/edit-datasource/${datasourceId}`, + }); + await this.ensureOnEditPageOrFail(); + }, }; } diff --git a/x-pack/test/security_solution_endpoint/services/index.ts b/x-pack/test/security_solution_endpoint/services/index.ts index 90b4bc0b4d0457..7eecae41aae4a3 100644 --- a/x-pack/test/security_solution_endpoint/services/index.ts +++ b/x-pack/test/security_solution_endpoint/services/index.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { services as apiIntegrationServices } from '../../api_integration/services'; import { services as xPackFunctionalServices } from '../../functional/services'; import { EndpointPolicyTestResourcesProvider } from './endpoint_policy'; +import { IngestManagerProvider } from '../../common/services/ingest_manager'; export const services = { ...xPackFunctionalServices, - ingestManager: apiIntegrationServices.ingestManager, policyTestResources: EndpointPolicyTestResourcesProvider, + ingestManager: IngestManagerProvider, }; diff --git a/yarn.lock b/yarn.lock index bb13ee8105e0dd..93db6de88775cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2297,6 +2297,11 @@ resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-1.1.1.tgz#619b70322c9cce4a7ee5fbf8f678b1baa7f06095" integrity sha512-F6tIk8Txdqjg8Siv60iAvXzO9ZdQI87K3sS/fh5xd2XaWK+T5ZfqeTvsT7srwG6fr6uCBfuQEJV1KBBl+JpLZA== +"@elastic/node-crypto@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-1.2.1.tgz#dfd9218f9b5729fa519762e6a6968aaf61b86eb0" + integrity sha512-RlZg+poLA2SwZZUM5RMJDJiKojlSB1mJkumIvLgXvvTCcCliC6rM0lUaNecV9pbQLIHrGlX2BrbwiuPWhv0czQ== + "@elastic/numeral@^2.5.0": version "2.5.0" resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.5.0.tgz#8da714827fc278f17546601fdfe55f5c920e2bc5"