From 6a173ba19d6b29d2097bb266af5f72e349636f85 Mon Sep 17 00:00:00 2001
From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com>
Date: Mon, 5 Oct 2020 14:39:40 -0600
Subject: [PATCH] [Security Solution][Detections] Adds sequence callout in the
exceptions modals for eql rule types (#79007)
---
.../common/detection_engine/utils.test.ts | 39 +++++++++-
.../common/detection_engine/utils.ts | 8 +++
.../add_exception_modal/index.test.tsx | 72 +++++++++++++++++++
.../exceptions/add_exception_modal/index.tsx | 18 +++++
.../add_exception_modal/translations.ts | 8 +++
.../edit_exception_modal/index.test.tsx | 65 ++++++++++++++++-
.../exceptions/edit_exception_modal/index.tsx | 17 +++++
.../edit_exception_modal/translations.ts | 8 +++
8 files changed, 233 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
index ea50acc9b46be..202733574b69f 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { hasLargeValueList, hasNestedEntry, isThreatMatchRule } from './utils';
+import { hasEqlSequenceQuery, hasLargeValueList, hasNestedEntry, isThreatMatchRule } from './utils';
import { EntriesArray } from '../shared_imports';
describe('#hasLargeValueList', () => {
@@ -113,3 +113,40 @@ describe('#hasNestedEntry', () => {
});
});
});
+
+describe('#hasEqlSequenceQuery', () => {
+ describe('when a non-sequence query is passed', () => {
+ const query = 'process where process.name == "regsvr32.exe"';
+ it('should return false', () => {
+ expect(hasEqlSequenceQuery(query)).toEqual(false);
+ });
+ });
+
+ describe('when a sequence query is passed', () => {
+ const query = 'sequence [process where process.name = "test.exe"]';
+ it('should return true', () => {
+ expect(hasEqlSequenceQuery(query)).toEqual(true);
+ });
+ });
+
+ describe('when a sequence query is passed with extra white space and escape characters', () => {
+ const query = '\tsequence \n [process where process.name = "test.exe"]';
+ it('should return true', () => {
+ expect(hasEqlSequenceQuery(query)).toEqual(true);
+ });
+ });
+
+ describe('when a non-sequence query is passed using the word sequence', () => {
+ const query = 'sequence where true';
+ it('should return false', () => {
+ expect(hasEqlSequenceQuery(query)).toEqual(false);
+ });
+ });
+
+ describe('when a non-sequence query is passed using the word sequence with extra white space and escape characters', () => {
+ const query = ' sequence\nwhere\ttrue';
+ it('should return false', () => {
+ expect(hasEqlSequenceQuery(query)).toEqual(false);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts
index d7b23755699f5..d35c5980d96a2 100644
--- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts
+++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts
@@ -17,6 +17,14 @@ export const hasNestedEntry = (entries: EntriesArray): boolean => {
return found.length > 0;
};
+export const hasEqlSequenceQuery = (ruleQuery: string | undefined): boolean => {
+ if (ruleQuery != null) {
+ const parsedQuery = ruleQuery.trim().split(/[ \t\r\n]+/);
+ return parsedQuery[0] === 'sequence' && parsedQuery[1] !== 'where';
+ }
+ return false;
+};
+
export const isEqlRule = (ruleType: Type | undefined): boolean => ruleType === 'eql';
export const isThresholdRule = (ruleType: Type | undefined): boolean => ruleType === 'threshold';
export const isQueryRule = (ruleType: Type | undefined): boolean =>
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
index 037462839c72d..35bd5ee572160 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
@@ -25,6 +25,11 @@ import * as helpers from '../helpers';
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { EntriesArray } from '../../../../../../lists/common/schemas/types';
import { ExceptionListItemSchema } from '../../../../../../lists/common';
+import {
+ getRulesEqlSchemaMock,
+ getRulesSchemaMock,
+} from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks';
+import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
jest.mock('../../../../common/lib/kibana');
@@ -34,6 +39,7 @@ jest.mock('../use_add_exception');
jest.mock('../use_fetch_or_create_rule_exception_list');
jest.mock('../builder');
jest.mock('../../../../shared_imports');
+jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async');
describe('When the add exception modal is opened', () => {
const ruleName = 'test rule';
@@ -73,6 +79,9 @@ describe('When the add exception modal is opened', () => {
},
]);
(useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' });
+ (useRuleAsync as jest.Mock).mockImplementation(() => ({
+ rule: getRulesSchemaMock(),
+ }));
});
afterEach(() => {
@@ -193,6 +202,9 @@ describe('When the add exception modal is opened', () => {
it('should contain the endpoint specific documentation text', () => {
expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy();
});
+ it('should not display the eql sequence callout', () => {
+ expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy();
+ });
});
describe('when there is alert data passed to a detection list exception', () => {
@@ -241,6 +253,66 @@ describe('When the add exception modal is opened', () => {
.getDOMNode()
).toBeDisabled();
});
+ it('should not display the eql sequence callout', () => {
+ expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy();
+ });
+ });
+
+ describe('when there is an exception being created on a sequence eql rule type', () => {
+ let wrapper: ReactWrapper;
+ beforeEach(async () => {
+ const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } };
+ (useRuleAsync as jest.Mock).mockImplementation(() => ({
+ rule: {
+ ...getRulesEqlSchemaMock(),
+ query:
+ 'sequence [process where process.name = "test.exe"] [process where process.name = "explorer.exe"]',
+ },
+ }));
+ wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+ const callProps = ExceptionBuilderComponent.mock.calls[0][0];
+ await waitFor(() =>
+ callProps.onChange({ exceptionItems: [getExceptionListItemSchemaMock()] })
+ );
+ });
+ it('has the add exception button enabled', () => {
+ expect(
+ wrapper.find('button[data-test-subj="add-exception-confirm-button"]').getDOMNode()
+ ).not.toBeDisabled();
+ });
+ it('should render the exception builder', () => {
+ expect(wrapper.find('[data-test-subj="alert-exception-builder"]').exists()).toBeTruthy();
+ });
+ it('should not prepopulate endpoint items', () => {
+ expect(defaultEndpointItems).not.toHaveBeenCalled();
+ });
+ it('should render the close on add exception checkbox', () => {
+ expect(
+ wrapper.find('[data-test-subj="close-alert-on-add-add-exception-checkbox"]').exists()
+ ).toBeTruthy();
+ });
+ it('should have the bulk close checkbox disabled', () => {
+ expect(
+ wrapper
+ .find('input[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]')
+ .getDOMNode()
+ ).toBeDisabled();
+ });
+ it('should display the eql sequence callout', () => {
+ expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).toBeTruthy();
+ });
});
describe('when there is bulk-closeable alert data passed to an endpoint list exception', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
index ad5bc98243467..bf483387580ce 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
@@ -19,7 +19,9 @@ import {
EuiSpacer,
EuiFormRow,
EuiText,
+ EuiCallOut,
} from '@elastic/eui';
+import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils';
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
import {
ExceptionListItemSchema,
@@ -315,6 +317,13 @@ export const AddExceptionModal = memo(function AddExceptionModal({
const addExceptionMessage =
exceptionListType === 'endpoint' ? i18n.ADD_ENDPOINT_EXCEPTION : i18n.ADD_EXCEPTION;
+ const isRuleEQLSequenceStatement = useMemo((): boolean => {
+ if (maybeRule != null) {
+ return isEqlRule(maybeRule.type) && hasEqlSequenceQuery(maybeRule.query);
+ }
+ return false;
+ }, [maybeRule]);
+
return (
@@ -353,6 +362,15 @@ export const AddExceptionModal = memo(function AddExceptionModal({
ruleExceptionList && (
<>
+ {isRuleEQLSequenceStatement && (
+ <>
+
+
+ >
+ )}
{i18n.EXCEPTION_BUILDER_INFO}
{
const ruleName = 'test rule';
@@ -58,6 +64,9 @@ describe('When the edit exception modal is opened', () => {
},
]);
(useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' });
+ (useRuleAsync as jest.Mock).mockImplementation(() => ({
+ rule: getRulesSchemaMock(),
+ }));
});
afterEach(() => {
@@ -190,7 +199,58 @@ describe('When the edit exception modal is opened', () => {
});
});
- describe('when an detection exception with entries is passed', () => {
+ describe('when an exception assigned to a sequence eql rule type is passed', () => {
+ let wrapper: ReactWrapper;
+ beforeEach(async () => {
+ (useRuleAsync as jest.Mock).mockImplementation(() => ({
+ rule: {
+ ...getRulesEqlSchemaMock(),
+ query:
+ 'sequence [process where process.name = "test.exe"] [process where process.name = "explorer.exe"]',
+ },
+ }));
+ wrapper = mount(
+ ({ eui: euiLightVars, darkMode: false })}>
+
+
+ );
+ const callProps = ExceptionBuilderComponent.mock.calls[0][0];
+ await waitFor(() => {
+ callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] });
+ });
+ });
+ it('has the edit exception button enabled', () => {
+ expect(
+ wrapper.find('button[data-test-subj="edit-exception-confirm-button"]').getDOMNode()
+ ).not.toBeDisabled();
+ });
+ it('renders the exceptions builder', () => {
+ expect(wrapper.find('[data-test-subj="edit-exception-modal-builder"]').exists()).toBeTruthy();
+ });
+ it('should not contain the endpoint specific documentation text', () => {
+ expect(wrapper.find('[data-test-subj="edit-exception-endpoint-text"]').exists()).toBeFalsy();
+ });
+ it('should have the bulk close checkbox disabled', () => {
+ expect(
+ wrapper
+ .find('input[data-test-subj="close-alert-on-add-edit-exception-checkbox"]')
+ .getDOMNode()
+ ).toBeDisabled();
+ });
+ it('should display the eql sequence callout', () => {
+ expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).toBeTruthy();
+ });
+ });
+
+ describe('when a detection exception with entries is passed', () => {
let wrapper: ReactWrapper;
beforeEach(async () => {
wrapper = mount(
@@ -229,6 +289,9 @@ describe('When the edit exception modal is opened', () => {
.getDOMNode()
).toBeDisabled();
});
+ it('should not display the eql sequence callout', () => {
+ expect(wrapper.find('[data-test-subj="eql-sequence-callout"]').exists()).not.toBeTruthy();
+ });
});
describe('when an exception with no entries is passed', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
index 08f7e3af90d0c..257c8e8c4d873 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
@@ -22,6 +22,7 @@ import {
EuiCallOut,
} from '@elastic/eui';
+import { hasEqlSequenceQuery, isEqlRule } from '../../../../../common/detection_engine/utils';
import { useFetchIndex } from '../../../containers/source';
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
@@ -246,6 +247,13 @@ export const EditExceptionModal = memo(function EditExceptionModal({
signalIndexName,
]);
+ const isRuleEQLSequenceStatement = useMemo((): boolean => {
+ if (maybeRule != null) {
+ return isEqlRule(maybeRule.type) && hasEqlSequenceQuery(maybeRule.query);
+ }
+ return false;
+ }, [maybeRule]);
+
return (
@@ -265,6 +273,15 @@ export const EditExceptionModal = memo(function EditExceptionModal({
{!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && (
<>
+ {isRuleEQLSequenceStatement && (
+ <>
+
+
+ >
+ )}
{i18n.EXCEPTION_BUILDER_INFO}