From c4504928e8e4dfe9a29ffffb32fe92c44175f562 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 2 Mar 2021 14:13:01 +0100 Subject: [PATCH 01/63] [Lens] Fix Workspace hidden when using Safari (#92616) (#93193) Co-authored-by: Marta Bondyra Co-authored-by: Marta Bondyra --- .../__snapshots__/drag_drop.test.tsx.snap | 14 +++++--------- .../plugins/lens/public/drag_drop/drag_drop.scss | 7 ------- x-pack/plugins/lens/public/drag_drop/drag_drop.tsx | 4 ++-- .../workspace_panel/workspace_panel_wrapper.scss | 1 + 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap index e5594bb0bb7699..7aa838021f2a85 100644 --- a/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap +++ b/x-pack/plugins/lens/public/drag_drop/__snapshots__/drag_drop.test.tsx.snap @@ -1,16 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DragDrop defined dropType is reflected in the className 1`] = ` -
- -
+ Hello! + `; exports[`DragDrop items that has dropType=undefined get special styling when another item is dragged 1`] = ` diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index 9e3f1e1c3cf264..961f7ee0ec4009 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -81,13 +81,6 @@ } } -.lnsDragDrop__container { - position: relative; - overflow: visible !important; // sass-lint:disable-line no-important - width: 100%; - height: 100%; -} - .lnsDragDrop__reorderableDrop { position: absolute; width: 100%; diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 618a7accb9b2b1..76e44c29eaed5f 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -456,7 +456,7 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { isActiveDropTarget && dropType !== 'reorder' && dragging?.ghost ? dragging.ghost : undefined; return ( -
+ <> {React.cloneElement(children, { 'data-test-subj': dataTestSubj || 'lnsDragDrop', className: classNames(children.props.className, classes, className), @@ -471,7 +471,7 @@ const DropInner = memo(function DropInner(props: DropInnerProps) { style: ghost.style, }) : null} -
+ ); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss index 3949c7deb53b4a..167c17ee6ae9c5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.scss @@ -11,6 +11,7 @@ min-height: $euiSizeXXL * 10; overflow: visible; border: none; + height: 100%; .lnsWorkspacePanelWrapper__pageContentBody { @include euiScrollBar; From 30325b18ce2b62b5ef0b1b12ee6c89894ad04fb0 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Mar 2021 09:50:41 -0500 Subject: [PATCH 02/63] [ML] Transforms: Fixes chart histograms for runtime fields. (#93028) (#93202) Fixes chart histograms for runtime fields. The runtime field configurations were not passed on to the endpoint to fetch the charts data, so charts ended up being empty with a 0 documents legend. Co-authored-by: Walter Rafelsberger --- x-pack/plugins/ml/common/index.ts | 1 + .../ml/common/types/field_histograms.ts | 68 ++++++++++++++++++ .../components/data_grid/column_chart.tsx | 4 +- .../application/components/data_grid/index.ts | 2 +- .../application/components/data_grid/types.ts | 2 +- .../data_grid/use_column_chart.test.tsx | 8 +-- .../components/data_grid/use_column_chart.tsx | 71 +++---------------- .../components/data_grid/use_data_grid.tsx | 2 +- .../boolean_content_preview.tsx | 4 +- .../field_data_row/top_values_preview.tsx | 5 +- .../models/data_visualizer/data_visualizer.ts | 15 ++-- .../transform/common/api_schemas/common.ts | 24 +++++++ .../common/api_schemas/field_histograms.ts | 8 ++- .../common/api_schemas/transforms.ts | 26 +------ .../transform/common/shared_imports.ts | 8 ++- .../transform/public/app/hooks/use_api.ts | 7 +- .../public/app/hooks/use_index_data.ts | 3 +- .../advanced_runtime_mappings_editor.tsx | 5 +- .../common/get_pivot_dropdown_options.ts | 4 +- .../step_define/common/types.test.ts | 71 +++++++++++++++++++ .../components/step_define/common/types.ts | 26 ++++++- .../server/routes/api/field_histograms.ts | 5 +- 22 files changed, 250 insertions(+), 119 deletions(-) create mode 100644 x-pack/plugins/ml/common/types/field_histograms.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index c049b68990d2d9..ac21954118e502 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -6,6 +6,7 @@ */ export { HitsTotalRelation, SearchResponse7, HITS_TOTAL_RELATION } from './types/es_client'; +export { ChartData } from './types/field_histograms'; export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from './constants/anomalies'; export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; export { composeValidators, patternValidator } from './util/validators'; diff --git a/x-pack/plugins/ml/common/types/field_histograms.ts b/x-pack/plugins/ml/common/types/field_histograms.ts new file mode 100644 index 00000000000000..22b0195a579acd --- /dev/null +++ b/x-pack/plugins/ml/common/types/field_histograms.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface NumericDataItem { + key: number; + key_as_string?: string | number; + doc_count: number; +} + +export interface NumericChartData { + data: NumericDataItem[]; + id: string; + interval: number; + stats: [number, number]; + type: 'numeric'; +} + +export const isNumericChartData = (arg: any): arg is NumericChartData => { + return ( + typeof arg === 'object' && + arg.hasOwnProperty('data') && + arg.hasOwnProperty('id') && + arg.hasOwnProperty('interval') && + arg.hasOwnProperty('stats') && + arg.hasOwnProperty('type') && + arg.type === 'numeric' + ); +}; + +export interface OrdinalDataItem { + key: string; + key_as_string?: string; + doc_count: number; +} + +export interface OrdinalChartData { + cardinality: number; + data: OrdinalDataItem[]; + id: string; + type: 'ordinal' | 'boolean'; +} + +export const isOrdinalChartData = (arg: any): arg is OrdinalChartData => { + return ( + typeof arg === 'object' && + arg.hasOwnProperty('data') && + arg.hasOwnProperty('cardinality') && + arg.hasOwnProperty('id') && + arg.hasOwnProperty('type') && + (arg.type === 'ordinal' || arg.type === 'boolean') + ); +}; + +export interface UnsupportedChartData { + id: string; + type: 'unsupported'; +} + +export const isUnsupportedChartData = (arg: any): arg is UnsupportedChartData => { + return typeof arg === 'object' && arg.hasOwnProperty('type') && arg.type === 'unsupported'; +}; + +export type ChartDataItem = NumericDataItem | OrdinalDataItem; +export type ChartData = NumericChartData | OrdinalChartData | UnsupportedChartData; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx index 102ccc560ba933..3800256927d542 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx @@ -13,7 +13,9 @@ import { EuiDataGridColumn } from '@elastic/eui'; import './column_chart.scss'; -import { isUnsupportedChartData, useColumnChart, ChartData } from './use_column_chart'; +import { isUnsupportedChartData, ChartData } from '../../../../common/types/field_histograms'; + +import { useColumnChart } from './use_column_chart'; interface Props { chartData: ChartData; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index a3f1995736624e..be37e381d1bae8 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -16,7 +16,7 @@ export { useRenderCellValue, getProcessedFields, } from './common'; -export { getFieldType, ChartData } from './use_column_chart'; +export { getFieldType } from './use_column_chart'; export { useDataGrid } from './use_data_grid'; export { DataGrid } from './data_grid'; export { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts index 77c7bdb3854698..2fb47a59284a38 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -11,10 +11,10 @@ import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } fro import { Dictionary } from '../../../../common/types/common'; import { HitsTotalRelation } from '../../../../common/types/es_client'; +import { ChartData } from '../../../../common/types/field_histograms'; import { INDEX_STATUS } from '../../data_frame_analytics/common/analytics'; -import { ChartData } from './use_column_chart'; import { FeatureImportanceBaseline } from '../../../../common/types/feature_importance'; export type ColumnId = string; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx index 0154d43a068652..631c214dd751cf 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.test.tsx @@ -13,17 +13,15 @@ import '@testing-library/jest-dom/extend-expect'; import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { - getFieldType, - getLegendText, - getXScaleType, isNumericChartData, isOrdinalChartData, isUnsupportedChartData, - useColumnChart, NumericChartData, OrdinalChartData, UnsupportedChartData, -} from './use_column_chart'; +} from '../../../../common/types/field_histograms'; + +import { getFieldType, getLegendText, getXScaleType, useColumnChart } from './use_column_chart'; describe('getFieldType()', () => { it('should return the Kibana field type for a given EUI data grid schema', () => { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx index fcb8a20f558fd0..4764a1674df2f6 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx @@ -17,6 +17,15 @@ import { i18n } from '@kbn/i18n'; import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { + isNumericChartData, + isOrdinalChartData, + ChartData, + ChartDataItem, + NumericDataItem, + OrdinalDataItem, +} from '../../../../common/types/field_histograms'; + import { NON_AGGREGATABLE } from './common'; export const hoveredRow$ = new BehaviorSubject(null); @@ -66,68 +75,6 @@ export const getFieldType = (schema: EuiDataGridColumn['schema']): KBN_FIELD_TYP return fieldType; }; -interface NumericDataItem { - key: number; - key_as_string?: string | number; - doc_count: number; -} - -export interface NumericChartData { - data: NumericDataItem[]; - id: string; - interval: number; - stats: [number, number]; - type: 'numeric'; -} - -export const isNumericChartData = (arg: any): arg is NumericChartData => { - return ( - typeof arg === 'object' && - arg.hasOwnProperty('data') && - arg.hasOwnProperty('id') && - arg.hasOwnProperty('interval') && - arg.hasOwnProperty('stats') && - arg.hasOwnProperty('type') && - arg.type === 'numeric' - ); -}; - -export interface OrdinalDataItem { - key: string; - key_as_string?: string; - doc_count: number; -} - -export interface OrdinalChartData { - cardinality: number; - data: OrdinalDataItem[]; - id: string; - type: 'ordinal' | 'boolean'; -} - -export const isOrdinalChartData = (arg: any): arg is OrdinalChartData => { - return ( - typeof arg === 'object' && - arg.hasOwnProperty('data') && - arg.hasOwnProperty('cardinality') && - arg.hasOwnProperty('id') && - arg.hasOwnProperty('type') && - (arg.type === 'ordinal' || arg.type === 'boolean') - ); -}; - -export interface UnsupportedChartData { - id: string; - type: 'unsupported'; -} - -export const isUnsupportedChartData = (arg: any): arg is UnsupportedChartData => { - return typeof arg === 'object' && arg.hasOwnProperty('type') && arg.type === 'unsupported'; -}; - -export type ChartDataItem = NumericDataItem | OrdinalDataItem; -export type ChartData = NumericChartData | OrdinalChartData | UnsupportedChartData; - type LegendText = string | JSX.Element; export const getLegendText = ( chartData: ChartData, diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index 4129f3a01bce98..228ee86a7f800a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -10,6 +10,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; import { HITS_TOTAL_RELATION } from '../../../../common/types/es_client'; +import { ChartData } from '../../../../common/types/field_histograms'; import { INDEX_STATUS } from '../../data_frame_analytics/common'; @@ -26,7 +27,6 @@ import { RowCountRelation, UseDataGridReturnType, } from './types'; -import { ChartData } from './use_column_chart'; export const useDataGrid = ( columns: EuiDataGridColumn[], diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx index 17b2e042560989..70adbbe85bc583 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/boolean_content_preview.tsx @@ -7,10 +7,10 @@ import React, { FC, useMemo } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; +import { OrdinalChartData } from '../../../../../../common/types/field_histograms'; +import { ColumnChart } from '../../../../components/data_grid/column_chart'; import { FieldDataRowProps } from '../../types'; import { getTFPercentage } from '../../utils'; -import { ColumnChart } from '../../../../components/data_grid/column_chart'; -import { OrdinalChartData } from '../../../../components/data_grid/use_column_chart'; export const BooleanContentPreview: FC = ({ config }) => { const chartData = useMemo(() => { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx index 122663b304024b..07a2eae95c8900 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_table/components/field_data_row/top_values_preview.tsx @@ -7,10 +7,9 @@ import React, { FC } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; -import type { FieldDataRowProps } from '../../types/field_data_row'; +import { ChartData, OrdinalDataItem } from '../../../../../../common/types/field_histograms'; import { ColumnChart } from '../../../../components/data_grid/column_chart'; -import { ChartData } from '../../../../components/data_grid'; -import { OrdinalDataItem } from '../../../../components/data_grid/use_column_chart'; +import type { FieldDataRowProps } from '../../types/field_data_row'; export const TopValuesPreview: FC = ({ config }) => { const { stats } = config; diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 69ebfe5f0bc112..4db8295d939975 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -16,7 +16,7 @@ import { buildSamplerAggregation, getSamplerAggregationsResponsePath, } from '../../lib/query_utils'; -import { AggCardinality } from '../../../common/types/fields'; +import { AggCardinality, RuntimeMappings } from '../../../common/types/fields'; import { getDatafeedAggregations } from '../../../common/util/datafeed_utils'; import { Datafeed } from '../../../common/types/anomaly_detection_jobs'; import { isPopulatedObject } from '../../../common/util/object_utils'; @@ -183,7 +183,8 @@ const getAggIntervals = async ( indexPatternTitle: string, query: any, fields: HistogramField[], - samplerShardSize: number + samplerShardSize: number, + runtimeMappings?: RuntimeMappings ): Promise => { const numericColumns = fields.filter((field) => { return field.type === KBN_FIELD_TYPES.NUMBER || field.type === KBN_FIELD_TYPES.DATE; @@ -210,6 +211,7 @@ const getAggIntervals = async ( query, aggs: buildSamplerAggregation(minMaxAggs, samplerShardSize), size: 0, + ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, }); @@ -240,7 +242,8 @@ export const getHistogramsForFields = async ( indexPatternTitle: string, query: any, fields: HistogramField[], - samplerShardSize: number + samplerShardSize: number, + runtimeMappings?: RuntimeMappings ) => { const { asCurrentUser } = client; const aggIntervals = await getAggIntervals( @@ -248,7 +251,8 @@ export const getHistogramsForFields = async ( indexPatternTitle, query, fields, - samplerShardSize + samplerShardSize, + runtimeMappings ); const chartDataAggs = fields.reduce((aggs, field) => { @@ -293,6 +297,7 @@ export const getHistogramsForFields = async ( query, aggs: buildSamplerAggregation(chartDataAggs, samplerShardSize), size: 0, + ...(runtimeMappings !== undefined ? { runtime_mappings: runtimeMappings } : {}), }, }); @@ -607,7 +612,7 @@ export class DataVisualizer { // Value count aggregation faster way of checking if field exists than using // filter aggregation with exists query. const aggs: Aggs = datafeedAggregations !== undefined ? { ...datafeedAggregations } : {}; - const runtimeMappings: any = {}; + const runtimeMappings: { runtime_mappings?: RuntimeMappings } = {}; aggregatableFields.forEach((field, i) => { const safeFieldName = getSafeAggregationName(field, i); diff --git a/x-pack/plugins/transform/common/api_schemas/common.ts b/x-pack/plugins/transform/common/api_schemas/common.ts index 1841724c16ef97..3651af69359a98 100644 --- a/x-pack/plugins/transform/common/api_schemas/common.ts +++ b/x-pack/plugins/transform/common/api_schemas/common.ts @@ -53,3 +53,27 @@ export interface ResponseStatus { export interface CommonResponseStatusSchema { [key: string]: ResponseStatus; } + +export const runtimeMappingsSchema = schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + type: schema.oneOf([ + schema.literal('keyword'), + schema.literal('long'), + schema.literal('double'), + schema.literal('date'), + schema.literal('ip'), + schema.literal('boolean'), + ]), + script: schema.maybe( + schema.oneOf([ + schema.string(), + schema.object({ + source: schema.string(), + }), + ]) + ), + }) + ) +); diff --git a/x-pack/plugins/transform/common/api_schemas/field_histograms.ts b/x-pack/plugins/transform/common/api_schemas/field_histograms.ts index fc5a95590c7c6d..9f6f4c15d803ae 100644 --- a/x-pack/plugins/transform/common/api_schemas/field_histograms.ts +++ b/x-pack/plugins/transform/common/api_schemas/field_histograms.ts @@ -7,14 +7,20 @@ import { schema, TypeOf } from '@kbn/config-schema'; +import { ChartData } from '../shared_imports'; + +import { runtimeMappingsSchema } from './common'; + export const fieldHistogramsRequestSchema = schema.object({ /** Query to match documents in the index. */ query: schema.any(), /** The fields to return histogram data. */ fields: schema.arrayOf(schema.any()), + /** Optional runtime mappings */ + runtimeMappings: runtimeMappingsSchema, /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ samplerShardSize: schema.number(), }); export type FieldHistogramsRequestSchema = TypeOf; -export type FieldHistogramsResponseSchema = any[]; +export type FieldHistogramsResponseSchema = ChartData[]; diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts index d86df3af3e1d0a..4d25bd74f4e74a 100644 --- a/x-pack/plugins/transform/common/api_schemas/transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts @@ -14,7 +14,7 @@ import type { PivotAggDict } from '../types/pivot_aggs'; import type { PivotGroupByDict } from '../types/pivot_group_by'; import type { TransformId, TransformPivotConfig } from '../types/transform'; -import { transformStateSchema } from './common'; +import { transformStateSchema, runtimeMappingsSchema } from './common'; // GET transforms export const getTransformsRequestSchema = schema.arrayOf( @@ -64,30 +64,6 @@ export const settingsSchema = schema.object({ docs_per_second: schema.maybe(schema.nullable(schema.number())), }); -export const runtimeMappingsSchema = schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - type: schema.oneOf([ - schema.literal('keyword'), - schema.literal('long'), - schema.literal('double'), - schema.literal('date'), - schema.literal('ip'), - schema.literal('boolean'), - ]), - script: schema.maybe( - schema.oneOf([ - schema.string(), - schema.object({ - source: schema.string(), - }), - ]) - ), - }) - ) -); - export const sourceSchema = schema.object({ runtime_mappings: runtimeMappingsSchema, index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), diff --git a/x-pack/plugins/transform/common/shared_imports.ts b/x-pack/plugins/transform/common/shared_imports.ts index 4506083a1876f2..3062c7ab8d23cb 100644 --- a/x-pack/plugins/transform/common/shared_imports.ts +++ b/x-pack/plugins/transform/common/shared_imports.ts @@ -6,5 +6,9 @@ */ export type { HitsTotalRelation, SearchResponse7 } from '../../ml/common'; -export { HITS_TOTAL_RELATION } from '../../ml/common'; -export { composeValidators, patternValidator } from '../../ml/common'; +export { + composeValidators, + patternValidator, + ChartData, + HITS_TOTAL_RELATION, +} from '../../ml/common'; diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts index 388bc8b432fc4e..7afbc5e403b78c 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_api.ts @@ -16,7 +16,10 @@ import type { DeleteTransformsRequestSchema, DeleteTransformsResponseSchema, } from '../../../common/api_schemas/delete_transforms'; -import type { FieldHistogramsResponseSchema } from '../../../common/api_schemas/field_histograms'; +import type { + FieldHistogramsRequestSchema, + FieldHistogramsResponseSchema, +} from '../../../common/api_schemas/field_histograms'; import type { StartTransformsRequestSchema, StartTransformsResponseSchema, @@ -194,6 +197,7 @@ export const useApi = () => { indexPatternTitle: string, fields: FieldHistogramRequestConfig[], query: string | SavedSearchQuery, + runtimeMappings?: FieldHistogramsRequestSchema['runtimeMappings'], samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE ): Promise { try { @@ -202,6 +206,7 @@ export const useApi = () => { query, fields, samplerShardSize, + ...(runtimeMappings !== undefined ? { runtimeMappings } : {}), }), }); } catch (e) { diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index dde4c7eb0f3a01..e12aa78e33622d 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -150,7 +150,8 @@ export const useIndexData = ( fieldName: cT.id, type: getFieldType(cT.schema), })), - isDefaultQuery(query) ? matchAllQuery : query + isDefaultQuery(query) ? matchAllQuery : query, + combinedRuntimeMappings ); if (!isFieldHistogramsResponseSchema(columnChartsData)) { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx index 087bae97e287ef..23c4cfcefe8ef0 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx @@ -13,6 +13,7 @@ import { EuiCodeEditor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { StepDefineFormHook } from '../step_define'; +import { isRuntimeMappings } from '../step_define/common/types'; export const AdvancedRuntimeMappingsEditor: FC = memo( ({ @@ -43,8 +44,8 @@ export const AdvancedRuntimeMappingsEditor: FC]/g; @@ -77,7 +77,7 @@ export function getPivotDropdownOptions( // Support for runtime_mappings that are defined by queries let runtimeFields: Field[] = []; - if (isPopulatedObject(runtimeMappings)) { + if (isRuntimeMappings(runtimeMappings)) { runtimeFields = Object.keys(runtimeMappings).map((fieldName) => { const field = runtimeMappings[fieldName]; return { name: fieldName, type: getKibanaFieldTypeFromEsType(field.type) }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts new file mode 100644 index 00000000000000..ec90d31a0d1698 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isRuntimeField, isRuntimeMappings } from './types'; + +describe('Transform: step_define type guards', () => { + it('isRuntimeField()', () => { + expect(isRuntimeField(1)).toBe(false); + expect(isRuntimeField(null)).toBe(false); + expect(isRuntimeField([])).toBe(false); + expect(isRuntimeField({})).toBe(false); + expect(isRuntimeField({ someAttribute: 'someValue' })).toBe(false); + expect(isRuntimeField({ type: 'wrong-type' })).toBe(false); + expect(isRuntimeField({ type: 'keyword', someAttribute: 'some value' })).toBe(false); + + expect(isRuntimeField({ type: 'keyword' })).toBe(true); + expect(isRuntimeField({ type: 'keyword', script: 'some script' })).toBe(true); + }); + + it('isRuntimeMappings()', () => { + expect(isRuntimeMappings(1)).toBe(false); + expect(isRuntimeMappings(null)).toBe(false); + expect(isRuntimeMappings([])).toBe(false); + expect(isRuntimeMappings({})).toBe(false); + expect(isRuntimeMappings({ someAttribute: 'someValue' })).toBe(false); + expect(isRuntimeMappings({ fieldName1: { type: 'keyword' }, fieldName2: 'someValue' })).toBe( + false + ); + expect( + isRuntimeMappings({ + fieldName1: { type: 'keyword' }, + fieldName2: { type: 'keyword', someAttribute: 'some value' }, + }) + ).toBe(false); + expect( + isRuntimeMappings({ + fieldName: { type: 'long', script: 1234 }, + }) + ).toBe(false); + expect( + isRuntimeMappings({ + fieldName: { type: 'long', script: { someAttribute: 'some value' } }, + }) + ).toBe(false); + expect( + isRuntimeMappings({ + fieldName: { type: 'long', script: { source: 1234 } }, + }) + ).toBe(false); + + expect(isRuntimeMappings({ fieldName: { type: 'keyword' } })).toBe(true); + expect( + isRuntimeMappings({ fieldName1: { type: 'keyword' }, fieldName2: { type: 'keyword' } }) + ).toBe(true); + expect( + isRuntimeMappings({ + fieldName1: { type: 'keyword' }, + fieldName2: { type: 'keyword', script: 'some script as script' }, + }) + ).toBe(true); + expect( + isRuntimeMappings({ + fieldName: { type: 'long', script: { source: 'some script as source' } }, + }) + ).toBe(true); + }); +}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index 6c9293a6d13cf6..d0218996b2e4f4 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -24,6 +24,8 @@ import { } from '../../../../../../../common/types/transform'; import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms'; +import { isPopulatedObject } from '../../../../../common/utils/object_utils'; + export interface ErrorMessage { query: string; message: string; @@ -70,10 +72,30 @@ export interface StepDefineExposedState { isRuntimeMappingsEditorEnabled: boolean; } +export function isRuntimeField(arg: any): arg is RuntimeField { + return ( + isPopulatedObject(arg) && + ((Object.keys(arg).length === 1 && arg.hasOwnProperty('type')) || + (Object.keys(arg).length === 2 && + arg.hasOwnProperty('type') && + arg.hasOwnProperty('script') && + (typeof arg.script === 'string' || + (isPopulatedObject(arg.script) && + Object.keys(arg.script).length === 1 && + arg.script.hasOwnProperty('source') && + typeof arg.script.source === 'string')))) && + RUNTIME_FIELD_TYPES.includes(arg.type) + ); +} + +export function isRuntimeMappings(arg: any): arg is RuntimeMappings { + return isPopulatedObject(arg) && Object.values(arg).every((d) => isRuntimeField(d)); +} + export function isPivotPartialRequest(arg: any): arg is { pivot: PivotConfigDefinition } { - return typeof arg === 'object' && arg.hasOwnProperty('pivot'); + return isPopulatedObject(arg) && arg.hasOwnProperty('pivot'); } export function isLatestPartialRequest(arg: any): arg is { latest: LatestFunctionConfig } { - return typeof arg === 'object' && arg.hasOwnProperty('latest'); + return isPopulatedObject(arg) && arg.hasOwnProperty('latest'); } diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts index d3269bf14322ab..bfe2f470785690 100644 --- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts +++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts @@ -32,7 +32,7 @@ export function registerFieldHistogramsRoutes({ router, license }: RouteDependen license.guardApiRoute( async (ctx, req, res) => { const { indexPatternTitle } = req.params; - const { query, fields, samplerShardSize } = req.body; + const { query, fields, runtimeMappings, samplerShardSize } = req.body; try { const resp = await getHistogramsForFields( @@ -40,7 +40,8 @@ export function registerFieldHistogramsRoutes({ router, license }: RouteDependen indexPatternTitle, query, fields, - samplerShardSize + samplerShardSize, + runtimeMappings ); return res.ok({ body: resp }); From 7b0ed48c01b84683fb4bd20fad114378219cc387 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 2 Mar 2021 08:45:36 -0700 Subject: [PATCH 03/63] [7.12] [Security Solution] [Timeline] Bugfix to include unmapped fields in the timeline event details JSON (#92025) (#93172) * fix merge * fix lint * fix func test * fix func test --- .../timeline/events/details/index.ts | 1 + .../__snapshots__/json_view.test.tsx.snap | 2 +- .../components/event_details/columns.tsx | 109 +++--- .../event_details/event_fields_browser.tsx | 1 - .../components/event_details/helpers.tsx | 1 + .../components/event_details/json_view.tsx | 20 +- .../components/event_details/translations.ts | 7 + .../public/common/mock/mock_detail_item.ts | 87 +++-- .../public/common/mock/timeline_results.ts | 2 + .../components/alerts_table/helpers.test.ts | 11 +- .../body/column_headers/header/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 12 +- .../get_column_renderer.test.tsx.snap | 2 +- .../plain_column_renderer.test.tsx.snap | 2 +- .../body/renderers/formatted_field.tsx | 17 +- .../body/renderers/plain_column_renderer.tsx | 4 +- .../public/timelines/store/timeline/model.ts | 3 +- .../search_strategy/helpers/to_array.ts | 45 ++- .../factory/hosts/all/helpers.ts | 8 +- .../factory/hosts/authentications/helpers.ts | 8 +- .../factory/hosts/details/helpers.ts | 8 +- .../hosts/uncommon_processes/helpers.ts | 8 +- .../factory/network/details/helpers.ts | 4 +- .../search_strategy/timeline/eql/helpers.ts | 78 +++-- .../search_strategy/timeline/eql/index.ts | 2 +- .../factory/events/all/helpers.test.ts | 73 ++-- .../timeline/factory/events/all/helpers.ts | 87 +++-- .../timeline/factory/events/all/index.ts | 6 +- .../factory/events/details/helpers.test.ts | 300 +++++++++------- .../factory/events/details/helpers.ts | 105 +++++- .../timeline/factory/events/details/index.ts | 22 +- .../details/query.events_details.dsl.test.ts | 4 + .../details/query.events_details.dsl.ts | 2 + .../timeline/factory/events/mocks.ts | 329 ++++++++++++++++++ .../security_solution/timeline_details.ts | 282 +++++++++++++-- 35 files changed, 1253 insertions(+), 402 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts index 5d6bc33ec49f8c..1f9820f8e5c2b0 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts @@ -16,6 +16,7 @@ export interface TimelineEventsDetailsItem { values?: Maybe; // eslint-disable-next-line @typescript-eslint/no-explicit-any originalValue?: Maybe; + isObjectArray: boolean; } export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap index 2b681870e92fe4..0412b3074e3f17 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -15,8 +15,8 @@ exports[`JSON View rendering should match snapshot 1`] = ` value="{ \\"_id\\": \\"pEMaMmkBUV60JmNWmWVi\\", \\"_index\\": \\"filebeat-8.0.0-2019.02.19-000001\\", - \\"_type\\": \\"_doc\\", \\"_score\\": 1, + \\"_type\\": \\"_doc\\", \\"@timestamp\\": \\"2019-02-28T16:50:54.621Z\\", \\"agent\\": { \\"ephemeral_id\\": \\"9d391ef2-a734-4787-8891-67031178c641\\", diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 8fc6633df247f8..a62b652492c5f4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -85,24 +85,28 @@ export const getColumns = ({ sortable: false, truncateText: false, width: '30px', - render: (field: string) => ( - - c.id === field) !== -1} - data-test-subj={`toggle-field-${field}`} - data-colindex={1} - id={field} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field, - width: DEFAULT_COLUMN_MIN_WIDTH, - }) - } - /> - - ), + render: (field: string, data: EventFieldsData) => { + const label = data.isObjectArray ? i18n.NESTED_COLUMN(field) : i18n.VIEW_COLUMN(field); + return ( + + c.id === field) !== -1} + data-test-subj={`toggle-field-${field}`} + data-colindex={1} + id={field} + onChange={() => + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field, + width: DEFAULT_COLUMN_MIN_WIDTH, + }) + } + disabled={data.isObjectArray && data.type !== 'geo_point'} + /> + + ); + }, }, { field: 'field', @@ -118,38 +122,42 @@ export const getColumns = ({ - ( -
- - - -
- )} - > - -
+ {data.isObjectArray && data.type !== 'geo_point' ? ( + <>{field} + ) : ( + ( +
+ + + +
+ )} + > + +
+ )}
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 497768785735b9..93d0e6ccfbe3c2 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -108,7 +108,6 @@ export const EventFieldsBrowser = React.memo( const columnHeaders = useDeepEqualSelector((state) => { const { columns } = getTimeline(state, timelineId) ?? timelineDefaults; - return getColumnHeaders(columns, browserFields); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx index 7c7b8ba70f9bdd..00e2ee276f1816 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/helpers.tsx @@ -112,6 +112,7 @@ export const getIconFromType = (type: string | null) => { case 'date': return 'clock'; case 'ip': + case 'geo_point': return 'globe'; case 'object': return 'questionInCircle'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx index 449010781d448b..c9ca93582cd9a2 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx @@ -54,12 +54,14 @@ export const JsonView = React.memo(({ data }) => { JsonView.displayName = 'JsonView'; export const buildJsonView = (data: TimelineEventsDetailsItem[]) => - data.reduce( - (accumulator, item) => - set( - item.field, - Array.isArray(item.originalValue) ? item.originalValue.join() : item.originalValue, - accumulator - ), - {} - ); + data + .sort((a, b) => a.field.localeCompare(b.field)) + .reduce( + (accumulator, item) => + set( + item.field, + Array.isArray(item.originalValue) ? item.originalValue.join() : item.originalValue, + accumulator + ), + {} + ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index c2b7bb4587dbd3..3a599b174251ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -61,3 +61,10 @@ export const VIEW_COLUMN = (field: string) => values: { field }, defaultMessage: 'View {field} column', }); + +export const NESTED_COLUMN = (field: string) => + i18n.translate('xpack.securitySolution.eventDetails.nestedColumnCheckboxAriaLabel', { + values: { field }, + defaultMessage: + 'The {field} field is an object, and is broken down into nested fields which can be added as column', + }); diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts index ca84ef539bec39..198ab084ae0b87 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts @@ -14,105 +14,126 @@ export const mockDetailItemData: TimelineEventsDetailsItem[] = [ field: '_id', originalValue: 'pEMaMmkBUV60JmNWmWVi', values: ['pEMaMmkBUV60JmNWmWVi'], + isObjectArray: false, }, { field: '_index', originalValue: 'filebeat-8.0.0-2019.02.19-000001', values: ['filebeat-8.0.0-2019.02.19-000001'], + isObjectArray: false, }, { field: '_type', originalValue: '_doc', values: ['_doc'], + isObjectArray: false, }, { field: '_score', originalValue: 1, values: ['1'], + isObjectArray: false, }, { field: '@timestamp', originalValue: '2019-02-28T16:50:54.621Z', values: ['2019-02-28T16:50:54.621Z'], + isObjectArray: false, }, { field: 'agent.ephemeral_id', originalValue: '9d391ef2-a734-4787-8891-67031178c641', values: ['9d391ef2-a734-4787-8891-67031178c641'], + isObjectArray: false, }, { field: 'agent.hostname', originalValue: 'siem-kibana', values: ['siem-kibana'], + isObjectArray: false, }, { - field: 'agent.id', - originalValue: '5de03d5f-52f3-482e-91d4-853c7de073c3', - values: ['5de03d5f-52f3-482e-91d4-853c7de073c3'], + field: 'cloud.project.id', + originalValue: 'elastic-beats', + values: ['elastic-beats'], + isObjectArray: false, }, { - field: 'agent.type', - originalValue: 'filebeat', - values: ['filebeat'], + field: 'cloud.provider', + originalValue: 'gce', + values: ['gce'], + isObjectArray: false, }, { - field: 'agent.version', - originalValue: '8.0.0', - values: ['8.0.0'], + field: 'destination.bytes', + originalValue: 584, + values: ['584'], + isObjectArray: false, }, { - field: 'cloud.availability_zone', - originalValue: 'projects/189716325846/zones/us-east1-b', - values: ['projects/189716325846/zones/us-east1-b'], + field: 'destination.ip', + originalValue: '10.47.8.200', + values: ['10.47.8.200'], + isObjectArray: false, }, { - field: 'cloud.instance.id', - originalValue: '5412578377715150143', - values: ['5412578377715150143'], + field: 'agent.id', + originalValue: '5de03d5f-52f3-482e-91d4-853c7de073c3', + values: ['5de03d5f-52f3-482e-91d4-853c7de073c3'], + isObjectArray: false, }, { field: 'cloud.instance.name', originalValue: 'siem-kibana', values: ['siem-kibana'], + isObjectArray: false, }, { field: 'cloud.machine.type', originalValue: 'projects/189716325846/machineTypes/n1-standard-1', values: ['projects/189716325846/machineTypes/n1-standard-1'], + isObjectArray: false, }, { - field: 'cloud.project.id', - originalValue: 'elastic-beats', - values: ['elastic-beats'], - }, - { - field: 'cloud.provider', - originalValue: 'gce', - values: ['gce'], - }, - { - field: 'destination.bytes', - originalValue: 584, - values: ['584'], - }, - { - field: 'destination.ip', - originalValue: '10.47.8.200', - values: ['10.47.8.200'], + field: 'agent.type', + originalValue: 'filebeat', + values: ['filebeat'], + isObjectArray: false, }, { field: 'destination.packets', originalValue: 4, values: ['4'], + isObjectArray: false, }, { field: 'destination.port', originalValue: 902, values: ['902'], + isObjectArray: false, }, { field: 'event.kind', originalValue: 'event', values: ['event'], + isObjectArray: false, + }, + { + field: 'agent.version', + originalValue: '8.0.0', + values: ['8.0.0'], + isObjectArray: false, + }, + { + field: 'cloud.availability_zone', + originalValue: 'projects/189716325846/zones/us-east1-b', + values: ['projects/189716325846/zones/us-east1-b'], + isObjectArray: false, + }, + { + field: 'cloud.instance.id', + originalValue: '5412578377715150143', + values: ['5412578377715150143'], + isObjectArray: false, }, ]; diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 70ed497ce0caca..26b30e0d1f89ac 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2287,11 +2287,13 @@ export const mockTimelineDetails: TimelineEventsDetailsItem[] = [ field: 'host.name', values: ['apache'], originalValue: 'apache', + isObjectArray: false, }, { field: 'user.id', values: ['1'], originalValue: 1, + isObjectArray: false, }, ]; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts index 3eb00f8534979e..c296b75a0a253a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/helpers.test.ts @@ -29,6 +29,7 @@ describe('helpers', () => { { field: 'x', values: ['The nickname of the developer we all :heart:'], + isObjectArray: false, originalValue: 'The nickname of the developer we all :heart:', }, ]); @@ -40,6 +41,7 @@ describe('helpers', () => { { field: 'x', values: ['The nickname of the developer we all :heart:'], + isObjectArray: false, originalValue: 'The nickname of the developer we all :heart:', }, ]); @@ -51,6 +53,7 @@ describe('helpers', () => { { field: 'x', values: ['The nickname of the developer we all :heart:', 'We are all made of stars'], + isObjectArray: false, originalValue: 'The nickname of the developer we all :heart:', }, ]); @@ -65,6 +68,7 @@ describe('helpers', () => { { field: 'x.y.z', values: ['zed'], + isObjectArray: false, originalValue: 'zed', }, ]); @@ -76,6 +80,7 @@ describe('helpers', () => { { field: 'x.y.z', values: ['zed'], + isObjectArray: false, originalValue: 'zed', }, ]); @@ -90,6 +95,7 @@ describe('helpers', () => { { field: 'a', values: (5 as unknown) as string[], + isObjectArray: false, originalValue: 'zed', }, ], @@ -104,7 +110,7 @@ describe('helpers', () => { 'when trying to access field:', 'a', 'from data object of:', - [{ field: 'a', originalValue: 'zed', values: 5 }] + [{ field: 'a', isObjectArray: false, originalValue: 'zed', values: 5 }] ); }); @@ -116,6 +122,7 @@ describe('helpers', () => { { field: 'a', values: (['hi', 5] as unknown) as string[], + isObjectArray: false, originalValue: 'zed', }, ], @@ -130,7 +137,7 @@ describe('helpers', () => { 'when trying to access field:', 'a', 'from data object of:', - [{ field: 'a', originalValue: 'zed', values: ['hi', 5] }] + [{ field: 'a', isObjectArray: false, originalValue: 'zed', values: ['hi', 5] }] ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx index bec241e10d6137..ece28faedb9511 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/header/index.tsx @@ -86,6 +86,7 @@ export const HeaderComponent: React.FC = ({ getManageTimelineById, timelineId, ]); + const showSortingCapability = !isEqlOn && !(header.subType && header.subType.nested); return ( <> @@ -94,7 +95,7 @@ export const HeaderComponent: React.FC = ({ isLoading={isLoading} isResizing={false} onClick={onColumnSort} - showSortingCapability={!isEqlOn} + showSortingCapability={showSortingCapability} sort={sort} > @@ -99,7 +99,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="event.action" fieldType="" - key="plain-column-renderer-formatted-field-value-test-event.action-1-event.action-Action" + key="plain-column-renderer-formatted-field-value-test-event.action-1-event.action-Action-0" truncate={true} value="Action" /> @@ -129,7 +129,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="host.name" fieldType="" - key="plain-column-renderer-formatted-field-value-test-host.name-1-host.name-apache" + key="plain-column-renderer-formatted-field-value-test-host.name-1-host.name-apache-0" truncate={true} value="apache" /> @@ -159,7 +159,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="source.ip" fieldType="" - key="plain-column-renderer-formatted-field-value-test-source.ip-1-source.ip-192.168.0.1" + key="plain-column-renderer-formatted-field-value-test-source.ip-1-source.ip-192.168.0.1-0" truncate={true} value="192.168.0.1" /> @@ -189,7 +189,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="destination.ip" fieldType="" - key="plain-column-renderer-formatted-field-value-test-destination.ip-1-destination.ip-192.168.0.3" + key="plain-column-renderer-formatted-field-value-test-destination.ip-1-destination.ip-192.168.0.3-0" truncate={true} value="192.168.0.3" /> @@ -219,7 +219,7 @@ exports[`Columns it renders the expected columns 1`] = ` fieldFormat="" fieldName="user.name" fieldType="" - key="plain-column-renderer-formatted-field-value-test-user.name-1-user.name-john.dee" + key="plain-column-renderer-formatted-field-value-test-user.name-1-user.name-john.dee-0" truncate={true} value="john.dee" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap index e2c46a07af8cce..4da4e12e0f7b3c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap @@ -8,7 +8,7 @@ exports[`get_column_renderer renders correctly against snapshot 1`] = ` fieldFormat="" fieldName="event.severity" fieldType="" - key="plain-column-renderer-formatted-field-value-test-event.severity-1-message-3" + key="plain-column-renderer-formatted-field-value-test-event.severity-1-message-3-0" value="3" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap index 8ea7708bf5907e..13912e6ad3da92 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap @@ -8,7 +8,7 @@ exports[`plain_column_renderer rendering renders correctly against snapshot 1`] fieldFormat="" fieldName="event.category" fieldType="keyword" - key="plain-column-renderer-formatted-field-value-test-event.category-1-event.category-Access" + key="plain-column-renderer-formatted-field-value-test-event.category-1-event.category-Access-0" value="Access" /> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index fa3612f08204d7..3032f556251f30 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -41,14 +41,27 @@ const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; const FormattedFieldValueComponent: React.FC<{ contextId: string; eventId: string; + isObjectArray?: boolean; fieldFormat?: string; fieldName: string; fieldType: string; truncate?: boolean; value: string | number | undefined | null; linkValue?: string | null | undefined; -}> = ({ contextId, eventId, fieldFormat, fieldName, fieldType, truncate, value, linkValue }) => { - if (fieldType === IP_FIELD_TYPE) { +}> = ({ + contextId, + eventId, + fieldFormat, + fieldName, + fieldType, + isObjectArray = false, + truncate, + value, + linkValue, +}) => { + if (isObjectArray) { + return <>{value}; + } else if (fieldType === IP_FIELD_TYPE) { return ( values != null - ? values.map((value) => ( + ? values.map((value, i) => ( (value: T | T[] | null): T[] => Array.isArray(value) ? value : value == null ? [] : [value]; - export const toStringArray = (value: T | T[] | null): string[] => { if (Array.isArray(value)) { return value.reduce((acc, v) => { @@ -42,3 +41,47 @@ export const toStringArray = (value: T | T[] | null): string[] => { return [`${value}`]; } }; +export const toObjectArrayOfStrings = ( + value: T | T[] | null +): Array<{ + str: string; + isObjectArray?: boolean; +}> => { + if (Array.isArray(value)) { + return value.reduce< + Array<{ + str: string; + isObjectArray?: boolean; + }> + >((acc, v) => { + if (v != null) { + switch (typeof v) { + case 'number': + case 'boolean': + return [...acc, { str: v.toString() }]; + case 'object': + try { + return [...acc, { str: JSON.stringify(v), isObjectArray: true }]; // need to track when string is not a simple value + } catch { + return [...acc, { str: 'Invalid Object' }]; + } + case 'string': + return [...acc, { str: v }]; + default: + return [...acc, { str: `${v}` }]; + } + } + return acc; + }, []); + } else if (value == null) { + return []; + } else if (!Array.isArray(value) && typeof value === 'object') { + try { + return [{ str: JSON.stringify(value), isObjectArray: true }]; + } catch { + return [{ str: 'Invalid Object' }]; + } + } else { + return [{ str: `${value}` }]; + } +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index 505f99dd284557..8b2397fd7fab07 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -11,7 +11,7 @@ import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostsEdges } from '../../../../../../common/search_strategy/security_solution/hosts'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; export const HOSTS_FIELDS: readonly string[] = [ '_id', @@ -33,7 +33,11 @@ export const formatHostEdgesData = ( flattenedFields.cursor.value = hostId || ''; const fieldValue = getHostFieldValue(fieldName, bucket); if (fieldValue != null) { - return set(`node.${fieldName}`, toStringArray(fieldValue), flattenedFields); + return set( + `node.${fieldName}`, + toObjectArrayOfStrings(fieldValue).map(({ str }) => str), + flattenedFields + ); } return flattenedFields; }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts index 06d81140f475e5..aeaefe690cbde3 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts @@ -8,7 +8,7 @@ import { get, getOr, isEmpty } from 'lodash/fp'; import { set } from '@elastic/safer-lodash-set/fp'; import { mergeFieldsWithHit } from '../../../../../utils/build_query'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; import { AuthenticationsEdges, AuthenticationHit, @@ -55,7 +55,11 @@ export const formatAuthenticationData = ( const fieldPath = `node.${fieldName}`; const fieldValue = get(fieldPath, mergedResult); if (!isEmpty(fieldValue)) { - return set(fieldPath, toStringArray(fieldValue), mergedResult); + return set( + fieldPath, + toObjectArrayOfStrings(fieldValue).map(({ str }) => str), + mergedResult + ); } else { return mergedResult; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index 9c522bd704ef06..2b35517d693d51 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -9,7 +9,7 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostItem } from '../../../../../../common/search_strategy/security_solution/hosts'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; @@ -42,7 +42,11 @@ export const formatHostItem = (bucket: HostAggEsItem): HostItem => if (fieldName === '_id') { return set('_id', fieldValue, flattenedFields); } - return set(fieldName, toStringArray(fieldValue), flattenedFields); + return set( + fieldName, + toObjectArrayOfStrings(fieldValue).map(({ str }) => str), + flattenedFields + ); } return flattenedFields; }, {}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts index 7b01f4e7dc816a..fe202b48540d7b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -14,7 +14,7 @@ import { HostsUncommonProcessesEdges, HostsUncommonProcessHit, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; import { HostHits } from '../../../../../../common/search_strategy'; export const uncommonProcessesFields = [ @@ -82,7 +82,11 @@ export const formatUncommonProcessesData = ( fieldPath = `node.hosts.0.name`; fieldValue = get(fieldPath, mergedResult); } - return set(fieldPath, toStringArray(fieldValue), mergedResult); + return set( + fieldPath, + toObjectArrayOfStrings(fieldValue).map(({ str }) => str), + mergedResult + ); }, { node: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts index 3b387597618e8d..8fc7ae0304a352 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts @@ -13,7 +13,7 @@ import { NetworkDetailsHostHit, NetworkHit, } from '../../../../../../common/search_strategy/security_solution/network'; -import { toStringArray } from '../../../../helpers/to_array'; +import { toObjectArrayOfStrings } from '../../../../helpers/to_array'; export const getNetworkDetailsAgg = (type: string, networkHit: NetworkHit | {}) => { const firstSeen = getOr(null, `firstSeen.value_as_string`, networkHit); @@ -53,7 +53,7 @@ const formatHostEcs = (data: Record | null): HostEcs | null => } return { ...acc, - [key]: toStringArray(value), + [key]: toObjectArrayOfStrings(value).map(({ str }) => str), }; }, {}); }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts index bdd3195b3b756d..b007307412e955 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/helpers.ts @@ -7,7 +7,7 @@ import { EqlSearchStrategyResponse } from '../../../../../data_enhanced/common'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; -import { EqlSearchResponse } from '../../../../common/detection_engine/types'; +import { EqlSearchResponse, EqlSequence } from '../../../../common/detection_engine/types'; import { EventHit, TimelineEdges } from '../../../../common/search_strategy'; import { TimelineEqlRequestOptions, @@ -56,51 +56,53 @@ export const buildEqlDsl = (options: TimelineEqlRequestOptions): Record>, fieldRequested: string[]) => + sequences.reduce>(async (acc, sequence, sequenceIndex) => { + const sequenceParentId = sequence.events[0]?._id ?? null; + const data = await acc; + const allData = await Promise.all( + sequence.events.map(async (event, eventIndex) => { + const item = await formatTimelineData( + fieldRequested, + TIMELINE_EVENTS_FIELDS, + event as EventHit + ); + return Promise.resolve({ + ...item, + node: { + ...item.node, + ecs: { + ...item.node.ecs, + ...(sequenceParentId != null + ? { + eql: { + parentId: sequenceParentId, + sequenceNumber: `${sequenceIndex}-${eventIndex}`, + }, + } + : {}), + }, + }, + }); + }) + ); + return Promise.resolve([...data, ...allData]); + }, Promise.resolve([])); -export const parseEqlResponse = ( +export const parseEqlResponse = async ( options: TimelineEqlRequestOptions, response: EqlSearchStrategyResponse> ): Promise => { const { activePage, querySize } = options.pagination; - // const totalCount = response.rawResponse?.body?.hits?.total?.value ?? 0; let edges: TimelineEdges[] = []; + if (response.rawResponse.body.hits.sequences !== undefined) { - edges = response.rawResponse.body.hits.sequences.reduce( - (data, sequence, sequenceIndex) => { - const sequenceParentId = sequence.events[0]?._id ?? null; - return [ - ...data, - ...sequence.events.map((event, eventIndex) => { - const item = formatTimelineData( - options.fieldRequested, - TIMELINE_EVENTS_FIELDS, - event as EventHit - ); - return { - ...item, - node: { - ...item.node, - ecs: { - ...item.node.ecs, - ...(sequenceParentId != null - ? { - eql: { - parentId: sequenceParentId, - sequenceNumber: `${sequenceIndex}-${eventIndex}`, - }, - } - : {}), - }, - }, - }; - }), - ]; - }, - [] - ); + edges = await parseSequences(response.rawResponse.body.hits.sequences, options.fieldRequested); } else if (response.rawResponse.body.hits.events !== undefined) { - edges = response.rawResponse.body.hits.events.map((event) => - formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, event as EventHit) + edges = await Promise.all( + response.rawResponse.body.hits.events.map(async (event) => + formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, event as EventHit) + ) ); } diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts index cf7877e987acea..249f5582d2a398 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/eql/index.ts @@ -38,7 +38,7 @@ export const securitySolutionTimelineEqlSearchStrategyProvider = ( }, }; }), - mergeMap((esSearchRes) => + mergeMap(async (esSearchRes) => parseEqlResponse( request, (esSearchRes as unknown) as EqlSearchStrategyResponse> diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts index 10bb606dc2387e..61af6a7664faa6 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -8,54 +8,23 @@ import { EventHit } from '../../../../../../common/search_strategy'; import { TIMELINE_EVENTS_FIELDS } from './constants'; import { formatTimelineData } from './helpers'; +import { eventHit } from '../mocks'; describe('#formatTimelineData', () => { - it('happy path', () => { - const response: EventHit = { - _index: 'auditbeat-7.8.0-2020.11.05-000003', - _id: 'tkCt1nUBaEgqnrVSZ8R_', - _score: 0, - _type: '', - fields: { - 'event.category': ['process'], - 'process.ppid': [3977], - 'user.name': ['jenkins'], - 'process.args': ['go', 'vet', './...'], - message: ['Process go (PID: 4313) by user jenkins STARTED'], - 'process.pid': [4313], - 'process.working_directory': [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', - ], - 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], - 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - 'process.name': ['go'], - 'event.action': ['process_started'], - 'agent.type': ['auditbeat'], - '@timestamp': ['2020-11-17T14:48:08.922Z'], - 'event.module': ['system'], - 'event.type': ['start'], - 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], - 'host.os.family': ['debian'], - 'event.kind': ['event'], - 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], - 'event.dataset': ['process'], - 'process.executable': [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', - ], - }, - _source: {}, - sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], - aggregations: {}, - }; - - expect( - formatTimelineData( - ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], - TIMELINE_EVENTS_FIELDS, - response - ) - ).toEqual({ + it('happy path', async () => { + const res = await formatTimelineData( + [ + '@timestamp', + 'host.name', + 'destination.ip', + 'source.ip', + 'source.geo.location', + 'threat.indicator.matched.field', + ], + TIMELINE_EVENTS_FIELDS, + eventHit + ); + expect(res).toEqual({ cursor: { tiebreaker: 'beats-ci-immutable-ubuntu-1804-1605624279743236239', value: '1605624488922', @@ -72,6 +41,14 @@ describe('#formatTimelineData', () => { field: 'host.name', value: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], }, + { + field: 'source.geo.location', + value: [`{"lon":118.7778,"lat":32.0617}`], + }, + { + field: 'threat.indicator.matched.field', + value: ['matched_field', 'matched_field_2'], + }, ], ecs: { '@timestamp': ['2020-11-17T14:48:08.922Z'], @@ -122,7 +99,7 @@ describe('#formatTimelineData', () => { }); }); - it('rule signal results', () => { + it('rule signal results', async () => { const response: EventHit = { _index: '.siem-signals-patrykkopycinski-default-000007', _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', @@ -290,7 +267,7 @@ describe('#formatTimelineData', () => { }; expect( - formatTimelineData( + await formatTimelineData( ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], TIMELINE_EVENTS_FIELDS, response diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index 1a83cbf40f1f4d..e5bb8cb7e14b73 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -6,9 +6,13 @@ */ import { get, has, merge, uniq } from 'lodash/fp'; -import { EventHit, TimelineEdges } from '../../../../../../common/search_strategy'; +import { + EventHit, + TimelineEdges, + TimelineNonEcsData, +} from '../../../../../../common/search_strategy'; import { toStringArray } from '../../../../helpers/to_array'; -import { formatGeoLocation, isGeoField } from '../details/helpers'; +import { getDataSafety, getDataFromFieldsHits } from '../details/helpers'; const getTimestamp = (hit: EventHit): string => { if (hit.fields && hit.fields['@timestamp']) { @@ -19,13 +23,14 @@ const getTimestamp = (hit: EventHit): string => { return ''; }; -export const formatTimelineData = ( +export const formatTimelineData = async ( dataFields: readonly string[], ecsFields: readonly string[], hit: EventHit ) => - uniq([...ecsFields, ...dataFields]).reduce( - (flattenedFields, fieldName) => { + uniq([...ecsFields, ...dataFields]).reduce>( + async (acc, fieldName) => { + const flattenedFields: TimelineEdges = await acc; flattenedFields.node._id = hit._id; flattenedFields.node._index = hit._index; flattenedFields.node.ecs._id = hit._id; @@ -35,30 +40,81 @@ export const formatTimelineData = ( flattenedFields.cursor.value = hit.sort[0]; flattenedFields.cursor.tiebreaker = hit.sort[1]; } - return mergeTimelineFieldsWithHit(fieldName, flattenedFields, hit, dataFields, ecsFields); + const waitForIt = await mergeTimelineFieldsWithHit( + fieldName, + flattenedFields, + hit, + dataFields, + ecsFields + ); + return Promise.resolve(waitForIt); }, - { + Promise.resolve({ node: { ecs: { _id: '' }, data: [], _id: '', _index: '' }, cursor: { value: '', tiebreaker: null, }, - } + }) ); const specialFields = ['_id', '_index', '_type', '_score']; -const mergeTimelineFieldsWithHit = ( +const getValuesFromFields = async ( + fieldName: string, + hit: EventHit, + nestedParentFieldName?: string +): Promise => { + if (specialFields.includes(fieldName)) { + return [{ field: fieldName, value: toStringArray(get(fieldName, hit)) }]; + } + + let fieldToEval; + if (has(fieldName, hit._source)) { + fieldToEval = { + [fieldName]: get(fieldName, hit._source), + }; + } else { + if (nestedParentFieldName == null || nestedParentFieldName === fieldName) { + fieldToEval = { + [fieldName]: hit.fields[fieldName], + }; + } else if (nestedParentFieldName != null) { + fieldToEval = { + [nestedParentFieldName]: hit.fields[nestedParentFieldName], + }; + } else { + // fallback, should never hit + fieldToEval = { + [fieldName]: [], + }; + } + } + const formattedData = await getDataSafety(getDataFromFieldsHits, fieldToEval); + return formattedData.reduce( + (acc: TimelineNonEcsData[], { field, values }) => + // nested fields return all field values, pick only the one we asked for + field.includes(fieldName) ? [...acc, { field, value: values }] : acc, + [] + ); +}; + +const mergeTimelineFieldsWithHit = async ( fieldName: string, flattenedFields: T, - hit: { _source: {}; fields: Record }, + hit: EventHit, dataFields: readonly string[], ecsFields: readonly string[] ) => { if (fieldName != null || dataFields.includes(fieldName)) { + const fieldNameAsArray = fieldName.split('.'); + const nestedParentFieldName = Object.keys(hit.fields ?? []).find((f) => { + return f === fieldNameAsArray.slice(0, f.split('.').length).join('.'); + }); if ( has(fieldName, hit._source) || has(fieldName, hit.fields) || + nestedParentFieldName != null || specialFields.includes(fieldName) ) { const objectWithProperty = { @@ -67,16 +123,7 @@ const mergeTimelineFieldsWithHit = ( data: dataFields.includes(fieldName) ? [ ...get('node.data', flattenedFields), - { - field: fieldName, - value: specialFields.includes(fieldName) - ? toStringArray(get(fieldName, hit)) - : isGeoField(fieldName) - ? formatGeoLocation(hit.fields[fieldName]) - : has(fieldName, hit._source) - ? toStringArray(get(fieldName, hit._source)) - : toStringArray(hit.fields[fieldName]), - }, + ...(await getValuesFromFields(fieldName, hit, nestedParentFieldName)), ] : get('node.data', flattenedFields), ecs: ecsFields.includes(fieldName) diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts index 93985baed770e9..05058e3ee7a2dc 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/index.ts @@ -40,8 +40,10 @@ export const timelineEventsAll: SecuritySolutionTimelineFactory - formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit) + const edges: TimelineEdges[] = await Promise.all( + hits.map((hit) => + formatTimelineData(options.fieldRequested, TIMELINE_EVENTS_FIELDS, hit as EventHit) + ) ); const inspect = { dsl: [inspectStringifyObject(buildTimelineEventsAllQuery(queryOptions))], diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts index ca9a4b708161de..dc3efc6909c634 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts @@ -5,150 +5,192 @@ * 2.0. */ -import { EventHit } from '../../../../../../common/search_strategy'; -import { getDataFromFieldsHits } from './helpers'; +import { EventHit, EventSource } from '../../../../../../common/search_strategy'; +import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers'; +import { eventDetailsFormattedFields, eventHit } from '../mocks'; -describe('#getDataFromFieldsHits', () => { - it('happy path', () => { - const response: EventHit = { - _index: 'auditbeat-7.8.0-2020.11.05-000003', - _id: 'tkCt1nUBaEgqnrVSZ8R_', - _score: 0, - _type: '', - fields: { - 'event.category': ['process'], - 'process.ppid': [3977], - 'user.name': ['jenkins'], - 'process.args': ['go', 'vet', './...'], - message: ['Process go (PID: 4313) by user jenkins STARTED'], - 'process.pid': [4313], - 'process.working_directory': [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', +describe('Events Details Helpers', () => { + const fields: EventHit['fields'] = eventHit.fields; + const resultFields = eventDetailsFormattedFields; + describe('#getDataFromFieldsHits', () => { + it('happy path', () => { + const result = getDataFromFieldsHits(fields); + expect(result).toEqual(resultFields); + }); + it('lets get weird', () => { + const whackFields = { + 'crazy.pants': [ + { + 'matched.field': ['matched_field'], + first_seen: ['2021-02-22T17:29:25.195Z'], + provider: ['yourself'], + type: ['custom'], + 'matched.atomic': ['matched_atomic'], + lazer: [ + { + 'great.field': ['grrrrr'], + lazer: [ + { + lazer: [ + { + cool: true, + lazer: [ + { + lazer: [ + { + lazer: [ + { + lazer: [ + { + whoa: false, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + lazer: [ + { + cool: false, + }, + ], + }, + ], + }, + { + 'great.field': ['grrrrr_2'], + }, + ], + }, ], - 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], - 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - 'process.name': ['go'], - 'event.action': ['process_started'], - 'agent.type': ['auditbeat'], - '@timestamp': ['2020-11-17T14:48:08.922Z'], - 'event.module': ['system'], - 'event.type': ['start'], - 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], - 'host.os.family': ['debian'], - 'event.kind': ['event'], - 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], - 'event.dataset': ['process'], - 'process.executable': [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', - ], - }, - _source: {}, - sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], - aggregations: {}, + }; + const whackResultFields = [ + { + category: 'crazy', + field: 'crazy.pants.matched.field', + values: ['matched_field'], + originalValue: ['matched_field'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.first_seen', + values: ['2021-02-22T17:29:25.195Z'], + originalValue: ['2021-02-22T17:29:25.195Z'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.provider', + values: ['yourself'], + originalValue: ['yourself'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.type', + values: ['custom'], + originalValue: ['custom'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.matched.atomic', + values: ['matched_atomic'], + originalValue: ['matched_atomic'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.lazer.great.field', + values: ['grrrrr', 'grrrrr_2'], + originalValue: ['grrrrr', 'grrrrr_2'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.lazer.lazer.lazer.cool', + values: ['true', 'false'], + originalValue: ['true', 'false'], + isObjectArray: false, + }, + { + category: 'crazy', + field: 'crazy.pants.lazer.lazer.lazer.lazer.lazer.lazer.lazer.whoa', + values: ['false'], + originalValue: ['false'], + isObjectArray: false, + }, + ]; + const result = getDataFromFieldsHits(whackFields); + expect(result).toEqual(whackResultFields); + }); + }); + it('#getDataFromSourceHits', () => { + const _source: EventSource = { + '@timestamp': '2021-02-24T00:41:06.527Z', + 'signal.status': 'open', + 'signal.rule.name': 'Rawr', + 'threat.indicator': [ + { + provider: 'yourself', + type: 'custom', + first_seen: ['2021-02-22T17:29:25.195Z'], + matched: { atomic: 'atom', field: 'field', type: 'type' }, + }, + { + provider: 'other_you', + type: 'custom', + first_seen: '2021-02-22T17:29:25.195Z', + matched: { atomic: 'atom', field: 'field', type: 'type' }, + }, + ], }; - - expect(getDataFromFieldsHits(response.fields)).toEqual([ - { - category: 'event', - field: 'event.category', - originalValue: ['process'], - values: ['process'], - }, - { category: 'process', field: 'process.ppid', originalValue: ['3977'], values: ['3977'] }, - { category: 'user', field: 'user.name', originalValue: ['jenkins'], values: ['jenkins'] }, - { - category: 'process', - field: 'process.args', - originalValue: ['go', 'vet', './...'], - values: ['go', 'vet', './...'], - }, - { - category: 'base', - field: 'message', - originalValue: ['Process go (PID: 4313) by user jenkins STARTED'], - values: ['Process go (PID: 4313) by user jenkins STARTED'], - }, - { category: 'process', field: 'process.pid', originalValue: ['4313'], values: ['4313'] }, - { - category: 'process', - field: 'process.working_directory', - originalValue: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', - ], - values: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', - ], - }, - { - category: 'process', - field: 'process.entity_id', - originalValue: ['Z59cIkAAIw8ZoK0H'], - values: ['Z59cIkAAIw8ZoK0H'], - }, - { - category: 'host', - field: 'host.ip', - originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], - }, - { category: 'process', field: 'process.name', originalValue: ['go'], values: ['go'] }, - { - category: 'event', - field: 'event.action', - originalValue: ['process_started'], - values: ['process_started'], - }, - { - category: 'agent', - field: 'agent.type', - originalValue: ['auditbeat'], - values: ['auditbeat'], - }, + expect(getDataFromSourceHits(_source)).toEqual([ { category: 'base', field: '@timestamp', - originalValue: ['2020-11-17T14:48:08.922Z'], - values: ['2020-11-17T14:48:08.922Z'], + values: ['2021-02-24T00:41:06.527Z'], + originalValue: ['2021-02-24T00:41:06.527Z'], + isObjectArray: false, }, - { category: 'event', field: 'event.module', originalValue: ['system'], values: ['system'] }, - { category: 'event', field: 'event.type', originalValue: ['start'], values: ['start'] }, { - category: 'host', - field: 'host.name', - originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], - values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + category: 'signal', + field: 'signal.status', + values: ['open'], + originalValue: ['open'], + isObjectArray: false, }, { - category: 'process', - field: 'process.hash.sha1', - originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], - values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + category: 'signal', + field: 'signal.rule.name', + values: ['Rawr'], + originalValue: ['Rawr'], + isObjectArray: false, }, - { category: 'host', field: 'host.os.family', originalValue: ['debian'], values: ['debian'] }, - { category: 'event', field: 'event.kind', originalValue: ['event'], values: ['event'] }, { - category: 'host', - field: 'host.id', - originalValue: ['e59991e835905c65ed3e455b33e13bd6'], - values: ['e59991e835905c65ed3e455b33e13bd6'], - }, - { - category: 'event', - field: 'event.dataset', - originalValue: ['process'], - values: ['process'], - }, - { - category: 'process', - field: 'process.executable', - originalValue: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', - ], + category: 'threat', + field: 'threat.indicator', values: [ - '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + '{"provider":"yourself","type":"custom","first_seen":["2021-02-22T17:29:25.195Z"],"matched":{"atomic":"atom","field":"field","type":"type"}}', + '{"provider":"other_you","type":"custom","first_seen":"2021-02-22T17:29:25.195Z","matched":{"atomic":"atom","field":"field","type":"type"}}', + ], + originalValue: [ + '{"provider":"yourself","type":"custom","first_seen":["2021-02-22T17:29:25.195Z"],"matched":{"atomic":"atom","field":"field","type":"type"}}', + '{"provider":"other_you","type":"custom","first_seen":"2021-02-22T17:29:25.195Z","matched":{"atomic":"atom","field":"field","type":"type"}}', ], + isObjectArray: true, }, ]); }); + it('#getDataSafety', async () => { + const result = await getDataSafety(getDataFromFieldsHits, fields); + expect(result).toEqual(resultFields); + }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 779454e9474ee7..2fc729729e4351 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -7,8 +7,12 @@ import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; -import { EventSource, TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; -import { toStringArray } from '../../../../helpers/to_array'; +import { + EventHit, + EventSource, + TimelineEventsDetailsItem, +} from '../../../../../../common/search_strategy'; +import { toObjectArrayOfStrings, toStringArray } from '../../../../helpers/to_array'; export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; @@ -24,7 +28,10 @@ export const formatGeoLocation = (item: unknown[]) => { const itemGeo = item.length > 0 ? (item[0] as { coordinates: number[] }) : null; if (itemGeo != null && !isEmpty(itemGeo.coordinates)) { try { - return toStringArray({ long: itemGeo.coordinates[0], lat: itemGeo.coordinates[1] }); + return toStringArray({ + lon: itemGeo.coordinates[0], + lat: itemGeo.coordinates[1], + }); } catch { return toStringArray(item); } @@ -46,13 +53,18 @@ export const getDataFromSourceHits = ( const field = path ? `${path}.${source}` : source; const fieldCategory = getFieldCategory(field); + const objArrStr = toObjectArrayOfStrings(item); + const strArr = objArrStr.map(({ str }) => str); + const isObjectArray = objArrStr.some((o) => o.isObjectArray); + return [ ...accumulator, { category: fieldCategory, field, - values: toStringArray(item), - originalValue: toStringArray(item), + values: strArr, + originalValue: strArr, + isObjectArray, } as TimelineEventsDetailsItem, ]; } else if (isObject(item)) { @@ -65,18 +77,81 @@ export const getDataFromSourceHits = ( }, []); export const getDataFromFieldsHits = ( - fields: Record + fields: EventHit['fields'], + prependField?: string, + prependFieldCategory?: string ): TimelineEventsDetailsItem[] => Object.keys(fields).reduce((accumulator, field) => { const item: unknown[] = fields[field]; - const fieldCategory = getFieldCategory(field); - return [ + + const fieldCategory = + prependFieldCategory != null ? prependFieldCategory : getFieldCategory(field); + if (isGeoField(field)) { + return [ + ...accumulator, + { + category: fieldCategory, + field, + values: formatGeoLocation(item), + originalValue: formatGeoLocation(item), + isObjectArray: true, // important for UI + }, + ]; + } + const objArrStr = toObjectArrayOfStrings(item); + const strArr = objArrStr.map(({ str }) => str); + const isObjectArray = objArrStr.some((o) => o.isObjectArray); + const dotField = prependField ? `${prependField}.${field}` : field; + + // return simple field value (non-object, non-array) + if (!isObjectArray) { + return [ + ...accumulator, + { + category: fieldCategory, + field: dotField, + values: strArr, + originalValue: strArr, + isObjectArray, + }, + ]; + } + + // format nested fields + const nestedFields = Array.isArray(item) + ? item + .reduce((acc, i) => [...acc, getDataFromFieldsHits(i, dotField, fieldCategory)], []) + .flat() + : getDataFromFieldsHits(item, prependField, fieldCategory); + + // combine duplicate fields + const flat: Record = [ ...accumulator, - { - category: fieldCategory, - field, - values: isGeoField(field) ? formatGeoLocation(item) : toStringArray(item), - originalValue: toStringArray(item), - } as TimelineEventsDetailsItem, - ]; + ...nestedFields, + ].reduce( + (acc, f) => ({ + ...acc, + // acc/flat is hashmap to determine if we already have the field or not without an array iteration + // its converted back to array in return with Object.values + ...(acc[f.field] != null + ? { + [f.field]: { + ...f, + originalValue: acc[f.field].originalValue.includes(f.originalValue[0]) + ? acc[f.field].originalValue + : [...acc[f.field].originalValue, ...f.originalValue], + values: acc[f.field].values.includes(f.values[0]) + ? acc[f.field].values + : [...acc[f.field].values, ...f.values], + }, + } + : { [f.field]: f }), + }), + {} + ); + + return Object.values(flat); }, []); + +export const getDataSafety = (fn: (args: A) => T, args: A): Promise => + new Promise((resolve) => setTimeout(() => resolve(fn(args)))); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index f5deb258fc1f48..7794de7f0f4119 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { cloneDeep, merge } from 'lodash/fp'; +import { cloneDeep, merge, unionBy } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { @@ -13,11 +13,13 @@ import { TimelineEventsQueries, TimelineEventsDetailsStrategyResponse, TimelineEventsDetailsRequestOptions, + TimelineEventsDetailsItem, + EventSource, } from '../../../../../../common/search_strategy'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionTimelineFactory } from '../../types'; import { buildTimelineDetailsQuery } from './query.events_details.dsl'; -import { getDataFromSourceHits } from './helpers'; +import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './helpers'; export const timelineEventsDetails: SecuritySolutionTimelineFactory = { buildDsl: (options: TimelineEventsDetailsRequestOptions) => { @@ -29,11 +31,10 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory ): Promise => { const { indexName, eventId, docValueFields = [] } = options; - const { _source, ...hitsData } = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); + const { _source, fields, ...hitsData } = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); const inspect = { dsl: [inspectStringifyObject(buildTimelineDetailsQuery(indexName, eventId, docValueFields))], }; - if (response.isRunning) { return { ...response, @@ -41,12 +42,19 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory( + getDataFromSourceHits, + _source + ); + const fieldsData = await getDataSafety( + getDataFromFieldsHits, + merge(fields, hitsData) + ); - const sourceData = getDataFromSourceHits(merge(_source, hitsData)); - + const data = unionBy('field', fieldsData, sourceData); return { ...response, - data: sourceData, + data, inspect, }; }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts index 47716e21bca319..4545a3a3e136b3 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.test.ts @@ -24,6 +24,7 @@ describe('buildTimelineDetailsQuery', () => { Object { "allowNoIndices": true, "body": Object { + "_source": true, "docvalue_fields": Array [ Object { "field": "@timestamp", @@ -38,6 +39,9 @@ describe('buildTimelineDetailsQuery', () => { "field": "agent.name", }, ], + "fields": Array [ + "*", + ], "query": Object { "terms": Object { "_id": Array [ diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index e8890072c1aff6..c624eb14ae969f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -22,6 +22,8 @@ export const buildTimelineDetailsQuery = ( _id: [id], }, }, + fields: ['*'], + _source: true, }, size: 1, }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts new file mode 100644 index 00000000000000..13b7fe70512460 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/mocks.ts @@ -0,0 +1,329 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const eventHit = { + _index: 'auditbeat-7.8.0-2020.11.05-000003', + _id: 'tkCt1nUBaEgqnrVSZ8R_', + _score: 0, + _type: '', + fields: { + 'event.category': ['process'], + 'process.ppid': [3977], + 'user.name': ['jenkins'], + 'process.args': ['go', 'vet', './...'], + message: ['Process go (PID: 4313) by user jenkins STARTED'], + 'process.pid': [4313], + 'process.working_directory': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + 'process.entity_id': ['Z59cIkAAIw8ZoK0H'], + 'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + 'process.name': ['go'], + 'event.action': ['process_started'], + 'agent.type': ['auditbeat'], + '@timestamp': ['2020-11-17T14:48:08.922Z'], + 'event.module': ['system'], + 'event.type': ['start'], + 'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + 'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + 'host.os.family': ['debian'], + 'event.kind': ['event'], + 'host.id': ['e59991e835905c65ed3e455b33e13bd6'], + 'event.dataset': ['process'], + 'process.executable': [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + 'source.geo.location': [{ coordinates: [118.7778, 32.0617], type: 'Point' }], + 'threat.indicator': [ + { + 'matched.field': ['matched_field'], + first_seen: ['2021-02-22T17:29:25.195Z'], + provider: ['yourself'], + type: ['custom'], + 'matched.atomic': ['matched_atomic'], + lazer: [ + { + 'great.field': ['grrrrr'], + }, + { + 'great.field': ['grrrrr_2'], + }, + ], + }, + { + 'matched.field': ['matched_field_2'], + first_seen: ['2021-02-22T17:29:25.195Z'], + provider: ['other_you'], + type: ['custom'], + 'matched.atomic': ['matched_atomic_2'], + lazer: [ + { + 'great.field': [ + { + wowoe: [ + { + fooooo: ['grrrrr'], + }, + ], + astring: 'cool', + aNumber: 1, + anObject: { + neat: true, + }, + }, + ], + }, + ], + }, + ], + }, + _source: {}, + sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'], + aggregations: {}, +}; + +export const eventDetailsFormattedFields = [ + { + category: 'event', + field: 'event.category', + isObjectArray: false, + originalValue: ['process'], + values: ['process'], + }, + { + category: 'process', + field: 'process.ppid', + isObjectArray: false, + originalValue: ['3977'], + values: ['3977'], + }, + { + category: 'user', + field: 'user.name', + isObjectArray: false, + originalValue: ['jenkins'], + values: ['jenkins'], + }, + { + category: 'process', + field: 'process.args', + isObjectArray: false, + originalValue: ['go', 'vet', './...'], + values: ['go', 'vet', './...'], + }, + { + category: 'base', + field: 'message', + isObjectArray: false, + originalValue: ['Process go (PID: 4313) by user jenkins STARTED'], + values: ['Process go (PID: 4313) by user jenkins STARTED'], + }, + { + category: 'process', + field: 'process.pid', + isObjectArray: false, + originalValue: ['4313'], + values: ['4313'], + }, + { + category: 'process', + field: 'process.working_directory', + isObjectArray: false, + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat', + ], + }, + { + category: 'process', + field: 'process.entity_id', + isObjectArray: false, + originalValue: ['Z59cIkAAIw8ZoK0H'], + values: ['Z59cIkAAIw8ZoK0H'], + }, + { + category: 'host', + field: 'host.ip', + isObjectArray: false, + originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'], + }, + { + category: 'process', + field: 'process.name', + isObjectArray: false, + originalValue: ['go'], + values: ['go'], + }, + { + category: 'event', + field: 'event.action', + isObjectArray: false, + originalValue: ['process_started'], + values: ['process_started'], + }, + { + category: 'agent', + field: 'agent.type', + isObjectArray: false, + originalValue: ['auditbeat'], + values: ['auditbeat'], + }, + { + category: 'base', + field: '@timestamp', + isObjectArray: false, + originalValue: ['2020-11-17T14:48:08.922Z'], + values: ['2020-11-17T14:48:08.922Z'], + }, + { + category: 'event', + field: 'event.module', + isObjectArray: false, + originalValue: ['system'], + values: ['system'], + }, + { + category: 'event', + field: 'event.type', + isObjectArray: false, + originalValue: ['start'], + values: ['start'], + }, + { + category: 'host', + field: 'host.name', + isObjectArray: false, + originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'], + }, + { + category: 'process', + field: 'process.hash.sha1', + isObjectArray: false, + originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'], + }, + { + category: 'host', + field: 'host.os.family', + isObjectArray: false, + originalValue: ['debian'], + values: ['debian'], + }, + { + category: 'event', + field: 'event.kind', + isObjectArray: false, + originalValue: ['event'], + values: ['event'], + }, + { + category: 'host', + field: 'host.id', + isObjectArray: false, + originalValue: ['e59991e835905c65ed3e455b33e13bd6'], + values: ['e59991e835905c65ed3e455b33e13bd6'], + }, + { + category: 'event', + field: 'event.dataset', + isObjectArray: false, + originalValue: ['process'], + values: ['process'], + }, + { + category: 'process', + field: 'process.executable', + isObjectArray: false, + originalValue: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + values: [ + '/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go', + ], + }, + { + category: 'source', + field: 'source.geo.location', + isObjectArray: true, + originalValue: [`{"lon":118.7778,"lat":32.0617}`], + values: [`{"lon":118.7778,"lat":32.0617}`], + }, + { + category: 'threat', + field: 'threat.indicator.matched.field', + values: ['matched_field', 'matched_field_2'], + originalValue: ['matched_field', 'matched_field_2'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.first_seen', + values: ['2021-02-22T17:29:25.195Z'], + originalValue: ['2021-02-22T17:29:25.195Z'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.provider', + values: ['yourself', 'other_you'], + originalValue: ['yourself', 'other_you'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.type', + values: ['custom'], + originalValue: ['custom'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.matched.atomic', + values: ['matched_atomic', 'matched_atomic_2'], + originalValue: ['matched_atomic', 'matched_atomic_2'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field', + values: ['grrrrr', 'grrrrr_2'], + originalValue: ['grrrrr', 'grrrrr_2'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field.wowoe.fooooo', + values: ['grrrrr'], + originalValue: ['grrrrr'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field.astring', + values: ['cool'], + originalValue: ['cool'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field.aNumber', + values: ['1'], + originalValue: ['1'], + isObjectArray: false, + }, + { + category: 'threat', + field: 'threat.indicator.lazer.great.field.neat', + values: ['true'], + originalValue: ['true'], + isObjectArray: false, + }, +]; diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index 2229f46a2e2735..8b60685b8b53d9 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -20,96 +20,140 @@ const EXPECTED_DATA = [ field: '@timestamp', values: ['2019-02-10T02:39:44.107Z'], originalValue: ['2019-02-10T02:39:44.107Z'], + isObjectArray: false, }, { category: '@version', field: '@version', values: ['1'], originalValue: ['1'], + isObjectArray: false, + }, + { + category: '_id', + field: '_id', + values: ['QRhG1WgBqd-n62SwZYDT'], + originalValue: ['QRhG1WgBqd-n62SwZYDT'], + isObjectArray: false, + }, + { + category: '_index', + field: '_index', + values: ['filebeat-7.0.0-iot-2019.06'], + originalValue: ['filebeat-7.0.0-iot-2019.06'], + isObjectArray: false, + }, + { + category: '_score', + field: '_score', + values: ['1'], + originalValue: ['1'], + isObjectArray: false, + }, + { + category: '_type', + field: '_type', + values: ['_doc'], + originalValue: ['_doc'], + isObjectArray: false, }, { category: 'agent', field: 'agent.ephemeral_id', values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], + isObjectArray: false, }, { category: 'agent', field: 'agent.hostname', values: ['raspberrypi'], originalValue: ['raspberrypi'], + isObjectArray: false, }, { category: 'agent', field: 'agent.id', values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], + isObjectArray: false, }, { category: 'agent', field: 'agent.type', values: ['filebeat'], originalValue: ['filebeat'], + isObjectArray: false, }, { category: 'agent', field: 'agent.version', values: ['7.0.0'], originalValue: ['7.0.0'], + isObjectArray: false, }, { category: 'destination', field: 'destination.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + isObjectArray: false, }, { category: 'destination', field: 'destination.ip', values: ['10.100.7.196'], originalValue: ['10.100.7.196'], + isObjectArray: false, }, { category: 'destination', field: 'destination.port', values: ['40684'], originalValue: ['40684'], + isObjectArray: false, }, { category: 'ecs', field: 'ecs.version', values: ['1.0.0-beta2'], originalValue: ['1.0.0-beta2'], + isObjectArray: false, }, { category: 'event', field: 'event.dataset', values: ['suricata.eve'], originalValue: ['suricata.eve'], + isObjectArray: false, }, { category: 'event', field: 'event.end', values: ['2019-02-10T02:39:44.107Z'], originalValue: ['2019-02-10T02:39:44.107Z'], + isObjectArray: false, }, { category: 'event', field: 'event.kind', values: ['event'], originalValue: ['event'], + isObjectArray: false, }, { category: 'event', field: 'event.module', values: ['suricata'], originalValue: ['suricata'], + isObjectArray: false, }, { category: 'event', field: 'event.type', values: ['fileinfo'], originalValue: ['fileinfo'], + isObjectArray: false, }, { category: 'file', @@ -120,270 +164,484 @@ const EXPECTED_DATA = [ originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], + isObjectArray: false, }, { category: 'file', field: 'file.size', values: ['48277'], originalValue: ['48277'], + isObjectArray: false, }, { category: 'fileset', field: 'fileset.name', values: ['eve'], originalValue: ['eve'], + isObjectArray: false, }, { category: 'flow', field: 'flow.locality', values: ['public'], originalValue: ['public'], + isObjectArray: false, }, { category: 'host', field: 'host.architecture', values: ['armv7l'], originalValue: ['armv7l'], + isObjectArray: false, + }, + { + category: 'host', + field: 'host.containerized', + values: ['false'], + originalValue: ['false'], + isObjectArray: false, }, { category: 'host', field: 'host.hostname', values: ['raspberrypi'], originalValue: ['raspberrypi'], + isObjectArray: false, }, { category: 'host', field: 'host.id', values: ['b19a781f683541a7a25ee345133aa399'], originalValue: ['b19a781f683541a7a25ee345133aa399'], + isObjectArray: false, }, { category: 'host', field: 'host.name', values: ['raspberrypi'], originalValue: ['raspberrypi'], + isObjectArray: false, }, { category: 'host', field: 'host.os.codename', values: ['stretch'], originalValue: ['stretch'], + isObjectArray: false, }, { category: 'host', field: 'host.os.family', values: [''], originalValue: [''], + isObjectArray: false, }, { category: 'host', field: 'host.os.kernel', values: ['4.14.50-v7+'], originalValue: ['4.14.50-v7+'], + isObjectArray: false, }, { category: 'host', field: 'host.os.name', values: ['Raspbian GNU/Linux'], originalValue: ['Raspbian GNU/Linux'], + isObjectArray: false, }, { category: 'host', field: 'host.os.platform', values: ['raspbian'], originalValue: ['raspbian'], + isObjectArray: false, }, { category: 'host', field: 'host.os.version', values: ['9 (stretch)'], originalValue: ['9 (stretch)'], + isObjectArray: false, }, { category: 'http', field: 'http.request.method', values: ['get'], originalValue: ['get'], + isObjectArray: false, }, { category: 'http', field: 'http.response.body.bytes', values: ['48277'], originalValue: ['48277'], + isObjectArray: false, }, { category: 'http', field: 'http.response.status_code', values: ['206'], originalValue: ['206'], + isObjectArray: false, }, { category: 'input', field: 'input.type', values: ['log'], originalValue: ['log'], + isObjectArray: false, }, { category: 'base', field: 'labels.pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], + isObjectArray: false, }, { category: 'log', field: 'log.file.path', values: ['/var/log/suricata/eve.json'], originalValue: ['/var/log/suricata/eve.json'], + isObjectArray: false, }, { category: 'log', field: 'log.offset', values: ['1856288115'], originalValue: ['1856288115'], + isObjectArray: false, }, { category: 'network', field: 'network.name', values: ['iot'], originalValue: ['iot'], + isObjectArray: false, }, { category: 'network', field: 'network.protocol', values: ['http'], originalValue: ['http'], + isObjectArray: false, }, { category: 'network', field: 'network.transport', values: ['tcp'], originalValue: ['tcp'], + isObjectArray: false, }, { category: 'service', field: 'service.type', values: ['suricata'], originalValue: ['suricata'], + isObjectArray: false, }, { category: 'source', field: 'source.as.num', values: ['16509'], originalValue: ['16509'], + isObjectArray: false, }, { category: 'source', field: 'source.as.org', values: ['Amazon.com, Inc.'], originalValue: ['Amazon.com, Inc.'], + isObjectArray: false, }, { category: 'source', field: 'source.domain', values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.city_name', values: ['Seattle'], originalValue: ['Seattle'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.continent_name', values: ['North America'], originalValue: ['North America'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], originalValue: ['US'], + isObjectArray: false, + }, + { + category: 'source', + field: 'source.geo.location', + values: ['{"lon":-122.3341,"lat":47.6103}'], + originalValue: ['{"lon":-122.3341,"lat":47.6103}'], + isObjectArray: true, }, { category: 'source', field: 'source.geo.location.lat', values: ['47.6103'], originalValue: ['47.6103'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.location.lon', values: ['-122.3341'], originalValue: ['-122.3341'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.region_iso_code', values: ['US-WA'], originalValue: ['US-WA'], + isObjectArray: false, }, { category: 'source', field: 'source.geo.region_name', values: ['Washington'], originalValue: ['Washington'], + isObjectArray: false, }, { category: 'source', field: 'source.ip', values: ['54.239.219.210'], originalValue: ['54.239.219.210'], + isObjectArray: false, }, { category: 'source', field: 'source.port', values: ['80'], originalValue: ['80'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.app_proto', + values: ['http'], + originalValue: ['http'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.dest_ip', + values: ['10.100.7.196'], + originalValue: ['10.100.7.196'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.dest_port', + values: ['40684'], + originalValue: ['40684'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.filename', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.size', + values: ['48277'], + originalValue: ['48277'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.fileinfo.state', values: ['CLOSED'], originalValue: ['CLOSED'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.fileinfo.stored', + values: ['false'], + originalValue: ['false'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.fileinfo.tx_id', values: ['301'], originalValue: ['301'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.flow_id', values: ['196625917175466'], originalValue: ['196625917175466'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.hostname', + values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.http.http_content_type', values: ['video/mp4'], originalValue: ['video/mp4'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.http_method', + values: ['get'], + originalValue: ['get'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.length', + values: ['48277'], + originalValue: ['48277'], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.http.protocol', values: ['HTTP/1.1'], originalValue: ['HTTP/1.1'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.status', + values: ['206'], + originalValue: ['206'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.http.url', + values: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + originalValue: [ + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], + isObjectArray: false, }, { category: 'suricata', field: 'suricata.eve.in_iface', values: ['eth0'], originalValue: ['eth0'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.proto', + values: ['tcp'], + originalValue: ['tcp'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.src_ip', + values: ['54.239.219.210'], + originalValue: ['54.239.219.210'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.src_port', + values: ['80'], + originalValue: ['80'], + isObjectArray: false, + }, + { + category: 'suricata', + field: 'suricata.eve.timestamp', + values: ['2019-02-10T02:39:44.107Z'], + originalValue: ['2019-02-10T02:39:44.107Z'], + isObjectArray: false, }, { category: 'base', field: 'tags', values: ['suricata'], originalValue: ['suricata'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.city_name', + values: ['Seattle'], + originalValue: ['Seattle'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.continent_name', + values: ['North America'], + originalValue: ['North America'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.country_iso_code', + values: ['US'], + originalValue: ['US'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.location', + values: ['{"lon":-122.3341,"lat":47.6103}'], + originalValue: ['{"lon":-122.3341,"lat":47.6103}'], + isObjectArray: true, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.region_iso_code', + values: ['US-WA'], + originalValue: ['US-WA'], + isObjectArray: false, + }, + { + category: 'traefik', + field: 'traefik.access.geoip.region_name', + values: ['Washington'], + originalValue: ['Washington'], + isObjectArray: false, }, { category: 'url', field: 'url.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + isObjectArray: false, }, { category: 'url', @@ -394,6 +652,7 @@ const EXPECTED_DATA = [ originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], + isObjectArray: false, }, { category: 'url', @@ -404,28 +663,9 @@ const EXPECTED_DATA = [ originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - }, - { - category: '_index', - field: '_index', - values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: ['filebeat-7.0.0-iot-2019.06'], - }, - { category: '_type', field: '_type', values: ['_doc'], originalValue: ['_doc'] }, - { - category: '_id', - field: '_id', - values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: ['QRhG1WgBqd-n62SwZYDT'], - }, - { - category: '_score', - field: '_score', - values: ['1'], - originalValue: ['1'], + isObjectArray: false, }, ]; - const EXPECTED_KPI_COUNTS = { destinationIpCount: 154, hostCount: 1, @@ -457,7 +697,7 @@ export default function ({ getService }: FtrProviderContext) { wait_for_completion_timeout: '10s', }) .expect(200); - expect(sortBy(detailsData, 'name')).to.eql(sortBy(EXPECTED_DATA, 'name')); + expect(sortBy(detailsData, 'field')).to.eql(sortBy(EXPECTED_DATA, 'field')); }); it('Make sure that we get kpi data', async () => { From d3b4f8e93bc5951da05001022c313c48809ece6b Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 2 Mar 2021 17:11:31 +0100 Subject: [PATCH 04/63] [Lens] Fix runtime validation error message (#93195) (#93217) --- .../editor_frame/workspace_panel/workspace_panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index af5411dd4d3b05..486c6f120d4a80 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -515,7 +515,7 @@ export const InnerVisualizationWrapper = ({

{localState.expandError ? ( -

visibleErrorMessage

+

{visibleErrorMessage}

) : null} } From 1af47c71c5133071def7b832cc2551dbef93a0a9 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Tue, 2 Mar 2021 11:19:47 -0500 Subject: [PATCH 05/63] [APM] Fix hidden search bar in error pages while loading (#84476) (#93139) (#93165) --- .../app/ErrorGroupDetails/index.tsx | 68 +++++++++++-------- .../app/error_group_overview/index.tsx | 2 +- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 4cd2db43621a8f..2ef64b9945e286 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -60,6 +60,45 @@ function getShortGroupId(errorGroupId?: string) { return errorGroupId.slice(0, 5); } +function ErrorGroupHeader({ + groupId, + isUnhandled, +}: { + groupId: string; + isUnhandled?: boolean; +}) { + return ( + <> + + + + +

+ {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', { + defaultMessage: 'Error group {errorGroupId}', + values: { + errorGroupId: getShortGroupId(groupId), + }, + })} +

+
+
+ {isUnhandled && ( + + + {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', { + defaultMessage: 'Unhandled', + })} + + + )} +
+
+ + + ); +} + type ErrorGroupDetailsProps = RouteComponentProps<{ groupId: string; serviceName: string; @@ -102,7 +141,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); if (!errorGroupData || !errorDistributionData) { - return null; + return ; } // If there are 0 occurrences, show only distribution chart w. empty message @@ -115,32 +154,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { return ( <> - - - - -

- {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', { - defaultMessage: 'Error group {errorGroupId}', - values: { - errorGroupId: getShortGroupId(groupId), - }, - })} -

-
-
- {isUnhandled && ( - - - {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', { - defaultMessage: 'Unhandled', - })} - - - )} -
-
- + diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index bde23eddaa44fa..d2aac0d4523e50 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -67,7 +67,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); if (!errorDistributionData || !errorGroupListData) { - return null; + return ; } return ( From 17d97e9ce6f85fc0751edc9ffec4df0b8342ce91 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 2 Mar 2021 11:25:25 -0500 Subject: [PATCH 06/63] [7.12] [Security Solution][Case][Bug] Prevent closing collection when pushing (#93095) (#93148) * [Security Solution][Case][Bug] Prevent closing collection when pushing (#93095) * Prevent closing collection when pushing * Fixing translations # Conflicts: # x-pack/plugins/translations/translations/ja-JP.json # x-pack/plugins/translations/translations/zh-CN.json * Fixing translations * Removing translation changes * Removing translation changes take 2 --- .../plugins/case/server/client/cases/push.ts | 28 +++++++-- .../configure_cases/closure_options.tsx | 7 ++- .../configure_cases/translations.ts | 7 +++ .../basic/tests/cases/push_case.ts | 61 ++++++++++++++++++- 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/case/server/client/cases/push.ts b/x-pack/plugins/case/server/client/cases/push.ts index 352328ed1dd40d..80dcc7a0e018c3 100644 --- a/x-pack/plugins/case/server/client/cases/push.ts +++ b/x-pack/plugins/case/server/client/cases/push.ts @@ -11,6 +11,8 @@ import { SavedObjectsClientContract, SavedObjectsUpdateResponse, Logger, + SavedObjectsFindResponse, + SavedObject, } from 'kibana/server'; import { ActionResult, ActionsClient } from '../../../../actions/server'; import { flattenCaseSavedObject, getAlertIndicesAndIDs } from '../../routes/api/utils'; @@ -25,6 +27,8 @@ import { CommentAttributes, CaseUserActionsResponse, User, + ESCasesConfigureAttributes, + CaseType, } from '../../../common/api'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; @@ -37,6 +41,22 @@ import { import { CaseClientHandler } from '../client'; import { createCaseError } from '../../common/error'; +/** + * Returns true if the case should be closed based on the configuration settings and whether the case + * is a collection. Collections are not closable because we aren't allowing their status to be changed. + * In the future we could allow push to close all the sub cases of a collection but that's not currently supported. + */ +function shouldCloseByPush( + configureSettings: SavedObjectsFindResponse, + caseInfo: SavedObject +): boolean { + return ( + configureSettings.total > 0 && + configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' && + caseInfo.attributes.type !== CaseType.collection + ); +} + interface PushParams { savedObjectsClient: SavedObjectsClientContract; caseService: CaseServiceSetup; @@ -190,14 +210,15 @@ export const push = async ({ let updatedCase: SavedObjectsUpdateResponse; let updatedComments: SavedObjectsBulkUpdateResponse; + const shouldMarkAsClosed = shouldCloseByPush(myCaseConfigure, myCase); + try { [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ client: savedObjectsClient, caseId, updatedAttributes: { - ...(myCaseConfigure.total > 0 && - myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' + ...(shouldMarkAsClosed ? { status: CaseStatuses.closed, closed_at: pushedDate, @@ -228,8 +249,7 @@ export const push = async ({ userActionService.postUserActions({ client: savedObjectsClient, actions: [ - ...(myCaseConfigure.total > 0 && - myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' + ...(shouldMarkAsClosed ? [ buildCaseUserActionItem({ action: 'update', diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx index 9417877e58f759..ba892116320ce2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/closure_options.tsx @@ -27,7 +27,12 @@ const ClosureOptionsComponent: React.FC = ({ {i18n.CASE_CLOSURE_OPTIONS_TITLE}} - description={i18n.CASE_CLOSURE_OPTIONS_DESC} + description={ + <> +

{i18n.CASE_CLOSURE_OPTIONS_DESC}

+

{i18n.CASE_COLSURE_OPTIONS_SUB_CASES}

+ + } data-test-subj="case-closure-options-form-group" > { @@ -228,6 +234,59 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.status).to.eql('closed'); }); + it('should push a collection case but not close it when closure_type: close-by-pushing', async () => { + const { body: connector } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send({ + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }) + .expect(200); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + }), + closure_type: 'close-by-pushing', + }) + .expect(200); + + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCollectionReq, + connector: getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + }).connector, + }) + .expect(200); + + const { body } = await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(200); + + expect(body.status).to.eql(CaseStatuses.open); + }); + it('unhappy path - 404s when case does not exist', async () => { await supertest .post(`${CASES_URL}/fake-id/connector/fake-connector/_push`) From b359f135cb88185957fbdd209f07ae403de74756 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Tue, 2 Mar 2021 17:26:32 +0100 Subject: [PATCH 07/63] Collect agent telemetry even when fleet server is disabled. (#93198) (#93222) --- x-pack/plugins/fleet/server/collectors/agent_collectors.ts | 5 ++++- x-pack/plugins/fleet/server/collectors/register.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index 154e78feae2832..ce4fb3e3df7d4c 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -6,6 +6,7 @@ */ import { ElasticsearchClient, SavedObjectsClient } from 'kibana/server'; +import { FleetConfigType } from '../../common/types'; import * as AgentService from '../services/agents'; import { isFleetServerSetup } from '../services/fleet_server'; @@ -17,11 +18,13 @@ export interface AgentUsage { } export const getAgentUsage = async ( + config: FleetConfigType, soClient?: SavedObjectsClient, esClient?: ElasticsearchClient ): Promise => { // TODO: unsure if this case is possible at all. - if (!soClient || !esClient || !(await isFleetServerSetup())) { + const fleetServerMissing = config.agents.fleetServerEnabled && !(await isFleetServerSetup()); + if (!soClient || !esClient || fleetServerMissing) { return { total: 0, online: 0, diff --git a/x-pack/plugins/fleet/server/collectors/register.ts b/x-pack/plugins/fleet/server/collectors/register.ts index 3e4ed80a3a83fe..c2e043145cd972 100644 --- a/x-pack/plugins/fleet/server/collectors/register.ts +++ b/x-pack/plugins/fleet/server/collectors/register.ts @@ -38,7 +38,7 @@ export function registerFleetUsageCollector( const [soClient, esClient] = await getInternalClients(core); return { agents_enabled: getIsAgentsEnabled(config), - agents: await getAgentUsage(soClient, esClient), + agents: await getAgentUsage(config, soClient, esClient), packages: await getPackageUsage(soClient), }; }, From f77dfbe15c4ad09d5482cc8e426db200a8647ed8 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Mar 2021 11:40:48 -0500 Subject: [PATCH 08/63] [Security Solution] [Detections] Updates warning message when no indices match provided index patterns (#93094) (#93221) * updates warning messages and modifies warning message when endpoint security rule is missing index pattern * fix integration test text Co-authored-by: Devin W. Hurley --- .../signals/signal_rule_alert_type.ts | 1 + .../detection_engine/signals/utils.test.ts | 56 +++++++++++++++++++ .../lib/detection_engine/signals/utils.ts | 13 +++-- .../security_and_spaces/tests/create_rules.ts | 2 +- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 94415f9b59e004..1fd0552569b2d8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -217,6 +217,7 @@ export const signalRulesAlertType = ({ hasTimestampFields( wroteStatus, hasTimestampOverride ? (timestampOverride as string) : '@timestamp', + name, timestampFieldCaps, inputIndices, ruleStatusService, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 7888bb6deaab79..a68fac7d920f26 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -843,6 +843,7 @@ describe('utils', () => { const res = await hasTimestampFields( false, timestampField, + 'myfakerulename', // eslint-disable-next-line @typescript-eslint/no-explicit-any timestampFieldCapsResponse as ApiResponse>, ['myfa*'], @@ -883,6 +884,7 @@ describe('utils', () => { const res = await hasTimestampFields( false, timestampField, + 'myfakerulename', // eslint-disable-next-line @typescript-eslint/no-explicit-any timestampFieldCapsResponse as ApiResponse>, ['myfa*'], @@ -895,6 +897,60 @@ describe('utils', () => { ); expect(res).toBeTruthy(); }); + + test('returns true when missing logs-endpoint.alerts-* index and rule name is Endpoint Security', async () => { + const timestampField = '@timestamp'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const timestampFieldCapsResponse: Partial, Context>> = { + body: { + indices: [], + fields: {}, + }, + }; + mockLogger.error.mockClear(); + const res = await hasTimestampFields( + false, + timestampField, + 'Endpoint Security', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + timestampFieldCapsResponse as ApiResponse>, + ['logs-endpoint.alerts-*'], + ruleStatusServiceMock, + mockLogger, + buildRuleMessage + ); + expect(mockLogger.error).toHaveBeenCalledWith( + 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' + ); + expect(res).toBeTruthy(); + }); + + test('returns true when missing logs-endpoint.alerts-* index and rule name is NOT Endpoint Security', async () => { + const timestampField = '@timestamp'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const timestampFieldCapsResponse: Partial, Context>> = { + body: { + indices: [], + fields: {}, + }, + }; + mockLogger.error.mockClear(); + const res = await hasTimestampFields( + false, + timestampField, + 'NOT Endpoint Security', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + timestampFieldCapsResponse as ApiResponse>, + ['logs-endpoint.alerts-*'], + ruleStatusServiceMock, + mockLogger, + buildRuleMessage + ); + expect(mockLogger.error).toHaveBeenCalledWith( + 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' + ); + expect(res).toBeTruthy(); + }); }); describe('wrapBuildingBlocks', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 58bf22be97bf87..30471ae2a35484 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -105,6 +105,7 @@ export const hasReadIndexPrivileges = async ( export const hasTimestampFields = async ( wroteStatus: boolean, timestampField: string, + ruleName: string, // any is derived from here // node_modules/@elastic/elasticsearch/api/kibana.d.ts // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -115,11 +116,15 @@ export const hasTimestampFields = async ( buildRuleMessage: BuildRuleMessage ): Promise => { if (!wroteStatus && isEmpty(timestampFieldCapsResponse.body.indices)) { - const errorString = `The following index patterns did not match any indices: ${JSON.stringify( + const errorString = `This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ${JSON.stringify( inputIndices - )}`; - logger.error(buildRuleMessage(errorString)); - await ruleStatusService.warning(errorString); + )} was found. This warning will continue to appear until a matching index is created or this rule is de-activated. ${ + ruleName === 'Endpoint Security' + ? 'If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent.' + : '' + }`; + logger.error(buildRuleMessage(errorString.trimEnd())); + await ruleStatusService.warning(errorString.trimEnd()); return true; } else if ( !wroteStatus && diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index 8d494724d9f766..6bdf881ba8ca29 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -138,7 +138,7 @@ export default ({ getService }: FtrProviderContext) => { expect(statusBody[body.id].current_status.status).to.eql('warning'); expect(statusBody[body.id].current_status.last_success_message).to.eql( - 'The following index patterns did not match any indices: ["does-not-exist-*"]' + 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["does-not-exist-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated.' ); }); From fc19967e6a33a60ee2875cc1e9b5f484bd9f38e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Tue, 2 Mar 2021 17:45:23 +0100 Subject: [PATCH 09/63] [ILM] Refactor edit_policy client integration tests into separate feature files (#92826) (#93229) * Refactor edit_policy client integration tests into separate feature files * Fixed merge conflicts with master * Updated rollover tests to match master branch code * Split reactive_form into smaller files * Renamed flyout tests file --- .../edit_policy/constants.ts | 21 +- .../edit_policy/edit_policy.helpers.tsx | 9 - .../edit_policy/edit_policy.test.ts | 1136 ----------------- .../edit_policy/features/cold_phase.test.ts | 52 + .../edit_policy/features/delete_phase.test.ts | 169 +++ .../node_allocation.test.ts | 138 +- .../features/request_flyout.test.ts | 66 + .../edit_policy/features/rollover.test.ts | 108 ++ .../features/searchable_snapshots.test.ts | 163 +++ .../edit_policy/features/timeline.test.ts | 64 + .../edit_policy/features/warm_phase.test.ts | 52 + .../form_validation/error_indicators.test.ts | 159 +++ .../reactive_form/reactive_form.test.ts | 143 --- .../policy_serialization.test.ts | 426 +++++++ 14 files changed, 1373 insertions(+), 1333 deletions(-) delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts rename x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/{reactive_form => features}/node_allocation.test.ts (78%) create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts delete mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index 2c8fbfc749a82d..e47036b82e5941 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -13,26 +13,6 @@ export const POLICY_NAME = 'my_policy'; export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy'; export const NEW_SNAPSHOT_POLICY_NAME = 'my_new_snapshot_policy'; -export const DEFAULT_POLICY: PolicyFromES = { - version: 1, - modified_date: Date.now().toString(), - policy: { - name: 'my_policy', - phases: { - hot: { - min_age: '0ms', - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - }, - }, - }, - }, - name: 'my_policy', -}; - export const POLICY_WITH_MIGRATE_OFF: PolicyFromES = { version: 1, modified_date: Date.now().toString(), @@ -191,6 +171,7 @@ export const POLICY_WITH_NODE_ROLE_ALLOCATION: PolicyFromES = { }, }, warm: { + min_age: '0ms', actions: {}, }, }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index c61b431eed46dc..b692d7fe69cd48 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -183,13 +183,6 @@ export const setup = async (arg?: { const enable = (phase: Phases) => createFormToggleAction(`enablePhaseSwitch-${phase}`); - const showDataAllocationOptions = (phase: Phases) => () => { - act(() => { - find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click'); - }); - component.update(); - }; - const createMinAgeActions = (phase: Phases) => { return { hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`), @@ -384,7 +377,6 @@ export const setup = async (arg?: { }, warm: { enable: enable('warm'), - showDataAllocationOptions: showDataAllocationOptions('warm'), ...createMinAgeActions('warm'), setReplicas: setReplicas('warm'), hasErrorIndicator: () => exists('phaseErrorIndicator-warm'), @@ -396,7 +388,6 @@ export const setup = async (arg?: { }, cold: { enable: enable('cold'), - showDataAllocationOptions: showDataAllocationOptions('cold'), ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), setFreeze, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts deleted file mode 100644 index 740aeebb852f18..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ /dev/null @@ -1,1136 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; - -import { licensingMock } from '../../../../licensing/public/mocks'; -import { API_BASE_PATH } from '../../../common/constants'; -import { setupEnvironment } from '../helpers/setup_environment'; -import { EditPolicyTestBed, setup } from './edit_policy.helpers'; - -import { - DELETE_PHASE_POLICY, - NEW_SNAPSHOT_POLICY_NAME, - SNAPSHOT_POLICY_NAME, - DEFAULT_POLICY, - POLICY_WITH_MIGRATE_OFF, - POLICY_WITH_INCLUDE_EXCLUDE, - POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, - POLICY_WITH_NODE_ROLE_ALLOCATION, - POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, - getDefaultHotPhasePolicy, -} from './constants'; - -describe('', () => { - let testBed: EditPolicyTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - afterAll(() => { - server.restore(); - }); - - describe('serialization', () => { - /** - * We assume that policies that populate this form are loaded directly from ES and so - * are valid according to ES. There may be settings in the policy created through the ILM - * API that the UI does not cater for, like the unfollow action. We do not want to overwrite - * the configuration for these actions in the UI. - */ - it('preserves policy settings it did not configure', async () => { - httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS]); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: {}, - nodesByAttributes: { test: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - - await act(async () => { - testBed = await setup(); - }); - - const { component, actions } = testBed; - component.update(); - - // Set max docs to test whether we keep the unknown fields in that object after serializing - await actions.hot.setMaxDocs('1000'); - // Remove the delete phase to ensure that we also correctly remove data - await actions.delete.disablePhase(); - await actions.savePolicy(); - - const latestRequest = server.requests[server.requests.length - 1]; - const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); - - expect(entirePolicy).toEqual({ - foo: 'bar', // Made up value - name: 'my_policy', - phases: { - hot: { - actions: { - rollover: { - max_docs: 1000, - max_size: '50gb', - unknown_setting: 123, // Made up setting that should stay preserved - }, - }, - min_age: '0ms', - }, - warm: { - actions: { - my_unfollow_action: {}, // Made up action - set_priority: { - priority: 22, - unknown_setting: true, - }, - }, - min_age: '0d', - }, - }, - }); - }); - }); - - describe('hot phase', () => { - describe('serialization', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: {}, - nodesByAttributes: { test: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('setting all values', async () => { - const { actions } = testBed; - - await actions.hot.toggleDefaultRollover(false); - await actions.hot.setMaxSize('123', 'mb'); - await actions.hot.setMaxDocs('123'); - await actions.hot.setMaxAge('123', 'h'); - await actions.hot.toggleForceMerge(true); - await actions.hot.setForcemergeSegmentsCount('123'); - await actions.hot.setBestCompression(true); - await actions.hot.toggleShrink(true); - await actions.hot.setShrink('2'); - await actions.hot.toggleReadonly(true); - await actions.hot.toggleIndexPriority(true); - await actions.hot.setIndexPriority('123'); - - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(entirePolicy).toMatchInlineSnapshot(` - Object { - "name": "my_policy", - "phases": Object { - "hot": Object { - "actions": Object { - "forcemerge": Object { - "index_codec": "best_compression", - "max_num_segments": 123, - }, - "readonly": Object {}, - "rollover": Object { - "max_age": "123h", - "max_docs": 123, - "max_size": "123mb", - }, - "set_priority": Object { - "priority": 123, - }, - "shrink": Object { - "number_of_shards": 2, - }, - }, - "min_age": "0ms", - }, - }, - } - `); - }); - - test('setting searchable snapshot', async () => { - const { actions } = testBed; - - await actions.hot.setSearchableSnapshot('my-repo'); - - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(entirePolicy.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe( - 'my-repo' - ); - }); - - test('disabling rollover', async () => { - const { actions } = testBed; - await actions.hot.toggleDefaultRollover(false); - await actions.hot.toggleRollover(false); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const policy = JSON.parse(JSON.parse(latestRequest.requestBody).body); - const hotActions = policy.phases.hot.actions; - const rolloverAction = hotActions.rollover; - expect(rolloverAction).toBe(undefined); - expect(hotActions).toMatchInlineSnapshot(`Object {}`); - }); - - test('enabling searchable snapshot should hide force merge, freeze and shrink in subsequent phases', async () => { - const { actions } = testBed; - - await actions.warm.enable(true); - await actions.cold.enable(true); - - expect(actions.warm.forceMergeFieldExists()).toBeTruthy(); - expect(actions.warm.shrinkExists()).toBeTruthy(); - expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); - expect(actions.cold.freezeExists()).toBeTruthy(); - - await actions.hot.setSearchableSnapshot('my-repo'); - - expect(actions.warm.forceMergeFieldExists()).toBeFalsy(); - expect(actions.warm.shrinkExists()).toBeFalsy(); - // searchable snapshot in cold is still visible - expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); - expect(actions.cold.freezeExists()).toBeFalsy(); - }); - - test('disabling rollover toggle, but enabling default rollover', async () => { - const { actions } = testBed; - await actions.hot.toggleDefaultRollover(false); - await actions.hot.toggleRollover(false); - await actions.hot.toggleDefaultRollover(true); - - expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); - expect(actions.hot.shrinkExists()).toBeTruthy(); - expect(actions.hot.searchableSnapshotsExists()).toBeTruthy(); - }); - }); - }); - - describe('warm phase', () => { - describe('serialization', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: {}, - nodesByAttributes: { test: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('default values', async () => { - const { actions } = testBed; - await actions.warm.enable(true); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm; - expect(warmPhase).toMatchInlineSnapshot(` - Object { - "actions": Object { - "set_priority": Object { - "priority": 50, - }, - }, - "min_age": "0d", - } - `); - }); - - test('setting all values', async () => { - const { actions } = testBed; - await actions.warm.enable(true); - await actions.warm.setDataAllocation('node_attrs'); - await actions.warm.setSelectedNodeAttribute('test:123'); - await actions.warm.setReplicas('123'); - await actions.warm.toggleShrink(true); - await actions.warm.setShrink('123'); - await actions.warm.toggleForceMerge(true); - await actions.warm.setForcemergeSegmentsCount('123'); - await actions.warm.setBestCompression(true); - await actions.warm.toggleReadonly(true); - await actions.warm.setIndexPriority('123'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); - // Check shape of entire policy - expect(entirePolicy).toMatchInlineSnapshot(` - Object { - "name": "my_policy", - "phases": Object { - "hot": Object { - "actions": Object { - "rollover": Object { - "max_age": "30d", - "max_size": "50gb", - }, - }, - "min_age": "0ms", - }, - "warm": Object { - "actions": Object { - "allocate": Object { - "number_of_replicas": 123, - "require": Object { - "test": "123", - }, - }, - "forcemerge": Object { - "index_codec": "best_compression", - "max_num_segments": 123, - }, - "readonly": Object {}, - "set_priority": Object { - "priority": 123, - }, - "shrink": Object { - "number_of_shards": 123, - }, - }, - "min_age": "0d", - }, - }, - } - `); - }); - }); - - describe('policy with include and exclude', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_INCLUDE_EXCLUDE]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: {}, - nodesByAttributes: { test: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('preserves include, exclude allocation settings', async () => { - const { actions } = testBed; - await actions.warm.setDataAllocation('node_attrs'); - await actions.warm.setSelectedNodeAttribute('test:123'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const warmPhaseAllocate = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm - .actions.allocate; - expect(warmPhaseAllocate).toMatchInlineSnapshot(` - Object { - "exclude": Object { - "def": "456", - }, - "include": Object { - "abc": "123", - }, - "require": Object { - "test": "123", - }, - } - `); - }); - }); - }); - - describe('cold phase', () => { - describe('serialization', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: {}, - nodesByAttributes: { test: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('default values', async () => { - const { actions } = testBed; - - await actions.cold.enable(true); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(entirePolicy.phases.cold).toMatchInlineSnapshot(` - Object { - "actions": Object { - "set_priority": Object { - "priority": 0, - }, - }, - "min_age": "0d", - } - `); - }); - - test('setting all values, excluding searchable snapshot', async () => { - const { actions } = testBed; - - await actions.cold.enable(true); - await actions.cold.setMinAgeValue('123'); - await actions.cold.setMinAgeUnits('s'); - await actions.cold.setDataAllocation('node_attrs'); - await actions.cold.setSelectedNodeAttribute('test:123'); - await actions.cold.setSearchableSnapshot('my-repo'); - await actions.cold.setReplicas('123'); - await actions.cold.setFreeze(true); - await actions.cold.setIndexPriority('123'); - - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); - - expect(entirePolicy).toMatchInlineSnapshot(` - Object { - "name": "my_policy", - "phases": Object { - "cold": Object { - "actions": Object { - "allocate": Object { - "number_of_replicas": 123, - "require": Object { - "test": "123", - }, - }, - "freeze": Object {}, - "searchable_snapshot": Object { - "snapshot_repository": "my-repo", - }, - "set_priority": Object { - "priority": 123, - }, - }, - "min_age": "123s", - }, - "hot": Object { - "actions": Object { - "rollover": Object { - "max_age": "30d", - "max_size": "50gb", - }, - }, - "min_age": "0ms", - }, - }, - } - `); - }); - }); - }); - - describe('delete phase', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([DELETE_PHASE_POLICY]); - httpRequestsMockHelpers.setLoadSnapshotPolicies([ - SNAPSHOT_POLICY_NAME, - NEW_SNAPSHOT_POLICY_NAME, - ]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('serialization', async () => { - httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); - await act(async () => { - testBed = await setup(); - }); - const { component, actions } = testBed; - component.update(); - await actions.delete.enablePhase(); - await actions.setWaitForSnapshotPolicy('test'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(entirePolicy.phases.delete).toEqual({ - min_age: '365d', - actions: { - delete: {}, - wait_for_snapshot: { - policy: 'test', - }, - }, - }); - }); - - test('wait for snapshot policy field should correctly display snapshot policy name', () => { - expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ - { - label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy, - }, - ]); - }); - - test('wait for snapshot field should correctly update snapshot policy name', async () => { - const { actions } = testBed; - - await actions.setWaitForSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME); - await actions.savePolicy(); - - const expected = { - name: DELETE_PHASE_POLICY.name, - phases: { - ...DELETE_PHASE_POLICY.policy.phases, - delete: { - ...DELETE_PHASE_POLICY.policy.phases.delete, - actions: { - ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, - wait_for_snapshot: { - policy: NEW_SNAPSHOT_POLICY_NAME, - }, - }, - }, - }, - }; - - const latestRequest = server.requests[server.requests.length - 1]; - expect(latestRequest.url).toBe(`${API_BASE_PATH}/policies`); - expect(latestRequest.method).toBe('POST'); - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); - }); - - test('wait for snapshot field should display a callout when the input is not an existing policy', async () => { - const { actions } = testBed; - - await actions.setWaitForSnapshotPolicy('my_custom_policy'); - expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); - expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); - expect(testBed.find('customPolicyCallout').exists()).toBeTruthy(); - }); - - test('wait for snapshot field should delete action if field is empty', async () => { - const { actions } = testBed; - - await actions.setWaitForSnapshotPolicy(''); - await actions.savePolicy(); - - const expected = { - name: DELETE_PHASE_POLICY.name, - phases: { - ...DELETE_PHASE_POLICY.policy.phases, - delete: { - ...DELETE_PHASE_POLICY.policy.phases.delete, - actions: { - ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, - }, - }, - }, - }; - - delete expected.phases.delete.actions.wait_for_snapshot; - - const latestRequest = server.requests[server.requests.length - 1]; - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); - }); - - test('wait for snapshot field should display a callout when there are no snapshot policies', async () => { - // need to call setup on testBed again for it to use a newly defined snapshot policies response - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - await act(async () => { - testBed = await setup(); - }); - - testBed.component.update(); - expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); - expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); - expect(testBed.find('noPoliciesCallout').exists()).toBeTruthy(); - }); - - test('wait for snapshot field should display a callout when there is an error loading snapshot policies', async () => { - // need to call setup on testBed again for it to use a newly defined snapshot policies response - httpRequestsMockHelpers.setLoadSnapshotPolicies([], { status: 500, body: 'error' }); - await act(async () => { - testBed = await setup(); - }); - - testBed.component.update(); - expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); - expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); - expect(testBed.find('policiesErrorCallout').exists()).toBeTruthy(); - }); - }); - - describe('data allocation', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_MIGRATE_OFF]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: {}, - nodesByAttributes: { test: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('setting node_attr based allocation, but not selecting node attribute', async () => { - const { actions } = testBed; - await actions.warm.setDataAllocation('node_attrs'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm; - - expect(warmPhase.actions.migrate).toEqual({ enabled: false }); - }); - - describe('node roles', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ROLE_ALLOCATION]); - httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, - }); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('detecting use of the recommended allocation type', () => { - const { find } = testBed; - const selectedDataAllocation = find( - 'warm-dataTierAllocationControls.dataTierSelect' - ).text(); - expect(selectedDataAllocation).toBe('Use warm nodes (recommended)'); - }); - - test('setting replicas serialization', async () => { - const { actions } = testBed; - await actions.warm.setReplicas('123'); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const warmPhaseActions = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm - .actions; - expect(warmPhaseActions).toMatchInlineSnapshot(` - Object { - "allocate": Object { - "number_of_replicas": 123, - }, - } - `); - }); - }); - - describe('node attr and none', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION]); - httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, - }); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('detecting use of the custom allocation type', () => { - const { find } = testBed; - expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom'); - }); - test('detecting use of the "off" allocation type', () => { - const { find } = testBed; - expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); - }); - }); - describe('on cloud', () => { - describe('using legacy data role config', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: { test: ['123'] }, - // On cloud, even if there are data_* roles set, the default, recommended allocation option should not - // be available. - nodesByRoles: { data_hot: ['123'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); - - await act(async () => { - testBed = await setup({ - appServicesContext: { - cloud: { - isCloudEnabled: true, - }, - license: licensingMock.createLicense({ license: { type: 'basic' } }), - }, - }); - }); - - const { component } = testBed; - component.update(); - }); - test('removes default, recommended option', async () => { - const { actions, find } = testBed; - await actions.warm.enable(true); - actions.warm.showDataAllocationOptions(); - - expect(find('defaultDataAllocationOption').exists()).toBeFalsy(); - expect(find('customDataAllocationOption').exists()).toBeTruthy(); - expect(find('noneDataAllocationOption').exists()).toBeTruthy(); - // Show the call-to-action for users to migrate their cluster to use node roles - expect(find('cloudDataTierCallout').exists()).toBeTruthy(); - }); - }); - describe('using node roles', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data_hot: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); - - await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); - }); - - const { component } = testBed; - component.update(); - }); - - test('should show recommended, custom and "off" options on cloud with data roles', async () => { - const { actions, find } = testBed; - - await actions.warm.enable(true); - actions.warm.showDataAllocationOptions(); - expect(find('defaultDataAllocationOption').exists()).toBeTruthy(); - expect(find('customDataAllocationOption').exists()).toBeTruthy(); - expect(find('noneDataAllocationOption').exists()).toBeTruthy(); - // We should not be showing the call-to-action for users to activate the cold tier on cloud - expect(find('cloudMissingColdTierCallout').exists()).toBeFalsy(); - // Do not show the call-to-action for users to migrate their cluster to use node roles - expect(find('cloudDataTierCallout').exists()).toBeFalsy(); - }); - - test('should show cloud notice when cold tier nodes do not exist', async () => { - const { actions, find } = testBed; - await actions.cold.enable(true); - expect(find('cloudMissingColdTierCallout').exists()).toBeTruthy(); - // Assert that other notices are not showing - expect(find('defaultAllocationNotice').exists()).toBeFalsy(); - expect(find('noNodeAttributesWarning').exists()).toBeFalsy(); - }); - }); - }); - }); - - describe('searchable snapshot', () => { - describe('on non-enterprise license', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, - }); - httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); - - await act(async () => { - testBed = await setup({ - appServicesContext: { - license: licensingMock.createLicense({ license: { type: 'basic' } }), - }, - }); - }); - - const { component } = testBed; - component.update(); - }); - test('disable setting searchable snapshots', async () => { - const { actions } = testBed; - - expect(actions.cold.searchableSnapshotsExists()).toBeFalsy(); - expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); - - await actions.cold.enable(true); - - // Still hidden in hot - expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); - - expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); - expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy(); - }); - }); - - describe('on cloud', () => { - describe('new policy', () => { - beforeEach(async () => { - // simulate creating a new policy - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]); - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); - - await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); - }); - - const { component } = testBed; - component.update(); - }); - test('defaults searchable snapshot to true on cloud', async () => { - const { find, actions } = testBed; - await actions.cold.enable(true); - expect( - find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked'] - ).toBe(true); - }); - }); - describe('existing policy', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data_hot: ['123'] }, - }); - httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); - - await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); - }); - - const { component } = testBed; - component.update(); - }); - test('correctly sets snapshot repository default to "found-snapshots"', async () => { - const { actions } = testBed; - await actions.cold.enable(true); - await actions.cold.toggleSearchableSnapshot(true); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( - 'found-snapshots' - ); - }); - }); - }); - }); - describe('with rollover', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, - }); - httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['abc'] }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('shows rollover tip on minimum age', async () => { - const { actions } = testBed; - - await actions.warm.enable(true); - await actions.cold.enable(true); - await actions.delete.enablePhase(); - - expect(actions.warm.hasRolloverTipOnMinAge()).toBeTruthy(); - expect(actions.cold.hasRolloverTipOnMinAge()).toBeTruthy(); - expect(actions.delete.hasRolloverTipOnMinAge()).toBeTruthy(); - }); - }); - - describe('without rollover', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, - nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, - }); - httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup({ - appServicesContext: { - license: licensingMock.createLicense({ license: { type: 'enterprise' } }), - }, - }); - }); - - const { component } = testBed; - component.update(); - }); - test('hides fields in hot phase', async () => { - const { actions } = testBed; - await actions.hot.toggleDefaultRollover(false); - await actions.hot.toggleRollover(false); - - expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); - expect(actions.hot.shrinkExists()).toBeFalsy(); - expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); - expect(actions.hot.readonlyExists()).toBeFalsy(); - }); - - test('hiding rollover tip on minimum age', async () => { - const { actions } = testBed; - await actions.hot.toggleDefaultRollover(false); - await actions.hot.toggleRollover(false); - - await actions.warm.enable(true); - await actions.cold.enable(true); - await actions.delete.enablePhase(); - - expect(actions.warm.hasRolloverTipOnMinAge()).toBeFalsy(); - expect(actions.cold.hasRolloverTipOnMinAge()).toBeFalsy(); - expect(actions.delete.hasRolloverTipOnMinAge()).toBeFalsy(); - }); - }); - - describe('policy timeline', () => { - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: {}, - nodesByAttributes: { test: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - test('showing all phases on the timeline', async () => { - const { actions } = testBed; - // This is how the default policy should look - expect(actions.timeline.hasHotPhase()).toBe(true); - expect(actions.timeline.hasWarmPhase()).toBe(false); - expect(actions.timeline.hasColdPhase()).toBe(false); - expect(actions.timeline.hasDeletePhase()).toBe(false); - - await actions.warm.enable(true); - expect(actions.timeline.hasHotPhase()).toBe(true); - expect(actions.timeline.hasWarmPhase()).toBe(true); - expect(actions.timeline.hasColdPhase()).toBe(false); - expect(actions.timeline.hasDeletePhase()).toBe(false); - - await actions.cold.enable(true); - expect(actions.timeline.hasHotPhase()).toBe(true); - expect(actions.timeline.hasWarmPhase()).toBe(true); - expect(actions.timeline.hasColdPhase()).toBe(true); - expect(actions.timeline.hasDeletePhase()).toBe(false); - - await actions.delete.enablePhase(); - expect(actions.timeline.hasHotPhase()).toBe(true); - expect(actions.timeline.hasWarmPhase()).toBe(true); - expect(actions.timeline.hasColdPhase()).toBe(true); - expect(actions.timeline.hasDeletePhase()).toBe(true); - }); - }); - - describe('policy error notifications', () => { - let runTimers: () => void; - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: {}, - nodesByAttributes: { test: ['123'] }, - isUsingDeprecatedDataRoleConfig: false, - }); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - - ({ runTimers } = testBed); - }); - - test('shows phase error indicators correctly', async () => { - // This test simulates a user configuring a policy phase by phase. The flow is the following: - // 0. Start with policy with no validation issues present - // 1. Configure hot, introducing a validation error - // 2. Configure warm, introducing a validation error - // 3. Configure cold, introducing a validation error - // 4. Fix validation error in hot - // 5. Fix validation error in warm - // 6. Fix validation error in cold - // We assert against each of these progressive states. - - const { actions } = testBed; - - // 0. No validation issues - expect(actions.hasGlobalErrorCallout()).toBe(false); - expect(actions.hot.hasErrorIndicator()).toBe(false); - expect(actions.warm.hasErrorIndicator()).toBe(false); - expect(actions.cold.hasErrorIndicator()).toBe(false); - - // 1. Hot phase validation issue - await actions.hot.toggleForceMerge(true); - await actions.hot.setForcemergeSegmentsCount('-22'); - runTimers(); - expect(actions.hasGlobalErrorCallout()).toBe(true); - expect(actions.hot.hasErrorIndicator()).toBe(true); - expect(actions.warm.hasErrorIndicator()).toBe(false); - expect(actions.cold.hasErrorIndicator()).toBe(false); - - // 2. Warm phase validation issue - await actions.warm.enable(true); - await actions.warm.toggleForceMerge(true); - await actions.warm.setForcemergeSegmentsCount('-22'); - runTimers(); - expect(actions.hasGlobalErrorCallout()).toBe(true); - expect(actions.hot.hasErrorIndicator()).toBe(true); - expect(actions.warm.hasErrorIndicator()).toBe(true); - expect(actions.cold.hasErrorIndicator()).toBe(false); - - // 3. Cold phase validation issue - await actions.cold.enable(true); - await actions.cold.setReplicas('-33'); - runTimers(); - expect(actions.hasGlobalErrorCallout()).toBe(true); - expect(actions.hot.hasErrorIndicator()).toBe(true); - expect(actions.warm.hasErrorIndicator()).toBe(true); - expect(actions.cold.hasErrorIndicator()).toBe(true); - - // 4. Fix validation issue in hot - await actions.hot.setForcemergeSegmentsCount('1'); - runTimers(); - expect(actions.hasGlobalErrorCallout()).toBe(true); - expect(actions.hot.hasErrorIndicator()).toBe(false); - expect(actions.warm.hasErrorIndicator()).toBe(true); - expect(actions.cold.hasErrorIndicator()).toBe(true); - - // 5. Fix validation issue in warm - await actions.warm.setForcemergeSegmentsCount('1'); - runTimers(); - expect(actions.hasGlobalErrorCallout()).toBe(true); - expect(actions.hot.hasErrorIndicator()).toBe(false); - expect(actions.warm.hasErrorIndicator()).toBe(false); - expect(actions.cold.hasErrorIndicator()).toBe(true); - - // 6. Fix validation issue in cold - await actions.cold.setReplicas('1'); - runTimers(); - expect(actions.hasGlobalErrorCallout()).toBe(false); - expect(actions.hot.hasErrorIndicator()).toBe(false); - expect(actions.warm.hasErrorIndicator()).toBe(false); - expect(actions.cold.hasErrorIndicator()).toBe(false); - }); - - test('global error callout should show if there are any form errors', async () => { - const { actions } = testBed; - - expect(actions.hasGlobalErrorCallout()).toBe(false); - expect(actions.hot.hasErrorIndicator()).toBe(false); - expect(actions.warm.hasErrorIndicator()).toBe(false); - expect(actions.cold.hasErrorIndicator()).toBe(false); - - await actions.saveAsNewPolicy(true); - await actions.setPolicyName(''); - runTimers(); - - expect(actions.hasGlobalErrorCallout()).toBe(true); - expect(actions.hot.hasErrorIndicator()).toBe(false); - expect(actions.warm.hasErrorIndicator()).toBe(false); - expect(actions.cold.hasErrorIndicator()).toBe(false); - }); - - test('clears all error indicators if last erroring field is unmounted', async () => { - const { actions } = testBed; - - await actions.cold.enable(true); - // introduce validation error - await actions.cold.setSearchableSnapshot(''); - runTimers(); - - await actions.savePolicy(); - runTimers(); - - expect(actions.hasGlobalErrorCallout()).toBe(true); - expect(actions.hot.hasErrorIndicator()).toBe(false); - expect(actions.warm.hasErrorIndicator()).toBe(false); - expect(actions.cold.hasErrorIndicator()).toBe(true); - - // unmount the field - await actions.cold.toggleSearchableSnapshot(false); - - expect(actions.hasGlobalErrorCallout()).toBe(false); - expect(actions.hot.hasErrorIndicator()).toBe(false); - expect(actions.warm.hasErrorIndicator()).toBe(false); - expect(actions.cold.hasErrorIndicator()).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts new file mode 100644 index 00000000000000..dfb7411eb941f4 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/cold_phase.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { getDefaultHotPhasePolicy } from '../constants'; + +describe(' cold phase', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.cold.hasMinAgeInput()).toBeFalsy(); + await actions.cold.enable(true); + expect(actions.cold.hasMinAgeInput()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts new file mode 100644 index 00000000000000..0fb4951e4a4a68 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/delete_phase.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + DELETE_PHASE_POLICY, + getDefaultHotPhasePolicy, + NEW_SNAPSHOT_POLICY_NAME, + SNAPSHOT_POLICY_NAME, +} from '../constants'; +import { act } from 'react-dom/test-utils'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { API_BASE_PATH } from '../../../../common/constants'; +import { setupEnvironment } from '../../helpers/setup_environment'; + +describe(' delete phase', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([DELETE_PHASE_POLICY]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([ + SNAPSHOT_POLICY_NAME, + NEW_SNAPSHOT_POLICY_NAME, + ]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('is hidden when disabled', async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + + expect(actions.delete.isShown()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.isShown()).toBeTruthy(); + }); + + test('shows timing after it was enabled', async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + + expect(actions.delete.hasMinAgeInput()).toBeFalsy(); + await actions.delete.enablePhase(); + expect(actions.delete.hasMinAgeInput()).toBeTruthy(); + }); + + describe('wait for snapshot', () => { + test('shows snapshot policy name', () => { + expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ + { + label: DELETE_PHASE_POLICY.policy.phases.delete?.actions.wait_for_snapshot?.policy, + }, + ]); + }); + + test('updates snapshot policy name', async () => { + const { actions } = testBed; + + await actions.setWaitForSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME); + await actions.savePolicy(); + + const expected = { + name: DELETE_PHASE_POLICY.name, + phases: { + ...DELETE_PHASE_POLICY.policy.phases, + delete: { + ...DELETE_PHASE_POLICY.policy.phases.delete, + actions: { + ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, + wait_for_snapshot: { + policy: NEW_SNAPSHOT_POLICY_NAME, + }, + }, + }, + }, + }; + + const latestRequest = server.requests[server.requests.length - 1]; + expect(latestRequest.url).toBe(`${API_BASE_PATH}/policies`); + expect(latestRequest.method).toBe('POST'); + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); + + test('shows a callout when the input is not an existing policy', async () => { + const { actions } = testBed; + + await actions.setWaitForSnapshotPolicy('my_custom_policy'); + expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); + expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); + expect(testBed.find('customPolicyCallout').exists()).toBeTruthy(); + }); + + test('removes the action if field is empty', async () => { + const { actions } = testBed; + + await actions.setWaitForSnapshotPolicy(''); + await actions.savePolicy(); + + const expected = { + name: DELETE_PHASE_POLICY.name, + phases: { + ...DELETE_PHASE_POLICY.policy.phases, + delete: { + ...DELETE_PHASE_POLICY.policy.phases.delete, + actions: { + ...DELETE_PHASE_POLICY.policy.phases.delete?.actions, + }, + }, + }, + }; + + delete expected.phases.delete.actions.wait_for_snapshot; + + const latestRequest = server.requests[server.requests.length - 1]; + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); + + test('shows a callout when there are no snapshot policies', async () => { + // need to call setup on testBed again for it to use a newly defined snapshot policies response + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); + expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy(); + expect(testBed.find('noPoliciesCallout').exists()).toBeTruthy(); + }); + + test('shows a callout when there is an error loading snapshot policies', async () => { + // need to call setup on testBed again for it to use a newly defined snapshot policies response + httpRequestsMockHelpers.setLoadSnapshotPolicies([], { status: 500, body: 'error' }); + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + expect(testBed.find('customPolicyCallout').exists()).toBeFalsy(); + expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy(); + expect(testBed.find('policiesErrorCallout').exists()).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts similarity index 78% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts rename to x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts index b02d190d108997..13e55a1f39e2c7 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts @@ -8,6 +8,11 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../../helpers/setup_environment'; import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { + POLICY_WITH_MIGRATE_OFF, + POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, + POLICY_WITH_NODE_ROLE_ALLOCATION, +} from '../constants'; describe(' node allocation', () => { let testBed: EditPolicyTestBed; @@ -308,7 +313,7 @@ describe(' node allocation', () => { }); describe('on cloud', () => { - describe('with deprecated data role config', () => { + describe('using legacy data role config', () => { test('should hide data tier option on cloud', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: { test: ['123'] }, @@ -319,7 +324,7 @@ describe(' node allocation', () => { await act(async () => { testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); }); - const { actions, component, exists } = testBed; + const { actions, component, exists, find } = testBed; component.update(); await actions.warm.enable(true); @@ -330,30 +335,13 @@ describe(' node allocation', () => { expect(exists('defaultDataAllocationOption')).toBeFalsy(); expect(exists('customDataAllocationOption')).toBeTruthy(); expect(exists('noneDataAllocationOption')).toBeTruthy(); - }); - - test('should ask users to migrate to node roles when on cloud using legacy data role', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: { test: ['123'] }, - // On cloud, if using legacy config there will not be any "data_*" roles set. - nodesByRoles: { data: ['test'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); - }); - const { actions, component, exists } = testBed; - - component.update(); - await actions.warm.enable(true); - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - - expect(exists('cloudDataTierCallout')).toBeTruthy(); + // Show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeTruthy(); }); }); - describe('with node role config', () => { - test('shows data role, custom and "off" options on cloud with data roles', async () => { + describe('using node role config', () => { + test('shows recommended, custom and "off" options on cloud with data roles', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: { test: ['123'] }, nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, @@ -362,7 +350,7 @@ describe(' node allocation', () => { await act(async () => { testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); }); - const { actions, component, exists } = testBed; + const { actions, component, exists, find } = testBed; component.update(); await actions.warm.enable(true); @@ -372,8 +360,10 @@ describe(' node allocation', () => { expect(exists('defaultDataAllocationOption')).toBeTruthy(); expect(exists('customDataAllocationOption')).toBeTruthy(); expect(exists('noneDataAllocationOption')).toBeTruthy(); - // We should not be showing the call-to-action for users to activate data tiers in cloud + // We should not be showing the call-to-action for users to activate data tier in cloud expect(exists('cloudDataTierCallout')).toBeFalsy(); + // Do not show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeFalsy(); }); test('shows cloud notice when cold tier nodes do not exist', async () => { @@ -398,4 +388,102 @@ describe(' node allocation', () => { }); }); }); + + describe('data allocation', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_MIGRATE_OFF]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('setting node_attr based allocation, but not selecting node attribute', async () => { + const { actions } = testBed; + await actions.warm.setDataAllocation('node_attrs'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm; + + expect(warmPhase.actions.migrate).toEqual({ enabled: false }); + }); + + describe('node roles', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ROLE_ALLOCATION]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('detecting use of the recommended allocation type', () => { + const { find } = testBed; + const selectedDataAllocation = find( + 'warm-dataTierAllocationControls.dataTierSelect' + ).text(); + expect(selectedDataAllocation).toBe('Use warm nodes (recommended)'); + }); + + test('setting replicas serialization', async () => { + const { actions } = testBed; + await actions.warm.setReplicas('123'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhaseActions = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm + .actions; + expect(warmPhaseActions).toMatchInlineSnapshot(` + Object { + "allocate": Object { + "number_of_replicas": 123, + }, + } + `); + }); + }); + + describe('node attr and none', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('detecting use of the custom allocation type', () => { + const { find } = testBed; + expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom'); + }); + test('detecting use of the "off" allocation type', () => { + const { find } = testBed; + expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); + }); + }); + }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts new file mode 100644 index 00000000000000..6584c19c85be3a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/request_flyout.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { getDefaultHotPhasePolicy } from '../constants'; + +describe(' request flyout', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('renders a json in flyout for a default policy', async () => { + const { find, component } = testBed; + await act(async () => { + find('requestButton').simulate('click'); + }); + component.update(); + + const json = component.find(`code`).text(); + const expected = `PUT _ilm/policy/my_policy\n${JSON.stringify( + { + policy: { + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '30d', + max_size: '50gb', + }, + }, + }, + }, + }, + }, + null, + 2 + )}`; + expect(json).toBe(expected); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts new file mode 100644 index 00000000000000..e2b67efbf588db --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { getDefaultHotPhasePolicy } from '../constants'; +import { act } from 'react-dom/test-utils'; +import { licensingMock } from '../../../../../licensing/public/mocks'; + +describe(' timeline', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['abc'] }); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup({ + appServicesContext: { + license: licensingMock.createLicense({ license: { type: 'enterprise' } }), + }, + }); + }); + + const { component } = testBed; + component.update(); + }); + + test('shows forcemerge when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); + }); + test('hides forcemerge when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); + }); + + test('shows shrink input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.shrinkExists()).toBeTruthy(); + }); + test('hides shrink input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.shrinkExists()).toBeFalsy(); + }); + test('shows readonly input when rollover enabled', async () => { + const { actions } = testBed; + expect(actions.hot.readonlyExists()).toBeTruthy(); + }); + test('hides readonly input when rollover is disabled', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + expect(actions.hot.readonlyExists()).toBeFalsy(); + }); + test('hides and disables searchable snapshot field', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + await actions.cold.enable(true); + + expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + }); + + test('shows rollover tip on minimum age', async () => { + const { actions } = testBed; + + await actions.warm.enable(true); + await actions.cold.enable(true); + await actions.delete.enablePhase(); + + expect(actions.warm.hasRolloverTipOnMinAge()).toBeTruthy(); + expect(actions.cold.hasRolloverTipOnMinAge()).toBeTruthy(); + expect(actions.delete.hasRolloverTipOnMinAge()).toBeTruthy(); + }); + test('hiding rollover tip on minimum age', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + + await actions.warm.enable(true); + await actions.cold.enable(true); + await actions.delete.enablePhase(); + + expect(actions.warm.hasRolloverTipOnMinAge()).toBeFalsy(); + expect(actions.cold.hasRolloverTipOnMinAge()).toBeFalsy(); + expect(actions.delete.hasRolloverTipOnMinAge()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts new file mode 100644 index 00000000000000..ed678a6b217ae9 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { licensingMock } from '../../../../../licensing/public/mocks'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { getDefaultHotPhasePolicy } from '../constants'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' searchable snapshots', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('enabling searchable snapshot should hide force merge, freeze and shrink in subsequent phases', async () => { + const { actions } = testBed; + + await actions.warm.enable(true); + await actions.cold.enable(true); + + expect(actions.warm.forceMergeFieldExists()).toBeTruthy(); + expect(actions.warm.shrinkExists()).toBeTruthy(); + expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); + expect(actions.cold.freezeExists()).toBeTruthy(); + + await actions.hot.setSearchableSnapshot('my-repo'); + + expect(actions.warm.forceMergeFieldExists()).toBeFalsy(); + expect(actions.warm.shrinkExists()).toBeFalsy(); + // searchable snapshot in cold is still visible + expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); + expect(actions.cold.freezeExists()).toBeFalsy(); + }); + + test('disabling rollover toggle, but enabling default rollover', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + await actions.hot.toggleDefaultRollover(true); + + expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); + expect(actions.hot.shrinkExists()).toBeTruthy(); + expect(actions.hot.searchableSnapshotsExists()).toBeTruthy(); + }); + + describe('on cloud', () => { + describe('new policy', () => { + beforeEach(async () => { + // simulate creating a new policy + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + test('defaults searchable snapshot to true on cloud', async () => { + const { find, actions } = testBed; + await actions.cold.enable(true); + expect( + find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked'] + ).toBe(true); + }); + }); + describe('existing policy', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + test('correctly sets snapshot repository default to "found-snapshots"', async () => { + const { actions } = testBed; + await actions.cold.enable(true); + await actions.cold.toggleSearchableSnapshot(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + 'found-snapshots' + ); + }); + }); + }); + describe('on non-enterprise license', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['my-repo'] }); + + await act(async () => { + testBed = await setup({ + appServicesContext: { + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); + }); + + const { component } = testBed; + component.update(); + }); + test('disable setting searchable snapshots', async () => { + const { actions } = testBed; + + expect(actions.cold.searchableSnapshotsExists()).toBeFalsy(); + expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + + await actions.cold.enable(true); + + // Still hidden in hot + expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + + expect(actions.cold.searchableSnapshotsExists()).toBeTruthy(); + expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts new file mode 100644 index 00000000000000..3618bad45e4f14 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/timeline.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { getDefaultHotPhasePolicy } from '../constants'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' timeline', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('showing all phases on the timeline', async () => { + const { actions } = testBed; + // This is how the default policy should look + expect(actions.timeline.hasHotPhase()).toBe(true); + expect(actions.timeline.hasWarmPhase()).toBe(false); + expect(actions.timeline.hasColdPhase()).toBe(false); + expect(actions.timeline.hasDeletePhase()).toBe(false); + + await actions.warm.enable(true); + expect(actions.timeline.hasHotPhase()).toBe(true); + expect(actions.timeline.hasWarmPhase()).toBe(true); + expect(actions.timeline.hasColdPhase()).toBe(false); + expect(actions.timeline.hasDeletePhase()).toBe(false); + + await actions.cold.enable(true); + expect(actions.timeline.hasHotPhase()).toBe(true); + expect(actions.timeline.hasWarmPhase()).toBe(true); + expect(actions.timeline.hasColdPhase()).toBe(true); + expect(actions.timeline.hasDeletePhase()).toBe(false); + + await actions.delete.enablePhase(); + expect(actions.timeline.hasHotPhase()).toBe(true); + expect(actions.timeline.hasWarmPhase()).toBe(true); + expect(actions.timeline.hasColdPhase()).toBe(true); + expect(actions.timeline.hasDeletePhase()).toBe(true); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts new file mode 100644 index 00000000000000..2252f8d1f5fa8b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/warm_phase.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { getDefaultHotPhasePolicy } from '../constants'; + +describe(' warm phase', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('shows timing only when enabled', async () => { + const { actions } = testBed; + expect(actions.warm.hasMinAgeInput()).toBeFalsy(); + await actions.warm.enable(true); + expect(actions.warm.hasMinAgeInput()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts new file mode 100644 index 00000000000000..e2d937cf9c8dbb --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/form_validation/error_indicators.test.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { getDefaultHotPhasePolicy } from '../constants'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' error indicators', () => { + let testBed: EditPolicyTestBed; + let runTimers: () => void; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + + ({ runTimers } = testBed); + }); + test('shows phase error indicators correctly', async () => { + // This test simulates a user configuring a policy phase by phase. The flow is the following: + // 0. Start with policy with no validation issues present + // 1. Configure hot, introducing a validation error + // 2. Configure warm, introducing a validation error + // 3. Configure cold, introducing a validation error + // 4. Fix validation error in hot + // 5. Fix validation error in warm + // 6. Fix validation error in cold + // We assert against each of these progressive states. + + const { actions } = testBed; + + // 0. No validation issues + expect(actions.hasGlobalErrorCallout()).toBe(false); + expect(actions.hot.hasErrorIndicator()).toBe(false); + expect(actions.warm.hasErrorIndicator()).toBe(false); + expect(actions.cold.hasErrorIndicator()).toBe(false); + + // 1. Hot phase validation issue + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('-22'); + runTimers(); + expect(actions.hasGlobalErrorCallout()).toBe(true); + expect(actions.hot.hasErrorIndicator()).toBe(true); + expect(actions.warm.hasErrorIndicator()).toBe(false); + expect(actions.cold.hasErrorIndicator()).toBe(false); + + // 2. Warm phase validation issue + await actions.warm.enable(true); + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('-22'); + runTimers(); + expect(actions.hasGlobalErrorCallout()).toBe(true); + expect(actions.hot.hasErrorIndicator()).toBe(true); + expect(actions.warm.hasErrorIndicator()).toBe(true); + expect(actions.cold.hasErrorIndicator()).toBe(false); + + // 3. Cold phase validation issue + await actions.cold.enable(true); + await actions.cold.setReplicas('-33'); + runTimers(); + expect(actions.hasGlobalErrorCallout()).toBe(true); + expect(actions.hot.hasErrorIndicator()).toBe(true); + expect(actions.warm.hasErrorIndicator()).toBe(true); + expect(actions.cold.hasErrorIndicator()).toBe(true); + + // 4. Fix validation issue in hot + await actions.hot.setForcemergeSegmentsCount('1'); + runTimers(); + expect(actions.hasGlobalErrorCallout()).toBe(true); + expect(actions.hot.hasErrorIndicator()).toBe(false); + expect(actions.warm.hasErrorIndicator()).toBe(true); + expect(actions.cold.hasErrorIndicator()).toBe(true); + + // 5. Fix validation issue in warm + await actions.warm.setForcemergeSegmentsCount('1'); + runTimers(); + expect(actions.hasGlobalErrorCallout()).toBe(true); + expect(actions.hot.hasErrorIndicator()).toBe(false); + expect(actions.warm.hasErrorIndicator()).toBe(false); + expect(actions.cold.hasErrorIndicator()).toBe(true); + + // 6. Fix validation issue in cold + await actions.cold.setReplicas('1'); + runTimers(); + expect(actions.hasGlobalErrorCallout()).toBe(false); + expect(actions.hot.hasErrorIndicator()).toBe(false); + expect(actions.warm.hasErrorIndicator()).toBe(false); + expect(actions.cold.hasErrorIndicator()).toBe(false); + }); + + test('global error callout should show if there are any form errors', async () => { + const { actions } = testBed; + + expect(actions.hasGlobalErrorCallout()).toBe(false); + expect(actions.hot.hasErrorIndicator()).toBe(false); + expect(actions.warm.hasErrorIndicator()).toBe(false); + expect(actions.cold.hasErrorIndicator()).toBe(false); + + await actions.saveAsNewPolicy(true); + await actions.setPolicyName(''); + runTimers(); + + expect(actions.hasGlobalErrorCallout()).toBe(true); + expect(actions.hot.hasErrorIndicator()).toBe(false); + expect(actions.warm.hasErrorIndicator()).toBe(false); + expect(actions.cold.hasErrorIndicator()).toBe(false); + }); + + test('clears all error indicators if last erroring field is unmounted', async () => { + const { actions } = testBed; + + await actions.cold.enable(true); + // introduce validation error + await actions.cold.setSearchableSnapshot(''); + runTimers(); + + await actions.savePolicy(); + runTimers(); + + expect(actions.hasGlobalErrorCallout()).toBe(true); + expect(actions.hot.hasErrorIndicator()).toBe(false); + expect(actions.warm.hasErrorIndicator()).toBe(false); + expect(actions.cold.hasErrorIndicator()).toBe(true); + + // unmount the field + await actions.cold.toggleSearchableSnapshot(false); + + expect(actions.hasGlobalErrorCallout()).toBe(false); + expect(actions.hot.hasErrorIndicator()).toBe(false); + expect(actions.warm.hasErrorIndicator()).toBe(false); + expect(actions.cold.hasErrorIndicator()).toBe(false); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts deleted file mode 100644 index 9c23780f1d0213..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/reactive_form.test.ts +++ /dev/null @@ -1,143 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from 'react-dom/test-utils'; -import { setupEnvironment } from '../../helpers/setup_environment'; -import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; -import { DEFAULT_POLICY } from '../constants'; - -describe(' reactive form', () => { - let testBed: EditPolicyTestBed; - const { server, httpRequestsMockHelpers } = setupEnvironment(); - - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - server.restore(); - }); - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); - httpRequestsMockHelpers.setListNodes({ - nodesByRoles: { data: ['node1'] }, - nodesByAttributes: { 'attribute:true': ['node1'] }, - isUsingDeprecatedDataRoleConfig: true, - }); - httpRequestsMockHelpers.setNodesDetails('attribute:true', [ - { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, - ]); - httpRequestsMockHelpers.setLoadSnapshotPolicies([]); - - await act(async () => { - testBed = await setup(); - }); - - const { component } = testBed; - component.update(); - }); - - describe('rollover', () => { - test('shows forcemerge when rollover enabled', async () => { - const { actions } = testBed; - expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); - }); - test('hides forcemerge when rollover is disabled', async () => { - const { actions } = testBed; - await actions.hot.toggleDefaultRollover(false); - await actions.hot.toggleRollover(false); - expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); - }); - - test('shows shrink input when rollover enabled', async () => { - const { actions } = testBed; - expect(actions.hot.shrinkExists()).toBeTruthy(); - }); - test('hides shrink input when rollover is disabled', async () => { - const { actions } = testBed; - await actions.hot.toggleDefaultRollover(false); - await actions.hot.toggleRollover(false); - expect(actions.hot.shrinkExists()).toBeFalsy(); - }); - test('shows readonly input when rollover enabled', async () => { - const { actions } = testBed; - expect(actions.hot.readonlyExists()).toBeTruthy(); - }); - test('hides readonly input when rollover is disabled', async () => { - const { actions } = testBed; - await actions.hot.toggleDefaultRollover(false); - await actions.hot.toggleRollover(false); - expect(actions.hot.readonlyExists()).toBeFalsy(); - }); - }); - - describe('timing', () => { - test('warm phase shows timing only when enabled', async () => { - const { actions } = testBed; - expect(actions.warm.hasMinAgeInput()).toBeFalsy(); - await actions.warm.enable(true); - expect(actions.warm.hasMinAgeInput()).toBeTruthy(); - }); - - test('cold phase shows timing only when enabled', async () => { - const { actions } = testBed; - expect(actions.cold.hasMinAgeInput()).toBeFalsy(); - await actions.cold.enable(true); - expect(actions.cold.hasMinAgeInput()).toBeTruthy(); - }); - - test('delete phase shows timing after it was enabled', async () => { - const { actions } = testBed; - expect(actions.delete.hasMinAgeInput()).toBeFalsy(); - await actions.delete.enablePhase(); - expect(actions.delete.hasMinAgeInput()).toBeTruthy(); - }); - }); - - describe('delete phase', () => { - test('is hidden when disabled', async () => { - const { actions } = testBed; - expect(actions.delete.isShown()).toBeFalsy(); - await actions.delete.enablePhase(); - expect(actions.delete.isShown()).toBeTruthy(); - }); - }); - - describe('json in flyout', () => { - test('renders a json in flyout for a default policy', async () => { - const { find, component } = testBed; - await act(async () => { - find('requestButton').simulate('click'); - }); - component.update(); - - const json = component.find(`code`).text(); - const expected = `PUT _ilm/policy/my_policy\n${JSON.stringify( - { - policy: { - phases: { - hot: { - min_age: '0ms', - actions: { - rollover: { - max_age: '30d', - max_size: '50gb', - }, - }, - }, - }, - }, - }, - null, - 2 - )}`; - expect(json).toBe(expected); - }); - }); -}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts new file mode 100644 index 00000000000000..61ceab1990c724 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/serialization/policy_serialization.test.ts @@ -0,0 +1,426 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { + getDefaultHotPhasePolicy, + POLICY_WITH_INCLUDE_EXCLUDE, + POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, +} from '../constants'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; + +describe(' serialization', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + describe('top level form', () => { + /** + * We assume that policies that populate this form are loaded directly from ES and so + * are valid according to ES. There may be settings in the policy created through the ILM + * API that the UI does not cater for, like the unfollow action. We do not want to overwrite + * the configuration for these actions in the UI. + */ + it('preserves policy settings it did not configure', async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS]); + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + + // Set max docs to test whether we keep the unknown fields in that object after serializing + await actions.hot.setMaxDocs('1000'); + // Remove the delete phase to ensure that we also correctly remove data + await actions.delete.disablePhase(); + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toEqual({ + foo: 'bar', // Made up value + name: 'my_policy', + phases: { + hot: { + actions: { + rollover: { + max_docs: 1000, + max_size: '50gb', + unknown_setting: 123, // Made up setting that should stay preserved + }, + }, + min_age: '0ms', + }, + warm: { + actions: { + my_unfollow_action: {}, // Made up action + set_priority: { + priority: 22, + unknown_setting: true, + }, + }, + min_age: '0d', + }, + }, + }); + }); + }); + + describe('hot phase', () => { + test('setting all values', async () => { + const { actions } = testBed; + + await actions.hot.toggleDefaultRollover(false); + await actions.hot.setMaxSize('123', 'mb'); + await actions.hot.setMaxDocs('123'); + await actions.hot.setMaxAge('123', 'h'); + await actions.hot.toggleForceMerge(true); + await actions.hot.setForcemergeSegmentsCount('123'); + await actions.hot.setBestCompression(true); + await actions.hot.toggleShrink(true); + await actions.hot.setShrink('2'); + await actions.hot.toggleReadonly(true); + await actions.hot.toggleIndexPriority(true); + await actions.hot.setIndexPriority('123'); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy).toMatchInlineSnapshot(` + Object { + "name": "my_policy", + "phases": Object { + "hot": Object { + "actions": Object { + "forcemerge": Object { + "index_codec": "best_compression", + "max_num_segments": 123, + }, + "readonly": Object {}, + "rollover": Object { + "max_age": "123h", + "max_docs": 123, + "max_size": "123mb", + }, + "set_priority": Object { + "priority": 123, + }, + "shrink": Object { + "number_of_shards": 2, + }, + }, + "min_age": "0ms", + }, + }, + } + `); + }); + + test('setting searchable snapshot', async () => { + const { actions } = testBed; + + await actions.hot.setSearchableSnapshot('my-repo'); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.hot.actions.searchable_snapshot.snapshot_repository).toBe( + 'my-repo' + ); + }); + + test('disabling rollover', async () => { + const { actions } = testBed; + await actions.hot.toggleDefaultRollover(false); + await actions.hot.toggleRollover(false); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const policy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + const hotActions = policy.phases.hot.actions; + const rolloverAction = hotActions.rollover; + expect(rolloverAction).toBe(undefined); + expect(hotActions).toMatchInlineSnapshot(`Object {}`); + }); + }); + + describe('warm phase', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('default values', async () => { + const { actions } = testBed; + await actions.warm.enable(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhase = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm; + expect(warmPhase).toMatchInlineSnapshot(` + Object { + "actions": Object { + "set_priority": Object { + "priority": 50, + }, + }, + "min_age": "0d", + } + `); + }); + + test('setting all values', async () => { + const { actions } = testBed; + await actions.warm.enable(true); + await actions.warm.setDataAllocation('node_attrs'); + await actions.warm.setSelectedNodeAttribute('test:123'); + await actions.warm.setReplicas('123'); + await actions.warm.toggleShrink(true); + await actions.warm.setShrink('123'); + await actions.warm.toggleForceMerge(true); + await actions.warm.setForcemergeSegmentsCount('123'); + await actions.warm.setBestCompression(true); + await actions.warm.toggleReadonly(true); + await actions.warm.setIndexPriority('123'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + // Check shape of entire policy + expect(entirePolicy).toMatchInlineSnapshot(` + Object { + "name": "my_policy", + "phases": Object { + "hot": Object { + "actions": Object { + "rollover": Object { + "max_age": "30d", + "max_size": "50gb", + }, + }, + "min_age": "0ms", + }, + "warm": Object { + "actions": Object { + "allocate": Object { + "number_of_replicas": 123, + "require": Object { + "test": "123", + }, + }, + "forcemerge": Object { + "index_codec": "best_compression", + "max_num_segments": 123, + }, + "readonly": Object {}, + "set_priority": Object { + "priority": 123, + }, + "shrink": Object { + "number_of_shards": 123, + }, + }, + "min_age": "0d", + }, + }, + } + `); + }); + + describe('policy with include and exclude', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_INCLUDE_EXCLUDE]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('preserves include, exclude allocation settings', async () => { + const { actions } = testBed; + await actions.warm.setDataAllocation('node_attrs'); + await actions.warm.setSelectedNodeAttribute('test:123'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const warmPhaseAllocate = JSON.parse(JSON.parse(latestRequest.requestBody).body).phases.warm + .actions.allocate; + expect(warmPhaseAllocate).toMatchInlineSnapshot(` + Object { + "exclude": Object { + "def": "456", + }, + "include": Object { + "abc": "123", + }, + "require": Object { + "test": "123", + }, + } + `); + }); + }); + }); + + describe('cold phase', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('default values', async () => { + const { actions } = testBed; + + await actions.cold.enable(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.cold).toMatchInlineSnapshot(` + Object { + "actions": Object { + "set_priority": Object { + "priority": 0, + }, + }, + "min_age": "0d", + } + `); + }); + + test('setting all values, excluding searchable snapshot', async () => { + const { actions } = testBed; + + await actions.cold.enable(true); + await actions.cold.setMinAgeValue('123'); + await actions.cold.setMinAgeUnits('s'); + await actions.cold.setDataAllocation('node_attrs'); + await actions.cold.setSelectedNodeAttribute('test:123'); + await actions.cold.setReplicas('123'); + await actions.cold.setFreeze(true); + await actions.cold.setIndexPriority('123'); + + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toMatchInlineSnapshot(` + Object { + "name": "my_policy", + "phases": Object { + "cold": Object { + "actions": Object { + "allocate": Object { + "number_of_replicas": 123, + "require": Object { + "test": "123", + }, + }, + "freeze": Object {}, + "set_priority": Object { + "priority": 123, + }, + }, + "min_age": "123s", + }, + "hot": Object { + "actions": Object { + "rollover": Object { + "max_age": "30d", + "max_size": "50gb", + }, + }, + "min_age": "0ms", + }, + }, + } + `); + }); + + // Setting searchable snapshot field disables setting replicas so we test this separately + test('setting searchable snapshot', async () => { + const { actions } = testBed; + await actions.cold.enable(true); + await actions.cold.setSearchableSnapshot('my-repo'); + await actions.savePolicy(); + const latestRequest2 = server.requests[server.requests.length - 1]; + const entirePolicy2 = JSON.parse(JSON.parse(latestRequest2.requestBody).body); + expect(entirePolicy2.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + 'my-repo' + ); + }); + }); + + test('delete phase', async () => { + const { actions } = testBed; + await actions.delete.enablePhase(); + await actions.setWaitForSnapshotPolicy('test'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.delete).toEqual({ + min_age: '365d', + actions: { + delete: {}, + wait_for_snapshot: { + policy: 'test', + }, + }, + }); + }); +}); From 395f0f50009ce087f310307dbf80e9e5cd3d4bef Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Mar 2021 12:08:53 -0500 Subject: [PATCH 10/63] Improve consistency for display of management items (#92694) (#93238) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Larry Gregory --- .../beats_management/public/bootstrap.tsx | 11 +++++++---- .../cross_cluster_replication/public/plugin.ts | 16 ++++++++++------ x-pack/plugins/watcher/public/plugin.ts | 16 ++++++++++++---- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/beats_management/public/bootstrap.tsx b/x-pack/plugins/beats_management/public/bootstrap.tsx index 4a4d3a893286ba..b5bdd39fa0817e 100644 --- a/x-pack/plugins/beats_management/public/bootstrap.tsx +++ b/x-pack/plugins/beats_management/public/bootstrap.tsx @@ -18,10 +18,13 @@ import { BeatsManagementConfigType } from '../common'; import { MANAGEMENT_SECTION } from '../common/constants'; async function startApp(libs: FrontendLibs, core: CoreSetup) { - const [startServices] = await Promise.all([ - core.getStartServices(), - libs.framework.waitUntilFrameworkReady(), - ]); + const startServices = await core.getStartServices(); + + if (startServices[0].http.anonymousPaths.isAnonymous(window.location.pathname)) { + return; + } + // Can't run until the `start` lifecycle, so we wait for start services to resolve above before calling this. + await libs.framework.waitUntilFrameworkReady(); const capabilities = startServices[0].application.capabilities; const hasBeatsCapability = capabilities.management.ingest?.[MANAGEMENT_SECTION] ?? false; diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts index 24c9d8dae379d1..7998cdbdf750b9 100644 --- a/x-pack/plugins/cross_cluster_replication/public/plugin.ts +++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts @@ -73,18 +73,21 @@ export class CrossClusterReplicationPlugin implements Plugin { // NOTE: We enable the plugin by default instead of disabling it by default because this // creates a race condition that causes functional tests to fail on CI (see #66781). - licensing.license$ - .pipe(first()) - .toPromise() - .then((license) => { + Promise.all([licensing.license$.pipe(first()).toPromise(), getStartServices()]).then( + ([license, startServices]) => { const licenseStatus = license.check(PLUGIN.ID, PLUGIN.minimumLicenseType); const isLicenseOk = licenseStatus.state === 'valid'; const config = this.initializerContext.config.get(); + const capabilities = startServices[0].application.capabilities; + // remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting. // The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if // the Remote Clusters UI is disabled we can't show the CCR UI. - const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled; + const isCcrUiEnabled = + capabilities.management.data?.[MANAGEMENT_ID] && + config.ui.enabled && + remoteClusters.isUiEnabled; if (isLicenseOk && isCcrUiEnabled) { if (indexManagement) { @@ -106,7 +109,8 @@ export class CrossClusterReplicationPlugin implements Plugin { } else { ccrApp.disable(); } - }); + } + ); } public start() {} diff --git a/x-pack/plugins/watcher/public/plugin.ts b/x-pack/plugins/watcher/public/plugin.ts index 9cc0b1bbe99a80..6c6d6f1169658e 100644 --- a/x-pack/plugins/watcher/public/plugin.ts +++ b/x-pack/plugins/watcher/public/plugin.ts @@ -6,9 +6,10 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, Plugin, CoreStart } from 'kibana/public'; +import { CoreSetup, Plugin, CoreStart, Capabilities } from 'kibana/public'; import { first, map, skip } from 'rxjs/operators'; +import { Subject, combineLatest } from 'rxjs'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { LicenseStatus } from '../common/types/license_status'; @@ -26,6 +27,8 @@ const licenseToLicenseStatus = (license: ILicense): LicenseStatus => { }; export class WatcherUIPlugin implements Plugin { + private capabilities$: Subject = new Subject(); + setup( { notifications, http, uiSettings, getStartServices }: CoreSetup, { licensing, management, data, home, charts }: Dependencies @@ -99,13 +102,16 @@ export class WatcherUIPlugin implements Plugin { home.featureCatalogue.register(watcherHome); - licensing.license$.pipe(first(), map(licenseToLicenseStatus)).subscribe(({ valid }) => { + combineLatest([ + licensing.license$.pipe(first(), map(licenseToLicenseStatus)), + this.capabilities$, + ]).subscribe(([{ valid }, capabilities]) => { // NOTE: We enable the plugin by default instead of disabling it by default because this // creates a race condition that can cause the app nav item to not render in the side nav. // The race condition still exists, but it will result in the item rendering when it shouldn't // (e.g. on a license it's not available for), instead of *not* rendering when it *should*, // which is a less frustrating UX. - if (valid) { + if (valid && capabilities.management.insightsAndAlerting?.watcher === true) { watcherESApp.enable(); } else { watcherESApp.disable(); @@ -113,7 +119,9 @@ export class WatcherUIPlugin implements Plugin { }); } - start(core: CoreStart) {} + start(core: CoreStart) { + this.capabilities$.next(core.application.capabilities); + } stop() {} } From dff54d7ed9670ca2796f53c58cc4261d42329992 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 2 Mar 2021 18:16:37 +0100 Subject: [PATCH 11/63] [ILM] Allow multiple searchable snapshot actions (#92789) (#93242) * remove logic that disables SS action in cold if no rollover and always show replicas field * update test coverage to be consistent with new form behaviour and expand hot phase without rollover test * only licensing can disable searchable snapshot field * clean up i18n * remove ss field callout * update error reporting logic to include causes chain, also update UI to show causes * updated searchable snapshot field in hot phase callout Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../errors/es_error_parser.ts | 6 ++--- .../errors/handle_es_error.ts | 11 +++++++- .../searchable_snapshot_field.tsx | 27 ++++--------------- .../public/application/services/api_errors.ts | 5 +++- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 6 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts index 61a8882317f9f5..fc400e4a87b3af 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/es_error_parser.ts @@ -11,14 +11,14 @@ interface ParsedError { cause: string[]; } -const getCause = (obj: any = {}, causes: string[] = []): string[] => { +export const getEsCause = (obj: any = {}, causes: string[] = []): string[] => { const updated = [...causes]; if (obj.caused_by) { updated.push(obj.caused_by.reason); // Recursively find all the "caused by" reasons - return getCause(obj.caused_by, updated); + return getEsCause(obj.caused_by, updated); } return updated.filter(Boolean); @@ -27,7 +27,7 @@ const getCause = (obj: any = {}, causes: string[] = []): string[] => { export const parseEsError = (err: string): ParsedError => { try { const { error } = JSON.parse(err); - const cause = getCause(error); + const cause = getEsCause(error); return { message: error.reason, cause, diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts index b645b62c863d5d..123db891bf41de 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts @@ -9,6 +9,7 @@ import { ApiError } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { IKibanaResponse, KibanaResponseFactory } from 'kibana/server'; +import { getEsCause } from './es_error_parser'; interface EsErrorHandlerParams { error: ApiError; @@ -34,7 +35,15 @@ export const handleEsError = ({ const { statusCode, body } = error as ResponseError; return response.customError({ statusCode, - body: { message: body.error?.reason }, + body: { + message: body.error?.reason, + attributes: { + // The full original ES error object + error: body.error, + // We assume that this is an ES error object with a nested caused by chain if we can see the "caused_by" field at the top-level + causes: body.error?.caused_by ? getEsCause(body.error) : undefined, + }, + }, }); } // Case: default diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 1a78149521e63c..3fc7064575555e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -61,9 +61,6 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => const isColdPhase = phase === 'cold'; const isDisabledDueToLicense = !license.canUseSearchableSnapshot(); - const isDisabledInColdDueToHotPhase = isColdPhase && isUsingSearchableSnapshotInHotPhase; - - const isDisabled = isDisabledDueToLicense || isDisabledInColdDueToHotPhase; const [isFieldToggleChecked, setIsFieldToggleChecked] = useState(() => Boolean( @@ -74,10 +71,10 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => ); useEffect(() => { - if (isDisabled) { + if (isDisabledDueToLicense) { setIsFieldToggleChecked(false); } - }, [isDisabled]); + }, [isDisabledDueToLicense]); const renderField = () => ( @@ -254,7 +251,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody', { defaultMessage: - 'Force merge, shrink, freeze and cold phase searchable snapshots are not allowed when searchable snapshots are enabled in the hot phase.', + 'Force merge, shrink and freeze actions are not allowed when searchable snapshots are enabled in this phase.', } )} data-test-subj="searchableSnapshotFieldsDisabledCallout" @@ -278,20 +275,6 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => )} ); - } else if (isDisabledInColdDueToHotPhase) { - infoCallout = ( - - ); } return infoCallout ? ( @@ -308,7 +291,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => data-test-subj={`searchableSnapshotField-${phase}`} switchProps={{ checked: isFieldToggleChecked, - disabled: isDisabled, + disabled: isDisabledDueToLicense, onChange: setIsFieldToggleChecked, 'data-test-subj': 'searchableSnapshotToggle', label: i18n.translate( @@ -339,7 +322,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => fieldNotices={renderInfoCallout()} fullWidth > - {isDisabled ?
: renderField} + {isDisabledDueToLicense ?
: renderField} ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.ts index 54ef91457b1f9a..fc37b62e30eb25 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api_errors.ts @@ -11,7 +11,10 @@ import { fatalErrors, toasts } from './notification'; function createToastConfig(error: IHttpFetchError, errorTitle: string) { if (error && error.body) { // Error body shape is defined by the API. - const { error: errorString, statusCode, message } = error.body; + const { error: errorString, statusCode, message: errorMessage, attributes } = error.body; + const message = attributes?.causes?.length + ? attributes.causes[attributes.causes.length - 1] + : errorMessage; return { title: errorTitle, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7698d11b10c223..472ecc3f801644 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9505,8 +9505,6 @@ "xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage": "新規ポリシーとして保存します", "xpack.indexLifecycleMgmt.editPolicy.saveButton": "ポリシーを保存", "xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "ライフサイクルポリシー {lifecycleName} の保存中にエラーが発生しました", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "検索可能なスナップショットがホットフェーズで有効な場合には、強制、マージ、縮小、凍結、コールドフェーズの検索可能なスナップショットは許可されません。", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotDisabledCalloutBody": "ホットフェーズで構成されているときには、コールドフェーズで検索可能なスナップショットを作成できません。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "選択したリポジトリで管理されたインデックスのスナップショットを作成し、検索可能なスナップショットとしてマウントします。{learnMoreLink}。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "検索可能なスナップショットリポジドリ", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "検索可能スナップショット", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 208c0a2d4e7e56..a72c83b736a6d0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9529,8 +9529,6 @@ "xpack.indexLifecycleMgmt.editPolicy.saveAsNewPolicyMessage": "另存为新策略", "xpack.indexLifecycleMgmt.editPolicy.saveButton": "保存策略", "xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "保存生命周期策略 {lifecycleName} 时出错", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "在热阶段启用可搜索快照时,不允许强制合并、缩小、冻结可搜索快照以及将其置入冷阶段。", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotDisabledCalloutBody": "无法在冷阶段创建在热阶段配置的可搜索快照。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "在所选存储库中拍取受管索引的快照,并将其安装为可搜索快照。{learnMoreLink}。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "可搜索快照存储库", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "可搜索快照", From 18c807b55d58dee9ab5e4286214e82a5ca1371ff Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 2 Mar 2021 13:11:20 -0500 Subject: [PATCH 12/63] [Time to Visualize] Disable Visualize URL Tracker When Linked to OriginatingApp (#92917) (#93252) * changed url tracker for visualize to not save when linked to originating app --- src/plugins/visualize/public/plugin.ts | 5 +---- .../dashboard/create_and_add_embeddables.ts | 4 ++-- .../apps/dashboard/edit_visualizations.js | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 300afd69c84ccd..4b369e8be86eee 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -116,10 +116,7 @@ export class VisualizePlugin ], getHistory: () => this.currentHistory!, onBeforeNavLinkSaved: (urlToSave: string) => { - if ( - !urlToSave.includes(`${VisualizeConstants.EDIT_PATH}/`) && - this.isLinkedToOriginatingApp?.() - ) { + if (this.isLinkedToOriginatingApp?.()) { return core.http.basePath.prepend(VisualizeConstants.VISUALIZE_BASE_PATH); } return urlToSave; diff --git a/test/functional/apps/dashboard/create_and_add_embeddables.ts b/test/functional/apps/dashboard/create_and_add_embeddables.ts index c26b375a37b8f5..f4ee4e99047686 100644 --- a/test/functional/apps/dashboard/create_and_add_embeddables.ts +++ b/test/functional/apps/dashboard/create_and_add_embeddables.ts @@ -69,10 +69,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); }); - it('saves the saved visualization url to the app link', async () => { + it('saves the listing page instead of the visualization to the app link', async () => { await PageObjects.header.clickVisualize(true); const currentUrl = await browser.getCurrentUrl(); - expect(currentUrl).to.contain(VisualizeConstants.EDIT_PATH); + expect(currentUrl).not.to.contain(VisualizeConstants.EDIT_PATH); }); after(async () => { diff --git a/test/functional/apps/dashboard/edit_visualizations.js b/test/functional/apps/dashboard/edit_visualizations.js index a918c017bd88f8..d5df97881a1d3b 100644 --- a/test/functional/apps/dashboard/edit_visualizations.js +++ b/test/functional/apps/dashboard/edit_visualizations.js @@ -109,6 +109,24 @@ export default function ({ getService, getPageObjects }) { expect(await testSubjects.exists('visualizationLandingPage')).to.be(true); }); + it('visualize app menu navigates to the visualize listing page if the last opened visualization was linked to dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + + // Create markdown by reference. + await createMarkdownVis('by reference'); + + // Edit then save and return + await editMarkdownVis(); + await PageObjects.visualize.saveVisualizationAndReturn(); + + await PageObjects.header.waitUntilLoadingHasFinished(); + await appsMenu.clickLink('Visualize Library'); + await PageObjects.common.clickConfirmOnModal(); + expect(await testSubjects.exists('visualizationLandingPage')).to.be(true); + }); + describe('by value', () => { it('save and return button returns to dashboard after editing visualization with changes saved', async () => { await PageObjects.common.navigateToApp('dashboard'); From cfbd64ff4f9edb34c81d11114a17b85ecbf1b115 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Tue, 2 Mar 2021 13:14:22 -0500 Subject: [PATCH 13/63] [Security Solution] - Bug fixes (#92294) (#93209) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../overview_description_list/index.tsx | 4 +- .../network/components/details/index.tsx | 6 +- .../host_overview/endpoint_overview/index.tsx | 134 ++-- .../components/host_overview/index.tsx | 12 +- .../view/resolver_no_process_events.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 36 +- .../event_details/expandable_event.tsx | 2 +- .../expandable_host.test.tsx.snap | 669 ++++++++++++++++++ .../host_details/expandable_host.test.tsx | 78 ++ .../host_details/expandable_host.tsx | 25 +- .../side_panel/host_details/index.tsx | 25 +- .../timelines/components/side_panel/index.tsx | 11 +- .../side_panel/network_details/index.tsx | 22 +- 13 files changed, 888 insertions(+), 138 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/__snapshots__/expandable_host.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/overview_description_list/index.tsx b/x-pack/plugins/security_solution/public/common/components/overview_description_list/index.tsx index 570ac4e9577b7e..606991bc089104 100644 --- a/x-pack/plugins/security_solution/public/common/components/overview_description_list/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/overview_description_list/index.tsx @@ -14,13 +14,11 @@ import { DescriptionListStyled } from '../../../common/components/page'; export const OverviewDescriptionList = ({ dataTestSubj, descriptionList, - isInDetailsSidePanel = false, }: { dataTestSubj?: string; descriptionList: DescriptionList[]; - isInDetailsSidePanel: boolean; }) => ( - + ); diff --git a/x-pack/plugins/security_solution/public/network/components/details/index.tsx b/x-pack/plugins/security_solution/public/network/components/details/index.tsx index e263d49e22fc01..851197a78520bc 100644 --- a/x-pack/plugins/security_solution/public/network/components/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/details/index.tsx @@ -144,11 +144,7 @@ export const IpOverview = React.memo( )} {descriptionLists.map((descriptionList, index) => ( - + ))} {loading && ( diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx index 0d8e763b649bf6..4caf854278cc2e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.tsx @@ -20,79 +20,73 @@ import * as i18n from './translations'; interface Props { contextID?: string; data: EndpointFields | null; - isInDetailsSidePanel?: boolean; } -export const EndpointOverview = React.memo( - ({ contextID, data, isInDetailsSidePanel = false }) => { - const getDefaultRenderer = useCallback( - (fieldName: string, fieldData: EndpointFields, attrName: string) => ( - - ), - [contextID] - ); - const descriptionLists: Readonly = useMemo( - () => [ - [ - { - title: i18n.ENDPOINT_POLICY, - description: - data != null && data.endpointPolicy != null - ? data.endpointPolicy - : getEmptyTagValue(), - }, - ], - [ - { - title: i18n.POLICY_STATUS, - description: - data != null && data.policyStatus != null ? ( - - {data.policyStatus} - - ) : ( - getEmptyTagValue() - ), - }, - ], - [ - { - title: i18n.SENSORVERSION, - description: - data != null && data.sensorVersion != null - ? getDefaultRenderer('sensorVersion', data, 'agent.version') - : getEmptyTagValue(), - }, - ], - [], // needs 4 columns for design +export const EndpointOverview = React.memo(({ contextID, data }) => { + const getDefaultRenderer = useCallback( + (fieldName: string, fieldData: EndpointFields, attrName: string) => ( + + ), + [contextID] + ); + const descriptionLists: Readonly = useMemo( + () => [ + [ + { + title: i18n.ENDPOINT_POLICY, + description: + data != null && data.endpointPolicy != null ? data.endpointPolicy : getEmptyTagValue(), + }, + ], + [ + { + title: i18n.POLICY_STATUS, + description: + data != null && data.policyStatus != null ? ( + + {data.policyStatus} + + ) : ( + getEmptyTagValue() + ), + }, ], - [data, getDefaultRenderer] - ); + [ + { + title: i18n.SENSORVERSION, + description: + data != null && data.sensorVersion != null + ? getDefaultRenderer('sensorVersion', data, 'agent.version') + : getEmptyTagValue(), + }, + ], + [], // needs 4 columns for design + ], + [data, getDefaultRenderer] + ); - return ( - <> - {descriptionLists.map((descriptionList, index) => ( - - ))} - - ); - } -); + return ( + <> + {descriptionLists.map((descriptionList, index) => ( + + ))} + + ); +}); EndpointOverview.displayName = 'EndpointOverview'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index de0d782b3ceb72..c5d51a9466235f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -207,11 +207,7 @@ export const HostOverview = React.memo( )} {descriptionLists.map((descriptionList, index) => ( - + ))} {loading && ( @@ -229,11 +225,7 @@ export const HostOverview = React.memo( <> - + {loading && ( ( - {"event.category: 'process'"} + {'event.category: "process"'} ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index aece377ee4f2dc..a55710f3461413 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -24,9 +24,9 @@ exports[`Details Panel Component DetailsPanel: rendering it should not render th exports[`Details Panel Component DetailsPanel:EventDetails: rendering it should render the Details Panel when the panelView is set and the associated params are set 1`] = ` .c0 { - -webkit-flex: 0; - -ms-flex: 0; - flex: 0; + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; }
, .c1 { - -webkit-flex: 0; - -ms-flex: 0; - flex: 0; + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; } .c2 .euiFlyoutBody__overflow { @@ -541,7 +541,7 @@ Array [ className="c0" data-test-subj="timeline:details-panel:flyout" onClose={[Function]} - size="s" + size="m" >
, .c1 { - -webkit-flex: 0; - -ms-flex: 0; - flex: 0; + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; } .c2 .euiFlyoutBody__overflow { @@ -804,7 +804,7 @@ Array [ }
+ + + + + + +`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx new file mode 100644 index 00000000000000..2ce7090a5b83af --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import '../../../../common/mock/match_media'; +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, + kibanaObservable, + createSecuritySolutionStorageMock, +} from '../../../../common/mock'; +import { createStore, State } from '../../../../common/store'; +import { ExpandableHostDetails } from './expandable_host'; + +jest.mock('react-apollo', () => { + const original = jest.requireActual('react-apollo'); + return { + ...original, + // eslint-disable-next-line react/display-name + Query: () => <>, + }; +}); + +describe('Expandable Host Component', () => { + const state: State = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + configIndexPatterns: ['IShouldBeUsed'], + }, + }; + + const { storage } = createSecuritySolutionStorageMock(); + const store = createStore( + state, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage + ); + + const mockProps = { + contextID: 'text-context', + hostName: 'testHostName', + }; + + describe('ExpandableHostDetails: rendering', () => { + test('it should render the HostOverview of the ExpandableHostDetails', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('ExpandableHostDetails')).toMatchSnapshot(); + }); + + test('it should render the HostOverview of the ExpandableHostDetails with the correct indices', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('HostOverviewByNameComponentQuery').prop('indexNames')).toStrictEqual([ + 'IShouldBeUsed', + ]); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx index 8fce9a186bbd46..78367d17d7b629 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/host_details/expandable_host.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiTitle } from '@elastic/eui'; +import { sourcererSelectors } from '../../../../common/store/sourcerer'; import { HostDetailsLink } from '../../../../common/components/links'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; @@ -19,6 +20,7 @@ import { AnomalyTableProvider } from '../../../../common/components/ml/anomaly/a import { hostToCriteria } from '../../../../common/components/ml/criteria/host_to_criteria'; import { scoreIntervalToDateTime } from '../../../../common/components/ml/score/score_interval_to_datetime'; import { HostOverviewByNameQuery } from '../../../../hosts/containers/hosts/details'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; interface ExpandableHostProps { hostName: string; @@ -54,10 +56,25 @@ export const ExpandableHostDetails = ({ hostName, }: ExpandableHostProps & { contextID: string }) => { const { to, from, isInitializing } = useGlobalTime(); - const { docValueFields, selectedPatterns } = useSourcererScope(); + const { docValueFields } = useSourcererScope(); + /* + Normally `selectedPatterns` from useSourcerScope would be where we obtain the indices, + but those indices are only loaded when viewing the pages where the sourcerer is initialized (i.e. Hosts and Overview) + When a user goes directly to the detections page, the patterns have not been loaded yet + as that information isn't used for the detections page. With this details component being accessible + from the detections page, the decision was made to get all existing index names to account for this. + Otherwise, an empty array is defaulted for the `indexNames` in the query which leads to inconsistencies in the data returned + (i.e. extraneous endpoint data is retrieved from the backend leading to endpoint data not being returned) + */ + const allExistingIndexNamesSelector = useMemo( + () => sourcererSelectors.getAllExistingIndexNamesSelector(), + [] + ); + const allPatterns = useDeepEqualSelector(allExistingIndexNamesSelector); + return ( `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 64px`}; + margin-bottom: ${({ theme }) => `${theme.eui.paddingSizes.l}`}; + padding: ${({ theme }) => `${theme.eui.paddingSizes.xs} ${theme.eui.paddingSizes.m} 0px`}; } } `; const StyledEuiFlexGroup = styled(EuiFlexGroup)` - flex: 0; -`; - -const StyledEuiFlexItem = styled(EuiFlexItem)` - &.euiFlexItem { - flex: 1 0 0; - overflow-y: scroll; - overflow-x: hidden; - } + flex: 1 0 auto; `; const StyledEuiFlexButtonWrapper = styled(EuiFlexItem)` align-self: flex-start; + flex: 1 0 auto; +`; + +const StyledPanelContent = styled.div` + display: block; + height: 100%; + overflow-y: scroll; + overflow-x: hidden; `; interface HostDetailsProps { @@ -107,9 +108,9 @@ export const HostDetailsPanel: React.FC = React.memo( - + - + ); } diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx index 0482491562f57b..177cd2e5ded41b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import { EuiFlyout } from '@elastic/eui'; +import { EuiFlyout, EuiFlyoutProps } from '@elastic/eui'; import styled from 'styled-components'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; @@ -69,9 +69,10 @@ export const DetailsPanel = React.memo( if (!currentTabDetail?.panelView) return null; let visiblePanel = null; // store in variable to make return statement more readable + let panelSize: EuiFlyoutProps['size'] = 's'; const contextID = `${timelineId}-${activeTab}`; - if (currentTabDetail?.panelView === 'eventDetail' && currentTabDetail?.params?.eventId) { + panelSize = 'm'; visiblePanel = ( + {visiblePanel} ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/index.tsx index e05c9435fc456f..ea857da926f84e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/index.tsx @@ -42,19 +42,19 @@ const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` `; const StyledEuiFlexGroup = styled(EuiFlexGroup)` - flex: 0; -`; - -const StyledEuiFlexItem = styled(EuiFlexItem)` - &.euiFlexItem { - flex: 1 0 0; - overflow-y: scroll; - overflow-x: hidden; - } + flex: 1 0 auto; `; const StyledEuiFlexButtonWrapper = styled(EuiFlexItem)` align-self: flex-start; + flex: 1 0 auto; +`; + +const StyledPanelContent = styled.div` + display: block; + height: 100%; + overflow-y: scroll; + overflow-x: hidden; `; interface NetworkDetailsProps { @@ -104,9 +104,9 @@ export const NetworkDetailsPanel = React.memo( - + - + ); } From 495c259d19568e46a8895a3221a02ab3e85214c2 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 2 Mar 2021 12:18:39 -0600 Subject: [PATCH 14/63] Pass service node name in query for instance table links (#91796) (#93240) For a non-Java service, the previous link was like: ``` http://localhost:5601/kbn/app/apm/services/opbeans-python/metrics?rangeFrom=now-15m&rangeTo=now ``` which did not filter by the `service.node.name`. It now is: ``` http://localhost:5601/kbn/app/apm/services/opbeans-python/metrics?kuery=service.node.name:%226a7f116fe344aee7e92fceeb426cbfdf6a534a8e3ba6345c16a47793eba6daf5%22&rangeFrom=now-15m&rangeTo=now ```` Which links to the metrics page with the filter applied. The component is using a `MetricOverviewLink` which was using a `EuiLink` and passing throught the props, including `mergeQuery`, which includes the `kuery` parameter. Replace the `EuiLink` with an `APMLink` which does use the `mergeQuery` prop and does pass the parameters through correctly. Looks like this was changed to an `EuiLink` by a refactor in #86986. Since we'll be making some further changes to how `kuery` is handled in #84526, I'm just making the minimal change to fix this bug at this time. --- .../components/shared/Links/apm/MetricOverviewLink.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index 3bfdb5df61c2ee..c3d418b63426b5 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { EuiLink } from '@elastic/eui'; import React from 'react'; import { APMQueryParams } from '../url_helpers'; -import { APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'host', @@ -29,6 +28,5 @@ interface Props extends APMLinkExtendProps { } export function MetricOverviewLink({ serviceName, ...rest }: Props) { - const href = useMetricOverviewHref(serviceName); - return ; + return ; } From e709686224432337a772739a86b797e06d41a1be Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 2 Mar 2021 20:33:50 +0100 Subject: [PATCH 15/63] [Discover] Change icon of saved search in open search panel and embeddable selection (#93001) (#93271) --- .../public/application/components/top_nav/open_search_panel.tsx | 2 +- .../public/application/embeddable/search_embeddable_factory.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx index 8c5f44b97f4b30..f6881d1856566a 100644 --- a/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx +++ b/src/plugins/discover/public/application/components/top_nav/open_search_panel.tsx @@ -59,7 +59,7 @@ export function OpenSearchPanel(props: OpenSearchPanelProps) { savedObjectMetaData={[ { type: SEARCH_OBJECT_TYPE, - getIconForSavedObject: () => 'search', + getIconForSavedObject: () => 'discoverApp', name: i18n.translate('discover.savedSearch.savedObjectName', { defaultMessage: 'Saved search', }), diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts index 043869f99bb350..77da138d118dd0 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable_factory.ts @@ -36,7 +36,7 @@ export class SearchEmbeddableFactory defaultMessage: 'Saved search', }), type: 'search', - getIconForSavedObject: () => 'search', + getIconForSavedObject: () => 'discoverApp', }; constructor( From 08417cbd6c15e4c866651a7dcdfeded58845206d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Mar 2021 14:39:39 -0500 Subject: [PATCH 16/63] [Bazel] More resilient Workspace Status (#93244) (#93273) Signed-off-by: Tyler Smalley Co-authored-by: Tyler Smalley --- src/dev/bazel_workspace_status.js | 37 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/dev/bazel_workspace_status.js b/src/dev/bazel_workspace_status.js index fe60f9176d243e..3c3ef1574cd8e8 100644 --- a/src/dev/bazel_workspace_status.js +++ b/src/dev/bazel_workspace_status.js @@ -43,25 +43,23 @@ // Commit SHA const commitSHACmdResult = await runCmd('git', ['rev-parse', 'HEAD']); - if (commitSHACmdResult.exitCode !== 0) { - process.exit(1); - } - console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`); + if (commitSHACmdResult.exitCode === 0) { + console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`); - // Git branch - const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']); - if (gitBranchCmdResult.exitCode !== 0) { - process.exit(1); - } - console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`); + // Branch + const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']); + if (gitBranchCmdResult.exitCode === 0) { + console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`); + } - // Tree status - const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']); - const treeStatusVarStr = 'GIT_TREE_STATUS'; - if (treeStatusCmdResult.exitCode === 0) { - console.log(`${treeStatusVarStr} Clean`); - } else { - console.log(`${treeStatusVarStr} Modified`); + // Tree status + const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']); + const treeStatusVarStr = 'GIT_TREE_STATUS'; + if (treeStatusCmdResult.exitCode === 0) { + console.log(`${treeStatusVarStr} Clean`); + } else { + console.log(`${treeStatusVarStr} Modified`); + } } // Host @@ -72,9 +70,8 @@ return !cpu.model.includes('Intel') || index % 2 === 1; }).length; - if (hostCmdResult.exitCode !== 0) { - process.exit(1); + if (hostCmdResult.exitCode === 0) { + console.log(`HOST ${hostStr}-${coresStr}`); } - console.log(`HOST ${hostStr}-${coresStr}`); } })(); From 3cd4cdf26fbeca92f3ae0b67fadc08fc71a2983a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Mar 2021 15:20:17 -0500 Subject: [PATCH 17/63] [Actions][Docs] Moving subaction and subaction params back to README (#92878) (#93281) * Moving subaction and subaction params back to README * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * PR fixes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: ymao1 Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/user/alerting/action-types/jira.asciidoc | 45 +---- .../alerting/action-types/resilient.asciidoc | 29 +-- .../alerting/action-types/servicenow.asciidoc | 33 +-- x-pack/plugins/actions/README.md | 190 +++++++++++++++--- 4 files changed, 175 insertions(+), 122 deletions(-) diff --git a/docs/user/alerting/action-types/jira.asciidoc b/docs/user/alerting/action-types/jira.asciidoc index 0740cf7838b15a..d37f565c1739ba 100644 --- a/docs/user/alerting/action-types/jira.asciidoc +++ b/docs/user/alerting/action-types/jira.asciidoc @@ -65,44 +65,13 @@ API token (or password):: Jira API authentication token (or password) for HTTP Jira actions have the following configuration properties: -Subaction:: The subaction to perform: `pushToService`, `getIncident`, `issueTypes`, `fieldsByIssueType`, `issues`, `issue`, or `getFields`. -Subaction params:: The parameters of the subaction. - -==== `pushToService` subaction configuration - -Incident:: A Jira incident has the following properties: -* `summary` - The title of the issue. -* `description` - A description of the issue. -* `externalId` - The ID of the issue in Jira. If present, the issue is updated. Otherwise, a new issue is created. -* `issueType` - The ID of the issue type in Jira. -* `priority` - The priority level in Jira. Example: `Medium`. -* `labels` - An array of labels. Labels cannot contain spaces. -* `parent` - The parent issue ID or key. Only for subtask issue types. -Comments:: A comment in the form of `{ commentId: string, version: string, comment: string }`. - -==== `getIncident` subaction configuration - -External ID:: The ID of the issue in Jira. - -==== `issueTypes` subaction configuration - -The `issueTypes` subaction has no parameters. Provide an empty object `{}`. - -==== `fieldsByIssueType` subaction configuration - -ID:: The ID of the issue in Jira. - -==== `issues` subaction configuration - -Title:: The title to search for. - -==== `issue` subaction configuration - -ID:: The ID of the issue in Jira. - -==== `getFields` subaction configuration - -The `getFields` subaction has no parameters. Provide an empty object `{}`. +Issue type:: The type of the issue. +Priority:: The priority of the incident. +Labels:: The labels for the incident. +Title:: A title for the issue, used for searching the contents of the knowledge base. +Description:: The details about the incident. +Parent:: The ID or key of the parent issue. Only for `Subtask` issue types. +Additional comments:: Additional information for the client, such as how to troubleshoot the issue. [[configuring-jira]] ==== Configuring and testing Jira diff --git a/docs/user/alerting/action-types/resilient.asciidoc b/docs/user/alerting/action-types/resilient.asciidoc index dfa95e2deec008..feca42a542a2f2 100644 --- a/docs/user/alerting/action-types/resilient.asciidoc +++ b/docs/user/alerting/action-types/resilient.asciidoc @@ -65,30 +65,11 @@ API key secret:: The authentication key secret for HTTP Basic authentication. IBM Resilient actions have the following configuration properties: -Subaction:: The subaction to perform: `pushToService`, `getFields`, `incidentTypes`, or `severity`. -Subaction params:: The parameters of the subaction. - -==== `pushToService` subaction configuration - -Incident:: The IBM resilient incident has the following properties: -* `name` - A name for the issue, used for searching the contents of the knowledge base. -* `description` - The details about the incident. -* `externalId` - The ID of the incident in IBM Resilient. If present, the incident is updated. Otherwise, a new incident is created. -* `incidentTypes` - An array with the IDs of IBM Resilient incident types. -* `severityCode` - The IBM Resilient ID of the severity code. -Comments:: A comment in the form of `{ commentId: string, version: string, comment: string }`. - -===== `getFields` subaction configuration - -The `getFields` subaction has not parameters. Provide an empty object `{}`. - -===== `incidentTypes` subaction configuration - -The `incidentTypes` subaction has no parameters. Provide an empty object `{}`. - -===== `severity` subaction configuration - -The `severity` subaction has no parameters. Provide an empty object `{}`. +Incident types:: The type of the incident. +Severity code:: The severity of the incident. +Name:: A name for the issue, used for searching the contents of the knowledge base. +Description:: The details about the incident. +Additional comments:: Additional information for the client, such as how to troubleshoot the issue. [[configuring-resilient]] ==== Configuring and testing IBM Resilient diff --git a/docs/user/alerting/action-types/servicenow.asciidoc b/docs/user/alerting/action-types/servicenow.asciidoc index d1ee1b9357737d..4a11a2e28712a0 100644 --- a/docs/user/alerting/action-types/servicenow.asciidoc +++ b/docs/user/alerting/action-types/servicenow.asciidoc @@ -60,33 +60,12 @@ Password:: Password for HTTP Basic authentication. ServiceNow actions have the following configuration properties: -Subaction:: The subaction to perform: `pushToService`, `getFields`, `getIncident`, or `getChoices`. -Subaction params:: The parameters of the subaction. - -==== `pushToService` subaction configuration - -Incident:: The ServiceNow incident has the following properties: -* `short_description` - A short description for the incident, used for searching the contents of the knowledge base. -* `description` - The details about the incident. -* `externalId` - The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. -* `severity` - The severity of the incident. -* `urgency` - The extent to which the incident resolution can delay. -* `impact` - The effect an incident has on business. Can be measured by the number of affected users or by how critical it is to the business in question. -* `category` - The name of the category in ServiceNow. -* `subcategory` - The name of the subcategory in ServiceNow. -Comments:: A comment in the form of `{ commentId: string, version: string, comment: string }`. - -===== `getFields` subaction configuration - -The `getFields` subaction has no parameters. Provide an empty object `{}`. - -===== `getIncident` subaction configuration - -External ID:: The ID of the incident in ServiceNow. - -===== `getChoices` subaction configuration - -Fields:: An array of fields. Example: `[priority, category, impact]`. +Urgency:: The extent to which the incident resolution can delay. +Severity:: The severity of the incident. +Impact:: The effect an incident has on business. Can be measured by the number of affected users or by how critical it is to the business in question. +Short description:: A short description for the incident, used for searching the contents of the knowledge base. +Description:: The details about the incident. +Additional comments:: Additional information for the client, such as how to troubleshoot the issue. [[configuring-servicenow]] ==== Configuring and testing ServiceNow diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 78094f4c0eb0b1..f1be1ec12b79c5 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -33,42 +33,14 @@ Table of Contents - [actionsClient.execute(options)](#actionsclientexecuteoptions) - [Example](#example-2) - [Built-in Action Types](#built-in-action-types) - - [Server log](#server-log) - - [`config`](#config) - - [`secrets`](#secrets) - - [`params`](#params) - - [Email](#email) - - [`config`](#config-1) - - [`secrets`](#secrets-1) - - [`params`](#params-1) - - [Slack](#slack) - - [`config`](#config-2) - - [`secrets`](#secrets-2) - - [`params`](#params-2) - - [Index](#index) - - [`config`](#config-3) - - [`secrets`](#secrets-3) - - [`params`](#params-3) - - [Webhook](#webhook) - - [`config`](#config-4) - - [`secrets`](#secrets-4) - - [`params`](#params-4) - - [PagerDuty](#pagerduty) - - [`config`](#config-5) - - [`secrets`](#secrets-5) - - [`params`](#params-5) - [ServiceNow](#servicenow) - - [`config`](#config-6) - - [`secrets`](#secrets-6) - - [`params`](#params-6) + - [`params`](#params) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice) - [`subActionParams (getFields)`](#subactionparams-getfields) - [`subActionParams (getIncident)`](#subactionparams-getincident) - [`subActionParams (getChoices)`](#subactionparams-getchoices) - [Jira](#jira) - - [`config`](#config-7) - - [`secrets`](#secrets-7) - - [`params`](#params-7) + - [`params`](#params-1) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) - [`subActionParams (getIncident)`](#subactionparams-getincident-1) - [`subActionParams (issueTypes)`](#subactionparams-issuetypes) @@ -77,9 +49,7 @@ Table of Contents - [`subActionParams (issue)`](#subactionparams-issue) - [`subActionParams (getFields)`](#subactionparams-getfields-1) - [IBM Resilient](#ibm-resilient) - - [`config`](#config-8) - - [`secrets`](#secrets-8) - - [`params`](#params-8) + - [`params`](#params-2) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) - [`subActionParams (getFields)`](#subactionparams-getfields-2) - [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes) @@ -272,6 +242,160 @@ const result = await actionsClient.execute({ Kibana ships with a set of built-in action types. See [Actions and connector types Documentation](https://www.elastic.co/guide/en/kibana/master/action-types.html). +In addition to the documented configurations, several built in action type offer additional `params` configurations. + +## ServiceNow + +The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available. +### `params` + +| Property | Description | Type | +| --------------- | -------------------------------------------------------------------------------------------------- | ------ | +| subAction | The subaction to perform. It can be `pushToService`, `getFields`, `getIncident`, and `getChoices`. | string | +| subActionParams | The parameters of the subaction. | object | + +#### `subActionParams (pushToService)` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The ServiceNow incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + +The following table describes the properties of the `incident` object. + +| Property | Description | Type | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| short_description | The title of the incident. | string | +| description | The description of the incident. | string _(optional)_ | +| externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | +| severity | The severity in ServiceNow. | string _(optional)_ | +| urgency | The urgency in ServiceNow. | string _(optional)_ | +| impact | The impact in ServiceNow. | string _(optional)_ | +| category | The category in ServiceNow. | string _(optional)_ | +| subcategory | The subcategory in ServiceNow. | string _(optional)_ | + +#### `subActionParams (getFields)` + +No parameters for the `getFields` subaction. Provide an empty object `{}`. + +#### `subActionParams (getIncident)` + +| Property | Description | Type | +| ---------- | ------------------------------------- | ------ | +| externalId | The ID of the incident in ServiceNow. | string | + + +#### `subActionParams (getChoices)` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------ | -------- | +| fields | An array of fields. Example: `[priority, category, impact]`. | string[] | + +--- + +## Jira + +The [Jira user documentation `params`](https://www.elastic.co/guide/en/kibana/master/jira-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available. +### `params` + +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------ | +| subAction | The subaction to perform. It can be `pushToService`, `getIncident`, `issueTypes`, `fieldsByIssueType`, `issues`, `issue`, and `getFields`. | string | +| subActionParams | The parameters of the subaction. | object | + +#### `subActionParams (pushToService)` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The Jira incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + +The following table describes the properties of the `incident` object. + +| Property | Description | Type | +| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | +| summary | The title of the issue. | string | +| description | The description of the issue. | string _(optional)_ | +| externalId | The ID of the issue in Jira. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | +| issueType | The ID of the issue type in Jira. | string _(optional)_ | +| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | +| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | +| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ | + +#### `subActionParams (getIncident)` + +| Property | Description | Type | +| ---------- | --------------------------- | ------ | +| externalId | The ID of the issue in Jira. | string | + +#### `subActionParams (issueTypes)` + +No parameters for the `issueTypes` subaction. Provide an empty object `{}`. + +#### `subActionParams (fieldsByIssueType)` + +| Property | Description | Type | +| -------- | -------------------------------- | ------ | +| id | The ID of the issue type in Jira. | string | + +#### `subActionParams (issues)` + +| Property | Description | Type | +| -------- | ----------------------- | ------ | +| title | The title to search for. | string | + +#### `subActionParams (issue)` + +| Property | Description | Type | +| -------- | --------------------------- | ------ | +| id | The ID of the issue in Jira. | string | + +#### `subActionParams (getFields)` + +No parameters for the `getFields` subaction. Provide an empty object `{}`. + +--- +## IBM Resilient + +The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/kibana/master/resilient-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available. + +### `params` + +| Property | Description | Type | +| --------------- | -------------------------------------------------------------------------------------------------- | ------ | +| subAction | The subaction to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity. | string | +| subActionParams | The parameters of the subaction. | object | + +#### `subActionParams (pushToService)` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The IBM Resilient incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + +The following table describes the properties of the `incident` object. + +| Property | Description | Type | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| name | The title of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | +| externalId | The ID of the incident in IBM Resilient. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | +| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ | +| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ | + +#### `subActionParams (getFields)` + +No parameters for the `getFields` subaction. Provide an empty object `{}`. + +#### `subActionParams (incidentTypes)` + +No parameters for the `incidentTypes` subaction. Provide an empty object `{}`. + +#### `subActionParams (severity)` + +No parameters for the `severity` subaction. Provide an empty object `{}`. + +--- # Command Line Utility The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: From 26fa8d4a5a370dc0dcd187f990ad47273212a411 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Mar 2021 15:34:19 -0500 Subject: [PATCH 18/63] Different screenshot (#92970) (#93309) Co-authored-by: ymao1 --- .../alert-flyout-alert-type-selection.png | Bin 255161 -> 166951 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/user/alerting/images/alert-flyout-alert-type-selection.png b/docs/user/alerting/images/alert-flyout-alert-type-selection.png index 4e5c49975160f3e810c22b0140512b04880aff18..6ebbe4213ba7d30b06bd3f36c372d88129272f6b 100644 GIT binary patch literal 166951 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�)OJ%dwZ8CKmec^u6&_{G_|;P?r`Rc2s~@QB11_ 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 Date: Tue, 2 Mar 2021 15:34:45 -0500 Subject: [PATCH 19/63] [core.logging] Fix calculating payload bytes for arrays and zlib streams. (#92100) (#93284) Co-authored-by: Luke Elmers --- .../src/utils/get_payload_size.test.ts | 106 +++++++++---- .../src/utils/get_payload_size.ts | 10 +- .../http/logging/get_payload_size.test.ts | 142 +++++++++++++----- .../server/http/logging/get_payload_size.ts | 35 +++-- 4 files changed, 208 insertions(+), 85 deletions(-) diff --git a/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts b/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts index 3bb97e57ca0a3c..01d2cf29758db9 100644 --- a/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts +++ b/packages/kbn-legacy-logging/src/utils/get_payload_size.test.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -import { createGunzip } from 'zlib'; import mockFs from 'mock-fs'; import { createReadStream } from 'fs'; +import { PassThrough } from 'stream'; +import { createGzip, createGunzip } from 'zlib'; import { getResponsePayloadBytes } from './get_payload_size'; @@ -27,38 +28,74 @@ describe('getPayloadSize', () => { }); }); - describe('handles fs streams', () => { + describe('handles streams', () => { afterEach(() => mockFs.restore()); - test('with ascii characters', async () => { - mockFs({ 'test.txt': 'heya' }); - const readStream = createReadStream('test.txt'); - - let data = ''; - for await (const chunk of readStream) { - data += chunk; - } - - const result = getResponsePayloadBytes(readStream); - expect(result).toBe(Buffer.byteLength(data)); - }); - - test('with special characters', async () => { - mockFs({ 'test.txt': '¡hola!' }); - const readStream = createReadStream('test.txt'); - - let data = ''; - for await (const chunk of readStream) { - data += chunk; - } - - const result = getResponsePayloadBytes(readStream); - expect(result).toBe(Buffer.byteLength(data)); + test('ignores streams that are not fs or zlib streams', async () => { + const result = getResponsePayloadBytes(new PassThrough()); + expect(result).toBe(undefined); }); - test('ignores streams that are not instances of ReadStream', async () => { - const result = getResponsePayloadBytes(createGunzip()); - expect(result).toBe(undefined); + describe('fs streams', () => { + test('with ascii characters', async () => { + mockFs({ 'test.txt': 'heya' }); + const readStream = createReadStream('test.txt'); + + let data = ''; + for await (const chunk of readStream) { + data += chunk; + } + + const result = getResponsePayloadBytes(readStream); + expect(result).toBe(Buffer.byteLength(data)); + }); + + test('with special characters', async () => { + mockFs({ 'test.txt': '¡hola!' }); + const readStream = createReadStream('test.txt'); + + let data = ''; + for await (const chunk of readStream) { + data += chunk; + } + + const result = getResponsePayloadBytes(readStream); + expect(result).toBe(Buffer.byteLength(data)); + }); + + describe('zlib streams', () => { + test('with ascii characters', async () => { + mockFs({ 'test.txt': 'heya' }); + const readStream = createReadStream('test.txt'); + const source = readStream.pipe(createGzip()).pipe(createGunzip()); + + let data = ''; + for await (const chunk of source) { + data += chunk; + } + + const result = getResponsePayloadBytes(source); + + expect(data).toBe('heya'); + expect(result).toBe(source.bytesWritten); + }); + + test('with special characters', async () => { + mockFs({ 'test.txt': '¡hola!' }); + const readStream = createReadStream('test.txt'); + const source = readStream.pipe(createGzip()).pipe(createGunzip()); + + let data = ''; + for await (const chunk of source) { + data += chunk; + } + + const result = getResponsePayloadBytes(source); + + expect(data).toBe('¡hola!'); + expect(result).toBe(source.bytesWritten); + }); + }); }); }); @@ -79,8 +116,17 @@ describe('getPayloadSize', () => { expect(result).toBe(JSON.stringify(payload).length); }); + test('when source is array object', () => { + const payload = [{ message: 'hey' }, { message: 'ya' }]; + const result = getResponsePayloadBytes(payload); + expect(result).toBe(JSON.stringify(payload).length); + }); + test('returns undefined when source is not plain object', () => { - const result = getResponsePayloadBytes([1, 2, 3]); + class TestClass { + constructor() {} + } + const result = getResponsePayloadBytes(new TestClass()); expect(result).toBe(undefined); }); }); diff --git a/packages/kbn-legacy-logging/src/utils/get_payload_size.ts b/packages/kbn-legacy-logging/src/utils/get_payload_size.ts index c7aeb0e8cac2bb..acc517c74c2d46 100644 --- a/packages/kbn-legacy-logging/src/utils/get_payload_size.ts +++ b/packages/kbn-legacy-logging/src/utils/get_payload_size.ts @@ -8,11 +8,15 @@ import { isPlainObject } from 'lodash'; import { ReadStream } from 'fs'; +import { Zlib } from 'zlib'; import type { ResponseObject } from '@hapi/hapi'; const isBuffer = (obj: unknown): obj is Buffer => Buffer.isBuffer(obj); const isFsReadStream = (obj: unknown): obj is ReadStream => typeof obj === 'object' && obj !== null && 'bytesRead' in obj && obj instanceof ReadStream; +const isZlibStream = (obj: unknown): obj is Zlib => { + return typeof obj === 'object' && obj !== null && 'bytesWritten' in obj; +}; const isString = (obj: unknown): obj is string => typeof obj === 'string'; /** @@ -51,11 +55,15 @@ export function getResponsePayloadBytes( return payload.bytesRead; } + if (isZlibStream(payload)) { + return payload.bytesWritten; + } + if (isString(payload)) { return Buffer.byteLength(payload); } - if (isPlainObject(payload)) { + if (isPlainObject(payload) || Array.isArray(payload)) { return Buffer.byteLength(JSON.stringify(payload)); } diff --git a/src/core/server/http/logging/get_payload_size.test.ts b/src/core/server/http/logging/get_payload_size.test.ts index dba5c7be30f3b0..30cb547dd98b77 100644 --- a/src/core/server/http/logging/get_payload_size.test.ts +++ b/src/core/server/http/logging/get_payload_size.test.ts @@ -6,12 +6,13 @@ * Side Public License, v 1. */ -import { createGunzip } from 'zlib'; import type { Request } from '@hapi/hapi'; import Boom from '@hapi/boom'; import mockFs from 'mock-fs'; import { createReadStream } from 'fs'; +import { PassThrough } from 'stream'; +import { createGunzip, createGzip } from 'zlib'; import { loggerMock, MockedLogger } from '../../logging/logger.mock'; @@ -55,59 +56,107 @@ describe('getPayloadSize', () => { }); }); - describe('handles fs streams', () => { + describe('handles streams', () => { afterEach(() => mockFs.restore()); - test('with ascii characters', async () => { - mockFs({ 'test.txt': 'heya' }); - const source = createReadStream('test.txt'); - - let data = ''; - for await (const chunk of source) { - data += chunk; - } - + test('ignores streams that are not fs or zlib streams', async () => { const result = getResponsePayloadBytes( { variety: 'stream', - source, + source: new PassThrough(), } as Response, logger ); - expect(result).toBe(Buffer.byteLength(data)); + expect(result).toBe(undefined); }); - test('with special characters', async () => { - mockFs({ 'test.txt': '¡hola!' }); - const source = createReadStream('test.txt'); + describe('fs streams', () => { + test('with ascii characters', async () => { + mockFs({ 'test.txt': 'heya' }); + const source = createReadStream('test.txt'); - let data = ''; - for await (const chunk of source) { - data += chunk; - } + let data = ''; + for await (const chunk of source) { + data += chunk; + } - const result = getResponsePayloadBytes( - { - variety: 'stream', - source, - } as Response, - logger - ); + const result = getResponsePayloadBytes( + { + variety: 'stream', + source, + } as Response, + logger + ); + + expect(result).toBe(Buffer.byteLength(data)); + }); + + test('with special characters', async () => { + mockFs({ 'test.txt': '¡hola!' }); + const source = createReadStream('test.txt'); + + let data = ''; + for await (const chunk of source) { + data += chunk; + } + + const result = getResponsePayloadBytes( + { + variety: 'stream', + source, + } as Response, + logger + ); - expect(result).toBe(Buffer.byteLength(data)); + expect(result).toBe(Buffer.byteLength(data)); + }); }); - test('ignores streams that are not instances of ReadStream', async () => { - const result = getResponsePayloadBytes( - { - variety: 'stream', - source: createGunzip(), - } as Response, - logger - ); + describe('zlib streams', () => { + test('with ascii characters', async () => { + mockFs({ 'test.txt': 'heya' }); + const readStream = createReadStream('test.txt'); + const source = readStream.pipe(createGzip()).pipe(createGunzip()); - expect(result).toBe(undefined); + let data = ''; + for await (const chunk of source) { + data += chunk; + } + + const result = getResponsePayloadBytes( + { + variety: 'stream', + source, + } as Response, + logger + ); + + expect(data).toBe('heya'); + expect(result).toBe(source.bytesWritten); + }); + + test('with special characters', async () => { + mockFs({ 'test.txt': '¡hola!' }); + const readStream = createReadStream('test.txt'); + const source = readStream.pipe(createGzip()).pipe(createGunzip()); + + let data = ''; + for await (const chunk of source) { + data += chunk; + } + + const result = getResponsePayloadBytes( + { + variety: 'stream', + source, + } as Response, + logger + ); + + expect(data).toBe('¡hola!'); + expect(result).toBe(source.bytesWritten); + }); }); }); @@ -134,7 +183,7 @@ describe('getPayloadSize', () => { expect(result).toBe(7); }); - test('when source is object', () => { + test('when source is plain object', () => { const payload = { message: 'heya' }; const result = getResponsePayloadBytes( { @@ -146,11 +195,26 @@ describe('getPayloadSize', () => { expect(result).toBe(JSON.stringify(payload).length); }); + test('when source is array object', () => { + const payload = [{ message: 'hey' }, { message: 'ya' }]; + const result = getResponsePayloadBytes( + { + variety: 'plain', + source: payload, + } as Response, + logger + ); + expect(result).toBe(JSON.stringify(payload).length); + }); + test('returns undefined when source is not a plain object', () => { + class TestClass { + constructor() {} + } const result = getResponsePayloadBytes( { variety: 'plain', - source: [1, 2, 3], + source: new TestClass(), } as Response, logger ); diff --git a/src/core/server/http/logging/get_payload_size.ts b/src/core/server/http/logging/get_payload_size.ts index 8e6dea13e1fa17..2797b4ba9f490e 100644 --- a/src/core/server/http/logging/get_payload_size.ts +++ b/src/core/server/http/logging/get_payload_size.ts @@ -8,25 +8,23 @@ import { isPlainObject } from 'lodash'; import { ReadStream } from 'fs'; +import { Zlib } from 'zlib'; import { isBoom } from '@hapi/boom'; import type { Request } from '@hapi/hapi'; import { Logger } from '../../logging'; type Response = Request['response']; -const isBuffer = (src: unknown, res: Response): src is Buffer => { - return !isBoom(res) && res.variety === 'buffer' && res.source === src; +const isBuffer = (src: unknown, variety: string): src is Buffer => + variety === 'buffer' && Buffer.isBuffer(src); +const isFsReadStream = (src: unknown, variety: string): src is ReadStream => { + return variety === 'stream' && src instanceof ReadStream; }; -const isFsReadStream = (src: unknown, res: Response): src is ReadStream => { - return ( - !isBoom(res) && - res.variety === 'stream' && - res.source === src && - res.source instanceof ReadStream - ); +const isZlibStream = (src: unknown, variety: string): src is Zlib => { + return variety === 'stream' && typeof src === 'object' && src !== null && 'bytesWritten' in src; }; -const isString = (src: unknown, res: Response): src is string => - !isBoom(res) && res.variety === 'plain' && typeof src === 'string'; +const isString = (src: unknown, variety: string): src is string => + variety === 'plain' && typeof src === 'string'; /** * Attempts to determine the size (in bytes) of a Hapi response @@ -57,19 +55,26 @@ export function getResponsePayloadBytes(response: Response, log: Logger): number return Buffer.byteLength(JSON.stringify(response.output.payload)); } - if (isBuffer(response.source, response)) { + if (isBuffer(response.source, response.variety)) { return response.source.byteLength; } - if (isFsReadStream(response.source, response)) { + if (isFsReadStream(response.source, response.variety)) { return response.source.bytesRead; } - if (isString(response.source, response)) { + if (isZlibStream(response.source, response.variety)) { + return response.source.bytesWritten; + } + + if (isString(response.source, response.variety)) { return Buffer.byteLength(response.source); } - if (response.variety === 'plain' && isPlainObject(response.source)) { + if ( + response.variety === 'plain' && + (isPlainObject(response.source) || Array.isArray(response.source)) + ) { return Buffer.byteLength(JSON.stringify(response.source)); } } catch (e) { From ae2e353979919e00c4e1704b21f1919b91791758 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Tue, 2 Mar 2021 16:33:29 -0500 Subject: [PATCH 20/63] [APM] Fixes duplicate ML job creation for existing environments (#85023) (#93098) (#93291) * [APM] Fixes duplicate ML job creation for existing environments (#85023) * Removes commented out test code. * Adds API integration tests * clean up code for readability --- .../create_anomaly_detection_jobs.ts | 31 +++++++++++++++++-- .../routes/settings/anomaly_detection.ts | 1 + .../settings/anomaly_detection/write_user.ts | 22 ++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index d70e19bf4a5f52..413b0a1c6983ec 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -19,6 +19,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { withApmSpan } from '../../utils/with_apm_span'; +import { getAnomalyDetectionJobs } from './get_anomaly_detection_jobs'; export async function createAnomalyDetectionJobs( setup: Setup, @@ -38,14 +39,19 @@ export async function createAnomalyDetectionJobs( throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } + const uniqueMlJobEnvs = await getUniqueMlJobEnvs(setup, environments, logger); + if (uniqueMlJobEnvs.length === 0) { + return []; + } + return withApmSpan('create_anomaly_detection_jobs', async () => { logger.info( - `Creating ML anomaly detection jobs for environments: [${environments}].` + `Creating ML anomaly detection jobs for environments: [${uniqueMlJobEnvs}].` ); const indexPatternName = indices['apm_oss.transactionIndices']; const responses = await Promise.all( - environments.map((environment) => + uniqueMlJobEnvs.map((environment) => createAnomalyDetectionJob({ ml, environment, indexPatternName }) ) ); @@ -105,3 +111,24 @@ async function createAnomalyDetectionJob({ }); }); } + +async function getUniqueMlJobEnvs( + setup: Setup, + environments: string[], + logger: Logger +) { + // skip creation of duplicate ML jobs + const jobs = await getAnomalyDetectionJobs(setup, logger); + const existingMlJobEnvs = jobs.map(({ environment }) => environment); + const requestedExistingMlJobEnvs = environments.filter((env) => + existingMlJobEnvs.includes(env) + ); + + if (requestedExistingMlJobEnvs.length) { + logger.warn( + `Skipping creation of existing ML jobs for environments: [${requestedExistingMlJobEnvs}]}` + ); + } + + return environments.filter((env) => !existingMlJobEnvs.includes(env)); +} diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 25afb11f264590..e5922d9ed3e941 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -66,6 +66,7 @@ export const createAnomalyDetectionJobsRoute = createRoute({ } await createAnomalyDetectionJobs(setup, environments, context.logger); + notifyFeatureUsage({ licensingPlugin: context.licensing, featureName: 'ml', diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts index a17804c46d21aa..83ff51ec1b4c28 100644 --- a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/write_user.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { countBy } from 'lodash'; import { registry } from '../../../common/registry'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; @@ -49,7 +50,26 @@ export default function apiTest({ getService }: FtrProviderContext) { const { body } = await getJobs(); expect(body.hasLegacyJobs).to.be(false); - expect(body.jobs.map((job: any) => job.environment)).to.eql(['production', 'staging']); + expect(countBy(body.jobs, 'environment')).to.eql({ + production: 1, + staging: 1, + }); + }); + + describe('with existing ML jobs', () => { + before(async () => { + await createJobs(['production', 'staging']); + }); + it('skips duplicate job creation', async () => { + await createJobs(['production', 'test']); + + const { body } = await getJobs(); + expect(countBy(body.jobs, 'environment')).to.eql({ + production: 1, + staging: 1, + test: 1, + }); + }); }); }); }); From ed0f8eba576a5b8467923a8965518ec5d368dd94 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Tue, 2 Mar 2021 17:05:33 -0500 Subject: [PATCH 21/63] [Security Solution] Fixes the Customize Event Renderers modal by removing the EuiOverlayMask (#93150) (#93215) * ## [Security Solution] Fixes the Customize Event Renderers modal by removing the EuiOverlayMask Fixes [this issue](https://github.com/elastic/kibana/issues/92798), introduced when [the EUI modal implementation changed](https://github.com/elastic/eui/pull/4480), such that it's no longer necessary to wrap modals in an `EuiOverlayMask`. The mask is now built-in to `EuiModal`. The change above became effective throughout Kibana when it was upgraded to use a newer version of EUI via [this commit on Feb 16](https://github.com/elastic/kibana/commit/8126488021b2efd674ea1ddec3c99c24029879f5#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519). This PR resolves the issue by removing the `EuiOverlayMask` around the `Customize Event Renderers modal`, shown in the `After` screenshot below: ### Before ![before](https://user-images.githubusercontent.com/59917825/109154007-b2e23880-7793-11eb-83bb-4774df77c5d6.png) ### After ![after](https://user-images.githubusercontent.com/4459398/109561954-0c4fad80-7a9b-11eb-9283-51d50ec8ea26.png) ### Desk testing Desk-tested on a 16" 2019 MBP, and on the desktop with the following browser versions: - Chrome `88.0.4324.192` - Firefox `86.0` - Safari `14.0.3` * - force precommit git hooks to run Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Andrew Goldstein Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../row_renderers_browser/index.tsx | 105 ++++++++---------- 1 file changed, 44 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index e96bccd32618df..4dcc799d79111b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -10,7 +10,6 @@ import { EuiButtonIcon, EuiText, EuiToolTip, - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -33,12 +32,12 @@ import { RowRenderersBrowser } from './row_renderers_browser'; import * as i18n from './translations'; const StyledEuiModal = styled(EuiModal)` - margin: 0 auto; + ${({ theme }) => `margin-top: ${theme.eui.euiSizeXXL};`} max-width: 95vw; - min-height: 95vh; + min-height: 90vh; > .euiModal__flex { - max-height: 95vh; + max-height: 90vh; } `; @@ -65,15 +64,6 @@ const StyledEuiModalBody = styled(EuiModalBody)` } `; -const StyledEuiOverlayMask = styled(EuiOverlayMask)` - z-index: 8001; - padding-bottom: 0; - - > div { - width: 100%; - } -`; - interface StatefulRowRenderersBrowserProps { timelineId: string; } @@ -125,54 +115,47 @@ const StatefulRowRenderersBrowserComponent: React.FC {show && ( - - - - - - {i18n.CUSTOMIZE_EVENT_RENDERERS_TITLE} - {i18n.CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION} - - - - - - {i18n.DISABLE_ALL} - - - - - - {i18n.ENABLE_ALL} - - - - - - - - - - - - + + + + + {i18n.CUSTOMIZE_EVENT_RENDERERS_TITLE} + {i18n.CUSTOMIZE_EVENT_RENDERERS_DESCRIPTION} + + + + + + {i18n.DISABLE_ALL} + + + + + + {i18n.ENABLE_ALL} + + + + + + + + + + + )} ); From a09a8adde32d4b811fc647fc3d89463162415744 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 2 Mar 2021 17:14:44 -0500 Subject: [PATCH 22/63] [SECURITY SOLUTIONS] Bug case connector (#93104) (#93299) * bring back case connector to design * disable connector sir in collection * missing to only create collection type * fix fields connector when you need to hide service-now sir --- .../cases/components/case_view/index.tsx | 3 + .../connectors_dropdown.test.tsx | 153 ++++++++++++++++-- .../configure_cases/connectors_dropdown.tsx | 49 +++--- .../components/connector_selector/form.tsx | 3 + .../connectors/case/alert_fields.tsx | 21 +-- .../connectors/case/existing_case.tsx | 114 +++++-------- .../connectors/case/translations.ts | 16 +- .../cases/components/create/connector.tsx | 24 ++- .../public/cases/components/create/form.tsx | 134 +++++++-------- .../cases/components/create/form_context.tsx | 31 ++-- .../cases/components/edit_connector/index.tsx | 5 +- .../create_case_modal.tsx | 13 +- .../use_create_case_modal/index.tsx | 5 +- .../public/cases/containers/api.ts | 3 + .../public/cases/containers/types.ts | 1 + .../public/cases/containers/use_get_cases.tsx | 8 +- .../rules/rule_actions_field/index.test.tsx | 68 +++++++- .../rules/rule_actions_field/index.tsx | 61 ++++++- .../rules/step_rule_actions/index.tsx | 8 +- .../use_manage_case_action.tsx | 63 ++++++++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 22 files changed, 567 insertions(+), 218 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 108e020d014c4c..83a0c4e7acd3d6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -447,6 +447,9 @@ export const CaseComponent = React.memo( caseFields={caseData.connector.fields} connectors={connectors} disabled={!userCanCrud} + hideConnectorServiceNowSir={ + subCaseId != null || caseData.type === CaseType.collection + } isLoading={isLoadingConnectors || (isLoading && updateKey === 'connector')} onSubmit={onSubmitConnector} selectedConnector={caseData.connector.id} diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx index e8c074faed32ef..1f1876756773d6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.test.tsx @@ -34,22 +34,122 @@ describe('ConnectorsDropdown', () => { test('it formats the connectors correctly', () => { const selectProps = wrapper.find(EuiSuperSelect).props(); - expect(selectProps.options).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - value: 'none', - 'data-test-subj': 'dropdown-connector-no-connector', - }), - expect.objectContaining({ - value: 'servicenow-1', - 'data-test-subj': 'dropdown-connector-servicenow-1', - }), - expect.objectContaining({ - value: 'resilient-2', - 'data-test-subj': 'dropdown-connector-resilient-2', - }), - ]) - ); + expect(selectProps.options).toMatchInlineSnapshot(` + Array [ + Object { + "data-test-subj": "dropdown-connector-no-connector", + "inputDisplay": + + + + + + No connector selected + + + , + "value": "none", + }, + Object { + "data-test-subj": "dropdown-connector-servicenow-1", + "inputDisplay": + + + + + + My Connector + + + , + "value": "servicenow-1", + }, + Object { + "data-test-subj": "dropdown-connector-resilient-2", + "inputDisplay": + + + + + + My Connector 2 + + + , + "value": "resilient-2", + }, + Object { + "data-test-subj": "dropdown-connector-jira-1", + "inputDisplay": + + + + + + Jira + + + , + "value": "jira-1", + }, + Object { + "data-test-subj": "dropdown-connector-servicenow-sir", + "inputDisplay": + + + + + + My Connector SIR + + + , + "value": "servicenow-sir", + }, + ] + `); }); test('it disables the dropdown', () => { @@ -79,4 +179,25 @@ describe('ConnectorsDropdown', () => { expect(newWrapper.find('button span:not([data-euiicon-type])').text()).toEqual('My Connector'); }); + + test('if the props hideConnectorServiceNowSir is true, the connector should not be part of the list of options ', () => { + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + const selectProps = newWrapper.find(EuiSuperSelect).props(); + const options = selectProps.options as Array<{ 'data-test-subj': string }>; + expect( + options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-1') + ).toBeTruthy(); + expect( + options.some((o) => o['data-test-subj'] === 'dropdown-connector-servicenow-sir') + ).toBeFalsy(); + }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx index ab4b9fcfe70930..b8eacb9dfdd91d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors_dropdown.tsx @@ -9,6 +9,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import styled from 'styled-components'; +import { ConnectorTypes } from '../../../../../case/common/api'; import { ActionConnector } from '../../containers/configure/types'; import { connectorsConfiguration } from '../connectors'; import * as i18n from './translations'; @@ -20,6 +21,7 @@ export interface Props { onChange: (id: string) => void; selectedConnector: string; appendAddConnectorButton?: boolean; + hideConnectorServiceNowSir?: boolean; } const ICON_SIZE = 'm'; @@ -61,29 +63,36 @@ const ConnectorsDropdownComponent: React.FC = ({ onChange, selectedConnector, appendAddConnectorButton = false, + hideConnectorServiceNowSir = false, }) => { const connectorsAsOptions = useMemo(() => { const connectorsFormatted = connectors.reduce( - (acc, connector) => [ - ...acc, - { - value: connector.id, - inputDisplay: ( - - - - - - {connector.name} - - - ), - 'data-test-subj': `dropdown-connector-${connector.id}`, - }, - ], + (acc, connector) => { + if (hideConnectorServiceNowSir && connector.actionTypeId === ConnectorTypes.serviceNowSIR) { + return acc; + } + + return [ + ...acc, + { + value: connector.id, + inputDisplay: ( + + + + + + {connector.name} + + + ), + 'data-test-subj': `dropdown-connector-${connector.id}`, + }, + ]; + }, [noConnectorOption] ); diff --git a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx index d5f5530acde9b3..586a7c19cc5324 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx @@ -22,6 +22,7 @@ interface ConnectorSelectorProps { isEdit: boolean; isLoading: boolean; handleChange?: (newValue: string) => void; + hideConnectorServiceNowSir?: boolean; } export const ConnectorSelector = ({ connectors, @@ -32,6 +33,7 @@ export const ConnectorSelector = ({ isEdit = true, isLoading = false, handleChange, + hideConnectorServiceNowSir = false, }: ConnectorSelectorProps) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const onChange = useCallback( @@ -58,6 +60,7 @@ export const ConnectorSelector = ({ ` - margin-top: ${theme.eui?.euiSize ?? '16px'}; + padding: ${theme.eui?.euiSizeS ?? '8px'} ${theme.eui?.euiSizeL ?? '24px'} ${ + theme.eui?.euiSizeL ?? '24px' + } ${theme.eui?.euiSizeL ?? '24px'}; `} `; const defaultAlertComment = { type: CommentType.generatedAlert, - alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{rule.id}}", "ruleName": "{{rule.name}}"}__SEPARATOR__{{/context.alerts}}]`, + alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{signal.rule.id}}", "ruleName": "{{signal.rule.name}}"}__SEPARATOR__{{/context.alerts}}]`, }; const CaseParamsFields: React.FunctionComponent> = ({ @@ -90,12 +92,13 @@ const CaseParamsFields: React.FunctionComponent - - - - - + + + + +

{i18n.CASE_CONNECTOR_CALL_OUT_MSG}

+ + ); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx index 5f564d7b62464c..c1013718d57561 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx @@ -5,22 +5,15 @@ * 2.0. */ -import { - EuiButton, - EuiButtonIcon, - EuiCallOut, - EuiTextColor, - EuiLoadingSpinner, -} from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import React, { memo, useEffect, useCallback, useState } from 'react'; +import React, { memo, useMemo, useCallback } from 'react'; import { CaseType } from '../../../../../../case/common/api'; -import { Case } from '../../../containers/types'; -import { useDeleteCases } from '../../../containers/use_delete_cases'; -import { useGetCase } from '../../../containers/use_get_case'; -import { ConfirmDeleteCaseModal } from '../../confirm_delete_case'; +import { + useGetCases, + DEFAULT_QUERY_PARAMS, + DEFAULT_FILTER_OPTIONS, +} from '../../../containers/use_get_cases'; import { useCreateCaseModal } from '../../use_create_case_modal'; -import * as i18n from './translations'; +import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown'; interface ExistingCaseProps { selectedCase: string | null; @@ -28,76 +21,53 @@ interface ExistingCaseProps { } const ExistingCaseComponent: React.FC = ({ onCaseChanged, selectedCase }) => { - const { data, isLoading, isError } = useGetCase(selectedCase ?? ''); - const [createdCase, setCreatedCase] = useState(null); + const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(DEFAULT_QUERY_PARAMS, { + ...DEFAULT_FILTER_OPTIONS, + onlyCollectionType: true, + }); const onCaseCreated = useCallback( - (newCase: Case) => { + (newCase) => { + refetchCases(); onCaseChanged(newCase.id); - setCreatedCase(newCase); }, - [onCaseChanged] + [onCaseChanged, refetchCases] ); - const { modal, openModal } = useCreateCaseModal({ caseType: CaseType.collection, onCaseCreated }); + const { modal, openModal } = useCreateCaseModal({ + onCaseCreated, + caseType: CaseType.collection, + // FUTURE DEVELOPER + // We are making the assumption that this component is only used in rules creation + // that's why we want to hide ServiceNow SIR + hideConnectorServiceNowSir: true, + }); - // Delete case - const { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isLoading: isDeleting, - isDeleted, - isDisplayConfirmDeleteModal, - } = useDeleteCases(); + const onChange = useCallback( + (id: string) => { + if (id === ADD_CASE_BUTTON_ID) { + openModal(); + return; + } - useEffect(() => { - if (isDeleted) { - setCreatedCase(null); - onCaseChanged(''); - dispatchResetIsDeleted(); - } - // onCaseChanged and/or dispatchResetIsDeleted causes re-renders - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isDeleted]); + onCaseChanged(id); + }, + [onCaseChanged, openModal] + ); - useEffect(() => { - if (!isLoading && !isError && data != null) { - setCreatedCase(data); - onCaseChanged(data.id); - } - // onCaseChanged causes re-renders - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data, isLoading, isError]); + const isCasesLoading = useMemo( + () => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'), + [isLoadingCases] + ); return ( <> - {createdCase == null && isEmpty(selectedCase) && ( - - {i18n.CREATE_CASE} - - )} - {createdCase == null && isLoading && } - {createdCase != null && !isLoading && ( - <> - - - {createdCase.title}{' '} - {!isDeleting && ( - - )} - {isDeleting && } - - - - - )} + {modal} ); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts index 731e94a17d9235..6ce5316d0eb88d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts @@ -40,7 +40,7 @@ export const CASE_CONNECTOR_COMMENT_REQUIRED = i18n.translate( export const CASE_CONNECTOR_CASES_DROPDOWN_ROW_LABEL = i18n.translate( 'xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel', { - defaultMessage: 'Case', + defaultMessage: 'Case allowing sub-cases', } ); @@ -72,10 +72,18 @@ export const CASE_CONNECTOR_CASE_REQUIRED = i18n.translate( } ); -export const CASE_CONNECTOR_CALL_OUT_INFO = i18n.translate( - 'xpack.securitySolution.case.components.connectors.case.callOutInfo', +export const CASE_CONNECTOR_CALL_OUT_TITLE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.callOutTitle', { - defaultMessage: 'All alerts after rule creation will be appended to the selected case.', + defaultMessage: 'Generated alerts will be attached to sub-cases', + } +); + +export const CASE_CONNECTOR_CALL_OUT_MSG = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.callOutMsg', + { + defaultMessage: + 'A case can contain multiple sub-cases to allow grouping of generated alerts. Sub-cases will give more granular control over the status of these generated alerts and prevents having too many alerts attached to one case.', } ); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx index 5e7972aec9d4be..bfe0d8dd78e282 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx @@ -8,6 +8,7 @@ import React, { memo, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { ConnectorTypes } from '../../../../../case/common/api'; import { UseField, useFormData, FieldHook, useFormContext } from '../../../shared_imports'; import { useConnectors } from '../../containers/configure/use_connectors'; import { ConnectorSelector } from '../connector_selector/form'; @@ -18,19 +19,32 @@ import { FormProps } from './schema'; interface Props { isLoading: boolean; + hideConnectorServiceNowSir?: boolean; } interface ConnectorsFieldProps { connectors: ActionConnector[]; field: FieldHook; isEdit: boolean; + hideConnectorServiceNowSir?: boolean; } -const ConnectorFields = ({ connectors, isEdit, field }: ConnectorsFieldProps) => { +const ConnectorFields = ({ + connectors, + isEdit, + field, + hideConnectorServiceNowSir = false, +}: ConnectorsFieldProps) => { const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); const { setValue } = field; - const connector = getConnectorById(connectorId, connectors) ?? null; - + let connector = getConnectorById(connectorId, connectors) ?? null; + if ( + connector && + hideConnectorServiceNowSir && + connector.actionTypeId === ConnectorTypes.serviceNowSIR + ) { + connector = null; + } return ( ); }; -const ConnectorComponent: React.FC = ({ isLoading }) => { +const ConnectorComponent: React.FC = ({ hideConnectorServiceNowSir = false, isLoading }) => { const { getFields } = useFormContext(); const { loading: isLoadingConnectors, connectors } = useConnectors(); const handleConnectorChange = useCallback( @@ -61,6 +75,7 @@ const ConnectorComponent: React.FC = ({ isLoading }) => { componentProps={{ connectors, handleChange: handleConnectorChange, + hideConnectorServiceNowSir, dataTestSubj: 'caseConnectors', disabled: isLoading || isLoadingConnectors, idAria: 'caseConnectors', @@ -74,6 +89,7 @@ const ConnectorComponent: React.FC = ({ isLoading }) => { component={ConnectorFields} componentProps={{ connectors, + hideConnectorServiceNowSir, isEdit: true, }} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx index f5b113ae8e26f3..09518c6f6adc11 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form.tsx @@ -36,78 +36,84 @@ const MySpinner = styled(EuiLoadingSpinner)` `; interface Props { + hideConnectorServiceNowSir?: boolean; withSteps?: boolean; } -export const CreateCaseForm: React.FC = React.memo(({ withSteps = true }) => { - const { isSubmitting } = useFormContext(); +export const CreateCaseForm: React.FC = React.memo( + ({ hideConnectorServiceNowSir = false, withSteps = true }) => { + const { isSubmitting } = useFormContext(); - const firstStep = useMemo( - () => ({ - title: i18n.STEP_ONE_TITLE, - children: ( - <> - + const firstStep = useMemo( + () => ({ + title: i18n.STEP_ONE_TITLE, + children: ( + <> + <Title isLoading={isSubmitting} /> + <Container> + <Tags isLoading={isSubmitting} /> + </Container> + <Container big> + <Description isLoading={isSubmitting} /> + </Container> + </> + ), + }), + [isSubmitting] + ); + + const secondStep = useMemo( + () => ({ + title: i18n.STEP_TWO_TITLE, + children: ( <Container> - <Tags isLoading={isSubmitting} /> - </Container> - <Container big> - <Description isLoading={isSubmitting} /> + <SyncAlertsToggle isLoading={isSubmitting} /> </Container> - </> - ), - }), - [isSubmitting] - ); - - const secondStep = useMemo( - () => ({ - title: i18n.STEP_TWO_TITLE, - children: ( - <Container> - <SyncAlertsToggle isLoading={isSubmitting} /> - </Container> - ), - }), - [isSubmitting] - ); + ), + }), + [isSubmitting] + ); - const thirdStep = useMemo( - () => ({ - title: i18n.STEP_THREE_TITLE, - children: ( - <Container> - <Connector isLoading={isSubmitting} /> - </Container> - ), - }), - [isSubmitting] - ); + const thirdStep = useMemo( + () => ({ + title: i18n.STEP_THREE_TITLE, + children: ( + <Container> + <Connector + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + isLoading={isSubmitting} + /> + </Container> + ), + }), + [hideConnectorServiceNowSir, isSubmitting] + ); - const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [ - firstStep, - secondStep, - thirdStep, - ]); + const allSteps = useMemo(() => [firstStep, secondStep, thirdStep], [ + firstStep, + secondStep, + thirdStep, + ]); - return ( - <> - {isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} - {withSteps ? ( - <EuiSteps - headingElement="h2" - steps={allSteps} - data-test-subj={'case-creation-form-steps'} - /> - ) : ( - <> - {firstStep.children} - {secondStep.children} - {thirdStep.children} - </> - )} - </> - ); -}); + return ( + <> + {isSubmitting && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} + {withSteps ? ( + <EuiSteps + headingElement="h2" + steps={allSteps} + data-test-subj={'case-creation-form-steps'} + /> + ) : ( + <> + {firstStep.children} + {secondStep.children} + {thirdStep.children} + </> + )} + </> + ); + } +); CreateCaseForm.displayName = 'CreateCaseForm'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index 26203d7268fd38..f56dcafdc95e4d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -19,7 +19,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service' import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { Case } from '../../containers/types'; -import { CaseType } from '../../../../../case/common/api'; +import { CaseType, ConnectorTypes } from '../../../../../case/common/api'; const initialCaseValue: FormProps = { description: '', @@ -31,29 +31,40 @@ const initialCaseValue: FormProps = { }; interface Props { + afterCaseCreated?: (theCase: Case) => Promise<void>; caseType?: CaseType; + hideConnectorServiceNowSir?: boolean; onSuccess?: (theCase: Case) => Promise<void>; - afterCaseCreated?: (theCase: Case) => Promise<void>; } export const FormContext: React.FC<Props> = ({ + afterCaseCreated, caseType = CaseType.individual, children, + hideConnectorServiceNowSir, onSuccess, - afterCaseCreated, }) => { const { connectors } = useConnectors(); const { connector: configurationConnector } = useCaseConfigure(); const { postCase } = usePostCase(); const { pushCaseToExternalService } = usePostPushToService(); - const connectorId = useMemo( - () => - connectors.some((connector) => connector.id === configurationConnector.id) - ? configurationConnector.id - : 'none', - [configurationConnector.id, connectors] - ); + const connectorId = useMemo(() => { + if ( + hideConnectorServiceNowSir && + configurationConnector.type === ConnectorTypes.serviceNowSIR + ) { + return 'none'; + } + return connectors.some((connector) => connector.id === configurationConnector.id) + ? configurationConnector.id + : 'none'; + }, [ + configurationConnector.id, + configurationConnector.type, + connectors, + hideConnectorServiceNowSir, + ]); const submitCase = useCallback( async ( diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx index 34dcacaf42a981..d0f478dc17f81f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx @@ -34,7 +34,6 @@ import * as i18n from './translations'; interface EditConnectorProps { caseFields: ConnectorTypeFields['fields']; connectors: ActionConnector[]; - disabled?: boolean; isLoading: boolean; onSubmit: ( connectorId: string, @@ -44,6 +43,8 @@ interface EditConnectorProps { ) => void; selectedConnector: string; userActions: CaseUserActions[]; + disabled?: boolean; + hideConnectorServiceNowSir?: boolean; } const MyFlexGroup = styled(EuiFlexGroup)` @@ -105,6 +106,7 @@ export const EditConnector = React.memo( caseFields, connectors, disabled = false, + hideConnectorServiceNowSir = false, isLoading, onSubmit, selectedConnector, @@ -234,6 +236,7 @@ export const EditConnector = React.memo( dataTestSubj: 'caseConnectors', defaultValue: selectedConnector, disabled, + hideConnectorServiceNowSir, idAria: 'caseConnectors', isEdit: editConnector, isLoading, diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx index 3e11ee526839cd..b1edaa56cd3482 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -21,6 +21,7 @@ export interface CreateCaseModalProps { onCloseCaseModal: () => void; onSuccess: (theCase: Case) => Promise<void>; caseType?: CaseType; + hideConnectorServiceNowSir?: boolean; } const Container = styled.div` @@ -35,6 +36,7 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({ onCloseCaseModal, onSuccess, caseType = CaseType.individual, + hideConnectorServiceNowSir = false, }) => { return isModalOpen ? ( <EuiModal onClose={onCloseCaseModal} data-test-subj="all-cases-modal"> @@ -42,8 +44,15 @@ const CreateModalComponent: React.FC<CreateCaseModalProps> = ({ <EuiModalHeaderTitle>{i18n.CREATE_TITLE}</EuiModalHeaderTitle> </EuiModalHeader> <EuiModalBody> - <FormContext caseType={caseType} onSuccess={onSuccess}> - <CreateCaseForm withSteps={false} /> + <FormContext + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + caseType={caseType} + onSuccess={onSuccess} + > + <CreateCaseForm + withSteps={false} + hideConnectorServiceNowSir={hideConnectorServiceNowSir} + /> <Container> <SubmitCaseButton /> </Container> diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx index 1cef63ae9cfbf8..50887f08dee6e4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -13,6 +13,7 @@ import { CreateCaseModal } from './create_case_modal'; export interface UseCreateCaseModalProps { onCaseCreated: (theCase: Case) => void; caseType?: CaseType; + hideConnectorServiceNowSir?: boolean; } export interface UseCreateCaseModalReturnedValues { modal: JSX.Element; @@ -24,6 +25,7 @@ export interface UseCreateCaseModalReturnedValues { export const useCreateCaseModal = ({ caseType = CaseType.individual, onCaseCreated, + hideConnectorServiceNowSir = false, }: UseCreateCaseModalProps) => { const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const closeModal = useCallback(() => setIsModalOpen(false), []); @@ -41,6 +43,7 @@ export const useCreateCaseModal = ({ modal: ( <CreateCaseModal caseType={caseType} + hideConnectorServiceNowSir={hideConnectorServiceNowSir} isModalOpen={isModalOpen} onCloseCaseModal={closeModal} onSuccess={onSuccess} @@ -50,7 +53,7 @@ export const useCreateCaseModal = ({ closeModal, openModal, }), - [caseType, closeModal, isModalOpen, onSuccess, openModal] + [caseType, closeModal, hideConnectorServiceNowSir, isModalOpen, onSuccess, openModal] ); return state; diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index c87e210b42bc05..01ef040aa19cda 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -15,6 +15,7 @@ import { CasesResponse, CasesStatusResponse, CaseStatuses, + CaseType, CaseUserActionsResponse, CommentRequest, CommentType, @@ -165,6 +166,7 @@ export const getSubCaseUserActions = async ( export const getCases = async ({ filterOptions = { + onlyCollectionType: false, search: '', reporters: [], status: CaseStatuses.open, @@ -183,6 +185,7 @@ export const getCases = async ({ tags: filterOptions.tags.map((t) => `"${t.replace(/"/g, '\\"')}"`), status: filterOptions.status, ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), + ...(filterOptions.onlyCollectionType ? { type: CaseType.collection } : {}), ...queryParams, }; const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, { diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index d2931a790bd798..399d8d43ce0655 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -99,6 +99,7 @@ export interface FilterOptions { status: CaseStatuses; tags: string[]; reporters: User[]; + onlyCollectionType?: boolean; } export interface CasesStatus { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx index c83cc02dedb977..f2e8e280bf158c 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx @@ -97,6 +97,7 @@ export const DEFAULT_FILTER_OPTIONS: FilterOptions = { reporters: [], status: CaseStatuses.open, tags: [], + onlyCollectionType: false, }; export const DEFAULT_QUERY_PARAMS: QueryParams = { @@ -129,10 +130,13 @@ export interface UseGetCases extends UseGetCasesState { setSelectedCases: (mySelectedCases: Case[]) => void; } -export const useGetCases = (initialQueryParams?: QueryParams): UseGetCases => { +export const useGetCases = ( + initialQueryParams?: QueryParams, + initialFilterOptions?: FilterOptions +): UseGetCases => { const [state, dispatch] = useReducer(dataFetchReducer, { data: initialData, - filterOptions: DEFAULT_FILTER_OPTIONS, + filterOptions: initialFilterOptions ?? DEFAULT_FILTER_OPTIONS, isError: false, loading: [], queryParams: initialQueryParams ?? DEFAULT_QUERY_PARAMS, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx index 7563c8d8f99f07..5dbe1f1cef5be7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.test.tsx @@ -8,10 +8,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { RuleActionsField } from './index'; +import { getSupportedActions, RuleActionsField } from './index'; import { useForm, Form } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; import { useFormFieldMock } from '../../../../common/mock'; +import { ActionType } from '../../../../../../actions/common'; jest.mock('../../../../common/lib/kibana'); describe('RuleActionsField', () => { @@ -45,7 +46,11 @@ describe('RuleActionsField', () => { return ( <Form form={form}> - <RuleActionsField field={field} messageVariables={messageVariables} /> + <RuleActionsField + field={field} + messageVariables={messageVariables} + hasErrorOnCreationCaseAction={false} + /> </Form> ); }; @@ -53,4 +58,63 @@ describe('RuleActionsField', () => { expect(wrapper.dive().find('ActionForm')).toHaveLength(0); }); + + describe('#getSupportedActions', () => { + const actions: ActionType[] = [ + { + id: '.jira', + name: 'My Jira', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + minimumLicenseRequired: 'gold', + }, + { + id: '.case', + name: 'Cases', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]; + + it('if we have an error on case action creation, we do not support case connector', () => { + expect(getSupportedActions(actions, true)).toMatchInlineSnapshot(` + Array [ + Object { + "enabled": true, + "enabledInConfig": false, + "enabledInLicense": true, + "id": ".jira", + "minimumLicenseRequired": "gold", + "name": "My Jira", + }, + ] + `); + }); + + it('if we do NOT have an error on case action creation, we are supporting case connector', () => { + expect(getSupportedActions(actions, false)).toMatchInlineSnapshot(` + Array [ + Object { + "enabled": true, + "enabledInConfig": false, + "enabledInLicense": true, + "id": ".jira", + "minimumLicenseRequired": "gold", + "name": "My Jira", + }, + Object { + "enabled": true, + "enabledInConfig": false, + "enabledInLicense": true, + "id": ".case", + "minimumLicenseRequired": "basic", + "name": "Cases", + }, + ] + `); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index cee85df5db4367..9fd9e910ee0f8f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -26,6 +26,7 @@ import { FORM_ERRORS_TITLE } from './translations'; interface Props { field: FieldHook; + hasErrorOnCreationCaseAction: boolean; messageVariables: ActionVariables; } @@ -39,7 +40,44 @@ const FieldErrorsContainer = styled.div` } `; -export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) => { +const ContainerActions = styled.div.attrs( + ({ className = '', $caseIndexes = [] }: { className?: string; $caseIndexes: string[] }) => ({ + className, + }) +)<{ $caseIndexes: string[] }>` + ${({ $caseIndexes }) => + $caseIndexes.map( + (index) => ` + div[id="${index}"].euiAccordion__childWrapper .euiAccordion__padding--l { + padding: 0px; + .euiFlexGroup { + display: none; + } + .euiSpacer.euiSpacer--xl { + height: 0px; + } + } + ` + )} +`; + +export const getSupportedActions = ( + actionTypes: ActionType[], + hasErrorOnCreationCaseAction: boolean +): ActionType[] => { + return actionTypes.filter((actionType) => { + if (actionType.id === '.case' && hasErrorOnCreationCaseAction) { + return false; + } + return NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id); + }); +}; + +export const RuleActionsField: React.FC<Props> = ({ + field, + hasErrorOnCreationCaseAction, + messageVariables, +}) => { const [fieldErrors, setFieldErrors] = useState<string | null>(null); const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>(); const form = useFormContext(); @@ -54,6 +92,17 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) = [field.value] ); + const caseActionIndexes = useMemo( + () => + actions.reduce<string[]>((acc, action, actionIndex) => { + if (action.actionTypeId === '.case') { + return [...acc, `${actionIndex}`]; + } + return acc; + }, []), + [actions] + ); + const setActionIdByIndex = useCallback( (id: string, index: number) => { const updatedActions = [...(actions as Array<Partial<AlertAction>>)]; @@ -83,13 +132,11 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) = useEffect(() => { (async function () { const actionTypes = await loadActionTypes({ http }); - const supportedTypes = actionTypes.filter((actionType) => - NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id) - ); + const supportedTypes = getSupportedActions(actionTypes, hasErrorOnCreationCaseAction); setSupportedActionTypes(supportedTypes); })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [hasErrorOnCreationCaseAction]); useEffect(() => { if (isSubmitting || !field.errors.length) { @@ -104,7 +151,7 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) = if (!supportedActionTypes) return <></>; return ( - <> + <ContainerActions $caseIndexes={caseActionIndexes}> {fieldErrors ? ( <> <FieldErrorsContainer> @@ -126,6 +173,6 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) = actionTypes={supportedActionTypes} defaultActionMessage={DEFAULT_ACTION_MESSAGE} /> - </> + </ContainerActions> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 30898cdeca4a3b..a31371c31cbbb5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -36,6 +36,7 @@ import { useKibana } from '../../../../common/lib/kibana'; import { getSchema } from './schema'; import * as I18n from './translations'; import { APP_ID } from '../../../../../common/constants'; +import { useManageCaseAction } from './use_manage_case_action'; interface StepRuleActionsProps extends RuleStepProps { defaultValues?: ActionsStepRule | null; @@ -70,6 +71,7 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({ setForm, actionMessageParams, }) => { + const [isLoadingCaseAction, hasErrorOnCreationCaseAction] = useManageCaseAction(); const { services: { application, @@ -138,13 +140,14 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({ () => ({ idAria: 'detectionEngineStepRuleActionsThrottle', isDisabled: isLoading, + isLoading: isLoadingCaseAction, dataTestSubj: 'detectionEngineStepRuleActionsThrottle', hasNoInitialSelection: false, euiFieldProps: { options: throttleOptions, }, }), - [isLoading, throttleOptions] + [isLoading, isLoadingCaseAction, throttleOptions] ); const displayActionsOptions = useMemo( @@ -157,13 +160,14 @@ const StepRuleActionsComponent: FC<StepRuleActionsProps> = ({ component={RuleActionsField} componentProps={{ messageVariables: actionMessageParams, + hasErrorOnCreationCaseAction, }} /> </> ) : ( <UseField path="actions" component={GhostFormField} /> ), - [throttle, actionMessageParams] + [throttle, actionMessageParams, hasErrorOnCreationCaseAction] ); // only display the actions dropdown if the user has "read" privileges for actions const displayActionsDropDown = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx new file mode 100644 index 00000000000000..55b2aefe213106 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useRef, useState } from 'react'; +import { ACTION_URL } from '../../../../../../case/common/constants'; +import { KibanaServices } from '../../../../common/lib/kibana'; + +interface CaseAction { + actionTypeId: string; + id: string; + isPreconfigured: boolean; + name: string; + referencedByCount: number; +} + +const CASE_ACTION_NAME = 'Cases'; + +export const useManageCaseAction = () => { + const hasInit = useRef(true); + const [loading, setLoading] = useState(true); + const [hasError, setHasError] = useState(false); + + useEffect(() => { + const abortCtrl = new AbortController(); + const fetchActions = async () => { + try { + const actions = await KibanaServices.get().http.fetch<CaseAction[]>(ACTION_URL, { + method: 'GET', + signal: abortCtrl.signal, + }); + if (!actions.some((a) => a.actionTypeId === '.case' && a.name === CASE_ACTION_NAME)) { + await KibanaServices.get().http.post<CaseAction[]>(`${ACTION_URL}/action`, { + method: 'POST', + body: JSON.stringify({ + actionTypeId: '.case', + config: {}, + name: CASE_ACTION_NAME, + secrets: {}, + }), + signal: abortCtrl.signal, + }); + } + setLoading(false); + } catch { + setLoading(false); + setHasError(true); + } + }; + if (hasInit.current) { + hasInit.current = false; + fetchActions(); + } + + return () => { + abortCtrl.abort(); + }; + }, []); + return [loading, hasError]; +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 472ecc3f801644..cbc629c556bf56 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17260,7 +17260,6 @@ "xpack.securitySolution.case.common.noConnector": "コネクターを選択していません", "xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "ケース", "xpack.securitySolution.case.components.connectors.case.addNewCaseOption": "新規ケースの追加", - "xpack.securitySolution.case.components.connectors.case.callOutInfo": "ルールを作成した後のすべてのアラートは、選択したケースの最後に追加されます。", "xpack.securitySolution.case.components.connectors.case.caseRequired": "ケースの選択が必要です。", "xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "ケースを選択", "xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "ケース", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a72c83b736a6d0..938086fb6788ac 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17303,7 +17303,6 @@ "xpack.securitySolution.case.common.noConnector": "未选择任何连接器", "xpack.securitySolution.case.components.connectors.case.actionTypeTitle": "案例", "xpack.securitySolution.case.components.connectors.case.addNewCaseOption": "添加新案例", - "xpack.securitySolution.case.components.connectors.case.callOutInfo": "规则创建后的所有告警将追加到选定案例。", "xpack.securitySolution.case.components.connectors.case.caseRequired": "必须选择策略。", "xpack.securitySolution.case.components.connectors.case.casesDropdownPlaceholder": "选择案例", "xpack.securitySolution.case.components.connectors.case.casesDropdownRowLabel": "案例", From 9fd352b4bd3fea2f80fb9a52dfb9b7c2a40c6aff Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper <Zacqary@users.noreply.github.com> Date: Tue, 2 Mar 2021 16:21:55 -0600 Subject: [PATCH 23/63] [Alerts] Add spaces as optional dep to triggers_actions_ui (#93267) (#93305) # Conflicts: # x-pack/plugins/triggers_actions_ui/public/plugin.ts --- x-pack/plugins/triggers_actions_ui/kibana.json | 2 +- x-pack/plugins/triggers_actions_ui/public/application/app.tsx | 3 +++ x-pack/plugins/triggers_actions_ui/public/plugin.ts | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 0487c58e662692..2938d94bf426a7 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["alerts", "features", "home"], + "optionalPlugins": ["alerts", "features", "home", "spaces"], "requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 0a59cff98ce26b..0afb81031e3bc7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -17,6 +17,8 @@ import { ActionTypeRegistryContract, AlertTypeRegistryContract } from '../types' import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerts/public'; +import type { SpacesPluginStart } from '../../../spaces/public'; + import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; @@ -33,6 +35,7 @@ export interface TriggersAndActionsUiServices extends CoreStart { data: DataPublicPluginStart; charts: ChartsPluginStart; alerts?: AlertingStart; + spaces?: SpacesPluginStart; storage?: Storage; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; actionTypeRegistry: ActionTypeRegistryContract; diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 65ab69d515f34a..14436fa99b6ea1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -28,6 +28,8 @@ import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; import type { ConnectorAddFlyoutProps } from './application/sections/action_connector_form/connector_add_flyout'; import type { ConnectorEditFlyoutProps } from './application/sections/action_connector_form/connector_edit_flyout'; +import type { SpacesPluginStart } from '../../spaces/public'; + import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; @@ -66,6 +68,7 @@ interface PluginsStart { data: DataPublicPluginStart; charts: ChartsPluginStart; alerts?: AlertingStart; + spaces?: SpacesPluginStart; navigateToApp: CoreStart['application']['navigateToApp']; features: FeaturesPluginStart; } @@ -140,6 +143,7 @@ export class Plugin data: pluginsStart.data, charts: pluginsStart.charts, alerts: pluginsStart.alerts, + spaces: pluginsStart.spaces, element: params.element, storage: new Storage(window.localStorage), setBreadcrumbs: params.setBreadcrumbs, From 37c0dd0eb147e5c0778dfed6d7b21fdb4d3f0625 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez <melissa.alvarez@elastic.co> Date: Tue, 2 Mar 2021 17:51:41 -0500 Subject: [PATCH 24/63] ensure regex works for filter with optional multiple spaces (#93071) (#93314) --- .../components/exploration_query_bar/exploration_query_bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx index dd451958e26465..3ef6dbc292c080 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_query_bar/exploration_query_bar.tsx @@ -60,7 +60,7 @@ export const ExplorationQueryBar: FC<ExplorationQueryBarProps> = ({ const searchChangeHandler = (q: Query) => setSearchInput(q); - const regex = useMemo(() => new RegExp(`${filters?.columnId}\s?:\s?(true|false)`, 'g'), [ + const regex = useMemo(() => new RegExp(`${filters?.columnId}\\s*:\\s*(true|false)`, 'g'), [ filters?.columnId, ]); From a4011baa9f21ab1e9c0fc2d9664d61491740f01b Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko <jo.naumenko@gmail.com> Date: Tue, 2 Mar 2021 15:16:30 -0800 Subject: [PATCH 25/63] [Alerting][Docs] Changed alerting documentation to point to a single source of explaining the configurations. (#92942) (#93167) * [Alerting][Docs] Changed alerting documentation to poin to a single source of explaining the configurations. * fixed due to comments * fixed due to comments * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * fixed due to comments Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/settings/alert-action-settings.asciidoc | 20 +++++++++++++++++ .../pre-configured-connectors.asciidoc | 2 +- .../alerting-getting-started.asciidoc | 2 +- ...lerting-production-considerations.asciidoc | 8 +++---- docs/user/alerting/defining-alerts.asciidoc | 22 +------------------ x-pack/plugins/triggers_actions_ui/README.md | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index d6f5fb1baba8eb..6813a77776b5ba 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -40,6 +40,8 @@ You can configure the following settings in the `kibana.yml` file. [cols="2*<"] |=== +| `xpack.actions.enabled` + | Feature toggle that enables Actions in {kib}. Defaults to `true`. | `xpack.actions.allowedHosts` {ess-icon} | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly added to the allowed hosts. An empty list `[]` can be used to block built-in actions from making any external connections. + @@ -51,6 +53,24 @@ You can configure the following settings in the `kibana.yml` file. + Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. +| `xpack.actions.preconfigured` + | Specifies preconfigured action IDs and configs. Defaults to {}. + +| `xpack.actions.proxyUrl` {ess-icon} + | Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used. + +| `xpack.actions.proxyHeaders` {ess-icon} + | Specifies HTTP headers for the proxy, if using a proxy for actions. Defaults to {}. + +a|`xpack.actions.` +`proxyRejectUnauthorizedCertificates` {ess-icon} + | Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Defaults to `true`. + +| `xpack.actions.rejectUnauthorized` {ess-icon} + | Set to `false` to bypass certificate validation for actions. Defaults to `true`. + + + + As an alternative to setting both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, you can point the OS level environment variable `NODE_EXTRA_CA_CERTS` to a file that contains the root CAs needed to trust certificates. + |=== [float] diff --git a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc index 722607ac05f87b..a748a06398ef3a 100644 --- a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc @@ -95,7 +95,7 @@ This example shows a preconfigured action type with one out-of-the box connector name: 'Server log #xyz' ``` -<1> `enabledActionTypes` excludes the preconfigured action type to prevent creating and deleting connectors. +<1> `enabledActionTypes` prevents the preconfigured action type from creating and deleting connectors. For more details, check <<action-settings, Action settings>>. <2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type. [[managing-pre-configured-action-types]] diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 8a83a0f8799dea..6c6e7e6305c81c 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -157,7 +157,7 @@ Pre-packaged *alert types* simplify setup, hide the details complex domain-speci If you are using an *on-premises* Elastic Stack deployment: -* In the kibana.yml configuration file, add the <<alert-action-settings-kb,`xpack.encryptedSavedObjects.encryptionKey`>> setting. +* In the kibana.yml configuration file, add the <<general-alert-action-settings,`xpack.encryptedSavedObjects.encryptionKey`>> setting. * For emails to have a footer with a link back to {kib}, set the <<server-publicBaseUrl, `server.publicBaseUrl`>> configuration setting. If you are using an *on-premises* Elastic Stack deployment with <<using-kibana-with-security, *security*>>: diff --git a/docs/user/alerting/alerting-production-considerations.asciidoc b/docs/user/alerting/alerting-production-considerations.asciidoc index 0442b760669cc6..58b4a263459b32 100644 --- a/docs/user/alerting/alerting-production-considerations.asciidoc +++ b/docs/user/alerting/alerting-production-considerations.asciidoc @@ -2,9 +2,9 @@ [[alerting-production-considerations]] == Production considerations -{kib} alerting run both alert checks and actions as persistent background tasks managed by the Kibana Task Manager. This has two major benefits: +{kib} alerting runs both alert checks and actions as persistent background tasks managed by the Kibana Task Manager. This has two major benefits: -* *Persistence*: all task state and scheduling is stored in {es}, so if {kib} is restarted, alerts and actions will pick up where they left off. Task definitions for alerts and actions are stored in the index specified by `xpack.task_manager.index` (defaults to `.kibana_task_manager`). It is important to have at least 1 replica of this index for production deployments, since if you lose this index all scheduled alerts and actions are also lost. +* *Persistence*: all task state and scheduling is stored in {es}, so if you restart {kib}, alerts and actions will pick up where they left off. Task definitions for alerts and actions are stored in the index specified by <<task-manager-settings, `xpack.task_manager.index`>>. The default is `.kibana_task_manager`. You must have at least one replica of this index for production deployments. If you lose this index, all scheduled alerts and actions are lost. * *Scaling*: multiple {kib} instances can read from and update the same task queue in {es}, allowing the alerting and action load to be distributed across instances. In cases where a {kib} instance no longer has capacity to run alert checks or actions, capacity can be increased by adding additional {kib} instances. [float] @@ -12,7 +12,7 @@ {kib} background tasks are managed by: -* Polling an {es} task index for overdue tasks at 3 second intervals. This interval can be changed using the `xpack.task_manager.poll_interval` setting. +* Polling an {es} task index for overdue tasks at 3 second intervals. You can change this interval using the <<task-manager-settings, `xpack.task_manager.poll_interval`>> setting. * Tasks are then claiming them by updating them in the {es} index, using optimistic concurrency control to prevent conflicts. Each {kib} instance can run a maximum of 10 concurrent tasks, so a maximum of 10 tasks are claimed each interval. * Tasks are run on the {kib} server. * In the case of alerts which are recurring background checks, upon completion the task is scheduled again according to the <<defining-alerts-general-details, check interval>>. @@ -32,4 +32,4 @@ For details on the settings that can influence the performance and throughput of [float] === Deployment considerations -{es} and {kib} instances use the system clock to determine the current time. To ensure schedules are triggered when expected, you should synchronize the clocks of all nodes in the cluster using a time service such as http://www.ntp.org/[Network Time Protocol]. \ No newline at end of file +{es} and {kib} instances use the system clock to determine the current time. To ensure schedules are triggered when expected, you should synchronize the clocks of all nodes in the cluster using a time service such as http://www.ntp.org/[Network Time Protocol]. diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 396896754f2b02..8c8e25cea407a0 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -101,29 +101,9 @@ image::images/alert-flyout-add-action.png[You can add multiple actions on an ale [NOTE] ============================================== -Actions are not required on alerts. In some cases you may want to run an alert without actions first to understand its behavior, and configure actions later. +Actions are not required on alerts. You can run an alert without actions to understand its behavior, and then <<action-settings, configure actions>> later. ============================================== -[float] -[[actions-configuration]] -=== Global actions configuration -Some actions configuration options apply to all actions. -If you are using an *on-prem* Elastic Stack deployment, you can set these in the kibana.yml file. -If you are using a cloud deployment, you can set these via the console. - -Here's a list of the available global configuration options and an explanation of what each one does: - -* `xpack.actions.enabled`: Feature toggle that enables Actions in {kib}. Default: `true` -* `xpack.actions.allowedHosts`: Specifies an array of host names which actions such as email, Slack, PagerDuty, and webhook can connect to. An element of * indicates any host can be connected to. An empty array indicates no hosts can be connected to. Default: [ {asterisk} ] -* `xpack.actions.enabledActionTypes`: Specifies an array of action types that are enabled. An {asterisk} indicates all action types registered are enabled. The action types that {kib} provides are `.email`, `.index`, `.jira`, `.pagerduty`, `.resilient`, `.server-log`, `.servicenow`, `.servicenow-sir`, `.slack`, `.teams`, and `.webhook`. Default: [ {asterisk} ] -* `xpack.actions.preconfigured`: Specifies preconfigured action IDs and configs. Default: {} -* `xpack.actions.proxyUrl`: Specifies the proxy URL to use, if using a proxy for actions. -* `xpack.actions.proxyHeader`: Specifies HTTP headers for proxy, if using a proxy for actions. -* `xpack.actions.proxyRejectUnauthorizedCertificates`: Set to `false` to bypass certificate validation for proxy, if using a proxy for actions. -* `xpack.actions.rejectUnauthorized`: Set to `false` to bypass certificate validation for actions. - -*NOTE:* As an alternative to both `xpack.actions.proxyRejectUnauthorizedCertificates` and `xpack.actions.rejectUnauthorized`, the OS level environment variable `NODE_EXTRA_CA_CERTS` can be set to point to a file that contains the root CA(s) needed for certificates to be trusted. - [float] === Managing alerts diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 37f605b0d500fb..163f5bcb7b8f1d 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -1149,7 +1149,7 @@ triggersActionsUi.actionTypeRegistry.register(getSomeNewActionType()); ## Create and register new action type UI -Before starting the UI implementation, the [server side registration](https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions#kibana-actions-configuration) should be done first. +Before starting the UI implementation, the [server side registration](https://github.com/elastic/kibana/tree/master/x-pack/plugins/actions#action-types) should be done first. Action type UI is expected to be defined as `ActionTypeModel` object. From 4a7ef3f33368ed655e6d13352dc6f8b5adf7e59e Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Tue, 2 Mar 2021 19:08:36 -0500 Subject: [PATCH 26/63] chore(NA): do not include fs within a storybook build (#93294) (#93326) Co-authored-by: Tiago Costa <tiagoffcc@hotmail.com> --- packages/kbn-storybook/lib/default_config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index 1b049761a3a985..e194c9789daab8 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -19,6 +19,9 @@ export const defaultConfig: StorybookConfig = { config.parallelism = 4; config.cache = true; } + + config.node = { fs: 'empty' }; + return config; }, }; From 0398d35a24dac7f97cc1be2ed0e1035587cd7b4b Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko <jo.naumenko@gmail.com> Date: Tue, 2 Mar 2021 16:18:31 -0800 Subject: [PATCH 27/63] [Alerting][Docs] Moved alerting links from hard-coded to documentation link service. (#92953) (#93322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Alerting][Docs] Moved alerting links from hard-coded to documentation link service * fixed due to comments * Update x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx Co-authored-by: Mike Côté <mikecote@users.noreply.github.com> * fixed jest tests * fixed due to comments Co-authored-by: Mike Côté <mikecote@users.noreply.github.com> Co-authored-by: Mike Côté <mikecote@users.noreply.github.com> --- .../public/doc_links/doc_links_service.ts | 10 ++-- .../alert_types/es_query/expression.test.tsx | 5 ++ .../alert_types/es_query/expression.tsx | 5 +- .../public/alert_types/es_query/index.ts | 4 +- .../public/alert_types/threshold/index.ts | 4 +- .../email/email_connector.tsx | 5 +- .../es_index/es_index_connector.tsx | 5 +- .../es_index/es_index_params.tsx | 5 +- .../pagerduty/pagerduty_connectors.tsx | 5 +- .../servicenow/servicenow_connectors.tsx | 5 +- .../slack/slack_connectors.tsx | 5 +- .../teams/teams_connectors.tsx | 5 +- .../application/components/health_check.tsx | 50 ++++--------------- .../public/application/home.tsx | 2 +- .../action_connector_form.tsx | 5 +- .../connector_edit_flyout.tsx | 5 +- 16 files changed, 33 insertions(+), 92 deletions(-) diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 9de24a6b9e33c6..bdc0724eae9af9 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -184,15 +184,17 @@ export class DocLinksService { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/managing-alerts-and-actions.html`, actionTypes: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/action-types.html`, emailAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html`, + emailActionConfig: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/email-action-type.html#configuring-email`, generalSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`, indexAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-action-type.html`, - indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-types.html#alert-type-index-threshold`, + esQuery: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-type-es-query.html`, + indexThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-type-index-threshold.html#index-action-configuration`, pagerDutyAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pagerduty-action-type.html`, preconfiguredConnectors: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/pre-configured-action-types-and-connectors.html`, - serviceNowAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/servicenow-action-type.html`, + serviceNowAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/servicenow-action-type.html#configuring-servicenow`, setupPrerequisites: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`, - slackAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/slack-action-type.html`, - teamsAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/teams-action-type.html`, + slackAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/slack-action-type.html#configuring-slack`, + teamsAction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/teams-action-type.html#configuring-teams`, }, maps: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`, diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx index f475d97e2f39d3..51c2f0471d486f 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx @@ -116,6 +116,11 @@ describe('EsQueryAlertTypeExpression', () => { docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '', + links: { + query: { + queryDsl: 'query-dsl.html', + }, + }, }, }, }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx index 37c64688ec49af..6adcada9b273af 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx @@ -287,10 +287,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< isInvalid={errors.esQuery.length > 0} error={errors.esQuery} helpText={ - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/query-dsl.html`} - target="_blank" - > + <EuiLink href={docLinks.links.query.queryDsl} target="_blank"> <FormattedMessage id="xpack.stackAlerts.esQuery.ui.queryPrompt.help" defaultMessage="ES Query DSL documentation" diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts index cec19aacaa93c7..6d42ee714a222f 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/index.ts @@ -18,9 +18,7 @@ export function getAlertType(): AlertTypeModel<EsQueryAlertParams> { defaultMessage: 'Alert on matches against an ES query.', }), iconClass: 'logoElastic', - documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/alert-types.html#alert-type-es-query`; - }, + documentationUrl: (docLinks) => docLinks.links.alerting.esQuery, alertParamsExpression: lazy(() => import('./expression')), validate: validateExpression, defaultActionMessage: i18n.translate( diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts index 95ecd121cf5a28..1b229bb4a9d0a1 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts @@ -18,9 +18,7 @@ export function getAlertType(): AlertTypeModel<IndexThresholdAlertParams> { defaultMessage: 'Alert when an aggregated query meets the threshold.', }), iconClass: 'alert', - documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/alert-types.html#alert-type-index-threshold`; - }, + documentationUrl: (docLinks) => docLinks.links.alerting.indexThreshold, alertParamsExpression: lazy(() => import('./expression')), validate: validateExpression, defaultActionMessage: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx index 2f518780c9f6bf..df6822c85340a7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -55,10 +55,7 @@ export const EmailActionConnectorFields: React.FunctionComponent< } )} helpText={ - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/email-action-type.html#configuring-email`} - target="_blank" - > + <EuiLink href={docLinks.links.alerting.emailActionConfig} target="_blank"> <FormattedMessage id="xpack.triggersActionsUI.components.builtinActionTypes.emailAction.configureAccountsHelpLabel" defaultMessage="Configure email accounts" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx index f377209ca00d3d..cd3a03ecce15c7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx @@ -88,10 +88,7 @@ const IndexActionConnectorFields: React.FunctionComponent< defaultMessage="Use * to broaden your query." /> <EuiSpacer size="s" /> - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/index-action-type.html`} - target="_blank" - > + <EuiLink href={docLinks.links.alerting.indexAction} target="_blank"> <FormattedMessage id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.configureIndexHelpLabel" defaultMessage="Configuring index connector." diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx index f01521b9424b94..c65c76ee6916e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -61,10 +61,7 @@ export const IndexParamsFields = ({ errors={errors.documents as string[]} onDocumentsChange={onDocumentsChange} helpText={ - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/index-action-type.html#index-action-configuration`} - target="_blank" - > + <EuiLink href={docLinks.links.alerting.indexAction} target="_blank"> <FormattedMessage id="xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel" defaultMessage="Index document example." diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx index 46d2b4b87d7187..60935684527e53 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx @@ -51,10 +51,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent< id="routingKey" fullWidth helpText={ - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/pagerduty-action-type.html`} - target="_blank" - > + <EuiLink href={docLinks.links.alerting.pagerDutyAction} target="_blank"> <FormattedMessage id="xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyNameHelpLabel" defaultMessage="Configure a PagerDuty account" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index 57c0ee3fd3f9de..4e0db7f2054c68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -60,10 +60,7 @@ const ServiceNowConnectorFields: React.FC< isInvalid={isApiUrlInvalid} label={i18n.API_URL_LABEL} helpText={ - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/servicenow-action-type.html#configuring-servicenow`} - target="_blank" - > + <EuiLink href={docLinks.links.alerting.serviceNowAction} target="_blank"> <FormattedMessage id="xpack.triggersActionsUI.components.builtinActionTypes.serviceNowAction.apiUrlHelpLabel" defaultMessage="Configure a Personal Developer Instance" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx index 202b87315739a8..fb0275b92bad89 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -25,10 +25,7 @@ const SlackActionFields: React.FunctionComponent< id="webhookUrl" fullWidth helpText={ - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/slack-action-type.html#configuring-slack`} - target="_blank" - > + <EuiLink href={docLinks.links.alerting.slackAction} target="_blank"> <FormattedMessage id="xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel" defaultMessage="Create a Slack Webhook URL" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx index d2a42cf8f3c1ad..3797d784131d74 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx @@ -25,10 +25,7 @@ const TeamsActionFields: React.FunctionComponent< id="webhookUrl" fullWidth helpText={ - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/teams-action-type.html#configuring-teams`} - target="_blank" - > + <EuiLink href={docLinks.links.alerting.teamsAction} target="_blank"> <FormattedMessage id="xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel" defaultMessage="Create a Microsoft Teams Webhook URL" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index 41407e3c258990..ffd6739282a3bc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -94,15 +94,11 @@ export const HealthCheck: React.FunctionComponent<Props> = ({ }; interface PromptErrorProps { - docLinks: Pick<DocLinksStart, 'ELASTIC_WEBSITE_URL' | 'DOC_LINK_VERSION'>; + docLinks: DocLinksStart; className?: string; } -const EncryptionError = ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, - className, -}: PromptErrorProps) => ( +const EncryptionError = ({ docLinks, className }: PromptErrorProps) => ( <EuiEmptyPrompt iconType="watchesApp" data-test-subj="actionNeededEmptyPrompt" @@ -133,11 +129,7 @@ const EncryptionError = ({ ' in your kibana.yml file and ensure the Encrypted Saved Objects plugin is enabled. ', } )} - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html#general-alert-action-settings`} - external - target="_blank" - > + <EuiLink href={docLinks.links.alerting.generalSettings} external target="_blank"> {i18n.translate( 'xpack.triggersActionsUI.components.healthCheck.encryptionErrorAction', { @@ -151,11 +143,7 @@ const EncryptionError = ({ /> ); -const TlsError = ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, - className, -}: PromptErrorProps) => ( +const TlsError = ({ docLinks, className }: PromptErrorProps) => ( <EuiEmptyPrompt iconType="watchesApp" data-test-subj="actionNeededEmptyPrompt" @@ -176,11 +164,7 @@ const TlsError = ({ defaultMessage: 'Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. ', })} - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/configuring-tls.html`} - external - target="_blank" - > + <EuiLink href={docLinks.links.security.kibanaTLS} external target="_blank"> {i18n.translate('xpack.triggersActionsUI.components.healthCheck.tlsErrorAction', { defaultMessage: 'Learn how to enable TLS.', })} @@ -191,11 +175,7 @@ const TlsError = ({ /> ); -const AlertsError = ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, - className, -}: PromptErrorProps) => ( +const AlertsError = ({ docLinks, className }: PromptErrorProps) => ( <EuiEmptyPrompt iconType="watchesApp" data-test-subj="alertsNeededEmptyPrompt" @@ -215,11 +195,7 @@ const AlertsError = ({ {i18n.translate('xpack.triggersActionsUI.components.healthCheck.alertsError', { defaultMessage: 'To create an alert, set alerts and actions plugins enabled. ', })} - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alert-action-settings-kb.html`} - external - target="_blank" - > + <EuiLink href={docLinks.links.alerting.generalSettings} external target="_blank"> {i18n.translate('xpack.triggersActionsUI.components.healthCheck.alertsErrorAction', { defaultMessage: 'Learn how to enable Alerts and Actions.', })} @@ -230,11 +206,7 @@ const AlertsError = ({ /> ); -const TlsAndEncryptionError = ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, - className, -}: PromptErrorProps) => ( +const TlsAndEncryptionError = ({ docLinks, className }: PromptErrorProps) => ( <EuiEmptyPrompt iconType="watchesApp" data-test-subj="actionNeededEmptyPrompt" @@ -255,11 +227,7 @@ const TlsAndEncryptionError = ({ defaultMessage: 'You must enable Transport Layer Security between Kibana and Elasticsearch and configure an encryption key in your kibana.yml file. ', })} - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/alerting-getting-started.html#alerting-setup-prerequisites`} - external - target="_blank" - > + <EuiLink href={docLinks.links.alerting.setupPrerequisites} external target="_blank"> {i18n.translate( 'xpack.triggersActionsUI.components.healthCheck.tlsAndEncryptionErrorAction', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index 1e7fa3c51e6891..c251a1d597f27c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -99,7 +99,7 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps< </EuiFlexItem> <EuiFlexItem grow={false}> <EuiButtonEmpty - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/managing-alerts-and-actions.html`} + href={docLinks.links.alerting.guide} target="_blank" iconType="help" data-test-subj="documentationLink" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index 77b03996e1b71a..ae77a3bdde8918 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -156,10 +156,7 @@ export const ActionConnectorForm = ({ values={{ actionType: actionTypeName ?? connector.actionTypeId, docLink: ( - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/action-types.html`} - target="_blank" - > + <EuiLink href={docLinks.links.alerting.actionTypes} target="_blank"> <FormattedMessage id="xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText" defaultMessage="Learn more." diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 5dbf69c7713ae0..21a90fb1ae17f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -319,10 +319,7 @@ export const ConnectorEditFlyout = ({ } )} </EuiText> - <EuiLink - href={`${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/pre-configured-action-types-and-connectors.html`} - target="_blank" - > + <EuiLink href={docLinks.links.alerting.preconfiguredConnectors} target="_blank"> <FormattedMessage id="xpack.triggersActionsUI.sections.editConnectorForm.preconfiguredHelpLabel" defaultMessage="Learn more about preconfigured connectors." From 6c63bb7d52a8c672d1baaef09feda978cf735721 Mon Sep 17 00:00:00 2001 From: Gabriel Landau <42078554+gabriellandau@users.noreply.github.com> Date: Tue, 2 Mar 2021 17:08:52 -0800 Subject: [PATCH 28/63] [7.12][Security] Shellcode telemetry update for schema adjustment (#93143) (#93341) * Shellcode telemetry update for schema adjustment * Lint * Lint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/telemetry/sender.ts | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index 6ce42eabeca5ee..e169c036419c52 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -395,27 +395,45 @@ const allowlistEventFields: AllowlistFields = { Ext: { call_stack: true, start_address: true, + start_address_allocation_offset: true, + start_address_bytes: true, + start_address_bytes_disasm: true, + start_address_bytes_disasm_hash: true, start_address_details: { - address_offset: true, allocation_base: true, allocation_protection: true, allocation_size: true, allocation_type: true, - base_address: true, - bytes_start_address: true, - compressed_bytes: true, - dest_bytes: true, - dest_bytes_disasm: true, - dest_bytes_disasm_hash: true, - pe: { + bytes_address: true, + bytes_allocation_offset: true, + bytes_compressed: true, + mapped_pe: { Ext: { + code_signature: { + status: true, + subject_name: true, + trusted: true, + }, legal_copyright: true, product_version: true, + }, + company: true, + description: true, + file_version: true, + imphash: true, + original_file_name: true, + product: true, + }, + mapped_pe_path: true, + memory_pe: { + Ext: { code_signature: { status: true, subject_name: true, trusted: true, }, + legal_copyright: true, + product_version: true, }, company: true, description: true, @@ -424,7 +442,8 @@ const allowlistEventFields: AllowlistFields = { original_file_name: true, product: true, }, - pe_detected: true, + memory_pe_detected: true, + region_base: true, region_protection: true, region_size: true, region_state: true, From 3338b75c95c0ef6232dbc3612c1f0341c21b04a8 Mon Sep 17 00:00:00 2001 From: Spencer <email@spalger.com> Date: Tue, 2 Mar 2021 18:21:56 -0700 Subject: [PATCH 29/63] [jenkins] convert baseline capture job to use tasks (#93288) (#93345) Co-authored-by: spalger <spalger@users.noreply.github.com> # Conflicts: # src/dev/ci_setup/setup.sh --- .ci/Jenkinsfile_baseline_capture | 62 +++++++++++++---------- src/dev/ci_setup/bootstrap_validations.sh | 45 ++++++++++++++++ src/dev/ci_setup/setup.sh | 46 ++--------------- 3 files changed, 84 insertions(+), 69 deletions(-) create mode 100644 src/dev/ci_setup/bootstrap_validations.sh diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 6993dc9e087f97..672f5127a97964 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -3,40 +3,48 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -kibanaPipeline(timeoutMinutes: 120) { +kibanaPipeline(timeoutMinutes: 210) { githubCommitStatus.trackBuild(params.commit, 'kibana-ci-baseline') { ciStats.trackBuild { - catchError { - withEnv([ - 'CI_PARALLEL_PROCESS_NUMBER=1' - ]) { - parallel([ - 'oss-baseline': { - workers.ci(name: 'oss-baseline', size: 'l', ramDisk: true, runErrorReporter: false, bootstrapped: false) { - // bootstrap ourselves, but with the env needed to upload the ts refs cache - withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') { - withEnv([ - 'BUILD_TS_REFS_CACHE_ENABLE=true', - 'BUILD_TS_REFS_CACHE_CAPTURE=true' - ]) { - kibanaPipeline.doSetup() - } - } + catchErrors { + slackNotifications.onFailure( + title: "*<${env.BUILD_URL}|[${params.branch}] Baseline Capture Failure>*", + message: "[${params.branch}/${params.commit}] Baseline Capture Failure", + ) { + retryable.enable(2) - kibanaPipeline.functionalTestProcess('oss-baseline', './test/scripts/jenkins_baseline.sh')() + catchErrors { + workers.ci( + name: 'baseline-worker', + size: 'xl', + ramDisk: true, + runErrorReporter: false, + bootstrapped: false + ) { + withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') { + withEnv([ + 'BUILD_TS_REFS_CACHE_ENABLE=true', + 'BUILD_TS_REFS_CACHE_CAPTURE=true', + 'DISABLE_BOOTSTRAP_VALIDATIONS=true', + ]) { + kibanaPipeline.doSetup() + } } - }, - 'xpack-baseline': { - workers.ci(name: 'xpack-baseline', size: 'l', ramDisk: true, runErrorReporter: false) { - kibanaPipeline.functionalTestProcess('xpack-baseline', './test/scripts/jenkins_xpack_baseline.sh')() + + kibanaPipeline.withCiTaskQueue([parallel: 2]) { + catchErrors { + tasks([ + kibanaPipeline.functionalTestProcess('oss-baseline', './test/scripts/jenkins_baseline.sh'), + kibanaPipeline.functionalTestProcess('xpack-baseline', './test/scripts/jenkins_xpack_baseline.sh'), + ]) + } } - }, - ]) + } + } } } - - kibanaPipeline.sendMail() - slackNotifications.onFailure() } + + kibanaPipeline.sendMail() } } diff --git a/src/dev/ci_setup/bootstrap_validations.sh b/src/dev/ci_setup/bootstrap_validations.sh new file mode 100644 index 00000000000000..5fc504aa5dffa2 --- /dev/null +++ b/src/dev/ci_setup/bootstrap_validations.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -e + +### +### verify no git modifications +### +GIT_CHANGES="$(git ls-files --modified)" +if [ "$GIT_CHANGES" ]; then + echo -e "\n${RED}ERROR: 'yarn kbn bootstrap' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + exit 1 +fi + +### +### rebuild kbn-pm distributable to ensure it's not out of date +### +echo " -- building kbn-pm distributable" +yarn kbn run build -i @kbn/pm + +### +### verify no git modifications +### +GIT_CHANGES="$(git ls-files --modified)" +if [ "$GIT_CHANGES" ]; then + echo -e "\n${RED}ERROR: 'yarn kbn run build -i @kbn/pm' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + exit 1 +fi + +### +### rebuild plugin list to ensure it's not out of date +### +echo " -- building plugin list docs" +node scripts/build_plugin_list_docs + +### +### verify no git modifications +### +GIT_CHANGES="$(git ls-files --modified)" +if [ "$GIT_CHANGES" ]; then + echo -e "\n${RED}ERROR: 'node scripts/build_plugin_list_docs' caused changes to the following files:${C_RESET}\n" + echo -e "$GIT_CHANGES\n" + exit 1 +fi diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index c4559029e5607a..c52d5a81517476 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -36,50 +36,12 @@ if [[ "$BUILD_TS_REFS_CACHE_CAPTURE" == "true" ]]; then cd "$KIBANA_DIR" fi +if [[ "$DISABLE_BOOTSTRAP_VALIDATIONS" != "true" ]]; then + source "$KIBANA_DIR/src/dev/ci_setup/bootstrap_validations.sh" +fi + ### ### Download es snapshots ### echo " -- downloading es snapshot" node scripts/es snapshot --download-only; - -### -### verify no git modifications -### -GIT_CHANGES="$(git ls-files --modified)" -if [ "$GIT_CHANGES" ]; then - echo -e "\n${RED}ERROR: 'yarn kbn bootstrap' caused changes to the following files:${C_RESET}\n" - echo -e "$GIT_CHANGES\n" - exit 1 -fi - -### -### rebuild kbn-pm distributable to ensure it's not out of date -### -echo " -- building kbn-pm distributable" -yarn kbn run build -i @kbn/pm - -### -### verify no git modifications -### -GIT_CHANGES="$(git ls-files --modified)" -if [ "$GIT_CHANGES" ]; then - echo -e "\n${RED}ERROR: 'yarn kbn run build -i @kbn/pm' caused changes to the following files:${C_RESET}\n" - echo -e "$GIT_CHANGES\n" - exit 1 -fi - -### -### rebuild plugin list to ensure it's not out of date -### -echo " -- building plugin list docs" -node scripts/build_plugin_list_docs - -### -### verify no git modifications -### -GIT_CHANGES="$(git ls-files --modified)" -if [ "$GIT_CHANGES" ]; then - echo -e "\n${RED}ERROR: 'node scripts/build_plugin_list_docs' caused changes to the following files:${C_RESET}\n" - echo -e "$GIT_CHANGES\n" - exit 1 -fi From df76ebca0f9a5373e8df558595cd7e16115c9750 Mon Sep 17 00:00:00 2001 From: ymao1 <ying.mao@elastic.co> Date: Tue, 2 Mar 2021 20:28:47 -0500 Subject: [PATCH 30/63] [Alerting][Docs] Adding template for documenting alert and action types (#92830) (#93348) * Alert type template * Action type template * Cleanup * Cleanup * Removing callout list * Cleanup * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * Adding title to actions page * PR fixes * PR fixes * PR fixes * PR fixes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/action-type-template.asciidoc | 73 ++++++++ docs/alert-type-template.asciidoc | 39 +++++ .../user/alerting/action-types/email.asciidoc | 59 +++---- .../user/alerting/action-types/index.asciidoc | 31 ++-- docs/user/alerting/action-types/jira.asciidoc | 39 ++--- .../alerting/action-types/pagerduty.asciidoc | 156 ++++++------------ .../alerting/action-types/resilient.asciidoc | 39 ++--- .../alerting/action-types/server-log.asciidoc | 7 +- .../alerting/action-types/servicenow.asciidoc | 35 ++-- .../user/alerting/action-types/slack.asciidoc | 22 +-- .../user/alerting/action-types/teams.asciidoc | 21 +-- .../alerting/action-types/webhook.asciidoc | 48 ++---- docs/user/alerting/alert-types.asciidoc | 5 +- docs/user/alerting/defining-alerts.asciidoc | 9 +- .../images/slack-copy-webhook-url.png | Bin 42332 -> 21738 bytes .../alerting/stack-alerts/es-query.asciidoc | 33 ++-- .../stack-alerts/index-threshold.asciidoc | 74 ++++----- x-pack/plugins/actions/README.md | 4 +- x-pack/plugins/alerts/README.md | 18 +- 19 files changed, 335 insertions(+), 377 deletions(-) create mode 100644 docs/action-type-template.asciidoc create mode 100644 docs/alert-type-template.asciidoc diff --git a/docs/action-type-template.asciidoc b/docs/action-type-template.asciidoc new file mode 100644 index 00000000000000..5b61c259563ad2 --- /dev/null +++ b/docs/action-type-template.asciidoc @@ -0,0 +1,73 @@ +[[<ACTION-TYPE>-action-type]] +=== <ACTION-TYPE> action +++++ +<titleabbrev><ACTION-TYPE></titleabbrev> +++++ + +Include a short description of the action type. + +[float] +[[<ACTION-TYPE>-connector-configuration]] +==== Connector configuration + +<ACTION-TYPE> connectors have the following configuration properties. + +//// +List of user-facing connector configurations. This should align with the fields available in the Create connector flyout form for this action type. +//// + +Property1:: A short description of this property. +Property2:: A short description of this property with format hints. This can be specified in `this specific format`. + +[float] +[[Preconfigured-<ACTION-TYPE>-configuration]] +==== Preconfigured action type + +//// +Example preconfigured format for this action type +//// + +[source,text] +-- + my-<ACTION-TYPE>: + name: preconfigured-<ACTION-TYPE>-action-type + actionTypeId: .<ACTION-TYPE> + config: + property1: value1 + property2: value2 + secrets: + property3: value3 +-- + +[float] +[[<ACTION-TYPE>-connector-config-properties]] +//// +List of properties from the ConfigSchema and SecretsSchema for this action type. +//// +Config defines information for the action type. + +`property1`:: A short description of this property. +`property2`:: A short descriptionn of this property. + +Secrets defines sensitive information for the action type. + +`property3`:: A short descriptionn of this property. + +[float] +[[<ACTION-TYPE>-action-configuration]] +==== Action configuration + +<ACTION-TYPE> actions have the following configuration properties. + +//// +List of user-facing action configurations. This should align with the fields available in the Action section of the Create/Update alert flyout. +//// + +Property1:: A short description of this property. +Property2:: A short description of this property with format hints. This can be specified in `this specific format`. + +//// +Optional - additional configuration details here +[[configuring-<ACTION-TYPE>]] +==== Configure <ACTION-TYPE> +//// diff --git a/docs/alert-type-template.asciidoc b/docs/alert-type-template.asciidoc new file mode 100644 index 00000000000000..292ed00d05496f --- /dev/null +++ b/docs/alert-type-template.asciidoc @@ -0,0 +1,39 @@ +[[alert-type-<ALERT TYPE>]] +=== <ALERT TYPE> + +Include a short description of the alert type. + +[float] +==== Create the alert + +Fill in the <<defining-alerts-general-details, alert details>>, then select *<ALERT TYPE>*. + +[float] +==== Define the conditions + +Define properties to detect the condition. + +//// +Optional, include a screenshot +[role="screenshot"] +image::user/alerting/images/alert-types-<ALERT TYPE>-conditions.png[Conditions for <ALERT TYPE> alert type] +//// + +Condition1:: This is a condition the user must define. +Condition2:: This is another condition the user must define. + +[float] +==== Add action variables + +<<defining-alerts-actions-details, Add an action>> to run when the alert condition is met. The following variables are specific to the <ALERT TYPE> alert. You can also specify <<defining-alerts-actions-variables, variables common to all alerts>>. + +`context.variableA`:: A short description of the context variable defined by the alert type. +`context.variableB`:: A short description of the context variable defined by the alert type with an example. Example: `this is what variableB outputs`. + +//// +Optional, include a step-by-step example for creating this alert +[float] +==== Example + +In this section, you will use the {kib} <<add-sample-data, weblog sample dataset>> to setup and tune the conditions on an <ALERT TYPE> alert. For this example, we want to detect when <DESCRIBE THE CONDITIONS>. +//// \ No newline at end of file diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index 0c238da1b39e38..3813eccd048d9d 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[email-action-type]] === Email action +++++ +<titleabbrev>Email</titleabbrev> +++++ The email action type uses the SMTP protocol to send mail message, using an integration of https://nodemailer.com/[Nodemailer]. Email message text is sent as both plain text and html text. @@ -10,14 +13,15 @@ NOTE: For emails to have a footer with a link back to {kib}, set the <<server-pu [[email-connector-configuration]] ==== Connector configuration -Email connectors have the following configuration properties: +Email connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Sender:: The from address for all emails sent with this connector. This can be specified in `user@host-name` format or as `"human name <user@host-name>"` format. See the https://nodemailer.com/message/addresses/[Nodemailer address documentation] for more information. Host:: Host name of the service provider. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure this hostname is added to the allowed hosts. Port:: The port to connect to on the service provider. Secure:: If true, the connection will use TLS when connecting to the service provider. Refer to the https://nodemailer.com/smtp/#tls-options[Nodemailer TLS documentation] for more information. If not true, the connection will initially connect over TCP, then attempt to switch to TLS via the SMTP STARTTLS command. -User:: Username for login type authentication. +Require authentication:: If true, a username and password for login type authentication must be provided. +Username:: Username for login type authentication. Password:: Password for login type authentication. [float] @@ -39,56 +43,33 @@ Password:: Password for login type authentication. password: passwordkeystorevalue -- -[[email-connector-config-properties]] -**`config`** defines the action type specific to the configuration and contains the following properties: +Config defines information for the action type. -[cols="2*<"] -|=== +`service`:: The name of a https://nodemailer.com/smtp/well-known/[well-known email service provider]. If `service` is provided, `host`, `port`, and `secure` properties are ignored. For more information on the `gmail` service value, see the https://nodemailer.com/usage/using-gmail/[Nodemailer Gmail documentation]. +`from`:: An email address that corresponds to *Sender*. +`host`:: A string that corresponds to *Host*. +`port`:: A number that corresponds to *Port*. +`secure`:: A boolean that corresponds to *Secure*. +`hasAuth`:: A boolean that corresponds to *Requires authentication*. If `true`, this connector will require values for `user` and `password` inside the secrets configuration. Defaults to `true`. -| `service` -| The name of a https://nodemailer.com/smtp/well-known/[well-known email service provider]. If `service` is provided, `host`, `port`, and `secure` properties are ignored. For more information on the `gmail` service value, see the (https://nodemailer.com/usage/using-gmail/)[Nodemailer Gmail documentation]. +Secrets defines sensitive information for the action type. -| `from` -| An email address that corresponds to *Sender*. +`user`:: A string that corresponds to *Username*. Required if `hasAuth` is set to `true`. +`password`:: A string that corresponds to *Password*. Should be stored in the <<creating-keystore, {kib} keystore>>. Required if `hasAuth` is set to `true`. -| `host` -| A string that corresponds to *Host*. - -| `port` -| A number that corresponds to *Port*. - -| `secure` -| A boolean that corresponds to *Secure*. - -| `hasAuth` -| If `true`, this connector will require values for `user` and `password` inside the secrets configuration. Defaults to `true`. - -|=== - -**`secrets`** defines sensitive information for the action type and contains the following properties: - -[cols="2*<"] -|=== - -| `user` -| A string that corresponds to *User*. Required if `hasAuth` is set to `true`. - -| `password` -| A string that corresponds to *Password*. Should be stored in the <<creating-keystore, {kib} keystore>>. Required if `hasAuth` is set to `true`. - -|=== +[float] [[email-action-configuration]] ==== Action configuration -Email actions have the following configuration properties: +Email actions have the following configuration properties. -To, CC, BCC:: Each is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name <user@host-name>` format. One of To, CC, or BCC must contain an entry. +To, CC, BCC:: Each item is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name <user@host-name>` format. One of To, CC, or BCC must contain an entry. Subject:: The subject line of the email. Message:: The message text of the email. Markdown format is supported. [[configuring-email]] -==== Configuring email accounts +==== Configuring email accounts for well-known services The email action can send email using many popular SMTP email services. diff --git a/docs/user/alerting/action-types/index.asciidoc b/docs/user/alerting/action-types/index.asciidoc index 7ecc1cdb4f74e8..a57048243d7577 100644 --- a/docs/user/alerting/action-types/index.asciidoc +++ b/docs/user/alerting/action-types/index.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[index-action-type]] === Index action +++++ +<titleabbrev>Index</titleabbrev> +++++ The index action type will index a document into {es}. See also the {ref}/indices-create-index.html[create index API]. @@ -8,7 +11,7 @@ The index action type will index a document into {es}. See also the {ref}/indice [[index-connector-configuration]] ==== Connector configuration -Index connectors have the following configuration properties: +Index connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Index:: The {es} index to be written to. @@ -30,32 +33,24 @@ Execution time field:: This field will be automatically set to the time the ale executionTimeField: somedate -- -[[index-connector-config-properties]] -**`config`** defines the action type specific to the configuration and contains the following properties: - -[cols="2*<"] -|=== - -|`index` -| A string that corresponds to *Index*. - -|`refresh` -| A boolean that corresponds to *Refresh*. Defaults to `false`. - -|`executionTimeField` -| A string that corresponds to *Execution time field*. - -|=== +Config defines information for the action type. +`index`:: A string that corresponds to *Index*. +`refresh`:: A boolean that corresponds to *Refresh*. Defaults to `false`. +`executionTimeField`:: A string that corresponds to *Execution time field*. [float] [[index-action-configuration]] ==== Action configuration -Index actions have the following properties: +Index actions have the following properties. Document:: The document to index in JSON format. +[float] +[[index-action-example]] +==== Example + Example of the index document for Index Threshold alert: [source,text] diff --git a/docs/user/alerting/action-types/jira.asciidoc b/docs/user/alerting/action-types/jira.asciidoc index d37f565c1739ba..a1941b4b302834 100644 --- a/docs/user/alerting/action-types/jira.asciidoc +++ b/docs/user/alerting/action-types/jira.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[jira-action-type]] === Jira action +++++ +<titleabbrev>Jira</titleabbrev> +++++ The Jira action type uses the https://developer.atlassian.com/cloud/jira/platform/rest/v2/[REST API v2] to create Jira issues. @@ -8,7 +11,7 @@ The Jira action type uses the https://developer.atlassian.com/cloud/jira/platfor [[jira-connector-configuration]] ==== Connector configuration -Jira connectors have the following configuration properties: +Jira connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. URL:: Jira instance URL. @@ -33,37 +36,21 @@ API token (or password):: Jira API authentication token (or password) for HTTP apiToken: tokenkeystorevalue -- -[[jira-connector-config-properties]] -**`config`** defines the action type specific to the configuration and contains the following properties: +Config defines information for the action type. -[cols="2*<"] -|=== +`apiUrl`:: An address that corresponds to *URL*. +`projectKey`:: A key that corresponds to *Project Key*. -| `apiUrl` -| An address that corresponds to *URL*. +Secrets defines sensitive information for the action type. -| `projectKey` -| A key that corresponds to *Project Key*. - -|=== - -**`secrets`** defines sensitive information for the action type and contains the following properties: - -[cols="2*<"] -|=== - -| `email` -| A string that corresponds to *Email*. - -| `apiToken` -| A string that corresponds to *API Token*. Should be stored in the <<creating-keystore, {kib} keystore>>. - -|=== +`email`:: A string that corresponds to *Email*. +`apiToken`:: A string that corresponds to *API Token*. Should be stored in the <<creating-keystore, {kib} keystore>>. +[float] [[jira-action-configuration]] ==== Action configuration -Jira actions have the following configuration properties: +Jira actions have the following configuration properties. Issue type:: The type of the issue. Priority:: The priority of the incident. @@ -74,6 +61,6 @@ Parent:: The ID or key of the parent issue. Only for `Subtask` issue types. Additional comments:: Additional information for the client, such as how to troubleshoot the issue. [[configuring-jira]] -==== Configuring and testing Jira +==== Configure Jira Jira offers free https://www.atlassian.com/software/jira/free[Instances], which you can use to test incidents. diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index cadf8e0b16a44d..f74b5773b37197 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -1,49 +1,72 @@ [role="xpack"] [[pagerduty-action-type]] === PagerDuty action +++++ +<titleabbrev>PagerDuty</titleabbrev> +++++ The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/events-api-v2[v2 Events API] to trigger, acknowledge, and resolve PagerDuty alerts. -* <<pagerduty-benefits, PagerDuty and Elastic integration benefits>> -* <<pagerduty-connector-configuration, Connector configuration>> -* <<pagerduty-action-configuration, Action configuration>> - [float] -[[pagerduty-benefits]] -==== PagerDuty + Elastic integration benefits +[[pagerduty-connector-configuration]] +==== Connector configuration -By integrating PagerDuty with alerts, you can: +PagerDuty connectors have the following configuration properties. -* Route your alerts to the right PagerDuty responder within your team, based on your structure, escalation policies, and workflows. -* Automatically generate incidents of different types and severity based on each alert’s context. -* Tailor the incident data to match your needs by easily passing the alerting context from Kibana to PagerDuty. +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts. +Integration Key:: A 32 character PagerDuty Integration Key for an integration on a service, also referred to as the routing key. [float] -[[pagerduty-how-it-works]] -===== How it works +[[Preconfigured-pagerduty-configuration]] +==== Preconfigured action type + +[source,text] +-- + my-pagerduty: + name: preconfigured-pagerduty-action-type + actionTypeId: .pagerduty + config: + apiUrl: https://test.host + secrets: + routingKey: testroutingkey +-- + +Config defines information for the action type. + +`apiURL`:: A URL string that corresponds to *API URL*. -{kib} allows you to create alerts to notify you of a significant move -in your dataset. -You can create alerts for all your Observability, Security, and Elastic Stack use cases. -Alerts will trigger a new incident on the corresponding PagerDuty service. +Secrets defines sensitive information for the action type. + +`routingKey`:: A string that corresponds to *Integration Key*. [float] -===== Requirements +[[pagerduty-action-configuration]] +==== Action configuration -In the `kibana.yml` configuration file, you must add the <<general-alert-action-settings, saved objects encryption setting>>. -This is required to encrypt parameters that must be secured, for example PagerDuty’s integration key. +PagerDuty actions have the following properties. -If you have security enabled: +Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). +Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. +Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if not set, defaults to `<alert ID>:<alert instance ID>`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. +Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. +Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. +Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. +Source:: An *optional* value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the {kib} saved object id of the action. +Summary:: An *optional* text summary of the event, defaults to `No summary provided`. The maximum length is 1024 characters. +Class:: An *optional* value indicating the class/type of the event, for example `ping failure` or `cpu load`. -* You must have -application privileges to access Metrics, APM, Uptime, or Security. -* If you are using a self-managed deployment with security, you must have -Transport Security Layer (TLS) enabled for communication <<configuring-tls-kib-es, between Elasticsearch and Kibana>>. -Alerts uses API keys to secure background alert checks and actions, -and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. +For more details on these properties, see https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[PagerDuty v2 event parameters]. -Although not a requirement, to harden the integrations security you might want to -review the <<action-settings, Actions settings>> that are available to you. +[float] +[[pagerduty-benefits]] +==== Configure PagerDuty + +By integrating PagerDuty with alerts, you can: + +* Route your alerts to the right PagerDuty responder within your team, based on your structure, escalation policies, and workflows. +* Automatically generate incidents of different types and severity based on each alert’s context. +* Tailor the incident data to match your needs by easily passing the alerting context from Kibana to PagerDuty. [float] [[pagerduty-support]] @@ -111,80 +134,3 @@ To see the available context variables, click on the *Add alert variable* icon n to each corresponding field. For more details on these parameters, see the <<pagerduty-action-configuration, Actions Configuration>> and the PagerDuty https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[API v2 documentation]. - - -[float] -[[pagerduty-uninstall]] -===== How to uninstall -To remove a PagerDuty connector from an alert, simply remove it -from the *Actions* section of that alert, using the remove (x) icon. -This will disable the integration for the particular alert. - -To delete the connector entirely, open the main menu, then click *Stack Management > Alerts and Actions*. -Select the *Connectors* tab, and then click on the delete icon. -This is an irreversible action and impacts all alerts that use this connector. - - -[float] -[[pagerduty-connector-configuration]] -==== Connector configuration - -PagerDuty connectors have the following configuration properties: - -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts. -Integration Key:: A 32 character PagerDuty Integration Key for an integration on a service, also referred to as the routing key. - -[float] -[[Preconfigured-pagerduty-configuration]] -==== Preconfigured action type - -[source,text] --- - my-pagerduty: - name: preconfigured-pagerduty-action-type - actionTypeId: .pagerduty - config: - apiUrl: https://test.host - secrets: - routingKey: testroutingkey --- - -[[pagerduty-connector-config-properties]] -**`config`** defines the action type specific to the configuration and contains the following properties: - -[cols="2*<"] -|=== - -|`apiURL` -| A URL string that corresponds to *API URL*. - -|=== - -**`secrets`** defines sensitive information for the action type and contains the following properties: - -[cols="2*<"] -|=== - -|`routingKey` -| A string that corresponds to *Integration Key*. - -|=== - -[float] -[[pagerduty-action-configuration]] -==== Action configuration - -PagerDuty actions have the following properties: - -Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). -Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. -Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if not set, defaults to `<alert ID>:<alert instance ID>`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. -Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. -Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. -Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. -Source:: An *optional* value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the {kib} saved object id of the action. -Summary:: An *optional* text summary of the event, defaults to `No summary provided`. The maximum length is 1024 characters. -Class:: An *optional* value indicating the class/type of the event, for example `ping failure` or `cpu load`. - -For more details on these properties, see https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[PagerDuty v2 event parameters]. diff --git a/docs/user/alerting/action-types/resilient.asciidoc b/docs/user/alerting/action-types/resilient.asciidoc index feca42a542a2f2..296156875ceb69 100644 --- a/docs/user/alerting/action-types/resilient.asciidoc +++ b/docs/user/alerting/action-types/resilient.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[resilient-action-type]] === IBM Resilient action +++++ +<titleabbrev>IBM Resilient</titleabbrev> +++++ The IBM Resilient action type uses the https://developer.ibm.com/security/resilient/rest/[RESILIENT REST v2] to create IBM Resilient incidents. @@ -8,7 +11,7 @@ The IBM Resilient action type uses the https://developer.ibm.com/security/resili [[resilient-connector-configuration]] ==== Connector configuration -IBM Resilient connectors have the following configuration properties: +IBM Resilient connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. URL:: IBM Resilient instance URL. @@ -33,37 +36,21 @@ API key secret:: The authentication key secret for HTTP Basic authentication. apiKeySecret: tokenkeystorevalue -- -[[resilient-connector-config-properties]] -**`config`** defines the action type specific to the configuration and contains the following properties: +Config defines information for the action type. -[cols="2*<"] -|=== +`apiUrl`:: An address that corresponds to *URL*. +`orgId`:: An ID that corresponds to *Organization ID*. -| `apiUrl` -| An address that corresponds to *URL*. +Secrets defines sensitive information for the action type. -| `orgId` -| An ID that corresponds to *Organization ID*. - -|=== - -**`secrets`** defines sensitive information for the action type and contains the following properties: - -[cols="2*<"] -|=== - -| `apiKeyId` -| A string that corresponds to *API key ID*. - -| `apiKeySecret` -| A string that corresponds to *API Key secret*. Should be stored in the <<creating-keystore, {kib} keystore>>. - -|=== +`apiKeyId`:: A string that corresponds to *API key ID*. +`apiKeySecret`:: A string that corresponds to *API Key secret*. Should be stored in the <<creating-keystore, {kib} keystore>>. +[float] [[resilient-action-configuration]] ==== Action configuration -IBM Resilient actions have the following configuration properties: +IBM Resilient actions have the following configuration properties. Incident types:: The type of the incident. Severity code:: The severity of the incident. @@ -72,6 +59,6 @@ Description:: The details about the incident. Additional comments:: Additional information for the client, such as how to troubleshoot the issue. [[configuring-resilient]] -==== Configuring and testing IBM Resilient +==== Configure IBM Resilient IBM Resilient offers https://www.ibm.com/security/intelligent-orchestration/resilient[Instances], which you can use to test incidents. diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc index 276f64e7786bd9..7849a70a239c36 100644 --- a/docs/user/alerting/action-types/server-log.asciidoc +++ b/docs/user/alerting/action-types/server-log.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[server-log-action-type]] === Server log action +++++ +<titleabbrev>Server log</titleabbrev> +++++ This action type writes an entry to the {kib} server log. @@ -8,7 +11,7 @@ This action type writes an entry to the {kib} server log. [[server-log-connector-configuration]] ==== Connector configuration -Server log connectors have the following configuration properties: +Server log connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. @@ -27,7 +30,7 @@ Name:: The name of the connector. The name is used to identify a connector [[server-log-action-configuration]] ==== Action configuration -Server log actions have the following properties: +Server log actions have the following properties. Message:: The message to log. Level:: The log level of the message: `trace`, `debug`, `info`, `warn`, `error` or `fatal`. Defaults to `info`. diff --git a/docs/user/alerting/action-types/servicenow.asciidoc b/docs/user/alerting/action-types/servicenow.asciidoc index 4a11a2e28712a0..f002c39416f1ad 100644 --- a/docs/user/alerting/action-types/servicenow.asciidoc +++ b/docs/user/alerting/action-types/servicenow.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[servicenow-action-type]] === ServiceNow action +++++ +<titleabbrev>ServiceNow</titleabbrev> +++++ The ServiceNow action type uses the https://developer.servicenow.com/app.do#!/rest_api_doc?v=orlando&id=c_TableAPI[V2 Table API] to create ServiceNow incidents. @@ -8,7 +11,7 @@ The ServiceNow action type uses the https://developer.servicenow.com/app.do#!/re [[servicenow-connector-configuration]] ==== Connector configuration -ServiceNow connectors have the following configuration properties: +ServiceNow connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. URL:: ServiceNow instance URL. @@ -31,34 +34,20 @@ Password:: Password for HTTP Basic authentication. password: passwordkeystorevalue -- -[[servicenow-connector-config-properties]] -**`config`** defines the action type specific to the configuration and contains the following properties: +Config defines information for the action type. -[cols="2*<"] -|=== +`apiUrl`:: An address that corresponds to *URL*. -| `apiUrl` -| An address that corresponds to *URL*. +Secrets defines sensitive information for the action type. -|=== - -**`secrets`** defines sensitive information for the action type and contains the following properties: - -[cols="2*<"] -|=== - -| `username` -| A string that corresponds to *Username*. - -| `password` -| A string that corresponds to *Password*. Should be stored in the <<creating-keystore, {kib} keystore>>. - -|=== +`username`:: A string that corresponds to *Username*. +`password`:: A string that corresponds to *Password*. Should be stored in the <<creating-keystore, {kib} keystore>>. +[float] [[servicenow-action-configuration]] ==== Action configuration -ServiceNow actions have the following configuration properties: +ServiceNow actions have the following configuration properties. Urgency:: The extent to which the incident resolution can delay. Severity:: The severity of the incident. @@ -68,6 +57,6 @@ Description:: The details about the incident. Additional comments:: Additional information for the client, such as how to troubleshoot the issue. [[configuring-servicenow]] -==== Configuring and testing ServiceNow +==== Configure ServiceNow ServiceNow offers free https://developer.servicenow.com/dev.do#!/guides/madrid/now-platform/pdi-guide/obtaining-a-pdi[Personal Developer Instances], which you can use to test incidents. diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc index 62589697533562..6f7d1b3e11d318 100644 --- a/docs/user/alerting/action-types/slack.asciidoc +++ b/docs/user/alerting/action-types/slack.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[slack-action-type]] === Slack action +++++ +<titleabbrev>Slack</titleabbrev> +++++ The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. @@ -8,7 +11,7 @@ The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incomin [[slack-connector-configuration]] ==== Connector configuration -Slack connectors have the following configuration properties: +Slack connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts. @@ -26,29 +29,20 @@ Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messa webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' -- -**`secrets`** defines sensitive information for the action type and contains the following properties: +Secrets defines sensitive information for the action type. -[cols="2*<"] -|=== - -| `webhookUrl` -| A string that corresponds to *Webhook URL*. - -|=== +`webhookUrl`:: A string that corresponds to *Webhook URL*. [float] [[slack-action-configuration]] ==== Action configuration -Slack actions have the following properties: +Slack actions have the following properties. Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. [[configuring-slack]] -==== Configuring Slack Accounts - -You configure the accounts Slack action type can use to communicate with Slack in the -connector form. +==== Configure a Slack account You need a https://api.slack.com/incoming-webhooks[Slack webhook URL] to configure a Slack account. To create a webhook diff --git a/docs/user/alerting/action-types/teams.asciidoc b/docs/user/alerting/action-types/teams.asciidoc index 7f4a29dc86fc52..294b5474e390a4 100644 --- a/docs/user/alerting/action-types/teams.asciidoc +++ b/docs/user/alerting/action-types/teams.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[teams-action-type]] === Microsoft Teams action +++++ +<titleabbrev>Microsoft Teams</titleabbrev> +++++ The Microsoft Teams action type uses https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook[Incoming Webhooks]. @@ -8,7 +11,7 @@ The Microsoft Teams action type uses https://docs.microsoft.com/en-us/microsoftt [[teams-connector-configuration]] ==== Connector configuration -Microsoft Teams connectors have the following configuration properties: +Microsoft Teams connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Webhook URL:: The URL of the incoming webhook. See https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook#add-an-incoming-webhook-to-a-teams-channel[Add Incoming Webhooks] for instructions on generating this URL. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts. @@ -26,28 +29,20 @@ Webhook URL:: The URL of the incoming webhook. See https://docs.microsoft.com/ webhookUrl: 'https://outlook.office.com/webhook/abcd@0123456/IncomingWebhook/abcdefgh/ijklmnopqrstuvwxyz' -- -[[teams-connector-config-properties]] -**`secrets`** defines sensitive information for the action type and contains the following properties: - -[cols="2*<"] -|=== - -| `webhookUrl` -| A string that corresponds to *Webhook URL*. - -|=== +Secrets defines sensitive information for the action type. +`webhookUrl`:: A string that corresponds to *Webhook URL*. [float] [[teams-action-configuration]] ==== Action configuration -Microsoft Teams actions have the following properties: +Microsoft Teams actions have the following properties. Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. [[configuring-teams]] -==== Configuring Microsoft Teams Accounts +==== Configure a Microsoft Teams account You need a https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook[Microsoft Teams webhook URL] to configure a Microsoft Teams action. To create a webhook diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc index efe1077707ef09..381d6e72bf9c0c 100644 --- a/docs/user/alerting/action-types/webhook.asciidoc +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -1,6 +1,9 @@ [role="xpack"] [[webhook-action-type]] === Webhook action +++++ +<titleabbrev>Webhook</titleabbrev> +++++ The Webhook action type uses https://github.com/axios/axios[axios] to send a POST or PUT request to a web service. @@ -8,13 +11,14 @@ The Webhook action type uses https://github.com/axios/axios[axios] to send a POS [[webhook-connector-configuration]] ==== Connector configuration -Webhook connectors have the following configuration properties: +Webhook connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. URL:: The request URL. If you are using the <<action-settings, `xpack.actions.allowedHosts`>> setting, make sure the hostname is added to the allowed hosts. Method:: HTTP request method, either `POST`(default) or `PUT`. Headers:: A set of key-value pairs sent as headers with the request -User:: Username for HTTP basic authentication. +Require authentication:: If true, a username and password for login type authentication must be provided. +Username:: Username for HTTP basic authentication. Password:: Password for HTTP basic authentication. [float] @@ -36,45 +40,23 @@ Password:: Password for HTTP basic authentication. password: passwordkeystorevalue -- -[[webhook-connector-config-properties]] -**`config`** defines the action type specific to the configuration and contains the following properties: +Config defines information for the action type. -[cols="2*<"] -|=== +`url`:: A URL string that corresponds to *URL*. +`method`:: A string that corresponds to *Method*. +`headers`:: A record<string, string> that corresponds to *Headers*. +`hasAuth`:: A boolean that corresponds to *Requires authentication*. If `true`, this connector will require values for `user` and `password` inside the secrets configuration. Defaults to `true`. -|`url` -| A URL string that corresponds to *URL*. - -|`method` -| A string that corresponds to *Method*. - -|`headers` -|A record<string, string> that corresponds to *Headers*. - -| `hasAuth` -| If `true`, this connector will require values for `user` and `password` inside the secrets configuration. Defaults to `true`. - -|=== - -**`secrets`** defines sensitive information for the action type and contains the following properties: - -[cols="2*<"] -|=== - -|`user` -|A string that corresponds to *User*. Required if `hasAuth` is set to `true`. - -|`password` -|A string that corresponds to *Password*. Should be stored in the <<creating-keystore, {kib} keystore>>. Required if `hasAuth` is set to `true`. - -|=== +Secrets defines sensitive information for the action type. +`user`:: A string that corresponds to *User*. Required if `hasAuth` is set to `true`. +`password`:: A string that corresponds to *Password*. Should be stored in the <<creating-keystore, {kib} keystore>>. Required if `hasAuth` is set to `true`. [float] [[webhook-action-configuration]] ==== Action configuration -Webhook actions have the following properties: +Webhook actions have the following properties. Body:: A JSON payload sent to the request URL. For example: + diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 5afce8fa6cd93b..9eac084bd03c40 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -10,14 +10,13 @@ Kibana provides two types of alerts: [float] ==== Standard stack alerts -Users require the `all` privilege to access to the *Stack Alerts* feature and create and edit alerts. . -See <<kibana-feature-privileges, feature privileges>> for more information. - {kib} provides two stack alerts: * <<alert-type-index-threshold>> * <<alert-type-es-query>> +Users require the `all` privilege to access the *Stack Alerts* feature and create and edit alerts. +See <<kibana-feature-privileges, feature privileges>> for more information. [float] ==== Domain-specific alerts diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 8c8e25cea407a0..27f3a6c7309cb0 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -5,9 +5,9 @@ {kib} alerts can be created in a variety of apps including <<xpack-apm,*APM*>>, <<xpack-ml,*{ml-app}*>>, <<metrics-app,*Metrics*>>, <<xpack-siem,*Security*>>, <<uptime-app,*Uptime*>> and from <<management,*Management*>> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. [float] -=== Alert flyout +=== Create an alert -When an alert is created in an app, the app will display a flyout panel with three main sections to configure: +When you create an alert, you must define the alert details, conditions, and actions. . <<defining-alerts-general-details, General alert details>> . <<defining-alerts-type-conditions, Alert type and conditions>> @@ -19,7 +19,7 @@ image::images/alert-flyout-sections.png[The three sections of an alert definitio [[defining-alerts-general-details]] === General alert details -All alerts share the following four properties in common: +All alerts share the following four properties. [role="screenshot"] image::images/alert-flyout-general-details.png[alt='All alerts have name, tags, check every, and notify properties in common'] @@ -69,6 +69,7 @@ Each action type exposes different properties. For example an email action allow image::images/alert-flyout-action-details.png[UI for defining an email action] [float] +[[defining-alerts-actions-variables]] ==== Action variables Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. You can access the list of available variables using the "add variable" button. Although available variables differ by alert type, all alert types pass the following variables: @@ -105,6 +106,6 @@ Actions are not required on alerts. You can run an alert without actions to unde ============================================== [float] -=== Managing alerts +=== Manage alerts To modify an alert after it was created, including muting or disabling it, use the <<alert-management, alert listing in the Management UI>>. diff --git a/docs/user/alerting/images/slack-copy-webhook-url.png b/docs/user/alerting/images/slack-copy-webhook-url.png index 0acc9488e22a335e31a9e429bcc033425f11baac..805f5719980dabed80af6a8fbaebf194b52305c7 100644 GIT binary patch literal 21738 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 CHZpVo literal 42332 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 AA^-pY diff --git a/docs/user/alerting/stack-alerts/es-query.asciidoc b/docs/user/alerting/stack-alerts/es-query.asciidoc index 9f4a882328b9f7..d25b7267ed18ff 100644 --- a/docs/user/alerting/stack-alerts/es-query.asciidoc +++ b/docs/user/alerting/stack-alerts/es-query.asciidoc @@ -2,24 +2,20 @@ [[alert-type-es-query]] === ES query -The ES query alert type is designed to run a user-configured {es} query over indices, compare the number of matches to a configured threshold, and schedule -actions to run when the threshold condition is met. +The ES query alert type runs a user-configured {es} query, compares the number of matches to a configured threshold, and schedules actions to run when the threshold condition is met. [float] -==== Creating the alert +==== Create the alert -An ES query alert can be created from the *Create* button in the <<alert-management, alert management UI>>. Fill in the <<defining-alerts-general-details, general alert details>>, then select *ES query*. - -[role="screenshot"] -image::user/alerting/images/alert-types-es-query-select.png[Choosing an ES query alert type] +Fill in the <<defining-alerts-general-details, alert details>>, then select *ES query*. [float] -==== Defining the conditions +==== Define the conditions -The ES query alert has 5 clauses that define the condition to detect. +Define properties to detect the condition. [role="screenshot"] -image::user/alerting/images/alert-types-es-query-conditions.png[Four clauses define the condition to detect] +image::user/alerting/images/alert-types-es-query-conditions.png[Five clauses define the condition to detect] Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. Size:: This clause specifies the number of documents to pass to the configured actions when the the threshold condition is met. @@ -29,9 +25,9 @@ Threshold:: This clause defines a threshold value and a comparison operator (`i Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be set to a value higher than the *check every* value in the <<defining-alerts-general-details, general alert details>>, to avoid gaps in detection. [float] -==== Action variables +==== Add action variables -When the ES query alert condition is met, the following variables are available to use inside each action: +<<defining-alerts-actions-details, Add an action>> to run when the alert condition is met. The following variables are specific to the ES query alert. You can also specify <<defining-alerts-actions-variables, variables common to all alerts>>. `context.title`:: A preconstructed title for the alert. Example: `alert term match alert query matched`. `context.message`:: A preconstructed message for the alert. Example: + @@ -45,22 +41,23 @@ When the ES query alert condition is met, the following variables are available `context.value`:: The value of the alert that met the condition. `context.conditions`:: A description of the condition. Example: `count greater than 4`. `context.hits`:: The most recent ES documents that matched the query. Using the https://mustache.github.io/[Mustache] template array syntax, you can iterate over these hits to get values from the ES documents into your actions. - ++ [role="screenshot"] image::images/alert-types-es-query-example-action-variable.png[Iterate over hits using Mustache template syntax] + [float] -==== Testing your query +==== Test your query Use the *Test query* feature to verify that your query DSL is valid. -When your query is valid:: Valid queries will be executed against the configured *index* using the configured *time window*. The number of documents that +* Valid queries are executed against the configured *index* using the configured *time window*. The number of documents that match the query will be displayed. - ++ [role="screenshot"] image::user/alerting/images/alert-types-es-query-valid.png[Test ES query returns number of matches when valid] -When your query is invalid:: An error message is shown if the query is invalid. - +* An error message is shown if the query is invalid. ++ [role="screenshot"] image::user/alerting/images/alert-types-es-query-invalid.png[Test ES query shows error when invalid] \ No newline at end of file diff --git a/docs/user/alerting/stack-alerts/index-threshold.asciidoc b/docs/user/alerting/stack-alerts/index-threshold.asciidoc index 6b45f69401c4aa..89ca8e3087f12a 100644 --- a/docs/user/alerting/stack-alerts/index-threshold.asciidoc +++ b/docs/user/alerting/stack-alerts/index-threshold.asciidoc @@ -2,20 +2,17 @@ [[alert-type-index-threshold]] === Index threshold -The index threshold alert type is designed to run an {es} query over indices, aggregating field values from documents, comparing them to threshold values, and scheduling actions to run when the thresholds are met. +The index threshold alert type runs an {es} query. It aggregates field values from documents, compares them to threshold values, and schedules actions to run when the thresholds are met. [float] -==== Creating the alert +==== Create the alert -An index threshold alert can be created from the *Create* button in the <<alert-management, alert management UI>>. Fill in the <<defining-alerts-general-details, general alert details>>, then select *Index Threshold*. - -[role="screenshot"] -image::user/alerting/images/alert-types-index-threshold-select.png[Choosing an index threshold alert type] +Fill in the <<defining-alerts-general-details, alert details>>, then select *Index Threshold*. [float] -==== Defining the conditions +==== Define the conditions -The index threshold has 5 clauses that define the condition to detect. +Define properties to detect the condition. [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-conditions.png[Five clauses define the condition to detect] @@ -32,9 +29,9 @@ If data is available and all clauses have been defined, a preview chart will ren image::user/alerting/images/alert-types-index-threshold-preview.png[Five clauses define the condition to detect] [float] -==== Action variables +==== Add action variables -When the index threshold alert condition is met, the following variables are available to use inside each action: +<<defining-alerts-actions-details, Add an action>> to run when the alert condition is met. The following variables are specific to the index threshold alert. You can also specify <<defining-alerts-actions-variables, variables common to all alerts>>. `context.title`:: A preconstructed title for the alert. Example: `alert kibana sites - high egress met threshold`. `context.message`:: A preconstructed message for the alert. Example: + @@ -51,68 +48,53 @@ When the index threshold alert condition is met, the following variables are ava [float] ==== Example -In this section, you will use the {kib} <<add-sample-data, weblog sample dataset>> to setup and tune the conditions on an index threshold alert. For this example, we want to detect when any of our top three sites have served more than 420,000 bytes over a 24 hour period. +In this example, you will use the {kib} <<add-sample-data, sample weblog dataset>> to set up and tune the conditions on an index threshold alert. For this example, you want to detect when any of the top four sites serve more than 420,000 bytes over a 24 hour period. -From the <<alert-management, alert management UI>>, create a new alert, and fill in the <<defining-alerts-general-details, general alert details>>. This alert will be checked every 4 hours, and will not execute actions more than once per day. Choose the index threshold alert type. +. Open the main menu, then click **Stack Management > Alerts and Actions**. +. Create a new alert that is checked every four hours and executes actions when the alert status changes. ++ [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-select.png[Choosing an index threshold alert type] -Click on each clause to open a control that helps you set the value: - -[float] -==== Index clause -The index clause control will list and allow you to search for available indices. Choose *kibana_sample_data_logs* +. Select the **Index threshold** alert type. +. Click *Index*, and set *Indices to query* to *kibana_sample_data_logs*. ++ [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-example-index.png[Choosing an index] -Once an index is selected, the list of time fields for that index will be available to select. Choose *@timestamp*. - +. Set the *Time field* to *@timestamp*. ++ [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-example-timefield.png[Choosing a time field] -[float] -==== When clause - -We want to detect the number of bytes served during the time window, so we select `sum` as the aggregation, and `bytes` as the field to aggregate. - +. To detect the number of bytes served during the time window, click *When* and select `sum` as the aggregation, and bytes as the field to aggregate. ++ [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-example-aggregation.png[Choosing the aggregation] -[float] -==== Over/Grouped over clause - -We want to alert on the three sites that have the most traffic, so we'll group the sum of bytes by the `host.keyword` field and take the top 3 values. - +. To detect the four sites that have the most traffic, click *Over* and select `top`, enter `4`, and select `host.keyword` as the field. ++ [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-example-grouping.png[Choosing the groups] -[float] -==== Threshold clause - -We want to alert when any site exceeds 420,000 bytes over a 24 hour period, so we'll set the threshold to 420,000 and use the `is above` comparison. - +. To alert when any of the top four sites exceeds 420,000 bytes over a 24 hour period, select `is above` and enter `420000`. ++ [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-example-threshold.png[Setting the threshold] -[float] -==== Time window clause - -Finally, set the time window to 24 hours to complete the alert configuration. - +. Finally, click *For the last*, enter `24` and select `hours` to complete the alert configuration. ++ [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-example-window.png[Setting the time window] -The preview chart will render showing the 24 hour sum of bytes at 4 hours intervals (the *check every* interval) for the past 120 hours (the last 30 intervals). - +. The preview chart will render showing the 24 hour sum of bytes at 4 hours intervals (the *check every* interval) for the past 120 hours (the last 30 intervals). ++ [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-example-preview.png[Setting the time window] -[float] -==== Comparing time windows - -You can interactively change the time window and observe the effect it has on the chart. Compare a 24 window to a 12 hour window. Notice the variability in the sum of bytes, due to different traffic levels during the day compared to at night. This variability would result in noisy alerts, so the 24 hour window is better. The preview chart can help you find the right values for your alert. - +. Change the time window and observe the effect it has on the chart. Compare a 24 window to a 12 hour window. Notice the variability in the sum of bytes, due to different traffic levels during the day compared to at night. This variability would result in noisy alerts, so the 24 hour window is better. The preview chart can help you find the right values for your alert. ++ [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-example-comparison.png[Comparing two time windows] \ No newline at end of file diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index f1be1ec12b79c5..3ec8545017ca11 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -434,7 +434,9 @@ Actions that take URLs or hostnames should check that those values are allowed. ## documentation -You should also create some asciidoc for the new action type. An entry should be made in the action type index - [`docs/user/alerting/action-types.asciidoc`](../../../docs/user/alerting/action-types.asciidoc) which points to a new document for the action type that should be in the directory [`docs/user/alerting/action-types`](../../../docs/user/alerting/action-types). +You should create asciidoc for the new action type. Add an entry to the action type index - [`docs/user/alerting/action-types.asciidoc`](../../../docs/user/alerting/action-types.asciidoc), which points to a new document for the action type that should be in the directory [`docs/user/alerting/action-types`](../../../docs/user/alerting/action-types). + +We suggest following the template provided in `docs/action-type-template.asciidoc`. The [Email action type](https://www.elastic.co/guide/en/kibana/master/email-action-type.html) is an example of documentation created following the template. ## tests diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 07bad42a3bfa3d..057d86e4c0f31c 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -19,9 +19,10 @@ Table of Contents - [Alert types](#alert-types) - [Methods](#methods) - [Executor](#executor) - - [Licensing](#licensing) - - [Documentation](#documentation) - - [Tests](#tests) + - [Action variables](#action-variables) + - [Licensing](#licensing) + - [Documentation](#documentation) + - [Tests](#tests) - [Example](#example) - [Role Based Access-Control](#role-based-access-control) - [Alert Navigation](#alert-navigation) @@ -147,9 +148,9 @@ This is the primary function for an alert type. Whenever the alert needs to exec |createdBy|The userid that created this alert.| |updatedBy|The userid that last updated this alert.| -### The `actionVariables` property +### Action Variables -This property should contain the **flattened** names of the state and context variables available when an executor calls `alertInstance.scheduleActions(actionGroup, context)`. These names are meant to be used in prompters in the alerting user interface, are used as text values for display, and can be inserted into to an action parameter text entry field via UI gesture (eg, clicking a menu item from a menu built with these names). They should be flattened, so if a state or context variable is an object with properties, these should be listed with the "parent" property/properties in the name, separated by a `.` (period). +The `actionVariables` property should contain the **flattened** names of the state and context variables available when an executor calls `alertInstance.scheduleActions(actionGroup, context)`. These names are meant to be used in prompters in the alerting user interface, are used as text values for display, and can be inserted into to an action parameter text entry field via UI gesture (eg, clicking a menu item from a menu built with these names). They should be flattened, so if a state or context variable is an object with properties, these should be listed with the "parent" property/properties in the name, separated by a `.` (period). For example, if the `context` has one variable `foo` which is an object that has one property `bar`, and there are no `state` variables, the `actionVariables` value would be in the following shape: @@ -167,7 +168,12 @@ Currently most of the alerts are free features. But some alert types are subscri ## Documentation -You should create documentation for the new alert type. Make an entry in the alert type index [`docs/user/alerting/alert-types.asciidoc`](../../../docs/user/alerting/alert-types.asciidoc) that points to a new document for the alert type that should be in the proper application directory. +You should create asciidoc for the new alert type. +* For stack alerts, add an entry to the alert type index - [`docs/user/alerting/alert-types.asciidoc`](../../../docs/user/alerting/alert-types.asciidoc) which points to a new document for the alert type that should be in the directory [`docs/user/alerting/stack-alerts`](../../../docs/user/alerting/stack-alerts). + +* Solution specific alert documentation should live within the docs for the solution. + +We suggest following the template provided in `docs/alert-type-template.asciidoc`. The [Index Threshold alert type](https://www.elastic.co/guide/en/kibana/master/alert-type-index-threshold.html) is an example of documentation created following the template. ## Tests From b7a215d38799562e7915bfde912fb610c3ced138 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Wed, 3 Mar 2021 11:56:46 +0300 Subject: [PATCH 31/63] [XY Chart] Fix "No data to display" error when using IP range aggregation to split series (#93024) (#93368) * Visualize: Can't use ip range to split series in xy chart * Refactor accessors.tsx * Revert "Refactor accessors.tsx" This reverts commit f2b088e251441cfaa8ab8104a3410ee877d4f08b. * Add accessors.test to cover getComplexAccessor function --- .../public/utils/accessors.test.ts | 101 ++++++++++++++++++ .../vis_type_xy/public/utils/accessors.tsx | 2 +- 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/plugins/vis_type_xy/public/utils/accessors.test.ts diff --git a/src/plugins/vis_type_xy/public/utils/accessors.test.ts b/src/plugins/vis_type_xy/public/utils/accessors.test.ts new file mode 100644 index 00000000000000..d074263e5bb25b --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/accessors.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { COMPLEX_SPLIT_ACCESSOR, getComplexAccessor } from './accessors'; +import { BUCKET_TYPES } from '../../../data/common'; +import { AccessorFn, Datum } from '@elastic/charts'; + +describe('XY chart datum accessors', () => { + const aspectBase = { + accessor: 'col-0-2', + formatter: (value: Datum) => value, + aggId: '', + title: '', + params: {}, + }; + + it('should return complex accessor for IP range aggregation', () => { + const aspect = { + aggType: BUCKET_TYPES.IP_RANGE, + ...aspectBase, + }; + const accessor = getComplexAccessor(COMPLEX_SPLIT_ACCESSOR)(aspect); + const datum = { + 'col-0-2': { type: 'range', from: '0.0.0.0', to: '127.255.255.255' }, + }; + + expect(typeof accessor).toBe('function'); + expect((accessor as AccessorFn)(datum)).toStrictEqual({ + type: 'range', + from: '0.0.0.0', + to: '127.255.255.255', + }); + }); + + it('should return complex accessor for date range aggregation', () => { + const aspect = { + aggType: BUCKET_TYPES.DATE_RANGE, + ...aspectBase, + }; + const accessor = getComplexAccessor(COMPLEX_SPLIT_ACCESSOR)(aspect); + const datum = { + 'col-0-2': { from: '1613941200000', to: '1614685113537' }, + }; + + expect(typeof accessor).toBe('function'); + expect((accessor as AccessorFn)(datum)).toStrictEqual({ + from: '1613941200000', + to: '1614685113537', + }); + }); + + it('should return complex accessor when isComplex option set to true', () => { + const aspect = { + aggType: BUCKET_TYPES.TERMS, + ...aspectBase, + }; + const accessor = getComplexAccessor(COMPLEX_SPLIT_ACCESSOR, true)(aspect); + + expect(typeof accessor).toBe('function'); + expect((accessor as AccessorFn)({ 'col-0-2': 'some value' })).toBe('some value'); + }); + + it('should return simple string accessor for not range (date histogram) aggregation', () => { + const aspect = { + aggType: BUCKET_TYPES.DATE_HISTOGRAM, + ...aspectBase, + }; + const accessor = getComplexAccessor(COMPLEX_SPLIT_ACCESSOR)(aspect); + + expect(typeof accessor).toBe('string'); + expect(accessor).toBe('col-0-2'); + }); + + it('should return simple string accessor when aspect has no formatter', () => { + const aspect = { + aggType: BUCKET_TYPES.RANGE, + ...aspectBase, + formatter: undefined, + }; + const accessor = getComplexAccessor(COMPLEX_SPLIT_ACCESSOR)(aspect); + + expect(typeof accessor).toBe('string'); + expect(accessor).toBe('col-0-2'); + }); + + it('should return undefined when aspect has no accessor', () => { + const aspect = { + aggType: BUCKET_TYPES.RANGE, + ...aspectBase, + accessor: null, + }; + const accessor = getComplexAccessor(COMPLEX_SPLIT_ACCESSOR)(aspect); + + expect(accessor).toBeUndefined(); + }); +}); diff --git a/src/plugins/vis_type_xy/public/utils/accessors.tsx b/src/plugins/vis_type_xy/public/utils/accessors.tsx index ccf709353b2a08..0f73ebe7eed868 100644 --- a/src/plugins/vis_type_xy/public/utils/accessors.tsx +++ b/src/plugins/vis_type_xy/public/utils/accessors.tsx @@ -27,7 +27,7 @@ const getFieldName = (fieldName: string, index?: number) => { }; export const isRangeAggType = (type: string | null) => - type === BUCKET_TYPES.DATE_RANGE || type === BUCKET_TYPES.RANGE; + type === BUCKET_TYPES.DATE_RANGE || type === BUCKET_TYPES.RANGE || type === BUCKET_TYPES.IP_RANGE; /** * Returns accessor function for complex accessor types From c9a26c335192560716ba677a8f21ea8db819b4a7 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 04:11:50 -0500 Subject: [PATCH 32/63] [CI] No longer collect APM span stack traces (#93263) (#93369) Signed-off-by: Tyler Smalley <tyler.smalley@elastic.co> Co-authored-by: Tyler Smalley <tyler.smalley@elastic.co> --- packages/kbn-apm-config-loader/src/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index 5a760cdabf91c9..8a3da17bc2bdde 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -34,6 +34,7 @@ const getDefaultConfig = (isDistributable: boolean): ApmAgentConfig => { globalLabels: {}, centralConfig: false, metricsInterval: isDistributable ? '120s' : '30s', + captureSpanStackTraces: false, transactionSampleRate: process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE ? parseFloat(process.env.ELASTIC_APM_TRANSACTION_SAMPLE_RATE) : 1.0, From 4ccabcde987f646c57fb4b2293b65dfd6dac45ae Mon Sep 17 00:00:00 2001 From: Maja Grubic <maja.grubic@elastic.co> Date: Wed, 3 Mar 2021 09:58:19 +0000 Subject: [PATCH 33/63] [Search Embeddable] Add highlighting when searching (#93178) (#93373) * [Search Embeddable] Add highlighting when searching * Adding a functional test --- .../embeddable/search_embeddable.ts | 5 ++ test/functional/apps/dashboard/index.ts | 1 + .../apps/dashboard/saved_search_embeddable.ts | 63 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 test/functional/apps/dashboard/saved_search_embeddable.ts diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 4ae0fb68056e58..1bf4cdc947be93 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -389,6 +389,11 @@ export class SearchEmbeddable if (forceFetch || isFetchRequired) { this.filtersSearchSource!.setField('filter', this.input.filters); this.filtersSearchSource!.setField('query', this.input.query); + if (this.input.query?.query || this.input.filters?.length) { + this.filtersSearchSource!.setField('highlightAll', true); + } else { + this.filtersSearchSource!.removeField('highlightAll'); + } this.prevFilters = this.input.filters; this.prevQuery = this.input.query; diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 212e747fadd975..d7bd9f6b515221 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -57,6 +57,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_back_button')); loadTestFile(require.resolve('./dashboard_error_handling')); loadTestFile(require.resolve('./legacy_urls')); + loadTestFile(require.resolve('./saved_search_embeddable')); // Note: This one must be last because it unloads some data for one of its tests! // No, this isn't ideal, but loading/unloading takes so much time and these are all bunched diff --git a/test/functional/apps/dashboard/saved_search_embeddable.ts b/test/functional/apps/dashboard/saved_search_embeddable.ts new file mode 100644 index 00000000000000..71f19b23da9dd9 --- /dev/null +++ b/test/functional/apps/dashboard/saved_search_embeddable.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const dashboardAddPanel = getService('dashboardAddPanel'); + const filterBar = getService('filterBar'); + const find = getService('find'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker', 'discover']); + + describe('dashboard saved search embeddable', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.loadIfNeeded('dashboard/current/data'); + await esArchiver.loadIfNeeded('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await filterBar.ensureFieldEditorModalIsClosed(); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.timePicker.setAbsoluteRange( + 'Sep 22, 2015 @ 00:00:00.000', + 'Sep 23, 2015 @ 00:00:00.000' + ); + }); + + it('highlighting on filtering works', async function () { + await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); + await filterBar.addFilter('agent', 'is', 'Mozilla'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + const dataTable = await find.byCssSelector(`[data-test-subj="embeddedSavedSearchDocTable"]`); + const $ = await dataTable.parseDomContent(); + const marks = $('mark') + .toArray() + .map((mark) => $(mark).text()); + expect(marks.length).to.be(50); + }); + + it('removing a filter removes highlights', async function () { + await filterBar.removeAllFilters(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + const dataTable = await find.byCssSelector(`[data-test-subj="embeddedSavedSearchDocTable"]`); + const $ = await dataTable.parseDomContent(); + const marks = $('mark') + .toArray() + .map((mark) => $(mark).text()); + expect(marks.length).to.be(0); + }); + }); +} From 950ade2893bd2d9f94ceb1f9a7338964cec9e227 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 05:51:49 -0500 Subject: [PATCH 34/63] [uptime] Fix anomaly alert edit (#93025) (#93382) Co-authored-by: Shahzad <shahzad.muhammad@elastic.co> --- .../alerts/uptime_edit_alert_flyout.tsx | 12 ++++----- .../components/monitor/ml/manage_ml_job.tsx | 26 ++++++++++++------- .../uptime/public/state/selectors/index.ts | 8 +++--- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx b/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx index 304b5882fb0a8c..aab08bf225b055 100644 --- a/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/common/alerts/uptime_edit_alert_flyout.tsx @@ -10,7 +10,7 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { Alert, TriggersAndActionsUIPublicPluginStart, -} from '../../../../../../plugins/triggers_actions_ui/public'; +} from '../../../../../triggers_actions_ui/public'; interface Props { alertFlyoutVisible: boolean; @@ -27,19 +27,17 @@ export const UptimeEditAlertFlyoutComponent = ({ initialAlert, setAlertFlyoutVisibility, }: Props) => { - const onClose = () => { - setAlertFlyoutVisibility(false); - }; const { triggersActionsUi } = useKibana<KibanaDeps>().services; const EditAlertFlyout = useMemo( () => triggersActionsUi.getEditAlertFlyout({ initialAlert, - onClose, + onClose: () => { + setAlertFlyoutVisibility(false); + }, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [initialAlert, setAlertFlyoutVisibility, triggersActionsUi] ); return <>{alertFlyoutVisible && EditAlertFlyout}</>; }; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx index d94918c7659cc9..719bc329c626a5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useContext, useState } from 'react'; +import React, { useCallback, useContext, useState } from 'react'; import { EuiButton, EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui'; import { useSelector, useDispatch } from 'react-redux'; @@ -14,6 +14,7 @@ import { canDeleteMLJobSelector, hasMLJobSelector, isMLJobCreatingSelector, + mlCapabilitiesSelector, } from '../../../state/selectors'; import { UptimeSettingsContext } from '../../../contexts'; import * as labels from './translations'; @@ -49,6 +50,7 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro const isAlertDeleting = useSelector(isAnomalyAlertDeleting); const { loading: isMLJobLoading } = useSelector(hasMLJobSelector); + const { loading: isCapbilityLoading } = useSelector(mlCapabilitiesSelector); const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); @@ -63,7 +65,7 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro const deleteAnomalyAlert = () => dispatch(deleteAnomalyAlertAction.get({ alertId: anomalyAlert?.id as string })); - const showLoading = isMLJobCreating || isMLJobLoading || isAlertDeleting; + const showLoading = isMLJobCreating || isMLJobLoading || isAlertDeleting || isCapbilityLoading; const btnText = hasMLJob ? labels.ANOMALY_DETECTION : labels.ENABLE_ANOMALY_DETECTION; @@ -149,6 +151,11 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro }, ]; + const onCloseFlyout = useCallback(() => { + setIsFlyoutOpen(false); + dispatch(getAnomalyAlertAction.get({ monitorId })); + }, [dispatch, monitorId]); + return ( <> <EuiPopover @@ -174,14 +181,13 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro }} /> )} - <UptimeEditAlertFlyoutComponent - initialAlert={anomalyAlert!} - alertFlyoutVisible={isFlyoutOpen} - setAlertFlyoutVisibility={() => { - setIsFlyoutOpen(false); - dispatch(getAnomalyAlertAction.get({ monitorId })); - }} - /> + {isFlyoutOpen && ( + <UptimeEditAlertFlyoutComponent + initialAlert={anomalyAlert!} + alertFlyoutVisible={isFlyoutOpen} + setAlertFlyoutVisibility={onCloseFlyout} + /> + )} </> ); }; diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts index afe4f25b7b18f6..53aaa5ca30ea6d 100644 --- a/x-pack/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/plugins/uptime/public/state/selectors/index.ts @@ -37,22 +37,22 @@ export const selectPingList = ({ pingList }: AppState) => pingList; export const snapshotDataSelector = ({ snapshot }: AppState) => snapshot; -const mlCapabilitiesSelector = (state: AppState) => state.ml.mlCapabilities.data; +export const mlCapabilitiesSelector = (state: AppState) => state.ml.mlCapabilities; export const hasMLFeatureSelector = createSelector( mlCapabilitiesSelector, (mlCapabilities) => - mlCapabilities?.isPlatinumOrTrialLicense && mlCapabilities?.mlFeatureEnabledInSpace + mlCapabilities?.data?.isPlatinumOrTrialLicense && mlCapabilities?.data?.mlFeatureEnabledInSpace ); export const canCreateMLJobSelector = createSelector( mlCapabilitiesSelector, - (mlCapabilities) => mlCapabilities?.capabilities.canCreateJob + (mlCapabilities) => mlCapabilities?.data?.capabilities?.canCreateJob ); export const canDeleteMLJobSelector = createSelector( mlCapabilitiesSelector, - (mlCapabilities) => mlCapabilities?.capabilities.canDeleteJob + (mlCapabilities) => mlCapabilities?.data?.capabilities?.canDeleteJob ); export const hasMLJobSelector = ({ ml }: AppState) => ml.mlJob; From 8ef74d02f518ebab4cf0bdeea5e4fb69893c6232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Casper=20H=C3=BCbertz?= <casper@elastic.co> Date: Wed, 3 Mar 2021 12:14:29 +0100 Subject: [PATCH 35/63] [APM] Add missing bottom border to header (#93179) (#93375) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index 2ba2ae4b5acb6f..f81157c5cffd50 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -13,6 +13,7 @@ import { EnvironmentFilter } from '../EnvironmentFilter'; const HeaderFlexGroup = euiStyled(EuiFlexGroup)` padding: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; background: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; `; export function ApmHeader({ children }: { children: ReactNode }) { From d5e64461db8966a59d972c87c4719a7907b7ad74 Mon Sep 17 00:00:00 2001 From: Joe Reuter <johannes.reuter@elastic.co> Date: Wed, 3 Mar 2021 14:18:47 +0100 Subject: [PATCH 36/63] remove portal for screenreader component (#93274) (#93403) --- .../public/drag_drop/providers/providers.tsx | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx index 6a78bc1b46ddfd..2c6b07ea117658 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers/providers.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useMemo } from 'react'; -import { EuiScreenReaderOnly, EuiPortal } from '@elastic/eui'; +import { EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DropIdentifier, @@ -103,25 +103,23 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode } > {children} </ChildDragDropProvider> - <EuiPortal> - <EuiScreenReaderOnly> - <div> - <p aria-live="assertive" aria-atomic={true}> - {a11yMessageState} - </p> - <p id={`lnsDragDrop-keyboardInstructionsWithReorder`}> - {i18n.translate('xpack.lens.dragDrop.keyboardInstructionsReorder', { - defaultMessage: `Press space or enter to start dragging. When dragging, use the up/down arrow keys to reorder items in the group and left/right arrow keys to choose drop targets outside of the group. Press space or enter again to finish.`, - })} - </p> - <p id={`lnsDragDrop-keyboardInstructions`}> - {i18n.translate('xpack.lens.dragDrop.keyboardInstructions', { - defaultMessage: `Press space or enter to start dragging. When dragging, use the left/right arrow keys to move between drop targets. Press space or enter again to finish.`, - })} - </p> - </div> - </EuiScreenReaderOnly> - </EuiPortal> + <EuiScreenReaderOnly> + <div> + <p aria-live="assertive" aria-atomic={true}> + {a11yMessageState} + </p> + <p id={`lnsDragDrop-keyboardInstructionsWithReorder`}> + {i18n.translate('xpack.lens.dragDrop.keyboardInstructionsReorder', { + defaultMessage: `Press space or enter to start dragging. When dragging, use the up/down arrow keys to reorder items in the group and left/right arrow keys to choose drop targets outside of the group. Press space or enter again to finish.`, + })} + </p> + <p id={`lnsDragDrop-keyboardInstructions`}> + {i18n.translate('xpack.lens.dragDrop.keyboardInstructions', { + defaultMessage: `Press space or enter to start dragging. When dragging, use the left/right arrow keys to move between drop targets. Press space or enter again to finish.`, + })} + </p> + </div> + </EuiScreenReaderOnly> </div> ); } From 112948ddf175a9d4124e0c4af94ac074540e7f20 Mon Sep 17 00:00:00 2001 From: Marta Bondyra <marta.bondyra@gmail.com> Date: Wed, 3 Mar 2021 15:24:20 +0100 Subject: [PATCH 37/63] [Lens] fix long field name on field stats panel doesn't wrap (#93279) (#93391) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/components/sidebar/discover_field.tsx | 4 +++- .../lens/public/indexpattern_datasource/field_item.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 8cd63f09e0d2cf..b0d71c774f445f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -305,7 +305,9 @@ export function DiscoverField({ anchorPosition="rightUp" panelClassName="dscSidebarItem__fieldPopoverPanel" > - <EuiPopoverTitle style={{ textTransform: 'none' }}>{field.displayName}</EuiPopoverTitle> + <EuiPopoverTitle style={{ textTransform: 'none' }} className="eui-textBreakWord"> + {field.displayName} + </EuiPopoverTitle> <EuiTitle size="xxxs"> <h5> {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index b1784b85d127d3..c8fe7180586956 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -289,7 +289,7 @@ function FieldPanelHeader({ <EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}> <EuiFlexItem> <EuiTitle size="xxs"> - <h5 className="lnsFieldItem__fieldPanelTitle">{field.displayName}</h5> + <h5 className="eui-textBreakWord lnsFieldItem__fieldPanelTitle">{field.displayName}</h5> </EuiTitle> </EuiFlexItem> From 5174f8316bed5c94dc98c73a35154f5277444aed Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 10:02:00 -0500 Subject: [PATCH 38/63] Bump handlebars from 4.7.6 to 4.7.7 (#93396) (#93414) Co-authored-by: Thomas Watson <w@tson.dk> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 172721604543a5..1ae6f1cb6198f5 100644 --- a/package.json +++ b/package.json @@ -209,7 +209,7 @@ "graphql-fields": "^1.0.2", "graphql-tag": "^2.10.3", "graphql-tools": "^3.0.2", - "handlebars": "4.7.6", + "handlebars": "4.7.7", "history": "^4.9.0", "hjson": "3.2.1", "http-proxy-agent": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 1b29b4250ab30c..b6afbff9b7ef9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16203,10 +16203,10 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@4.7.6, handlebars@^4.7.6: - version "4.7.6" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" - integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== +handlebars@4.7.7, handlebars@^4.7.6: + version "4.7.7" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== dependencies: minimist "^1.2.5" neo-async "^2.6.0" From b522ac19b947f452b62601b30702682cc5bdedb3 Mon Sep 17 00:00:00 2001 From: Liza Katz <lizka.k@gmail.com> Date: Wed, 3 Mar 2021 18:07:06 +0200 Subject: [PATCH 39/63] [Bug] Fix filter creation for numeric scripted fields in Discover (#93224) (#93425) * fixes #74301 * comment * tests * test title --- .../es_query/filters/phrase_filter.test.ts | 54 ++++++++++++++++++- .../common/es_query/filters/phrase_filter.ts | 15 ++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.test.ts b/src/plugins/data/common/es_query/filters/phrase_filter.test.ts index 1505f6a628ddc2..513f0e29b5b245 100644 --- a/src/plugins/data/common/es_query/filters/phrase_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/phrase_filter.test.ts @@ -27,10 +27,25 @@ describe('Phrase filter builder', () => { expect(typeof buildPhraseFilter).toBe('function'); }); - it('should return a match query filter when passed a standard field', () => { + it('should return a match query filter when passed a standard string field', () => { + const field = getField('extension'); + + expect(buildPhraseFilter(field, 'jpg', indexPattern)).toEqual({ + meta: { + index: 'id', + }, + query: { + match_phrase: { + extension: 'jpg', + }, + }, + }); + }); + + it('should return a match query filter when passed a standard numeric field', () => { const field = getField('bytes'); - expect(buildPhraseFilter(field, 5, indexPattern)).toEqual({ + expect(buildPhraseFilter(field, '5', indexPattern)).toEqual({ meta: { index: 'id', }, @@ -42,6 +57,21 @@ describe('Phrase filter builder', () => { }); }); + it('should return a match query filter when passed a standard bool field', () => { + const field = getField('ssl'); + + expect(buildPhraseFilter(field, 'true', indexPattern)).toEqual({ + meta: { + index: 'id', + }, + query: { + match_phrase: { + ssl: true, + }, + }, + }); + }); + it('should return a script filter when passed a scripted field', () => { const field = getField('script number'); @@ -61,6 +91,26 @@ describe('Phrase filter builder', () => { }, }); }); + + it('should return a script filter when passed a scripted field with numeric conversion', () => { + const field = getField('script number'); + + expect(buildPhraseFilter(field, '5', indexPattern)).toEqual({ + meta: { + index: 'id', + field: 'script number', + }, + script: { + script: { + lang: 'expression', + params: { + value: 5, + }, + source: '(1234) == value', + }, + }, + }); + }); }); describe('buildInlineScriptForPhraseFilter', () => { diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.ts b/src/plugins/data/common/es_query/filters/phrase_filter.ts index 2aeaa5272787fc..364e8dc1b035fd 100644 --- a/src/plugins/data/common/es_query/filters/phrase_filter.ts +++ b/src/plugins/data/common/es_query/filters/phrase_filter.ts @@ -96,9 +96,14 @@ export const getPhraseScript = (field: IFieldType, value: string) => { }; }; -// See https://github.com/elastic/elasticsearch/issues/20941 and https://github.com/elastic/kibana/issues/8677 -// and https://github.com/elastic/elasticsearch/pull/22201 -// for the reason behind this change. Aggs now return boolean buckets with a key of 1 or 0. +/** + * See issues bellow for the reason behind this change. + * Values need to be converted to correct types for boolean \ numeric fields. + * https://github.com/elastic/kibana/issues/74301 + * https://github.com/elastic/kibana/issues/8677 + * https://github.com/elastic/elasticsearch/issues/20941 + * https://github.com/elastic/elasticsearch/pull/22201 + **/ export const getConvertedValueForField = (field: IFieldType, value: any) => { if (typeof value !== 'boolean' && field.type === 'boolean') { if ([1, 'true'].includes(value)) { @@ -109,6 +114,10 @@ export const getConvertedValueForField = (field: IFieldType, value: any) => { throw new Error(`${value} is not a valid boolean value for boolean field ${field.name}`); } } + + if (typeof value !== 'number' && field.type === 'number') { + return Number(value); + } return value; }; From 8ace353fc70389d1eb72e799610bd1f6ce7f1381 Mon Sep 17 00:00:00 2001 From: Peter Pisljar <peter.pisljar@gmail.com> Date: Wed, 3 Mar 2021 17:24:12 +0100 Subject: [PATCH 40/63] adding schema for all current query_string settings (#93175) (#93429) --- src/plugins/data/server/ui_settings.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index 2df6e4cc34f10b..6d68d29d8df69c 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -93,7 +93,27 @@ export function getUiSettings(): Record<string, UiSettingsParams<unknown>> { }), type: 'json', schema: schema.object({ + default_field: schema.nullable(schema.string()), + allow_leading_wildcard: schema.nullable(schema.boolean()), analyze_wildcard: schema.boolean(), + analyzer: schema.nullable(schema.string()), + auto_generate_synonyms_phrase_query: schema.nullable(schema.boolean()), + boost: schema.nullable(schema.number()), + default_operator: schema.nullable(schema.string()), + enable_position_increments: schema.nullable(schema.boolean()), + fields: schema.nullable(schema.arrayOf<string>(schema.string())), + fuzziness: schema.nullable(schema.string()), + fuzzy_max_expansions: schema.nullable(schema.number()), + fuzzy_prefix_length: schema.nullable(schema.number()), + fuzzy_transpositions: schema.nullable(schema.boolean()), + lenient: schema.nullable(schema.boolean()), + max_determinized_states: schema.nullable(schema.number()), + minimum_should_match: schema.nullable(schema.string()), + quote_analyzer: schema.nullable(schema.string()), + phrase_slop: schema.nullable(schema.number()), + quote_field_suffix: schema.nullable(schema.string()), + rewrite: schema.nullable(schema.string()), + time_zone: schema.nullable(schema.string()), }), }, [UI_SETTINGS.QUERY_ALLOW_LEADING_WILDCARDS]: { From 14cc0a8623b9e384798cc823ea127ef7dbd70704 Mon Sep 17 00:00:00 2001 From: Marta Bondyra <marta.bondyra@gmail.com> Date: Wed, 3 Mar 2021 18:02:11 +0100 Subject: [PATCH 41/63] [Lens] Fix unintentional switching to pie (#93219) (#93439) --- .../pie_visualization/visualization.test.ts | 34 +++++++++++++++++-- .../pie_visualization/visualization.tsx | 5 --- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts index 0cdeaa8c043d83..2a961cef315bf2 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts @@ -17,7 +17,7 @@ const pieVisualization = getPieVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), }); -function exampleState(): PieVisualizationState { +function getExampleState(): PieVisualizationState { return { shape: 'pie', layers: [ @@ -38,9 +38,39 @@ function exampleState(): PieVisualizationState { describe('pie_visualization', () => { describe('#getErrorMessages', () => { it('returns undefined if no error is raised', () => { - const error = pieVisualization.getErrorMessages(exampleState()); + const error = pieVisualization.getErrorMessages(getExampleState()); expect(error).not.toBeDefined(); }); }); + describe('#setDimension', () => { + it('returns expected state', () => { + const prevState: PieVisualizationState = { + layers: [ + { + groups: ['a'], + layerId: LAYER_ID, + numberDisplay: 'percent', + categoryDisplay: 'default', + legendDisplay: 'default', + nestedLegend: false, + metric: undefined, + }, + ], + shape: 'donut', + }; + const setDimensionResult = pieVisualization.setDimension({ + prevState, + columnId: 'x', + layerId: LAYER_ID, + groupId: 'a', + }); + + expect(setDimensionResult).toEqual( + expect.objectContaining({ + shape: 'donut', + }) + ); + }); + }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 683acc49859b68..c9c0463a118f56 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -193,11 +193,6 @@ export const getPieVisualization = ({ setDimension({ prevState, layerId, columnId, groupId }) { return { ...prevState, - - shape: - prevState.shape === 'donut' && prevState.layers.every((l) => l.groups.length === 1) - ? 'pie' - : prevState.shape, layers: prevState.layers.map((l) => { if (l.layerId !== layerId) { return l; From dd8f63edf510d8cc30fdc467da60af5c9263165b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ece=20=C3=96zalp?= <ozale272@newschool.edu> Date: Wed, 3 Mar 2021 12:24:41 -0500 Subject: [PATCH 42/63] Add searchDuration to EQL and Threshold rules (#93149) (#93335) Closes #82861. --- .../signals/signal_rule_alert_type.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 1fd0552569b2d8..78dfa4bbf5780f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -11,6 +11,7 @@ import { Logger, KibanaRequest } from 'src/core/server'; import isEmpty from 'lodash/isEmpty'; import { chain, tryCatch } from 'fp-ts/lib/TaskEither'; import { flow } from 'fp-ts/lib/function'; +import { performance } from 'perf_hooks'; import { toError, toPromise } from '../../../../common/fp_utils'; @@ -52,6 +53,7 @@ import { checkPrivileges, hasTimestampFields, hasReadIndexPrivileges, + makeFloatString, } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; @@ -409,7 +411,11 @@ export const signalRulesAlertType = ({ lists: exceptionItems ?? [], }); - const { searchResult: thresholdResults, searchErrors } = await findThresholdSignals({ + const { + searchResult: thresholdResults, + searchErrors, + searchDuration: thresholdSearchDuration, + } = await findThresholdSignals({ inputIndexPattern: inputIndex, from, to, @@ -464,6 +470,7 @@ export const signalRulesAlertType = ({ createdSignalsCount: createdItemsCount, createdSignals: createdItems, bulkCreateTimes: bulkCreateDuration ? [bulkCreateDuration] : [], + searchAfterTimes: [thresholdSearchDuration], }), ]); } else if (isThreatMatchRule(type)) { @@ -599,10 +606,14 @@ export const signalRulesAlertType = ({ exceptionItems ?? [], eventCategoryOverride ); + const eqlSignalSearchStart = performance.now(); const response: EqlSignalSearchResponse = await services.callCluster( 'transport.request', request ); + const eqlSignalSearchEnd = performance.now(); + const eqlSearchDuration = makeFloatString(eqlSignalSearchEnd - eqlSignalSearchStart); + result.searchAfterTimes = [eqlSearchDuration]; let newSignals: WrappedSignalHit[] | undefined; if (response.hits.sequences !== undefined) { newSignals = response.hits.sequences.reduce( @@ -643,7 +654,6 @@ export const signalRulesAlertType = ({ const fromInMs = parseScheduleDates(`now-${interval}`)?.format('x'); const toInMs = parseScheduleDates('now')?.format('x'); - const resultsLink = getNotificationResultsLink({ from: fromInMs, to: toInMs, From 37c5da4a976cca8e9e90ae912732c97f7bf8f293 Mon Sep 17 00:00:00 2001 From: Bhavya RM <bhavya@elastic.co> Date: Wed, 3 Mar 2021 13:10:59 -0500 Subject: [PATCH 43/63] [7.12] removing the linked issue in comments from PR (#93303) (#93351) * removing the linked issue in comments from PR (#93303) # Conflicts: # test/functional/apps/management/_import_objects.ts * addressing es-lint errors by removing blank lines * Fixing the eslint error again Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/management/_import_objects.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index e2a056359b48e2..a3daaf86294939 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -23,8 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/89478 - describe.skip('import objects', function describeIndexTests() { + describe('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { await esArchiver.load('management'); From 780febd22b9217f5e6e1f5c65c3bf62cc9deb796 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper <Zacqary@users.noreply.github.com> Date: Wed, 3 Mar 2021 12:54:55 -0600 Subject: [PATCH 44/63] [Metrics UI] Fix removing warning threshold from alert expressions (#93338) (#93455) --- .../infra/public/alerting/inventory/components/expression.tsx | 2 +- .../public/alerting/metric_threshold/components/expression.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx index d43bbb6888a6e0..7233ce3de74971 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/expression.tsx @@ -137,7 +137,7 @@ export const Expressions: React.FC<Props> = (props) => { const updateParams = useCallback( (id, e: InventoryMetricConditions) => { const exp = alertParams.criteria ? alertParams.criteria.slice() : []; - exp[id] = { ...exp[id], ...e }; + exp[id] = e; setAlertParams('criteria', exp); }, [setAlertParams, alertParams.criteria] 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 64190f55577071..6b6de68bae158c 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 @@ -102,7 +102,7 @@ export const Expressions: React.FC<Props> = (props) => { const updateParams = useCallback( (id, e: MetricExpression) => { const exp = alertParams.criteria ? alertParams.criteria.slice() : []; - exp[id] = { ...exp[id], ...e }; + exp[id] = e; setAlertParams('criteria', exp); }, [setAlertParams, alertParams.criteria] From b823a8c059f7dd60dbb8075f63379333b84a79cd Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni <rashmi.kulkarni@elastic.co> Date: Wed, 3 Mar 2021 11:17:03 -0800 Subject: [PATCH 45/63] Test huge fields functional test (#93334) (#93462) * fixes https://github.com/elastic/kibana/issues/74449 * fix for unskipping test huge fields functional test * fix eslint --- .../apps/management/_test_huge_fields.js | 6 +++--- .../es_archiver/large_fields/data.json.gz | Bin 48347 -> 48347 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.js index 3102becbe181f6..b9c9e964ac3f54 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.js @@ -13,15 +13,15 @@ export default function ({ getService, getPageObjects }) { const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings']); - // Failing: See https://github.com/elastic/kibana/issues/89031 - describe.skip('test large number of fields', function () { + describe('test large number of fields', function () { this.tags(['skipCloud']); const EXPECTED_FIELD_COUNT = '10006'; before(async function () { - await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); + await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('large_fields'); + await PageObjects.settings.navigateTo(); await PageObjects.settings.createIndexPattern('testhuge', 'date'); }); diff --git a/test/functional/fixtures/es_archiver/large_fields/data.json.gz b/test/functional/fixtures/es_archiver/large_fields/data.json.gz index e3879e288b6f21c93ac50be1f78fee8e15448b50..ff82acd5842ee4d20e384c929142bf63a56a8ce5 100644 GIT binary patch delta 282 zcmV+#0p<SN`2ySd0)HQi2mk;80006LWc^*P>{!60=iPf0g03x~7!mwQjl&ZV)D7E! zZ5dKau%OAizk1h!;qwA@Q=4Ze$w+cBb|$%=|KtDk-~2cK?SJ{-|KI=lzy0t3oB#Gd z{ZIenfBS#_k01PR|HJ>|fBlc2{2%}IfBx(L{@?%8e=NQJU4Ql;|NH;?PyhM<^B+I? zFaP;}`7i&s|Mma+pZ_fQU;d~6^?&{!|9`*tUw`tS{)hka?<?n@>-q1lfBw6F?*GfR z|NftU{_B6|m;UQN`hWiK|J(oYKmYSz|JgtP|3A9^>yNI#e)J#z?E3rP{licF<MVSb gxLBZ9|0gVn7P0=(Rsl=1&{qRx2uvZj8EeP`0Hrjy7ytkO delta 282 zcmV+#0p<SN`2ySd0)HQi2mk;80006LWc^#N>{!5T=e@fX!g-c}VnpzgYKJ=@NC&n7 z+cI>61$Ex%uQ9g`?*Y_HE$)>hBgx5FndJQbkN?wu^<Vv`|K)%GfBonG^xyth|LK4D zAOHLR^k4tS5B|6R?*IP3{>M-LkN@&N|K)%GhyU(>`FDB#xqqJj=KANq`RD#WU;A(W z`RBj<_wT>|FaPBK_CNf`Ke+zC|NH;^AO7?I`#*m2zx?O_`G5Jp{;&U+|NLjc|NKAx zum973|NndG|MHXn$N&Dn{dfP<KmX+?|NPhge?PkZ%a5+Ve)J#z?E3rP{licF<MVSb gxLBZ9|0gVn7P0=(Rsln^&{qRx2<wGyB5BA20F-9C=>Px# From ad756250dd2f953f6defb2b47a6b98eb0d4a0967 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 14:37:57 -0500 Subject: [PATCH 46/63] [ML] fix alert instance key for the single metric job (#93442) (#93467) Co-authored-by: Dima Arnautov <dmitrii.arnautov@elastic.co> --- x-pack/plugins/ml/server/lib/alerts/alerting_service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts index b6d0e9ae261c70..e0fec5938d5213 100644 --- a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts @@ -258,7 +258,9 @@ export function alertingServiceProvider(mlClient: MlClient, esClient: Elasticsea } else if (source.result_type === ANOMALY_RESULT_TYPE.RECORD) { const fieldName = getEntityFieldName(source); const fieldValue = getEntityFieldValue(source); - alertInstanceKey += `_${source.detector_index}_${source.function}_${fieldName}_${fieldValue}`; + const entity = + fieldName !== undefined && fieldValue !== undefined ? `_${fieldName}_${fieldValue}` : ''; + alertInstanceKey += `_${source.detector_index}_${source.function}${entity}`; } return alertInstanceKey; }; From ccbaa999d487ed1ac4931e4f6364abaafbc9143a Mon Sep 17 00:00:00 2001 From: Devon Thomson <devon.thomson@hotmail.com> Date: Wed, 3 Mar 2021 15:35:25 -0500 Subject: [PATCH 47/63] fixed getIsDirty, used function for disabling save button (#93328) (#93480) --- src/plugins/dashboard/public/application/dashboard_app.tsx | 2 +- .../dashboard/public/application/dashboard_state_manager.ts | 5 ++++- .../public/application/top_nav/dashboard_top_nav.tsx | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index fd73741cef8cbd..3d6f08f3219779 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -218,7 +218,7 @@ export function DashboardApp({ ); dashboardStateManager.registerChangeListener(() => { - setUnsavedChanges(dashboardStateManager?.hasUnsavedPanelState()); + setUnsavedChanges(dashboardStateManager.getIsDirty(data.query.timefilter.timefilter)); // we aren't checking dirty state because there are changes the container needs to know about // that won't make the dashboard "dirty" - like a view mode change. triggerRefresh$.next(); diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index d11bdd0399d411..58b413f4303e66 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -558,7 +558,10 @@ export class DashboardStateManager { // Filter bar comparison is done manually (see cleanFiltersForComparison for the reason) and time picker // changes are not tracked by the state monitor. const hasTimeFilterChanged = timeFilter ? this.getFiltersChanged(timeFilter) : false; - return this.getIsEditMode() && (this.isDirty || hasTimeFilterChanged); + return ( + this.hasUnsavedPanelState() || + (this.getIsEditMode() && (this.isDirty || hasTimeFilterChanged)) + ); } public getPanels(): SavedDashboardPanel[] { diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 65cb4db5ad543d..4c67acbfc79df2 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -479,7 +479,7 @@ export function DashboardTopNav({ const topNav = getTopNavConfig(viewMode, dashboardTopNavActions, { hideWriteControls: dashboardCapabilities.hideWriteControls, isNewDashboard: !savedDashboard.id, - isDirty: dashboardStateManager.isDirty, + isDirty: dashboardStateManager.getIsDirty(timefilter), isSaveInProgress, }); From 6ea239955916cd28a37df3d550236536fe1a5adb Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 16:58:42 -0500 Subject: [PATCH 48/63] [alerting] adds doc on JSON-expanded action variables and task manager max_workers (#92720) (#93520) resolves https://github.com/elastic/kibana/issues/90006 For task manager, adds a note about the fact that the max_workers will be limited to 100 starting in 8.0. Currently we allow any value (because we always have), but do print a "deprecation" warning that the limit cannot be exceeded starting in 8.0 For alerting, adds note about the JSON expansion of action variables which are objects. Co-authored-by: Patrick Mueller <pmuellr@gmail.com> --- docs/settings/task-manager-settings.asciidoc | 2 +- docs/user/alerting/defining-alerts.asciidoc | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc index 507e54349276bd..52878279ff0611 100644 --- a/docs/settings/task-manager-settings.asciidoc +++ b/docs/settings/task-manager-settings.asciidoc @@ -27,6 +27,6 @@ Task Manager runs background tasks by polling for work on an interval. You can | `xpack.task_manager.max_workers` | The maximum number of tasks that this Kibana instance will run simultaneously. Defaults to 10. - + Starting in 8.0, it will not be possible to set the value greater than 100. |=== diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 27f3a6c7309cb0..8f1a0f06f75ae3 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -95,6 +95,10 @@ Some cases exist where the variable values will be "escaped", when used in a con Mustache also supports "triple braces" of the form `{{{variable name}}}`, which indicates no escaping should be done at all. Care should be used when using this form, as it could end up rendering the variable content in such a way as to make the resulting parameter invalid or formatted incorrectly. +Each alert type defines additional variables as properties of the variable `context`. For example, if an alert type defines a variable `value`, it can be used in an action parameter as `{{context.value}}`. + +For diagnostic or exploratory purposes, action variables whose values are objects, such as `context`, can be referenced directly as variables. The resulting value will be a JSON representation of the object. For example, if an action parameter includes `{{context}}`, it will expand to the JSON representation of all the variables and values provided by the alert type. + You can attach more than one action. Clicking the "Add action" button will prompt you to select another alert type and repeat the above steps again. [role="screenshot"] From a6f9c4057620942b51971308dec80f20873e13a0 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Wed, 3 Mar 2021 22:01:52 +0000 Subject: [PATCH 49/63] [7.12] [Security Solution] Case ui enhancement (#91863) (#93395) * [Security Solution] Case ui enhancement (#91863) * ui enhancement * fix actions * unit test * update row actions * add case status all * update find status * fix type * remove all case count from dropdown * fix type error * fix unit test * disable bulk actions on status all * clean up * fix types * fix cypress tests * review * review * update status is only available for individual cases * update available actions on status all * fix unit test * remove lodash get * rename status all * omit status if it is set to all * do not sent status if itis set to all * Remove all status from the backend * Hide actions on all status * fix unit test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christos Nasikas <christos.nasikas@elastic.co> # Conflicts: # x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx # x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx # x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx # x-pack/plugins/security_solution/public/cases/components/status/config.ts * fix unit tests * fix cypress test --- .../server/routes/api/cases/find_cases.ts | 2 - .../case/server/routes/api/cases/helpers.ts | 2 +- .../integration/cases/creation.spec.ts | 4 + .../cypress/screens/all_cases.ts | 2 + .../cypress/tasks/create_new_case.ts | 6 + .../cases/components/all_cases/actions.tsx | 26 ++- .../cases/components/all_cases/columns.tsx | 4 +- .../cases/components/all_cases/helpers.ts | 12 +- .../cases/components/all_cases/index.test.tsx | 153 ++++++++++++++++++ .../cases/components/all_cases/index.tsx | 30 ++-- .../all_cases/status_filter.test.tsx | 2 + .../components/all_cases/status_filter.tsx | 20 +-- .../components/all_cases/table_filters.tsx | 11 +- .../cases/components/bulk_actions/index.tsx | 78 +++++---- .../case_action_bar/status_context_menu.tsx | 5 +- .../public/cases/components/status/config.ts | 22 +-- .../public/cases/components/status/index.ts | 1 + .../public/cases/components/status/status.tsx | 10 +- .../cases/components/status/translations.ts | 4 + .../public/cases/components/status/types.ts | 34 ++++ .../public/cases/containers/api.test.tsx | 1 - .../public/cases/containers/api.ts | 8 +- .../public/cases/containers/types.ts | 5 +- .../public/cases/containers/use_get_cases.tsx | 4 +- 24 files changed, 342 insertions(+), 104 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/cases/components/status/types.ts diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts index d04f01eb735379..bc6907f52b9eba 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts @@ -37,7 +37,6 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { CasesFindRequestRt.decode(request.query), fold(throwErrors(Boom.badRequest), identity) ); - const queryArgs = { tags: queryParams.tags, reporters: queryParams.reporters, @@ -47,7 +46,6 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { }; const caseQueries = constructQueryOptions(queryArgs); - const cases = await caseService.findCasesGroupedByID({ client, caseOptions: { ...queryParams, ...caseQueries.case }, diff --git a/x-pack/plugins/case/server/routes/api/cases/helpers.ts b/x-pack/plugins/case/server/routes/api/cases/helpers.ts index a1a7f4f9da8f5b..8659ab02d6d532 100644 --- a/x-pack/plugins/case/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/case/server/routes/api/cases/helpers.ts @@ -28,7 +28,7 @@ export const addStatusFilter = ({ appendFilter, type = CASE_SAVED_OBJECT, }: { - status: CaseStatuses | undefined; + status?: CaseStatuses; appendFilter?: string; type?: string; }) => { diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts index 5a2cf5408b04de..8cc49544eae868 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts @@ -6,6 +6,7 @@ */ import { case1 } from '../../objects/case'; +import { COLLAPSED_ACTION_BTN } from '../../screens/alerts_detection_rules'; import { ALL_CASES_CLOSE_ACTION, @@ -47,6 +48,7 @@ import { backToCases, createCase, fillCasesMandatoryfields, + filterStatusOpen, } from '../../tasks/create_new_case'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; @@ -75,6 +77,7 @@ describe('Cases', () => { attachTimeline(this.mycase); createCase(); backToCases(); + filterStatusOpen(); cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases'); cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', 'Open cases1'); @@ -91,6 +94,7 @@ describe('Cases', () => { cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', '0'); cy.get(ALL_CASES_OPENED_ON).should('include.text', 'ago'); cy.get(ALL_CASES_SERVICE_NOW_INCIDENT).should('have.text', 'Not pushed'); + cy.get(COLLAPSED_ACTION_BTN).click(); cy.get(ALL_CASES_DELETE_ACTION).should('exist'); cy.get(ALL_CASES_CLOSE_ACTION).should('exist'); diff --git a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts index 06d1a9fca91c67..7a951de756ae05 100644 --- a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts +++ b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts @@ -27,6 +27,8 @@ export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]'; export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="case-status-filter"]'; +export const ALL_CASES_OPEN_FILTER = '[data-test-subj="case-status-filter-open"]'; + export const ALL_CASES_OPEN_CASES_STATS = '[data-test-subj="openStatsHeader"]'; export const ALL_CASES_OPENED_ON = '[data-test-subj="case-table-column-createdAt"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts index e67cee4f38734e..ed9174e2a74bb9 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts @@ -11,6 +11,7 @@ import { ServiceNowconnectorOptions, TestCase, } from '../objects/case'; +import { ALL_CASES_OPEN_CASES_COUNT, ALL_CASES_OPEN_FILTER } from '../screens/all_cases'; import { BACK_TO_CASES_BTN, @@ -40,6 +41,11 @@ export const backToCases = () => { cy.get(BACK_TO_CASES_BTN).click({ force: true }); }; +export const filterStatusOpen = () => { + cy.get(ALL_CASES_OPEN_CASES_COUNT).click(); + cy.get(ALL_CASES_OPEN_FILTER).click(); +}; + export const fillCasesMandatoryfields = (newCase: TestCase) => { cy.get(TITLE_INPUT).type(newCase.name, { force: true }); newCase.tags.forEach((tag) => { diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx index 8178e7e9f9e8f8..6ee5f6ff41d457 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx @@ -12,6 +12,7 @@ import { CaseStatuses } from '../../../../../case/common/api'; import { Case, SubCase } from '../../containers/types'; import { UpdateCase } from '../../containers/use_get_cases'; import * as i18n from './translations'; +import { isIndividual } from './helpers'; interface GetActions { caseStatus: string; @@ -19,24 +20,13 @@ interface GetActions { deleteCaseOnClick: (deleteCase: Case) => void; } -const hasSubCases = (subCases: SubCase[] | null | undefined) => - subCases != null && subCases?.length > 0; - export const getActions = ({ caseStatus, dispatchUpdate, deleteCaseOnClick, }: GetActions): Array<DefaultItemIconButtonAction<Case>> => [ { - description: i18n.DELETE_CASE, - icon: 'trash', - name: i18n.DELETE_CASE, - onClick: deleteCaseOnClick, - type: 'icon', - 'data-test-subj': 'action-delete', - }, - { - available: (item) => caseStatus === CaseStatuses.open && !hasSubCases(item.subCases), + enabled: (item: Case | SubCase) => isIndividual(item) && item.status !== CaseStatuses.closed, description: i18n.CLOSE_CASE, icon: 'folderCheck', name: i18n.CLOSE_CASE, @@ -51,9 +41,9 @@ export const getActions = ({ 'data-test-subj': 'action-close', }, { - available: (item) => caseStatus !== CaseStatuses.open && !hasSubCases(item.subCases), + enabled: (item: Case | SubCase) => isIndividual(item) && item.status !== CaseStatuses.open, description: i18n.REOPEN_CASE, - icon: 'folderExclamation', + icon: 'folderOpen', name: i18n.REOPEN_CASE, onClick: (theCase: Case) => dispatchUpdate({ @@ -65,4 +55,12 @@ export const getActions = ({ type: 'icon', 'data-test-subj': 'action-open', }, + { + description: i18n.DELETE_CASE, + icon: 'trash', + name: i18n.DELETE_CASE, + onClick: deleteCaseOnClick, + type: 'icon', + 'data-test-subj': 'action-delete', + }, ]; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx index 47db362c7b4bfe..e69f85c8629620 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx @@ -19,7 +19,7 @@ import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; -import { CaseStatuses } from '../../../../../case/common/api'; +import { CaseStatuses, CaseType } from '../../../../../case/common/api'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { Case, SubCase } from '../../containers/types'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; @@ -204,7 +204,7 @@ export const getCasesColumns = ( name: i18n.STATUS, render: (theCase: Case) => { if (theCase?.subCases == null || theCase.subCases.length === 0) { - if (theCase.status == null) { + if (theCase.status == null || theCase.type === CaseType.collection) { return getEmptyTagValue(); } return <Status type={theCase.status} />; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts b/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts index 1ab36d3c672250..519be95fcdfef5 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts @@ -6,14 +6,24 @@ */ import { filter } from 'lodash/fp'; -import { AssociationType, CaseStatuses } from '../../../../../case/common/api'; +import { AssociationType, CaseStatuses, CaseType } from '../../../../../case/common/api'; import { Case, SubCase } from '../../containers/types'; import { statuses } from '../status'; +export const isSelectedCasesIncludeCollections = (selectedCases: Case[]) => + selectedCases.length > 0 && + selectedCases.some((caseObj: Case) => caseObj.type === CaseType.collection); + export const isSubCase = (theCase: Case | SubCase): theCase is SubCase => (theCase as SubCase).caseParentId !== undefined && (theCase as SubCase).associationType === AssociationType.subCase; +export const isCollection = (theCase: Case | SubCase | null | undefined) => + theCase != null && (theCase as Case).type === CaseType.collection; + +export const isIndividual = (theCase: Case | SubCase | null | undefined) => + theCase != null && (theCase as Case).type === CaseType.individual; + export const getSubCasesStatusCountsBadges = ( subCases: SubCase[] ): Array<{ name: CaseStatuses; color: string; count: number }> => { diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index a44ccd2384843c..1fbda69d8916c6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -24,6 +24,7 @@ import { useUpdateCases } from '../../containers/use_bulk_update_case'; import { useGetActionLicense } from '../../containers/use_get_action_license'; import { getCasesColumns } from './columns'; import { AllCases } from '.'; +import { StatusAll } from '../status'; jest.mock('../../containers/use_bulk_update_case'); jest.mock('../../containers/use_delete_cases'); @@ -111,6 +112,11 @@ describe('AllCases', () => { }); it('should render AllCases', async () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open }, + }); + const wrapper = mount( <TestProviders> <AllCases userCanCrud={true} /> @@ -144,6 +150,11 @@ describe('AllCases', () => { }); it('should render the stats', async () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed }, + }); + const wrapper = mount( <TestProviders> <AllCases userCanCrud={true} /> @@ -202,6 +213,7 @@ describe('AllCases', () => { it('should render empty fields', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open }, data: { ...defaultGetCases.data, cases: [ @@ -240,6 +252,72 @@ describe('AllCases', () => { }); }); + it('should render correct actions for case (with type individual and filter status open)', async () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open }, + }); + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="action-open"]').first().props().disabled).toBeTruthy(); + expect(wrapper.find('[data-test-subj="action-close"]').first().props().disabled).toBeFalsy(); + expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toBeFalsy(); + }); + }); + + it('should enable correct actions for sub cases', async () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + data: { + ...defaultGetCases.data, + cases: [ + { + ...defaultGetCases.data.cases[0], + id: 'my-case-with-subcases', + createdAt: null, + createdBy: null, + status: null, + subCases: [ + { + id: 'sub-case-id', + }, + ], + tags: null, + title: null, + totalComment: null, + totalAlerts: null, + type: CaseType.collection, + }, + ], + }, + }); + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + await waitFor(() => { + wrapper + .find( + '[data-test-subj="sub-cases-table-my-case-with-subcases"] [data-test-subj="euiCollapsedItemActionsButton"]' + ) + .last() + .simulate('click'); + expect(wrapper.find('[data-test-subj="action-open"]').first().props().disabled).toEqual(true); + expect(wrapper.find('[data-test-subj="action-close"]').first().props().disabled).toEqual( + true + ); + expect(wrapper.find('[data-test-subj="action-delete"]').first().props().disabled).toEqual( + false + ); + }); + }); + it('should not render case link or actions on modal=true', async () => { const wrapper = mount( <TestProviders> @@ -281,6 +359,7 @@ describe('AllCases', () => { </TestProviders> ); await waitFor(() => { + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ @@ -296,6 +375,15 @@ describe('AllCases', () => { it('opens case when row action icon clicked', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, + data: { + ...defaultGetCases.data, + cases: [ + { + ...defaultGetCases.data.cases[0], + status: CaseStatuses.closed, + }, + ], + }, filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed }, }); @@ -305,6 +393,7 @@ describe('AllCases', () => { </TestProviders> ); await waitFor(() => { + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').last().simulate('click'); wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ @@ -320,6 +409,7 @@ describe('AllCases', () => { it('Bulk delete', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed }, selectedCases: useGetCasesMockState.data.cases, }); @@ -355,9 +445,72 @@ describe('AllCases', () => { }); }); + it('Renders only bulk delete on status all', async () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: StatusAll }, + selectedCases: [...useGetCasesMockState.data.cases], + }); + + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + await waitFor(() => { + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="cases-bulk-close-button"]').exists()).toEqual(false); + expect( + wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().props().disabled + ).toEqual(false); + }); + }); + + it('Renders correct bulk actoins for case collection when filter status is set to all - enable only bulk delete if any collection is selected', async () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open }, + selectedCases: [ + ...useGetCasesMockState.data.cases, + { + ...useGetCasesMockState.data.cases[0], + type: CaseType.collection, + }, + ], + }); + + useDeleteCasesMock + .mockReturnValueOnce({ + ...defaultDeleteCases, + isDisplayConfirmDeleteModal: false, + }) + .mockReturnValue({ + ...defaultDeleteCases, + isDisplayConfirmDeleteModal: true, + }); + + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + await waitFor(() => { + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + expect(wrapper.find('[data-test-subj="cases-bulk-open-button"]').exists()).toEqual(false); + expect( + wrapper.find('[data-test-subj="cases-bulk-close-button"]').first().props().disabled + ).toEqual(true); + expect( + wrapper.find('[data-test-subj="cases-bulk-delete-button"]').first().props().disabled + ).toEqual(false); + }); + }); + it('Bulk close status update', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.open }, selectedCases: useGetCasesMockState.data.cases, }); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index 56dcf3bc28757e..5f0e72564f60e9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -54,7 +54,9 @@ import { SecurityPageName } from '../../../app/types'; import { useKibana } from '../../../common/lib/kibana'; import { APP_ID } from '../../../../common/constants'; import { Stats } from '../status'; +import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../translations'; import { getExpandedRowMap } from './expanded_row'; +import { isSelectedCasesIncludeCollections } from './helpers'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -268,10 +270,17 @@ export const AllCases = React.memo<AllCasesProps>( deleteCasesAction: toggleBulkDeleteModal, selectedCaseIds, updateCaseStatus: handleUpdateCaseStatus, + includeCollections: isSelectedCasesIncludeCollections(selectedCases), })} /> ), - [selectedCaseIds, filterOptions.status, toggleBulkDeleteModal, handleUpdateCaseStatus] + [ + selectedCases, + selectedCaseIds, + filterOptions.status, + toggleBulkDeleteModal, + handleUpdateCaseStatus, + ] ); const handleDispatchUpdate = useCallback( (args: Omit<UpdateCase, 'refetchCasesStatus'>) => { @@ -379,9 +388,8 @@ export const AllCases = React.memo<AllCasesProps>( const euiBasicTableSelectionProps = useMemo<EuiTableSelectionType<Case>>( () => ({ - selectable: (theCase) => isEmpty(theCase.subCases), onSelectionChange: setSelectedCases, - selectableMessage: (selectable) => (!selectable ? i18n.SELECTABLE_MESSAGE_COLLECTIONS : ''), + selectableMessage: (selectable) => (!selectable ? SELECTABLE_MESSAGE_COLLECTIONS : ''), }), [setSelectedCases] ); @@ -410,6 +418,8 @@ export const AllCases = React.memo<AllCasesProps>( [isModal, onRowClick] ); + const enableBuckActions = userCanCrud && !isModal; + return ( <> {!isEmpty(actionsErrors) && ( @@ -506,10 +516,12 @@ export const AllCases = React.memo<AllCasesProps>( </UtilityBarGroup> {!isModal && ( <UtilityBarGroup data-test-subj="case-table-utility-bar-actions"> - <UtilityBarText data-test-subj="case-table-selected-case-count"> - {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} - </UtilityBarText> - {userCanCrud && ( + {enableBuckActions && ( + <UtilityBarText data-test-subj="case-table-selected-case-count"> + {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} + </UtilityBarText> + )} + {enableBuckActions && ( <UtilityBarAction data-test-subj="case-table-bulk-actions" iconSide="right" @@ -529,7 +541,7 @@ export const AllCases = React.memo<AllCasesProps>( <BasicTable columns={memoizedGetCasesColumns} data-test-subj="cases-table" - isSelectable={userCanCrud && !isModal} + isSelectable={enableBuckActions} itemId="id" items={data.cases} itemIdToExpandedRowMap={itemIdToExpandedRowMap} @@ -556,7 +568,7 @@ export const AllCases = React.memo<AllCasesProps>( onChange={tableOnChangeCallback} pagination={memoizedPagination} rowProps={tableRowProps} - selection={userCanCrud && !isModal ? euiBasicTableSelectionProps : undefined} + selection={enableBuckActions ? euiBasicTableSelectionProps : undefined} sorting={sorting} className={classnames({ isModal })} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx index 11d53b6609e74c..9d5b36515182dd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.test.tsx @@ -11,8 +11,10 @@ import { waitFor } from '@testing-library/react'; import { CaseStatuses } from '../../../../../case/common/api'; import { StatusFilter } from './status_filter'; +import { StatusAll } from '../status'; const stats = { + [StatusAll]: 0, [CaseStatuses.open]: 2, [CaseStatuses['in-progress']]: 5, [CaseStatuses.closed]: 7, diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx index 41997d6f384214..34186a201cc05e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/status_filter.tsx @@ -7,14 +7,13 @@ import React, { memo } from 'react'; import { EuiSuperSelect, EuiSuperSelectOption, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { CaseStatuses } from '../../../../../case/common/api'; -import { Status, statuses } from '../status'; +import { Status, statuses, StatusAll, CaseStatusWithAllStatus } from '../status'; interface Props { - stats: Record<CaseStatuses, number>; - selectedStatus: CaseStatuses; - onStatusChanged: (status: CaseStatuses) => void; - disabledStatuses?: CaseStatuses[]; + stats: Record<CaseStatusWithAllStatus, number | null>; + selectedStatus: CaseStatusWithAllStatus; + onStatusChanged: (status: CaseStatusWithAllStatus) => void; + disabledStatuses?: CaseStatusWithAllStatus[]; } const StatusFilterComponent: React.FC<Props> = ({ @@ -23,15 +22,18 @@ const StatusFilterComponent: React.FC<Props> = ({ onStatusChanged, disabledStatuses = [], }) => { - const caseStatuses = Object.keys(statuses) as CaseStatuses[]; - const options: Array<EuiSuperSelectOption<CaseStatuses>> = caseStatuses.map((status) => ({ + const caseStatuses = Object.keys(statuses) as CaseStatusWithAllStatus[]; + const options: Array<EuiSuperSelectOption<CaseStatusWithAllStatus>> = [ + StatusAll, + ...caseStatuses, + ].map((status) => ({ value: status, inputDisplay: ( <EuiFlexGroup gutterSize="xs" alignItems={'center'}> <EuiFlexItem grow={false}> <Status type={status} /> </EuiFlexItem> - <EuiFlexItem grow={false}>{` (${stats[status]})`}</EuiFlexItem> + {status !== StatusAll && <EuiFlexItem grow={false}>{` (${stats[status]})`}</EuiFlexItem>} </EuiFlexGroup> ), disabled: disabledStatuses.includes(status), diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx index 61bbbac5a1e847..84b032489f3269 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/table_filters.tsx @@ -15,6 +15,7 @@ import { FilterOptions } from '../../containers/types'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; import { FilterPopover } from '../filter_popover'; +import { CaseStatusWithAllStatus, StatusAll } from '../status'; import { StatusFilter } from './status_filter'; import * as i18n from './translations'; @@ -42,7 +43,12 @@ const StatusFilterWrapper = styled(EuiFlexItem)` * @param onFilterChanged change listener to be notified on filter changes */ -const defaultInitial = { search: '', reporters: [], status: CaseStatuses.open, tags: [] }; +const defaultInitial = { + search: '', + reporters: [], + status: StatusAll, + tags: [], +}; const CasesTableFiltersComponent = ({ countClosedCases, @@ -126,7 +132,7 @@ const CasesTableFiltersComponent = ({ ); const onStatusChanged = useCallback( - (status: CaseStatuses) => { + (status: CaseStatusWithAllStatus) => { onFilterChanged({ status }); }, [onFilterChanged] @@ -134,6 +140,7 @@ const CasesTableFiltersComponent = ({ const stats = useMemo( () => ({ + [StatusAll]: null, [CaseStatuses.open]: countOpenCases ?? 0, [CaseStatuses['in-progress']]: countInProgressCases ?? 0, [CaseStatuses.closed]: countClosedCases ?? 0, diff --git a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx index f9722b3903b122..17e196d590418c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx @@ -9,14 +9,16 @@ import React from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { CaseStatuses } from '../../../../../case/common/api'; +import { CaseStatusWithAllStatus } from '../status'; import * as i18n from './translations'; interface GetBulkItems { - caseStatus: string; + caseStatus: CaseStatusWithAllStatus; closePopover: () => void; deleteCasesAction: (cases: string[]) => void; selectedCaseIds: string[]; updateCaseStatus: (status: string) => void; + includeCollections: boolean; } export const getBulkItems = ({ @@ -25,35 +27,55 @@ export const getBulkItems = ({ deleteCasesAction, selectedCaseIds, updateCaseStatus, + includeCollections, }: GetBulkItems) => { + let statusMenuItems: JSX.Element[] = []; + + const openMenuItem = ( + <EuiContextMenuItem + data-test-subj="cases-bulk-open-button" + disabled={selectedCaseIds.length === 0 || includeCollections} + key={i18n.BULK_ACTION_OPEN_SELECTED} + icon="folderOpen" + onClick={() => { + closePopover(); + updateCaseStatus(CaseStatuses.open); + }} + > + {i18n.BULK_ACTION_OPEN_SELECTED} + </EuiContextMenuItem> + ); + + const closeMenuItem = ( + <EuiContextMenuItem + data-test-subj="cases-bulk-close-button" + disabled={selectedCaseIds.length === 0 || includeCollections} + key={i18n.BULK_ACTION_CLOSE_SELECTED} + icon="folderCheck" + onClick={() => { + closePopover(); + updateCaseStatus(CaseStatuses.closed); + }} + > + {i18n.BULK_ACTION_CLOSE_SELECTED} + </EuiContextMenuItem> + ); + + switch (caseStatus) { + case CaseStatuses.open: + statusMenuItems = [closeMenuItem]; + break; + + case CaseStatuses.closed: + statusMenuItems = [openMenuItem]; + break; + + default: + break; + } + return [ - caseStatus === CaseStatuses.open ? ( - <EuiContextMenuItem - data-test-subj="cases-bulk-close-button" - disabled={selectedCaseIds.length === 0} - key={i18n.BULK_ACTION_CLOSE_SELECTED} - icon="folderCheck" - onClick={() => { - closePopover(); - updateCaseStatus(CaseStatuses.closed); - }} - > - {i18n.BULK_ACTION_CLOSE_SELECTED} - </EuiContextMenuItem> - ) : ( - <EuiContextMenuItem - data-test-subj="cases-bulk-open-button" - disabled={selectedCaseIds.length === 0} - key={i18n.BULK_ACTION_OPEN_SELECTED} - icon="folderExclamation" - onClick={() => { - closePopover(); - updateCaseStatus(CaseStatuses.open); - }} - > - {i18n.BULK_ACTION_OPEN_SELECTED} - </EuiContextMenuItem> - ), + ...statusMenuItems, <EuiContextMenuItem data-test-subj="cases-bulk-delete-button" key={i18n.BULK_ACTION_DELETE_SELECTED} diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.tsx index e0cdf9dc6d9ebf..7f9ffbd8dc01dd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/status_context_menu.tsx @@ -8,8 +8,8 @@ import React, { memo, useCallback, useMemo, useState } from 'react'; import { memoize } from 'lodash/fp'; import { EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; -import { CaseStatuses } from '../../../../../case/common/api'; -import { Status, statuses } from '../status'; +import { caseStatuses, CaseStatuses } from '../../../../../case/common/api'; +import { Status } from '../status'; interface Props { currentStatus: CaseStatuses; @@ -34,7 +34,6 @@ const StatusContextMenuComponent: React.FC<Props> = ({ currentStatus, onStatusCh [closePopover, onStatusChanged] ); - const caseStatuses = Object.keys(statuses) as CaseStatuses[]; const panelItems = caseStatuses.map((status: CaseStatuses) => ( <EuiContextMenuItem key={status} diff --git a/x-pack/plugins/security_solution/public/cases/components/status/config.ts b/x-pack/plugins/security_solution/public/cases/components/status/config.ts index e91653a6957fbc..6c1f4f7d6dd652 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/config.ts +++ b/x-pack/plugins/security_solution/public/cases/components/status/config.ts @@ -4,27 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { CaseStatuses } from '../../../../../case/common/api'; import * as i18n from './translations'; +import { AllCaseStatus, Statuses, StatusAll } from './types'; -type Statuses = Record< - CaseStatuses, - { - color: string; - label: string; - actionBar: { - title: string; - }; - button: { - label: string; - icon: string; - }; - stats: { - title: string; - }; - } ->; +export const allCaseStatus: AllCaseStatus = { + [StatusAll]: { color: 'hollow', label: i18n.ALL }, +}; export const statuses: Statuses = { [CaseStatuses.open]: { diff --git a/x-pack/plugins/security_solution/public/cases/components/status/index.ts b/x-pack/plugins/security_solution/public/cases/components/status/index.ts index 2da6cd26d5ab4e..94d7cb6a318302 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/status/index.ts @@ -8,3 +8,4 @@ export * from './status'; export * from './config'; export * from './stats'; +export * from './types'; diff --git a/x-pack/plugins/security_solution/public/cases/components/status/status.tsx b/x-pack/plugins/security_solution/public/cases/components/status/status.tsx index ba0f9a9cfde00a..de4c979daf4c1a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/status.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/status.tsx @@ -9,12 +9,12 @@ import React, { memo, useMemo } from 'react'; import { noop } from 'lodash/fp'; import { EuiBadge } from '@elastic/eui'; -import { CaseStatuses } from '../../../../../case/common/api'; -import { statuses } from './config'; +import { allCaseStatus, statuses } from './config'; +import { CaseStatusWithAllStatus, StatusAll } from './types'; import * as i18n from './translations'; interface Props { - type: CaseStatuses; + type: CaseStatusWithAllStatus; withArrow?: boolean; onClick?: () => void; } @@ -22,7 +22,7 @@ interface Props { const StatusComponent: React.FC<Props> = ({ type, withArrow = false, onClick = noop }) => { const props = useMemo( () => ({ - color: statuses[type].color, + color: type === StatusAll ? allCaseStatus[StatusAll].color : statuses[type].color, ...(withArrow ? { iconType: 'arrowDown', iconSide: 'right' as const } : {}), }), [withArrow, type] @@ -35,7 +35,7 @@ const StatusComponent: React.FC<Props> = ({ type, withArrow = false, onClick = n iconOnClickAriaLabel={i18n.STATUS_ICON_ARIA} data-test-subj={`status-badge-${type}`} > - {statuses[type].label} + {type === StatusAll ? allCaseStatus[StatusAll].label : statuses[type].label} </EuiBadge> ); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/status/translations.ts b/x-pack/plugins/security_solution/public/cases/components/status/translations.ts index c47795020abdf4..17c5b8199c0c62 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/status/translations.ts @@ -8,6 +8,10 @@ import { i18n } from '@kbn/i18n'; export * from '../../translations'; +export const ALL = i18n.translate('xpack.securitySolution.case.status.all', { + defaultMessage: 'All', +}); + export const OPEN = i18n.translate('xpack.securitySolution.case.status.open', { defaultMessage: 'Open', }); diff --git a/x-pack/plugins/security_solution/public/cases/components/status/types.ts b/x-pack/plugins/security_solution/public/cases/components/status/types.ts new file mode 100644 index 00000000000000..4129282404ebca --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/status/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; +import { CaseStatuses } from '../../../../../case/common/api'; + +export const StatusAll = 'all' as const; +type StatusAllType = typeof StatusAll; + +export type CaseStatusWithAllStatus = CaseStatuses | StatusAllType; + +export type AllCaseStatus = Record<StatusAllType, { color: string; label: string }>; + +export type Statuses = Record< + CaseStatuses, + { + color: string; + label: string; + actionBar: { + title: string; + }; + button: { + label: string; + icon: EuiIconType; + }; + stats: { + title: string; + }; + } +>; diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index ee63749b494354..01f1ba173d5be2 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -137,7 +137,6 @@ describe('Case Configuration API', () => { ...DEFAULT_QUERY_PARAMS, reporters: [], tags: [], - status: CaseStatuses.open, }, signal: abortCtrl.signal, }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 01ef040aa19cda..a064189854879a 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { assign } from 'lodash'; +import { assign, omit } from 'lodash'; import { CasePatchRequest, @@ -14,7 +14,6 @@ import { CasesFindResponse, CasesResponse, CasesStatusResponse, - CaseStatuses, CaseType, CaseUserActionsResponse, CommentRequest, @@ -45,6 +44,7 @@ import { } from '../../../../case/common/api/helpers'; import { KibanaServices } from '../../common/lib/kibana'; +import { StatusAll } from '../components/status'; import { ActionLicense, @@ -169,7 +169,7 @@ export const getCases = async ({ onlyCollectionType: false, search: '', reporters: [], - status: CaseStatuses.open, + status: StatusAll, tags: [], }, queryParams = { @@ -190,7 +190,7 @@ export const getCases = async ({ }; const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, { method: 'GET', - query, + query: query.status === StatusAll ? omit(query, ['status']) : query, signal, }); return convertAllCasesToCamel(decodeCasesFindResponse(response)); diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index 399d8d43ce0655..09c911d93ea474 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -17,11 +17,10 @@ import { CaseType, AssociationType, } from '../../../../case/common/api'; +import { CaseStatusWithAllStatus } from '../components/status'; export { CaseConnector, ActionConnector, CaseStatuses } from '../../../../case/common/api'; -export type AllCaseType = AssociationType & CaseType; - export type Comment = CommentRequest & { associationType: AssociationType; id: string; @@ -96,7 +95,7 @@ export interface QueryParams { export interface FilterOptions { search: string; - status: CaseStatuses; + status: CaseStatusWithAllStatus; tags: string[]; reporters: User[]; onlyCollectionType?: boolean; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx index f2e8e280bf158c..d27bb5ab1b4625 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_cases.tsx @@ -6,12 +6,12 @@ */ import { useCallback, useEffect, useReducer, useRef } from 'react'; -import { CaseStatuses } from '../../../../case/common/api'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case, UpdateByKey } from './types'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import * as i18n from './translations'; import { getCases, patchCase } from './api'; +import { StatusAll } from '../components/status'; export interface UseGetCasesState { data: AllCases; @@ -95,7 +95,7 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS export const DEFAULT_FILTER_OPTIONS: FilterOptions = { search: '', reporters: [], - status: CaseStatuses.open, + status: StatusAll, tags: [], onlyCollectionType: false, }; From 3b61e924348e50016460f58abe3bfb1d4f2a78c3 Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Wed, 3 Mar 2021 14:16:15 -0800 Subject: [PATCH 50/63] [Security Solution][Lists] Escape quotes in list ids and quote the id in KQL query (#93176) (#93503) * Escape quotes in list ids and quote the id in KQL query * Remove decodeURIComponent because too many KQL queries don't handle quotes * Add quotes to user supplied IDs for other KQL queries Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lists/server/routes/delete_list_route.ts | 3 ++- .../find_exception_list_items.test.ts | 25 +++++++++++++------ .../find_exception_list_items.ts | 7 ++++-- .../server/services/utils/escape_query.ts | 10 ++++++++ .../services/utils/get_query_filter.test.ts | 4 +-- .../server/services/utils/get_query_filter.ts | 7 +++++- .../security_and_spaces/tests/delete_lists.ts | 22 ++++++++++++++++ 7 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/lists/server/services/utils/escape_query.ts diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts index 4732b25dbf5e7f..3e9b76a1b330a9 100644 --- a/x-pack/plugins/lists/server/routes/delete_list_route.ts +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -19,6 +19,7 @@ import { } from '../../common/schemas'; import { getSavedObjectType } from '../services/exception_lists/utils'; import { ExceptionListClient } from '../services/exception_lists/exception_list_client'; +import { escapeQuotes } from '../services/utils/escape_query'; import { getExceptionListClient, getListClient } from '.'; @@ -142,7 +143,7 @@ const getReferencedExceptionLists = async ( (item) => `${getSavedObjectType({ namespaceType: item.namespace_type, - })}.attributes.list_id: ${item.list_id}` + })}.attributes.list_id: "${escapeQuotes(item.list_id)}"` ) .join(' OR '); return exceptionLists.findExceptionList({ diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.test.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.test.ts index 0d3dd2d9b65c38..3a2b12c3589175 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.test.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.test.ts @@ -18,7 +18,18 @@ describe('find_exception_list_items', () => { savedObjectType: ['exception-list'], }); expect(filter).toEqual( - '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: some-list-id)' + '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "some-list-id")' + ); + }); + + test('It should create a filter escaping quotes in list ids', () => { + const filter = getExceptionListsItemFilter({ + filter: [], + listId: ['list-id-"-with-quote'], + savedObjectType: ['exception-list'], + }); + expect(filter).toEqual( + '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-id-\\"-with-quote")' ); }); @@ -29,7 +40,7 @@ describe('find_exception_list_items', () => { savedObjectType: ['exception-list'], }); expect(filter).toEqual( - '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: some-list-id) AND exception-list.attributes.name: "Sample Endpoint Exception List")' + '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "some-list-id") AND exception-list.attributes.name: "Sample Endpoint Exception List")' ); }); @@ -40,7 +51,7 @@ describe('find_exception_list_items', () => { savedObjectType: ['exception-list', 'exception-list-agnostic'], }); expect(filter).toEqual( - '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2)' + '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2")' ); }); @@ -51,7 +62,7 @@ describe('find_exception_list_items', () => { savedObjectType: ['exception-list', 'exception-list-agnostic'], }); expect(filter).toEqual( - '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) AND exception-list.attributes.name: "Sample Endpoint Exception List") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2)' + '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") AND exception-list.attributes.name: "Sample Endpoint Exception List") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2")' ); }); @@ -62,7 +73,7 @@ describe('find_exception_list_items', () => { savedObjectType: ['exception-list', 'exception-list-agnostic', 'exception-list-agnostic'], }); expect(filter).toEqual( - '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-3)' + '(exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-3")' ); }); @@ -73,7 +84,7 @@ describe('find_exception_list_items', () => { savedObjectType: ['exception-list', 'exception-list-agnostic', 'exception-list-agnostic'], }); expect(filter).toEqual( - '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) AND exception-list.attributes.name: "Sample Endpoint Exception List") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-3)' + '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") AND exception-list.attributes.name: "Sample Endpoint Exception List") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-3")' ); }); @@ -88,7 +99,7 @@ describe('find_exception_list_items', () => { savedObjectType: ['exception-list', 'exception-list-agnostic', 'exception-list-agnostic'], }); expect(filter).toEqual( - '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: list-1) AND exception-list.attributes.name: "Sample Endpoint Exception List 1") OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-2) AND exception-list.attributes.name: "Sample Endpoint Exception List 2") OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: list-3) AND exception-list.attributes.name: "Sample Endpoint Exception List 3")' + '((exception-list.attributes.list_type: item AND exception-list.attributes.list_id: "list-1") AND exception-list.attributes.name: "Sample Endpoint Exception List 1") OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-2") AND exception-list.attributes.name: "Sample Endpoint Exception List 2") OR ((exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.list_id: "list-3") AND exception-list.attributes.name: "Sample Endpoint Exception List 3")' ); }); }); diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts index cc84314eaa7a02..155408dafc79dd 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -24,6 +24,7 @@ import { SortFieldOrUndefined, SortOrderOrUndefined, } from '../../../common/schemas'; +import { escapeQuotes } from '../utils/escape_query'; import { getSavedObjectTypes, transformSavedObjectsToFoundExceptionListItem } from './utils'; import { getExceptionList } from './get_exception_list'; @@ -89,7 +90,8 @@ export const getExceptionListsItemFilter = ({ savedObjectType: SavedObjectType[]; }): string => { return listId.reduce((accum, singleListId, index) => { - const listItemAppend = `(${savedObjectType[index]}.attributes.list_type: item AND ${savedObjectType[index]}.attributes.list_id: ${singleListId})`; + const escapedListId = escapeQuotes(singleListId); + const listItemAppend = `(${savedObjectType[index]}.attributes.list_type: item AND ${savedObjectType[index]}.attributes.list_id: "${escapedListId}")`; const listItemAppendWithFilter = filter[index] != null ? `(${listItemAppend} AND ${filter[index]})` : listItemAppend; if (accum === '') { @@ -117,8 +119,9 @@ export const findValueListExceptionListItems = async ({ sortField, sortOrder, }: FindValueListExceptionListsItems): Promise<FoundExceptionListItemSchema | null> => { + const escapedValueListId = escapeQuotes(valueListId); const savedObjectsFindResponse = await savedObjectsClient.find<ExceptionListSoSchema>({ - filter: `(exception-list.attributes.list_type: item AND exception-list.attributes.entries.list.id:${valueListId}) OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.entries.list.id:${valueListId}) `, + filter: `(exception-list.attributes.list_type: item AND exception-list.attributes.entries.list.id:"${escapedValueListId}") OR (exception-list-agnostic.attributes.list_type: item AND exception-list-agnostic.attributes.entries.list.id:"${escapedValueListId}") `, page, perPage, sortField, diff --git a/x-pack/plugins/lists/server/services/utils/escape_query.ts b/x-pack/plugins/lists/server/services/utils/escape_query.ts new file mode 100644 index 00000000000000..f654b8a2b9ebe0 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/escape_query.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const escapeQuotes = (str: string): string => { + return str.replace(/[\\"]/g, '\\$&'); +}; diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts index d189012aec0e12..0f6cc171bc04c0 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter.test.ts @@ -46,7 +46,7 @@ describe('get_query_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { list_id: 'list-123', }, }, @@ -74,7 +74,7 @@ describe('get_query_filter', () => { minimum_should_match: 1, should: [ { - match: { + match_phrase: { list_id: 'list-123', }, }, diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts index 5cbad8c284a553..25c8f9880063fb 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter.ts @@ -9,6 +9,8 @@ import { DslQuery, EsQueryConfig } from 'src/plugins/data/common'; import { Filter, Query, esQuery } from '../../../../../../src/plugins/data/server'; +import { escapeQuotes } from './escape_query'; + export interface GetQueryFilterOptions { filter: string; } @@ -41,7 +43,10 @@ export const getQueryFilterWithListId = ({ filter, listId, }: GetQueryFilterWithListIdOptions): GetQueryFilterReturn => { + const escapedListId = escapeQuotes(listId); const filterWithListId = - filter.trim() !== '' ? `list_id: ${listId} AND (${filter})` : `list_id: ${listId}`; + filter.trim() !== '' + ? `list_id: "${escapedListId}" AND (${filter})` + : `list_id: "${escapedListId}"`; return getQueryFilter({ filter: filterWithListId }); }; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_lists.ts index adba0a2f626e19..4ce3c7f0e5661d 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_lists.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_lists.ts @@ -63,6 +63,28 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare).to.eql(getListResponseMockWithoutAutoGeneratedValues()); }); + it('should delete a single list with a list id containing non-alphanumeric characters', async () => { + // create a list + const id = `some""-list-id"(1)`; + await supertest + .post(LIST_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateMinimalListSchemaMock(), + id, + }) + .expect(200); + + // delete the list by its list id + const { body } = await supertest + .delete(`${LIST_URL}?id=${id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getListResponseMockWithoutAutoGeneratedValues()); + }); + it('should delete a single list using an auto generated id', async () => { // add a list const { body: bodyWithCreatedList } = await supertest From 8394b0856f5f032c3a7ac297904149d39e0c1d3d Mon Sep 17 00:00:00 2001 From: Wylie Conlon <william.conlon@elastic.co> Date: Wed, 3 Mar 2021 17:20:16 -0500 Subject: [PATCH 51/63] [7.12] [Index patterns] Guarantee order of fields in flattenHits (#93344) (#93485) * [Index patterns] Guarantee order of fields in flattenHits (#93344) * [Discover] Guarantee order of fields in table preview * Remove comments * Fix test that relied on discover ordering * Fix ordering of test # Conflicts: # test/functional/apps/discover/_large_string.ts # x-pack/test/functional/apps/security/doc_level_security_roles.js # x-pack/test/functional/apps/security/field_level_security.js * Fix tests * Fix whitespace --- .../index_patterns/flatten_hit.test.ts | 69 +++++++++++++++++++ .../index_patterns/flatten_hit.ts | 21 +++++- .../angular/helpers/row_formatter.test.ts | 39 +++++++++-- .../angular/helpers/row_formatter.ts | 4 +- .../functional/apps/discover/_large_string.ts | 6 +- .../apps/security/doc_level_security_roles.js | 2 +- .../apps/security/field_level_security.js | 4 +- 7 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 src/plugins/data/common/index_patterns/index_patterns/flatten_hit.test.ts diff --git a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.test.ts b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.test.ts new file mode 100644 index 00000000000000..9a33b0cfa6f1ce --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPattern } from './index_pattern'; + +// @ts-expect-error +import mockLogStashFields from './fixtures/logstash_fields'; +import { stubbedSavedObjectIndexPattern } from './fixtures/stubbed_saved_object_index_pattern'; + +import { fieldFormatsMock } from '../../field_formats/mocks'; +import { flattenHitWrapper } from './flatten_hit'; + +class MockFieldFormatter {} + +fieldFormatsMock.getInstance = jest.fn().mockImplementation(() => new MockFieldFormatter()) as any; + +// helper function to create index patterns +function create(id: string) { + const { + type, + version, + attributes: { timeFieldName, fields, title }, + } = stubbedSavedObjectIndexPattern(id); + + return new IndexPattern({ + spec: { + id, + type, + version, + timeFieldName, + fields, + title, + runtimeFieldMap: {}, + }, + fieldFormats: fieldFormatsMock, + shortDotsEnable: false, + metaFields: [], + }); +} + +describe('flattenHit', () => { + let indexPattern: IndexPattern; + + // create an indexPattern instance for each test + beforeEach(() => { + indexPattern = create('test-pattern'); + }); + + it('returns sorted object keys that combine _source, fields and metaFields in a defined order', () => { + const response = flattenHitWrapper(indexPattern, ['_id', '_type', '_score', '_routing'])({ + _id: 'a', + _source: { + name: 'first', + }, + fields: { + date: ['1'], + zzz: ['z'], + }, + }); + const expectedOrder = ['date', 'name', 'zzz', '_id', '_routing', '_score', '_type']; + expect(Object.keys(response)).toEqual(expectedOrder); + expect(Object.entries(response).map(([key]) => key)).toEqual(expectedOrder); + }); +}); diff --git a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts index dadf302ec6ebf6..7cd88c8a87c196 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts @@ -75,7 +75,26 @@ function decorateFlattenedWrapper(hit: Record<string, any>, metaFields: Record<s } }); - return flattened; + // Force all usage of Object.keys to use a predefined sort order, + // instead of using insertion order + return new Proxy(flattened, { + ownKeys: (target) => { + return Reflect.ownKeys(target).sort((a, b) => { + const aIsMeta = _.includes(metaFields, a); + const bIsMeta = _.includes(metaFields, b); + if (aIsMeta && bIsMeta) { + return String(a).localeCompare(String(b)); + } + if (aIsMeta) { + return 1; + } + if (bIsMeta) { + return -1; + } + return String(a).localeCompare(String(b)); + }); + }, + }); }; } diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts index abbc5294605918..050959dff98a45 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts @@ -13,10 +13,15 @@ import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks describe('Row formatter', () => { const hit = { - foo: 'bar', - number: 42, - hello: '<h1>World</h1>', - also: 'with "quotes" or \'single quotes\'', + _id: 'a', + _type: 'doc', + _score: 1, + _source: { + foo: 'bar', + number: 42, + hello: '<h1>World</h1>', + also: 'with "quotes" or \'single quotes\'', + }, }; const createIndexPattern = () => { @@ -37,12 +42,17 @@ describe('Row formatter', () => { const indexPattern = createIndexPattern(); + // Realistic response with alphabetical insertion order const formatHitReturnValue = { also: 'with \\"quotes\\" or 'single qoutes'', - number: '42', foo: 'bar', + number: '42', hello: '<h1>World</h1>', + _id: 'a', + _type: 'doc', + _score: 1, }; + const formatHitMock = jest.fn().mockReturnValue(formatHitReturnValue); beforeEach(() => { @@ -52,7 +62,7 @@ describe('Row formatter', () => { it('formats document properly', () => { expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( - `"<dl class=\\"source truncate-by-height\\"><dt>also:</dt><dd>with \\\\"quotes\\\\" or 'single qoutes'</dd> <dt>number:</dt><dd>42</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd><h1>World</h1></dd> </dl>"` + `"<dl class=\\"source truncate-by-height\\"><dt>also:</dt><dd>with \\\\"quotes\\\\" or 'single qoutes'</dd> <dt>foo:</dt><dd>bar</dd> <dt>number:</dt><dd>42</dd> <dt>hello:</dt><dd><h1>World</h1></dd> <dt>_id:</dt><dd>a</dd> <dt>_type:</dt><dd>doc</dd> <dt>_score:</dt><dd>1</dd> </dl>"` ); }); @@ -60,7 +70,7 @@ describe('Row formatter', () => { expect( formatRow({ ...hit, highlight: { number: '42' } }, indexPattern).trim() ).toMatchInlineSnapshot( - `"<dl class=\\"source truncate-by-height\\"><dt>number:</dt><dd>42</dd> <dt>also:</dt><dd>with \\\\"quotes\\\\" or 'single qoutes'</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd><h1>World</h1></dd> </dl>"` + `"<dl class=\\"source truncate-by-height\\"><dt>number:</dt><dd>42</dd> <dt>also:</dt><dd>with \\\\"quotes\\\\" or 'single qoutes'</dd> <dt>foo:</dt><dd>bar</dd> <dt>hello:</dt><dd><h1>World</h1></dd> <dt>_id:</dt><dd>a</dd> <dt>_type:</dt><dd>doc</dd> <dt>_score:</dt><dd>1</dd> </dl>"` ); }); @@ -88,6 +98,21 @@ describe('Row formatter', () => { ); }); + it('formats top level objects in alphabetical order', () => { + indexPattern.getFieldByName = jest.fn().mockReturnValue({ + name: 'subfield', + }); + indexPattern.getFormatterForField = jest.fn().mockReturnValue({ + convert: () => 'formatted', + }); + const formatted = formatTopLevelObject( + { fields: { 'a.zzz': [100], 'a.ccc': [50] } }, + { 'a.zzz': [100], 'a.ccc': [50] }, + indexPattern + ).trim(); + expect(formatted.indexOf('<dt>a.ccc:</dt>')).toBeLessThan(formatted.indexOf('<dt>a.zzz:</dt>')); + }); + it('formats top level objects with subfields and highlights', () => { indexPattern.getFieldByName = jest.fn().mockReturnValue({ name: 'subfield', diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts index e17e840e404846..a226cefb53960f 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts @@ -26,6 +26,7 @@ export const doTemplate = template(noWhiteSpace(templateHtml)); export const formatRow = (hit: Record<string, any>, indexPattern: IndexPattern) => { const highlights = hit?.highlight ?? {}; + // Keys are sorted in the hits object const formatted = indexPattern.formatHit(hit); const highlightPairs: Array<[string, unknown]> = []; const sourcePairs: Array<[string, unknown]> = []; @@ -44,7 +45,8 @@ export const formatTopLevelObject = ( const highlights = row.highlight ?? {}; const highlightPairs: Array<[string, unknown]> = []; const sourcePairs: Array<[string, unknown]> = []; - Object.entries(fields).forEach(([key, values]) => { + const sorted = Object.entries(fields).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); + sorted.forEach(([key, values]) => { const field = indexPattern.getFieldByName(key); const formatter = field ? indexPattern.getFormatterForField(field) diff --git a/test/functional/apps/discover/_large_string.ts b/test/functional/apps/discover/_large_string.ts index 775b92ffc0542a..5dd5834ffe103c 100644 --- a/test/functional/apps/discover/_large_string.ts +++ b/test/functional/apps/discover/_large_string.ts @@ -32,8 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('verify the large string book present', async function () { const ExpectedDoc = - '_id:1 _type:_doc _index:testlargestring _score:0' + - ' mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' + + 'mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' + ' This eBook is for the use of anyone anywhere in the United States' + ' and most other parts of the world at no cost and with almost no restrictions whatsoever.' + ' You may copy it, give it away or re-use it under the terms of the' + @@ -42,7 +41,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ' you’ll have to check the laws of the country where you are' + ' located before using this ebook.' + ' Title: Hamlet Author: William Shakespeare Release Date: November 1998 [EBook #1524]' + - ' Last Updated: December 30, 2017 Language: English Character set encoding:'; + ' Last Updated: December 30, 2017 Language: English Character set encoding:' + + ' _id:1 _type:_doc _index:testlargestring _score:0'; let rowData; await PageObjects.common.navigateToApp('discover'); diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index e023b319420467..5ecfa80c240a43 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -81,7 +81,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to.be( - '_id:doc1 _type:_doc _index:dlstest _score:0 region.keyword:EAST name:ABC Company name.keyword:ABC Company region:EAST' + 'name:ABC Company name.keyword:ABC Company region:EAST region.keyword:EAST _id:doc1 _index:dlstest _score:0 _type:_doc' ); }); after('logout', async () => { diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 97a701444523fc..67ac8f98117e54 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -113,7 +113,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to.be( - '_id:2 _type:_doc _index:flstest _score:0 customer_name.keyword:ABC Company customer_ssn:444.555.6666 customer_region.keyword:WEST runtime_customer_ssn:444.555.6666 calculated at runtime customer_region:WEST customer_name:ABC Company customer_ssn.keyword:444.555.6666' + 'customer_name:ABC Company customer_name.keyword:ABC Company customer_region:WEST customer_region.keyword:WEST customer_ssn:444.555.6666 customer_ssn.keyword:444.555.6666 runtime_customer_ssn:444.555.6666 calculated at runtime _id:2 _index:flstest _score:0 _type:_doc' ); }); @@ -127,7 +127,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to.be( - '_id:2 _type:_doc _index:flstest _score:0 customer_name.keyword:ABC Company customer_region.keyword:WEST customer_region:WEST customer_name:ABC Company' + 'customer_name:ABC Company customer_name.keyword:ABC Company customer_region:WEST customer_region.keyword:WEST _id:2 _index:flstest _score:0 _type:_doc' ); }); From 7b50a6a15ab630c6a5129ab4da2380c5afc155e6 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 3 Mar 2021 17:48:37 -0500 Subject: [PATCH 52/63] [Security Solution][Case][Bug] Only update alert status in its specific index (#92530) (#93509) * Writing failing test for duplicate ids * Test is correctly failing prior to bug fix * Working jest tests * Adding more jest tests * Fixing jest tests * Adding await and gzip * Fixing type errors * Updating log message Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/case/server/client/alerts/get.ts | 13 +- .../client/alerts/update_status.test.ts | 10 +- .../server/client/alerts/update_status.ts | 12 +- .../plugins/case/server/client/cases/push.ts | 7 +- .../case/server/client/cases/update.test.ts | 43 +- .../case/server/client/cases/update.ts | 46 +- x-pack/plugins/case/server/client/client.ts | 12 +- .../case/server/client/comments/add.test.ts | 78 +- .../case/server/client/comments/add.ts | 35 +- x-pack/plugins/case/server/client/mocks.ts | 4 + x-pack/plugins/case/server/client/types.ts | 17 +- x-pack/plugins/case/server/common/index.ts | 1 + x-pack/plugins/case/server/common/types.ts | 14 + x-pack/plugins/case/server/common/utils.ts | 32 +- .../api/__fixtures__/mock_saved_objects.ts | 37 + .../api/cases/comments/patch_comment.test.ts | 79 + .../server/routes/api/cases/get_case.test.ts | 2 +- .../server/routes/api/cases/push_case.test.ts | 3 +- .../api/cases/sub_case/patch_sub_cases.ts | 44 +- .../plugins/case/server/routes/api/utils.ts | 110 +- .../case/server/services/alerts/index.test.ts | 27 +- .../case/server/services/alerts/index.ts | 106 +- .../basic/tests/cases/patch_cases.ts | 151 +- .../tests/cases/sub_cases/patch_sub_cases.ts | 72 +- .../case_api_integration/common/lib/utils.ts | 15 +- .../cases/signals/duplicate_ids/data.json.gz | Bin 0 -> 5513 bytes .../cases/signals/duplicate_ids/mappings.json | 6624 +++++++++++++++++ 27 files changed, 7282 insertions(+), 312 deletions(-) create mode 100644 x-pack/plugins/case/server/common/types.ts create mode 100644 x-pack/test/functional/es_archives/cases/signals/duplicate_ids/data.json.gz create mode 100644 x-pack/test/functional/es_archives/cases/signals/duplicate_ids/mappings.json diff --git a/x-pack/plugins/case/server/client/alerts/get.ts b/x-pack/plugins/case/server/client/alerts/get.ts index 0b2663b7372041..6a6e961e952c09 100644 --- a/x-pack/plugins/case/server/client/alerts/get.ts +++ b/x-pack/plugins/case/server/client/alerts/get.ts @@ -6,34 +6,33 @@ */ import { ElasticsearchClient, Logger } from 'kibana/server'; +import { AlertInfo } from '../../common'; import { AlertServiceContract } from '../../services'; import { CaseClientGetAlertsResponse } from './types'; interface GetParams { alertsService: AlertServiceContract; - ids: string[]; - indices: Set<string>; + alertsInfo: AlertInfo[]; scopedClusterClient: ElasticsearchClient; logger: Logger; } export const get = async ({ alertsService, - ids, - indices, + alertsInfo, scopedClusterClient, logger, }: GetParams): Promise<CaseClientGetAlertsResponse> => { - if (ids.length === 0 || indices.size <= 0) { + if (alertsInfo.length === 0) { return []; } - const alerts = await alertsService.getAlerts({ ids, indices, scopedClusterClient, logger }); + const alerts = await alertsService.getAlerts({ alertsInfo, scopedClusterClient, logger }); if (!alerts) { return []; } - return alerts.hits.hits.map((alert) => ({ + return alerts.docs.map((alert) => ({ id: alert._id, index: alert._index, ...alert._source, diff --git a/x-pack/plugins/case/server/client/alerts/update_status.test.ts b/x-pack/plugins/case/server/client/alerts/update_status.test.ts index b3ed3c2b84a993..4662ce1976620c 100644 --- a/x-pack/plugins/case/server/client/alerts/update_status.test.ts +++ b/x-pack/plugins/case/server/client/alerts/update_status.test.ts @@ -15,17 +15,13 @@ describe('updateAlertsStatus', () => { const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); await caseClient.client.updateAlertsStatus({ - ids: ['alert-id-1'], - status: CaseStatuses.closed, - indices: new Set<string>(['.siem-signals']), + alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], }); expect(caseClient.services.alertsService.updateAlertsStatus).toHaveBeenCalledWith({ - scopedClusterClient: expect.anything(), logger: expect.anything(), - ids: ['alert-id-1'], - indices: new Set<string>(['.siem-signals']), - status: CaseStatuses.closed, + scopedClusterClient: expect.anything(), + alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], }); }); }); diff --git a/x-pack/plugins/case/server/client/alerts/update_status.ts b/x-pack/plugins/case/server/client/alerts/update_status.ts index 2194c3a18afdd6..cd6f97273d6d7e 100644 --- a/x-pack/plugins/case/server/client/alerts/update_status.ts +++ b/x-pack/plugins/case/server/client/alerts/update_status.ts @@ -6,25 +6,21 @@ */ import { ElasticsearchClient, Logger } from 'src/core/server'; -import { CaseStatuses } from '../../../common/api'; import { AlertServiceContract } from '../../services'; +import { UpdateAlertRequest } from '../types'; interface UpdateAlertsStatusArgs { alertsService: AlertServiceContract; - ids: string[]; - status: CaseStatuses; - indices: Set<string>; + alerts: UpdateAlertRequest[]; scopedClusterClient: ElasticsearchClient; logger: Logger; } export const updateAlertsStatus = async ({ alertsService, - ids, - status, - indices, + alerts, scopedClusterClient, logger, }: UpdateAlertsStatusArgs): Promise<void> => { - await alertsService.updateAlertsStatus({ ids, status, indices, scopedClusterClient, logger }); + await alertsService.updateAlertsStatus({ alerts, scopedClusterClient, logger }); }; diff --git a/x-pack/plugins/case/server/client/cases/push.ts b/x-pack/plugins/case/server/client/cases/push.ts index 80dcc7a0e018c3..8aab11be21b015 100644 --- a/x-pack/plugins/case/server/client/cases/push.ts +++ b/x-pack/plugins/case/server/client/cases/push.ts @@ -15,7 +15,7 @@ import { SavedObject, } from 'kibana/server'; import { ActionResult, ActionsClient } from '../../../../actions/server'; -import { flattenCaseSavedObject, getAlertIndicesAndIDs } from '../../routes/api/utils'; +import { flattenCaseSavedObject, getAlertInfoFromComments } from '../../routes/api/utils'; import { ActionConnector, @@ -108,12 +108,11 @@ export const push = async ({ ); } - const { ids, indices } = getAlertIndicesAndIDs(theCase?.comments); + const alertsInfo = getAlertInfoFromComments(theCase?.comments); try { alerts = await caseClient.getAlerts({ - ids, - indices, + alertsInfo, }); } catch (e) { throw createCaseError({ diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 752b0ab369de09..be68aa1266023e 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -430,9 +430,13 @@ describe('update', () => { await caseClient.client.update(patchCases); expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({ - ids: ['test-id'], - status: 'closed', - indices: new Set<string>(['test-index']), + alerts: [ + { + id: 'test-id', + index: 'test-index', + status: 'closed', + }, + ], }); }); @@ -458,11 +462,10 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.updateAlertsStatus = jest.fn(); await caseClient.client.update(patchCases); - expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled(); + expect(caseClient.esClient.bulk).not.toHaveBeenCalled(); }); test('it updates alert status when syncAlerts is turned on', async () => { @@ -492,9 +495,7 @@ describe('update', () => { await caseClient.client.update(patchCases); expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({ - ids: ['test-id'], - status: 'open', - indices: new Set<string>(['test-index']), + alerts: [{ id: 'test-id', index: 'test-index', status: 'open' }], }); }); @@ -515,11 +516,10 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.updateAlertsStatus = jest.fn(); await caseClient.client.update(patchCases); - expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled(); + expect(caseClient.esClient.bulk).not.toHaveBeenCalled(); }); test('it updates alert status for multiple cases', async () => { @@ -576,22 +576,12 @@ describe('update', () => { caseClient.client.updateAlertsStatus = jest.fn(); await caseClient.client.update(patchCases); - /** - * the update code will put each comment into a status bucket and then make at most 1 call - * to ES for each status bucket - * Now instead of doing a call per case to get the comments, it will do a single call with all the cases - * and sub cases and get all the comments in one go - */ - expect(caseClient.client.updateAlertsStatus).toHaveBeenNthCalledWith(1, { - ids: ['test-id'], - status: 'open', - indices: new Set<string>(['test-index']), - }); - expect(caseClient.client.updateAlertsStatus).toHaveBeenNthCalledWith(2, { - ids: ['test-id-2'], - status: 'closed', - indices: new Set<string>(['test-index-2']), + expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({ + alerts: [ + { id: 'test-id', index: 'test-index', status: 'open' }, + { id: 'test-id-2', index: 'test-index-2', status: 'closed' }, + ], }); }); @@ -611,11 +601,10 @@ describe('update', () => { }); const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient }); - caseClient.client.updateAlertsStatus = jest.fn(); await caseClient.client.update(patchCases); - expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled(); + expect(caseClient.esClient.bulk).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/case/server/client/cases/update.ts b/x-pack/plugins/case/server/client/cases/update.ts index 36318f03bd33f5..8c788d6f3bcd92 100644 --- a/x-pack/plugins/case/server/client/cases/update.ts +++ b/x-pack/plugins/case/server/client/cases/update.ts @@ -18,7 +18,6 @@ import { Logger, } from 'kibana/server'; import { - AlertInfo, flattenCaseSavedObject, isCommentRequestTypeAlertOrGenAlert, } from '../../routes/api/utils'; @@ -53,7 +52,8 @@ import { SUB_CASE_SAVED_OBJECT, } from '../../saved_object_types'; import { CaseClientHandler } from '..'; -import { addAlertInfoToStatusMap } from '../../common'; +import { createAlertUpdateRequest } from '../../common'; +import { UpdateAlertRequest } from '../types'; import { createCaseError } from '../../common/error'; /** @@ -291,33 +291,25 @@ async function updateAlerts({ // get a map of sub case id to the sub case status const subCasesToStatus = await getSubCasesToStatus({ totalAlerts, client, caseService }); - // create a map of the case statuses to the alert information that we need to update for that status - // This allows us to make at most 3 calls to ES, one for each status type that we need to update - // One potential improvement here is to do a tick (set timeout) to reduce the memory footprint if that becomes an issue - const alertsToUpdate = totalAlerts.saved_objects.reduce((acc, alertComment) => { - if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { - const status = getSyncStatusForComment({ - alertComment, - casesToSyncToStatus, - subCasesToStatus, - }); + // create an array of requests that indicate the id, index, and status to update an alert + const alertsToUpdate = totalAlerts.saved_objects.reduce( + (acc: UpdateAlertRequest[], alertComment) => { + if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { + const status = getSyncStatusForComment({ + alertComment, + casesToSyncToStatus, + subCasesToStatus, + }); - addAlertInfoToStatusMap({ comment: alertComment.attributes, statusMap: acc, status }); - } + acc.push(...createAlertUpdateRequest({ comment: alertComment.attributes, status })); + } - return acc; - }, new Map<CaseStatuses, AlertInfo>()); - - // This does at most 3 calls to Elasticsearch to update the status of the alerts to either open, closed, or in-progress - for (const [status, alertInfo] of alertsToUpdate.entries()) { - if (alertInfo.ids.length > 0 && alertInfo.indices.size > 0) { - caseClient.updateAlertsStatus({ - ids: alertInfo.ids, - status, - indices: alertInfo.indices, - }); - } - } + return acc; + }, + [] + ); + + await caseClient.updateAlertsStatus({ alerts: alertsToUpdate }); } interface UpdateArgs { diff --git a/x-pack/plugins/case/server/client/client.ts b/x-pack/plugins/case/server/client/client.ts index c34c3942b18d0e..9f4bf606776492 100644 --- a/x-pack/plugins/case/server/client/client.ts +++ b/x-pack/plugins/case/server/client/client.ts @@ -169,9 +169,9 @@ export class CaseClientHandler implements CaseClient { }); } catch (error) { throw createCaseError({ - message: `Failed to update alerts status using client ids: ${JSON.stringify( - args.ids - )} \nindices: ${JSON.stringify([...args.indices])} \nstatus: ${args.status}: ${error}`, + message: `Failed to update alerts status using client alerts: ${JSON.stringify( + args.alerts + )}: ${error}`, error, logger: this.logger, }); @@ -218,9 +218,9 @@ export class CaseClientHandler implements CaseClient { }); } catch (error) { throw createCaseError({ - message: `Failed to get alerts using client ids: ${JSON.stringify( - args.ids - )} \nindices: ${JSON.stringify([...args.indices])}: ${error}`, + message: `Failed to get alerts using client requested alerts: ${JSON.stringify( + args.alertsInfo + )}: ${error}`, error, logger: this.logger, }); diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index 123ecec6abea33..460a03643b63db 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -15,6 +15,8 @@ import { } from '../../routes/api/__fixtures__'; import { createCaseClientWithMockSavedObjectsClient } from '../mocks'; +type AlertComment = CommentType.alert | CommentType.generatedAlert; + describe('addComment', () => { beforeEach(async () => { jest.restoreAllMocks(); @@ -248,9 +250,7 @@ describe('addComment', () => { }); expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({ - ids: ['test-alert'], - status: 'open', - indices: new Set<string>(['test-index']), + alerts: [{ id: 'test-alert', index: 'test-index', status: 'open' }], }); }); @@ -517,5 +517,77 @@ describe('addComment', () => { expect(boomErr.output.statusCode).toBe(400); }); }); + + describe('alert format', () => { + it.each([ + ['1', ['index1', 'index2'], CommentType.alert], + [['1', '2'], 'index', CommentType.alert], + ['1', ['index1', 'index2'], CommentType.generatedAlert], + [['1', '2'], 'index', CommentType.generatedAlert], + ])( + 'throws an error with an alert comment with contents id: %p indices: %p type: %s', + async (alertId, index, type) => { + expect.assertions(1); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ + savedObjectsClient, + }); + await expect( + caseClient.client.addComment({ + caseId: 'mock-id-4', + comment: { + // casting because type must be either alert or generatedAlert but type is CommentType + type: type as AlertComment, + alertId, + index, + rule: { + id: 'test-rule1', + name: 'test-rule', + }, + }, + }) + ).rejects.toThrow(); + } + ); + + it.each([ + ['1', ['index1'], CommentType.alert], + [['1', '2'], ['index', 'other-index'], CommentType.alert], + ])( + 'does not throw an error with an alert comment with contents id: %p indices: %p type: %s', + async (alertId, index, type) => { + expect.assertions(1); + + const savedObjectsClient = createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }); + + const caseClient = await createCaseClientWithMockSavedObjectsClient({ + savedObjectsClient, + }); + await expect( + caseClient.client.addComment({ + caseId: 'mock-id-1', + comment: { + // casting because type must be either alert or generatedAlert but type is CommentType + type: type as AlertComment, + alertId, + index, + rule: { + id: 'test-rule1', + name: 'test-rule', + }, + }, + }) + ).resolves.not.toBeUndefined(); + } + ); + }); }); }); diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index d3d7047e71bd3c..22a59e4d0539bd 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -11,11 +11,7 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { SavedObject, SavedObjectsClientContract, Logger } from 'src/core/server'; -import { - decodeCommentRequest, - getAlertIds, - isCommentRequestTypeGenAlert, -} from '../../routes/api/utils'; +import { decodeCommentRequest, isCommentRequestTypeGenAlert } from '../../routes/api/utils'; import { throwErrors, @@ -36,7 +32,7 @@ import { } from '../../services/user_actions/helpers'; import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; -import { CommentableCase } from '../../common'; +import { CommentableCase, createAlertUpdateRequest } from '../../common'; import { CaseClientHandler } from '..'; import { createCaseError } from '../../common/error'; import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; @@ -177,15 +173,12 @@ const addGeneratedAlerts = async ({ newComment.attributes.type === CommentType.generatedAlert) && caseInfo.attributes.settings.syncAlerts ) { - const ids = getAlertIds(query); - await caseClient.updateAlertsStatus({ - ids, + const alertsToUpdate = createAlertUpdateRequest({ + comment: query, status: subCase.attributes.status, - indices: new Set([ - ...(Array.isArray(newComment.attributes.index) - ? newComment.attributes.index - : [newComment.attributes.index]), - ]), + }); + await caseClient.updateAlertsStatus({ + alerts: alertsToUpdate, }); } @@ -331,15 +324,13 @@ export const addComment = async ({ }); if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { - const ids = getAlertIds(query); - await caseClient.updateAlertsStatus({ - ids, + const alertsToUpdate = createAlertUpdateRequest({ + comment: query, status: updatedCase.status, - indices: new Set([ - ...(Array.isArray(newComment.attributes.index) - ? newComment.attributes.index - : [newComment.attributes.index]), - ]), + }); + + await caseClient.updateAlertsStatus({ + alerts: alertsToUpdate, }); } diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index 98ffed0eaf8c5e..5cbd31c79885e1 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { ElasticsearchClient } from 'kibana/server'; +import { DeeplyMockedKeys } from 'packages/kbn-utility-types/target/jest'; import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { AlertServiceContract, @@ -45,6 +47,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ userActionService: jest.Mocked<CaseUserActionServiceSetup>; alertsService: jest.Mocked<AlertServiceContract>; }; + esClient: DeeplyMockedKeys<ElasticsearchClient>; }> => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); const log = loggingSystemMock.create().get('case'); @@ -82,5 +85,6 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ return { client: caseClient, services: { userActionService, alertsService }, + esClient, }; }; diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index adc66d8b1ea77b..3f4ef77d7f3489 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -19,6 +19,7 @@ import { CaseUserActionsResponse, User, } from '../../common/api'; +import { AlertInfo } from '../common'; import { CaseConfigureServiceSetup, CaseServiceSetup, @@ -46,14 +47,11 @@ export interface CaseClientAddComment { } export interface CaseClientUpdateAlertsStatus { - ids: string[]; - status: CaseStatuses; - indices: Set<string>; + alerts: UpdateAlertRequest[]; } export interface CaseClientGetAlerts { - ids: string[]; - indices: Set<string>; + alertsInfo: AlertInfo[]; } export interface CaseClientGetUserActions { @@ -85,6 +83,15 @@ export interface ConfigureFields { connectorType: string; } +/** + * Defines the fields necessary to update an alert's status. + */ +export interface UpdateAlertRequest { + id: string; + index: string; + status: CaseStatuses; +} + /** * This represents the interface that other plugins can access. */ diff --git a/x-pack/plugins/case/server/common/index.ts b/x-pack/plugins/case/server/common/index.ts index 0960b28b3d25ac..b07ed5d4ae2d62 100644 --- a/x-pack/plugins/case/server/common/index.ts +++ b/x-pack/plugins/case/server/common/index.ts @@ -7,3 +7,4 @@ export * from './models'; export * from './utils'; +export * from './types'; diff --git a/x-pack/plugins/case/server/common/types.ts b/x-pack/plugins/case/server/common/types.ts new file mode 100644 index 00000000000000..b58d8ec0e849e7 --- /dev/null +++ b/x-pack/plugins/case/server/common/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * This structure holds the alert ID and index from an alert comment + */ +export interface AlertInfo { + id: string; + index: string; +} diff --git a/x-pack/plugins/case/server/common/utils.ts b/x-pack/plugins/case/server/common/utils.ts index a3ac0361569d53..dce26f3d5998a0 100644 --- a/x-pack/plugins/case/server/common/utils.ts +++ b/x-pack/plugins/case/server/common/utils.ts @@ -6,8 +6,15 @@ */ import { SavedObjectsFindResult, SavedObjectsFindResponse } from 'kibana/server'; -import { CaseStatuses, CommentAttributes, CommentType, User } from '../../common/api'; -import { AlertInfo, getAlertIndicesAndIDs } from '../routes/api/utils'; +import { + CaseStatuses, + CommentAttributes, + CommentRequest, + CommentType, + User, +} from '../../common/api'; +import { UpdateAlertRequest } from '../client/types'; +import { getAlertInfoFromComments } from '../routes/api/utils'; /** * Default sort field for querying saved objects. @@ -22,27 +29,14 @@ export const nullUser: User = { username: null, full_name: null, email: null }; /** * Adds the ids and indices to a map of statuses */ -export function addAlertInfoToStatusMap({ +export function createAlertUpdateRequest({ comment, - statusMap, status, }: { - comment: CommentAttributes; - statusMap: Map<CaseStatuses, AlertInfo>; + comment: CommentRequest; status: CaseStatuses; -}) { - const newAlertInfo = getAlertIndicesAndIDs([comment]); - - // combine the already accumulated ids and indices with the new ones from this alert comment - if (newAlertInfo.ids.length > 0 && newAlertInfo.indices.size > 0) { - const accAlertInfo = statusMap.get(status) ?? { ids: [], indices: new Set<string>() }; - accAlertInfo.ids.push(...newAlertInfo.ids); - accAlertInfo.indices = new Set<string>([ - ...accAlertInfo.indices.values(), - ...newAlertInfo.indices.values(), - ]); - statusMap.set(status, accAlertInfo); - } +}): UpdateAlertRequest[] { + return getAlertInfoFromComments([comment]).map((alert) => ({ ...alert, status })); } /** diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index e67a6f6dd3344b..f2318c45e6ed39 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -404,6 +404,43 @@ export const mockCaseComments: Array<SavedObject<CommentAttributes>> = [ updated_at: '2019-11-25T22:32:30.608Z', version: 'WzYsMV0=', }, + { + type: 'cases-comment', + id: 'mock-comment-6', + attributes: { + associationType: AssociationType.case, + type: CommentType.generatedAlert, + index: 'test-index', + alertId: 'test-id', + created_at: '2019-11-25T22:32:30.608Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + rule: { + id: 'rule-id-1', + name: 'rule-name-1', + }, + updated_at: '2019-11-25T22:32:30.608Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + references: [ + { + type: 'cases', + name: 'associated-cases', + id: 'mock-id-4', + }, + ], + updated_at: '2019-11-25T22:32:30.608Z', + version: 'WzYsMV0=', + }, ]; export const mockCaseConfigure: Array<SavedObject<ESCasesConfigureAttributes>> = [ diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts index 1ebd336c83af75..9cc0575f9bb94a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts @@ -296,4 +296,83 @@ describe('PATCH comment', () => { expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); }); + + describe('alert format', () => { + it.each([ + ['1', ['index1', 'index2'], CommentType.alert, 'mock-comment-4'], + [['1', '2'], 'index', CommentType.alert, 'mock-comment-4'], + ['1', ['index1', 'index2'], CommentType.generatedAlert, 'mock-comment-6'], + [['1', '2'], 'index', CommentType.generatedAlert, 'mock-comment-6'], + ])( + 'returns an error with an alert comment with contents id: %p indices: %p type: %s comment id: %s', + async (alertId, index, type, commentID) => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'patch', + params: { + case_id: 'mock-id-4', + }, + body: { + type, + alertId, + index, + rule: { + id: 'rule-id', + name: 'rule', + }, + id: commentID, + version: 'WzYsMV0=', + }, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(response.status).toEqual(400); + } + ); + + it.each([ + ['1', ['index1'], CommentType.alert], + [['1', '2'], ['index', 'other-index'], CommentType.alert], + ])( + 'does not return an error with an alert comment with contents id: %p indices: %p type: %s', + async (alertId, index, type) => { + const request = httpServerMock.createKibanaRequest({ + path: CASE_COMMENTS_URL, + method: 'patch', + params: { + case_id: 'mock-id-4', + }, + body: { + type, + alertId, + index, + rule: { + id: 'rule-id', + name: 'rule', + }, + id: 'mock-comment-4', + // this version is different than the one in mockCaseComments because it gets updated in place + version: 'WzE3LDFd', + }, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseCommentSavedObject: mockCaseComments, + }) + ); + + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + } + ); + }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts index 968dd0424fe3f0..b9312331b4df28 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts @@ -105,7 +105,7 @@ describe('GET case', () => { const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.comments).toHaveLength(5); + expect(response.payload.comments).toHaveLength(6); }); it(`returns an error when thrown from getAllCaseComments`, async () => { diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts index c8501130493bac..0c3ebe67d227ae 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts @@ -132,8 +132,7 @@ describe('Push case', () => { const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(caseClient.getAlerts).toHaveBeenCalledWith({ - ids: ['test-id'], - indices: new Set<string>(['test-index']), + alertsInfo: [{ id: 'test-id', index: 'test-index' }], }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts index 73aacc2c2b0ba4..da7ec956cad1d4 100644 --- a/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -39,7 +39,6 @@ import { import { SUB_CASES_PATCH_DEL_URL } from '../../../../../common/constants'; import { RouteDeps } from '../../types'; import { - AlertInfo, escapeHatch, flattenSubCaseSavedObject, isCommentRequestTypeAlertOrGenAlert, @@ -47,7 +46,8 @@ import { } from '../../utils'; import { getCaseToUpdate } from '../helpers'; import { buildSubCaseUserActions } from '../../../../services/user_actions/helpers'; -import { addAlertInfoToStatusMap } from '../../../../common'; +import { createAlertUpdateRequest } from '../../../../common'; +import { UpdateAlertRequest } from '../../../../client/types'; import { createCaseError } from '../../../../common/error'; interface UpdateArgs { @@ -235,29 +235,23 @@ async function updateAlerts({ // get all the alerts for all sub cases that need to be synced const totalAlerts = await getAlertComments({ caseService, client, subCasesToSync }); // create a map of the status (open, closed, etc) to alert info that needs to be updated - const alertsToUpdate = totalAlerts.saved_objects.reduce((acc, alertComment) => { - if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { - const id = getID(alertComment); - const status = - id !== undefined - ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open - : CaseStatuses.open; - - addAlertInfoToStatusMap({ comment: alertComment.attributes, statusMap: acc, status }); - } - return acc; - }, new Map<CaseStatuses, AlertInfo>()); - - // This does at most 3 calls to Elasticsearch to update the status of the alerts to either open, closed, or in-progress - for (const [status, alertInfo] of alertsToUpdate.entries()) { - if (alertInfo.ids.length > 0 && alertInfo.indices.size > 0) { - caseClient.updateAlertsStatus({ - ids: alertInfo.ids, - status, - indices: alertInfo.indices, - }); - } - } + const alertsToUpdate = totalAlerts.saved_objects.reduce( + (acc: UpdateAlertRequest[], alertComment) => { + if (isCommentRequestTypeAlertOrGenAlert(alertComment.attributes)) { + const id = getID(alertComment); + const status = + id !== undefined + ? subCasesToSyncMap.get(id)?.status ?? CaseStatuses.open + : CaseStatuses.open; + + acc.push(...createAlertUpdateRequest({ comment: alertComment.attributes, status })); + } + return acc; + }, + [] + ); + + await caseClient.updateAlertsStatus({ alerts: alertsToUpdate }); } catch (error) { throw createCaseError({ message: `Failed to update alert status while updating sub cases: ${JSON.stringify( diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 298f8bb877cda5..37bffafa4377dd 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -45,6 +45,7 @@ import { import { transformESConnectorToCaseConnector } from './cases/helpers'; import { SortFieldCase } from './types'; +import { AlertInfo } from '../../common'; import { isCaseError } from '../../common/error'; export const transformNewSubCase = ({ @@ -111,55 +112,50 @@ export const getAlertIds = (comment: CommentRequest): string[] => { return []; }; -/** - * This structure holds the alert IDs and indices found from multiple alert comments - */ -export interface AlertInfo { - ids: string[]; - indices: Set<string>; -} - -const accumulateIndicesAndIDs = (comment: CommentAttributes, acc: AlertInfo): AlertInfo => { - if (isCommentRequestTypeAlertOrGenAlert(comment)) { - acc.ids.push(...getAlertIds(comment)); - const indices = Array.isArray(comment.index) ? comment.index : [comment.index]; - indices.forEach((index) => acc.indices.add(index)); - } - return acc; +const getIDsAndIndicesAsArrays = ( + comment: CommentRequestAlertType +): { ids: string[]; indices: string[] } => { + return { + ids: Array.isArray(comment.alertId) ? comment.alertId : [comment.alertId], + indices: Array.isArray(comment.index) ? comment.index : [comment.index], + }; }; /** - * Builds an AlertInfo object accumulating the alert IDs and indices for the passed in alerts. + * This functions extracts the ids and indices from an alert comment. It enforces that the alertId and index are either + * both strings or string arrays that are the same length. If they are arrays they represent a 1-to-1 mapping of + * id existing in an index at each position in the array. This is not ideal. Ideally an alert comment request would + * accept an array of objects like this: Array<{id: string; index: string; ruleName: string ruleID: string}> instead. + * + * To reformat the alert comment request requires a migration and a breaking API change. */ -export const getAlertIndicesAndIDs = (comments: CommentAttributes[] | undefined): AlertInfo => { - if (comments === undefined) { - return { ids: [], indices: new Set<string>() }; +const getAndValidateAlertInfoFromComment = (comment: CommentRequest): AlertInfo[] => { + if (!isCommentRequestTypeAlertOrGenAlert(comment)) { + return []; + } + + const { ids, indices } = getIDsAndIndicesAsArrays(comment); + + if (ids.length !== indices.length) { + return []; } - return comments.reduce( - (acc: AlertInfo, comment) => { - return accumulateIndicesAndIDs(comment, acc); - }, - { ids: [], indices: new Set<string>() } - ); + return ids.map((id, index) => ({ id, index: indices[index] })); }; /** - * Builds an AlertInfo object accumulating the alert IDs and indices for the passed in alert saved objects. + * Builds an AlertInfo object accumulating the alert IDs and indices for the passed in alerts. */ -export const getAlertIndicesAndIDsFromSO = ( - comments: SavedObjectsFindResponse<CommentAttributes> | undefined -): AlertInfo => { +export const getAlertInfoFromComments = (comments: CommentRequest[] | undefined): AlertInfo[] => { if (comments === undefined) { - return { ids: [], indices: new Set<string>() }; + return []; } - return comments.saved_objects.reduce( - (acc: AlertInfo, comment) => { - return accumulateIndicesAndIDs(comment.attributes, acc); - }, - { ids: [], indices: new Set<string>() } - ); + return comments.reduce((acc: AlertInfo[], comment) => { + const alertInfo = getAndValidateAlertInfoFromComment(comment); + acc.push(...alertInfo); + return acc; + }, []); }; export const transformNewComment = ({ @@ -378,5 +374,47 @@ export const decodeCommentRequest = (comment: CommentRequest) => { pipe(excess(ContextTypeUserRt).decode(comment), fold(throwErrors(badRequest), identity)); } else if (isCommentRequestTypeAlertOrGenAlert(comment)) { pipe(excess(AlertCommentRequestRt).decode(comment), fold(throwErrors(badRequest), identity)); + const { ids, indices } = getIDsAndIndicesAsArrays(comment); + + /** + * The alertId and index field must either be both of type string or they must both be string[] and be the same length. + * Having a one-to-one relationship between the id and index of an alert avoids accidentally updating or + * retrieving the wrong alert. Elasticsearch only guarantees that the _id (the field we use for alertId) to be + * unique within a single index. So if we attempt to update or get a specific alert across multiple indices we could + * update or receive the wrong one. + * + * Consider the situation where we have a alert1 with _id = '100' in index 'my-index-awesome' and also in index + * 'my-index-hi'. + * If we attempt to update the status of alert1 using an index pattern like `my-index-*` or even providing multiple + * indices, there's a chance we'll accidentally update too many alerts. + * + * This check doesn't enforce that the API request has the correct alert ID to index relationship it just guards + * against accidentally making a request like: + * { + * alertId: [1,2,3], + * index: awesome, + * } + * + * Instead this requires the requestor to provide: + * { + * alertId: [1,2,3], + * index: [awesome, awesome, awesome] + * } + * + * Ideally we'd change the format of the comment request to be an array of objects like: + * { + * alerts: [{id: 1, index: awesome}, {id: 2, index: awesome}] + * } + * + * But we'd need to also implement a migration because the saved object document currently stores the id and index + * in separate fields. + */ + if (ids.length !== indices.length) { + throw badRequest( + `Received an alert comment with ids and indices arrays of different lengths ids: ${JSON.stringify( + ids + )} indices: ${JSON.stringify(indices)}` + ); + } } }; diff --git a/x-pack/plugins/case/server/services/alerts/index.test.ts b/x-pack/plugins/case/server/services/alerts/index.test.ts index 3b1020d3ef5569..042e415b77e43c 100644 --- a/x-pack/plugins/case/server/services/alerts/index.test.ts +++ b/x-pack/plugins/case/server/services/alerts/index.test.ts @@ -17,10 +17,8 @@ describe('updateAlertsStatus', () => { describe('happy path', () => { let alertService: AlertServiceContract; const args = { - ids: ['alert-id-1'], - indices: new Set<string>(['.siem-signals']), + alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], request: {} as KibanaRequest, - status: CaseStatuses.closed, scopedClusterClient: esClient, logger, }; @@ -33,14 +31,17 @@ describe('updateAlertsStatus', () => { test('it update the status of the alert correctly', async () => { await alertService.updateAlertsStatus(args); - expect(esClient.updateByQuery).toHaveBeenCalledWith({ - body: { - query: { ids: { values: args.ids } }, - script: { lang: 'painless', source: `ctx._source.signal.status = '${args.status}'` }, - }, - conflicts: 'abort', - ignore_unavailable: true, - index: [...args.indices], + expect(esClient.bulk).toHaveBeenCalledWith({ + body: [ + { update: { _id: 'alert-id-1', _index: '.siem-signals' } }, + { + doc: { + signal: { + status: CaseStatuses.closed, + }, + }, + }, + ], }); }); @@ -48,9 +49,7 @@ describe('updateAlertsStatus', () => { it('ignores empty indices', async () => { expect( await alertService.updateAlertsStatus({ - ids: ['alert-id-1'], - status: CaseStatuses.closed, - indices: new Set<string>(['']), + alerts: [{ id: 'alert-id-1', index: '', status: CaseStatuses.closed }], scopedClusterClient: esClient, logger, }) diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts index 45245b86ba2d51..6ce4db61ab9563 100644 --- a/x-pack/plugins/case/server/services/alerts/index.ts +++ b/x-pack/plugins/case/server/services/alerts/index.ts @@ -5,28 +5,26 @@ * 2.0. */ -import _ from 'lodash'; +import { isEmpty } from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, Logger } from 'kibana/server'; -import { CaseStatuses } from '../../../common/api'; import { MAX_ALERTS_PER_SUB_CASE } from '../../../common/constants'; +import { UpdateAlertRequest } from '../../client/types'; +import { AlertInfo } from '../../common'; import { createCaseError } from '../../common/error'; export type AlertServiceContract = PublicMethodsOf<AlertService>; interface UpdateAlertsStatusArgs { - ids: string[]; - status: CaseStatuses; - indices: Set<string>; + alerts: UpdateAlertRequest[]; scopedClusterClient: ElasticsearchClient; logger: Logger; } interface GetAlertsArgs { - ids: string[]; - indices: Set<string>; + alertsInfo: AlertInfo[]; scopedClusterClient: ElasticsearchClient; logger: Logger; } @@ -38,54 +36,33 @@ interface Alert { } interface AlertsResponse { - hits: { - hits: Alert[]; - }; + docs: Alert[]; } -/** - * remove empty strings from the indices, I'm not sure how likely this is but in the case that - * the document doesn't have _index set the security_solution code sets the value to an empty string - * instead - */ -function getValidIndices(indices: Set<string>): string[] { - return [...indices].filter((index) => !_.isEmpty(index)); +function isEmptyAlert(alert: AlertInfo): boolean { + return isEmpty(alert.id) || isEmpty(alert.index); } export class AlertService { constructor() {} - public async updateAlertsStatus({ - ids, - status, - indices, - scopedClusterClient, - logger, - }: UpdateAlertsStatusArgs) { - const sanitizedIndices = getValidIndices(indices); - if (sanitizedIndices.length <= 0) { - logger.warn(`Empty alert indices when updateAlertsStatus ids: ${JSON.stringify(ids)}`); - return; - } - + public async updateAlertsStatus({ alerts, scopedClusterClient, logger }: UpdateAlertsStatusArgs) { try { - const result = await scopedClusterClient.updateByQuery({ - index: sanitizedIndices, - conflicts: 'abort', - body: { - script: { - source: `ctx._source.signal.status = '${status}'`, - lang: 'painless', - }, - query: { ids: { values: ids } }, - }, - ignore_unavailable: true, - }); - - return result; + const body = alerts + .filter((alert) => !isEmptyAlert(alert)) + .flatMap((alert) => [ + { update: { _id: alert.id, _index: alert.index } }, + { doc: { signal: { status: alert.status } } }, + ]); + + if (body.length <= 0) { + return; + } + + return scopedClusterClient.bulk({ body }); } catch (error) { throw createCaseError({ - message: `Failed to update alert status ids: ${JSON.stringify(ids)}: ${error}`, + message: `Failed to update alert status ids: ${JSON.stringify(alerts)}: ${error}`, error, logger, }); @@ -94,38 +71,25 @@ export class AlertService { public async getAlerts({ scopedClusterClient, - ids, - indices, + alertsInfo, logger, }: GetAlertsArgs): Promise<AlertsResponse | undefined> { - const index = getValidIndices(indices); - if (index.length <= 0) { - logger.warn(`Empty alert indices when retrieving alerts ids: ${JSON.stringify(ids)}`); - return; - } - try { - const result = await scopedClusterClient.search<AlertsResponse>({ - index, - body: { - query: { - bool: { - filter: { - ids: { - values: ids, - }, - }, - }, - }, - }, - size: MAX_ALERTS_PER_SUB_CASE, - ignore_unavailable: true, - }); + const docs = alertsInfo + .filter((alert) => !isEmptyAlert(alert)) + .slice(0, MAX_ALERTS_PER_SUB_CASE) + .map((alert) => ({ _id: alert.id, _index: alert.index })); + + if (docs.length <= 0) { + return; + } + + const results = await scopedClusterClient.mget<AlertsResponse>({ body: { docs } }); - return result.body; + return results.body; } catch (error) { throw createCaseError({ - message: `Failed to retrieve alerts ids: ${JSON.stringify(ids)}: ${error}`, + message: `Failed to retrieve alerts ids: ${JSON.stringify(alertsInfo)}: ${error}`, error, logger, }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts index b51b728df7155b..b41f77fc42e05e 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts @@ -438,8 +438,12 @@ export default ({ getService }: FtrProviderContext): void => { }); // There should be no change in their status since syncing is disabled - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.open); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses.open); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses.open + ); const updatedIndWithStatus: CasesResponse = (await setStatus({ supertest, @@ -467,8 +471,12 @@ export default ({ getService }: FtrProviderContext): void => { }); // There should still be no change in their status since syncing is disabled - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.open); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses.open); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses.open + ); // turn on the sync settings await supertest @@ -492,8 +500,139 @@ export default ({ getService }: FtrProviderContext): void => { }); // alerts should be updated now that the - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.closed); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses['in-progress']); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.closed + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + }); + + describe('esArchiver', () => { + const defaultSignalsIndex = '.siem-signals-default-000001'; + + beforeEach(async () => { + await esArchiver.load('cases/signals/duplicate_ids'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/duplicate_ids'); + await deleteAllCaseItems(es); + }); + + it('should not update the status of duplicate alert ids in separate indices', async () => { + const getSignals = async () => { + return getSignalsWithES({ + es, + indices: [defaultSignalsIndex, signalsIndex2], + ids: [signalIDInFirstIndex, signalIDInSecondIndex], + }); + }; + + // this id exists only in .siem-signals-default-000001 + const signalIDInFirstIndex = + 'cae78067e65582a3b277c1ad46ba3cb29044242fe0d24bbf3fcde757fdd31d1c'; + // This id exists in both .siem-signals-default-000001 and .siem-signals-default-000002 + const signalIDInSecondIndex = 'duplicate-signal-id'; + const signalsIndex2 = '.siem-signals-default-000002'; + + const { body: individualCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const { body: updatedIndWithComment } = await supertest + .post(`${CASES_URL}/${individualCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + alertId: signalIDInFirstIndex, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }) + .expect(200); + + const { body: updatedIndWithComment2 } = await supertest + .post(`${CASES_URL}/${updatedIndWithComment.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + alertId: signalIDInSecondIndex, + index: signalsIndex2, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignals(); + // There should be no change in their status since syncing is disabled + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source.signal.status + ).to.be(CaseStatuses.open); + expect( + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source.signal.status + ).to.be(CaseStatuses.open); + + const updatedIndWithStatus: CasesResponse = (await setStatus({ + supertest, + cases: [ + { + id: updatedIndWithComment2.id, + version: updatedIndWithComment2.version, + status: CaseStatuses.closed, + }, + ], + type: 'case', + })) as CasesResponse; + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignals(); + + // There should still be no change in their status since syncing is disabled + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source.signal.status + ).to.be(CaseStatuses.open); + expect( + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source.signal.status + ).to.be(CaseStatuses.open); + + // turn on the sync settings + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: updatedIndWithStatus[0].id, + version: updatedIndWithStatus[0].version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignals(); + + // alerts should be updated now that the + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source.signal.status + ).to.be(CaseStatuses.closed); + expect( + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source.signal.status + ).to.be(CaseStatuses.closed); + + // the duplicate signal id in the other index should not be affect (so its status should be open) + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInSecondIndex)?._source.signal.status + ).to.be(CaseStatuses.open); }); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts index 5a1da194a721f5..746d8a601bed66 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/sub_cases/patch_sub_cases.ts @@ -96,7 +96,9 @@ export default function ({ getService }: FtrProviderContext) { let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.open); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.open + ); await setStatus({ supertest, @@ -114,7 +116,9 @@ export default function ({ getService }: FtrProviderContext) { signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses['in-progress']); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses['in-progress'] + ); }); it('should update the status of multiple alerts attached to a sub case', async () => { @@ -152,8 +156,12 @@ export default function ({ getService }: FtrProviderContext) { ids: [signalID, signalID2], }); - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.open); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses.open); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses.open + ); await setStatus({ supertest, @@ -175,8 +183,12 @@ export default function ({ getService }: FtrProviderContext) { ids: [signalID, signalID2], }); - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses['in-progress']); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses['in-progress']); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses['in-progress'] + ); }); it('should update the status of multiple alerts attached to multiple sub cases in one collection', async () => { @@ -232,8 +244,12 @@ export default function ({ getService }: FtrProviderContext) { }); // There should be no change in their status since syncing is disabled - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.open); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses.open); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses.open + ); await setStatus({ supertest, @@ -256,8 +272,12 @@ export default function ({ getService }: FtrProviderContext) { }); // There still should be no change in their status since syncing is disabled - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.open); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses.open); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses.open + ); // Turn sync alerts on await supertest @@ -282,8 +302,12 @@ export default function ({ getService }: FtrProviderContext) { ids: [signalID, signalID2], }); - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.closed); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses['in-progress']); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.closed + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses['in-progress'] + ); }); it('should update the status of alerts attached to a case and sub case when sync settings is turned on', async () => { @@ -342,8 +366,12 @@ export default function ({ getService }: FtrProviderContext) { }); // There should be no change in their status since syncing is disabled - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.open); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses.open); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses.open + ); await setStatus({ supertest, @@ -380,8 +408,12 @@ export default function ({ getService }: FtrProviderContext) { }); // There should still be no change in their status since syncing is disabled - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses.open); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses.open); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses.open + ); // Turn sync alerts on await supertest @@ -421,8 +453,12 @@ export default function ({ getService }: FtrProviderContext) { }); // alerts should be updated now that the - expect(signals.get(signalID)?._source.signal.status).to.be(CaseStatuses['in-progress']); - expect(signals.get(signalID2)?._source.signal.status).to.be(CaseStatuses.closed); + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source.signal.status).to.be( + CaseStatuses.closed + ); }); it('404s when sub case id is invalid', async () => { diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 3ade7ef96f9dd1..169f85080f4eb2 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -54,11 +54,11 @@ export const getSignalsWithES = async ({ es: Client; indices: string | string[]; ids: string | string[]; -}): Promise<Map<string, Hit<SignalHit>>> => { +}): Promise<Map<string, Map<string, Hit<SignalHit>>>> => { const signals = await es.search<SearchResponse<SignalHit>>({ index: indices, body: { - size: ids.length, + size: 10000, query: { bool: { filter: [ @@ -72,10 +72,17 @@ export const getSignalsWithES = async ({ }, }, }); + return signals.body.hits.hits.reduce((acc, hit) => { - acc.set(hit._id, hit); + let indexMap = acc.get(hit._index); + if (indexMap === undefined) { + indexMap = new Map<string, Hit<SignalHit>>([[hit._id, hit]]); + } else { + indexMap.set(hit._id, hit); + } + acc.set(hit._index, indexMap); return acc; - }, new Map<string, Hit<SignalHit>>()); + }, new Map<string, Map<string, Hit<SignalHit>>>()); }; interface SetStatusCasesParams { diff --git a/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/data.json.gz b/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..46843482c9781c734ee8c0f73249e45b9f7cdcf5 GIT binary patch literal 5513 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 LaEsfZh+6>w)zN{p literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/mappings.json b/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/mappings.json new file mode 100644 index 00000000000000..97b28971502126 --- /dev/null +++ b/x-pack/test/functional/es_archives/cases/signals/duplicate_ids/mappings.json @@ -0,0 +1,6624 @@ +{ + "type": "index", + "value": { + "aliases": { + ".siem-signals-default": { + "is_write_index": true + } + }, + "index": ".siem-signals-default-000001", + "mappings": { + "_meta": { + "version": 14 + }, + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "doc_values": false, + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "signal": { + "properties": { + "_meta": { + "properties": { + "version": { + "type": "long" + } + } + }, + "ancestors": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "depth": { + "type": "integer" + }, + "group": { + "properties": { + "id": { + "type": "keyword" + }, + "index": { + "type": "integer" + } + } + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_signal": { + "dynamic": "false", + "enabled": false, + "type": "object" + }, + "original_time": { + "type": "date" + }, + "parent": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "parents": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "type": "keyword" + }, + "building_block_type": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "output_index": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "rule_id": { + "type": "keyword" + }, + "rule_name_override": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "severity_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + }, + "subtechnique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + } + } + } + } + }, + "threshold": { + "properties": { + "field": { + "type": "keyword" + }, + "value": { + "type": "float" + } + } + }, + "timeline_id": { + "type": "keyword" + }, + "timeline_title": { + "type": "keyword" + }, + "timestamp_override": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "threshold_count": { + "type": "float" + }, + "threshold_result": { + "properties": { + "count": { + "type": "long" + }, + "value": { + "type": "keyword" + } + } + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".siem-signals-default", + "rollover_alias": ".siem-signals-default" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": ".siem-signals-default-000002", + "mappings": { + "_meta": { + "version": 14 + }, + "dynamic": "false", + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "norms": false, + "type": "text" + }, + "stack_trace": { + "doc_values": false, + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "logger": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "function": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "original": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "message": { + "norms": false, + "type": "text" + }, + "network": { + "properties": { + "application": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "forwarded_ip": { + "type": "ip" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "observer": { + "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "zone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "install_scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "installed": { + "type": "date" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "related": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "license": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "signal": { + "properties": { + "_meta": { + "properties": { + "version": { + "type": "long" + } + } + }, + "ancestors": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "depth": { + "type": "integer" + }, + "group": { + "properties": { + "id": { + "type": "keyword" + }, + "index": { + "type": "integer" + } + } + }, + "original_event": { + "properties": { + "action": { + "type": "keyword" + }, + "category": { + "type": "keyword" + }, + "code": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "type": "keyword" + }, + "duration": { + "type": "long" + }, + "end": { + "type": "date" + }, + "hash": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + }, + "module": { + "type": "keyword" + }, + "original": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "outcome": { + "type": "keyword" + }, + "provider": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_norm": { + "type": "float" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "start": { + "type": "date" + }, + "timezone": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "original_signal": { + "dynamic": "false", + "enabled": false, + "type": "object" + }, + "original_time": { + "type": "date" + }, + "parent": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "parents": { + "properties": { + "depth": { + "type": "long" + }, + "id": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "rule": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "author": { + "type": "keyword" + }, + "building_block_type": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "enabled": { + "type": "keyword" + }, + "false_positives": { + "type": "keyword" + }, + "filters": { + "type": "object" + }, + "from": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "immutable": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "language": { + "type": "keyword" + }, + "license": { + "type": "keyword" + }, + "max_signals": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "output_index": { + "type": "keyword" + }, + "query": { + "type": "keyword" + }, + "references": { + "type": "keyword" + }, + "risk_score": { + "type": "float" + }, + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "rule_id": { + "type": "keyword" + }, + "rule_name_override": { + "type": "keyword" + }, + "saved_id": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "severity_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + }, + "subtechnique": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "reference": { + "type": "keyword" + } + } + } + } + } + } + }, + "threshold": { + "properties": { + "field": { + "type": "keyword" + }, + "value": { + "type": "float" + } + } + }, + "timeline_id": { + "type": "keyword" + }, + "timeline_title": { + "type": "keyword" + }, + "timestamp_override": { + "type": "keyword" + }, + "to": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "threshold_count": { + "type": "float" + }, + "threshold_result": { + "properties": { + "count": { + "type": "long" + }, + "value": { + "type": "keyword" + } + } + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "tls": { + "properties": { + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "vulnerability": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "classification": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "enumeration": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "scanner": { + "properties": { + "vendor": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "severity": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": ".siem-signals-default", + "rollover_alias": ".siem-signals-default" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} From faadbce9329ed36abd09cabf5ccb0060a461f407 Mon Sep 17 00:00:00 2001 From: Wylie Conlon <william.conlon@elastic.co> Date: Wed, 3 Mar 2021 19:02:24 -0500 Subject: [PATCH 53/63] Fix expanding document when using saved search data grid (#92999) (#93523) Co-authored-by: Matthias Wilhelm <matthias.wilhelm@elastic.co> --- .../create_discover_grid_directive.tsx | 12 +++++-- .../apps/dashboard/embeddable_data_grid.ts | 36 ++++++++++++------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx index a6c36c199f8524..0d17fcbba9c238 100644 --- a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx @@ -6,19 +6,27 @@ * Side Public License, v 1. */ -import * as React from 'react'; +import React, { useState } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid'; import { getServices } from '../../kibana_services'; +import { ElasticSearchHit } from '../doc_views/doc_views_types'; export const DataGridMemoized = React.memo((props: DiscoverGridProps) => ( <DiscoverGrid {...props} /> )); export function DiscoverGridEmbeddable(props: DiscoverGridProps) { + const [expandedDoc, setExpandedDoc] = useState<ElasticSearchHit | undefined>(undefined); + return ( <I18nProvider> - <DataGridMemoized {...props} services={getServices()} /> + <DataGridMemoized + {...props} + setExpandedDoc={setExpandedDoc} + expandedDoc={expandedDoc} + services={getServices()} + /> </I18nProvider> ); } diff --git a/test/functional/apps/dashboard/embeddable_data_grid.ts b/test/functional/apps/dashboard/embeddable_data_grid.ts index 54fa9f08c5763f..00a75baae4be7f 100644 --- a/test/functional/apps/dashboard/embeddable_data_grid.ts +++ b/test/functional/apps/dashboard/embeddable_data_grid.ts @@ -16,6 +16,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const find = getService('find'); const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'timePicker', 'discover']); + const retry = getService('retry'); + const dataGrid = getService('dataGrid'); describe('dashboard embeddable data grid', () => { before(async () => { @@ -31,22 +33,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.gotoDashboardLandingPage(); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.timePicker.setDefaultDataRange(); + await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); }); - describe('saved search filters', function () { - it('are added when a cell filter is clicked', async function () { - await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); - await find.clickByCssSelector(`[role="gridcell"]:nth-child(3)`); - // needs a short delay between becoming visible & being clickable - await PageObjects.common.sleep(250); - await find.clickByCssSelector(`[data-test-subj="filterOutButton"]`); - await PageObjects.header.waitUntilLoadingHasFinished(); - await find.clickByCssSelector(`[role="gridcell"]:nth-child(3)`); - await PageObjects.common.sleep(250); - await find.clickByCssSelector(`[data-test-subj="filterForButton"]`); - const filterCount = await filterBar.getFilterCount(); - expect(filterCount).to.equal(2); + it('should expand the detail row when the toggle arrow is clicked', async function () { + await retry.try(async function () { + await dataGrid.clickRowToggle({ isAnchorRow: false, rowIndex: 0 }); + const detailsEl = await dataGrid.getDetailsRows(); + const defaultMessageEl = await detailsEl[0].findByTestSubject('docTableRowDetailsTitle'); + expect(defaultMessageEl).to.be.ok(); + await dataGrid.closeFlyout(); }); }); + + it('are added when a cell filter is clicked', async function () { + await find.clickByCssSelector(`[role="gridcell"]:nth-child(3)`); + // needs a short delay between becoming visible & being clickable + await PageObjects.common.sleep(250); + await find.clickByCssSelector(`[data-test-subj="filterOutButton"]`); + await PageObjects.header.waitUntilLoadingHasFinished(); + await find.clickByCssSelector(`[role="gridcell"]:nth-child(3)`); + await PageObjects.common.sleep(250); + await find.clickByCssSelector(`[data-test-subj="filterForButton"]`); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.equal(2); + }); }); } From da2a7fc36872e9a50a728312b0833f6b8b20986a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 20:44:27 -0500 Subject: [PATCH 54/63] Fix service map for All environment single service (#93517) (#93534) Before we removed environment from the UI filters (#89647), the environment query parameter would be undefined if "All" was selected. Now we send ENVIRONMENT_ALL in as the query parameter. Changes in https://github.com/elastic/kibana/blob/master/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts made it so no connections would be returned if ENVIRONMENT_ALL was selected, rather than all connections. Since no connections were being returned, no elements except the selected service would be returned in the API response. This changes it so if ENVIRONMENT_ALL is selected, the connection will always be returned, just like what used to be the case when environment was undefined. Add an API test for this case. Fixes #93385. Co-authored-by: Nathan L Smith <nathan.smith@elastic.co> --- .../get_service_map_from_trace_ids.ts | 6 +- .../tests/service_maps/service_maps.ts | 188 ++++++++++-------- 2 files changed, 105 insertions(+), 89 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts index 6e9225041b199c..7e43c8f6843776 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts @@ -30,10 +30,8 @@ export function getConnections({ if (!paths) { return []; } - const isEnvironmentSelected = - environment && environment !== ENVIRONMENT_ALL.value; - if (serviceName || isEnvironmentSelected) { + if (serviceName || environment) { paths = paths.filter((path) => { return ( path @@ -46,7 +44,7 @@ export function getConnections({ return false; } - if (!environment) { + if (!environment || environment === ENVIRONMENT_ALL.value) { return true; } diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts index f452514cb5172b..a48811ad70249a 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.ts @@ -6,6 +6,7 @@ */ import querystring from 'querystring'; +import url from 'url'; import expect from '@kbn/expect'; import { isEmpty, uniq } from 'lodash'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; @@ -131,110 +132,127 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) expect(environments.has(ENVIRONMENT_NOT_DEFINED)).to.eql(true); expectSnapshot(body).toMatch(); }); - }); - - describe('/api/apm/service-map with ML data', () => { - describe('with the default apm user', () => { - let response: PromiseReturnType<typeof supertest.get>; - before(async () => { - response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); - }); + describe('with ML data', () => { + describe('with the default apm user', () => { + before(async () => { + response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + }); - it('returns service map elements with anomaly stats', () => { - expect(response.status).to.be(200); - const dataWithAnomalies = response.body.elements.filter( - (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) - ); + it('returns service map elements with anomaly stats', () => { + expect(response.status).to.be(200); + const dataWithAnomalies = response.body.elements.filter( + (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + ); - expect(dataWithAnomalies).to.not.empty(); + expect(dataWithAnomalies).to.not.empty(); - dataWithAnomalies.forEach(({ data }: any) => { - expect( - Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value)) - ).to.not.empty(); + dataWithAnomalies.forEach(({ data }: any) => { + expect( + Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value)) + ).to.not.empty(); + }); }); - }); - it('returns the correct anomaly stats', () => { - const dataWithAnomalies = response.body.elements.filter( - (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) - ); - - expectSnapshot(dataWithAnomalies.length).toMatchInline(`8`); - expectSnapshot(dataWithAnomalies.slice(0, 3)).toMatchInline(` - Array [ - Object { - "data": Object { - "agent.name": "python", - "id": "opbeans-python", - "service.name": "opbeans-python", - "serviceAnomalyStats": Object { - "actualValue": 24282.2352941176, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", - "serviceName": "opbeans-python", - "transactionType": "request", + it('returns the correct anomaly stats', () => { + const dataWithAnomalies = response.body.elements.filter( + (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + ); + + expectSnapshot(dataWithAnomalies.length).toMatchInline(`8`); + expectSnapshot(dataWithAnomalies.slice(0, 3)).toMatchInline(` + Array [ + Object { + "data": Object { + "agent.name": "python", + "id": "opbeans-python", + "service.name": "opbeans-python", + "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, + "anomalyScore": 0, + "healthStatus": "healthy", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", + "transactionType": "request", + }, }, }, - }, - Object { - "data": Object { - "agent.name": "nodejs", - "id": "opbeans-node", - "service.environment": "testing", - "service.name": "opbeans-node", - "serviceAnomalyStats": Object { - "actualValue": 29300.5555555556, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-testing-384f-high_mean_transaction_duration", - "serviceName": "opbeans-node", - "transactionType": "request", + Object { + "data": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "testing", + "service.name": "opbeans-node", + "serviceAnomalyStats": Object { + "actualValue": 29300.5555555556, + "anomalyScore": 0, + "healthStatus": "healthy", + "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", + "transactionType": "request", + }, }, }, - }, - Object { - "data": Object { - "agent.name": "rum-js", - "id": "opbeans-rum", - "service.environment": "testing", - "service.name": "opbeans-rum", - "serviceAnomalyStats": Object { - "actualValue": 2386500, - "anomalyScore": 0, - "healthStatus": "healthy", - "jobId": "apm-testing-384f-high_mean_transaction_duration", - "serviceName": "opbeans-rum", - "transactionType": "page-load", + Object { + "data": Object { + "agent.name": "rum-js", + "id": "opbeans-rum", + "service.environment": "testing", + "service.name": "opbeans-rum", + "serviceAnomalyStats": Object { + "actualValue": 2386500, + "anomalyScore": 0, + "healthStatus": "healthy", + "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", + "transactionType": "page-load", + }, }, }, - }, - ] - `); + ] + `); - expectSnapshot(response.body).toMatch(); + expectSnapshot(response.body).toMatch(); + }); }); - }); - describe('with a user that does not have access to ML', () => { - let response: PromiseReturnType<typeof supertest.get>; + describe('with a user that does not have access to ML', () => { + before(async () => { + response = await supertestAsApmReadUserWithoutMlAccess.get( + `/api/apm/service-map?start=${start}&end=${end}` + ); + }); - before(async () => { - response = await supertestAsApmReadUserWithoutMlAccess.get( - `/api/apm/service-map?start=${start}&end=${end}` - ); - }); + it('returns service map elements without anomaly stats', () => { + expect(response.status).to.be(200); - it('returns service map elements without anomaly stats', () => { - expect(response.status).to.be(200); + const dataWithAnomalies = response.body.elements.filter( + (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) + ); - const dataWithAnomalies = response.body.elements.filter( - (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) - ); + expect(dataWithAnomalies).to.be.empty(); + }); + }); + }); + + describe('with a single service', () => { + describe('when ENVIRONMENT_ALL is selected', () => { + it('returns service map elements', async () => { + response = await supertest.get( + url.format({ + pathname: '/api/apm/service-map', + query: { + environment: 'ENVIRONMENT_ALL', + start: metadata.start, + end: metadata.end, + serviceName: 'opbeans-java', + }, + }) + ); - expect(dataWithAnomalies).to.be.empty(); + expect(response.status).to.be(200); + expect(response.body.elements.length).to.be.greaterThan(1); + }); }); }); }); From a83b1c2fef953ad219c756a4355bb7701540cfe8 Mon Sep 17 00:00:00 2001 From: Wylie Conlon <william.conlon@elastic.co> Date: Wed, 3 Mar 2021 21:18:30 -0500 Subject: [PATCH 55/63] [Discover] Fix link from dashboard saved search to Discover (#92937) (#93500) * [Discover] Fix link from dashboard saved search to Discover * Fix tests that weren't fully testing the navigation * Fix snapshot * Fix test navigation to context app by reverting to previous * Unskip functional test and fix issue in data grid * Respond to review comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../doc_table/components/row_headers.test.js | 1 + .../angular/doc_table/components/table_row.ts | 11 +++++- .../components/table_row/details.html | 2 +- .../angular/doc_table/doc_table.html | 3 +- .../discover_grid_flyout.test.tsx | 18 +++++++--- .../discover_grid/discover_grid_flyout.tsx | 16 ++++++--- .../__snapshots__/doc_viewer.test.tsx.snap | 1 + .../components/doc_viewer/doc_viewer.tsx | 2 +- .../helpers/get_context_url.test.ts | 21 ++++++++--- .../application/helpers/get_context_url.tsx | 12 ++++--- .../apps/context/_discover_navigation.js | 35 ++++++++++++++++++- .../apps/discover/_data_grid_context.ts | 35 ++++++++++++++++++- test/functional/page_objects/discover_page.ts | 4 +++ 13 files changed, 139 insertions(+), 22 deletions(-) diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js index 1824110c85b1a9..270f366bddbf24 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js +++ b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js @@ -58,6 +58,7 @@ describe('Doc Table', () => { setServices({ uiSettings: core.uiSettings, filterManager: dataMock.query.filterManager, + addBasePath: (path) => path, }); setDocViewsRegistry({ diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index 12ec9445f4afcd..cf6b507edc070e 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -105,7 +105,16 @@ export function createTableRowDirective($compile: ng.ICompileService) { $scope.row._id, $scope.indexPattern.id, $scope.columns, - getServices().filterManager + getServices().filterManager, + getServices().addBasePath + ); + }; + + $scope.getSingleDocHref = () => { + return getServices().addBasePath( + `/app/discover#/doc/${$scope.indexPattern.id}/${ + $scope.row._index + }?id=${encodeURIComponent($scope.row._id)}` ); }; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html index bb443b880e2171..faa3d51c19feef 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html @@ -31,7 +31,7 @@ <a class="euiLink" data-test-subj="docTableRowAction" - ng-href="#/doc/{{indexPattern.id}}/{{row._index}}?id={{uriEncodedId}}" + ng-href="{{ getSingleDocHref() }}" i18n-id="discover.docTable.tableRow.viewSingleDocumentLinkText" i18n-default-message="View single document" ></a> diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.html b/src/plugins/discover/public/application/angular/doc_table/doc_table.html index 427893bd3e6fe8..4f297643a28f70 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.html +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.html @@ -26,7 +26,7 @@ </div> </div> <div class="kbnDocTable__container kbnDocTable__padBottom"> - <table class="kbnDocTable table" ng-if="indexPattern"> + <table class="kbnDocTable table" ng-if="indexPattern" data-test-subj="docTable"> <thead kbn-table-header columns="columns" @@ -44,6 +44,7 @@ index-pattern="indexPattern" filter="filter" class="kbnDocTable__row" + data-test-subj="docTableRow" on-add-column="onAddColumn" on-remove-column="onRemoveColumn" use-new-fields-api="useNewFieldsApi" diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx index ea5763e0bd2b81..b63aca85b1ec9b 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx @@ -32,7 +32,12 @@ describe('Discover flyout', function () { onClose={onClose} onFilter={jest.fn()} onRemoveColumn={jest.fn()} - services={({ filterManager: createFilterManagerMock() } as unknown) as DiscoverServices} + services={ + ({ + filterManager: createFilterManagerMock(), + addBasePath: (path: string) => path, + } as unknown) as DiscoverServices + } /> ); @@ -53,17 +58,22 @@ describe('Discover flyout', function () { onClose={onClose} onFilter={jest.fn()} onRemoveColumn={jest.fn()} - services={({ filterManager: createFilterManagerMock() } as unknown) as DiscoverServices} + services={ + ({ + filterManager: createFilterManagerMock(), + addBasePath: (path: string) => `/base${path}`, + } as unknown) as DiscoverServices + } /> ); const actions = findTestSubject(component, 'docTableRowAction'); expect(actions.length).toBe(2); expect(actions.first().prop('href')).toMatchInlineSnapshot( - `"#/doc/index-pattern-with-timefield-id/i?id=1"` + `"/base#/doc/index-pattern-with-timefield-id/i?id=1"` ); expect(actions.last().prop('href')).toMatchInlineSnapshot( - `"#/context/index-pattern-with-timefield-id/1?_g=(filters:!())&_a=(columns:!(date),filters:!())"` + `"/base/app/discover#/context/index-pattern-with-timefield-id/1?_g=(filters:!())&_a=(columns:!(date),filters:!())"` ); findTestSubject(component, 'euiFlyoutCloseButton').simulate('click'); expect(onClose).toHaveBeenCalled(); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx index a88cd239e2f04f..5994892ca2d40a 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.tsx @@ -81,9 +81,11 @@ export function DiscoverGridFlyout({ <EuiButtonEmpty size="xs" iconType="document" - href={`#/doc/${indexPattern.id}/${hit._index}?id=${encodeURIComponent( - hit._id as string - )}`} + href={services.addBasePath( + `#/doc/${indexPattern.id}/${hit._index}?id=${encodeURIComponent( + hit._id as string + )}` + )} data-test-subj="docTableRowAction" > {i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkTextSimple', { @@ -96,7 +98,13 @@ export function DiscoverGridFlyout({ <EuiButtonEmpty size="xs" iconType="documents" - href={getContextUrl(hit._id, indexPattern.id, columns, services.filterManager)} + href={getContextUrl( + hit._id, + indexPattern.id, + columns, + services.filterManager, + services.addBasePath + )} data-test-subj="docTableRowAction" > {i18n.translate('discover.grid.tableRow.viewSurroundingDocumentsLinkTextSimple', { diff --git a/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap index d02b484a06a49f..b0f5dc98a801ac 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap @@ -3,6 +3,7 @@ exports[`Render <DocViewer/> with 3 different tabs 1`] = ` <div className="kbnDocViewer" + data-test-subj="kbnDocViewer" > <EuiTabbedContent autoFocus="initial" diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.tsx index 0c888091bcbdfd..d0476d4c38b489 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.tsx @@ -46,7 +46,7 @@ export function DocViewer(renderProps: DocViewRenderProps) { } return ( - <div className="kbnDocViewer"> + <div className="kbnDocViewer" data-test-subj="kbnDocViewer"> <EuiTabbedContent size="s" tabs={tabs} /> </div> ); diff --git a/src/plugins/discover/public/application/helpers/get_context_url.test.ts b/src/plugins/discover/public/application/helpers/get_context_url.test.ts index 366432a6d6532c..4856c98845669b 100644 --- a/src/plugins/discover/public/application/helpers/get_context_url.test.ts +++ b/src/plugins/discover/public/application/helpers/get_context_url.test.ts @@ -12,19 +12,32 @@ const filterManager = ({ getGlobalFilters: () => [], getAppFilters: () => [], } as unknown) as FilterManager; +const addBasePath = (path: string) => `/base${path}`; describe('Get context url', () => { test('returning a valid context url', async () => { - const url = await getContextUrl('docId', 'ipId', ['test1', 'test2'], filterManager); + const url = await getContextUrl( + 'docId', + 'ipId', + ['test1', 'test2'], + filterManager, + addBasePath + ); expect(url).toMatchInlineSnapshot( - `"#/context/ipId/docId?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` + `"/base/app/discover#/context/ipId/docId?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` ); }); test('returning a valid context url when docId contains whitespace', async () => { - const url = await getContextUrl('doc Id', 'ipId', ['test1', 'test2'], filterManager); + const url = await getContextUrl( + 'doc Id', + 'ipId', + ['test1', 'test2'], + filterManager, + addBasePath + ); expect(url).toMatchInlineSnapshot( - `"#/context/ipId/doc%20Id?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` + `"/base/app/discover#/context/ipId/doc%20Id?_g=(filters:!())&_a=(columns:!(test1,test2),filters:!())"` ); }); }); diff --git a/src/plugins/discover/public/application/helpers/get_context_url.tsx b/src/plugins/discover/public/application/helpers/get_context_url.tsx index caed16edabb1d5..057f8bc2afc522 100644 --- a/src/plugins/discover/public/application/helpers/get_context_url.tsx +++ b/src/plugins/discover/public/application/helpers/get_context_url.tsx @@ -10,6 +10,7 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; import { url } from '../../../../kibana_utils/common'; import { esFilters, FilterManager } from '../../../../data/public'; +import { DiscoverServices } from '../../build_services'; /** * Helper function to generate an URL to a document in Discover's context view @@ -18,7 +19,8 @@ export function getContextUrl( documentId: string, indexPatternId: string, columns: string[], - filterManager: FilterManager + filterManager: FilterManager, + addBasePath: DiscoverServices['addBasePath'] ) { const globalFilters = filterManager.getGlobalFilters(); const appFilters = filterManager.getAppFilters(); @@ -36,7 +38,9 @@ export function getContextUrl( { encode: false, sort: false } ); - return `#/context/${encodeURIComponent(indexPatternId)}/${encodeURIComponent( - documentId - )}?${hash}`; + return addBasePath( + `/app/discover#/context/${encodeURIComponent(indexPatternId)}/${encodeURIComponent( + documentId + )}?${hash}` + ); } diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index 6152659e47f270..7756a915139e2c 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -18,8 +18,18 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const docTable = getService('docTable'); const filterBar = getService('filterBar'); - const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']); + const PageObjects = getPageObjects([ + 'common', + 'discover', + 'timePicker', + 'settings', + 'dashboard', + 'context', + 'header', + ]); const testSubjects = getService('testSubjects'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const browser = getService('browser'); describe('context link in discover', () => { before(async () => { @@ -94,5 +104,28 @@ export default function ({ getService, getPageObjects }) { await PageObjects.discover.waitForDiscoverAppOnScreen(); await PageObjects.discover.waitForDocTableLoadingComplete(); }); + + it('navigates to doc view from embeddable', async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.saveSearch('my search'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + + await dashboardAddPanel.addSavedSearch('my search'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await docTable.clickRowToggle({ rowIndex: 0 }); + const rowActions = await docTable.getRowActions({ rowIndex: 0 }); + await rowActions[1].click(); + await PageObjects.common.sleep(250); + // accept alert if it pops up + const alert = await browser.getAlert(); + await alert?.accept(); + expect(await browser.getCurrentUrl()).to.contain('#/doc'); + expect(await PageObjects.discover.isShowingDocViewer()).to.be(true); + }); }); } diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 8f817dbea35c3d..896cd4ad595c95 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -20,10 +20,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const filterBar = getService('filterBar'); const dataGrid = getService('dataGrid'); const docTable = getService('docTable'); - const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']); + const PageObjects = getPageObjects([ + 'common', + 'discover', + 'timePicker', + 'settings', + 'dashboard', + 'header', + ]); const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; const kibanaServer = getService('kibanaServer'); const esArchiver = getService('esArchiver'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const browser = getService('browser'); describe('discover data grid context tests', () => { before(async () => { @@ -78,5 +87,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } expect(disabledFilterCounter).to.be(TEST_FILTER_COLUMN_NAMES.length); }); + + it('navigates to context view from embeddable', async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.saveSearch('my search'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + + await dashboardAddPanel.addSavedSearch('my search'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await dataGrid.clickRowToggle({ rowIndex: 0 }); + const rowActions = await dataGrid.getRowActions({ rowIndex: 0 }); + await rowActions[1].click(); + await PageObjects.common.sleep(250); + // accept alert if it pops up + const alert = await browser.getAlert(); + await alert?.accept(); + expect(await browser.getCurrentUrl()).to.contain('#/context'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await docTable.getRowsText()).to.have.length(6); + }); }); } diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 4763a12c293296..d4813d51b589ec 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -229,6 +229,10 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider await find.clickByCssSelector('.fa-sort-up'); } + public async isShowingDocViewer() { + return await testSubjects.exists('kbnDocViewer'); + } + public async getMarks() { const table = await docTable.getTable(); const $ = await table.parseDomContent(); From 826d4863b16f1177d9126eec188b34a1899a184b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 21:41:07 -0500 Subject: [PATCH 56/63] [Security Solution] fix data provider cypress test (#93465) (#93538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR is to fix data_provider's cypress test: displays the data provider action menu when Enter is pressed. When I ran it locally, I couldn’t reproduce it every time. There’s a chance that the timeline was opened but the filter we put wasn’t there, this happen when I simulate slow 3G with Chrome or once out of my 10-time-trial with `loop_cypress_tests.js` [failure 1](https://kibana-ci.elastic.co/job/elastic+kibana+security-cypress/4313/testReport/junit/(root)/displays%20the%20data%20provider%20action%20menu%20when%20Enter%20is%20pressed/timeline_data_providers_displays_the_data_provider_action_menu_when_Enter_is_pressed/) [failure 2](https://kibana-ci.elastic.co/job/elastic+kibana+security-cypress/4313/testReport/junit/(root)/displays%20the%20data%20provider%20action%20menu%20when%20Enter%20is%20pressed/timeline_data_providers_displays_the_data_provider_action_menu_when_Enter_is_pressed_2/) How to run this test several times automatically: 1. Go to the file and mark your case with .only 2. Copy the relative path of the file 3. Go to x-pack/plugins/security_solution/package.json Line 13, change —spec to the path you just copied (e.g: ./cypress/integration/timelines/data_providers.spec.ts) 4. Go to Kibana/ and run node x-pack/plugins/security_solution/scripts/loop_cypress_tests.js 1 (1 means run it once, you can put more to check flakiness) 5. After it finishes, it generates you a report under: kibana/target/loop-cypress-tests.txt 6. Search for `displays the data provider action menu when Enter is pressed.` and see if all passes. Co-authored-by: Angela Chuang <6295984+angorayc@users.noreply.github.com> --- .../integration/timelines/data_providers.spec.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts index 1955e320556a8c..9f0d64a77c8a15 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/data_providers.spec.ts @@ -10,6 +10,7 @@ import { TIMELINE_DATA_PROVIDERS_EMPTY, TIMELINE_DROPPED_DATA_PROVIDERS, TIMELINE_DATA_PROVIDERS_ACTION_MENU, + TIMELINE_FLYOUT_HEADER, } from '../../screens/timeline'; import { HOSTS_NAMES_DRAGGABLE } from '../../screens/hosts/all_hosts'; @@ -60,8 +61,13 @@ describe('timeline data providers', () => { openTimelineUsingToggle(); cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('not.exist'); - cy.get(TIMELINE_DROPPED_DATA_PROVIDERS).first().focus(); - cy.get(TIMELINE_DROPPED_DATA_PROVIDERS).first().parent().type('{enter}'); + cy.get(`${TIMELINE_FLYOUT_HEADER} ${TIMELINE_DROPPED_DATA_PROVIDERS}`) + .pipe(($el) => $el.trigger('focus')) + .should('exist'); + cy.get(`${TIMELINE_FLYOUT_HEADER} ${TIMELINE_DROPPED_DATA_PROVIDERS}`) + .first() + .parent() + .type('{enter}'); cy.get(TIMELINE_DATA_PROVIDERS_ACTION_MENU).should('exist'); }); From 767a3360bd22a6ff504b22f98d3901b7179d3b0f Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 22:29:54 -0500 Subject: [PATCH 57/63] chore(NA): do not use execa on bazel workspace status update script (#93532) (#93546) Co-authored-by: Tiago Costa <tiagoffcc@hotmail.com> --- src/dev/bazel_workspace_status.js | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/dev/bazel_workspace_status.js b/src/dev/bazel_workspace_status.js index 3c3ef1574cd8e8..c7ae05ce487446 100644 --- a/src/dev/bazel_workspace_status.js +++ b/src/dev/bazel_workspace_status.js @@ -17,13 +17,21 @@ // If the script exits with non-zero code, it's considered as a failure // and the output will be discarded. -(async () => { - const execa = require('execa'); +(() => { + const cp = require('child_process'); const os = require('os'); - async function runCmd(cmd, args) { + function runCmd(cmd, args) { try { - return await execa(cmd, args); + const spawnResult = cp.spawnSync(cmd, args); + const exitCode = spawnResult.status !== null ? spawnResult.status : 1; + const stdoutStr = spawnResult.stdout.toString(); + const stdout = stdoutStr ? stdoutStr.trim() : null; + + return { + exitCode, + stdout, + }; } catch (e) { return { exitCode: 1 }; } @@ -31,29 +39,25 @@ // Git repo const kbnGitOriginName = process.env.KBN_GIT_ORIGIN_NAME || 'origin'; - const repoUrlCmdResult = await runCmd('git', [ - 'config', - '--get', - `remote.${kbnGitOriginName}.url`, - ]); + const repoUrlCmdResult = runCmd('git', ['config', '--get', `remote.${kbnGitOriginName}.url`]); if (repoUrlCmdResult.exitCode === 0) { // Only output REPO_URL when found it console.log(`REPO_URL ${repoUrlCmdResult.stdout}`); } // Commit SHA - const commitSHACmdResult = await runCmd('git', ['rev-parse', 'HEAD']); + const commitSHACmdResult = runCmd('git', ['rev-parse', 'HEAD']); if (commitSHACmdResult.exitCode === 0) { console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`); // Branch - const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']); + const gitBranchCmdResult = runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']); if (gitBranchCmdResult.exitCode === 0) { console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`); } // Tree status - const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']); + const treeStatusCmdResult = runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']); const treeStatusVarStr = 'GIT_TREE_STATUS'; if (treeStatusCmdResult.exitCode === 0) { console.log(`${treeStatusVarStr} Clean`); @@ -64,7 +68,7 @@ // Host if (process.env.CI) { - const hostCmdResult = await runCmd('hostname'); + const hostCmdResult = runCmd('hostname'); const hostStr = hostCmdResult.stdout.split('-').slice(0, -1).join('-'); const coresStr = os.cpus().filter((cpu, index) => { return !cpu.model.includes('Intel') || index % 2 === 1; From f6f37cefcdeb3e2d5e56ab26c5040be022c21308 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 23:18:07 -0500 Subject: [PATCH 58/63] Bump dependencies (#93511) (#93543) Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 66 ++++++++++++++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 1ae6f1cb6198f5..c173228ae3cd35 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "**/load-grunt-config/lodash": "^4.17.21", "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", - "**/prismjs": "1.22.0", + "**/prismjs": "1.23.0", "**/react-syntax-highlighter": "^15.3.1", "**/react-syntax-highlighter/**/highlight.js": "^10.4.1", "**/request": "^2.88.2", diff --git a/yarn.lock b/yarn.lock index b6afbff9b7ef9b..e1d640430e9401 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9197,10 +9197,10 @@ bmp-js@^0.1.0: resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233" integrity sha1-4Fpj95amwf8l9Hcex62twUjAcjM= -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.9: + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== body-parser@1.19.0, body-parser@^1.18.1, body-parser@^1.18.3: version "1.19.0" @@ -9340,7 +9340,7 @@ broadcast-channel@^3.0.3: rimraf "3.0.0" unload "2.2.0" -brorand@^1.0.1: +brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= @@ -13110,17 +13110,17 @@ element-resize-detector@^1.1.15: batch-processor "^1.0.0" elliptic@^6.0.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" + bn.js "^4.11.9" + brorand "^1.1.0" hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" emittery@^0.7.1: version "0.7.1" @@ -16553,7 +16553,7 @@ hjson@3.2.1: resolved "https://registry.yarnpkg.com/hjson/-/hjson-3.2.1.tgz#20de41dc87fc9a10d1557d0230b0e02afb1b09ac" integrity sha512-OhhrFMeC7dVuA1xvxuXGTv/yTdhTvbe8hz+3LgVNsfi9+vgz0sF/RrkuX8eegpKaMc9cwYwydImBH6iePoJtdQ== -hmac-drbg@^1.0.0: +hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= @@ -20782,12 +20782,12 @@ mini-css-extract-plugin@0.8.0: schema-utils "^1.0.0" webpack-sources "^1.1.0" -minimalistic-assert@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" - integrity sha1-cCvi3aazf0g2vLP121ZkG2Sh09M= +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: +minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= @@ -21564,9 +21564,9 @@ node-modules-regexp@^1.0.0: integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= node-notifier@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.0.tgz#a7eee2d51da6d0f7ff5094bc7108c911240c1620" - integrity sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA== + version "8.0.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.1.tgz#f86e89bbc925f2b068784b31f382afdc6ca56be1" + integrity sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA== dependencies: growly "^1.3.0" is-wsl "^2.2.0" @@ -23380,10 +23380,10 @@ printj@~1.1.0: resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== -prismjs@1.22.0, prismjs@^1.22.0, prismjs@~1.22.0: - version "1.22.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.22.0.tgz#73c3400afc58a823dd7eed023f8e1ce9fd8977fa" - integrity sha512-lLJ/Wt9yy0AiSYBf212kK3mM5L8ycwlyTlSxHBAneXLR0nzFMlZ5y7riFPF3E33zXOF2IH95xdY5jIyZbM9z/w== +prismjs@1.23.0, prismjs@^1.22.0, prismjs@~1.22.0: + version "1.23.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" + integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA== optionalDependencies: clipboard "^2.0.0" @@ -28670,9 +28670,9 @@ typescript@4.1.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.5.3, ty integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== ua-parser-js@^0.7.18: - version "0.7.23" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.23.tgz#704d67f951e13195fbcd3d78818577f5bc1d547b" - integrity sha512-m4hvMLxgGHXG3O3fQVAyyAQpZzDOvwnhOTjYz5Xmr7r/+LpkNy3vJXdVRWgd1TkAb7NGROZuSy96CrlNVjA7KA== + version "0.7.24" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c" + integrity sha512-yo+miGzQx5gakzVK3QFfN0/L9uVhosXBBO7qmnk7c2iw1IhL212wfA3zbnI54B0obGwC/5NWub/iT9sReMx+Fw== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" @@ -29255,9 +29255,9 @@ url-parse-lax@^3.0.0: prepend-http "^2.0.0" url-parse@^1.4.3, url-parse@^1.4.7: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + version "1.5.1" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" + integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" From 605e54a0061230a2dc455339b52655b95ceaa515 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 3 Mar 2021 23:51:32 -0500 Subject: [PATCH 59/63] [Security Solution][Detections] ML Popover overflow fix (#93525) (#93551) Co-authored-by: Garrett Spong <spong@users.noreply.github.com> Co-authored-by: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Co-authored-by: Garrett Spong <spong@users.noreply.github.com> --- .../public/common/components/ml_popover/ml_popover.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx index 1f216bb8da1a34..561805217e8a14 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx @@ -27,6 +27,10 @@ import { useSecurityJobs } from './hooks/use_security_jobs'; const PopoverContentsDiv = styled.div` max-width: 684px; + max-height: 90vh; + overflow-y: auto; + overflow-x: hidden; + padding-bottom: 15px; `; PopoverContentsDiv.displayName = 'PopoverContentsDiv'; From bb1f1d912ef59ba917f6d1aaf68f3da13c600b89 Mon Sep 17 00:00:00 2001 From: Liza Katz <lizka.k@gmail.com> Date: Thu, 4 Mar 2021 11:53:53 +0200 Subject: [PATCH 60/63] master docs are broken (#93405) (#93561) # Conflicts: # api_docs/data.json --- src/plugins/data/common/es_query/filters/phrase_filter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.ts b/src/plugins/data/common/es_query/filters/phrase_filter.ts index 364e8dc1b035fd..2a7f2458a27de8 100644 --- a/src/plugins/data/common/es_query/filters/phrase_filter.ts +++ b/src/plugins/data/common/es_query/filters/phrase_filter.ts @@ -97,6 +97,7 @@ export const getPhraseScript = (field: IFieldType, value: string) => { }; /** + * @internal * See issues bellow for the reason behind this change. * Values need to be converted to correct types for boolean \ numeric fields. * https://github.com/elastic/kibana/issues/74301 @@ -122,6 +123,7 @@ export const getConvertedValueForField = (field: IFieldType, value: any) => { }; /** + * @internal * Takes a scripted field and returns an inline script appropriate for use in a script query. * Handles lucene expression and Painless scripts. Other langs aren't guaranteed to generate valid * scripts. From 8c8993dda916a78809a41a86050e8d93e6cb639d Mon Sep 17 00:00:00 2001 From: Christos Nasikas <christos.nasikas@elastic.co> Date: Thu, 4 Mar 2021 12:58:16 +0200 Subject: [PATCH 61/63] [Security Solution][Case] Fix individual case deletion on case view (#93218) (#93564) # Conflicts: # x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx --- .../cases/components/all_cases/index.test.tsx | 15 +++++--- .../cases/components/all_cases/index.tsx | 30 +++++++--------- .../cases/components/bulk_actions/index.tsx | 15 ++++---- .../case_action_bar/actions.test.tsx | 2 +- .../components/case_action_bar/actions.tsx | 4 ++- .../cases/components/case_view/index.tsx | 36 ++++++++++--------- .../public/cases/containers/mock.ts | 31 ++++++++++++++++ .../public/cases/containers/types.ts | 2 +- .../containers/use_delete_cases.test.tsx | 8 ++++- 9 files changed, 92 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 1fbda69d8916c6..b4fb090700382e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -11,7 +11,7 @@ import moment from 'moment-timezone'; import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock'; -import { casesStatus, useGetCasesMockState } from '../../containers/mock'; +import { casesStatus, useGetCasesMockState, collectionCase } from '../../containers/mock'; import * as i18n from './translations'; import { CaseStatuses, CaseType } from '../../../../../case/common/api'; @@ -410,7 +410,7 @@ describe('AllCases', () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, filterOptions: { ...defaultGetCases.filterOptions, status: CaseStatuses.closed }, - selectedCases: useGetCasesMockState.data.cases, + selectedCases: [...useGetCasesMockState.data.cases, collectionCase], }); useDeleteCasesMock @@ -439,9 +439,14 @@ describe('AllCases', () => { ) .last() .simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( - useGetCasesMockState.data.cases.map(({ id }) => ({ id })) - ); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual([ + ...useGetCasesMockState.data.cases.map(({ id, type, title }) => ({ id, type, title })), + { + id: collectionCase.id, + title: collectionCase.title, + type: collectionCase.type, + }, + ]); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index 5f0e72564f60e9..7b72a2e1889038 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -167,6 +167,7 @@ export const AllCases = React.memo<AllCasesProps>( const [deleteThisCase, setDeleteThisCase] = useState<DeleteCase>({ title: '', id: '', + type: null, }); const [deleteBulk, setDeleteBulk] = useState<DeleteCase[]>([]); const filterRefetch = useRef<() => void>(); @@ -230,10 +231,10 @@ export const AllCases = React.memo<AllCasesProps>( ); const toggleBulkDeleteModal = useCallback( - (caseIds: string[]) => { + (cases: Case[]) => { handleToggleModal(); - if (caseIds.length === 1) { - const singleCase = selectedCases.find((theCase) => theCase.id === caseIds[0]); + if (cases.length === 1) { + const singleCase = cases[0]; if (singleCase) { return setDeleteThisCase({ id: singleCase.id, @@ -242,10 +243,14 @@ export const AllCases = React.memo<AllCasesProps>( }); } } - const convertToDeleteCases: DeleteCase[] = caseIds.map((id) => ({ id })); + const convertToDeleteCases: DeleteCase[] = cases.map(({ id, title, type }) => ({ + id, + title, + type, + })); setDeleteBulk(convertToDeleteCases); }, - [selectedCases, setDeleteBulk, handleToggleModal] + [setDeleteBulk, handleToggleModal] ); const handleUpdateCaseStatus = useCallback( @@ -255,11 +260,6 @@ export const AllCases = React.memo<AllCasesProps>( [selectedCases, updateBulkStatus] ); - const selectedCaseIds = useMemo( - (): string[] => selectedCases.map((caseObj: Case) => caseObj.id), - [selectedCases] - ); - const getBulkItemsPopoverContent = useCallback( (closePopover: () => void) => ( <EuiContextMenuPanel @@ -268,19 +268,13 @@ export const AllCases = React.memo<AllCasesProps>( caseStatus: filterOptions.status, closePopover, deleteCasesAction: toggleBulkDeleteModal, - selectedCaseIds, + selectedCases, updateCaseStatus: handleUpdateCaseStatus, includeCollections: isSelectedCasesIncludeCollections(selectedCases), })} /> ), - [ - selectedCases, - selectedCaseIds, - filterOptions.status, - toggleBulkDeleteModal, - handleUpdateCaseStatus, - ] + [selectedCases, filterOptions.status, toggleBulkDeleteModal, handleUpdateCaseStatus] ); const handleDispatchUpdate = useCallback( (args: Omit<UpdateCase, 'refetchCasesStatus'>) => { diff --git a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx index 17e196d590418c..e443ca77cb755b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx @@ -11,12 +11,13 @@ import { EuiContextMenuItem } from '@elastic/eui'; import { CaseStatuses } from '../../../../../case/common/api'; import { CaseStatusWithAllStatus } from '../status'; import * as i18n from './translations'; +import { Case } from '../../containers/types'; interface GetBulkItems { caseStatus: CaseStatusWithAllStatus; closePopover: () => void; - deleteCasesAction: (cases: string[]) => void; - selectedCaseIds: string[]; + deleteCasesAction: (cases: Case[]) => void; + selectedCases: Case[]; updateCaseStatus: (status: string) => void; includeCollections: boolean; } @@ -25,7 +26,7 @@ export const getBulkItems = ({ caseStatus, closePopover, deleteCasesAction, - selectedCaseIds, + selectedCases, updateCaseStatus, includeCollections, }: GetBulkItems) => { @@ -34,7 +35,7 @@ export const getBulkItems = ({ const openMenuItem = ( <EuiContextMenuItem data-test-subj="cases-bulk-open-button" - disabled={selectedCaseIds.length === 0 || includeCollections} + disabled={selectedCases.length === 0 || includeCollections} key={i18n.BULK_ACTION_OPEN_SELECTED} icon="folderOpen" onClick={() => { @@ -49,7 +50,7 @@ export const getBulkItems = ({ const closeMenuItem = ( <EuiContextMenuItem data-test-subj="cases-bulk-close-button" - disabled={selectedCaseIds.length === 0 || includeCollections} + disabled={selectedCases.length === 0 || includeCollections} key={i18n.BULK_ACTION_CLOSE_SELECTED} icon="folderCheck" onClick={() => { @@ -80,10 +81,10 @@ export const getBulkItems = ({ data-test-subj="cases-bulk-delete-button" key={i18n.BULK_ACTION_DELETE_SELECTED} icon="trash" - disabled={selectedCaseIds.length === 0} + disabled={selectedCases.length === 0} onClick={() => { closePopover(); - deleteCasesAction(selectedCaseIds); + deleteCasesAction(selectedCases); }} > {i18n.BULK_ACTION_DELETE_SELECTED} diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/actions.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/actions.test.tsx index 58e0e60160c9cc..ba0c725f994604 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/actions.test.tsx @@ -74,7 +74,7 @@ describe('CaseView actions', () => { expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([ - { id: basicCase.id, title: basicCase.title }, + { id: basicCase.id, title: basicCase.title, type: 'individual' }, ]); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/actions.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/actions.tsx index 80047d7e573ba5..74d2a40f1ceb92 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/actions.tsx @@ -42,7 +42,9 @@ const ActionsComponent: React.FC<CaseViewActions> = ({ isModalVisible={isDisplayConfirmDeleteModal} isPlural={false} onCancel={handleToggleModal} - onConfirm={handleOnDeleteConfirm.bind(null, [{ id: caseData.id, title: caseData.title }])} + onConfirm={handleOnDeleteConfirm.bind(null, [ + { id: caseData.id, title: caseData.title, type: caseData.type }, + ])} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 83a0c4e7acd3d6..9bbf2db3d83c58 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -397,27 +397,29 @@ export const CaseComponent = React.memo<CaseProps>( userCanCrud={userCanCrud} /> {(caseData.type !== CaseType.collection || hasDataToPush) && ( - <EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexEnd"> + <> <MyEuiHorizontalRule margin="s" data-test-subj="case-view-bottom-actions-horizontal-rule" /> - {caseData.type !== CaseType.collection && ( - <EuiFlexItem grow={false}> - <StatusActionButton - status={caseData.status} - onStatusChanged={changeStatus} - disabled={!userCanCrud} - isLoading={isLoading && updateKey === 'status'} - /> - </EuiFlexItem> - )} - {hasDataToPush && ( - <EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}> - {pushButton} - </EuiFlexItem> - )} - </EuiFlexGroup> + <EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexEnd"> + {caseData.type !== CaseType.collection && ( + <EuiFlexItem grow={false}> + <StatusActionButton + status={caseData.status} + onStatusChanged={changeStatus} + disabled={!userCanCrud} + isLoading={isLoading && updateKey === 'status'} + /> + </EuiFlexItem> + )} + {hasDataToPush && ( + <EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}> + {pushButton} + </EuiFlexItem> + )} + </EuiFlexGroup> + </> )} </> )} diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index d8692da986cbef..719fe015792858 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -103,6 +103,37 @@ export const basicCase: Case = { subCaseIds: [], }; +export const collectionCase: Case = { + type: CaseType.collection, + closedAt: null, + closedBy: null, + id: 'collection-id', + comments: [basicComment], + createdAt: basicCreatedAt, + createdBy: elasticUser, + connector: { + id: '123', + name: 'My Connector', + type: ConnectorTypes.none, + fields: null, + }, + description: 'Security banana Issue', + externalService: null, + status: CaseStatuses.open, + tags, + title: 'Another horrible breach in a collection!!', + totalComment: 1, + totalAlerts: 0, + updatedAt: basicUpdatedAt, + updatedBy: elasticUser, + version: 'WzQ3LDFd', + settings: { + syncAlerts: true, + }, + subCases: [], + subCaseIds: [], +}; + export const basicCasePost: Case = { ...basicCase, updatedAt: null, diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index 09c911d93ea474..98e0ced2b60670 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -150,8 +150,8 @@ export interface ActionLicense { export interface DeleteCase { id: string; + type: CaseType | null; title?: string; - type?: CaseType; } export interface FieldMappings { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.test.tsx index 1525f145f9030f..422eb0c92cbd84 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.test.tsx @@ -6,6 +6,8 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; + +import { CaseType } from '../../../../case/common/api'; import { useDeleteCases, UseDeleteCase } from './use_delete_cases'; import * as api from './api'; @@ -13,7 +15,11 @@ jest.mock('./api'); describe('useDeleteCases', () => { const abortCtrl = new AbortController(); - const deleteObj = [{ id: '1' }, { id: '2' }, { id: '3' }]; + const deleteObj = [ + { id: '1', type: CaseType.individual }, + { id: '2', type: CaseType.individual }, + { id: '3', type: CaseType.individual }, + ]; const deleteArr = ['1', '2', '3']; it('init', async () => { await act(async () => { From cedee32192d5b2e9e17a8d32d9ce64d855499988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= <alejandro.fernandez@elastic.co> Date: Thu, 4 Mar 2021 13:38:05 +0100 Subject: [PATCH 62/63] [Fleet] Correctly track install status of an integration (#93464) (#93575) --- .../fleet/sections/epm/screens/detail/index.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index 3cb57b63e707da..61de3a6f6c9cea 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -35,7 +35,8 @@ import { Error, Loading } from '../../../../components'; import { useBreadcrumbs } from '../../../../hooks'; import { WithHeaderLayout, WithHeaderLayoutProps } from '../../../../layouts'; import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from '../../components/release_badge'; -import { useSetPackageInstallStatus } from '../../hooks'; +import { useGetPackageInstallStatus, useSetPackageInstallStatus } from '../../hooks'; + import { IntegrationAgentPolicyCount, UpdateIcon, IconPanel, LoadingIconPanel } from './components'; import { OverviewPage } from './overview'; import { PackagePoliciesPage } from './policies'; @@ -74,6 +75,15 @@ export function Detail() { // Package info state const [packageInfo, setPackageInfo] = useState<PackageInfo | null>(null); const setPackageInstallStatus = useSetPackageInstallStatus(); + const getPackageInstallStatus = useGetPackageInstallStatus(); + + const packageInstallStatus = useMemo(() => { + if (packageInfo === null || !packageInfo.name) { + return undefined; + } + return getPackageInstallStatus(packageInfo.name).status; + }, [packageInfo, getPackageInstallStatus]); + const updateAvailable = packageInfo && 'savedObject' in packageInfo && @@ -85,7 +95,6 @@ export function Detail() { pkgkey ); - const packageInstallStatus = packageInfoData?.response.status; const showCustomTab = useUIExtension(packageInfoData?.response.name ?? '', 'package-detail-custom') !== undefined; From 5f41bd2ecf6cc8150f24ad88d3cea35ff76ddb54 Mon Sep 17 00:00:00 2001 From: Anton Dosov <anton.dosov@elastic.co> Date: Thu, 4 Mar 2021 14:31:30 +0100 Subject: [PATCH 63/63] Fix wrong import in data plugin causing 100kB bundle increase (#93448) (#93580) # Conflicts: # api_docs/data.json # api_docs/data_search.json # src/plugins/data/common/search/search_source/search_source.ts --- .eslintrc.js | 2 +- packages/kbn-optimizer/limits.yml | 2 +- .../common/search/search_source/search_source.ts | 13 +++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index be2e4aa7353a87..9e8feccdc7022d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1344,7 +1344,7 @@ module.exports = { 'no-restricted-imports': [ 'error', { - patterns: ['lodash/*', '!lodash/fp'], + patterns: ['lodash/*', '!lodash/fp', 'rxjs/internal-compatibility'], }, ], }, diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 657aabca1e86d0..d57d460c79306b 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -14,7 +14,7 @@ pageLoadAssetSize: dashboard: 374267 dashboardEnhanced: 65646 dashboardMode: 22716 - data: 1319839 + data: 900000 dataEnhanced: 50420 devTools: 38781 discover: 105147 diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 61af244a416005..60e7b458eef31d 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -59,10 +59,19 @@ */ import { setWith } from '@elastic/safer-lodash-set'; -import { uniqueId, keyBy, pick, difference, omit, isFunction, isEqual, uniqWith } from 'lodash'; +import { + uniqueId, + keyBy, + pick, + difference, + isFunction, + isEqual, + uniqWith, + isObject, + omit, +} from 'lodash'; import { map, switchMap, tap } from 'rxjs/operators'; import { defer, from } from 'rxjs'; -import { isObject } from 'rxjs/internal-compatibility'; import { normalizeSortRequest } from './normalize_sort_request'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern, IndexPattern, IndexPatternField } from '../../index_patterns';

W*_pdOd{W+4P?o&VT6AM%^E=?N1wX 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 VAUMk*;UEJLc)I$ztaD0e0svNfxw!xU literal 255161 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%0A+o&Bpl$9vG zay?H0KS^sRiqo!**{!AJj3519(jhp2>-i*r&>G0+?p}9w`<5JO{~$3Cl2BfD}S5SlHviTA@mq+OVkcKE`bMs2M(@2h1J+v2X%E!0x-`NNb;uhm!oZt7ZT%JEKkq>94!0m0#bnhk!x)AZ`F@~IVkqOKl^Wc z|NqIzZwTtu{^SDq*WBfAzW;mA|K8yL+tvT^5&v@?f1VHk#rmJ4{L>i!a~J=)@cyrI z7l4Q)3YTIHRE=!UKXukP;R>SALq&S6Lq_$*(4kMM`4;gNj0_CJ{_2Z#|8}Ts>a8q{a6a4$W{BtTFxR~zqdCv>%wQh)#uqYm& zxgolO_VsGTS_V|oQUyF^StLN<|L3;+*FUAe6ff_@J)a95byft1JwHADB@@C3MMQ6= z!_Azy#dF;%OLbGP`-}{+i;;5~zlxFGO0grur@ThYIJ2nP8N{)toRFc_URpq39LQCZ z1htJ=tO0%T9aeynJk^868h+8!OfbQA&Svo0zvsJWP^0tVWsX^LD? zfKc4>r!8y(AVixC)rOcfU!HMp@&lz1~%o&4_kL0{3z`;=z=i^2GJ`83yW&Yzz%*(PCa|@t`yPE`n{O| zKqia68>rk0G^wVVBnjEtXxmiPLq}aiQET7P$t-bsn4T}E9+1B<>HlFMKG&KuetAjG9?0%xJNwJN04MYOR_2ZUP~kWT5#mMP@MB* zyYqUr-Hqhz3ny|opQ%@`RBTYIWD)JEr|Tb z$^7dGy-KH$<7AK*m~amcE!!P|$|dU67z*sGd5 zrD}WtqZZqQp~u#|5GoEnbr#{aGEI&;gfgYQ8LHJ%1aVo9r#MZKA?nsSIFnWkX^g*z zV!JoP;%39o+JSlY-bR*q8!K%Cw-%%bFvrb@fUhQ)`j)R|1j}O77k8SJ`DVR^g`S`G zM^s*n(q&Wa{^X7Tw7>Yv?1+o}naQ}D^zUi38Y-^!CW3@H?GaV8-!~uugdC@5N3%Jj zgfd;_Yy6KKVxEJ&ie3d2hhol&_5kf!3m~UGJD5mjA{*zmrvGpC=8MAVO*tlpal217 zbG4jZpU=HAHt1gWrRTuY@J2|L-dTF=F3xs4Jg0}ucrH{}67&L4r*-9KxmSUnjFf_3 zzSVudw}FbV(pp$|A4zZL&JE@pOF&b))6+YMcvY!gzdP{Ia>YP}a7m2k19vJ;%61EN z{VM^Vpa~Nny;saKE_6JX=UP)AmNXu^K+?4R4P3p=SSWXY?wuWL@FoCKb%N-6l}Q`B z!}8|sDL%oSnzLe}BBT7?M>~sf=xdx)rY%NvecJxza)Nf4`1-3s@9yD8SU?IH5YLV zhepMe%y7{!`|H!ZXxD+u`05miR%7(U56)T0CUBCnVWq$<)B(Kfj9h$fy8}52Xfh`P zZYF7McqK!rv0?z|d_dk()4RF*lQ1h4KySfhw)Oq)%h%B|?AIJWd8I1PRy`71de0Q% zkAhyA`lMLWDYjH*>=lZPIAtJOiuO)R8Lt2aI+$3e6KVe|slM>63Q7#2Z?d>l5MnIU zkxUE050o8gjhYUY(U7Kwx@T~wu^Z2mCC+%1F4Wrd`LvX?)cAHZ4gy?i&Kj`f{7pEC z7F!6nbZ-t5V>te!{cfv$~9D3n`v^%ECeZ z8Qm#@ohj4SY9s80CACp&m3r}(cl1Z_g#LL+nJX~>{BYa11=Lr^GRYq9`Z4>iMRgWG z^f-rad*+&jF*2B|Y2p;v654bkx9wL`_*L4v@DG6rfXGYGc!0Yit|RFociJ0fbh7y* z*QQFhZl%@uR~wX80MhGRR*TG7qqZc(we#_Dn?|By+sTRv=qVB~M&NC^G~HH!9XhOJ zDIk9qV_`PHX2xQ78HC_{s30>wV&I{STBa_1WL9)N@0;I$Zq(n?abG``k7f7+9@~wk zIjYGJL!V8py*`<|%K-BuOup}A! zA2+M7Iz=8(IBw4t()9@qv0U8snQzZcG!2&=7qFv|9B6IXi@zc#5o%>fPIxo$epwG@ zC)X&0vr}v(!?&SJAA}kcUBU5RcF4Xp2}pKqn4CNBFR?LXe}ONiW0Ma6N{t}u-Pib0 zDRz=S0Xg`^5nU%Or>Pgy{F8pg7Z>A{{UG*7bAKsWyMcSzu2^@Fp5tYezt%VYFmudJ zUqGoc?o(KWsz`potw@!Qotr7ny;tPq%?gZt*b?NVl58x9>kfK|9_dV4_ZoBofKDic z(MdW(5m5_MCy^^7P}rr)GInaEaf?tCvjB$>s;ijHkbyBYKKqQaCJ7{rgZ+)ovRdm!)3Gm4yB@?;BuQFF>so@;xL`RK5b7Qz3fPb<^n(b-_UL z_syWDfDg0;#2Ar+lLep|!}Q+5jvvK6S0yiVvZlHFi=K61_!D)U?XSq)x>4|TA^p{T zJb9Vgp2u)0$cI?}gl+`CFn?jfdnz!C7lbgp{I;CRhkT`xV1V4z$B$hj#~h5csc3ls z0S4Q8JJj2nmq01v%5mW4%W3w^AuWWY8qp{R(cd)G=#qFV%=CwlUbCJ@4gJ?_j65;0 zk@GKSy@H|Cf?X5;t*3Nx=f)6la5DJ0ov${ND^(f5LtF~!#n%T*lm~OVzgmh;UXc@( zE3kilVeTlOG%UUQ*AUk;#2AVFER+)!fDlwVf8SvsHPBZgeby7WCBvUN?KR5;KBDZ& zx_z++>c1X|)EUM7R&jwXG#{5yl;7L6b@rZ32ai0ZC}4hh5z;Nc3zuk5as1rqk>Fw* z-)Uq0Y*g&%^b2H!-pA~a-u@O@Kco?bQi+BiG@RBrm1gf`ZCY1DD3@aG$tXC+-zD zRf;09jfvQ6Zonkm4FTkK!_jl|`3~Z>V^gw+fZtLFZW}tUdC|SEiY4Zo_gv=OM((O5L>qOvyx` znZv|Eof8r1@yljWE3ld3)PZB+{Bu51gW+LDqY4%A$5VM_qMSv<|7)AbO3 z+jv3@iP&aVW=8iDf-G*~*)inpE8INF-0{|^5a@7$UlS+Fu3Ye(J00Ftz0+jgvgTwMt|{}QOvU6E2O*ST;EJ5na@RvzAw$rv zN7fwlNc^~Zyu5iCT;qNa8?SV9=_u@wy{C;_qJsL%skgiKw^x_&*nd+;B&WZcCy~3r z;HSJt-+|+gEVwwIN_8kOLw*nB4O~|5IDJb1oPx8jG?js|HwNaptlP+T8@Mu=5kF6o zx2f6Fq3>t{(!6&c-X)p`=&Jas3_L?IYb!>B4{l(x){QMpnEpF!0Xo_OrKs`bN1k0e zLD<{XpC>7eRE;@W4?^o`LmtGrpX9f8vu5J%ZlZb{i4xMOj3NV*?FsSN=@A?fw}~G# zzR610Y~;6Hd8{W>MQK&=X8%Cgw98y)I_BB#w*|6b{|vv!Pw?bf8-P}fk@$g3@Jg#K z*CYhWluAlH)GNkClI<(Q)4LlPbL-1wpV}9v9+58sHhrHbJ30Olg#z4MfMq+D_SXiL zd!1B-!zE68L$)1iB=fMwIjT+4kkXdh;h+>0IUYNa{J~~5rqTnIB6cK#VYA-Z?JYkf z7e6U?I}43`ak+BVjuWLgy2+o|ej@@Io_c9B;hCSFT~lv8Zf;d+DoDF_b7Fks)AUGb zKhShhjHv4F2=EzL1fXT25xX?5H;ZhbYw?C|nHT`Vn-FNU2&?r2(P6THdiJ6R%GAk-I_EO)aG^&kpY}?=i2d_X zhX@9U<#^KI(+-7h$LZMaHu#KVT^EHS*A3CGt;!Wc$hOyVoliRr@)aRoKE4zU4?X^) zmSmuPDH6vN_^dlgsFd6%^X((Hhw>|`iI2KfG{(lbD$Mvb09@wn0K=ny{J~eoH!pgO zFdCA_0rcN>1IE8|HF?%4+*4s-5Y70zTE(B)ZX(68+bJbKe9MTX-C(*Frj)F$tmwFD z!dv^HLKagu_Ed!aAGZVmtPBz0M?dK08?@-*k=Ks5eA&x2jQn&%ilyDM>)DM6laHTB z{`o223HeutX-?hMS2YwJor?4X9v<8-(TlRC@_0dzWpdR~;vdiR8@^x=kUK|Ml^sM9 zQ{m;yiqD(f{>8!t41cEiT9KN5(4ms51n6+RgWpbvC_cWrXgrzQuT+C4u5evb%u2Fz zTl~SqAWHEo`y7wnh)~VCJCc7$?*i6Bp`eu&CnKXTn9u@sIVX>a=?Mt3BCbsS_bqw> zJpG0b29T$+*PrnI;qNc*-0KtmF266u2w+10`=kFe?;q#j z|I0@5^}CSV9qu)z`+t$#m6QH>R$#0Ct>A?1ikwz&4COC*87rc4^R zzR=H4s-y}kL5#-#sJ2~%pT1TXn!DUD+svyUs_9Q^THM$9Z{f*LHj=($zR;dFhJg#ulp&A z?HB)dJx?64g_SDUk@YX1`1e0Q>H-g>MWRjd-}&F~PNgCMu*D|3;?^&!sCZX2Nq`5k z`l68W@74ZNH2rvZVGD8dcEmq}lK;Kjzb07!$G%)MunNw!*lp`wT31z*3cuv3P&1by z{Ka4y)C)B*5ULy>8YdWfk4w8fMwIkdXE=t+7btuyCkwV-^I46iiOSvVpAoPvckMYZ znAz#VN+y0e^09fKoJ^a%f78oFz*bKvNc_9X#j(t0;G2&&>Q>D3Zm->^?i3Wn!t~ky zS=F1rG9-rwMOGY-SM=_Q9_^ie=E;GfE{kFd9b2(RssaH9ySgS(q3ff9_GFX8TxD`5 zG1P89wws(#29sM4kH}C*pBa*g^aO)H<5IC5`X{To;KUj42JAJv_MA_Y1G&o#{#CquA@QXOtRdh>;21~v-@cI@ zKF%F)t;B5)fgJ*uo8&&D_vQ!0EF6k)nJ;vgS6yy`Ux8fY@J#)L#$YZv-5QA4dRFqE}qHax^hvw6~V6Fn-)nR z!}ITtadW+psYURM6v23Vh|o(w+tzt>xLZ9GZH^HB)>D1>ML0SH;)8Yg8iF*P=&RwO zsoPu68LaJmn!Fpo7|^oLV`3*}&bhZ)GT4Fv(yPX*u;yu`J-WC$3*4>yxWaz2%Xt!M>v#WySfXkpHq&h`Gp(Ck2(jI& z+N%$5842yC?wm|as06p?j5DK8cHM^dMvgMQ9K6$Fwkx^gKZL<&J}E!Ih1Uy?Or5P) zfuWmq-9eBevd-k>G+f6NrlTs+WBgTiwi0gZ-ZRvQUwzEyh-M)n&hk_rQpqodLPkbJ z5{Y56Fw#MX&FnMF!~;p>tAo=3$wXG!^E*56rDBhlMat2ZY^nZ?LVEW-ZZ`oxe`V*h zmMr(0#7l~OXGvG!QRuy{Bi}6WZ^C++=FZ)CMCroVUd>$nvTvtRE{C64XU@KOp6v}? z^ASg*k)O;22vE^pl0mo3nS;WP%~IU+M0<4yycB-a z9_J}%Vm*6i7Q~qv`Hjv+SZa0RkEVqTB!<;GR_}h={(N)$G~(bI8?Csh!DinepfeS* zILKjy*!7WA78HKGm+f|ja!7O>GaJ6W96_u7aMHDRk#!bFH!N=Nv!j=DZJ%tTe8&-z zR-_{ktj%DC7Ox+DbbK1X4%x6D%(Py+YY6wt?&7eeP9`g?>Z(m$C@_}*IF_8kYfnif z-OG#Jp{=A7iyNb1!E|?Q1@hm}?{fhFhx<9t$lszUU)?7cJD1)I)b325Dn6w|3y}65 zHU3;uTH)q;4*Tv;UjvPETD@7~Ht7=NLz?;B?nu(LS#GHR3ry9OnYBm53m#IuEr#X! zdhWup>2w4Zo=CCi7UJ>QK;<>Bgt2n77_tO%i9OHg$5i=5tPMTL&OF<-5a=T&S9z*t zar7qGup6$hJeHbV#qUKv!7veq&MRQzK;N0xxnp=|OpxNryekVSxn^UGq~x{7W=u>E z8Lu%lopz@bY)JtHO zkGjxlVV$D9Q|a&@G+3ml@gQxu2k#kpY<`A)`06g~_GPe)opXoj+?L!MNrx)Ie-u{p zFa+mX_yakI=u;Z&(7p6q@w;OwCxW}n%j?B=^xDBsv5jXfCoJSLByC+7XvLb%Q zR#-*7x<{Oe8Hn)ffw9^~no9%pdh(vv2rH|d!)MJ&osboL*P0g&??$xfGBWZM0VTX6 zKJ(=?*?Dkd-^yoFr8E$V`~JdwC-F{|<|=|+dN`P9k3Z|u(YniTy`CX*w~Mzlh>PX>d*Uk<%Jj2@ zB7&Hmmb7NQV*51xj^7ozcgR~^#U#jA^8}IAk1g*EMSHn~c&4}MGTX7qPr02-!0wn& zmnW=PLs|`cA^xivhl=83yE{6r8)c?qPn&-N>O90HbCq7!;Ub1DFBwAb3Qup$Y!kxs z-^Q8bJF;8gv9I~3u8oeDLudLl56=&59y`yth~DOsA00fGLAq*2^4)_sB1g<@m!71& zSq#et*LuJmn(hgAJEM;>Cb=x#7TWcw1ABM?w!y8JT0BN~>orFBCahCWzv}M|Q4tzF ziIXb}a!LnGQpN6*u<0SOkYN#5mD*SV_~_+C+oCq=4R?@PFM0Y>vQNn0brn7?-#peK zmfLL}utm8u&Afwm6$=0)bXX^)=DL`IN9JwVtI)^t+zvXB*XB}$6TNVx2B@I7kSk4nx&)REz+ zQtFO{z|;%v2Lac5E2^v4hKiK-2qjR?IJ+^ml>v_gjAp83ITQax9 zzh(2=y9=*~QEPLcMX3Pta8v5#niEIm%oV=&`|>h;aCF}Yf&#R!N<=i|w z5|C&@cun+19Lkz%k=(qKKh-%oF*~hTEc*F(kruDDuf;jfqA^dxRZiUox-378QXEk6 zD;Iqn7&{-WVE%NR9szhku>u9fX0Jsf4rk5H;+w67W%%k*Zw}MAC%}>~@0oRb!qRCg zZ;G9f6|oZ|{J2%FifO4DMr+m>srE}OB#txe-7Z89jYok04| zK9(gFt{Z?WAYa})Yd&!+ur4l#C2ViYtJH)y-XFbMxyPGh{jDKB{SEUAMTx;v21#oQ zcY_Eg9IgA3E{8a@)4@$ImEdnBgpV#1$D>!}9B3^uy!})Me0i5PL7K(Hc;d{*V$K0>?J3jwd$VttT(u)tKAEqYrz57x=`2b8ZC2$(VMj@nx1u-;F%mCG3R z-*Nf+^SnMe{c+D!X|_&cm;5=pF>ju%V?t$HakW{#G0O~`8Ntlp*BiriyDkNXR1`yP zG~iAkB}Lk8HaIBl3;1Jfeu{NRnX2|%0L9Qn8_M9GiQ!6TKxUAKN=yt;p~|A3u3NcY zbD}C{16>jDl>UdnxWL~vnuetCabkFD4V^B%Ni+K9ImY*sQx9ELb^zTRz#uK6bv-pd z-_n};BpWw=8g%h74XE+5&wV<2{gHAoor~i%27oiX98erJ)w&%WvSD??FKcSXC&->T z$@HCy*fqSwV=p&(y>`+GU@-F#p+PR4DqV}E*sB2giw% z59DzkwUt?F%rMLPHRF3deC|N0QhT_W!e|eS^3iPIL|(wYQi4(r;d>2B9E}(nSL`TY z8+O&)@w-^V@@ByNSdKxpZx)y4RL}JY zd;Sssh|4l|nxA7NS6<(?pUTWfZ8b$;wWiRj6OZ`s89ZA#7u>dYYC?$e z^$=HqdrQn* z`viLub$umwtwcvwDoQIJS$Fz_#5=3N+ItGaS~Yr_OT@gL1{-ZrNW~!myfrTL z=z8LjPgiY#U2=us8TkyKwb4Xq(gTK;X`cxX*v=xSl1dsUv7>D{~ zTTx1;V`W&=Cw24a?x{RTf=z&SaSxuV)K*BcA8S+br~t~Lbv^5ri&gB`Kt?}3!`PPf z7)U)&78s_}T{t>E;_OGi_9H5a*EOt0e%bPVq2$U0e{M$S>ZRl=GU2_MflwZl4Yml2Rgj7rv=H?)#jU_EzK;Q#EiTIg2mBG-dR}UpQ{@&Um#mx+8K7CZ{l$IKxTyea%5zynJ zoo`G6iFL$A6Ho$xxXNoEKu6lliDw!oo&vd%=T&K?$|Fjq%|(39g||L@{XrrpKUjY>hH4OX z4$wlWG@K{$UL@tI6{-m5?&75<0RR13w_q3hH3#iyK{tDTJ6yK}#n!>HX#7nGv%;H_ z&B&;Q?kkOa!xTi~?vqX@iFy=6Ctf=YR?nH_1fKEU$V+6szswAQGhd|&PiblZL)X58 zbH9<^C{zLB?7I;kSReM1?>=PaEaB7l9Lj+N985l~Qv`N%cNkSXj=Twxz+bMESd4rF zXvkGwg?t$NInwvgRsW3gk=eb!RU3h@2oTzeF%<=>b6Ncv?Y^#L zoA&2r0CHW`{qg>C+i|wNeXG?-e+Iv~HE%?BS@%7NODnh?S!ol!nxr^n)KzY!Ju6VI z4I*|piu@kvG8Lk!pxXSAS(CfA?SB8Kq||Vj|MO}};-<@tnJE8ppHEJ`D#a#q8|^Tx0{9hwXwB=^&e^AHwYF87tom^@-N{4z z+Y@u|%c25FfPGk&H=eD!#V1)xW47HHTa**kw9&8X#nJ20FT4~G4H)DDnn;J`L28wJo!;&_sjm1r)!D&&i&7jEVJL*0yL&J{I?Bjlk=-y z(z0k#kYrV-2NT)UsqbHVo>=Cy_-_4^qG2$^(iuBdR)!zdqe>xdFrjPc=p67Nvnl+) z(hnB^xztrF20R)I1{8U#N65OWQp1neB=wWG{Ctl7sO>TS!0N-M;lV=EdSQ}iQ;f|)ZRM0(_rQ$TmUEtZj4#X_RUadkj zhJt5EBjT82>y~=#A?>P^|e2eV><~j?8eXtfWNtR+!b?y=SwcYOW zehzM=s{m+sl9JVXEyV|lxwYE?wSzsvO5`P*zr!097lLqa?{gCY zMV%~Wb!OOQPWKaFH{52km`wrQ1V4kWRzozSoD3zuDQ|AlOV;ldEDzajW*WraiZEMZ z3XXJ$w!p}*AUlJeXCx5_O9DiJVNh8R(6=QgVizyi z(?BF?ElOmgB~{zLycuy%q^O~1vb1-PLkGmONdfAEy^X%_o9+_P{5IVNAWzw8?>`iq z{=}Z7Hh*_V`hYu9*Ny`EG+R{>lY;TqNee`6R1uU$GUYv=deWo)Juy%#gSh81+iiuw^+(WejCfmb|Glf<$`iH>GZ`{06J(@V~sIeTdF9Vvp>jyaM~Vo@61 zf0yH`(<;KlrivwXW9i4=aOHeVi@vJ5z=|^ttKK;Hc3kW4`KSV1V63TfF4)}THl>#1 z2~cL23q0MN50HEaZ`duGjD^?e2g@#B25BJf&BqDvzI{ji-Q|6SCg$g)yjx~xe0=J2 zNv>;vBmR6V+~>A+`@%{~k@ZWH?X%I|QY5<{pc~5qW~@22a(lWm+(l%h+Q$CQ1R%~% zw)bxU!k_`0N?FBc|J8eg!^f{skaCB$sX*FU)}OW99vvqE6?$WUxW{`%(LwU`yI4WP zu^>)>$vRZ$@^(x`uWgq6V9Pu8-e^7R;UF*7C~o%xfJrrt+XGe-_#KUoShZ^HHX%ff>8c9?yisCX^B^M6+$pY4oaCJ3{AKnOocS1bRzqKe% zuo6vCJRT)Adl^ZjAPuvPe-&+!pn~p#zswb()ktFISdrk4Weh zSymbx(Q`h+d6VR8%tLba0z_(^7r4HCO?N6I7zhcBmprV&IDEYZw<;e3rCXf$(Q22f zlsj2K_R*g@>f_VG5H^QZ8PGeLb|vJ~ES)u8cLD&$$iHHjE;sTb0pOG4?P6V`31Z<~ zz#%8m{Mi-JM!WHHZ^uQ-=K`i9_8R9O&O4KGif)m{p~A|}jK$ca0yC7VR>DzF!r#(Z z>a+^X>>*DMPz^qTd`0m#YAmCDUe+I`2s)*u`NOzPx!-Uk*>VMKoJI1nK7@W98qbk# z5X*_>zPDmC9voL8X1q=hnJ*s)GVK!xJEya5yf%q=HFnhLqvNO-ufa1L7=M}UNs9#4 zd|fq6qM`QIZS)A(Iepfmkwjr5O75=srONAZZ;&|$Ufcwo_vR;@c* zBBu9HTS4P0%grLc69)eWRH+EQ0E{_Qw=M%$u+$3zpt{2il9|nbRMM7F3vXyOFK<0Y znaXpm2Gu@P`O7#lutLxUIz=h*`%l$8uC%pALEd&z6-#(KG5NgsdnWy37HEZ8o7I|` z;=X7Uc#7{>K7-o|rwFIFqisr?DDs+jAp7h4{0+Y%{^EOa>up^Ki%Llo2;BQonCTNL zzEW*4!q>W0epzPJWbM<iHQl;?@evOEi-J$c@uJRNvpv63L(57p&sa~- zNcG->&S(6?*$VIFuK&jjzz2!||ES3s9Gm?1SBSk9AcvX1{xXWZtLz zCI7X~$Mp-uo>o!)mkU=!0pGfaOWTh4ug~)5I-0ZqVqah?ukhy&{%WO<+Q1!N5ZltB z{C9!fKYrJ@;6eu##wCCCR|^ka%*9ufy{uvX+d=x*nNouR4^$Aw!v4#Jb>#tDXy4`( zCj48D_ph&309>f*!==nDzg(CT7#1JyFj>m^E2R17CYBn$m~|J6%Od#I!d5JREp)-i zDav2I12N!%v|6KL{=Kl2{1v5Sp=r;Hxths`rrq=?Nb+)byukm%-dlLJ`E_Za#T|lE zAOtOKai_Sum*NyF?iQTjQd*=GcZcHc!Ao&1?(Xh-)8BW$nYlBaKj5x4YrQM$4M{lf z*+=%XpR)y6KR3wO_-W#`unFX*kJEB|h7(rLh4ywm``<#I^!}p@lG#l18+WI?2A3hv zwtJIBCXw&XD66#=n>^%Y9m}Cl+%RuDz3MBS;uKh!fjFM0w#SviC}EEBmMe{*)o6rk z?xEzzX0mKpM$YWpyA(FFaH+3@xS>ba_7H)mxNBwLSve!vuIUB*51J2}Zx5QzvCTFK z=LsFaLs6c+i#SPt{Op-@7)`Mk+(xV#v+Uc*tjk)q%s6`MO!c(7%Tk@jV{ngC?3<^( zPsZrSvS@G?mrLr8beMI%>Ok4om^iF1%vIB)YHeFQFZVupJkcwpemZKpjwxhn47A0s ztGDf%Bk9i)yxxb^FkM2Sh≪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