Revert "[Security Solution] Fixes issues with the Raw events Top N view (#121562) (#121583)"

This reverts commit d1dd97f4fe.
This commit is contained in:
Brian Seeders 2021-12-17 21:24:33 -05:00
parent d1dd97f4fe
commit 667a3d5c99
No known key found for this signature in database
GPG key ID: 424927146CC9A682
4 changed files with 33 additions and 520 deletions

View file

@ -5,155 +5,7 @@
* 2.0.
*/
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,
];
import { allEvents, defaultOptions, getOptions, rawEvents, alertEvents } from './helpers';
describe('getOptions', () => {
test(`it returns the default options when 'activeTimelineEventType' is undefined`, () => {
@ -172,123 +24,3 @@ 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);
});
});
});

View file

@ -5,60 +5,7 @@
* 2.0.
*/
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 { TimelineEventsType } from '../../../../common/types/timeline';
import * as i18n from './translations';
@ -118,126 +65,3 @@ 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;
};

View file

@ -8,8 +8,6 @@
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';
@ -128,59 +126,9 @@ 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 disables view selection when timelineId is undefined', () => {
const wrapper = mount(
<TestProviders>
<TopN {...testProps} timelineId={undefined} />
</TestProviders>
);
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(
<TestProviders>
<TopN {...testProps} timelineId={TimelineId.active} />
</TestProviders>
);
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(
<TestProviders>
<TopN {...testProps} timelineId={timelineId} />
</TestProviders>
);
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(
<TestProviders>
<TopN {...testProps} timelineId={timelineId} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="view-select"]').first().props().disabled).toBe(true);
});
test('it enables the view select by default', () => {
expect(wrapper.find('[data-test-subj="view-select"]').first().props().disabled).toBe(false);
});
});
@ -255,6 +203,10 @@ 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()

View file

@ -6,7 +6,9 @@
*/
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';
@ -15,15 +17,13 @@ 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 { useSourcererDataView } from '../../containers/sourcerer';
import {
isDetectionsAlertsTable,
getSourcererScopeName,
removeIgnoredAlertFilters,
TopNOption,
} from './helpers';
import { 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,7 +89,19 @@ const TopNComponent: React.FC<Props> = ({
(value: string) => setView(value as TimelineEventsType),
[setView]
);
const { selectedPatterns } = useSourcererDataView(getSourcererScopeName({ timelineId, view }));
const indicesSelector = useMemo(getIndicesSelector, []);
const { all: allIndices, raw: rawIndices } = useSelector<State, IndicesSelector>(
(state) =>
indicesSelector(
state,
timelineId != null
? defaultView === 'alert'
? SourcererScopeName.detections
: SourcererScopeName.timeline
: SourcererScopeName.default
),
deepEqual
);
useEffect(() => {
setView(defaultView);
@ -99,20 +111,13 @@ const TopNComponent: React.FC<Props> = ({
() => (
<ViewSelect
data-test-subj="view-select"
disabled={!isDetectionsAlertsTable(timelineId)}
disabled={options.length === 1}
onChange={onViewSelected}
options={options}
valueOfSelected={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]
[onViewSelected, options, view]
);
return (
@ -129,11 +134,11 @@ const TopNComponent: React.FC<Props> = ({
<EventsByDataset
combinedQueries={combinedQueries}
deleteQuery={deleteQuery}
filters={applicableFilters}
filters={filters}
from={from}
headerChildren={headerChildren}
indexPattern={indexPattern}
indexNames={selectedPatterns}
indexNames={view === 'raw' ? rawIndices : allIndices}
onlyField={field}
paddingSize={paddingSize}
query={query}
@ -148,7 +153,7 @@ const TopNComponent: React.FC<Props> = ({
) : (
<SignalsByCategory
combinedQueries={combinedQueries}
filters={applicableFilters}
filters={filters}
headerChildren={headerChildren}
onlyField={field}
paddingSize={paddingSize}