= ({ onClose }) => {
})}
fullWidth
>
-
+
+
+ {(!inputs.additionalYamlConfig.value || inputs.additionalYamlConfig.value === '') && (
+
+ {`# YAML settings here will be added to the Elasticsearch output section of each policy`}
+
+ )}
+
diff --git a/x-pack/plugins/lens/common/expressions/datatable/sorting.test.tsx b/x-pack/plugins/lens/common/expressions/datatable/sorting.test.tsx
index f5d457db652341..b5ff0a7e6915b3 100644
--- a/x-pack/plugins/lens/common/expressions/datatable/sorting.test.tsx
+++ b/x-pack/plugins/lens/common/expressions/datatable/sorting.test.tsx
@@ -6,7 +6,7 @@
*/
import { getSortingCriteria } from './sorting';
-import { FieldFormat } from 'src/plugins/data/public';
+import type { FieldFormat } from 'src/plugins/field_formats/common';
import { DatatableColumnType } from 'src/plugins/expressions';
function getMockFormatter() {
diff --git a/x-pack/plugins/lens/common/expressions/datatable/sorting.tsx b/x-pack/plugins/lens/common/expressions/datatable/sorting.tsx
index 13ca811b0b0823..30060c10ea37e5 100644
--- a/x-pack/plugins/lens/common/expressions/datatable/sorting.tsx
+++ b/x-pack/plugins/lens/common/expressions/datatable/sorting.tsx
@@ -7,7 +7,7 @@
import ipaddr from 'ipaddr.js';
import type { IPv4, IPv6 } from 'ipaddr.js';
-import type { FieldFormat } from '../../../../../../src/plugins/data/common';
+import type { FieldFormat } from '../../../../../../src/plugins/field_formats/common';
function isIPv6Address(ip: IPv4 | IPv6): ip is IPv6 {
return ip.kind() === 'ipv6';
diff --git a/x-pack/plugins/lens/common/expressions/datatable/summary.test.ts b/x-pack/plugins/lens/common/expressions/datatable/summary.test.ts
index 9f8f56cc927683..61e74e0ef3bc7c 100644
--- a/x-pack/plugins/lens/common/expressions/datatable/summary.test.ts
+++ b/x-pack/plugins/lens/common/expressions/datatable/summary.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { IFieldFormat } from 'src/plugins/data/common';
+import type { IFieldFormat } from 'src/plugins/field_formats/common';
import { Datatable } from 'src/plugins/expressions';
import { computeSummaryRowForColumn, getFinalSummaryConfiguration } from './summary';
diff --git a/x-pack/plugins/lens/common/expressions/datatable/summary.ts b/x-pack/plugins/lens/common/expressions/datatable/summary.ts
index aceade2a3a5139..76c607ca4c4ee9 100644
--- a/x-pack/plugins/lens/common/expressions/datatable/summary.ts
+++ b/x-pack/plugins/lens/common/expressions/datatable/summary.ts
@@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
-import type { FieldFormat } from '../../../../../../src/plugins/data/common';
+import type { FieldFormat } from '../../../../../../src/plugins/field_formats/common';
import type { Datatable } from '../../../../../../src/plugins/expressions/common';
import { ColumnConfigArg } from './datatable_column';
import { getOriginalId } from './transpose_helpers';
diff --git a/x-pack/plugins/lens/common/expressions/datatable/transpose_helpers.test.ts b/x-pack/plugins/lens/common/expressions/datatable/transpose_helpers.test.ts
index 7ac6b3d987c842..6adb8b59474db8 100644
--- a/x-pack/plugins/lens/common/expressions/datatable/transpose_helpers.test.ts
+++ b/x-pack/plugins/lens/common/expressions/datatable/transpose_helpers.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import type { FieldFormat } from 'src/plugins/data/public';
+import type { FieldFormat } from 'src/plugins/field_formats/common';
import type { Datatable } from 'src/plugins/expressions';
import { DatatableArgs } from './datatable';
diff --git a/x-pack/plugins/lens/common/expressions/datatable/transpose_helpers.ts b/x-pack/plugins/lens/common/expressions/datatable/transpose_helpers.ts
index 06798413c8f40e..e2d928fda24ed1 100644
--- a/x-pack/plugins/lens/common/expressions/datatable/transpose_helpers.ts
+++ b/x-pack/plugins/lens/common/expressions/datatable/transpose_helpers.ts
@@ -10,7 +10,7 @@ import type {
DatatableColumn,
DatatableRow,
} from '../../../../../../src/plugins/expressions';
-import type { FieldFormat } from '../../../../../../src/plugins/data/common';
+import type { FieldFormat } from '../../../../../../src/plugins/field_formats/common';
import type { DatatableArgs } from './datatable';
import type { ColumnConfig, ColumnConfigArg } from './datatable_column';
diff --git a/x-pack/plugins/lens/common/suffix_formatter/index.ts b/x-pack/plugins/lens/common/suffix_formatter/index.ts
index 97fa8c067331e8..00ae005c38b148 100644
--- a/x-pack/plugins/lens/common/suffix_formatter/index.ts
+++ b/x-pack/plugins/lens/common/suffix_formatter/index.ts
@@ -6,11 +6,11 @@
*/
import { i18n } from '@kbn/i18n';
+import { KBN_FIELD_TYPES } from '@kbn/field-types';
import {
FieldFormat,
FieldFormatInstanceType,
- KBN_FIELD_TYPES,
-} from '../../../../../src/plugins/data/common';
+} from '../../../../../src/plugins/field_formats/common';
import type { FormatFactory } from '../types';
import type { TimeScaleUnit } from '../expressions/time_scale';
diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts
index a60061a3aa054b..06fa31b87ce640 100644
--- a/x-pack/plugins/lens/common/types.ts
+++ b/x-pack/plugins/lens/common/types.ts
@@ -5,7 +5,8 @@
* 2.0.
*/
-import type { FilterMeta, Filter, IFieldFormat } from '../../../../src/plugins/data/common';
+import type { FilterMeta, Filter } from '../../../../src/plugins/data/common';
+import type { IFieldFormat } from '../../../../src/plugins/field_formats/common';
import type { Datatable, SerializedFieldFormat } from '../../../../src/plugins/expressions/common';
export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json
index fd394aea90d668..6a3e0f40c48f4a 100644
--- a/x-pack/plugins/lens/kibana.json
+++ b/x-pack/plugins/lens/kibana.json
@@ -36,7 +36,8 @@
"savedObjects",
"kibanaUtils",
"kibanaReact",
- "embeddable"
+ "embeddable",
+ "fieldFormats"
],
"owner": {
"name": "Kibana App",
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx
index fb9cb992fcf47d..96413444d60c46 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.test.tsx
@@ -9,7 +9,7 @@ import { mountWithIntl } from '@kbn/test/jest';
import React from 'react';
import { DataContext } from './table_basic';
import { createGridCell } from './cell_value';
-import { FieldFormat } from 'src/plugins/data/public';
+import type { FieldFormat } from 'src/plugins/field_formats/common';
import { Datatable } from 'src/plugins/expressions/public';
import { IUiSettingsClient } from 'kibana/public';
import { act } from 'react-dom/test-utils';
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
index bb678a361e1741..a0d137b90e84c5 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
@@ -10,7 +10,8 @@ import { ReactWrapper, shallow } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { mountWithIntl } from '@kbn/test/jest';
import { EuiDataGrid } from '@elastic/eui';
-import { IAggType, IFieldFormat } from 'src/plugins/data/public';
+import { IAggType } from 'src/plugins/data/public';
+import { IFieldFormat } from 'src/plugins/field_formats/common';
import { VisualizationContainer } from '../../visualization_container';
import { EmptyPlaceholder } from '../../shared_components';
import { LensIconChartDatatable } from '../../assets/chart_datatable';
diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx
index 4b4d2275d0dec0..163971c4ba9fb3 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx
@@ -8,7 +8,7 @@
import { DatatableProps } from '../../common/expressions';
import type { LensMultiTable } from '../../common';
import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks';
-import { IFieldFormat } from '../../../../../src/plugins/data/public';
+import type { IFieldFormat } from '../../../../../src/plugins/field_formats/common';
import { getDatatable } from './expression';
function sampleArgs() {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx
index 43f5527e42d4bf..84bcb48f95234d 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx
@@ -23,7 +23,7 @@ import {
keys,
} from '@elastic/eui';
import { useDebounceWithOptions } from '../../../../shared_components';
-import { IFieldFormat } from '../../../../../../../../src/plugins/data/common';
+import { IFieldFormat } from '../../../../../../../../src/plugins/field_formats/common';
import { RangeTypeLens, isValidRange } from './ranges';
import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants';
import {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx
index 3389c723b4daf6..69460c649c3e1c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx
@@ -18,7 +18,7 @@ import {
EuiRange,
EuiToolTip,
} from '@elastic/eui';
-import type { IFieldFormat } from 'src/plugins/data/public';
+import type { IFieldFormat } from 'src/plugins/field_formats/common';
import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public';
import { RangeColumnParams, UpdateParamsFnType, MODES_TYPES } from './ranges';
import { AdvancedRangeEditor } from './advanced_editor';
@@ -59,7 +59,7 @@ const GranularityHelpPopover = () => {
{UI_SETTINGS.HISTOGRAM_MAX_BARS},
diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx
index 21c68a9fe1d826..10575f37dba6e0 100644
--- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx
+++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx
@@ -10,7 +10,7 @@ import { MetricConfig } from '../../common/expressions';
import React from 'react';
import { shallow } from 'enzyme';
import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks';
-import { IFieldFormat } from '../../../../../src/plugins/data/public';
+import type { IFieldFormat } from '../../../../../src/plugins/field_formats/common';
import type { LensMultiTable } from '../../common';
function sampleArgs() {
diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
index c17eeec91dc2b6..cf5ca5c9c0c002 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap
@@ -17,6 +17,7 @@ exports[`xy_expression XYChart component it renders area 1`] = `
legendPosition="top"
onBrushEnd={[Function]}
onElementClick={[Function]}
+ onPointerUpdate={[Function]}
rotation={0}
showLegend={false}
showLegendExtra={false}
@@ -234,6 +235,7 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
legendPosition="top"
onBrushEnd={[Function]}
onElementClick={[Function]}
+ onPointerUpdate={[Function]}
rotation={0}
showLegend={false}
showLegendExtra={false}
@@ -465,6 +467,7 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
legendPosition="top"
onBrushEnd={[Function]}
onElementClick={[Function]}
+ onPointerUpdate={[Function]}
rotation={90}
showLegend={false}
showLegendExtra={false}
@@ -696,6 +699,7 @@ exports[`xy_expression XYChart component it renders line 1`] = `
legendPosition="top"
onBrushEnd={[Function]}
onElementClick={[Function]}
+ onPointerUpdate={[Function]}
rotation={0}
showLegend={false}
showLegendExtra={false}
@@ -913,6 +917,7 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
legendPosition="top"
onBrushEnd={[Function]}
onElementClick={[Function]}
+ onPointerUpdate={[Function]}
rotation={0}
showLegend={false}
showLegendExtra={false}
@@ -1138,6 +1143,7 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
legendPosition="top"
onBrushEnd={[Function]}
onElementClick={[Function]}
+ onPointerUpdate={[Function]}
rotation={0}
showLegend={false}
showLegendExtra={false}
@@ -1377,6 +1383,7 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
legendPosition="top"
onBrushEnd={[Function]}
onElementClick={[Function]}
+ onPointerUpdate={[Function]}
rotation={90}
showLegend={false}
showLegendExtra={false}
diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts
index 83d86eb410b195..95c9140624e63e 100644
--- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts
@@ -8,7 +8,7 @@
import { FormatFactory } from '../../common';
import { AxisExtentConfig, XYLayerConfig } from '../../common/expressions';
import { Datatable, SerializedFieldFormat } from '../../../../../src/plugins/expressions/public';
-import { IFieldFormat } from '../../../../../src/plugins/data/public';
+import type { IFieldFormat } from '../../../../../src/plugins/field_formats/common';
interface FormattedMetric {
layer: string;
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
index 56a59bd7713b2e..94ed5037000422 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx
@@ -49,7 +49,12 @@ import { XyEndzones } from './x_domain';
const onClickValue = jest.fn();
const onSelectRange = jest.fn();
-const chartsThemeService = chartPluginMock.createSetupContract().theme;
+const chartSetupContract = chartPluginMock.createSetupContract();
+const chartStartContract = chartPluginMock.createStartContract();
+
+const chartsThemeService = chartSetupContract.theme;
+const chartsActiveCursorService = chartStartContract.activeCursor;
+
const paletteService = chartPluginMock.createPaletteRegistry();
const mockPaletteOutput: PaletteOutput = {
@@ -473,6 +478,7 @@ describe('xy_expression', () => {
timeZone: 'UTC',
renderMode: 'display',
chartsThemeService,
+ chartsActiveCursorService,
paletteService,
minInterval: 50,
onClickValue,
diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
index b7f22ebf8968d7..23b251b76e9504 100644
--- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx
@@ -7,7 +7,7 @@
import './expression.scss';
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import {
Chart,
@@ -47,8 +47,10 @@ import { isHorizontalChart, getSeriesColor } from './state_helpers';
import { search } from '../../../../../src/plugins/data/public';
import {
ChartsPluginSetup,
+ ChartsPluginStart,
PaletteRegistry,
SeriesLayer,
+ useActiveCursor,
} from '../../../../../src/plugins/charts/public';
import { EmptyPlaceholder } from '../shared_components';
import { getFitOptions } from './fitting_functions';
@@ -85,6 +87,7 @@ export {
export type XYChartRenderProps = XYChartProps & {
chartsThemeService: ChartsPluginSetup['theme'];
+ chartsActiveCursorService: ChartsPluginStart['activeCursor'];
paletteService: PaletteRegistry;
formatFactory: FormatFactory;
timeZone: string;
@@ -121,7 +124,8 @@ export function calculateMinInterval({ args: { layers }, data }: XYChartProps) {
export const getXyChartRenderer = (dependencies: {
formatFactory: Promise;
- chartsThemeService: ChartsPluginSetup['theme'];
+ chartsThemeService: ChartsPluginStart['theme'];
+ chartsActiveCursorService: ChartsPluginStart['activeCursor'];
paletteService: PaletteRegistry;
timeZone: string;
}): ExpressionRenderDefinition => ({
@@ -150,6 +154,7 @@ export const getXyChartRenderer = (dependencies: {
(null);
const chartTheme = chartsThemeService.useChartsTheme();
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
const darkMode = chartsThemeService.useDarkMode();
const filteredLayers = getFilteredLayers(layers, data);
+ const handleCursorUpdate = useActiveCursor(chartsActiveCursorService, chartRef, {
+ datatables: Object.values(data.tables),
+ });
+
if (filteredLayers.length === 0) {
const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar';
return ;
@@ -486,8 +497,9 @@ export function XYChart({
} as LegendPositionConfig;
return (
-
+
,
- { expressions, formatFactory, editorFrame, charts }: XyVisualizationPluginSetupPlugins
+ { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins
) {
editorFrame.registerVisualization(async () => {
const {
@@ -41,7 +41,7 @@ export class XyVisualization {
getXyChartRenderer,
getXyVisualization,
} = await import('../async_services');
- const [, { data }] = await core.getStartServices();
+ const [, { data, charts }] = await core.getStartServices();
const palettes = await charts.palettes.getPalettes();
expressions.registerFunction(() => legendConfig);
expressions.registerFunction(() => yAxisConfig);
@@ -57,6 +57,7 @@ export class XyVisualization {
getXyChartRenderer({
formatFactory,
chartsThemeService: charts.theme,
+ chartsActiveCursorService: charts.activeCursor,
paletteService: palettes,
timeZone: getTimeZone(core.uiSettings),
})
diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json
index 134f0b4185b840..6c4d3631a12f37 100644
--- a/x-pack/plugins/lens/tsconfig.json
+++ b/x-pack/plugins/lens/tsconfig.json
@@ -37,5 +37,6 @@
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
{ "path": "../../../src/plugins/embeddable/tsconfig.json"},
{ "path": "../../../src/plugins/presentation_util/tsconfig.json"},
+ { "path": "../../../src/plugins/field_formats/tsconfig.json"}
]
- }
\ No newline at end of file
+ }
diff --git a/x-pack/plugins/ml/common/constants/alerts.ts b/x-pack/plugins/ml/common/constants/alerts.ts
index a801872c5444dc..2192b2b504b59c 100644
--- a/x-pack/plugins/ml/common/constants/alerts.ts
+++ b/x-pack/plugins/ml/common/constants/alerts.ts
@@ -21,26 +21,57 @@ export const TOP_N_BUCKETS_COUNT = 1;
export const ALL_JOBS_SELECTION = '*';
-export const HEALTH_CHECK_NAMES: Record = {
- datafeed: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedCheckName', {
- defaultMessage: 'Datafeed is not started',
- }),
- mml: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlCheckName', {
- defaultMessage: 'Model memory limit reached',
- }),
- errorMessages: i18n.translate(
- 'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesCheckName',
- {
+export const HEALTH_CHECK_NAMES: Record = {
+ datafeed: {
+ name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedCheckName', {
+ defaultMessage: 'Datafeed is not started',
+ }),
+ description: i18n.translate(
+ 'xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedCheckDescription',
+ {
+ defaultMessage: 'Get alerted if the corresponding datafeed of the job is not started',
+ }
+ ),
+ },
+ mml: {
+ name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlCheckName', {
+ defaultMessage: 'Model memory limit reached',
+ }),
+ description: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlCheckDescription', {
+ defaultMessage: 'Get alerted when job reaches soft or hard model memory limit.',
+ }),
+ },
+ delayedData: {
+ name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataCheckName', {
+ defaultMessage: 'Data delay has occurred',
+ }),
+ description: i18n.translate(
+ 'xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataCheckDescription',
+ {
+ defaultMessage: 'Get alerted if a job missed data due to data delay.',
+ }
+ ),
+ },
+ errorMessages: {
+ name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesCheckName', {
defaultMessage: 'There are errors in the job messages',
- }
- ),
- behindRealtime: i18n.translate(
- 'xpack.ml.alertTypes.jobsHealthAlertingRule.behindRealtimeCheckName',
- {
+ }),
+ description: i18n.translate(
+ 'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesCheckDescription',
+ {
+ defaultMessage: 'There are errors in the job messages',
+ }
+ ),
+ },
+ behindRealtime: {
+ name: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.behindRealtimeCheckName', {
defaultMessage: 'Job is running behind real-time',
- }
- ),
- delayedData: i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataCheckName', {
- defaultMessage: 'Data delay has occurred',
- }),
+ }),
+ description: i18n.translate(
+ 'xpack.ml.alertTypes.jobsHealthAlertingRule.behindRealtimeCheckDescription',
+ {
+ defaultMessage: 'Job is running behind real-time',
+ }
+ ),
+ },
};
diff --git a/x-pack/plugins/ml/common/types/alerts.ts b/x-pack/plugins/ml/common/types/alerts.ts
index 877bb2d2933655..408c424f327d55 100644
--- a/x-pack/plugins/ml/common/types/alerts.ts
+++ b/x-pack/plugins/ml/common/types/alerts.ts
@@ -109,7 +109,7 @@ export interface JobAlertingRuleStats {
alerting_rules?: MlAnomalyDetectionAlertRule[];
}
-interface CommonHealthCheckConfig {
+export interface CommonHealthCheckConfig {
enabled: boolean;
}
diff --git a/x-pack/plugins/ml/common/types/annotations.ts b/x-pack/plugins/ml/common/types/annotations.ts
index 0417e769e9ed56..6234444322a5bd 100644
--- a/x-pack/plugins/ml/common/types/annotations.ts
+++ b/x-pack/plugins/ml/common/types/annotations.ts
@@ -86,7 +86,12 @@ export interface Annotation {
annotation: string;
job_id: string;
type: ANNOTATION_TYPE.ANNOTATION | ANNOTATION_TYPE.COMMENT;
- event?: string;
+ event?:
+ | 'user'
+ | 'delayed_data'
+ | 'model_snapshot_stored'
+ | 'model_change'
+ | 'categorization_status_change';
detector_index?: number;
partition_field_name?: string;
partition_field_value?: string;
diff --git a/x-pack/plugins/ml/common/util/alerts.test.ts b/x-pack/plugins/ml/common/util/alerts.test.ts
index dda9fd449467b7..84205e6806133e 100644
--- a/x-pack/plugins/ml/common/util/alerts.test.ts
+++ b/x-pack/plugins/ml/common/util/alerts.test.ts
@@ -90,6 +90,11 @@ describe('getResultJobsHealthRuleConfig', () => {
mml: {
enabled: true,
},
+ delayedData: {
+ docsCount: 1,
+ enabled: true,
+ timeInterval: null,
+ },
});
});
test('returns config with overridden values based on provided configuration', () => {
@@ -97,6 +102,10 @@ describe('getResultJobsHealthRuleConfig', () => {
getResultJobsHealthRuleConfig({
mml: { enabled: false },
errorMessages: { enabled: true },
+ delayedData: {
+ enabled: true,
+ docsCount: 1,
+ },
})
).toEqual({
datafeed: {
@@ -105,6 +114,11 @@ describe('getResultJobsHealthRuleConfig', () => {
mml: {
enabled: false,
},
+ delayedData: {
+ docsCount: 1,
+ enabled: true,
+ timeInterval: null,
+ },
});
});
});
diff --git a/x-pack/plugins/ml/common/util/alerts.ts b/x-pack/plugins/ml/common/util/alerts.ts
index 86afd70ad74740..7328c2a4dcc711 100644
--- a/x-pack/plugins/ml/common/util/alerts.ts
+++ b/x-pack/plugins/ml/common/util/alerts.ts
@@ -54,7 +54,7 @@ export function getTopNBuckets(job: Job): number {
return Math.ceil(narrowBucketLength / bucketSpan.asSeconds());
}
-const implementedTests = ['datafeed', 'mml'] as JobsHealthTests[];
+const implementedTests = ['datafeed', 'mml', 'delayedData'] as JobsHealthTests[];
/**
* Returns tests configuration combined with default values.
@@ -70,6 +70,8 @@ export function getResultJobsHealthRuleConfig(config: JobsHealthRuleTestsConfig)
},
delayedData: {
enabled: config?.delayedData?.enabled ?? true,
+ docsCount: config?.delayedData?.docsCount ?? 1,
+ timeInterval: config?.delayedData?.timeInterval ?? null,
},
behindRealtime: {
enabled: config?.behindRealtime?.enabled ?? true,
diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json
index 7f3ad80968b7a8..1196247fe4629d 100644
--- a/x-pack/plugins/ml/kibana.json
+++ b/x-pack/plugins/ml/kibana.json
@@ -39,7 +39,8 @@
"savedObjects",
"home",
"maps",
- "usageCollection"
+ "usageCollection",
+ "fieldFormats"
],
"extraPublicDirs": [
"common"
diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts
index dc4b10102e4f13..f6446b454a8778 100644
--- a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts
+++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts
@@ -12,6 +12,7 @@ import { PluginSetupContract as AlertingSetup } from '../../../../alerting/publi
import { ML_ALERT_TYPES } from '../../../common/constants/alerts';
import { MlAnomalyDetectionJobsHealthRuleParams } from '../../../common/types/alerts';
import { getResultJobsHealthRuleConfig } from '../../../common/util/alerts';
+import { validateLookbackInterval } from '../validators';
export function registerJobsHealthAlertingRule(
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup,
@@ -32,6 +33,7 @@ export function registerJobsHealthAlertingRule(
errors: {
includeJobs: new Array(),
testsConfig: new Array(),
+ delayedData: new Array(),
} as Record,
};
@@ -53,6 +55,31 @@ export function registerJobsHealthAlertingRule(
);
}
+ if (
+ !!resultTestConfig.delayedData.timeInterval &&
+ validateLookbackInterval(resultTestConfig.delayedData.timeInterval)
+ ) {
+ validationResult.errors.delayedData.push(
+ i18n.translate(
+ 'xpack.ml.alertTypes.jobsHealthAlertingRule.testsConfig.delayedData.timeIntervalErrorMessage',
+ {
+ defaultMessage: 'Invalid time interval',
+ }
+ )
+ );
+ }
+
+ if (resultTestConfig.delayedData.docsCount === 0) {
+ validationResult.errors.delayedData.push(
+ i18n.translate(
+ 'xpack.ml.alertTypes.jobsHealthAlertingRule.testsConfig.delayedData.docsCountErrorMessage',
+ {
+ defaultMessage: 'Invalid number of documents',
+ }
+ )
+ );
+ }
+
return validationResult;
},
requiresAppContext: false,
@@ -68,6 +95,9 @@ export function registerJobsHealthAlertingRule(
\\{\\{#memory_status\\}\\}Memory status: \\{\\{memory_status\\}\\} \\{\\{/memory_status\\}\\}
\\{\\{#log_time\\}\\}Memory logging time: \\{\\{log_time\\}\\} \\{\\{/log_time\\}\\}
\\{\\{#failed_category_count\\}\\}Failed category count: \\{\\{failed_category_count\\}\\} \\{\\{/failed_category_count\\}\\}
+ \\{\\{#annotation\\}\\}Annotation: \\{\\{annotation\\}\\} \\{\\{/annotation\\}\\}
+ \\{\\{#missed_docs_count\\}\\}Number of missed documents: \\{\\{missed_docs_count\\}\\} \\{\\{/missed_docs_count\\}\\}
+ \\{\\{#end_timestamp\\}\\}Latest finalized bucket with missing docs: \\{\\{end_timestamp\\}\\} \\{\\{/end_timestamp\\}\\}
\\{\\{/context.results\\}\\}
`,
}
diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/tests_selection_control.tsx b/x-pack/plugins/ml/public/alerting/jobs_health_rule/tests_selection_control.tsx
index 07fbc63346cd44..b78c963670da24 100644
--- a/x-pack/plugins/ml/public/alerting/jobs_health_rule/tests_selection_control.tsx
+++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/tests_selection_control.tsx
@@ -5,12 +5,22 @@
* 2.0.
*/
-import React, { FC, Fragment, useCallback } from 'react';
-import { i18n } from '@kbn/i18n';
-import { EuiFormFieldset, EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui';
+import React, { FC, useCallback } from 'react';
+import {
+ EuiDescribedFormGroup,
+ EuiFieldNumber,
+ EuiForm,
+ EuiFormRow,
+ EuiIcon,
+ EuiSpacer,
+ EuiSwitch,
+ EuiToolTip,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
import { JobsHealthRuleTestsConfig, JobsHealthTests } from '../../../common/types/alerts';
import { getResultJobsHealthRuleConfig } from '../../../common/util/alerts';
import { HEALTH_CHECK_NAMES } from '../../../common/constants/alerts';
+import { TimeIntervalControl } from '../time_interval_control';
interface TestsSelectionControlProps {
config: JobsHealthRuleTestsConfig;
@@ -18,53 +28,127 @@ interface TestsSelectionControlProps {
errors?: string[];
}
-export const TestsSelectionControl: FC = ({
- config,
- onChange,
- errors,
-}) => {
- const uiConfig = getResultJobsHealthRuleConfig(config);
+export const TestsSelectionControl: FC = React.memo(
+ ({ config, onChange, errors }) => {
+ const uiConfig = getResultJobsHealthRuleConfig(config);
- const updateCallback = useCallback(
- (update: Partial>) => {
- onChange({
- ...(config ?? {}),
- ...update,
- });
- },
- [onChange, config]
- );
+ const updateCallback = useCallback(
+ (update: Partial>) => {
+ onChange({
+ ...(config ?? {}),
+ ...update,
+ });
+ },
+ [onChange, config]
+ );
- return (
-
- {Object.entries(uiConfig).map(([name, conf], i) => {
- return (
-
-
+ {(Object.entries(uiConfig) as Array<
+ [JobsHealthTests, typeof uiConfig[JobsHealthTests]]
+ >).map(([name, conf], i) => {
+ return (
+ {HEALTH_CHECK_NAMES[name]?.name}}
+ description={HEALTH_CHECK_NAMES[name]?.description}
>
-
-
-
-
- );
- })}
-
- );
-};
+
+
+ }
+ onChange={updateCallback.bind(null, {
+ [name]: {
+ ...uiConfig[name],
+ enabled: !uiConfig[name].enabled,
+ },
+ })}
+ checked={uiConfig[name].enabled}
+ />
+
+
+
+ {name === 'delayedData' ? (
+ <>
+
+
+
+ }
+ >
+
+
+ >
+ }
+ >
+ {
+ updateCallback({
+ [name]: {
+ ...uiConfig[name],
+ docsCount: Number(e.target.value),
+ },
+ });
+ }}
+ min={1}
+ />
+
+
+
+
+
+
+
+ }
+ >
+
+
+ >
+ }
+ value={uiConfig.delayedData.timeInterval}
+ onChange={(e) => {
+ updateCallback({
+ [name]: {
+ ...uiConfig[name],
+ timeInterval: e,
+ },
+ });
+ }}
+ />
+
+
+ >
+ ) : null}
+
+ );
+ })}
+
+ );
+ }
+);
diff --git a/x-pack/plugins/ml/public/alerting/time_interval_control.tsx b/x-pack/plugins/ml/public/alerting/time_interval_control.tsx
index 8030d340a37745..4ab73500958ae7 100644
--- a/x-pack/plugins/ml/public/alerting/time_interval_control.tsx
+++ b/x-pack/plugins/ml/public/alerting/time_interval_control.tsx
@@ -27,7 +27,7 @@ export const TimeIntervalControl: FC = ({
const validationErrors = useMemo(() => validators(value), [value]);
- const isInvalid = value !== undefined && !!validationErrors;
+ const isInvalid = !!value && !!validationErrors;
return (
{
const mlClient = ({
@@ -20,12 +23,15 @@ describe('JobsHealthService', () => {
jobs = [
({
job_id: 'test_job_01',
+ analysis_config: { bucket_span: '1h' },
} as unknown) as MlJob,
({
job_id: 'test_job_02',
+ analysis_config: { bucket_span: '15m' },
} as unknown) as MlJob,
({
job_id: 'test_job_03',
+ analysis_config: { bucket_span: '8m' },
} as unknown) as MlJob,
];
}
@@ -34,6 +40,7 @@ describe('JobsHealthService', () => {
jobs = [
({
job_id: jobIds[0],
+ analysis_config: { bucket_span: '1h' },
} as unknown) as MlJob,
];
}
@@ -84,13 +91,32 @@ describe('JobsHealthService', () => {
return Promise.resolve(
jobIds.map((j) => {
return {
+ job_id: j,
datafeed_id: j.replace('job', 'datafeed'),
+ query_delay: '3m',
};
})
);
}),
} as unknown) as jest.Mocked;
+ const annotationService = ({
+ getDelayedDataAnnotations: jest.fn().mockImplementation(({ jobIds }: { jobIds: string[] }) => {
+ return Promise.resolve(
+ jobIds.map((jobId) => {
+ return {
+ job_id: jobId,
+ annotation: `Datafeed has missed ${
+ jobId === 'test_job_01' ? 11 : 8
+ } documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay`,
+ modified_time: 1627660295141,
+ end_timestamp: 1627653300000,
+ };
+ })
+ );
+ }),
+ } as unknown) as jest.Mocked;
+
const logger = ({
warn: jest.fn(),
info: jest.fn(),
@@ -100,13 +126,19 @@ describe('JobsHealthService', () => {
const jobHealthService: JobsHealthService = jobsHealthServiceProvider(
mlClient,
datafeedsService,
+ annotationService,
logger
);
- beforeEach(() => {});
+ let dateNowSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_DATE_NOW);
+ });
afterEach(() => {
jest.clearAllMocks();
+ dateNowSpy.mockRestore();
});
test('returns empty results when no jobs provided', async () => {
@@ -131,7 +163,11 @@ describe('JobsHealthService', () => {
enabled: false,
},
behindRealtime: null,
- delayedData: null,
+ delayedData: {
+ enabled: false,
+ docsCount: null,
+ timeInterval: null,
+ },
errorMessages: null,
mml: {
enabled: false,
@@ -149,6 +185,54 @@ describe('JobsHealthService', () => {
expect(executionResult).toEqual([]);
});
+ test('takes into account delayed data params', async () => {
+ const executionResult = await jobHealthService.getTestsResults('testRule_04', {
+ testsConfig: {
+ delayedData: {
+ enabled: true,
+ docsCount: 10,
+ timeInterval: '4h',
+ },
+ behindRealtime: { enabled: false, timeInterval: null },
+ mml: { enabled: false },
+ datafeed: { enabled: false },
+ errorMessages: { enabled: false },
+ },
+ includeJobs: {
+ jobIds: [],
+ groupIds: ['test_group'],
+ },
+ excludeJobs: {
+ jobIds: ['test_job_03'],
+ groupIds: [],
+ },
+ });
+
+ expect(annotationService.getDelayedDataAnnotations).toHaveBeenCalledWith({
+ jobIds: ['test_job_01', 'test_job_02'],
+ // 1487076708000 - 4h
+ earliestMs: 1487062308000,
+ });
+
+ expect(executionResult).toEqual([
+ {
+ name: 'Data delay has occurred',
+ context: {
+ results: [
+ {
+ job_id: 'test_job_01',
+ annotation:
+ 'Datafeed has missed 11 documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay',
+ end_timestamp: 1627653300000,
+ missed_docs_count: 11,
+ },
+ ],
+ message: '1 job is suffering from delayed data.',
+ },
+ },
+ ]);
+ });
+
test('returns results based on provided selection', async () => {
const executionResult = await jobHealthService.getTestsResults('testRule_03', {
testsConfig: null,
@@ -169,11 +253,17 @@ describe('JobsHealthService', () => {
'test_job_01',
'test_job_02',
]);
+ expect(datafeedsService.getDatafeedByJobId).toHaveBeenCalledTimes(1);
expect(mlClient.getJobStats).toHaveBeenCalledWith({ job_id: 'test_job_01,test_job_02' });
expect(mlClient.getDatafeedStats).toHaveBeenCalledWith({
datafeed_id: 'test_datafeed_01,test_datafeed_02',
});
expect(mlClient.getJobStats).toHaveBeenCalledTimes(1);
+ expect(annotationService.getDelayedDataAnnotations).toHaveBeenCalledWith({
+ jobIds: ['test_job_01', 'test_job_02'],
+ earliestMs: 1487069268000,
+ });
+
expect(executionResult).toEqual([
{
name: 'Datafeed is not started',
@@ -203,6 +293,28 @@ describe('JobsHealthService', () => {
'1 job reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.',
},
},
+ {
+ name: 'Data delay has occurred',
+ context: {
+ results: [
+ {
+ job_id: 'test_job_01',
+ annotation:
+ 'Datafeed has missed 11 documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay',
+ end_timestamp: 1627653300000,
+ missed_docs_count: 11,
+ },
+ {
+ job_id: 'test_job_02',
+ annotation:
+ 'Datafeed has missed 8 documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay',
+ end_timestamp: 1627653300000,
+ missed_docs_count: 8,
+ },
+ ],
+ message: '2 jobs are suffering from delayed data.',
+ },
+ },
]);
});
});
diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts
index 5c34a84abd9719..52e17fed7a4143 100644
--- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts
+++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts
@@ -5,10 +5,11 @@
* 2.0.
*/
+import { memoize, keyBy } from 'lodash';
import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
import { i18n } from '@kbn/i18n';
import { Logger } from 'kibana/server';
-import { MlJobStats } from '@elastic/elasticsearch/api/types';
+import { MlJob } from '@elastic/elasticsearch/api/types';
import { MlClient } from '../ml_client';
import {
AnomalyDetectionJobsHealthRuleParams,
@@ -20,10 +21,18 @@ import { DatafeedStats } from '../../../common/types/anomaly_detection_jobs';
import { GetGuards } from '../../shared_services/shared_services';
import {
AnomalyDetectionJobsHealthAlertContext,
+ DelayedDataResponse,
MmlTestResponse,
NotStartedDatafeedResponse,
} from './register_jobs_monitoring_rule_type';
-import { getResultJobsHealthRuleConfig } from '../../../common/util/alerts';
+import {
+ getResultJobsHealthRuleConfig,
+ resolveLookbackInterval,
+} from '../../../common/util/alerts';
+import { AnnotationService } from '../../models/annotation_service/annotation';
+import { annotationServiceProvider } from '../../models/annotation_service';
+import { parseInterval } from '../../../common/util/parse_interval';
+import { isDefined } from '../../../common/types/guards';
interface TestResult {
name: string;
@@ -35,14 +44,18 @@ type TestsResults = TestResult[];
export function jobsHealthServiceProvider(
mlClient: MlClient,
datafeedsService: DatafeedsService,
+ annotationService: AnnotationService,
logger: Logger
) {
/**
- * Extracts result list of job ids based on included and excluded selection of jobs and groups.
+ * Extracts result list of jobs based on included and excluded selection of jobs and groups.
* @param includeJobs
* @param excludeJobs
*/
- const getResultJobIds = async (includeJobs: JobSelection, excludeJobs?: JobSelection | null) => {
+ const getResultJobs = async (
+ includeJobs: JobSelection,
+ excludeJobs?: JobSelection | null
+ ): Promise => {
const jobAndGroupIds = [...(includeJobs.jobIds ?? []), ...(includeJobs.groupIds ?? [])];
const includeAllJobs = jobAndGroupIds.some((id) => id === ALL_JOBS_SELECTION);
@@ -54,7 +67,7 @@ export function jobsHealthServiceProvider(
})
).body.jobs;
- let resultJobIds = jobsResponse.map((v) => v.job_id);
+ let resultJobs = jobsResponse;
if (excludeJobs && (!!excludeJobs.jobIds.length || !!excludeJobs?.groupIds.length)) {
const excludedJobAndGroupIds = [
@@ -69,33 +82,41 @@ export function jobsHealthServiceProvider(
const excludedJobsIds: Set = new Set(excludedJobsResponse.map((v) => v.job_id));
- resultJobIds = resultJobIds.filter((v) => !excludedJobsIds.has(v));
+ resultJobs = resultJobs.filter((v) => !excludedJobsIds.has(v.job_id));
}
- return resultJobIds;
+ return resultJobs;
};
- const getJobStats: (jobIds: string[]) => Promise = (() => {
- const cachedStats = new Map();
+ /**
+ * Resolves the timestamp for delayed data check.
+ *
+ * @param timeInterval - Custom time interval provided by the user.
+ * @param defaultLookbackInterval - Interval derived from the jobs and datefeeds configs.
+ */
+ const getDelayedDataLookbackTimestamp = (
+ timeInterval: string | null,
+ defaultLookbackInterval: string
+ ): number => {
+ const currentTimestamp = Date.now();
- return async (jobIds: string[]) => {
- if (jobIds.every((j) => cachedStats.has(j))) {
- logger.debug(`Return jobs stats from cache`);
- return Array.from(cachedStats.values());
- }
+ const defaultLookbackTimestamp =
+ currentTimestamp - parseInterval(defaultLookbackInterval)!.asMilliseconds();
- const {
- body: { jobs: jobsStats },
- } = await mlClient.getJobStats({ job_id: jobIds.join(',') });
+ const customIntervalOffsetTimestamp = timeInterval
+ ? currentTimestamp - parseInterval(timeInterval)!.asMilliseconds()
+ : null;
- // update cache
- jobsStats.forEach((v) => {
- cachedStats.set(v.job_id, v);
- });
+ return Math.min(...[defaultLookbackTimestamp, customIntervalOffsetTimestamp].filter(isDefined));
+ };
- return jobsStats;
- };
- })();
+ const getJobIds = memoize((jobs: MlJob[]) => jobs.map((j) => j.job_id));
+
+ const getDatafeeds = memoize(datafeedsService.getDatafeedByJobId);
+
+ const getJobStats = memoize(
+ async (jobIds: string[]) => (await mlClient.getJobStats({ job_id: jobIds.join(',') })).body.jobs
+ );
return {
/**
@@ -103,7 +124,7 @@ export function jobsHealthServiceProvider(
* @param jobIds
*/
async getNotStartedDatafeeds(jobIds: string[]): Promise {
- const datafeeds = await datafeedsService.getDatafeedByJobId(jobIds);
+ const datafeeds = await getDatafeeds(jobIds);
if (datafeeds) {
const jobsStats = await getJobStats(jobIds);
@@ -154,6 +175,67 @@ export function jobsHealthServiceProvider(
};
});
},
+ /**
+ * Returns annotations about delayed data.
+ *
+ * @param jobs
+ * @param timeInterval - Custom time interval provided by the user.
+ * @param docsCount - The threshold for a number of missing documents to alert upon.
+ */
+ async getDelayedDataReport(
+ jobs: MlJob[],
+ timeInterval: string | null,
+ docsCount: number | null
+ ): Promise {
+ const jobIds = getJobIds(jobs);
+ const datafeeds = await getDatafeeds(jobIds);
+
+ const datafeedsMap = keyBy(datafeeds, 'job_id');
+
+ // We shouldn't check jobs that don't have associated datafeeds
+ const resultJobs = jobs.filter((j) => datafeedsMap[j.job_id] !== undefined);
+ const resultJobIds = getJobIds(resultJobs);
+ const jobsMap = keyBy(resultJobs, 'job_id');
+
+ const defaultLookbackInterval = resolveLookbackInterval(resultJobs, datafeeds!);
+ const earliestMs = getDelayedDataLookbackTimestamp(timeInterval, defaultLookbackInterval);
+
+ const annotations: DelayedDataResponse[] = (
+ await annotationService.getDelayedDataAnnotations({
+ jobIds: resultJobIds,
+ earliestMs,
+ })
+ )
+ .map((v) => {
+ const match = v.annotation.match(/Datafeed has missed (\d+)\s/);
+ const missedDocsCount = match ? parseInt(match[1], 10) : 0;
+ return {
+ annotation: v.annotation,
+ // end_timestamp is always defined for delayed_data annotation
+ end_timestamp: v.end_timestamp!,
+ missed_docs_count: missedDocsCount,
+ job_id: v.job_id,
+ };
+ })
+ .filter((v) => {
+ // As we retrieved annotations based on the longest bucket span and query delay,
+ // we need to check end_timestamp against appropriate job configuration.
+
+ const job = jobsMap[v.job_id];
+ const datafeed = datafeedsMap[v.job_id];
+
+ const isDocCountExceededThreshold = docsCount ? v.missed_docs_count >= docsCount : true;
+
+ const jobLookbackInterval = resolveLookbackInterval([job], [datafeed]);
+
+ const isEndTimestampWithinRange =
+ v.end_timestamp > getDelayedDataLookbackTimestamp(timeInterval, jobLookbackInterval);
+
+ return isDocCountExceededThreshold && isEndTimestampWithinRange;
+ });
+
+ return annotations;
+ },
/**
* Retrieves report grouped by test.
*/
@@ -165,7 +247,8 @@ export function jobsHealthServiceProvider(
const results: TestsResults = [];
- const jobIds = await getResultJobIds(includeJobs, excludeJobs);
+ const jobs = await getResultJobs(includeJobs, excludeJobs);
+ const jobIds = getJobIds(jobs);
if (jobIds.length === 0) {
logger.warn(`Rule "${ruleInstanceName}" does not have associated jobs.`);
@@ -178,7 +261,7 @@ export function jobsHealthServiceProvider(
const response = await this.getNotStartedDatafeeds(jobIds);
if (response && response.length > 0) {
results.push({
- name: HEALTH_CHECK_NAMES.datafeed,
+ name: HEALTH_CHECK_NAMES.datafeed.name,
context: {
results: response,
message: i18n.translate(
@@ -200,7 +283,7 @@ export function jobsHealthServiceProvider(
}, 0);
results.push({
- name: HEALTH_CHECK_NAMES.mml,
+ name: HEALTH_CHECK_NAMES.mml.name,
context: {
results: response,
message:
@@ -226,6 +309,31 @@ export function jobsHealthServiceProvider(
}
}
+ if (config.delayedData.enabled) {
+ const response = await this.getDelayedDataReport(
+ jobs,
+ config.delayedData.timeInterval,
+ config.delayedData.docsCount
+ );
+
+ if (response.length > 0) {
+ results.push({
+ name: HEALTH_CHECK_NAMES.delayedData.name,
+ context: {
+ results: response,
+ message: i18n.translate(
+ 'xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataMessage',
+ {
+ defaultMessage:
+ '{jobsCount, plural, one {# job is} other {# jobs are}} suffering from delayed data.',
+ values: { jobsCount: response.length },
+ }
+ ),
+ },
+ });
+ }
+ }
+
return results;
},
};
@@ -251,6 +359,7 @@ export function getJobsHealthServiceProvider(getGuards: GetGuards) {
jobsHealthServiceProvider(
mlClient,
datafeedsProvider(scopedClient, mlClient),
+ annotationServiceProvider(scopedClient),
logger
).getTestsResults(...args)
);
diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts
index 0e4270fb94e3be..063d8ad5a89803 100644
--- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts
+++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts
@@ -42,7 +42,20 @@ export interface NotStartedDatafeedResponse {
job_state: MlJobState;
}
-export type AnomalyDetectionJobHealthResult = MmlTestResponse | NotStartedDatafeedResponse;
+export interface DelayedDataResponse {
+ job_id: string;
+ /** Annotation string */
+ annotation: string;
+ /** Number of missed documents */
+ missed_docs_count: number;
+ /** Timestamp of the latest finalized bucket with missing docs */
+ end_timestamp: number;
+}
+
+export type AnomalyDetectionJobHealthResult =
+ | MmlTestResponse
+ | NotStartedDatafeedResponse
+ | DelayedDataResponse;
export type AnomalyDetectionJobsHealthAlertContext = {
results: AnomalyDetectionJobHealthResult[];
@@ -107,7 +120,7 @@ export function registerJobsMonitoringRuleType({
producer: PLUGIN_ID,
minimumLicenseRequired: MINIMUM_FULL_LICENSE,
isExportable: true,
- async executor({ services, params, alertId, state, previousStartedAt, startedAt, name }) {
+ async executor({ services, params, alertId, state, previousStartedAt, startedAt, name, rule }) {
const fakeRequest = {} as KibanaRequest;
const { getTestsResults } = mlServicesProviders.jobsHealthServiceProvider(
services.savedObjectsClient,
diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts
index c5f3c152ddc7ae..9552735a57d358 100644
--- a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts
+++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts
@@ -9,6 +9,7 @@ import Boom from '@hapi/boom';
import { each, get } from 'lodash';
import { IScopedClusterClient } from 'kibana/server';
+import { estypes } from '@elastic/elasticsearch';
import { ANNOTATION_EVENT_USER, ANNOTATION_TYPE } from '../../../common/constants/annotations';
import { PARTITION_FIELDS } from '../../../common/constants/anomalies';
import {
@@ -25,6 +26,7 @@ import {
getAnnotationFieldValue,
EsAggregationResult,
} from '../../../common/types/annotations';
+import { JobId } from '../../../common/types/anomaly_detection_jobs';
// TODO All of the following interface/type definitions should
// eventually be replaced by the proper upstream definitions
@@ -46,6 +48,7 @@ export interface IndexAnnotationArgs {
fields?: FieldToBucket[];
detectorIndex?: number;
entities?: any[];
+ event?: Annotation['event'];
}
export interface AggTerm {
@@ -60,7 +63,7 @@ export interface GetParams {
export interface GetResponse {
success: true;
- annotations: Record;
+ annotations: Record;
aggregations: EsAggregationResult;
}
@@ -116,7 +119,8 @@ export function annotationProvider({ asInternalUser }: IScopedClusterClient) {
fields,
detectorIndex,
entities,
- }: IndexAnnotationArgs) {
+ event,
+ }: IndexAnnotationArgs): Promise {
const obj: GetResponse = {
success: true,
annotations: {},
@@ -190,6 +194,12 @@ export function annotationProvider({ asInternalUser }: IScopedClusterClient) {
exists: { field: 'annotation' },
});
+ if (event) {
+ boolCriteria.push({
+ term: { event },
+ });
+ }
+
if (jobIds && jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*')) {
let jobIdFilterStr = '';
each(jobIds, (jobId, i: number) => {
@@ -332,6 +342,68 @@ export function annotationProvider({ asInternalUser }: IScopedClusterClient) {
}
}
+ /**
+ * Fetches the latest delayed data annotation per job.
+ * @param jobIds
+ * @param earliestMs - Timestamp for the end_timestamp range query.
+ */
+ async function getDelayedDataAnnotations({
+ jobIds,
+ earliestMs,
+ }: {
+ jobIds: string[];
+ earliestMs?: number;
+ }): Promise {
+ const params: estypes.SearchRequest = {
+ index: ML_ANNOTATIONS_INDEX_ALIAS_READ,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ ...(earliestMs ? [{ range: { end_timestamp: { gte: earliestMs } } }] : []),
+ {
+ term: { event: { value: 'delayed_data' } },
+ },
+ { terms: { job_id: jobIds } },
+ ],
+ },
+ },
+ aggs: {
+ by_job: {
+ terms: { field: 'job_id', size: jobIds.length },
+ aggs: {
+ latest_delayed: {
+ top_hits: {
+ size: 1,
+ sort: [
+ {
+ end_timestamp: {
+ order: 'desc',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+
+ const { body } = await asInternalUser.search(params);
+
+ const annotations = (body.aggregations!.by_job as estypes.AggregationsTermsAggregate<{
+ key: string;
+ doc_count: number;
+ latest_delayed: Pick, 'hits'>;
+ }>).buckets.map((bucket) => {
+ return bucket.latest_delayed.hits.hits[0]._source!;
+ });
+
+ return annotations;
+ }
+
async function deleteAnnotation(id: string) {
const params: DeleteParams = {
index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE,
@@ -347,5 +419,8 @@ export function annotationProvider({ asInternalUser }: IScopedClusterClient) {
getAnnotations,
indexAnnotation,
deleteAnnotation,
+ getDelayedDataAnnotations,
};
}
+
+export type AnnotationService = ReturnType;
diff --git a/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts b/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts
index 4e0f9a9aa7c928..ed58d8d5c9313e 100644
--- a/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/alerting_schema.ts
@@ -79,7 +79,7 @@ export const anomalyDetectionJobsHealthRuleParams = schema.object({
delayedData: schema.nullable(
schema.object({
enabled: schema.boolean({ defaultValue: true }),
- docsCount: schema.nullable(schema.number()),
+ docsCount: schema.nullable(schema.number({ min: 1 })),
timeInterval: schema.nullable(schema.string()),
})
),
diff --git a/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts
index 96edbeb0fce0ec..73e876f0a91220 100644
--- a/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/annotations_schema.ts
@@ -21,7 +21,15 @@ export const indexAnnotationSchema = schema.object({
create_username: schema.maybe(schema.string()),
modified_time: schema.maybe(schema.number()),
modified_username: schema.maybe(schema.string()),
- event: schema.maybe(schema.string()),
+ event: schema.maybe(
+ schema.oneOf([
+ schema.literal('user'),
+ schema.literal('delayed_data'),
+ schema.literal('model_snapshot_stored'),
+ schema.literal('model_change'),
+ schema.literal('categorization_status_change'),
+ ])
+ ),
detector_index: schema.maybe(schema.number()),
partition_field_name: schema.maybe(schema.string()),
partition_field_value: schema.maybe(schema.string()),
diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts
index 964de86ddf377e..b61af3a61c3dc1 100644
--- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts
+++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts
@@ -5,11 +5,11 @@
* 2.0.
*/
+import type { FieldFormat as IFieldFormat } from 'src/plugins/field_formats/common';
import { SavedObjectNotFound } from '../../../../../../../../src/plugins/kibana_utils/public';
import {
DataPublicPluginStart,
IndexPattern,
- FieldFormat as IFieldFormat,
IndexPatternSpec,
} from '../../../../../../../../src/plugins/data/public';
import { rumFieldFormats } from '../configurations/rum/field_formats';
diff --git a/x-pack/plugins/observability/public/services/call_observability_api/index.ts b/x-pack/plugins/observability/public/services/call_observability_api/index.ts
index c87a97fb1dc8a9..7b1b9d222b36e8 100644
--- a/x-pack/plugins/observability/public/services/call_observability_api/index.ts
+++ b/x-pack/plugins/observability/public/services/call_observability_api/index.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
-import { formatRequest } from '@kbn/server-route-repository/target/format_request';
+// @ts-expect-error
+import { formatRequest } from '@kbn/server-route-repository/target_node/format_request';
+import type { formatRequest as formatRequestType } from '@kbn/server-route-repository/target_types/format_request';
import type { HttpSetup } from 'kibana/public';
import type { AbstractObservabilityClient, ObservabilityClient } from './types';
@@ -17,7 +19,9 @@ export function createCallObservabilityApi(http: HttpSetup) {
const client: AbstractObservabilityClient = (options) => {
const { params: { path, body, query } = {}, endpoint, ...rest } = options;
- const { method, pathname } = formatRequest(endpoint, path);
+ const { method, pathname } = formatRequest(endpoint, path) as ReturnType<
+ typeof formatRequestType
+ >;
return http[method](pathname, {
...rest,
diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts
index 65b53b3b77eb49..835eddc03d9f91 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts
@@ -14,7 +14,11 @@ import moment from 'moment';
import Puid from 'puid';
import sinon from 'sinon';
import { ReportingConfig, ReportingCore } from '../../';
-import { fieldFormats, UI_SETTINGS } from '../../../../../../src/plugins/data/server';
+import {
+ FieldFormatsRegistry,
+ StringFormat,
+ FORMATS_UI_SETTINGS,
+} from '../../../../../../src/plugins/field_formats/common';
import {
CSV_QUOTE_VALUES_SETTING,
CSV_SEPARATOR_SETTING,
@@ -104,15 +108,13 @@ describe('CSV Execute Job', function () {
setFieldFormats({
fieldFormatServiceFactory() {
const uiConfigMock = {};
- (uiConfigMock as any)[UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = {
+ (uiConfigMock as any)[FORMATS_UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = {
_default_: { id: 'string', params: {} },
};
- const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry();
+ const fieldFormatsRegistry = new FieldFormatsRegistry();
- fieldFormatsRegistry.init((key) => (uiConfigMock as any)[key], {}, [
- fieldFormats.StringFormat,
- ]);
+ fieldFormatsRegistry.init((key) => (uiConfigMock as any)[key], {}, [StringFormat]);
return Promise.resolve(fieldFormatsRegistry);
},
diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts
index ed303fb0c27bd4..6e1ad21397e73a 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.test.ts
@@ -6,7 +6,13 @@
*/
import expect from '@kbn/expect';
-import { fieldFormats, FieldFormatsGetConfigFn, UI_SETTINGS } from 'src/plugins/data/server';
+import {
+ FieldFormatsGetConfigFn,
+ FieldFormatsRegistry,
+ BytesFormat,
+ NumberFormat,
+ FORMATS_UI_SETTINGS,
+} from 'src/plugins/field_formats/common';
import { IndexPatternSavedObjectDeprecatedCSV } from '../types';
import { fieldFormatMapFactory } from './field_format_map';
@@ -22,16 +28,16 @@ describe('field format map', function () {
},
};
const configMock: Record = {};
- configMock[UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = {
+ configMock[FORMATS_UI_SETTINGS.FORMAT_DEFAULT_TYPE_MAP] = {
number: { id: 'number', params: {} },
};
- configMock[UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN] = '0,0.[000]';
+ configMock[FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_PATTERN] = '0,0.[000]';
const getConfig = ((key: string) => configMock[key]) as FieldFormatsGetConfigFn;
const testValue = '4000';
const mockTimezone = 'Browser';
- const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry();
- fieldFormatsRegistry.init(getConfig, {}, [fieldFormats.BytesFormat, fieldFormats.NumberFormat]);
+ const fieldFormatsRegistry = new FieldFormatsRegistry();
+ fieldFormatsRegistry.init(getConfig, {}, [BytesFormat, NumberFormat]);
const formatMap = fieldFormatMapFactory(
indexPatternSavedObject,
diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts
index e5508f42f8c0d6..9d094f4308ed73 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/field_format_map.ts
@@ -6,8 +6,11 @@
*/
import _ from 'lodash';
-import { FieldFormat } from 'src/plugins/data/common';
-import { FieldFormatConfig, IFieldFormatsRegistry } from 'src/plugins/data/server';
+import {
+ FieldFormat,
+ IFieldFormatsRegistry,
+ FieldFormatConfig,
+} from 'src/plugins/field_formats/common';
import { IndexPatternSavedObjectDeprecatedCSV } from '../types';
/**
diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts
index 440dcc69b1c730..006aa41c6a35e4 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/format_csv_values.ts
@@ -6,7 +6,7 @@
*/
import { isNull, isObject, isUndefined } from 'lodash';
-import { FieldFormat } from 'src/plugins/data/common';
+import { FieldFormat } from 'src/plugins/field_formats/common';
import { RawValue } from '../types';
export function createFormatCsvValues(
diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts
index b0a3a8c7001b18..87512005044059 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts
@@ -14,7 +14,8 @@ import {
savedObjectsClientMock,
uiSettingsServiceMock,
} from 'src/core/server/mocks';
-import { FieldFormatsRegistry, ISearchStartSearchSource } from 'src/plugins/data/common';
+import { ISearchStartSearchSource } from 'src/plugins/data/common';
+import { FieldFormatsRegistry } from 'src/plugins/field_formats/common';
import { searchSourceInstanceMock } from 'src/plugins/data/common/search/search_source/mocks';
import { IScopedSearchClient } from 'src/plugins/data/server';
import { dataPluginMock } from 'src/plugins/data/server/mocks';
diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts
index 3855eff3821b9a..8488b54b77f00a 100644
--- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts
@@ -15,9 +15,6 @@ import { ReportingConfig } from '../../..';
import {
cellHasFormulas,
ES_SEARCH_STRATEGY,
- FieldFormat,
- FieldFormatConfig,
- IFieldFormatsRegistry,
IndexPattern,
ISearchSource,
ISearchStartSearchSource,
@@ -25,6 +22,11 @@ import {
SearchSourceFields,
tabifyDocs,
} from '../../../../../../../src/plugins/data/common';
+import {
+ FieldFormat,
+ FieldFormatConfig,
+ IFieldFormatsRegistry,
+} from '../../../../../../../src/plugins/field_formats/common';
import { KbnServerError } from '../../../../../../../src/plugins/kibana_utils/server';
import { CancellationToken } from '../../../../common';
import { CONTENT_TYPE_CSV } from '../../../../common/constants';
diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
index 695f29dd8d6324..8ad2e62d98c522 100644
--- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
+++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
@@ -12,7 +12,7 @@ jest.mock('../browsers');
import _ from 'lodash';
import * as Rx from 'rxjs';
import { coreMock, elasticsearchServiceMock } from 'src/core/server/mocks';
-import { fieldFormats } from 'src/plugins/data/server';
+import { FieldFormatsRegistry } from 'src/plugins/field_formats/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { dataPluginMock } from 'src/plugins/data/server/mocks';
import { ReportingConfig, ReportingCore } from '../';
@@ -171,7 +171,7 @@ export const createMockReportingCore = async (
setFieldFormats({
fieldFormatServiceFactory() {
- const fieldFormatsRegistry = new fieldFormats.FieldFormatsRegistry();
+ const fieldFormatsRegistry = new FieldFormatsRegistry();
return Promise.resolve(fieldFormatsRegistry);
},
});
diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json
index c28086b96aea28..406fe9965b8a03 100644
--- a/x-pack/plugins/reporting/tsconfig.json
+++ b/x-pack/plugins/reporting/tsconfig.json
@@ -24,6 +24,7 @@
{ "path": "../../../src/plugins/share/tsconfig.json" },
{ "path": "../../../src/plugins/ui_actions/tsconfig.json" },
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
+ { "path": "../../../src/plugins/field_formats/tsconfig.json" },
{ "path": "../features/tsconfig.json" },
{ "path": "../licensing/tsconfig.json" },
{ "path": "../security/tsconfig.json" },
diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md
index 19e276aeec4895..16e4b8f3e01e65 100644
--- a/x-pack/plugins/rule_registry/README.md
+++ b/x-pack/plugins/rule_registry/README.md
@@ -124,7 +124,6 @@ The following fields are defined in the technical field component template and s
- `rule.uuid`: the saved objects id of the rule.
- `rule.name`: the name of the rule (as specified by the user).
- `rule.category`: the name of the rule type (as defined by the rule type producer)
-- `kibana.alert.producer`: the producer of the rule type. Usually a Kibana plugin. e.g., `APM`.
- `kibana.alert.owner`: the feature which produced the alert. Usually a Kibana feature id like `apm`, `siem`...
- `kibana.alert.id`: the id of the alert, that is unique within the context of the rule execution it was created in. E.g., for a rule that monitors latency for all services in all environments, this might be `opbeans-java:production`.
- `kibana.alert.uuid`: the unique identifier for the alert during its lifespan. If an alert recovers (or closes), this identifier is re-generated when it is opened again.
diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts
index ff81e05851f7e5..859070bd498e32 100644
--- a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts
+++ b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts
@@ -2148,7 +2148,7 @@ export const ecsFieldMap = {
'rule.id': {
type: 'keyword',
array: false,
- required: false,
+ required: true,
},
'rule.license': {
type: 'keyword',
diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts
index ebe7c88f756b20..11e572260d1335 100644
--- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts
+++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts
@@ -20,9 +20,9 @@ export const technicalRuleFieldMap = {
Fields.RULE_CATEGORY,
Fields.TAGS
),
- [Fields.ALERT_OWNER]: { type: 'keyword' },
+ [Fields.ALERT_OWNER]: { type: 'keyword', required: true },
[Fields.ALERT_PRODUCER]: { type: 'keyword' },
- [Fields.SPACE_IDS]: { type: 'keyword', array: true },
+ [Fields.SPACE_IDS]: { type: 'keyword', array: true, required: true },
[Fields.ALERT_UUID]: { type: 'keyword' },
[Fields.ALERT_ID]: { type: 'keyword' },
[Fields.ALERT_START]: { type: 'date' },
diff --git a/x-pack/plugins/rule_registry/server/mocks.ts b/x-pack/plugins/rule_registry/server/mocks.ts
index cc5c3cfd484a7c..395b53e229d643 100644
--- a/x-pack/plugins/rule_registry/server/mocks.ts
+++ b/x-pack/plugins/rule_registry/server/mocks.ts
@@ -5,8 +5,10 @@
* 2.0.
*/
+import { ruleDataPluginServiceMock } from './rule_data_plugin_service/rule_data_plugin_service.mock';
import { createLifecycleAlertServicesMock } from './utils/lifecycle_alert_services_mock';
export const ruleRegistryMocks = {
createLifecycleAlertServices: createLifecycleAlertServicesMock,
+ createRuleDataPluginService: ruleDataPluginServiceMock.create,
};
diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts
index d89eb305545e88..073a48248f89ad 100644
--- a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts
+++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts
@@ -13,6 +13,7 @@ import {
CONSUMERS,
ECS_VERSION,
RULE_ID,
+ SPACE_IDS,
TIMESTAMP,
VERSION,
} from '@kbn/rule-data-utils';
@@ -33,6 +34,7 @@ const getMockAlert = (): ParsedTechnicalFields => ({
[ALERT_OWNER]: 'apm',
[ALERT_STATUS]: 'open',
[ALERT_RULE_RISK_SCORE]: 20,
+ [SPACE_IDS]: ['fake-space-id'],
[ALERT_RULE_SEVERITY]: 'warning',
});
diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts
index 275d68621864fc..348c0fc6b1cfce 100644
--- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts
+++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock.ts
@@ -6,11 +6,11 @@
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
-import { RuleDataPluginService, RuleDataPluginServiceConstructorOptions } from './';
+import { RuleDataPluginService } from './';
type Schema = PublicMethodsOf;
-const createRuleDataPluginServiceMock = (_: RuleDataPluginServiceConstructorOptions) => {
+const createRuleDataPluginService = () => {
const mocked: jest.Mocked = {
init: jest.fn(),
isReady: jest.fn(),
@@ -27,9 +27,7 @@ const createRuleDataPluginServiceMock = (_: RuleDataPluginServiceConstructorOpti
};
export const ruleDataPluginServiceMock: {
- create: (
- _: RuleDataPluginServiceConstructorOptions
- ) => jest.Mocked>;
+ create: () => jest.Mocked>;
} = {
- create: createRuleDataPluginServiceMock,
+ create: createRuleDataPluginService,
};
diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts
index 80b75b8c74732d..037efadabd8def 100644
--- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts
+++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts
@@ -23,6 +23,9 @@ import {
ALERT_STATUS,
EVENT_ACTION,
EVENT_KIND,
+ RULE_ID,
+ ALERT_OWNER,
+ SPACE_IDS,
} from '../../common/technical_rule_data_field_names';
import { createRuleDataClientMock } from '../rule_data_client/create_rule_data_client_mock';
import { createLifecycleExecutor } from './create_lifecycle_executor';
@@ -128,12 +131,16 @@ describe('createLifecycleExecutor', () => {
{
fields: {
[ALERT_ID]: 'TEST_ALERT_0',
+ [ALERT_OWNER]: 'CONSUMER',
+ [RULE_ID]: 'RULE_TYPE_ID',
labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc
},
},
{
fields: {
[ALERT_ID]: 'TEST_ALERT_1',
+ [ALERT_OWNER]: 'CONSUMER',
+ [RULE_ID]: 'RULE_TYPE_ID',
labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc
},
},
@@ -222,6 +229,9 @@ describe('createLifecycleExecutor', () => {
fields: {
'@timestamp': '',
[ALERT_ID]: 'TEST_ALERT_0',
+ [ALERT_OWNER]: 'CONSUMER',
+ [RULE_ID]: 'RULE_TYPE_ID',
+ [SPACE_IDS]: ['fake-space-id'],
labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc
},
},
@@ -229,6 +239,9 @@ describe('createLifecycleExecutor', () => {
fields: {
'@timestamp': '',
[ALERT_ID]: 'TEST_ALERT_1',
+ [ALERT_OWNER]: 'CONSUMER',
+ [RULE_ID]: 'RULE_TYPE_ID',
+ [SPACE_IDS]: ['fake-space-id'],
labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc
},
},
diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
index 50ac8afb945b43..23ae24cb91bc40 100644
--- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
+++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts
@@ -30,6 +30,7 @@ import {
EVENT_ACTION,
EVENT_KIND,
ALERT_OWNER,
+ RULE_ID,
RULE_UUID,
TIMESTAMP,
SPACE_IDS,
@@ -154,6 +155,8 @@ export const createLifecycleExecutor = (
currentAlerts[id] = {
...fields,
[ALERT_ID]: id,
+ [RULE_ID]: rule.ruleTypeId,
+ [ALERT_OWNER]: rule.consumer,
};
return alertInstanceFactory(id);
},
@@ -226,6 +229,8 @@ export const createLifecycleExecutor = (
alertsDataMap[alertId] = {
...fields,
[ALERT_ID]: alertId,
+ [RULE_ID]: rule.ruleTypeId,
+ [ALERT_OWNER]: rule.consumer,
};
});
}
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index 6d4a2b78840ea6..0ae42d4baaec45 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -16,6 +16,7 @@ export const allowedExperimentalValues = Object.freeze({
ruleRegistryEnabled: false,
tGridEnabled: false,
trustedAppsByPolicyEnabled: false,
+ excludePoliciesInFilterEnabled: false,
uebaEnabled: false,
});
diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts
index 2eba6aba0c5e22..243bfd113bfd23 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/login.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts
@@ -205,11 +205,6 @@ const credentialsProvidedByEnvironment = (): boolean =>
* Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed).
*/
const loginViaEnvironmentCredentials = () => {
- const providerName =
- Cypress.env('protocol') === 'http' || Cypress.config().baseUrl!.includes('staging')
- ? 'basic'
- : 'cloud-basic';
-
cy.log(
`Authenticating via environment credentials from the \`CYPRESS_${ELASTICSEARCH_USERNAME}\` and \`CYPRESS_${ELASTICSEARCH_PASSWORD}\` environment variables`
);
@@ -218,7 +213,7 @@ const loginViaEnvironmentCredentials = () => {
cy.request({
body: {
providerType: 'basic',
- providerName,
+ providerName: 'basic',
currentURL: '/',
params: {
username: Cypress.env(ELASTICSEARCH_USERNAME),
diff --git a/x-pack/plugins/security_solution/public/common/components/help_menu/index.tsx b/x-pack/plugins/security_solution/public/common/components/help_menu/index.tsx
index 20bb4f51c075b0..28ff98d5c9c880 100644
--- a/x-pack/plugins/security_solution/public/common/components/help_menu/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/help_menu/index.tsx
@@ -32,7 +32,7 @@ export const HelpMenu = React.memo(() => {
content: i18n.translate('xpack.securitySolution.chrome.helpMenu.documentation.ecs', {
defaultMessage: 'ECS documentation',
}),
- href: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/ecs/current/index.html`,
+ href: docLinks.links.ecs.guide,
iconType: 'documents',
linkType: 'custom',
target: '_blank',
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 cc6ac5355f90b8..d7d62d82a95b04 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
@@ -197,7 +197,7 @@ export const MlPopover = React.memo(() => {
values={{
mlDocs: (
diff --git a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts
index 5b0a2330a408d0..6ab572490f5d7f 100644
--- a/x-pack/plugins/security_solution/public/common/store/app/reducer.ts
+++ b/x-pack/plugins/security_solution/public/common/store/app/reducer.ts
@@ -19,6 +19,7 @@ export const initialAppState: AppState = {
errors: [],
enableExperimental: {
trustedAppsByPolicyEnabled: false,
+ excludePoliciesInFilterEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
tGridEnabled: false,
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx
index 935a5ff336c00f..70b98eb22fd89a 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.test.tsx
@@ -20,6 +20,9 @@ describe('UpdatePrePackagedRulesCallOut', () => {
docLinks: {
ELASTIC_WEBSITE_URL: '',
DOC_LINK_VERSION: '',
+ links: {
+ siem: { ruleChangeLog: '' },
+ },
},
},
});
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx
index 754acea40ed9bd..9d67a1e6b9ae86 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/update_callout.tsx
@@ -56,10 +56,7 @@ const UpdatePrePackagedRulesCallOutComponent: React.FC
{prepackagedRulesOrTimelines?.callOutMessage}
-
+
{i18n.RELEASE_NOTES_HELP}
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_index.tsx
index b65cb5069d399b..19219208b4079a 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_no_index.tsx
@@ -33,7 +33,7 @@ const DetectionEngineNoIndexComponent: React.FC<{
detections: {
icon: 'documents',
label: i18n.GO_TO_DOCUMENTATION,
- url: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/security/${docLinks.DOC_LINK_VERSION}/detections-permissions-section.html`,
+ url: `${docLinks.links.siem.detectionsReq}`,
target: '_blank',
},
}),
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.tsx
index 19dd14c46a4a94..c725bae54b06c7 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine_user_unauthenticated.tsx
@@ -18,7 +18,7 @@ export const DetectionEngineUserUnauthenticated = React.memo(() => {
detectionUnauthenticated: {
icon: 'documents',
label: i18n.GO_TO_DOCUMENTATION,
- url: `${docLinks.ELASTIC_WEBSITE_URL}guide/en/security/${docLinks.DOC_LINK_VERSION}/detections-permissions-section.html`,
+ url: `${docLinks.links.siem.detectionsReq}`,
target: '_blank',
},
}),
diff --git a/x-pack/plugins/security_solution/public/management/common/routing.test.ts b/x-pack/plugins/security_solution/public/management/common/routing.test.ts
index 82b7a15d642e40..e0e6ed2a080371 100644
--- a/x-pack/plugins/security_solution/public/management/common/routing.test.ts
+++ b/x-pack/plugins/security_solution/public/management/common/routing.test.ts
@@ -106,26 +106,28 @@ describe('routing', () => {
});
it('builds proper path when only page size provided', () => {
- expect(getTrustedAppsListPath({ page_size: 20 })).toEqual(
- '/administration/trusted_apps?page_size=20'
+ const pageSize = 20;
+ expect(getTrustedAppsListPath({ page_size: pageSize })).toEqual(
+ `/administration/trusted_apps?page_size=${pageSize}`
);
});
it('builds proper path when only page index provided', () => {
- expect(getTrustedAppsListPath({ page_index: 2 })).toEqual(
- '/administration/trusted_apps?page_index=2'
+ const pageIndex = 2;
+ expect(getTrustedAppsListPath({ page_index: pageIndex })).toEqual(
+ `/administration/trusted_apps?page_index=${pageIndex}`
);
});
it('builds proper path when only "show" provided', () => {
- expect(getTrustedAppsListPath({ show: 'create' })).toEqual(
- '/administration/trusted_apps?show=create'
- );
+ const show = 'create';
+ expect(getTrustedAppsListPath({ show })).toEqual(`/administration/trusted_apps?show=${show}`);
});
it('builds proper path when only view type provided', () => {
- expect(getTrustedAppsListPath({ view_type: 'list' })).toEqual(
- '/administration/trusted_apps?view_type=list'
+ const viewType = 'list';
+ expect(getTrustedAppsListPath({ view_type: viewType })).toEqual(
+ `/administration/trusted_apps?view_type=${viewType}`
);
});
@@ -135,56 +137,82 @@ describe('routing', () => {
page_size: 20,
show: 'create',
view_type: 'list',
- filter: '',
+ filter: 'test',
+ included_policies: 'globally',
+ excluded_policies: 'unassigned',
};
expect(getTrustedAppsListPath(location)).toEqual(
- '/administration/trusted_apps?page_index=2&page_size=20&view_type=list&show=create'
+ `/administration/trusted_apps?page_index=${location.page_index}&page_size=${location.page_size}&view_type=${location.view_type}&show=${location.show}&filter=${location.filter}&included_policies=${location.included_policies}&excluded_policies=${location.excluded_policies}`
);
});
it('builds proper path when page index is equal to default', () => {
- const path = getTrustedAppsListPath({
+ const location: TrustedAppsListPageLocation = {
page_index: MANAGEMENT_DEFAULT_PAGE,
page_size: 20,
show: 'create',
view_type: 'list',
- });
+ filter: '',
+ included_policies: '',
+ excluded_policies: '',
+ };
+ const path = getTrustedAppsListPath(location);
- expect(path).toEqual('/administration/trusted_apps?page_size=20&view_type=list&show=create');
+ expect(path).toEqual(
+ `/administration/trusted_apps?page_size=${location.page_size}&view_type=${location.view_type}&show=${location.show}`
+ );
});
it('builds proper path when page size is equal to default', () => {
- const path = getTrustedAppsListPath({
+ const location: TrustedAppsListPageLocation = {
page_index: 2,
page_size: MANAGEMENT_DEFAULT_PAGE_SIZE,
show: 'create',
view_type: 'list',
- });
+ filter: '',
+ included_policies: '',
+ excluded_policies: '',
+ };
+ const path = getTrustedAppsListPath(location);
- expect(path).toEqual('/administration/trusted_apps?page_index=2&view_type=list&show=create');
+ expect(path).toEqual(
+ `/administration/trusted_apps?page_index=${location.page_index}&view_type=${location.view_type}&show=${location.show}`
+ );
});
it('builds proper path when "show" is equal to default', () => {
- const path = getTrustedAppsListPath({
+ const location: TrustedAppsListPageLocation = {
page_index: 2,
page_size: 20,
show: undefined,
view_type: 'list',
- });
+ filter: '',
+ included_policies: '',
+ excluded_policies: '',
+ };
+ const path = getTrustedAppsListPath(location);
- expect(path).toEqual('/administration/trusted_apps?page_index=2&page_size=20&view_type=list');
+ expect(path).toEqual(
+ `/administration/trusted_apps?page_index=${location.page_index}&page_size=${location.page_size}&view_type=${location.view_type}`
+ );
});
it('builds proper path when view type is equal to default', () => {
- const path = getTrustedAppsListPath({
+ const location: TrustedAppsListPageLocation = {
page_index: 2,
page_size: 20,
show: 'create',
view_type: 'grid',
- });
+ filter: '',
+ included_policies: '',
+ excluded_policies: '',
+ };
+ const path = getTrustedAppsListPath(location);
- expect(path).toEqual('/administration/trusted_apps?page_index=2&page_size=20&show=create');
+ expect(path).toEqual(
+ `/administration/trusted_apps?page_index=${location.page_index}&page_size=${location.page_size}&show=${location.show}`
+ );
});
it('builds proper path when params are equal to default', () => {
diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts
index e6c02ffefa3c51..d044fc0f1f2f60 100644
--- a/x-pack/plugins/security_solution/public/management/common/routing.ts
+++ b/x-pack/plugins/security_solution/public/management/common/routing.ts
@@ -140,6 +140,12 @@ const normalizeTrustedAppsPageLocation = (
...(!isDefaultOrMissing(location.show, undefined) ? { show: location.show } : {}),
...(!isDefaultOrMissing(location.id, undefined) ? { id: location.id } : {}),
...(!isDefaultOrMissing(location.filter, '') ? { filter: location.filter } : ''),
+ ...(!isDefaultOrMissing(location.included_policies, '')
+ ? { included_policies: location.included_policies }
+ : ''),
+ ...(!isDefaultOrMissing(location.excluded_policies, '')
+ ? { excluded_policies: location.excluded_policies }
+ : ''),
};
} else {
return {};
@@ -196,12 +202,26 @@ const extractFilter = (query: querystring.ParsedUrlQuery): string => {
return extractFirstParamValue(query, 'filter') || '';
};
+const extractIncludedPolicies = (query: querystring.ParsedUrlQuery): string => {
+ return extractFirstParamValue(query, 'included_policies') || '';
+};
+
+const extractExcludedPolicies = (query: querystring.ParsedUrlQuery): string => {
+ return extractFirstParamValue(query, 'excluded_policies') || '';
+};
+
export const extractListPaginationParams = (query: querystring.ParsedUrlQuery) => ({
page_index: extractPageIndex(query),
page_size: extractPageSize(query),
filter: extractFilter(query),
});
+export const extractTrustedAppsListPaginationParams = (query: querystring.ParsedUrlQuery) => ({
+ ...extractListPaginationParams(query),
+ included_policies: extractIncludedPolicies(query),
+ excluded_policies: extractExcludedPolicies(query),
+});
+
export const extractTrustedAppsListPageLocation = (
query: querystring.ParsedUrlQuery
): TrustedAppsListPageLocation => {
@@ -211,7 +231,7 @@ export const extractTrustedAppsListPageLocation = (
) as TrustedAppsListPageLocation['show'];
return {
- ...extractListPaginationParams(query),
+ ...extractTrustedAppsListPaginationParams(query),
view_type: extractFirstParamValue(query, 'view_type') === 'list' ? 'list' : 'grid',
show:
showParamValue && ['edit', 'create'].includes(showParamValue) ? showParamValue : undefined,
diff --git a/x-pack/plugins/security_solution/public/management/common/utils.ts b/x-pack/plugins/security_solution/public/management/common/utils.ts
index 616e395c8ad479..3fbe5662f338c8 100644
--- a/x-pack/plugins/security_solution/public/management/common/utils.ts
+++ b/x-pack/plugins/security_solution/public/management/common/utils.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { isEmpty } from 'lodash/fp';
+
export const parseQueryFilterToKQL = (filter: string, fields: Readonly): string => {
if (!filter) return '';
const kuery = fields
@@ -19,3 +21,31 @@ export const parseQueryFilterToKQL = (filter: string, fields: Readonly
return `(${kuery})`;
};
+
+const getPolicyQuery = (policyId: string): string => {
+ if (policyId === 'global') return 'exception-list-agnostic.attributes.tags:"policy:all"';
+ if (policyId === 'unassigned') return '(not exception-list-agnostic.attributes.tags:*)';
+ return `exception-list-agnostic.attributes.tags:"policy:${policyId}"`;
+};
+
+export const parsePoliciesToKQL = (includedPolicies: string, excludedPolicies: string): string => {
+ if (isEmpty(includedPolicies) && isEmpty(excludedPolicies)) return '';
+
+ const parsedIncludedPolicies = includedPolicies ? includedPolicies.split(',') : undefined;
+ const parsedExcludedPolicies = excludedPolicies ? excludedPolicies.split(',') : undefined;
+
+ const includedPoliciesKuery = parsedIncludedPolicies
+ ? parsedIncludedPolicies.map(getPolicyQuery).join(' OR ')
+ : '';
+
+ const excludedPoliciesKuery = parsedExcludedPolicies
+ ? parsedExcludedPolicies.map((policyId) => `not ${getPolicyQuery(policyId)}`).join(' AND ')
+ : '';
+
+ const kuery = [];
+
+ if (includedPoliciesKuery) kuery.push(includedPoliciesKuery);
+ if (excludedPoliciesKuery) kuery.push(excludedPoliciesKuery);
+
+ return `(${kuery.join(' AND ')})`;
+};
diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
index a3b1597377fa09..33e2980dcc1124 100644
--- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
@@ -24,6 +24,7 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import onboardingLogo from '../images/security_administration_onboarding.svg';
+import { useKibana } from '../../common/lib/kibana';
const TEXT_ALIGN_CENTER: CSSProperties = Object.freeze({
textAlign: 'center',
@@ -44,6 +45,7 @@ const PolicyEmptyState = React.memo<{
onActionClick: (event: MouseEvent) => void;
actionDisabled?: boolean;
}>(({ loading, onActionClick, actionDisabled }) => {
+ const docLinks = useKibana().services.docLinks;
return (
{loading ? (
@@ -83,7 +85,7 @@ const PolicyEmptyState = React.memo<{
id="xpack.securitySolution.endpoint.policyList.onboardingSectionThree"
defaultMessage="To get started, add the Endpoint Security integration to your Agents. For more information, "
/>
-
+
{
+ let getElement: (params: Partial) => RenderResult;
+ beforeEach(() => {
+ onChangeSelectionMock = jest.fn();
+ useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
+ getElement = (params: Partial) => {
+ return render(
+
+
+
+ );
+ };
+ });
+ const generator = new EndpointDocGenerator('policy-list');
+ const policy = generator.generatePolicyPackagePolicy();
+ policy.name = 'test policy A';
+ policy.id = 'abc123';
+
+ describe('When click on policy', () => {
+ it('should have a default value', () => {
+ const defaultIncludedPolicies = 'abc123';
+ const defaultExcludedPolicies = 'global';
+ const element = getElement({ defaultExcludedPolicies, defaultIncludedPolicies });
+ act(() => {
+ fireEvent.click(element.getByTestId('policiesSelectorButton'));
+ });
+ expect(element.getByText(policy.name)).toHaveTextContent(policy.name);
+ act(() => {
+ fireEvent.click(element.getByText('Unassigned entries'));
+ });
+ expect(onChangeSelectionMock).toHaveBeenCalledWith([
+ { checked: 'on', id: 'abc123', name: 'test policy A' },
+ { checked: 'off', id: 'global', name: 'Global entries' },
+ { checked: 'on', id: 'unassigned', name: 'Unassigned entries' },
+ ]);
+ });
+
+ it('should disable enabled default value', () => {
+ useIsExperimentalFeatureEnabledMock.mockReturnValue(true);
+ const defaultIncludedPolicies = 'abc123';
+ const defaultExcludedPolicies = 'global';
+ const element = getElement({ defaultExcludedPolicies, defaultIncludedPolicies });
+ act(() => {
+ fireEvent.click(element.getByTestId('policiesSelectorButton'));
+ });
+ act(() => {
+ fireEvent.click(element.getByText(policy.name));
+ });
+ expect(onChangeSelectionMock).toHaveBeenCalledWith([
+ { checked: 'off', id: 'abc123', name: 'test policy A' },
+ { checked: 'off', id: 'global', name: 'Global entries' },
+ { checked: undefined, id: 'unassigned', name: 'Unassigned entries' },
+ ]);
+ });
+
+ it('should remove disabled default value', () => {
+ const defaultIncludedPolicies = 'abc123';
+ const defaultExcludedPolicies = 'global';
+ const element = getElement({ defaultExcludedPolicies, defaultIncludedPolicies });
+ act(() => {
+ fireEvent.click(element.getByTestId('policiesSelectorButton'));
+ });
+ act(() => {
+ fireEvent.click(element.getByText('Global entries'));
+ });
+ expect(onChangeSelectionMock).toHaveBeenCalledWith([
+ { checked: 'on', id: 'abc123', name: 'test policy A' },
+ { checked: undefined, id: 'global', name: 'Global entries' },
+ { checked: undefined, id: 'unassigned', name: 'Unassigned entries' },
+ ]);
+ });
+ });
+
+ describe('When filter policy', () => {
+ it('should filter policy by name', () => {
+ const element = getElement({});
+ act(() => {
+ fireEvent.click(element.getByTestId('policiesSelectorButton'));
+ });
+ act(() => {
+ fireEvent.change(element.getByTestId('policiesSelectorSearch'), {
+ target: { value: policy.name },
+ });
+ });
+ expect(element.queryAllByText('Global entries')).toStrictEqual([]);
+ expect(element.getByText(policy.name)).toHaveTextContent(policy.name);
+ });
+ it('should filter with no results', () => {
+ const element = getElement({});
+ act(() => {
+ fireEvent.click(element.getByTestId('policiesSelectorButton'));
+ });
+ act(() => {
+ fireEvent.change(element.getByTestId('policiesSelectorSearch'), {
+ target: { value: 'no results' },
+ });
+ });
+ expect(element.queryAllByText('Global entries')).toStrictEqual([]);
+ expect(element.queryAllByText('Unassigned entries')).toStrictEqual([]);
+ expect(element.queryAllByText(policy.name)).toStrictEqual([]);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.tsx b/x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.tsx
new file mode 100644
index 00000000000000..b86c8f6de7abd1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/policies_selector/policies_selector.tsx
@@ -0,0 +1,205 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo, useCallback, useMemo, useState, useEffect, ChangeEvent } from 'react';
+
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFilterGroup,
+ EuiPopover,
+ EuiPopoverTitle,
+ EuiFieldSearch,
+ EuiFilterButton,
+ EuiFilterSelectItem,
+ FilterChecked,
+ EuiText,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types';
+import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
+
+export interface PoliciesSelectorProps {
+ policies: ImmutableArray;
+ defaultIncludedPolicies?: string;
+ defaultExcludedPolicies?: string;
+ onChangeSelection: (items: PolicySelectionItem[]) => void;
+}
+
+export interface PolicySelectionItem {
+ name: string;
+ id?: string;
+ checked?: FilterChecked;
+}
+
+interface DefaultPoliciesByKey {
+ [key: string]: boolean;
+}
+
+const GLOBAL_ENTRIES = i18n.translate(
+ 'xpack.securitySolution.management.policiesSelector.globalEntries',
+ {
+ defaultMessage: 'Global entries',
+ }
+);
+const UNASSIGNED_ENTRIES = i18n.translate(
+ 'xpack.securitySolution.management.policiesSelector.unassignedEntries',
+ {
+ defaultMessage: 'Unassigned entries',
+ }
+);
+
+export const PoliciesSelector = memo(
+ ({ policies, onChangeSelection, defaultExcludedPolicies, defaultIncludedPolicies }) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const [query, setQuery] = useState('');
+ const [itemsList, setItemsList] = useState([]);
+
+ const isExcludePoliciesInFilterEnabled = useIsExperimentalFeatureEnabled(
+ 'excludePoliciesInFilterEnabled'
+ );
+
+ useEffect(() => {
+ const defaultIncludedPoliciesByKey: DefaultPoliciesByKey = defaultIncludedPolicies
+ ? defaultIncludedPolicies.split(',').reduce((acc, val) => ({ ...acc, [val]: true }), {})
+ : {};
+
+ const defaultExcludedPoliciesByKey: DefaultPoliciesByKey = defaultExcludedPolicies
+ ? defaultExcludedPolicies.split(',').reduce((acc, val) => ({ ...acc, [val]: true }), {})
+ : {};
+
+ const getCheckedValue = (id: string): FilterChecked | undefined =>
+ defaultIncludedPoliciesByKey[id]
+ ? 'on'
+ : defaultExcludedPoliciesByKey[id]
+ ? 'off'
+ : undefined;
+
+ setItemsList([
+ ...policies.map((policy) => ({
+ name: policy.name,
+ id: policy.id,
+ checked: getCheckedValue(policy.id),
+ })),
+ { name: GLOBAL_ENTRIES, id: 'global', checked: getCheckedValue('global') },
+ { name: UNASSIGNED_ENTRIES, id: 'unassigned', checked: getCheckedValue('unassigned') },
+ ]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [policies]);
+
+ const onButtonClick = useCallback(() => {
+ setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen);
+ }, []);
+
+ const closePopover = useCallback(() => {
+ setIsPopoverOpen(false);
+ }, []);
+
+ const onChange = useCallback((ev: ChangeEvent) => {
+ const value = ev.target.value || '';
+ setQuery(value);
+ }, []);
+
+ const updateItem = useCallback(
+ (index: number) => {
+ if (!itemsList[index]) {
+ return;
+ }
+
+ const newItems = [...itemsList];
+
+ switch (newItems[index].checked) {
+ case 'on':
+ newItems[index].checked = isExcludePoliciesInFilterEnabled ? 'off' : undefined;
+ break;
+
+ case 'off':
+ newItems[index].checked = undefined;
+ break;
+
+ default:
+ newItems[index].checked = 'on';
+ }
+
+ setItemsList(newItems);
+ onChangeSelection(newItems);
+ },
+ [itemsList, onChangeSelection, isExcludePoliciesInFilterEnabled]
+ );
+
+ const dropdownItems = useMemo(
+ () =>
+ itemsList.map((item, index) =>
+ item.name.match(new RegExp(query, 'i')) ? (
+ updateItem(index)}
+ >
+ {item.name}
+
+ ) : null
+ ),
+ [itemsList, query, updateItem]
+ );
+
+ const button = useMemo(
+ () => (
+ item.checked === 'on')}
+ numActiveFilters={itemsList.filter((item) => item.checked === 'on').length}
+ >
+
+
+
+
+ ),
+ [isPopoverOpen, itemsList, onButtonClick]
+ );
+
+ return (
+
+
+
+
+
+
+
+ {dropdownItems}
+
+
+
+
+ );
+ }
+);
+
+PoliciesSelector.displayName = 'PoliciesSelector';
diff --git a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx
deleted file mode 100644
index 5ace2b901da11c..00000000000000
--- a/x-pack/plugins/security_solution/public/management/components/search_bar/index.tsx
+++ /dev/null
@@ -1,51 +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 React, { memo, useCallback, useState } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-
-export interface SearchBarProps {
- defaultValue?: string;
- placeholder: string;
- onSearch(value: string): void;
-}
-
-export const SearchBar = memo(({ defaultValue = '', onSearch, placeholder }) => {
- const [query, setQuery] = useState(defaultValue);
-
- const handleOnChangeSearchField = useCallback(
- (ev: React.ChangeEvent) => setQuery(ev.target.value),
- [setQuery]
- );
- const handleOnSearch = useCallback(() => onSearch(query), [query, onSearch]);
-
- return (
-
-
-
-
-
-
- {i18n.translate('xpack.securitySolution.management.search.button', {
- defaultMessage: 'Refresh',
- })}
-
-
-
- );
-});
-
-SearchBar.displayName = 'SearchBar';
diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/security_solution/public/management/components/search_exceptions/index.ts
similarity index 76%
rename from x-pack/plugins/apm/common/ui_settings_keys.ts
rename to x-pack/plugins/security_solution/public/management/components/search_exceptions/index.ts
index 427c30605e71b8..6a870dbb06c66c 100644
--- a/x-pack/plugins/apm/common/ui_settings_keys.ts
+++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export const enableServiceOverview = 'apm:enableServiceOverview';
+export { SearchExceptions, SearchExceptionsProps } from './search_exceptions';
diff --git a/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx
similarity index 89%
rename from x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx
rename to x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx
index 707a96938655a7..5c909e062ceb9c 100644
--- a/x-pack/plugins/security_solution/public/management/components/search_bar/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx
@@ -8,7 +8,7 @@
import { mount } from 'enzyme';
import React from 'react';
-import { SearchBar } from '.';
+import { SearchExceptions } from '.';
let onSearchMock: jest.Mock;
@@ -16,13 +16,17 @@ interface EuiFieldSearchPropsFake {
onSearch(value: string): void;
}
-describe('Search bar', () => {
+describe('Search exceptions', () => {
beforeEach(() => {
onSearchMock = jest.fn();
});
const getElement = (defaultValue: string = '') => (
-
+
);
it('should have a default value', () => {
@@ -45,7 +49,7 @@ describe('Search bar', () => {
searchFieldProps.onSearch(expectedDefaultValue);
expect(onSearchMock).toHaveBeenCalledTimes(1);
- expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue);
+ expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', '');
});
it('should dispatch search action when click on button', () => {
@@ -55,6 +59,6 @@ describe('Search bar', () => {
element.find('[data-test-subj="searchButton"]').first().simulate('click');
expect(onSearchMock).toHaveBeenCalledTimes(1);
- expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue);
+ expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', '');
});
});
diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx
new file mode 100644
index 00000000000000..a737b53e2d9b9a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx
@@ -0,0 +1,116 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { memo, useCallback, useState } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { PolicySelectionItem, PoliciesSelector } from '../policies_selector';
+import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types';
+
+export interface SearchExceptionsProps {
+ defaultValue?: string;
+ placeholder: string;
+ hasPolicyFilter?: boolean;
+ policyList?: ImmutableArray;
+ defaultExcludedPolicies?: string;
+ defaultIncludedPolicies?: string;
+ onSearch(query: string, includedPolicies?: string, excludedPolicies?: string): void;
+}
+
+export const SearchExceptions = memo(
+ ({
+ defaultValue = '',
+ onSearch,
+ placeholder,
+ hasPolicyFilter,
+ policyList,
+ defaultIncludedPolicies,
+ defaultExcludedPolicies,
+ }) => {
+ const [query, setQuery] = useState(defaultValue);
+ const [includedPolicies, setIncludedPolicies] = useState(defaultIncludedPolicies || '');
+ const [excludedPolicies, setExcludedPolicies] = useState(defaultExcludedPolicies || '');
+
+ const onChangeSelection = useCallback(
+ (items: PolicySelectionItem[]) => {
+ const includePoliciesNew = items
+ .filter((item) => item.checked === 'on')
+ .map((item) => item.id)
+ .join(',');
+ const excludePoliciesNew = items
+ .filter((item) => item.checked === 'off')
+ .map((item) => item.id)
+ .join(',');
+
+ setIncludedPolicies(includePoliciesNew);
+ setExcludedPolicies(excludePoliciesNew);
+
+ onSearch(query, includePoliciesNew, excludePoliciesNew);
+ },
+ [onSearch, query]
+ );
+
+ const handleOnChangeSearchField = useCallback(
+ (ev: React.ChangeEvent) => setQuery(ev.target.value),
+ [setQuery]
+ );
+ const handleOnSearch = useCallback(() => onSearch(query, includedPolicies, excludedPolicies), [
+ onSearch,
+ query,
+ includedPolicies,
+ excludedPolicies,
+ ]);
+
+ const handleOnSearchQuery = useCallback(
+ (value) => {
+ onSearch(value, includedPolicies, excludedPolicies);
+ },
+ [onSearch, includedPolicies, excludedPolicies]
+ );
+
+ return (
+
+
+
+
+ {hasPolicyFilter && policyList ? (
+
+
+
+ ) : null}
+
+
+
+ {i18n.translate('xpack.securitySolution.management.search.button', {
+ defaultMessage: 'Refresh',
+ })}
+
+
+
+ );
+ }
+);
+
+SearchExceptions.displayName = 'SearchExceptions';
diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
index 95f3e856a6ff6d..3c537320bc92af 100644
--- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx
@@ -42,7 +42,7 @@ import {
} from '../../../../common/components/exceptions/viewer/exception_item';
import { EventFilterDeleteModal } from './components/event_filter_delete_modal';
-import { SearchBar } from '../../../components/search_bar';
+import { SearchExceptions } from '../../../components/search_exceptions';
import { BackToExternalAppButton } from '../../../components/back_to_external_app_button';
import { ABOUT_EVENT_FILTERS } from './translations';
@@ -226,7 +226,7 @@ export const EventFiltersListPage = memo(() => {
{doesDataExist && (
<>
- ({
id: undefined,
view_type: 'grid',
filter: '',
+ included_policies: '',
+ excluded_policies: '',
},
active: false,
forceRefresh: false,
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts
index 9624987c8af567..f64003ec6ad910 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts
@@ -26,6 +26,7 @@ import { initialTrustedAppsPageState } from './builders';
import { trustedAppsPageReducer } from './reducer';
import { createTrustedAppsPageMiddleware } from './middleware';
import { Immutable } from '../../../../../common/endpoint/types';
+import { getGeneratedPolicyResponse } from './mocks';
const initialNow = 111111;
const dateNowMock = jest.fn();
@@ -189,8 +190,10 @@ describe('middleware', () => {
const location = createLocationState();
const service = createTrustedAppsServiceMock();
const { store, spyMiddleware } = createStoreSetup(service);
+ const policiesResponse = getGeneratedPolicyResponse();
service.getTrustedAppsList.mockResolvedValue(createGetTrustedListAppsResponse(pagination));
+ service.getPolicyList.mockResolvedValue(policiesResponse);
store.dispatch(createUserChangedUrlAction('/administration/trusted_apps'));
@@ -215,10 +218,15 @@ describe('middleware', () => {
});
await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged');
+ await spyMiddleware.waitForAction('trustedAppsPoliciesStateChanged');
expect(store.getState()).toStrictEqual({
...initialState,
...entriesExistLoadedState(),
+ policies: {
+ data: policiesResponse,
+ type: 'LoadedResourceState',
+ },
listView: createLoadedListViewWithPagination(newNow, pagination),
active: true,
location,
@@ -304,9 +312,11 @@ describe('middleware', () => {
it('submits successfully when entry is defined', async () => {
const service = createTrustedAppsServiceMock();
const { store, spyMiddleware } = createStoreSetup(service);
+ const policiesResponse = getGeneratedPolicyResponse();
service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse);
service.deleteTrustedApp.mockResolvedValue();
+ service.getPolicyList.mockResolvedValue(policiesResponse);
store.dispatch(createUserChangedUrlAction('/administration/trusted_apps'));
@@ -335,6 +345,10 @@ describe('middleware', () => {
expect(store.getState()).toStrictEqual({
...testStartState,
...entriesExistLoadedState(),
+ policies: {
+ data: policiesResponse,
+ type: 'LoadedResourceState',
+ },
listView: listViewNew,
});
expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' });
@@ -344,9 +358,11 @@ describe('middleware', () => {
it('does not submit twice', async () => {
const service = createTrustedAppsServiceMock();
const { store, spyMiddleware } = createStoreSetup(service);
+ const policiesResponse = getGeneratedPolicyResponse();
service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse);
service.deleteTrustedApp.mockResolvedValue();
+ service.getPolicyList.mockResolvedValue(policiesResponse);
store.dispatch(createUserChangedUrlAction('/administration/trusted_apps'));
@@ -376,6 +392,10 @@ describe('middleware', () => {
expect(store.getState()).toStrictEqual({
...testStartState,
...entriesExistLoadedState(),
+ policies: {
+ data: policiesResponse,
+ type: 'LoadedResourceState',
+ },
listView: listViewNew,
});
expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' });
@@ -385,9 +405,11 @@ describe('middleware', () => {
it('does not submit when server response with failure', async () => {
const service = createTrustedAppsServiceMock();
const { store, spyMiddleware } = createStoreSetup(service);
+ const policiesResponse = getGeneratedPolicyResponse();
service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse);
service.deleteTrustedApp.mockRejectedValue({ body: notFoundError });
+ service.getPolicyList.mockResolvedValue(policiesResponse);
store.dispatch(createUserChangedUrlAction('/administration/trusted_apps'));
@@ -409,10 +431,15 @@ describe('middleware', () => {
});
await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged');
+ await spyMiddleware.waitForAction('trustedAppsPoliciesStateChanged');
expect(store.getState()).toStrictEqual({
...testStartState,
...entriesExistLoadedState(),
+ policies: {
+ data: policiesResponse,
+ type: 'LoadedResourceState',
+ },
deletionDialog: {
entry,
confirmed: true,
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts
index da6394a9ab8969..cf7cff30d6d6d1 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts
@@ -26,6 +26,7 @@ import {
isLoadedResourceState,
isLoadingResourceState,
isStaleResourceState,
+ isUninitialisedResourceState,
StaleResourceState,
TrustedAppsListData,
TrustedAppsListPageState,
@@ -63,8 +64,10 @@ import {
editingTrustedApp,
getListItems,
editItemState,
+ getCurrentLocationIncludedPolicies,
+ getCurrentLocationExcludedPolicies,
} from './selectors';
-import { parseQueryFilterToKQL } from '../../../common/utils';
+import { parsePoliciesToKQL, parseQueryFilterToKQL } from '../../../common/utils';
import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app';
import { SEARCHABLE_FIELDS } from '../constants';
@@ -95,11 +98,21 @@ const refreshListIfNeeded = async (
const pageIndex = getCurrentLocationPageIndex(store.getState());
const pageSize = getCurrentLocationPageSize(store.getState());
const filter = getCurrentLocationFilter(store.getState());
+ const includedPolicies = getCurrentLocationIncludedPolicies(store.getState());
+ const excludedPolicies = getCurrentLocationExcludedPolicies(store.getState());
+
+ const kuery = [];
+
+ const filterKuery = parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined;
+ if (filterKuery) kuery.push(filterKuery);
+
+ const policiesKuery = parsePoliciesToKQL(includedPolicies, excludedPolicies) || undefined;
+ if (policiesKuery) kuery.push(policiesKuery);
const response = await trustedAppsService.getTrustedAppsList({
page: pageIndex + 1,
per_page: pageSize,
- kuery: parseQueryFilterToKQL(filter, SEARCHABLE_FIELDS) || undefined,
+ kuery: kuery.join(' AND ') || undefined,
});
store.dispatch(
@@ -112,6 +125,8 @@ const refreshListIfNeeded = async (
totalItemsCount: response.total,
timestamp: Date.now(),
filter,
+ includedPolicies,
+ excludedPolicies,
},
})
);
@@ -311,8 +326,9 @@ export const retrieveListOfPoliciesIfNeeded = async (
const isLoading = isLoadingResourceState(currentPoliciesState);
const isPageActive = trustedAppsListPageActive(currentState);
const isCreateFlow = isCreationDialogLocation(currentState);
+ const isUninitialized = isUninitialisedResourceState(currentPoliciesState);
- if (isPageActive && isCreateFlow && !isLoading) {
+ if (isPageActive && ((isCreateFlow && !isLoading) || isUninitialized)) {
dispatch({
type: 'trustedAppsPoliciesStateChanged',
payload: {
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts
new file mode 100644
index 00000000000000..c97dd37db6bbf0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/mocks.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { GetPolicyListResponse } from '../../policy/types';
+
+import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
+
+export const getGeneratedPolicyResponse = (): GetPolicyListResponse => ({
+ items: [new EndpointDocGenerator('seed').generatePolicyPackagePolicy()],
+ total: 1,
+ perPage: 1,
+ page: 1,
+});
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts
index ac4d29a6016b2a..71bea620306760 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts
@@ -31,7 +31,7 @@ describe('reducer', () => {
initialState,
createUserChangedUrlAction(
'/administration/trusted_apps',
- '?page_index=5&page_size=50&show=create&view_type=list&filter=test'
+ '?page_index=5&page_size=50&show=create&view_type=list&filter=test&included_policies=global&excluded_policies=unassigned'
)
);
@@ -44,6 +44,8 @@ describe('reducer', () => {
view_type: 'list',
id: undefined,
filter: 'test',
+ included_policies: 'global',
+ excluded_policies: 'unassigned',
},
active: true,
});
@@ -53,7 +55,14 @@ describe('reducer', () => {
const result = trustedAppsPageReducer(
{
...initialState,
- location: { page_index: 5, page_size: 50, view_type: 'grid', filter: '' },
+ location: {
+ page_index: 5,
+ page_size: 50,
+ view_type: 'grid',
+ filter: '',
+ included_policies: '',
+ excluded_policies: '',
+ },
},
createUserChangedUrlAction(
'/administration/trusted_apps',
@@ -68,7 +77,14 @@ describe('reducer', () => {
const result = trustedAppsPageReducer(
{
...initialState,
- location: { page_index: 5, page_size: 50, view_type: 'grid', filter: '' },
+ location: {
+ page_index: 5,
+ page_size: 50,
+ view_type: 'grid',
+ filter: '',
+ included_policies: '',
+ excluded_policies: '',
+ },
},
createUserChangedUrlAction('/administration/trusted_apps')
);
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts
index 4d8ce097a72633..387a354fe38c4a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts
@@ -94,6 +94,8 @@ describe('selectors', () => {
page_size: 10,
view_type: 'grid',
filter: '',
+ included_policies: '',
+ excluded_policies: '',
};
expect(needsRefreshOfListData({ ...initialState, listView, active: true, location })).toBe(
@@ -176,6 +178,8 @@ describe('selectors', () => {
page_size: 10,
view_type: 'grid',
filter: '',
+ included_policies: '',
+ excluded_policies: '',
};
expect(getCurrentLocationPageIndex({ ...initialState, location })).toBe(3);
@@ -189,6 +193,8 @@ describe('selectors', () => {
page_size: 20,
view_type: 'grid',
filter: '',
+ included_policies: '',
+ excluded_policies: '',
};
expect(getCurrentLocationPageSize({ ...initialState, location })).toBe(20);
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts
index 338f30b447a8a7..388d8b5e6ed66d 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts
@@ -74,6 +74,18 @@ export const getCurrentLocationFilter = (state: Immutable
+): string => {
+ return state.location.included_policies;
+};
+
+export const getCurrentLocationExcludedPolicies = (
+ state: Immutable
+): string => {
+ return state.location.excluded_policies;
+};
+
export const getListTotalItemsCount = (state: Immutable): number => {
return getLastLoadedResourceState(state.listView.listResourceState)?.data.totalItemsCount || 0;
};
@@ -188,6 +200,14 @@ export const entriesExist: (state: Immutable) => boole
}
);
+export const prevEntriesExist: (
+ state: Immutable
+) => boolean = createSelector(entriesExistState, (doEntriesExists) => {
+ return (
+ isLoadingResourceState(doEntriesExists) && !!getLastLoadedResourceState(doEntriesExists)?.data
+ );
+});
+
export const trustedAppsListPageActive: (state: Immutable) => boolean = (
state
) => state.active;
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts
index 7783ac96e192d4..cad8f606f85bba 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts
@@ -80,6 +80,8 @@ export const createTrustedAppsListData = (
totalItemsCount: fullPagination.totalItemCount,
timestamp,
filter: '',
+ excludedPolicies: '',
+ includedPolicies: '',
};
};
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx
index 5c627d1d7a8373..2ba357a349b5db 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx
@@ -173,9 +173,9 @@ describe('When on the Trusted Apps Page', () => {
expect(addButton.textContent).toBe('Add Trusted Application');
});
- it('should display the searchbar', async () => {
+ it('should display the searchExceptions', async () => {
const renderResult = await renderWithListData();
- expect(await renderResult.findByTestId('searchBar')).not.toBeNull();
+ expect(await renderResult.findByTestId('searchExceptions')).not.toBeNull();
});
describe('and the Grid view is being displayed', () => {
@@ -774,6 +774,20 @@ describe('When on the Trusted Apps Page', () => {
return releaseListResponse();
}
}
+
+ if (path === PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) {
+ const policy = generator.generatePolicyPackagePolicy();
+ policy.name = 'test policy A';
+ policy.id = 'abc123';
+
+ const response: GetPackagePoliciesResponse = {
+ items: [policy],
+ page: 1,
+ perPage: 1000,
+ total: 1,
+ };
+ return response;
+ }
if (priorMockImplementation) {
return priorMockImplementation(path);
}
@@ -874,12 +888,12 @@ describe('When on the Trusted Apps Page', () => {
expect(await renderResult.findByTestId('trustedAppEmptyState')).not.toBeNull();
});
- it('should not display the searchbar', async () => {
+ it('should not display the searchExceptions', async () => {
const renderResult = render();
await act(async () => {
await waitForAction('trustedAppsExistStateChanged');
});
- expect(renderResult.queryByTestId('searchBar')).toBeNull();
+ expect(renderResult.queryByTestId('searchExceptions')).toBeNull();
});
});
@@ -922,6 +936,27 @@ describe('When on the Trusted Apps Page', () => {
backButtonUrl: '/fleet',
});
});
+
+ const priorMockImplementation = coreStart.http.get.getMockImplementation();
+ // @ts-ignore
+ coreStart.http.get.mockImplementation((path, options) => {
+ if (path === PACKAGE_POLICY_API_ROUTES.LIST_PATTERN) {
+ const policy = generator.generatePolicyPackagePolicy();
+ policy.name = 'test policy A';
+ policy.id = 'abc123';
+
+ const response: GetPackagePoliciesResponse = {
+ items: [policy],
+ page: 1,
+ perPage: 1000,
+ total: 1,
+ };
+ return response;
+ }
+ if (priorMockImplementation) {
+ return priorMockImplementation(path);
+ }
+ });
});
it('back button is present', () => {
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx
index ec80b4c5ae21b5..48ff54a0e3b56f 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx
@@ -26,6 +26,8 @@ import {
entriesExist,
getCurrentLocation,
getListTotalItemsCount,
+ listOfPolicies,
+ prevEntriesExist,
} from '../store/selectors';
import { useTrustedAppsNavigateCallback, useTrustedAppsSelector } from './hooks';
import { AdministrationListPage } from '../../../components/administration_list_page';
@@ -38,18 +40,31 @@ import { TrustedAppsNotifications } from './trusted_apps_notifications';
import { AppAction } from '../../../../common/store/actions';
import { ABOUT_TRUSTED_APPS, SEARCH_TRUSTED_APP_PLACEHOLDER } from './translations';
import { EmptyState } from './components/empty_state';
-import { SearchBar } from '../../../components/search_bar';
+import { SearchExceptions } from '../../../components/search_exceptions';
import { BackToExternalAppButton } from '../../../components/back_to_external_app_button';
import { ListPageRouteState } from '../../../../../common/endpoint/types';
+import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
export const TrustedAppsPage = memo(() => {
+ const isTrustedAppsByPolicyEnabled = useIsExperimentalFeatureEnabled(
+ 'trustedAppsByPolicyEnabled'
+ );
const dispatch = useDispatch>();
const { state: routeState } = useLocation();
const location = useTrustedAppsSelector(getCurrentLocation);
const totalItemsCount = useTrustedAppsSelector(getListTotalItemsCount);
const isCheckingIfEntriesExists = useTrustedAppsSelector(checkingIfEntriesExist);
- const doEntriesExist = useTrustedAppsSelector(entriesExist) === true;
- const navigationCallback = useTrustedAppsNavigateCallback((query: string) => ({ filter: query }));
+ const policyList = useTrustedAppsSelector(listOfPolicies);
+ const doEntriesExist = useTrustedAppsSelector(entriesExist);
+ const didEntriesExist = useTrustedAppsSelector(prevEntriesExist);
+ const navigationCallbackQuery = useTrustedAppsNavigateCallback(
+ (query: string, includedPolicies?: string, excludedPolicies?: string) => ({
+ filter: query,
+ included_policies: includedPolicies,
+ excluded_policies: excludedPolicies,
+ })
+ );
+
const handleAddButtonClick = useTrustedAppsNavigateCallback(() => ({
show: 'create',
id: undefined,
@@ -61,12 +76,13 @@ export const TrustedAppsPage = memo(() => {
const handleViewTypeChange = useTrustedAppsNavigateCallback((viewType: ViewType) => ({
view_type: viewType,
}));
+
const handleOnSearch = useCallback(
- (query: string) => {
+ (query: string, includedPolicies?: string, excludedPolicies?: string) => {
dispatch({ type: 'trustedAppForceRefresh', payload: { forceRefresh: true } });
- navigationCallback(query);
+ navigationCallbackQuery(query, includedPolicies, excludedPolicies);
},
- [dispatch, navigationCallback]
+ [dispatch, navigationCallbackQuery]
);
const showCreateFlyout = !!location.show;
@@ -105,12 +121,16 @@ export const TrustedAppsPage = memo(() => {
/>
)}
- {doEntriesExist ? (
+ {doEntriesExist || (isCheckingIfEntriesExists && didEntriesExist) ? (
<>
-
{
>
- {isCheckingIfEntriesExists ? (
+ {isCheckingIfEntriesExists && !didEntriesExist ? (
}
diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx
index 4c738958e9be87..63a4571ca11e52 100644
--- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx
@@ -32,7 +32,12 @@ jest.mock('../../../common/lib/kibana', () => {
embeddable: {
EmbeddablePanel: jest.fn(() => ),
},
- docLinks: { ELASTIC_WEBSITE_URL: 'ELASTIC_WEBSITE_URL' },
+ docLinks: {
+ ELASTIC_WEBSITE_URL: 'ELASTIC_WEBSITE_URL',
+ links: {
+ siem: { networkMap: '' },
+ },
+ },
},
}),
};
diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx
index 6e36b85e3819ca..f2cb974a5b5c1a 100644
--- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx
@@ -221,10 +221,7 @@ export const EmbeddedMapComponent = ({
-
+
{i18n.EMBEDDABLE_HEADER_HELP}
diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx
index 43e57e519e2388..b0e2e6cd7f1862 100644
--- a/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/embeddables/index_patterns_missing_prompt.tsx
@@ -39,7 +39,7 @@ export const IndexPatternsMissingPromptComponent = () => {
),
beats: (
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_dapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_adapter.ts
similarity index 76%
rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_dapter.ts
rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_adapter.ts
index 3f56b26d32a097..90574528a9338d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_dapter.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/rule_registry_adapter.ts
@@ -6,17 +6,18 @@
*/
import { merge } from 'lodash';
-import { RuleDataPluginService } from '../../../../../../rule_registry/server';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
-import { IRuleStatusSOAttributes } from '../../rules/types';
import { RuleRegistryLogClient } from '../rule_registry_log_client/rule_registry_log_client';
import {
+ CreateExecutionLogArgs,
ExecutionMetric,
ExecutionMetricArgs,
FindBulkExecutionLogArgs,
FindExecutionLogArgs,
+ IRuleDataPluginService,
IRuleExecutionLogClient,
LogStatusChangeArgs,
+ UpdateExecutionLogArgs,
} from '../types';
/**
@@ -25,7 +26,7 @@ import {
export class RuleRegistryAdapter implements IRuleExecutionLogClient {
private ruleRegistryClient: RuleRegistryLogClient;
- constructor(ruleDataService: RuleDataPluginService) {
+ constructor(ruleDataService: IRuleDataPluginService) {
this.ruleRegistryClient = new RuleRegistryLogClient(ruleDataService);
}
@@ -58,37 +59,45 @@ export class RuleRegistryAdapter implements IRuleExecutionLogClient {
return merge(statusesById, lastErrorsById);
}
- public async create(event: IRuleStatusSOAttributes, spaceId: string) {
- if (event.status) {
+ public async create({ attributes, spaceId }: CreateExecutionLogArgs) {
+ if (attributes.status) {
await this.ruleRegistryClient.logStatusChange({
- ruleId: event.alertId,
- newStatus: event.status,
+ ruleId: attributes.alertId,
+ newStatus: attributes.status,
spaceId,
});
}
- if (event.bulkCreateTimeDurations) {
+ if (attributes.bulkCreateTimeDurations) {
await this.ruleRegistryClient.logExecutionMetric({
- ruleId: event.alertId,
+ ruleId: attributes.alertId,
metric: ExecutionMetric.indexingDurationMax,
- value: Math.max(...event.bulkCreateTimeDurations.map(Number)),
+ value: Math.max(...attributes.bulkCreateTimeDurations.map(Number)),
spaceId,
});
}
- if (event.gap) {
+ if (attributes.gap) {
await this.ruleRegistryClient.logExecutionMetric({
- ruleId: event.alertId,
+ ruleId: attributes.alertId,
metric: ExecutionMetric.executionGap,
- value: Number(event.gap),
+ value: Number(attributes.gap),
spaceId,
});
}
+
+ return {
+ id: '',
+ type: '',
+ score: 0,
+ attributes,
+ references: [],
+ };
}
- public async update(id: string, event: IRuleStatusSOAttributes, spaceId: string) {
+ public async update({ attributes, spaceId }: UpdateExecutionLogArgs) {
// execution events are immutable, so we just use 'create' here instead of 'update'
- await this.create(event, spaceId);
+ await this.create({ attributes, spaceId });
}
public async delete(id: string) {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/saved_objects_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/saved_objects_adapter.ts
index 55f65caf34b037..444e11dc5b9f09 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/saved_objects_adapter.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/adapters/saved_objects_adapter.ts
@@ -6,18 +6,19 @@
*/
import { SavedObjectsClientContract } from '../../../../../../../../src/core/server';
-import { IRuleStatusSOAttributes } from '../../rules/types';
import {
RuleStatusSavedObjectsClient,
ruleStatusSavedObjectsClientFactory,
} from '../../signals/rule_status_saved_objects_client';
import {
+ CreateExecutionLogArgs,
ExecutionMetric,
ExecutionMetricArgs,
FindBulkExecutionLogArgs,
FindExecutionLogArgs,
IRuleExecutionLogClient,
LogStatusChangeArgs,
+ UpdateExecutionLogArgs,
} from '../types';
export class SavedObjectsAdapter implements IRuleExecutionLogClient {
@@ -41,12 +42,12 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient {
return this.ruleStatusClient.findBulk(ruleIds, logsCount);
}
- public async create(event: IRuleStatusSOAttributes) {
- await this.ruleStatusClient.create(event);
+ public async create({ attributes }: CreateExecutionLogArgs) {
+ return this.ruleStatusClient.create(attributes);
}
- public async update(id: string, event: IRuleStatusSOAttributes) {
- await this.ruleStatusClient.update(id, event);
+ public async update({ id, attributes }: UpdateExecutionLogArgs) {
+ await this.ruleStatusClient.update(id, attributes);
}
public async delete(id: string) {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts
index 286238b292cb71..26b36c367bda68 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts
@@ -6,21 +6,22 @@
*/
import { SavedObjectsClientContract } from '../../../../../../../src/core/server';
-import { RuleDataPluginService } from '../../../../../rule_registry/server';
-import { IRuleStatusSOAttributes } from '../rules/types';
-import { RuleRegistryAdapter } from './adapters/rule_registry_dapter';
+import { RuleRegistryAdapter } from './adapters/rule_registry_adapter';
import { SavedObjectsAdapter } from './adapters/saved_objects_adapter';
import {
+ CreateExecutionLogArgs,
ExecutionMetric,
ExecutionMetricArgs,
FindBulkExecutionLogArgs,
FindExecutionLogArgs,
+ IRuleDataPluginService,
IRuleExecutionLogClient,
LogStatusChangeArgs,
+ UpdateExecutionLogArgs,
} from './types';
export interface RuleExecutionLogClientArgs {
- ruleDataService: RuleDataPluginService;
+ ruleDataService: IRuleDataPluginService;
savedObjectsClient: SavedObjectsClientContract;
}
@@ -45,14 +46,12 @@ export class RuleExecutionLogClient implements IRuleExecutionLogClient {
return this.client.findBulk(args);
}
- // TODO args as an object
- public async create(event: IRuleStatusSOAttributes, spaceId: string) {
- return this.client.create(event, spaceId);
+ public async create(args: CreateExecutionLogArgs) {
+ return this.client.create(args);
}
- // TODO args as an object
- public async update(id: string, event: IRuleStatusSOAttributes, spaceId: string) {
- return this.client.update(id, event, spaceId);
+ public async update(args: UpdateExecutionLogArgs) {
+ return this.client.update(args);
}
public async delete(id: string) {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_log_bootstrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_log_bootstrapper.ts
index 9b13fbb1d21d66..2c10811b21c48e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_log_bootstrapper.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_log_bootstrapper.ts
@@ -7,14 +7,14 @@
import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../../../../../rule_registry/common/assets';
import { mappingFromFieldMap } from '../../../../../../rule_registry/common/mapping_from_field_map';
-import { RuleDataPluginService } from '../../../../../../rule_registry/server';
+import { IRuleDataPluginService } from '../types';
import { ruleExecutionFieldMap } from './rule_execution_field_map';
/**
* @deprecated bootstrapRuleExecutionLog is kept here only as a reference. It will be superseded with EventLog implementation
*/
export const bootstrapRuleExecutionLog = async (
- ruleDataService: RuleDataPluginService,
+ ruleDataService: IRuleDataPluginService,
indexAlias: string
) => {
const indexPattern = `${indexAlias}*`;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts
index 5094f9a8c6e3ce..e2c5c98702f12b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts
@@ -6,20 +6,32 @@
*/
import { estypes } from '@elastic/elasticsearch';
-import { EVENT_ACTION, EVENT_KIND, RULE_ID, SPACE_IDS, TIMESTAMP } from '@kbn/rule-data-utils';
+import {
+ ALERT_OWNER,
+ EVENT_ACTION,
+ EVENT_KIND,
+ RULE_ID,
+ SPACE_IDS,
+ TIMESTAMP,
+} from '@kbn/rule-data-utils';
import { once } from 'lodash/fp';
import moment from 'moment';
-import { RuleDataClient, RuleDataPluginService } from '../../../../../../rule_registry/server';
+import { RuleDataClient } from '../../../../../../rule_registry/server';
import { SERVER_APP_ID } from '../../../../../common/constants';
import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas';
import { invariant } from '../../../../../common/utils/invariant';
import { IRuleStatusSOAttributes } from '../../rules/types';
import { makeFloatString } from '../../signals/utils';
-import { ExecutionMetric, ExecutionMetricArgs, LogStatusChangeArgs } from '../types';
+import {
+ ExecutionMetric,
+ ExecutionMetricArgs,
+ IRuleDataPluginService,
+ LogStatusChangeArgs,
+} from '../types';
import {
EVENTS_INDEX_PREFIX,
- MESSAGE,
EVENT_SEQUENCE,
+ MESSAGE,
RULE_STATUS,
RULE_STATUS_SEVERITY,
} from './constants';
@@ -65,7 +77,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient {
private sequence = 0;
private ruleDataClient: RuleDataClient;
- constructor(ruleDataService: RuleDataPluginService) {
+ constructor(ruleDataService: IRuleDataPluginService) {
this.ruleDataClient = ruleDataService.getRuleDataClient(
SERVER_APP_ID,
EVENTS_INDEX_PREFIX,
@@ -73,7 +85,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient {
);
}
- private initialize = once(async (ruleDataService: RuleDataPluginService, indexAlias: string) => {
+ private initialize = once(async (ruleDataService: IRuleDataPluginService, indexAlias: string) => {
await bootstrapRuleExecutionLog(ruleDataService, indexAlias);
});
@@ -216,6 +228,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient {
[getMetricField(metric)]: value,
[RULE_ID]: ruleId,
[TIMESTAMP]: new Date().toISOString(),
+ [ALERT_OWNER]: 'siem',
},
namespace
);
@@ -239,6 +252,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient {
[RULE_STATUS_SEVERITY]: statusSeverityDict[newStatus],
[RULE_STATUS]: newStatus,
[TIMESTAMP]: new Date().toISOString(),
+ [ALERT_OWNER]: 'siem',
},
namespace
);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts
index ca589fd1d584fc..42b9a3bbd66cce 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts
@@ -5,7 +5,9 @@
* 2.0.
*/
-import { SavedObjectsFindResult } from '../../../../../../../src/core/server';
+import { PublicMethodsOf } from '@kbn/utility-types';
+import { SavedObject, SavedObjectsFindResult } from '../../../../../../../src/core/server';
+import { RuleDataPluginService } from '../../../../../rule_registry/server';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { IRuleStatusSOAttributes } from '../rules/types';
@@ -16,6 +18,8 @@ export enum ExecutionMetric {
'indexingLookback' = 'indexingLookback',
}
+export type IRuleDataPluginService = PublicMethodsOf;
+
export type ExecutionMetricValue = {
[ExecutionMetric.executionGap]: number;
[ExecutionMetric.searchDurationMax]: number;
@@ -43,6 +47,17 @@ export interface LogStatusChangeArgs {
message?: string;
}
+export interface UpdateExecutionLogArgs {
+ id: string;
+ attributes: IRuleStatusSOAttributes;
+ spaceId: string;
+}
+
+export interface CreateExecutionLogArgs {
+ attributes: IRuleStatusSOAttributes;
+ spaceId: string;
+}
+
export interface ExecutionMetricArgs {
ruleId: string;
spaceId: string;
@@ -60,8 +75,8 @@ export interface IRuleExecutionLogClient {
args: FindExecutionLogArgs
) => Promise>>;
findBulk: (args: FindBulkExecutionLogArgs) => Promise;
- create: (event: IRuleStatusSOAttributes, spaceId: string) => Promise;
- update: (id: string, event: IRuleStatusSOAttributes, spaceId: string) => Promise;
+ create: (args: CreateExecutionLogArgs) => Promise>;
+ update: (args: UpdateExecutionLogArgs) => Promise;
delete: (id: string) => Promise;
// TODO These methods are intended to supersede ones provided by RuleStatusService
logStatusChange: (args: LogStatusChangeArgs) => Promise;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/with_rule_execution_log.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/with_rule_execution_log.ts
index 95598ea943d8f3..a78001ee4f674c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/with_rule_execution_log.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/with_rule_execution_log.ts
@@ -11,10 +11,10 @@ import {
AlertTypeParams,
AlertTypeState,
} from '../../../../../alerting/common';
-import { AlertTypeWithExecutor, RuleDataPluginService } from '../../../../../rule_registry/server';
+import { AlertTypeWithExecutor } from '../../../../../rule_registry/server';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { RuleExecutionLogClient } from './rule_execution_log_client';
-import { IRuleExecutionLogClient } from './types';
+import { IRuleDataPluginService, IRuleExecutionLogClient } from './types';
export interface ExecutionLogServices {
ruleExecutionLogClient: IRuleExecutionLogClient;
@@ -23,7 +23,7 @@ export interface ExecutionLogServices {
type WithRuleExecutionLog = (args: {
logger: Logger;
- ruleDataService: RuleDataPluginService;
+ ruleDataService: IRuleDataPluginService;
}) => <
TState extends AlertTypeState,
TParams extends AlertTypeParams,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts
index 60dcbb3a4e77e7..0fa2bcc270a167 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts
@@ -17,6 +17,7 @@ import { ConfigType } from '../../../../config';
import { AlertAttributes } from '../../signals/types';
import { createRuleMock } from './rule';
import { listMock } from '../../../../../../lists/server/mocks';
+import { ruleRegistryMocks } from '../../../../../../rule_registry/server/mocks';
export const createRuleTypeMocks = () => {
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -85,6 +86,7 @@ export const createRuleTypeMocks = () => {
})),
isWriteEnabled: jest.fn(() => true),
} as unknown) as RuleDataClient,
+ ruleDataService: ruleRegistryMocks.createRuleDataPluginService(),
},
services,
scheduleActions,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts
index 71d922ed543c18..168502d120b8fb 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts
@@ -12,7 +12,6 @@ import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
import { ListArray } from '@kbn/securitysolution-io-ts-list-types';
import { toError } from '@kbn/securitysolution-list-api';
import { createPersistenceRuleTypeFactory } from '../../../../../rule_registry/server';
-import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client';
import { ruleStatusServiceFactory } from '../signals/rule_status_service';
import { buildRuleMessageFactory } from './factories/build_rule_message_factory';
import {
@@ -33,6 +32,7 @@ import {
import { getNotificationResultsLink } from '../notifications/utils';
import { createResultObject } from './utils';
import { bulkCreateFactory, wrapHitsFactory } from './factories';
+import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client';
/* eslint-disable complexity */
export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
@@ -41,6 +41,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
logger,
mergeStrategy,
ruleDataClient,
+ ruleDataService,
}) => (type) => {
const persistenceRuleType = createPersistenceRuleTypeFactory({ ruleDataClient, logger });
return persistenceRuleType({
@@ -62,8 +63,9 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
const esClient = scopedClusterClient.asCurrentUser;
- const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
+ const ruleStatusClient = new RuleExecutionLogClient({ savedObjectsClient, ruleDataService });
const ruleStatusService = await ruleStatusServiceFactory({
+ spaceId,
alertId,
ruleStatusClient,
});
@@ -189,8 +191,10 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ({
);
const wrapHits = wrapHitsFactory({
- ruleSO,
+ logger,
mergeStrategy,
+ ruleSO,
+ spaceId,
});
for (const tuple of tuples) {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts
new file mode 100644
index 00000000000000..f9874478e7a5d3
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts
@@ -0,0 +1,419 @@
+/*
+ * 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 {
+ ALERT_OWNER,
+ ALERT_RULE_NAMESPACE,
+ ALERT_STATUS,
+ ALERT_WORKFLOW_STATUS,
+ SPACE_IDS,
+} from '@kbn/rule-data-utils';
+
+import { sampleDocNoSortIdWithTimestamp } from '../../../signals/__mocks__/es_results';
+import { flattenWithPrefix } from './flatten_with_prefix';
+import {
+ buildAlert,
+ buildParent,
+ buildAncestors,
+ additionalAlertFields,
+ removeClashes,
+} from './build_alert';
+import { Ancestor, SignalSourceHit } from '../../../signals/types';
+import {
+ getRulesSchemaMock,
+ ANCHOR_DATE,
+} from '../../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
+import { getListArrayMock } from '../../../../../../common/detection_engine/schemas/types/lists.mock';
+import {
+ ALERT_ANCESTORS,
+ ALERT_ORIGINAL_EVENT,
+ ALERT_ORIGINAL_TIME,
+} from '../../field_maps/field_names';
+import { SERVER_APP_ID } from '../../../../../../common/constants';
+
+type SignalDoc = SignalSourceHit & {
+ _source: Required['_source'] & { '@timestamp': string };
+};
+
+const SPACE_ID = 'space';
+
+describe('buildAlert', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('it builds an alert as expected without original_event if event does not exist', () => {
+ const doc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ delete doc._source.event;
+ const rule = getRulesSchemaMock();
+ const alert = {
+ ...buildAlert([doc], rule, SPACE_ID),
+ ...additionalAlertFields(doc),
+ };
+ const timestamp = alert['@timestamp'];
+ const expected = {
+ '@timestamp': timestamp,
+ [SPACE_IDS]: [SPACE_ID],
+ [ALERT_OWNER]: SERVER_APP_ID,
+ [ALERT_ANCESTORS]: [
+ {
+ id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
+ [ALERT_ORIGINAL_TIME]: '2020-04-20T21:27:45.000Z',
+ [ALERT_STATUS]: 'open',
+ [ALERT_WORKFLOW_STATUS]: 'open',
+ ...flattenWithPrefix(ALERT_RULE_NAMESPACE, {
+ author: [],
+ id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9',
+ created_at: new Date(ANCHOR_DATE).toISOString(),
+ updated_at: new Date(ANCHOR_DATE).toISOString(),
+ created_by: 'elastic',
+ description: 'some description',
+ enabled: true,
+ false_positives: ['false positive 1', 'false positive 2'],
+ from: 'now-6m',
+ immutable: false,
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ references: ['test 1', 'test 2'],
+ severity: 'high',
+ severity_mapping: [],
+ updated_by: 'elastic_kibana',
+ tags: ['some fake tag 1', 'some fake tag 2'],
+ to: 'now',
+ type: 'query',
+ threat: [],
+ version: 1,
+ status: 'succeeded',
+ status_date: '2020-02-22T16:47:50.047Z',
+ last_success_at: '2020-02-22T16:47:50.047Z',
+ last_success_message: 'succeeded',
+ output_index: '.siem-signals-default',
+ max_signals: 100,
+ risk_score: 55,
+ risk_score_mapping: [],
+ language: 'kuery',
+ rule_id: 'query-rule-id',
+ interval: '5m',
+ exceptions_list: getListArrayMock(),
+ }),
+ 'kibana.alert.depth': 1,
+ };
+ expect(alert).toEqual(expected);
+ });
+
+ test('it builds an alert as expected with original_event if is present', () => {
+ const doc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ doc._source.event = {
+ action: 'socket_opened',
+ dataset: 'socket',
+ kind: 'event',
+ module: 'system',
+ };
+ const rule = getRulesSchemaMock();
+ const alert = {
+ ...buildAlert([doc], rule, SPACE_ID),
+ ...additionalAlertFields(doc),
+ };
+ const timestamp = alert['@timestamp'];
+ const expected = {
+ '@timestamp': timestamp,
+ [SPACE_IDS]: [SPACE_ID],
+ [ALERT_OWNER]: SERVER_APP_ID,
+ [ALERT_ANCESTORS]: [
+ {
+ id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
+ [ALERT_ORIGINAL_TIME]: '2020-04-20T21:27:45.000Z',
+ [ALERT_ORIGINAL_EVENT]: {
+ action: 'socket_opened',
+ dataset: 'socket',
+ kind: 'event',
+ module: 'system',
+ },
+ [ALERT_STATUS]: 'open',
+ [ALERT_WORKFLOW_STATUS]: 'open',
+ ...flattenWithPrefix(ALERT_RULE_NAMESPACE, {
+ author: [],
+ id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9',
+ created_at: new Date(ANCHOR_DATE).toISOString(),
+ updated_at: new Date(ANCHOR_DATE).toISOString(),
+ created_by: 'elastic',
+ description: 'some description',
+ enabled: true,
+ false_positives: ['false positive 1', 'false positive 2'],
+ from: 'now-6m',
+ immutable: false,
+ name: 'Query with a rule id',
+ query: 'user.name: root or user.name: admin',
+ references: ['test 1', 'test 2'],
+ severity: 'high',
+ severity_mapping: [],
+ updated_by: 'elastic_kibana',
+ tags: ['some fake tag 1', 'some fake tag 2'],
+ to: 'now',
+ type: 'query',
+ threat: [],
+ version: 1,
+ status: 'succeeded',
+ status_date: '2020-02-22T16:47:50.047Z',
+ last_success_at: '2020-02-22T16:47:50.047Z',
+ last_success_message: 'succeeded',
+ output_index: '.siem-signals-default',
+ max_signals: 100,
+ risk_score: 55,
+ risk_score_mapping: [],
+ language: 'kuery',
+ rule_id: 'query-rule-id',
+ interval: '5m',
+ exceptions_list: getListArrayMock(),
+ }),
+ 'kibana.alert.depth': 1,
+ };
+ expect(alert).toEqual(expected);
+ });
+
+ test('it builds an ancestor correctly if the parent does not exist', () => {
+ const doc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ doc._source.event = {
+ action: 'socket_opened',
+ dataset: 'socket',
+ kind: 'event',
+ module: 'system',
+ };
+ const parent = buildParent(doc);
+ const expected: Ancestor = {
+ id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ };
+ expect(parent).toEqual(expected);
+ });
+
+ test('it builds an ancestor correctly if the parent does exist', () => {
+ const doc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ doc._source.event = {
+ action: 'socket_opened',
+ dataset: 'socket',
+ kind: 'event',
+ module: 'system',
+ };
+ doc._source.signal = {
+ parents: [
+ {
+ id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
+ ancestors: [
+ {
+ id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
+ depth: 1,
+ rule: {
+ id: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
+ },
+ };
+ const parent = buildParent(doc);
+ const expected: Ancestor = {
+ rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
+ id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
+ type: 'signal',
+ index: 'myFakeSignalIndex',
+ depth: 1,
+ };
+ expect(parent).toEqual(expected);
+ });
+
+ test('it builds an alert ancestor correctly if the parent does not exist', () => {
+ const sampleDoc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ const doc: SignalDoc = {
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ '@timestamp': new Date().toISOString(),
+ },
+ };
+ doc._source.event = {
+ action: 'socket_opened',
+ dataset: 'socket',
+ kind: 'event',
+ module: 'system',
+ };
+ const ancestor = buildAncestors(doc);
+ const expected: Ancestor[] = [
+ {
+ id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ];
+ expect(ancestor).toEqual(expected);
+ });
+
+ test('it builds an alert ancestor correctly if the parent does exist', () => {
+ const sampleDoc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ const doc: SignalDoc = {
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ '@timestamp': new Date().toISOString(),
+ },
+ };
+ doc._source.event = {
+ action: 'socket_opened',
+ dataset: 'socket',
+ kind: 'event',
+ module: 'system',
+ };
+ doc._source.signal = {
+ parents: [
+ {
+ id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
+ ancestors: [
+ {
+ id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ ],
+ rule: {
+ id: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
+ },
+ depth: 1,
+ };
+ const ancestors = buildAncestors(doc);
+ const expected: Ancestor[] = [
+ {
+ id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87',
+ type: 'event',
+ index: 'myFakeSignalIndex',
+ depth: 0,
+ },
+ {
+ rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b',
+ id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71',
+ type: 'signal',
+ index: 'myFakeSignalIndex',
+ depth: 1,
+ },
+ ];
+ expect(ancestors).toEqual(expected);
+ });
+
+ describe('removeClashes', () => {
+ test('it will call renameClashes with a regular doc and not mutate it if it does not have a signal clash', () => {
+ const sampleDoc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ const doc: SignalDoc = {
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ '@timestamp': new Date().toISOString(),
+ },
+ };
+ const output = removeClashes(doc);
+ expect(output).toBe(doc); // reference check
+ });
+
+ test('it will call renameClashes with a regular doc and not change anything', () => {
+ const sampleDoc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ const doc: SignalDoc = {
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ '@timestamp': new Date().toISOString(),
+ },
+ };
+ const output = removeClashes(doc);
+ expect(output).toEqual(doc); // deep equal check
+ });
+
+ test('it will remove a "signal" numeric clash', () => {
+ const sampleDoc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ const doc = ({
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ signal: 127,
+ },
+ } as unknown) as SignalDoc;
+ const output = removeClashes(doc);
+ const timestamp = output._source['@timestamp'];
+ expect(output).toEqual({
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ '@timestamp': timestamp,
+ },
+ });
+ });
+
+ test('it will remove a "signal" object clash', () => {
+ const sampleDoc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ const doc = ({
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ signal: { child_1: { child_2: 'Test nesting' } },
+ },
+ } as unknown) as SignalDoc;
+ const output = removeClashes(doc);
+ const timestamp = output._source['@timestamp'];
+ expect(output).toEqual({
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ '@timestamp': timestamp,
+ },
+ });
+ });
+
+ test('it will not remove a "signal" if that is signal is one of our signals', () => {
+ const sampleDoc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71');
+ const doc = ({
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ signal: { rule: { id: '123' } },
+ },
+ } as unknown) as SignalDoc;
+ const output = removeClashes(doc);
+ const timestamp = output._source['@timestamp'];
+ const expected = {
+ ...sampleDoc,
+ _source: {
+ ...sampleDoc._source,
+ signal: { rule: { id: '123' } },
+ '@timestamp': timestamp,
+ },
+ };
+ expect(output).toEqual(expected);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts
index fbd033a7f4ec48..641b37cb54bc40 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts
@@ -5,124 +5,113 @@
* 2.0.
*/
-import { ALERT_STATUS, ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils';
-import { SearchTypes } from '../../../../../../common/detection_engine/types';
+import {
+ ALERT_OWNER,
+ ALERT_RULE_NAMESPACE,
+ ALERT_STATUS,
+ ALERT_WORKFLOW_STATUS,
+ SPACE_IDS,
+} from '@kbn/rule-data-utils';
import { RulesSchema } from '../../../../../../common/detection_engine/schemas/response/rules_schema';
import { isEventTypeSignal } from '../../../signals/build_event_type_signal';
+import { Ancestor, BaseSignalHit, SimpleHit } from '../../../signals/types';
import {
- Ancestor,
- BaseSignalHit,
- SignalHit,
- SignalSourceHit,
- ThresholdResult,
-} from '../../../signals/types';
-import { getValidDateFromDoc } from '../../../signals/utils';
+ getField,
+ getValidDateFromDoc,
+ isWrappedRACAlert,
+ isWrappedSignalHit,
+} from '../../../signals/utils';
import { invariant } from '../../../../../../common/utils/invariant';
-import { DEFAULT_MAX_SIGNALS } from '../../../../../../common/constants';
+import { RACAlert } from '../../types';
+import { flattenWithPrefix } from './flatten_with_prefix';
+import {
+ ALERT_ANCESTORS,
+ ALERT_DEPTH,
+ ALERT_ORIGINAL_EVENT,
+ ALERT_ORIGINAL_TIME,
+} from '../../field_maps/field_names';
+import { SERVER_APP_ID } from '../../../../../../common/constants';
/**
- * Takes a parent signal or event document and extracts the information needed for the corresponding entry in the child
- * signal's `signal.parents` array.
- * @param doc The parent signal or event
+ * Takes an event document and extracts the information needed for the corresponding entry in the child
+ * alert's ancestors array.
+ * @param doc The parent event
*/
-export const buildParent = (doc: BaseSignalHit): Ancestor => {
- if (doc._source?.signal != null) {
- return {
- rule: doc._source?.signal.rule.id,
- id: doc._id,
- type: 'signal',
- index: doc._index,
- // We first look for signal.depth and use that if it exists. If it doesn't exist, this should be a pre-7.10 signal
- // and should have signal.parent.depth instead. signal.parent.depth in this case is treated as equivalent to signal.depth.
- depth: doc._source?.signal.depth ?? doc._source?.signal.parent?.depth ?? 1,
- };
- } else {
- return {
- id: doc._id,
- type: 'event',
- index: doc._index,
- depth: 0,
- };
+export const buildParent = (doc: SimpleHit): Ancestor => {
+ const isSignal: boolean = isWrappedSignalHit(doc) || isWrappedRACAlert(doc);
+ const parent: Ancestor = {
+ id: doc._id,
+ type: isSignal ? 'signal' : 'event',
+ index: doc._index,
+ depth: isSignal ? getField(doc, 'signal.depth') ?? 1 : 0,
+ };
+ if (isSignal) {
+ parent.rule = getField(doc, 'signal.rule.id');
}
+ return parent;
};
/**
- * Takes a parent signal or event document with N ancestors and adds the parent document to the ancestry array,
+ * Takes a parent event document with N ancestors and adds the parent document to the ancestry array,
* creating an array of N+1 ancestors.
- * @param doc The parent signal/event for which to extend the ancestry.
+ * @param doc The parent event for which to extend the ancestry.
*/
-export const buildAncestors = (doc: BaseSignalHit): Ancestor[] => {
+export const buildAncestors = (doc: SimpleHit): Ancestor[] => {
const newAncestor = buildParent(doc);
- const existingAncestors = doc._source?.signal?.ancestors;
- if (existingAncestors != null) {
- return [...existingAncestors, newAncestor];
- } else {
- return [newAncestor];
- }
+ const existingAncestors: Ancestor[] = getField(doc, 'signal.ancestors') ?? [];
+ return [...existingAncestors, newAncestor];
};
/**
- * This removes any signal name clashes such as if a source index has
+ * This removes any alert name clashes such as if a source index has
* "signal" but is not a signal object we put onto the object. If this
* is our "signal object" then we don't want to remove it.
* @param doc The source index doc to a signal.
*/
-export const removeClashes = (doc: BaseSignalHit): BaseSignalHit => {
- invariant(doc._source, '_source field not found');
- const { signal, ...noSignal } = doc._source;
- if (signal == null || isEventTypeSignal(doc)) {
- return doc;
- } else {
- return {
- ...doc,
- _source: { ...noSignal },
- };
+export const removeClashes = (doc: SimpleHit) => {
+ if (isWrappedSignalHit(doc)) {
+ invariant(doc._source, '_source field not found');
+ const { signal, ...noSignal } = doc._source;
+ if (signal == null || isEventTypeSignal(doc)) {
+ return doc;
+ } else {
+ return {
+ ...doc,
+ _source: { ...noSignal },
+ };
+ }
}
+ return doc;
};
/**
- * Builds the `signal.*` fields that are common across all signals.
- * @param docs The parent signals/events of the new signal to be built.
- * @param rule The rule that is generating the new signal.
+ * Builds the `kibana.alert.*` fields that are common across all alerts.
+ * @param docs The parent alerts/events of the new alert to be built.
+ * @param rule The rule that is generating the new alert.
*/
-export const buildAlert = (doc: SignalSourceHit, rule: RulesSchema) => {
- const removedClashes = removeClashes(doc);
- const parent = buildParent(removedClashes);
- const ancestors = buildAncestors(removedClashes);
- const immutable = doc._source?.signal?.rule.immutable ? 'true' : 'false';
-
- const source = doc._source as SignalHit;
- const signal = source?.signal;
- const signalRule = signal?.rule;
+export const buildAlert = (
+ docs: SimpleHit[],
+ rule: RulesSchema,
+ spaceId: string | null | undefined
+): RACAlert => {
+ const removedClashes = docs.map(removeClashes);
+ const parents = removedClashes.map(buildParent);
+ const depth = parents.reduce((acc, parent) => Math.max(parent.depth, acc), 0) + 1;
+ const ancestors = removedClashes.reduce(
+ (acc: Ancestor[], doc) => acc.concat(buildAncestors(doc)),
+ []
+ );
- return {
- 'kibana.alert.ancestors': ancestors as object[],
+ return ({
+ '@timestamp': new Date().toISOString(),
+ [ALERT_OWNER]: SERVER_APP_ID,
+ [SPACE_IDS]: spaceId != null ? [spaceId] : [],
+ [ALERT_ANCESTORS]: ancestors,
[ALERT_STATUS]: 'open',
[ALERT_WORKFLOW_STATUS]: 'open',
- 'kibana.alert.depth': parent.depth,
- 'kibana.alert.rule.false_positives': signalRule?.false_positives ?? [],
- 'kibana.alert.rule.id': rule.id,
- 'kibana.alert.rule.immutable': immutable,
- 'kibana.alert.rule.index': signalRule?.index ?? [],
- 'kibana.alert.rule.language': signalRule?.language ?? 'kuery',
- 'kibana.alert.rule.max_signals': signalRule?.max_signals ?? DEFAULT_MAX_SIGNALS,
- 'kibana.alert.rule.query': signalRule?.query ?? '*:*',
- 'kibana.alert.rule.saved_id': signalRule?.saved_id ?? '',
- 'kibana.alert.rule.threat_index': signalRule?.threat_index,
- 'kibana.alert.rule.threat_indicator_path': signalRule?.threat_indicator_path,
- 'kibana.alert.rule.threat_language': signalRule?.threat_language,
- 'kibana.alert.rule.threat_mapping.field': '', // TODO
- 'kibana.alert.rule.threat_mapping.value': '', // TODO
- 'kibana.alert.rule.threat_mapping.type': '', // TODO
- 'kibana.alert.rule.threshold.field': signalRule?.threshold?.field,
- 'kibana.alert.rule.threshold.value': signalRule?.threshold?.value,
- 'kibana.alert.rule.threshold.cardinality.field': '', // TODO
- 'kibana.alert.rule.threshold.cardinality.value': 0, // TODO
- };
-};
-
-const isThresholdResult = (thresholdResult: SearchTypes): thresholdResult is ThresholdResult => {
- return typeof thresholdResult === 'object';
+ [ALERT_DEPTH]: depth,
+ ...flattenWithPrefix(ALERT_RULE_NAMESPACE, rule),
+ } as unknown) as RACAlert;
};
/**
@@ -131,17 +120,16 @@ const isThresholdResult = (thresholdResult: SearchTypes): thresholdResult is Thr
* @param doc The parent signal/event of the new signal to be built.
*/
export const additionalAlertFields = (doc: BaseSignalHit) => {
- const thresholdResult = doc._source?.threshold_result;
- if (thresholdResult != null && !isThresholdResult(thresholdResult)) {
- throw new Error(`threshold_result failed to validate: ${thresholdResult}`);
- }
const originalTime = getValidDateFromDoc({
doc,
timestampOverride: undefined,
});
- return {
- 'kibana.alert.original_time': originalTime != null ? originalTime.toISOString() : undefined,
- 'kibana.alert.original_event': doc._source?.event ?? undefined,
- 'kibana.alert.threshold_result': thresholdResult,
+ const additionalFields: Record = {
+ [ALERT_ORIGINAL_TIME]: originalTime != null ? originalTime.toISOString() : undefined,
};
+ const event = doc._source?.event;
+ if (event != null) {
+ additionalFields[ALERT_ORIGINAL_EVENT] = event;
+ }
+ return additionalFields;
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts
index 8c868ece26cebf..ca5857e0ee3958 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts
@@ -6,14 +6,21 @@
*/
import { SavedObject } from 'src/core/types';
+import { BaseHit } from '../../../../../../common/detection_engine/types';
import type { ConfigType } from '../../../../../config';
-import { buildRuleWithOverrides } from '../../../signals/build_rule';
+import { buildRuleWithOverrides, buildRuleWithoutOverrides } from '../../../signals/build_rule';
import { getMergeStrategy } from '../../../signals/source_fields_merging/strategies';
-import { AlertAttributes, SignalSourceHit } from '../../../signals/types';
+import { AlertAttributes, SignalSource, SignalSourceHit } from '../../../signals/types';
import { RACAlert } from '../../types';
import { additionalAlertFields, buildAlert } from './build_alert';
import { filterSource } from './filter_source';
+const isSourceDoc = (
+ hit: SignalSourceHit
+): hit is BaseHit<{ '@timestamp': string; _source: SignalSource }> => {
+ return hit._source != null;
+};
+
/**
* Formats the search_after result for insertion into the signals index. We first create a
* "best effort" merged "fields" with the "_source" object, then build the signal object,
@@ -24,17 +31,25 @@ import { filterSource } from './filter_source';
* @returns The body that can be added to a bulk call for inserting the signal.
*/
export const buildBulkBody = (
+ spaceId: string | null | undefined,
ruleSO: SavedObject,
doc: SignalSourceHit,
- mergeStrategy: ConfigType['alertMergeStrategy']
+ mergeStrategy: ConfigType['alertMergeStrategy'],
+ applyOverrides: boolean
): RACAlert => {
const mergedDoc = getMergeStrategy(mergeStrategy)({ doc });
- const rule = buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {});
+ const rule = applyOverrides
+ ? buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {})
+ : buildRuleWithoutOverrides(ruleSO);
const filteredSource = filterSource(mergedDoc);
- return {
- ...filteredSource,
- ...buildAlert(mergedDoc, rule),
- ...additionalAlertFields(mergedDoc),
- '@timestamp': new Date().toISOString(),
- };
+ if (isSourceDoc(mergedDoc)) {
+ return {
+ ...filteredSource,
+ ...buildAlert([mergedDoc], rule, spaceId),
+ ...additionalAlertFields(mergedDoc),
+ '@timestamp': new Date().toISOString(),
+ };
+ }
+
+ throw Error('Error building alert from source document.');
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts
new file mode 100644
index 00000000000000..d472dc5885e57d
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/flatten_with_prefix.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 { SearchTypes } from '../../../../../../common/detection_engine/types';
+
+export const flattenWithPrefix = (
+ prefix: string,
+ obj: Record
+): Record => {
+ return Object.keys(obj).reduce((acc: Record, key) => {
+ return {
+ ...acc,
+ [`${prefix}.${key}`]: obj[key],
+ };
+ }, {});
+};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts
index 620e599e7a4993..0b00b2f656379c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { Logger } from 'kibana/server';
+
import { SearchAfterAndBulkCreateParams, SignalSourceHit, WrapHits } from '../../signals/types';
import { buildBulkBody } from './utils/build_bulk_body';
import { generateId } from '../../signals/utils';
@@ -13,24 +15,33 @@ import type { ConfigType } from '../../../../config';
import { WrappedRACAlert } from '../types';
export const wrapHitsFactory = ({
- ruleSO,
+ logger,
mergeStrategy,
+ ruleSO,
+ spaceId,
}: {
+ logger: Logger;
ruleSO: SearchAfterAndBulkCreateParams['ruleSO'];
mergeStrategy: ConfigType['alertMergeStrategy'];
+ spaceId: string | null | undefined;
}): WrapHits => (events) => {
- const wrappedDocs: WrappedRACAlert[] = events.flatMap((doc) => [
- {
- _index: '',
- _id: generateId(
- doc._index,
- doc._id,
- String(doc._version),
- ruleSO.attributes.params.ruleId ?? ''
- ),
- _source: buildBulkBody(ruleSO, doc as SignalSourceHit, mergeStrategy),
- },
- ]);
+ try {
+ const wrappedDocs: WrappedRACAlert[] = events.flatMap((doc) => [
+ {
+ _index: '',
+ _id: generateId(
+ doc._index,
+ doc._id,
+ String(doc._version),
+ ruleSO.attributes.params.ruleId ?? ''
+ ),
+ _source: buildBulkBody(spaceId, ruleSO, doc as SignalSourceHit, mergeStrategy, true),
+ },
+ ]);
- return filterDuplicateSignals(ruleSO.id, wrappedDocs, true);
+ return filterDuplicateSignals(ruleSO.id, wrappedDocs, true);
+ } catch (error) {
+ logger.error(error);
+ return [];
+ }
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts
index 244905329f8ca2..7ab998fe16074c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts
@@ -43,6 +43,21 @@ export const alertsFieldMap: FieldMap = {
array: false,
required: true,
},
+ 'kibana.alert.group': {
+ type: 'object',
+ array: false,
+ required: false,
+ },
+ 'kibana.alert.group.id': {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
+ 'kibana.alert.group.index': {
+ type: 'keyword',
+ array: false,
+ required: false,
+ },
'kibana.alert.original_event': {
type: 'object',
array: false,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts
new file mode 100644
index 00000000000000..41b7e6b02c9c6d
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/field_names.ts
@@ -0,0 +1,13 @@
+/*
+ * 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 { ALERT_NAMESPACE } from '@kbn/rule-data-utils';
+
+export const ALERT_ANCESTORS = `${ALERT_NAMESPACE}.ancestors`;
+export const ALERT_DEPTH = `${ALERT_NAMESPACE}.depth`;
+export const ALERT_ORIGINAL_EVENT = `${ALERT_NAMESPACE}.original_event`;
+export const ALERT_ORIGINAL_TIME = `${ALERT_NAMESPACE}.original_time`;
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts
index 0127477e4800ac..a5ecc92df8f27b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts
@@ -41,6 +41,7 @@ describe('Custom query alerts', () => {
logger: dependencies.logger,
mergeStrategy: 'allFields',
ruleDataClient: dependencies.ruleDataClient,
+ ruleDataService: dependencies.ruleDataService,
version: '1.0.0',
});
@@ -88,6 +89,7 @@ describe('Custom query alerts', () => {
logger: dependencies.logger,
mergeStrategy: 'allFields',
ruleDataClient: dependencies.ruleDataClient,
+ ruleDataService: dependencies.ruleDataService,
version: '1.0.0',
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts
index 3321597d8268fb..c487d8c93119dd 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts
@@ -7,16 +7,14 @@
import { Logger } from '@kbn/logging';
import { validateNonExact } from '@kbn/securitysolution-io-ts-utils';
-
import { PersistenceServices, RuleDataClient } from '../../../../../../rule_registry/server';
import { QUERY_ALERT_TYPE_ID } from '../../../../../common/constants';
import { ExperimentalFeatures } from '../../../../../common/experimental_features';
import { ConfigType } from '../../../../config';
import { SetupPlugins } from '../../../../plugin';
-
+import { IRuleDataPluginService } from '../../rule_execution_log/types';
import { queryRuleParams, QueryRuleParams } from '../../schemas/rule_schemas';
import { queryExecutor } from '../../signals/executors/query';
-
import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory';
export const createQueryAlertType = (createOptions: {
@@ -27,6 +25,7 @@ export const createQueryAlertType = (createOptions: {
mergeStrategy: ConfigType['alertMergeStrategy'];
ruleDataClient: RuleDataClient;
version: string;
+ ruleDataService: IRuleDataPluginService;
}) => {
const {
experimentalFeatures,
@@ -36,6 +35,7 @@ export const createQueryAlertType = (createOptions: {
mergeStrategy,
ruleDataClient,
version,
+ ruleDataService,
} = createOptions;
const createSecurityRuleType = createSecurityRuleTypeFactory({
indexAlias,
@@ -43,6 +43,7 @@ export const createQueryAlertType = (createOptions: {
logger,
mergeStrategy,
ruleDataClient,
+ ruleDataService,
});
return createSecurityRuleType({
id: QUERY_ALERT_TYPE_ID,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts
index dc4cbba680b256..20fb7213086008 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts
@@ -28,6 +28,7 @@ import {
import { BaseHit } from '../../../../common/detection_engine/types';
import { ConfigType } from '../../../config';
import { SetupPlugins } from '../../../plugin';
+import { IRuleDataPluginService } from '../rule_execution_log/types';
import { RuleParams } from '../schemas/rule_schemas';
import { BuildRuleMessage } from '../signals/rule_messages';
import { AlertAttributes, BulkCreate, WrapHits } from '../signals/types';
@@ -96,6 +97,7 @@ export type CreateSecurityRuleTypeFactory = (options: {
logger: Logger;
mergeStrategy: ConfigType['alertMergeStrategy'];
ruleDataClient: RuleDataClient;
+ ruleDataService: IRuleDataPluginService;
}) => <
TParams extends RuleParams & { index: string[] | undefined },
TAlertInstanceContext extends AlertInstanceContext,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts
index b727596cd3b025..c6a5c003802428 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts
@@ -42,13 +42,13 @@ export const enableRule = async ({
// set current status for this rule to be 'going to run'
if (ruleCurrentStatus && ruleCurrentStatus.length > 0) {
const currentStatusToDisable = ruleCurrentStatus[0];
- await ruleStatusClient.update(
- currentStatusToDisable.id,
- {
+ await ruleStatusClient.update({
+ id: currentStatusToDisable.id,
+ attributes: {
...currentStatusToDisable.attributes,
status: RuleExecutionStatus['going to run'],
},
- spaceId
- );
+ spaceId,
+ });
}
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
index 5f4a9f5f7a422d..ed93c41035dca3 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -168,6 +168,22 @@ export const sampleDocNoSortId = (
sort: [],
});
+export const sampleDocNoSortIdWithTimestamp = (
+ someUuid: string = sampleIdGuid,
+ ip?: string
+): SignalSourceHit & {
+ _source: Required['_source'] & { '@timestamp': string };
+} => {
+ const doc = sampleDocNoSortId(someUuid, ip);
+ return {
+ ...doc,
+ _source: {
+ ...doc._source,
+ '@timestamp': new Date().toISOString(),
+ },
+ };
+};
+
export const sampleDocSeverity = (severity?: unknown, fieldName?: string): SignalSourceHit => {
const doc = {
_index: 'myFakeSignalIndex',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts
index fb562a2d11f0a6..460cf6894a73cb 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_duplicate_signals.ts
@@ -8,37 +8,21 @@
import { WrappedRACAlert } from '../rule_types/types';
import { Ancestor, SimpleHit, WrappedSignalHit } from './types';
-const isWrappedSignalHit = (
- signals: SimpleHit[],
- isRuleRegistryEnabled: boolean
-): signals is WrappedSignalHit[] => {
- return !isRuleRegistryEnabled;
-};
-
-const isWrappedRACAlert = (
- signals: SimpleHit[],
- isRuleRegistryEnabled: boolean
-): signals is WrappedRACAlert[] => {
- return isRuleRegistryEnabled;
-};
-
export const filterDuplicateSignals = (
ruleId: string,
signals: SimpleHit[],
isRuleRegistryEnabled: boolean
) => {
- if (isWrappedSignalHit(signals, isRuleRegistryEnabled)) {
- return signals.filter(
+ if (!isRuleRegistryEnabled) {
+ return (signals as WrappedSignalHit[]).filter(
(doc) => !doc._source.signal?.ancestors.some((ancestor) => ancestor.rule === ruleId)
);
- } else if (isWrappedRACAlert(signals, isRuleRegistryEnabled)) {
- return signals.filter(
+ } else {
+ return (signals as WrappedRACAlert[]).filter(
(doc) =>
!(doc._source['kibana.alert.ancestors'] as Ancestor[]).some(
(ancestor) => ancestor.rule === ruleId
)
);
- } else {
- return signals;
}
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts
index 5058056b169a32..c8ef0093291d58 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts
@@ -34,6 +34,7 @@ describe('get_input_output_index', () => {
index: ['test-input-index-1'],
experimentalFeatures: {
trustedAppsByPolicyEnabled: false,
+ excludePoliciesInFilterEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
tGridEnabled: false,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts
index 8c4ffdb2a6c4a2..0390c073354a6e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts
@@ -8,47 +8,54 @@
import { SavedObject } from 'src/core/server';
import { IRuleStatusSOAttributes } from '../rules/types';
-import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
-import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
+import { IRuleExecutionLogClient } from '../rule_execution_log/types';
+import { MAX_RULE_STATUSES } from './rule_status_service';
interface RuleStatusParams {
alertId: string;
- ruleStatusClient: RuleStatusSavedObjectsClient;
+ spaceId: string;
+ ruleStatusClient: IRuleExecutionLogClient;
}
export const createNewRuleStatus = async ({
alertId,
+ spaceId,
ruleStatusClient,
}: RuleStatusParams): Promise> => {
const now = new Date().toISOString();
return ruleStatusClient.create({
- alertId,
- statusDate: now,
- status: RuleExecutionStatus['going to run'],
- lastFailureAt: null,
- lastSuccessAt: null,
- lastFailureMessage: null,
- lastSuccessMessage: null,
- gap: null,
- bulkCreateTimeDurations: [],
- searchAfterTimeDurations: [],
- lastLookBackDate: null,
+ spaceId,
+ attributes: {
+ alertId,
+ statusDate: now,
+ status: RuleExecutionStatus['going to run'],
+ lastFailureAt: null,
+ lastSuccessAt: null,
+ lastFailureMessage: null,
+ lastSuccessMessage: null,
+ gap: null,
+ bulkCreateTimeDurations: [],
+ searchAfterTimeDurations: [],
+ lastLookBackDate: null,
+ },
});
};
export const getOrCreateRuleStatuses = async ({
+ spaceId,
alertId,
ruleStatusClient,
}: RuleStatusParams): Promise>> => {
- const ruleStatuses = await getRuleStatusSavedObjects({
- alertId,
- ruleStatusClient,
+ const ruleStatuses = await ruleStatusClient.find({
+ spaceId,
+ ruleId: alertId,
+ logsCount: MAX_RULE_STATUSES,
});
if (ruleStatuses.length > 0) {
return ruleStatuses;
}
- const newStatus = await createNewRuleStatus({ alertId, ruleStatusClient });
+ const newStatus = await createNewRuleStatus({ alertId, spaceId, ruleStatusClient });
return [newStatus];
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts
deleted file mode 100644
index dd3a2826018e2c..00000000000000
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts
+++ /dev/null
@@ -1,29 +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 { SavedObjectsFindResult } from 'kibana/server';
-import { IRuleStatusSOAttributes } from '../rules/types';
-import { MAX_RULE_STATUSES } from './rule_status_service';
-import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
-
-interface GetRuleStatusSavedObject {
- alertId: string;
- ruleStatusClient: RuleStatusSavedObjectsClient;
-}
-
-export const getRuleStatusSavedObjects = async ({
- alertId,
- ruleStatusClient,
-}: GetRuleStatusSavedObject): Promise>> => {
- return ruleStatusClient.find({
- perPage: MAX_RULE_STATUSES,
- sortField: 'statusDate',
- sortOrder: 'desc',
- search: `${alertId}`,
- searchFields: ['alertId'],
- });
-};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts
index 62f02ad6a251ab..b7450091855248 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts
@@ -34,6 +34,9 @@ export interface FindBulkResponse {
[key: string]: IRuleStatusSOAttributes[] | undefined;
}
+/**
+ * @pdeprecated Use RuleExecutionLogClient instead
+ */
export const ruleStatusSavedObjectsClientFactory = (
savedObjectsClient: SavedObjectsClientContract
): RuleStatusSavedObjectsClient => ({
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts
index ec843351d74b50..9a36dd0103a606 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.test.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { ruleStatusSavedObjectsClientMock } from './__mocks__/rule_status_saved_objects_client.mock';
import {
buildRuleStatusAttributes,
RuleStatusService,
@@ -14,6 +13,8 @@ import {
} from './rule_status_service';
import { exampleRuleStatus, exampleFindRuleStatusResponse } from './__mocks__/es_results';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
+import { RuleExecutionLogClient } from '../rule_execution_log/__mocks__/rule_execution_log_client';
+import { UpdateExecutionLogArgs } from '../rule_execution_log/types';
const expectIsoDateString = expect.stringMatching(/2.*Z$/);
const buildStatuses = (n: number) =>
@@ -87,27 +88,32 @@ describe('buildRuleStatusAttributes', () => {
describe('ruleStatusService', () => {
let currentStatus: ReturnType;
- let ruleStatusClient: ReturnType;
+ let ruleStatusClient: ReturnType;
let service: RuleStatusService;
beforeEach(async () => {
currentStatus = exampleRuleStatus();
- ruleStatusClient = ruleStatusSavedObjectsClientMock.create();
+ ruleStatusClient = new RuleExecutionLogClient();
ruleStatusClient.find.mockResolvedValue(exampleFindRuleStatusResponse([currentStatus]));
- service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient });
+ service = await ruleStatusServiceFactory({
+ alertId: 'mock-alert-id',
+ ruleStatusClient,
+ spaceId: 'default',
+ });
});
describe('goingToRun', () => {
it('updates the current status to "going to run"', async () => {
await service.goingToRun();
- expect(ruleStatusClient.update).toHaveBeenCalledWith(
- currentStatus.id,
- expect.objectContaining({
+ expect(ruleStatusClient.update).toHaveBeenCalledWith<[UpdateExecutionLogArgs]>({
+ id: currentStatus.id,
+ spaceId: 'default',
+ attributes: expect.objectContaining({
status: 'going to run',
statusDate: expectIsoDateString,
- })
- );
+ }),
+ });
});
});
@@ -115,15 +121,16 @@ describe('ruleStatusService', () => {
it('updates the current status to "succeeded"', async () => {
await service.success('hey, it worked');
- expect(ruleStatusClient.update).toHaveBeenCalledWith(
- currentStatus.id,
- expect.objectContaining({
+ expect(ruleStatusClient.update).toHaveBeenCalledWith<[UpdateExecutionLogArgs]>({
+ id: currentStatus.id,
+ spaceId: 'default',
+ attributes: expect.objectContaining({
status: 'succeeded',
statusDate: expectIsoDateString,
lastSuccessAt: expectIsoDateString,
lastSuccessMessage: 'hey, it worked',
- })
- );
+ }),
+ });
});
});
@@ -136,15 +143,16 @@ describe('ruleStatusService', () => {
it('updates the current status to "failed"', async () => {
await service.error('oh no, it broke');
- expect(ruleStatusClient.update).toHaveBeenCalledWith(
- currentStatus.id,
- expect.objectContaining({
+ expect(ruleStatusClient.update).toHaveBeenCalledWith<[UpdateExecutionLogArgs]>({
+ id: currentStatus.id,
+ spaceId: 'default',
+ attributes: expect.objectContaining({
status: 'failed',
statusDate: expectIsoDateString,
lastFailureAt: expectIsoDateString,
lastFailureMessage: 'oh no, it broke',
- })
- );
+ }),
+ });
});
it('does not delete statuses if we have less than the max number of statuses', async () => {
@@ -158,7 +166,11 @@ describe('ruleStatusService', () => {
ruleStatusClient.find.mockResolvedValue(
exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES - 1))
);
- service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient });
+ service = await ruleStatusServiceFactory({
+ alertId: 'mock-alert-id',
+ ruleStatusClient,
+ spaceId: 'default',
+ });
await service.error('oh no, it broke');
@@ -170,7 +182,11 @@ describe('ruleStatusService', () => {
ruleStatusClient.find.mockResolvedValue(
exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES))
);
- service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient });
+ service = await ruleStatusServiceFactory({
+ alertId: 'mock-alert-id',
+ ruleStatusClient,
+ spaceId: 'default',
+ });
await service.error('oh no, it broke');
@@ -184,7 +200,11 @@ describe('ruleStatusService', () => {
ruleStatusClient.find.mockResolvedValue(
exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES + 1))
);
- service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient });
+ service = await ruleStatusServiceFactory({
+ alertId: 'mock-alert-id',
+ ruleStatusClient,
+ spaceId: 'default',
+ });
await service.error('oh no, it broke');
@@ -200,7 +220,11 @@ describe('ruleStatusService', () => {
ruleStatusClient.find.mockResolvedValue(
exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES))
);
- service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient });
+ service = await ruleStatusServiceFactory({
+ alertId: 'mock-alert-id',
+ ruleStatusClient,
+ spaceId: 'default',
+ });
await service.error('oh no, it broke');
await service.error('oh no, it broke');
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts
index 0d51a6663b709e..45eff57d304e64 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_status_service.ts
@@ -9,7 +9,7 @@ import { assertUnreachable } from '../../../../common/utility_types';
import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas';
import { IRuleStatusSOAttributes } from '../rules/types';
import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses';
-import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client';
+import { IRuleExecutionLogClient } from '../rule_execution_log/types';
// 1st is mutable status, followed by 5 most recent failures
export const MAX_RULE_STATUSES = 6;
@@ -78,51 +78,69 @@ export const buildRuleStatusAttributes: (
};
export const ruleStatusServiceFactory = async ({
+ spaceId,
alertId,
ruleStatusClient,
}: {
+ spaceId: string;
alertId: string;
- ruleStatusClient: RuleStatusSavedObjectsClient;
+ ruleStatusClient: IRuleExecutionLogClient;
}): Promise => {
return {
goingToRun: async () => {
const [currentStatus] = await getOrCreateRuleStatuses({
+ spaceId,
alertId,
ruleStatusClient,
});
- await ruleStatusClient.update(currentStatus.id, {
- ...currentStatus.attributes,
- ...buildRuleStatusAttributes(RuleExecutionStatus['going to run']),
+ await ruleStatusClient.update({
+ id: currentStatus.id,
+ attributes: {
+ ...currentStatus.attributes,
+ ...buildRuleStatusAttributes(RuleExecutionStatus['going to run']),
+ },
+ spaceId,
});
},
success: async (message, attributes) => {
const [currentStatus] = await getOrCreateRuleStatuses({
+ spaceId,
alertId,
ruleStatusClient,
});
- await ruleStatusClient.update(currentStatus.id, {
- ...currentStatus.attributes,
- ...buildRuleStatusAttributes(RuleExecutionStatus.succeeded, message, attributes),
+ await ruleStatusClient.update({
+ id: currentStatus.id,
+ attributes: {
+ ...currentStatus.attributes,
+ ...buildRuleStatusAttributes(RuleExecutionStatus.succeeded, message, attributes),
+ },
+ spaceId,
});
},
partialFailure: async (message, attributes) => {
const [currentStatus] = await getOrCreateRuleStatuses({
+ spaceId,
alertId,
ruleStatusClient,
});
- await ruleStatusClient.update(currentStatus.id, {
- ...currentStatus.attributes,
- ...buildRuleStatusAttributes(RuleExecutionStatus['partial failure'], message, attributes),
+ await ruleStatusClient.update({
+ id: currentStatus.id,
+ attributes: {
+ ...currentStatus.attributes,
+ ...buildRuleStatusAttributes(RuleExecutionStatus['partial failure'], message, attributes),
+ },
+ spaceId,
});
},
error: async (message, attributes) => {
const ruleStatuses = await getOrCreateRuleStatuses({
+ spaceId,
alertId,
ruleStatusClient,
});
@@ -134,8 +152,12 @@ export const ruleStatusServiceFactory = async ({
};
// We always update the newest status, so to 'persist' a failure we push a copy to the head of the list
- await ruleStatusClient.update(currentStatus.id, failureAttributes);
- const newStatus = await ruleStatusClient.create(failureAttributes);
+ await ruleStatusClient.update({
+ id: currentStatus.id,
+ attributes: failureAttributes,
+ spaceId,
+ });
+ const newStatus = await ruleStatusClient.create({ attributes: failureAttributes, spaceId });
// drop oldest failures
const oldStatuses = [newStatus, ...ruleStatuses].slice(MAX_RULE_STATUSES);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
index a14c678d275361..6435204d1b7dfd 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts
@@ -34,6 +34,7 @@ import { mlExecutor } from './executors/ml';
import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock';
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import { allowedExperimentalValues } from '../../../../common/experimental_features';
+import { ruleRegistryMocks } from '../../../../../rule_registry/server/mocks';
jest.mock('./rule_status_saved_objects_client');
jest.mock('./rule_status_service');
@@ -119,6 +120,7 @@ describe('signal_rule_alert_type', () => {
let logger: ReturnType;
let alertServices: AlertServicesMock;
let ruleStatusService: Record;
+ let ruleDataService: ReturnType;
beforeEach(() => {
alertServices = alertsMock.createAlertServices();
@@ -130,6 +132,7 @@ describe('signal_rule_alert_type', () => {
error: jest.fn(),
partialFailure: jest.fn(),
};
+ ruleDataService = ruleRegistryMocks.createRuleDataPluginService();
(ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService);
(getListsClient as jest.Mock).mockReturnValue({
listClient: getListClientMock(),
@@ -196,6 +199,7 @@ describe('signal_rule_alert_type', () => {
ml: mlMock,
lists: listMock.createSetup(),
mergeStrategy: 'missingFields',
+ ruleDataService,
});
});
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 d1cb7194f86eed..7e467891e6d4d4 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
@@ -47,7 +47,6 @@ import {
} from '../notifications/schedule_notification_actions';
import { ruleStatusServiceFactory } from './rule_status_service';
import { buildRuleMessageFactory } from './rule_messages';
-import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client';
import { getNotificationResultsLink } from '../notifications/utils';
import { TelemetryEventsSender } from '../../telemetry/sender';
import { eqlExecutor } from './executors/eql';
@@ -70,6 +69,8 @@ import { wrapHitsFactory } from './wrap_hits_factory';
import { wrapSequencesFactory } from './wrap_sequences_factory';
import { ConfigType } from '../../../config';
import { ExperimentalFeatures } from '../../../../common/experimental_features';
+import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client';
+import { IRuleDataPluginService } from '../rule_execution_log/types';
export const signalRulesAlertType = ({
logger,
@@ -79,6 +80,7 @@ export const signalRulesAlertType = ({
ml,
lists,
mergeStrategy,
+ ruleDataService,
}: {
logger: Logger;
eventsTelemetry: TelemetryEventsSender | undefined;
@@ -87,6 +89,7 @@ export const signalRulesAlertType = ({
ml: SetupPlugins['ml'];
lists: SetupPlugins['lists'] | undefined;
mergeStrategy: ConfigType['alertMergeStrategy'];
+ ruleDataService: IRuleDataPluginService;
}): SignalRuleAlertTypeDefinition => {
return {
id: SIGNALS_ID,
@@ -124,8 +127,12 @@ export const signalRulesAlertType = ({
const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE);
let hasError: boolean = false;
let result = createSearchAfterReturnType();
- const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient);
+ const ruleStatusClient = new RuleExecutionLogClient({
+ ruleDataService,
+ savedObjectsClient: services.savedObjectsClient,
+ });
const ruleStatusService = await ruleStatusServiceFactory({
+ spaceId,
alertId,
ruleStatusClient,
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
index 4ad734c3bf7d96..48d372853e6d04 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts
@@ -33,6 +33,8 @@ import { BuildRuleMessage } from './rule_messages';
import { TelemetryEventsSender } from '../../telemetry/sender';
import { RuleParams } from '../schemas/rule_schemas';
import { GenericBulkCreateResponse } from './bulk_create_factory';
+import { EcsFieldMap } from '../../../../../rule_registry/common/assets/field_maps/ecs_field_map';
+import { TypeOfFieldMap } from '../../../../../rule_registry/common/field_map';
// used for gap detection code
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -166,6 +168,12 @@ export interface GetResponse {
_source: SearchTypes;
}
+export type EventHit = Exclude, '@timestamp'> & {
+ '@timestamp': string;
+ [key: string]: SearchTypes;
+};
+export type WrappedEventHit = BaseHit;
+
export type SignalSearchResponse = estypes.SearchResponse;
export type SignalSourceHit = estypes.SearchHit;
export type WrappedSignalHit = BaseHit;
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 cb1bf9d774359a..72ac4f6d0f550c 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
@@ -5,18 +5,20 @@
* 2.0.
*/
import { createHash } from 'crypto';
+import { chunk, get, isEmpty, partition } from 'lodash';
import moment from 'moment';
import uuidv5 from 'uuid/v5';
+
import dateMath from '@elastic/datemath';
import type { estypes } from '@elastic/elasticsearch';
-import { chunk, isEmpty, partition } from 'lodash';
import { ApiResponse, Context } from '@elastic/elasticsearch/lib/Transport';
-
+import { ALERT_ID } from '@kbn/rule-data-utils';
import type { ListArray, ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types';
import { MAX_EXCEPTION_LIST_SIZE } from '@kbn/securitysolution-list-constants';
import { hasLargeValueList } from '@kbn/securitysolution-list-utils';
import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils';
import { ElasticsearchClient } from '@kbn/securitysolution-es-utils';
+
import {
TimestampOverrideOrUndefined,
Privilege,
@@ -39,6 +41,8 @@ import {
RuleRangeTuple,
BaseSignalHit,
SignalSourceHit,
+ SimpleHit,
+ WrappedEventHit,
} from './types';
import { BuildRuleMessage } from './rule_messages';
import { ShardError } from '../../types';
@@ -52,6 +56,8 @@ import {
ThreatRuleParams,
ThresholdRuleParams,
} from '../schemas/rule_schemas';
+import { WrappedRACAlert } from '../rule_types/types';
+import { SearchTypes } from '../../../../common/detection_engine/types';
interface SortExceptionsReturn {
exceptionsWithValueLists: ExceptionListItemSchema[];
@@ -928,3 +934,25 @@ export const buildChunkedOrFilter = (field: string, values: string[], chunkSize:
})
.join(' OR ');
};
+
+export const isWrappedEventHit = (event: SimpleHit): event is WrappedEventHit => {
+ return !isWrappedSignalHit(event) && !isWrappedRACAlert(event);
+};
+
+export const isWrappedSignalHit = (event: SimpleHit): event is WrappedSignalHit => {
+ return (event as WrappedSignalHit)?._source?.signal != null;
+};
+
+export const isWrappedRACAlert = (event: SimpleHit): event is WrappedRACAlert => {
+ return (event as WrappedRACAlert)?._source?.[ALERT_ID] != null;
+};
+
+export const getField = (event: SimpleHit, field: string): T | undefined => {
+ if (isWrappedRACAlert(event)) {
+ return event._source, field.replace('signal', 'kibana.alert') as T; // TODO: handle special cases
+ } else if (isWrappedSignalHit(event)) {
+ return get(event._source, field) as T;
+ } else if (isWrappedEventHit(event)) {
+ return get(event._source, field) as T;
+ }
+};
diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts
index 96171154f60071..fd3a32a2fa689d 100644
--- a/x-pack/plugins/security_solution/server/plugin.ts
+++ b/x-pack/plugins/security_solution/server/plugin.ts
@@ -187,7 +187,6 @@ export class Plugin implements IPlugin
new RuleExecutionLogClient({
ruleDataService: plugins.ruleRegistry.ruleDataService,
- // TODO check if savedObjects.client contains spaceId
savedObjectsClient: context.core.savedObjects.client,
}),
})
@@ -262,6 +261,7 @@ export class Plugin implements IPlugin {
});
expect(disableAlert).toHaveBeenCalled();
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+
// Enable the alert
await act(async () => {
wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click');
@@ -546,6 +551,77 @@ describe('disable button', () => {
// Ensure error banner is back
expect(wrapper.find('[data-test-subj="dismiss-execution-error"]').length).toBeGreaterThan(0);
});
+
+ it('should show the loading spinner when the rule enabled switch was clicked and the server responded with some delay', async () => {
+ const alert = mockAlert({
+ enabled: true,
+ executionStatus: {
+ status: 'error',
+ lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
+ error: {
+ reason: AlertExecutionStatusErrorReasons.Execute,
+ message: 'Fail',
+ },
+ },
+ });
+
+ const alertType: AlertType = {
+ id: '.noop',
+ name: 'No Op',
+ actionGroups: [{ id: 'default', name: 'Default' }],
+ recoveryActionGroup,
+ actionVariables: { context: [], state: [], params: [] },
+ defaultActionGroupId: 'default',
+ producer: ALERTS_FEATURE_ID,
+ authorizedConsumers,
+ minimumLicenseRequired: 'basic',
+ enabledInLicense: true,
+ };
+
+ const disableAlert = jest.fn(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 6000));
+ });
+ const enableAlert = jest.fn();
+ const wrapper = mountWithIntl(
+
+ );
+
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+
+ // Dismiss the error banner
+ await act(async () => {
+ wrapper.find('[data-test-subj="dismiss-execution-error"]').first().simulate('click');
+ await nextTick();
+ });
+
+ // Disable the alert
+ await act(async () => {
+ wrapper.find('[data-test-subj="enableSwitch"] .euiSwitch__button').first().simulate('click');
+ await nextTick();
+ });
+ expect(disableAlert).toHaveBeenCalled();
+
+ await act(async () => {
+ await nextTick();
+ wrapper.update();
+ });
+
+ // Enable the alert
+ await act(async () => {
+ expect(wrapper.find('[data-test-subj="enableSpinner"]').length).toBeGreaterThan(0);
+ await nextTick();
+ });
+ });
});
describe('mute button', () => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx
index 1328ba6479f687..2558993a13fe61 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx
@@ -21,6 +21,7 @@ import {
EuiSpacer,
EuiButtonEmpty,
EuiButton,
+ EuiLoadingSpinner,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common';
@@ -99,6 +100,8 @@ export const AlertDetails: React.FunctionComponent = ({
const alertActions = alert.actions;
const uniqueActions = Array.from(new Set(alertActions.map((item: any) => item.actionTypeId)));
const [isEnabled, setIsEnabled] = useState(alert.enabled);
+ const [isEnabledUpdating, setIsEnabledUpdating] = useState(false);
+ const [isMutedUpdating, setIsMutedUpdating] = useState(false);
const [isMuted, setIsMuted] = useState(alert.muteAll);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false);
const [dissmissAlertErrors, setDissmissAlertErrors] = useState(false);
@@ -218,54 +221,92 @@ export const AlertDetails: React.FunctionComponent = ({
- {
- if (isEnabled) {
- setIsEnabled(false);
- await disableAlert(alert);
- // Reset dismiss if previously clicked
- setDissmissAlertErrors(false);
- } else {
- setIsEnabled(true);
- await enableAlert(alert);
+ {isEnabledUpdating ? (
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+ {
+ setIsEnabledUpdating(true);
+ if (isEnabled) {
+ setIsEnabled(false);
+ await disableAlert(alert);
+ // Reset dismiss if previously clicked
+ setDissmissAlertErrors(false);
+ } else {
+ setIsEnabled(true);
+ await enableAlert(alert);
+ }
+ requestRefresh();
+ setIsEnabledUpdating(false);
+ }}
+ label={
+
}
- requestRefresh();
- }}
- label={
-
- }
- />
+ />
+ )}
- {
- if (isMuted) {
- setIsMuted(false);
- await unmuteAlert(alert);
- } else {
- setIsMuted(true);
- await muteAlert(alert);
+ {isMutedUpdating ? (
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+ {
+ setIsMutedUpdating(true);
+ if (isMuted) {
+ setIsMuted(false);
+ await unmuteAlert(alert);
+ } else {
+ setIsMuted(true);
+ await muteAlert(alert);
+ }
+ requestRefresh();
+ setIsMutedUpdating(false);
+ }}
+ label={
+
}
- requestRefresh();
- }}
- label={
-
- }
- />
+ />
+ )}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx
index 29290af0d02852..1583cb188f1c18 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx
@@ -8,7 +8,7 @@
import React, { useState } from 'react';
import moment, { Duration } from 'moment';
import { i18n } from '@kbn/i18n';
-import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch, EuiToolTip } from '@elastic/eui';
+import { EuiBasicTable, EuiHealth, EuiSpacer, EuiToolTip } from '@elastic/eui';
// @ts-ignore
import { RIGHT_ALIGNMENT, CENTER_ALIGNMENT } from '@elastic/eui/lib/services';
import { padStart, chunk } from 'lodash';
@@ -26,6 +26,7 @@ import {
} from '../../common/components/with_bulk_alert_api_operations';
import { DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants';
import './alert_instances.scss';
+import { RuleMutedSwitch } from './rule_muted_switch';
type AlertInstancesProps = {
alert: Alert;
@@ -112,17 +113,11 @@ export const alertInstancesTableColumns = (
),
render: (alertInstance: AlertInstanceListItem) => {
return (
- <>
- onMuteAction(alertInstance)}
- />
- >
+ await onMuteAction(alertInstance)}
+ alertInstance={alertInstance}
+ />
);
},
sortable: false,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx
new file mode 100644
index 00000000000000..bee0c8aef706d0
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/rule_muted_switch.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useState } from 'react';
+import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui';
+
+import { AlertInstanceListItem } from './alert_instances';
+
+interface ComponentOpts {
+ alertInstance: AlertInstanceListItem;
+ onMuteAction: (instance: AlertInstanceListItem) => Promise;
+ disabled: boolean;
+}
+
+export const RuleMutedSwitch: React.FunctionComponent = ({
+ alertInstance,
+ onMuteAction,
+ disabled,
+}: ComponentOpts) => {
+ const [isMuted, setIsMuted] = useState(alertInstance?.isMuted);
+ const [isUpdating, setIsUpdating] = useState(false);
+
+ return isUpdating ? (
+
+ ) : (
+ {
+ setIsUpdating(true);
+ await onMuteAction(alertInstance);
+ setIsMuted(!isMuted);
+ setIsUpdating(false);
+ }}
+ data-test-subj={`muteAlertInstanceButton_${alertInstance.instance}`}
+ showLabel={false}
+ label="mute"
+ />
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx
index c031f189ffa4d0..49871549d27344 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx
@@ -5,9 +5,8 @@
* 2.0.
*/
-import { asyncScheduler } from 'rxjs';
-import React, { useEffect, useState } from 'react';
-import { EuiSwitch } from '@elastic/eui';
+import React, { useState, useEffect } from 'react';
+import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui';
import { Alert, AlertTableItem } from '../../../../types';
@@ -28,8 +27,11 @@ export const RuleEnabledSwitch: React.FunctionComponent = ({
useEffect(() => {
setIsEnabled(item.enabled);
}, [item.enabled]);
+ const [isUpdating, setIsUpdating] = useState(false);
- return (
+ return isUpdating ? (
+
+ ) : (
= ({
checked={isEnabled}
data-test-subj="enableSwitch"
onChange={async () => {
- const enabled = isEnabled;
- asyncScheduler.schedule(async () => {
- if (enabled) {
- await disableAlert({ ...item, enabled });
- } else {
- await enableAlert({ ...item, enabled });
- }
- onAlertChanged();
- }, 10);
+ setIsUpdating(true);
+ const enabled = item.enabled;
+ if (enabled) {
+ await disableAlert({ ...item, enabled });
+ } else {
+ await enableAlert({ ...item, enabled });
+ }
setIsEnabled(!isEnabled);
+ setIsUpdating(false);
+ onAlertChanged();
}}
label=""
/>
diff --git a/x-pack/plugins/watcher/kibana.json b/x-pack/plugins/watcher/kibana.json
index b9df25d80e62e1..84fe2b509b2637 100644
--- a/x-pack/plugins/watcher/kibana.json
+++ b/x-pack/plugins/watcher/kibana.json
@@ -15,6 +15,7 @@
"ui": true,
"requiredBundles": [
"esUiShared",
- "kibanaReact"
+ "kibanaReact",
+ "fieldFormats"
]
}
diff --git a/x-pack/plugins/watcher/public/legacy/time_buckets.js b/x-pack/plugins/watcher/public/legacy/time_buckets.js
index a65e069bb208ba..173369152f8e0d 100644
--- a/x-pack/plugins/watcher/public/legacy/time_buckets.js
+++ b/x-pack/plugins/watcher/public/legacy/time_buckets.js
@@ -7,7 +7,8 @@
import _ from 'lodash';
import moment from 'moment';
-import { search, FIELD_FORMAT_IDS, UI_SETTINGS } from '../../../../../src/plugins/data/public';
+import { search, UI_SETTINGS } from '../../../../../src/plugins/data/public';
+import { FIELD_FORMAT_IDS } from '../../../../../src/plugins/field_formats/common';
import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval';
import {
convertDurationToNormalizedEsInterval,
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts
index a6ea27be21cc84..d2f9acf35d6323 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts
@@ -116,7 +116,7 @@ export default function ({ getService }: FtrProviderContext) {
};
before(async () => {
- await ml.api.indexAnnotation(annotation, annotationId);
+ await ml.api.indexAnnotation(annotation as Partial, annotationId);
});
it('displays the original annotation correctly', async () => {
@@ -189,7 +189,7 @@ export default function ({ getService }: FtrProviderContext) {
const annotationId = `delete-annotation-id-${Date.now()}`;
before(async () => {
- await ml.api.indexAnnotation(annotation, annotationId);
+ await ml.api.indexAnnotation(annotation as Partial, annotationId);
});
it('displays the original annotation', async () => {
diff --git a/x-pack/test/functional/es_archives/discover/default/data.json.gz b/x-pack/test/functional/es_archives/discover/default/data.json.gz
deleted file mode 100644
index 047d890f6d4101..00000000000000
Binary files a/x-pack/test/functional/es_archives/discover/default/data.json.gz and /dev/null differ
diff --git a/x-pack/test/functional/es_archives/discover/default/mappings.json b/x-pack/test/functional/es_archives/discover/default/mappings.json
deleted file mode 100644
index 519af2dd75b9e1..00000000000000
--- a/x-pack/test/functional/es_archives/discover/default/mappings.json
+++ /dev/null
@@ -1,279 +0,0 @@
-{
- "type": "index",
- "value": {
- "aliases": {
- ".kibana": {}
- },
- "index": ".kibana_1",
- "mappings": {
- "properties": {
- "config": {
- "dynamic": "true",
- "properties": {
- "buildNum": {
- "type": "keyword"
- }
- }
- },
- "dashboard": {
- "dynamic": "strict",
- "properties": {
- "description": {
- "type": "text"
- },
- "hits": {
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "type": "text"
- }
- }
- },
- "optionsJSON": {
- "type": "text"
- },
- "panelsJSON": {
- "type": "text"
- },
- "refreshInterval": {
- "properties": {
- "display": {
- "type": "keyword"
- },
- "pause": {
- "type": "boolean"
- },
- "section": {
- "type": "integer"
- },
- "value": {
- "type": "integer"
- }
- }
- },
- "timeFrom": {
- "type": "keyword"
- },
- "timeRestore": {
- "type": "boolean"
- },
- "timeTo": {
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "uiStateJSON": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "index-pattern": {
- "dynamic": "strict",
- "properties": {
- "fieldFormatMap": {
- "type": "text"
- },
- "fields": {
- "type": "text"
- },
- "intervalName": {
- "type": "keyword"
- },
- "notExpandable": {
- "type": "boolean"
- },
- "sourceFilters": {
- "type": "text"
- },
- "timeFieldName": {
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "fieldAttrs": {
- "type": "text"
- }
- }
- },
- "search": {
- "dynamic": "strict",
- "properties": {
- "columns": {
- "type": "keyword"
- },
- "description": {
- "type": "text"
- },
- "hits": {
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "type": "text"
- }
- }
- },
- "sort": {
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "server": {
- "dynamic": "strict",
- "properties": {
- "uuid": {
- "type": "keyword"
- }
- }
- },
- "timelion-sheet": {
- "dynamic": "strict",
- "properties": {
- "description": {
- "type": "text"
- },
- "hits": {
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "type": "text"
- }
- }
- },
- "timelion_chart_height": {
- "type": "integer"
- },
- "timelion_columns": {
- "type": "integer"
- },
- "timelion_interval": {
- "type": "keyword"
- },
- "timelion_other_interval": {
- "type": "keyword"
- },
- "timelion_rows": {
- "type": "integer"
- },
- "timelion_sheet": {
- "type": "text"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "type": {
- "type": "keyword"
- },
- "url": {
- "dynamic": "strict",
- "properties": {
- "accessCount": {
- "type": "long"
- },
- "accessDate": {
- "type": "date"
- },
- "createDate": {
- "type": "date"
- },
- "url": {
- "fields": {
- "keyword": {
- "ignore_above": 2048,
- "type": "keyword"
- }
- },
- "type": "text"
- }
- }
- },
- "visualization": {
- "dynamic": "strict",
- "properties": {
- "description": {
- "type": "text"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "type": "text"
- }
- }
- },
- "savedSearchId": {
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "uiStateJSON": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- },
- "visState": {
- "type": "text"
- }
- }
- },
- "query": {
- "properties": {
- "title": {
- "type": "text"
- },
- "description": {
- "type": "text"
- },
- "query": {
- "properties": {
- "language": {
- "type": "keyword"
- },
- "query": {
- "type": "keyword",
- "index": false
- }
- }
- },
- "filters": {
- "type": "object",
- "enabled": false
- },
- "timefilter": {
- "type": "object",
- "enabled": false
- }
- }
- }
- }
- },
- "settings": {
- "index": {
- "number_of_replicas": "1",
- "number_of_shards": "1"
- }
- }
- }
-}
diff --git a/x-pack/test/functional/fixtures/kbn_archiver/discover/default.json b/x-pack/test/functional/fixtures/kbn_archiver/discover/default.json
new file mode 100644
index 00000000000000..2b0d414c67c990
--- /dev/null
+++ b/x-pack/test/functional/fixtures/kbn_archiver/discover/default.json
@@ -0,0 +1,65 @@
+{
+ "attributes": {
+ "buildNum": 9007199254740991
+ },
+ "coreMigrationVersion": "7.15.0",
+ "id": "7.15.0",
+ "migrationVersion": {
+ "config": "7.13.0"
+ },
+ "references": [],
+ "type": "config",
+ "updated_at": "2021-08-04T16:20:48.415Z",
+ "version": "WzUsMl0="
+}
+
+{
+ "attributes": {
+ "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"nestedField.child\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true,\"subType\":{\"nested\":{\"path\":\"nestedField\"}}}]",
+ "timeFieldName": "@timestamp",
+ "title": "logstash-*"
+ },
+ "coreMigrationVersion": "7.15.0",
+ "id": "logstash-*",
+ "migrationVersion": {
+ "index-pattern": "7.11.0"
+ },
+ "references": [],
+ "type": "index-pattern",
+ "version": "WzIsMl0="
+}
+
+{
+ "attributes": {
+ "columns": [
+ "_source"
+ ],
+ "description": "A Saved Search Description",
+ "hits": 0,
+ "kibanaSavedObjectMeta": {
+ "searchSourceJSON": "{\"highlightAll\":true,\"filter\":[],\"query\":{\"query_string\":{\"query\":\"*\",\"analyze_wildcard\":true}},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}"
+ },
+ "sort": [
+ [
+ "@timestamp",
+ "desc"
+ ]
+ ],
+ "title": "A Saved Search",
+ "version": 1
+ },
+ "coreMigrationVersion": "7.15.0",
+ "id": "ab12e3c0-f231-11e6-9486-733b1ac9221a",
+ "migrationVersion": {
+ "search": "7.9.3"
+ },
+ "references": [
+ {
+ "id": "logstash-*",
+ "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
+ "type": "index-pattern"
+ }
+ ],
+ "type": "search",
+ "version": "WzMsMl0="
+}
\ No newline at end of file
diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts
index 074ecafddf3810..37fe0eccea31af 100644
--- a/x-pack/test/functional/page_objects/security_page.ts
+++ b/x-pack/test/functional/page_objects/security_page.ts
@@ -485,10 +485,7 @@ export class SecurityPageObject extends FtrService {
if (roleObj.elasticsearch.indices[0].query) {
await this.testSubjects.click('restrictDocumentsQuery0');
- await this.monacoEditor.setCodeEditorValue(
- 0,
- JSON.stringify(roleObj.elasticsearch.indices[0].query)
- );
+ await this.monacoEditor.setCodeEditorValue(roleObj.elasticsearch.indices[0].query);
}
const globalPrivileges = (roleObj.kibana as any).global;
diff --git a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts
index 93dca78b34a826..3f02e64056325d 100644
--- a/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts
+++ b/x-pack/test/search_sessions_integration/tests/apps/discover/async_search.ts
@@ -25,15 +25,23 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
]);
const searchSessions = getService('searchSessions');
const retry = getService('retry');
+ const kibanaServer = getService('kibanaServer');
describe('discover async search', () => {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
- await esArchiver.load('x-pack/test/functional/es_archives/discover/default');
+ await kibanaServer.importExport.load(
+ 'x-pack/test/functional/fixtures/kbn_archiver/discover/default'
+ );
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.header.waitUntilLoadingHasFinished();
});
+ after(async () => {
+ await kibanaServer.importExport.unload(
+ 'x-pack/test/functional/fixtures/kbn_archiver/discover/default'
+ );
+ });
it('search session id should change between searches', async () => {
const searchSessionId1 = await getSearchSessionId();