diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx
index bd1a2892262a..c13be767f9be 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx
@@ -5,7 +5,155 @@
* 2.0.
*/
-import { allEvents, defaultOptions, getOptions, rawEvents, alertEvents } from './helpers';
+import type { Filter } from '@kbn/es-query';
+
+import { TimelineId } from '../../../../common/types/timeline';
+import {
+ alertEvents,
+ allEvents,
+ defaultOptions,
+ getOptions,
+ getSourcererScopeName,
+ isDetectionsAlertsTable,
+ rawEvents,
+ removeIgnoredAlertFilters,
+ shouldIgnoreAlertFilters,
+} from './helpers';
+import { SourcererScopeName } from '../../store/sourcerer/model';
+
+/** the following `TimelineId`s are detection alert tables */
+const detectionAlertsTimelines = [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage];
+
+/** the following `TimelineId`s are NOT detection alert tables */
+const otherTimelines = [
+ TimelineId.hostsPageEvents,
+ TimelineId.hostsPageExternalAlerts,
+ TimelineId.networkPageExternalAlerts,
+ TimelineId.uebaPageExternalAlerts,
+ TimelineId.active,
+ TimelineId.casePage,
+ TimelineId.test,
+ TimelineId.alternateTest,
+];
+
+const othersWithoutActive = otherTimelines.filter((x) => x !== TimelineId.active);
+
+const hostNameFilter: Filter = {
+ meta: {
+ alias: null,
+ negate: false,
+ disabled: false,
+ type: 'phrase',
+ key: 'host.name',
+ params: {
+ query: 'Host-abcd',
+ },
+ },
+ query: {
+ match_phrase: {
+ 'host.name': {
+ query: 'Host-abcd',
+ },
+ },
+ },
+};
+
+const buildingBlockTypeFilter: Filter = {
+ meta: {
+ alias: null,
+ negate: true,
+ disabled: false,
+ type: 'exists',
+ key: 'kibana.alert.building_block_type',
+ value: 'exists',
+ },
+ query: {
+ exists: {
+ field: 'kibana.alert.building_block_type',
+ },
+ },
+};
+
+const ruleIdFilter: Filter = {
+ meta: {
+ alias: null,
+ negate: false,
+ disabled: false,
+ type: 'phrase',
+ key: 'kibana.alert.rule.rule_id',
+ params: {
+ query: '32a4aefa-80fb-4716-bc0f-3f7bb1f14929',
+ },
+ },
+ query: {
+ match_phrase: {
+ 'kibana.alert.rule.rule_id': '32a4aefa-80fb-4716-bc0f-3f7bb1f14929',
+ },
+ },
+};
+
+const ruleNameFilter: Filter = {
+ meta: {
+ alias: null,
+ negate: false,
+ disabled: false,
+ type: 'phrase',
+ key: 'kibana.alert.rule.name',
+ params: {
+ query: 'baz',
+ },
+ },
+ query: {
+ match_phrase: {
+ 'kibana.alert.rule.name': {
+ query: 'baz',
+ },
+ },
+ },
+};
+
+const threatMappingFilter: Filter = {
+ meta: {
+ alias: null,
+ negate: true,
+ disabled: false,
+ type: 'exists',
+ key: 'kibana.alert.rule.threat_mapping',
+ value: 'exists',
+ },
+ query: {
+ exists: {
+ field: 'kibana.alert.rule.threat_mapping',
+ },
+ },
+};
+
+const workflowStatusFilter: Filter = {
+ meta: {
+ alias: null,
+ negate: false,
+ disabled: false,
+ type: 'phrase',
+ key: 'kibana.alert.workflow_status',
+ params: {
+ query: 'open',
+ },
+ },
+ query: {
+ term: {
+ 'kibana.alert.workflow_status': 'open',
+ },
+ },
+};
+
+const allFilters = [
+ hostNameFilter,
+ buildingBlockTypeFilter,
+ ruleIdFilter,
+ ruleNameFilter,
+ threatMappingFilter,
+ workflowStatusFilter,
+];
describe('getOptions', () => {
test(`it returns the default options when 'activeTimelineEventType' is undefined`, () => {
@@ -24,3 +172,123 @@ describe('getOptions', () => {
expect(getOptions('alert')).toEqual(alertEvents);
});
});
+
+describe('isDetectionsAlertsTable', () => {
+ detectionAlertsTimelines.forEach((timelineId) =>
+ test(`it returns true for detections alerts table '${timelineId}'`, () => {
+ expect(isDetectionsAlertsTable(timelineId)).toEqual(true);
+ })
+ );
+
+ otherTimelines.forEach((timelineId) =>
+ test(`it returns false for (NON alert table) timeline '${timelineId}'`, () => {
+ expect(isDetectionsAlertsTable(timelineId)).toEqual(false);
+ })
+ );
+});
+
+describe('shouldIgnoreAlertFilters', () => {
+ detectionAlertsTimelines.forEach((timelineId) => {
+ test(`it returns true when the view is 'raw' for detections alerts table '${timelineId}'`, () => {
+ const view = 'raw';
+ expect(shouldIgnoreAlertFilters({ timelineId, view })).toEqual(true);
+ });
+
+ test(`it returns false when the view is NOT 'raw' for detections alerts table '${timelineId}'`, () => {
+ const view = 'alert'; // the default selection for detection alert tables
+ expect(shouldIgnoreAlertFilters({ timelineId, view })).toEqual(false);
+ });
+ });
+
+ otherTimelines.forEach((timelineId) => {
+ test(`it returns false when the view is 'raw' for (NON alert table) timeline'${timelineId}'`, () => {
+ const view = 'raw';
+ expect(shouldIgnoreAlertFilters({ timelineId, view })).toEqual(false);
+ });
+
+ test(`it returns false when the view is NOT 'raw' for (NON alert table) timeline '${timelineId}'`, () => {
+ const view = 'alert';
+ expect(shouldIgnoreAlertFilters({ timelineId, view })).toEqual(false);
+ });
+ });
+});
+
+describe('removeIgnoredAlertFilters', () => {
+ detectionAlertsTimelines.forEach((timelineId) => {
+ test(`it removes the ignored alert filters when the view is 'raw' for detections alerts table '${timelineId}'`, () => {
+ const view = 'raw';
+ expect(removeIgnoredAlertFilters({ filters: allFilters, timelineId, view })).toEqual([
+ hostNameFilter,
+ ]);
+ });
+
+ test(`it does NOT remove any filters when the view is NOT 'raw' for detections alerts table '${timelineId}'`, () => {
+ const view = 'alert';
+ expect(removeIgnoredAlertFilters({ filters: allFilters, timelineId, view })).toEqual(
+ allFilters
+ );
+ });
+ });
+
+ otherTimelines.forEach((timelineId) => {
+ test(`it does NOT remove any filters when the view is 'raw' for (NON alert table) '${timelineId}'`, () => {
+ const view = 'alert';
+ expect(removeIgnoredAlertFilters({ filters: allFilters, timelineId, view })).toEqual(
+ allFilters
+ );
+ });
+
+ test(`it does NOT remove any filters when the view is NOT 'raw' for (NON alert table '${timelineId}'`, () => {
+ const view = 'alert';
+ expect(removeIgnoredAlertFilters({ filters: allFilters, timelineId, view })).toEqual(
+ allFilters
+ );
+ });
+ });
+});
+
+describe('getSourcererScopeName', () => {
+ detectionAlertsTimelines.forEach((timelineId) => {
+ test(`it returns the 'default' SourcererScopeName when the view is 'raw' for detections alerts table '${timelineId}'`, () => {
+ const view = 'raw';
+ expect(getSourcererScopeName({ timelineId, view })).toEqual(SourcererScopeName.default);
+ });
+
+ test(`it returns the 'detections' SourcererScopeName when the view is NOT 'raw' for detections alerts table '${timelineId}'`, () => {
+ const view = 'alert';
+ expect(getSourcererScopeName({ timelineId, view })).toEqual(SourcererScopeName.detections);
+ });
+ });
+
+ test(`it returns the 'default' SourcererScopeName when timelineId is undefined'`, () => {
+ const timelineId = undefined;
+ const view = 'raw';
+ expect(getSourcererScopeName({ timelineId, view })).toEqual(SourcererScopeName.default);
+ });
+
+ test(`it returns the 'timeline' SourcererScopeName when the view is 'raw' for the active timeline '${TimelineId.active}'`, () => {
+ const view = 'raw';
+ expect(getSourcererScopeName({ timelineId: TimelineId.active, view })).toEqual(
+ SourcererScopeName.timeline
+ );
+ });
+
+ test(`it returns the 'timeline' SourcererScopeName when the view is NOT 'raw' for the active timeline '${TimelineId.active}'`, () => {
+ const view = 'all';
+ expect(getSourcererScopeName({ timelineId: TimelineId.active, view })).toEqual(
+ SourcererScopeName.timeline
+ );
+ });
+
+ othersWithoutActive.forEach((timelineId) => {
+ test(`it returns the 'default' SourcererScopeName when the view is 'raw' for (NON alert table) timeline '${timelineId}'`, () => {
+ const view = 'raw';
+ expect(getSourcererScopeName({ timelineId, view })).toEqual(SourcererScopeName.default);
+ });
+
+ test(`it returns the 'default' SourcererScopeName when the view is NOT 'raw' for detections alerts table '${timelineId}'`, () => {
+ const view = 'alert';
+ expect(getSourcererScopeName({ timelineId, view })).toEqual(SourcererScopeName.default);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts
index c7a931030826..37eeac326afd 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts
@@ -5,7 +5,60 @@
* 2.0.
*/
-import { TimelineEventsType } from '../../../../common/types/timeline';
+import type { Filter } from '@kbn/es-query';
+import {
+ ALERT_ACTION_GROUP,
+ ALERT_BUILDING_BLOCK_TYPE,
+ ALERT_DURATION,
+ ALERT_END,
+ ALERT_EVALUATION_THRESHOLD,
+ ALERT_EVALUATION_VALUE,
+ ALERT_INSTANCE_ID,
+ ALERT_NAMESPACE,
+ ALERT_REASON,
+ ALERT_RISK_SCORE,
+ ALERT_RULE_AUTHOR,
+ ALERT_RULE_CATEGORY,
+ ALERT_RULE_CONSUMER,
+ ALERT_RULE_CREATED_AT,
+ ALERT_RULE_CREATED_BY,
+ ALERT_RULE_DESCRIPTION,
+ ALERT_RULE_ENABLED,
+ ALERT_RULE_FROM,
+ ALERT_RULE_INTERVAL,
+ ALERT_RULE_LICENSE,
+ ALERT_RULE_NAME,
+ ALERT_RULE_NAMESPACE,
+ ALERT_RULE_NOTE,
+ ALERT_RULE_PARAMETERS,
+ ALERT_RULE_PRODUCER,
+ ALERT_RULE_REFERENCES,
+ ALERT_RULE_RISK_SCORE,
+ ALERT_RULE_RISK_SCORE_MAPPING,
+ ALERT_RULE_RULE_ID,
+ ALERT_RULE_RULE_NAME_OVERRIDE,
+ ALERT_RULE_SEVERITY,
+ ALERT_RULE_SEVERITY_MAPPING,
+ ALERT_RULE_TAGS,
+ ALERT_RULE_TO,
+ ALERT_RULE_TYPE,
+ ALERT_RULE_TYPE_ID,
+ ALERT_RULE_UPDATED_AT,
+ ALERT_RULE_UPDATED_BY,
+ ALERT_RULE_UUID,
+ ALERT_RULE_VERSION,
+ ALERT_SEVERITY,
+ ALERT_START,
+ ALERT_STATUS,
+ ALERT_SYSTEM_STATUS,
+ ALERT_UUID,
+ ALERT_WORKFLOW_REASON,
+ ALERT_WORKFLOW_STATUS,
+ ALERT_WORKFLOW_USER,
+} from '@kbn/rule-data-utils';
+
+import { TimelineEventsType, TimelineId } from '../../../../common/types/timeline';
+import { SourcererScopeName } from '../../store/sourcerer/model';
import * as i18n from './translations';
@@ -65,3 +118,126 @@ export const getOptions = (activeTimelineEventsType?: TimelineEventsType): TopNO
return defaultOptions;
}
};
+
+/** returns true if the specified timelineId is a detections alert table */
+export const isDetectionsAlertsTable = (timelineId: string | undefined): boolean =>
+ timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage;
+
+/**
+ * The following fields are used to filter alerts tables, (i.e. tables in the
+ * `Security > Alert` and `Security > Rule > Details` pages). These fields,
+ * MUST be ignored when showing Top N alerts for `raw` documents, because
+ * the raw documents don't include them.
+ */
+export const IGNORED_ALERT_FILTERS = [
+ ALERT_ACTION_GROUP,
+ ALERT_BUILDING_BLOCK_TYPE, // an "Additional filters" option on the alerts table
+ ALERT_DURATION,
+ ALERT_END,
+ ALERT_EVALUATION_THRESHOLD,
+ ALERT_EVALUATION_VALUE,
+ ALERT_INSTANCE_ID,
+ ALERT_NAMESPACE,
+ ALERT_RULE_NAMESPACE,
+ ALERT_RULE_CONSUMER,
+ ALERT_RULE_PRODUCER,
+ ALERT_REASON,
+ ALERT_RISK_SCORE,
+ ALERT_STATUS,
+ ALERT_WORKFLOW_REASON,
+ ALERT_WORKFLOW_STATUS, // open | acknowledged | closed filter
+ ALERT_WORKFLOW_USER,
+ ALERT_RULE_AUTHOR,
+ ALERT_RULE_CREATED_AT,
+ ALERT_RULE_CREATED_BY,
+ ALERT_RULE_DESCRIPTION,
+ ALERT_RULE_ENABLED,
+ ALERT_RULE_FROM,
+ ALERT_RULE_INTERVAL,
+ ALERT_RULE_LICENSE,
+ ALERT_RULE_NAME, // not a built-in view filter, but frequently applied via the `Filter In` and `Filter Out` actions
+ ALERT_RULE_NOTE,
+ ALERT_RULE_PARAMETERS,
+ ALERT_RULE_REFERENCES,
+ ALERT_RULE_RISK_SCORE,
+ ALERT_RULE_RISK_SCORE_MAPPING,
+ ALERT_RULE_RULE_ID, // filters alerts to a single rule on the Security > Rules > details pages
+ ALERT_RULE_RULE_NAME_OVERRIDE,
+ ALERT_RULE_SEVERITY_MAPPING,
+ ALERT_RULE_TAGS,
+ 'kibana.alert.rule.threat_mapping', // an "Additional filters" option on the alerts table
+ ALERT_RULE_TO,
+ ALERT_RULE_TYPE,
+ ALERT_RULE_TYPE_ID,
+ ALERT_RULE_UPDATED_AT,
+ ALERT_RULE_UPDATED_BY,
+ ALERT_RULE_UUID,
+ ALERT_RULE_CATEGORY,
+ ALERT_RULE_VERSION,
+ ALERT_RULE_SEVERITY,
+ ALERT_SEVERITY,
+ ALERT_START,
+ ALERT_SYSTEM_STATUS,
+ ALERT_UUID,
+];
+
+/**
+ * returns true if the Top N query should ignore filters specific to alerts
+ * when querying raw documents
+ *
+ * @see IGNORED_ALERT_FILTERS
+ */
+export const shouldIgnoreAlertFilters = ({
+ timelineId,
+ view,
+}: {
+ timelineId: string | undefined;
+ view: TimelineEventsType;
+}): boolean => view === 'raw' && isDetectionsAlertsTable(timelineId);
+
+/**
+ * returns a new set of `filters` that don't contain the fields specified in
+ * `IGNORED_ALERT_FILTERS` when they should be ignored
+ *
+ * @see IGNORED_ALERT_FILTERS
+ */
+export const removeIgnoredAlertFilters = ({
+ filters,
+ timelineId,
+ view,
+}: {
+ filters: Filter[];
+ timelineId: string | undefined;
+ view: TimelineEventsType;
+}): Filter[] => {
+ if (!shouldIgnoreAlertFilters({ timelineId, view })) {
+ return filters; // unmodified filters
+ }
+
+ return filters.filter((x) => !IGNORED_ALERT_FILTERS.includes(`${x.meta.key}`));
+};
+
+/** returns the SourcererScopeName applicable to the specified timelineId and view */
+export const getSourcererScopeName = ({
+ timelineId,
+ view,
+}: {
+ timelineId: string | undefined;
+ view: TimelineEventsType;
+}): SourcererScopeName => {
+ // When alerts should be ignored, use the `default` Sourcerer scope,
+ // because it does NOT include alert indexes:
+ if (shouldIgnoreAlertFilters({ timelineId, view })) {
+ return SourcererScopeName.default; // no alerts in this scope
+ }
+
+ if (isDetectionsAlertsTable(timelineId)) {
+ return SourcererScopeName.detections;
+ }
+
+ if (timelineId === TimelineId.active) {
+ return SourcererScopeName.timeline;
+ }
+
+ return SourcererScopeName.default;
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx
index 8cb56d7581b3..c5a8e9314535 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx
@@ -8,6 +8,8 @@
import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
import { waitFor } from '@testing-library/react';
+
+import { TimelineId } from '../../../../common/types';
import '../../mock/match_media';
import { TestProviders, mockIndexPattern } from '../../mock';
@@ -126,9 +128,59 @@ describe('TopN', () => {
expect(toggleTopN).toHaveBeenCalled();
});
+ });
+
+ describe('view selection', () => {
+ const detectionAlertsTimelines = [
+ TimelineId.detectionsPage,
+ TimelineId.detectionsRulesDetailsPage,
+ ];
+
+ const nonDetectionAlertTables = [
+ TimelineId.hostsPageEvents,
+ TimelineId.hostsPageExternalAlerts,
+ TimelineId.networkPageExternalAlerts,
+ TimelineId.casePage,
+ ];
- test('it enables the view select by default', () => {
- expect(wrapper.find('[data-test-subj="view-select"]').first().props().disabled).toBe(false);
+ test('it disables view selection when timelineId is undefined', () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="view-select"]').first().props().disabled).toBe(true);
+ });
+
+ test('it disables view selection when timelineId is `active`', () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="view-select"]').first().props().disabled).toBe(true);
+ });
+
+ detectionAlertsTimelines.forEach((timelineId) => {
+ test(`it enables view selection for detection alert table '${timelineId}'`, () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="view-select"]').first().props().disabled).toBe(false);
+ });
+ });
+
+ nonDetectionAlertTables.forEach((timelineId) => {
+ test(`it disables view selection for NON detection alert table '${timelineId}'`, () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(wrapper.find('[data-test-subj="view-select"]').first().props().disabled).toBe(true);
+ });
});
});
@@ -203,10 +255,6 @@ describe('TopN', () => {
);
});
- test(`it disables the view select when 'options' contains only one entry`, () => {
- expect(wrapper.find('[data-test-subj="view-select"]').first().props().disabled).toBe(true);
- });
-
test(`it renders EventsByDataset when defaultView is 'all'`, () => {
expect(
wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists()
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx
index f0d7000e4ed9..a450b56d70a9 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx
@@ -6,9 +6,7 @@
*/
import { EuiButtonIcon, EuiSuperSelect } from '@elastic/eui';
-import deepEqual from 'fast-deep-equal';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { useSelector } from 'react-redux';
import styled from 'styled-components';
import type { DataViewBase, Filter, Query } from '@kbn/es-query';
@@ -17,13 +15,15 @@ import { EventsByDataset } from '../../../overview/components/events_by_dataset'
import { SignalsByCategory } from '../../../overview/components/signals_by_category';
import { InputsModelId } from '../../store/inputs/constants';
import { TimelineEventsType } from '../../../../common/types/timeline';
-
-import { TopNOption } from './helpers';
+import { useSourcererDataView } from '../../containers/sourcerer';
+import {
+ isDetectionsAlertsTable,
+ getSourcererScopeName,
+ removeIgnoredAlertFilters,
+ TopNOption,
+} from './helpers';
import * as i18n from './translations';
-import { getIndicesSelector, IndicesSelector } from './selectors';
-import { State } from '../../store';
import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types';
-import { SourcererScopeName } from '../../store/sourcerer/model';
const TopNContainer = styled.div`
min-width: 600px;
@@ -89,19 +89,7 @@ const TopNComponent: React.FC = ({
(value: string) => setView(value as TimelineEventsType),
[setView]
);
- const indicesSelector = useMemo(getIndicesSelector, []);
- const { all: allIndices, raw: rawIndices } = useSelector(
- (state) =>
- indicesSelector(
- state,
- timelineId != null
- ? defaultView === 'alert'
- ? SourcererScopeName.detections
- : SourcererScopeName.timeline
- : SourcererScopeName.default
- ),
- deepEqual
- );
+ const { selectedPatterns } = useSourcererDataView(getSourcererScopeName({ timelineId, view }));
useEffect(() => {
setView(defaultView);
@@ -111,13 +99,20 @@ const TopNComponent: React.FC = ({
() => (
),
- [onViewSelected, options, view]
+ [onViewSelected, options, timelineId, view]
+ );
+
+ // alert workflow statuses (e.g. open | closed) and other alert-specific
+ // filters must be ignored when viewing raw alerts
+ const applicableFilters = useMemo(
+ () => removeIgnoredAlertFilters({ filters, timelineId, view }),
+ [filters, timelineId, view]
);
return (
@@ -134,11 +129,11 @@ const TopNComponent: React.FC = ({
= ({
) : (