diff --git a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index d2579f427debe..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,3 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`HistogramSignals it renders 1`] = ``;
diff --git a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.test.tsx
deleted file mode 100644
index 5d2f3256ef509..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.test.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { shallow } from 'enzyme';
-import React from 'react';
-
-import { TestProviders } from '../../../../mock';
-import { HistogramSignals } from './index';
-
-describe('HistogramSignals', () => {
- test('it renders', () => {
- const wrapper = shallow(
-
-
-
- );
-
- expect(wrapper.find('HistogramSignals')).toMatchSnapshot();
- });
-});
diff --git a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.tsx
deleted file mode 100644
index 35fe8a2d90509..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/page/detection_engine/histogram_signals/index.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import {
- Axis,
- Chart,
- HistogramBarSeries,
- Settings,
- niceTimeFormatByDay,
- timeFormatter,
-} from '@elastic/charts';
-import React from 'react';
-import { npStart } from 'ui/new_platform';
-
-export const HistogramSignals = React.memo(() => {
- const sampleChartData = [
- { x: 1571090784000, y: 2, a: 'a' },
- { x: 1571090784000, y: 2, b: 'b' },
- { x: 1571093484000, y: 7, a: 'a' },
- { x: 1571096184000, y: 3, a: 'a' },
- { x: 1571098884000, y: 2, a: 'a' },
- { x: 1571101584000, y: 7, a: 'a' },
- { x: 1571104284000, y: 3, a: 'a' },
- { x: 1571106984000, y: 2, a: 'a' },
- { x: 1571109684000, y: 7, a: 'a' },
- { x: 1571112384000, y: 3, a: 'a' },
- { x: 1571115084000, y: 2, a: 'a' },
- { x: 1571117784000, y: 7, a: 'a' },
- { x: 1571120484000, y: 3, a: 'a' },
- { x: 1571123184000, y: 2, a: 'a' },
- { x: 1571125884000, y: 7, a: 'a' },
- { x: 1571128584000, y: 3, a: 'a' },
- { x: 1571131284000, y: 2, a: 'a' },
- { x: 1571133984000, y: 7, a: 'a' },
- { x: 1571136684000, y: 3, a: 'a' },
- { x: 1571139384000, y: 2, a: 'a' },
- { x: 1571142084000, y: 7, a: 'a' },
- { x: 1571144784000, y: 3, a: 'a' },
- { x: 1571147484000, y: 2, a: 'a' },
- { x: 1571150184000, y: 7, a: 'a' },
- { x: 1571152884000, y: 3, a: 'a' },
- { x: 1571155584000, y: 2, a: 'a' },
- { x: 1571158284000, y: 7, a: 'a' },
- { x: 1571160984000, y: 3, a: 'a' },
- { x: 1571163684000, y: 2, a: 'a' },
- { x: 1571166384000, y: 7, a: 'a' },
- { x: 1571169084000, y: 3, a: 'a' },
- { x: 1571171784000, y: 2, a: 'a' },
- { x: 1571174484000, y: 7, a: 'a' },
- ];
-
- return (
-
-
-
-
-
-
-
-
-
- );
-});
-HistogramSignals.displayName = 'HistogramSignals';
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts
index e7641fd37678e..8754d73637e7c 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts
@@ -42,7 +42,7 @@ export const fetchQuerySignals = async ({
'content-type': 'application/json',
'kbn-xsrf': 'true',
},
- body: query,
+ body: JSON.stringify(query),
signal,
});
await throwIfNotOk(response);
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts
index 32f53691bae87..34cb7684a0399 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts
@@ -10,7 +10,7 @@ export interface BasicSignals {
signal: AbortSignal;
}
export interface QuerySignals extends BasicSignals {
- query: string;
+ query: object;
}
export interface SignalsResponse {
@@ -18,7 +18,8 @@ export interface SignalsResponse {
timeout: boolean;
}
-export interface SignalSearchResponse extends SignalsResponse {
+export interface SignalSearchResponse
+ extends SignalsResponse {
_shards: {
total: number;
successful: number;
diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx
index 65a5ac866e68d..fa88a84fb1187 100644
--- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx
@@ -4,20 +4,25 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useEffect, useState } from 'react';
+import React, { SetStateAction, useEffect, useState } from 'react';
import { fetchQuerySignals } from './api';
import { SignalSearchResponse } from './types';
-type Return = [boolean, SignalSearchResponse | null];
+type Return = [
+ boolean,
+ SignalSearchResponse | null,
+ React.Dispatch>
+];
/**
* Hook for using to get a Signals from the Detection Engine API
*
- * @param query convert a dsl into string
+ * @param initialQuery query dsl object
*
*/
-export const useQuerySignals = (query: string): Return => {
+export const useQuerySignals = (initialQuery: object): Return => {
+ const [query, setQuery] = useState(initialQuery);
const [signals, setSignals] = useState | null>(null);
const [loading, setLoading] = useState(true);
@@ -53,5 +58,5 @@ export const useQuerySignals = (query: string): Return =>
};
}, [query]);
- return [loading, signals];
+ return [loading, signals, setQuery];
};
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_chart/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_chart/index.tsx
deleted file mode 100644
index 01ebafdccfefd..0000000000000
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_chart/index.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { EuiPanel, EuiSelect } from '@elastic/eui';
-import { noop } from 'lodash/fp';
-import React, { memo } from 'react';
-
-import { HeaderSection } from '../../../../components/header_section';
-import { HistogramSignals } from '../../../../components/page/detection_engine/histogram_signals';
-
-export const sampleChartOptions = [
- { text: 'Risk scores', value: 'risk_scores' },
- { text: 'Severities', value: 'severities' },
- { text: 'Top destination IPs', value: 'destination_ips' },
- { text: 'Top event actions', value: 'event_actions' },
- { text: 'Top event categories', value: 'event_categories' },
- { text: 'Top host names', value: 'host_names' },
- { text: 'Top rule types', value: 'rule_types' },
- { text: 'Top rules', value: 'rules' },
- { text: 'Top source IPs', value: 'source_ips' },
- { text: 'Top users', value: 'users' },
-];
-
-const SignalsChartsComponent = () => (
-
-
- noop}
- prepend="Stack by"
- value={sampleChartOptions[0].value}
- />
-
-
-
-
-);
-
-export const SignalsCharts = memo(SignalsChartsComponent);
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts
new file mode 100644
index 0000000000000..f329780b075e3
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as i18n from './translations';
+import { SignalsHistogramOption } from './types';
+
+export const signalsHistogramOptions: SignalsHistogramOption[] = [
+ { text: i18n.STACK_BY_RISK_SCORES, value: 'signal.rule.risk_score' },
+ { text: i18n.STACK_BY_SEVERITIES, value: 'signal.rule.severity' },
+ { text: i18n.STACK_BY_DESTINATION_IPS, value: 'destination.ip' },
+ { text: i18n.STACK_BY_ACTIONS, value: 'event.action' },
+ { text: i18n.STACK_BY_CATEGORIES, value: 'event.category' },
+ { text: i18n.STACK_BY_HOST_NAMES, value: 'host.name' },
+ { text: i18n.STACK_BY_RULE_TYPES, value: 'signal.rule.type' },
+ { text: i18n.STACK_BY_RULE_NAMES, value: 'signal.rule.name' },
+ { text: i18n.STACK_BY_SOURCE_IPS, value: 'source.ip' },
+ { text: i18n.STACK_BY_USERS, value: 'user.name' },
+];
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx
new file mode 100644
index 0000000000000..fda40f5f9fa5d
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx
@@ -0,0 +1,112 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Position } from '@elastic/charts';
+import { EuiButton, EuiPanel, EuiSelect } from '@elastic/eui';
+import numeral from '@elastic/numeral';
+import React, { memo, useCallback, useMemo, useState } from 'react';
+
+import { HeaderSection } from '../../../../components/header_section';
+import { SignalsHistogram } from './signals_histogram';
+
+import * as i18n from './translations';
+import { Query } from '../../../../../../../../../src/plugins/data/common/query';
+import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query';
+import { SignalsHistogramOption, SignalsTotal } from './types';
+import { signalsHistogramOptions } from './config';
+import { getDetectionEngineUrl } from '../../../../components/link_to';
+import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants';
+import { useUiSetting$ } from '../../../../lib/kibana';
+
+const defaultTotalSignalsObj: SignalsTotal = {
+ value: 0,
+ relation: 'eq',
+};
+
+interface SignalsHistogramPanelProps {
+ defaultStackByOption?: SignalsHistogramOption;
+ filters?: esFilters.Filter[];
+ from: number;
+ query?: Query;
+ legendPosition?: Position;
+ loadingInitial?: boolean;
+ showLinkToSignals?: boolean;
+ showTotalSignalsCount?: boolean;
+ stackByOptions?: SignalsHistogramOption[];
+ title?: string;
+ to: number;
+ updateDateRange: (min: number, max: number) => void;
+}
+
+export const SignalsHistogramPanel = memo(
+ ({
+ defaultStackByOption = signalsHistogramOptions[0],
+ filters,
+ query,
+ from,
+ legendPosition = 'bottom',
+ loadingInitial = false,
+ showLinkToSignals = false,
+ showTotalSignalsCount = false,
+ stackByOptions,
+ to,
+ title = i18n.HISTOGRAM_HEADER,
+ updateDateRange,
+ }) => {
+ const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT);
+ const [totalSignalsObj, setTotalSignalsObj] = useState(defaultTotalSignalsObj);
+ const [selectedStackByOption, setSelectedStackByOption] = useState(
+ defaultStackByOption
+ );
+
+ const totalSignals = useMemo(
+ () =>
+ i18n.SHOWING_SIGNALS(
+ numeral(totalSignalsObj.value).format(defaultNumberFormat),
+ totalSignalsObj.value,
+ totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : ''
+ ),
+ [totalSignalsObj]
+ );
+
+ const setSelectedOptionCallback = useCallback((event: React.ChangeEvent) => {
+ setSelectedStackByOption(
+ stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption
+ );
+ }, []);
+
+ return (
+
+
+ {stackByOptions && (
+
+ )}
+ {showLinkToSignals && (
+ {i18n.VIEW_SIGNALS}
+ )}
+
+
+
+
+ );
+ }
+);
+
+SignalsHistogramPanel.displayName = 'SignalsHistogramPanel';
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/helpers.tsx
new file mode 100644
index 0000000000000..ed503e9872f0a
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/helpers.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from '../types';
+import { SignalSearchResponse } from '../../../../../containers/detection_engine/signals/types';
+import * as i18n from '../translations';
+
+export const formatSignalsData = (
+ signalsData: SignalSearchResponse<{}, SignalsAggregation> | null
+) => {
+ const groupBuckets: SignalsGroupBucket[] =
+ signalsData?.aggregations?.signalsByGrouping?.buckets ?? [];
+ return groupBuckets.reduce((acc, { key: group, signals }) => {
+ const signalsBucket: SignalsBucket[] = signals.buckets ?? [];
+
+ return [
+ ...acc,
+ ...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({
+ x: key,
+ y: doc_count,
+ g: group,
+ })),
+ ];
+ }, []);
+};
+
+export const getSignalsHistogramQuery = (
+ stackByField: string,
+ from: number,
+ to: number,
+ additionalFilters: Array<{
+ bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] };
+ }>
+) => ({
+ aggs: {
+ signalsByGrouping: {
+ terms: {
+ field: stackByField,
+ missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS,
+ order: {
+ _count: 'desc',
+ },
+ size: 10,
+ },
+ aggs: {
+ signals: {
+ auto_date_histogram: {
+ field: '@timestamp',
+ buckets: 36,
+ },
+ },
+ },
+ },
+ },
+ query: {
+ bool: {
+ filter: [
+ ...additionalFilters,
+ {
+ range: {
+ '@timestamp': {
+ gte: from,
+ lte: to,
+ },
+ },
+ },
+ ],
+ },
+ },
+});
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx
new file mode 100644
index 0000000000000..218fcc3a70f79
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx
@@ -0,0 +1,122 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ Axis,
+ Chart,
+ getAxisId,
+ getSpecId,
+ HistogramBarSeries,
+ niceTimeFormatByDay,
+ Position,
+ Settings,
+ timeFormatter,
+} from '@elastic/charts';
+import React, { useEffect, useMemo } from 'react';
+import { EuiLoadingContent } from '@elastic/eui';
+import { isEmpty } from 'lodash/fp';
+import { useQuerySignals } from '../../../../../containers/detection_engine/signals/use_query';
+import { Query } from '../../../../../../../../../../src/plugins/data/common/query';
+import { esFilters, esQuery } from '../../../../../../../../../../src/plugins/data/common/es_query';
+import { SignalsAggregation, SignalsTotal } from '../types';
+import { formatSignalsData, getSignalsHistogramQuery } from './helpers';
+import { useTheme } from '../../../../../components/charts/common';
+import { useKibana } from '../../../../../lib/kibana';
+
+interface HistogramSignalsProps {
+ filters?: esFilters.Filter[];
+ from: number;
+ legendPosition?: Position;
+ loadingInitial: boolean;
+ query?: Query;
+ setTotalSignalsCount: React.Dispatch;
+ stackByField: string;
+ to: number;
+ updateDateRange: (min: number, max: number) => void;
+}
+
+export const SignalsHistogram = React.memo(
+ ({
+ to,
+ from,
+ query,
+ filters,
+ legendPosition = 'bottom',
+ loadingInitial,
+ setTotalSignalsCount,
+ stackByField,
+ updateDateRange,
+ }) => {
+ const [isLoadingSignals, signalsData, setQuery] = useQuerySignals<{}, SignalsAggregation>(
+ getSignalsHistogramQuery(stackByField, from, to, [])
+ );
+ const theme = useTheme();
+ const kibana = useKibana();
+
+ const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]);
+
+ useEffect(() => {
+ setTotalSignalsCount(
+ signalsData?.hits.total ?? {
+ value: 0,
+ relation: 'eq',
+ }
+ );
+ }, [signalsData]);
+
+ useEffect(() => {
+ const converted = esQuery.buildEsQuery(
+ undefined,
+ query != null ? [query] : [],
+ filters?.filter(f => f.meta.disabled === false) ?? [],
+ {
+ ...esQuery.getEsQueryConfig(kibana.services.uiSettings),
+ dateFormatTZ: undefined,
+ }
+ );
+
+ setQuery(
+ getSignalsHistogramQuery(stackByField, from, to, !isEmpty(converted) ? [converted] : [])
+ );
+ }, [stackByField, from, to, query, filters]);
+
+ return (
+ <>
+ {loadingInitial || isLoadingSignals ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+ }
+);
+SignalsHistogram.displayName = 'SignalsHistogram';
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts
new file mode 100644
index 0000000000000..0245b9968cc36
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const STACK_BY_LABEL = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.stackByLabel',
+ {
+ defaultMessage: 'Stack by',
+ }
+);
+
+export const STACK_BY_RISK_SCORES = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.riskScoresDropDown',
+ {
+ defaultMessage: 'Risk scores',
+ }
+);
+
+export const STACK_BY_SEVERITIES = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.severitiesDropDown',
+ {
+ defaultMessage: 'Severities',
+ }
+);
+
+export const STACK_BY_DESTINATION_IPS = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.destinationIpsDropDown',
+ {
+ defaultMessage: 'Top destination IPs',
+ }
+);
+
+export const STACK_BY_SOURCE_IPS = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.sourceIpsDropDown',
+ {
+ defaultMessage: 'Top source IPs',
+ }
+);
+
+export const STACK_BY_ACTIONS = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventActionsDropDown',
+ {
+ defaultMessage: 'Top event actions',
+ }
+);
+
+export const STACK_BY_CATEGORIES = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.eventCategoriesDropDown',
+ {
+ defaultMessage: 'Top event categories',
+ }
+);
+
+export const STACK_BY_HOST_NAMES = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.hostNamesDropDown',
+ {
+ defaultMessage: 'Top host names',
+ }
+);
+
+export const STACK_BY_RULE_TYPES = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.ruleTypesDropDown',
+ {
+ defaultMessage: 'Top rule types',
+ }
+);
+
+export const STACK_BY_RULE_NAMES = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.rulesDropDown',
+ {
+ defaultMessage: 'Top rules',
+ }
+);
+
+export const STACK_BY_USERS = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.stackByOptions.usersDropDown',
+ {
+ defaultMessage: 'Top users',
+ }
+);
+
+export const HISTOGRAM_HEADER = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.headerTitle',
+ {
+ defaultMessage: 'Signal detection frequency',
+ }
+);
+
+export const ALL_OTHERS = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.allOthersGroupingLabel',
+ {
+ defaultMessage: 'All others',
+ }
+);
+
+export const VIEW_SIGNALS = i18n.translate(
+ 'xpack.siem.detectionEngine.signals.histogram.viewSignalsButtonLabel',
+ {
+ defaultMessage: 'View signals',
+ }
+);
+
+export const SHOWING_SIGNALS = (
+ totalSignalsFormatted: string,
+ totalSignals: number,
+ modifier: string
+) =>
+ i18n.translate('xpack.siem.detectionEngine.signals.histogram.showingSignalsTitle', {
+ values: { totalSignalsFormatted, totalSignals, modifier },
+ defaultMessage:
+ 'Showing: {modifier}{totalSignalsFormatted} {totalSignals, plural, =1 {signal} other {signals}}',
+ });
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts
new file mode 100644
index 0000000000000..4eb10852450ad
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface SignalsHistogramOption {
+ text: string;
+ value: string;
+}
+
+export interface HistogramData {
+ x: number;
+ y: number;
+ g: string;
+}
+
+export interface SignalsAggregation {
+ signalsByGrouping: {
+ buckets: SignalsGroupBucket[];
+ };
+}
+
+export interface SignalsBucket {
+ key_as_string: string;
+ key: number;
+ doc_count: number;
+}
+
+export interface SignalsGroupBucket {
+ key: string;
+ signals: {
+ buckets: SignalsBucket[];
+ };
+}
+
+export interface SignalsTotal {
+ value: number;
+ relation: string;
+}
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx
index a90022d4a34ce..fc1110e382847 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx
@@ -9,7 +9,7 @@ import { FormattedRelative } from '@kbn/i18n/react';
import React, { useState, useEffect } from 'react';
import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query';
-import { buildlastSignalsQuery } from './query.dsl';
+import { buildLastSignalsQuery } from './query.dsl';
import { Aggs } from './types';
interface SignalInfo {
@@ -26,14 +26,7 @@ export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => {
);
- let query = '';
- try {
- query = JSON.stringify(buildlastSignalsQuery(ruleId));
- } catch {
- query = '';
- }
-
- const [loading, signals] = useQuerySignals(query);
+ const [loading, signals] = useQuerySignals(buildLastSignalsQuery(ruleId));
useEffect(() => {
if (signals != null) {
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts
index 0b14aa17a9450..8cb07a4f8e6b5 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const buildlastSignalsQuery = (ruleId: string | undefined | null) => {
+export const buildLastSignalsQuery = (ruleId: string | undefined | null) => {
const queryFilter = [
{
bool: { should: [{ match: { 'signal.status': 'open' } }], minimum_should_match: 1 },
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx
index 8e5c3e9f13118..2a91a559ec0e4 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx
@@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButton, EuiSpacer, EuiPanel, EuiLoadingContent } from '@elastic/eui';
-import React from 'react';
+import { EuiButton, EuiLoadingContent, EuiPanel, EuiSpacer } from '@elastic/eui';
+import React, { useCallback } from 'react';
import { StickyContainer } from 'react-sticky';
+import { connect } from 'react-redux';
+import { ActionCreator } from 'typescript-fsa';
import { FiltersGlobal } from '../../components/filters_global';
import { HeaderPage } from '../../components/header_page';
import { SiemSearchBar } from '../../components/search_bar';
@@ -18,24 +20,63 @@ import { SpyRoute } from '../../utils/route/spy_routes';
import { SignalsTable } from './components/signals';
import * as signalsI18n from './components/signals/translations';
-import { SignalsCharts } from './components/signals_chart';
+import { SignalsHistogramPanel } from './components/signals_histogram_panel';
+import { Query } from '../../../../../../../src/plugins/data/common/query';
+import { esFilters } from '../../../../../../../src/plugins/data/common/es_query';
+import { inputsSelectors } from '../../store/inputs';
+import { State } from '../../store';
+import { InputsRange } from '../../store/inputs/model';
+import { signalsHistogramOptions } from './components/signals_histogram_panel/config';
import { useSignalInfo } from './components/signals_info';
import { DetectionEngineEmptyPage } from './detection_engine_empty_page';
import { DetectionEngineNoIndex } from './detection_engine_no_signal_index';
import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated';
import * as i18n from './translations';
import { HeaderSection } from '../../components/header_section';
+import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions';
+import { InputsModelId } from '../../store/inputs/constants';
-interface DetectionEngineComponentProps {
+interface OwnProps {
loading: boolean;
isSignalIndexExists: boolean | null;
isUserAuthenticated: boolean | null;
signalsIndex: string | null;
}
+interface ReduxProps {
+ filters: esFilters.Filter[];
+ query: Query;
+}
+
+export interface DispatchProps {
+ setAbsoluteRangeDatePicker: ActionCreator<{
+ id: InputsModelId;
+ from: number;
+ to: number;
+ }>;
+}
+
+type DetectionEngineComponentProps = OwnProps & ReduxProps & DispatchProps;
+
export const DetectionEngineComponent = React.memo(
- ({ loading, isSignalIndexExists, isUserAuthenticated, signalsIndex }) => {
+ ({
+ filters,
+ loading,
+ isSignalIndexExists,
+ isUserAuthenticated,
+ query,
+ setAbsoluteRangeDatePicker,
+ signalsIndex,
+ }) => {
const [lastSignals] = useSignalInfo({});
+
+ const updateDateRangeCallback = useCallback(
+ (min: number, max: number) => {
+ setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
+ },
+ [setAbsoluteRangeDatePicker]
+ );
+
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
return (
@@ -81,22 +122,33 @@ export const DetectionEngineComponent = React.memo
-
-
-
- {({ to, from }) =>
- !loading ? (
- isSignalIndexExists && (
-
- )
- ) : (
-
-
-
-
- )
- }
+ {({ to, from }) => (
+ <>
+
+
+
+
+ {!loading ? (
+ isSignalIndexExists && (
+
+ )
+ ) : (
+
+
+
+
+ )}
+ >
+ )}
@@ -115,3 +167,22 @@ export const DetectionEngineComponent = React.memo {
+ const getGlobalInputs = inputsSelectors.globalSelector();
+ return (state: State) => {
+ const globalInputs: InputsRange = getGlobalInputs(state);
+ const { query, filters } = globalInputs;
+
+ return {
+ query,
+ filters,
+ };
+ };
+};
+
+export const DetectionEngine = connect(makeMapStateToProps, {
+ setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker,
+})(DetectionEngineComponent);
+
+DetectionEngine.displayName = 'DetectionEngine';
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx
index 9c95c74cd62a5..c32cab7f933b2 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx
@@ -11,9 +11,9 @@ import { useSignalIndex } from '../../containers/detection_engine/signals/use_si
import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user';
import { CreateRuleComponent } from './rules/create';
-import { DetectionEngineComponent } from './detection_engine';
+import { DetectionEngine } from './detection_engine';
import { EditRuleComponent } from './rules/edit';
-import { RuleDetailsComponent } from './rules/details';
+import { RuleDetails } from './rules/details';
import { RulesComponent } from './rules';
const detectionEnginePath = `/:pageName(detection-engine)`;
@@ -44,7 +44,7 @@ export const DetectionEngineContainer = React.memo(() => {
return (
- (() => {
-
+
diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx
index 4d887c7cb5b6e..9b6998ab4a132 100644
--- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx
@@ -6,10 +6,12 @@
import { EuiButton, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-import React, { memo, useMemo } from 'react';
+import React, { memo, useCallback, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { StickyContainer } from 'react-sticky';
+import { ActionCreator } from 'typescript-fsa';
+import { connect } from 'react-redux';
import { FiltersGlobal } from '../../../../components/filters_global';
import { FormattedDate } from '../../../../components/formatted_date';
import { HeaderPage } from '../../../../components/header_page';
@@ -24,7 +26,7 @@ import {
} from '../../../../containers/source';
import { SpyRoute } from '../../../../utils/route/spy_routes';
-import { SignalsCharts } from '../../components/signals_chart';
+import { SignalsHistogramPanel } from '../../components/signals_histogram_panel';
import { SignalsTable } from '../../components/signals';
import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page';
import { useSignalInfo } from '../../components/signals_info';
@@ -39,198 +41,261 @@ import { getStepsData } from '../helpers';
import * as ruleI18n from '../translations';
import * as i18n from './translations';
import { GlobalTime } from '../../../../containers/global_time';
+import { signalsHistogramOptions } from '../../components/signals_histogram_panel/config';
+import { InputsModelId } from '../../../../store/inputs/constants';
+import { esFilters } from '../../../../../../../../../src/plugins/data/common/es_query';
+import { Query } from '../../../../../../../../../src/plugins/data/common/query';
+import { inputsSelectors } from '../../../../store/inputs';
+import { State } from '../../../../store';
+import { InputsRange } from '../../../../store/inputs/model';
+import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions';
-interface RuleDetailsComponentProps {
+interface OwnProps {
signalsIndex: string | null;
}
-export const RuleDetailsComponent = memo(({ signalsIndex }) => {
- const { ruleId } = useParams();
- const [loading, rule] = useRule(ruleId);
- const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({
- rule,
- detailsView: true,
- });
- const [lastSignals] = useSignalInfo({ ruleId });
-
- const title = loading === true || rule === null ? : rule.name;
- const subTitle = useMemo(
- () =>
- loading === true || rule === null ? (
-
- ) : (
- [
-
- ),
- }}
- />,
- rule?.updated_by != null ? (
+interface ReduxProps {
+ filters: esFilters.Filter[];
+ query: Query;
+}
+
+export interface DispatchProps {
+ setAbsoluteRangeDatePicker: ActionCreator<{
+ id: InputsModelId;
+ from: number;
+ to: number;
+ }>;
+}
+
+type RuleDetailsComponentProps = OwnProps & ReduxProps & DispatchProps;
+
+const RuleDetailsComponent = memo(
+ ({ filters, query, setAbsoluteRangeDatePicker, signalsIndex }) => {
+ const { ruleId } = useParams();
+ const [loading, rule] = useRule(ruleId);
+ const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({
+ rule,
+ detailsView: true,
+ });
+ const [lastSignals] = useSignalInfo({ ruleId });
+
+ const title = loading === true || rule === null ? : rule.name;
+ const subTitle = useMemo(
+ () =>
+ loading === true || rule === null ? (
+
+ ) : (
+ [
),
}}
- />
- ) : (
- ''
- ),
- ]
- ),
- [loading, rule]
- );
-
- const signalDefaultFilters = useMemo(
- () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []),
- [ruleId]
- );
- return (
- <>
-
- {({ indicesExist, indexPattern }) => {
- return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
-
- {({ to, from }) => (
-
-
-
-
-
-
-
- {detectionI18n.LAST_SIGNAL}
- {': '}
- {lastSignals}
- >
- ) : null,
- 'Status: Comming Soon',
- ]}
- title={title}
- >
-
-
-
-
+ />,
+ rule?.updated_by != null ? (
+
+ ),
+ }}
+ />
+ ) : (
+ ''
+ ),
+ ]
+ ),
+ [loading, rule]
+ );
-
-
-
-
- {ruleI18n.EDIT_RULE_SETTINGS}
-
-
-
-
-
-
-
-
-
-
-
-
- {defineRuleData != null && (
-
- )}
-
-
-
-
-
- {aboutRuleData != null && (
-
- )}
-
-
-
-
-
- {scheduleRuleData != null && (
- (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []),
+ [ruleId]
+ );
+
+ const signalMergedFilters = useMemo(() => [...signalDefaultFilters, ...filters], [
+ signalDefaultFilters,
+ filters,
+ ]);
+
+ const updateDateRangeCallback = useCallback(
+ (min: number, max: number) => {
+ setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max });
+ },
+ [setAbsoluteRangeDatePicker]
+ );
+
+ return (
+ <>
+
+ {({ indicesExist, indexPattern }) => {
+ return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? (
+
+ {({ to, from }) => (
+
+
+
+
+
+
+
+ {detectionI18n.LAST_SIGNAL}
+ {': '}
+ {lastSignals}
+ >
+ ) : null,
+ 'Status: Comming Soon',
+ ]}
+ title={title}
+ >
+
+
+
- )}
-
-
-
+
-
+
+
+
+
+ {ruleI18n.EDIT_RULE_SETTINGS}
+
+
+
+
+
+
-
+
-
+
+
+
+ {defineRuleData != null && (
+
+ )}
+
+
+
+
+
+ {aboutRuleData != null && (
+
+ )}
+
+
- {ruleId != null && (
-
+
+ {scheduleRuleData != null && (
+
+ )}
+
+
+
+
+
+
- )}
-
-
- )}
-
- ) : (
-
-
-
-
-
- );
- }}
-
-
-
- >
- );
-});
+
+
+
+ {ruleId != null && (
+
+ )}
+
+
+ )}
+
+ ) : (
+
+
+
+
+
+ );
+ }}
+
+
+
+ >
+ );
+ }
+);
RuleDetailsComponent.displayName = 'RuleDetailsComponent';
+
+const makeMapStateToProps = () => {
+ const getGlobalInputs = inputsSelectors.globalSelector();
+ return (state: State) => {
+ const globalInputs: InputsRange = getGlobalInputs(state);
+ const { query, filters } = globalInputs;
+
+ return {
+ query,
+ filters,
+ };
+ };
+};
+
+export const RuleDetails = connect(makeMapStateToProps, {
+ setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker,
+})(RuleDetailsComponent);
+
+RuleDetails.displayName = 'RuleDetails';