Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Fixes issues with the Raw events Top N view #121562

Merged
merged 2 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`, () => {
Expand All @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* 2.0.
*/

import { TimelineEventsType } from '../../../../common/types/timeline';
import type { Filter } from '@kbn/es-query';

import { TimelineEventsType, TimelineId } from '../../../../common/types/timeline';
import { SourcererScopeName } from '../../store/sourcerer/model';

import * as i18n from './translations';

Expand Down Expand Up @@ -65,3 +68,82 @@ 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 = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might make sense to use the consts from '@kbn/rule-data-utils' here and in the tests above just in case these are changed en masse again

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great suggestion @kqualters-elastic! implemented in eea414d

'kibana.alert.building_block_type', // an "Additional filters" option on the alerts table
'kibana.alert.rule.rule_id', // filters alerts to a single rule on the Security > Rules > details pages
'kibana.alert.rule.name', // not a built-in view filter, but frequently applied via the `Filter In` and `Filter Out` actions
'kibana.alert.rule.threat_mapping', // an "Additional filters" option on the alerts table
'kibana.alert.workflow_status', // open | acknowledged | closed filter
];

/**
* 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;
};
Loading