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

[7.x] [Security Solution][Detections] Adds sequence callout in the exceptions modals for eql rule types (#79007) #79562

Merged
merged 2 commits into from
Oct 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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';
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<AddExceptionModal
ruleId={'123'}
ruleIndices={['filebeat-*']}
ruleName={ruleName}
exceptionListType={'detection'}
onCancel={jest.fn()}
onConfirm={jest.fn()}
alertData={alertDataMock}
/>
</ThemeProvider>
);
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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<EuiOverlayMask onClick={onCancel}>
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
Expand Down Expand Up @@ -353,6 +362,15 @@ export const AddExceptionModal = memo(function AddExceptionModal({
ruleExceptionList && (
<>
<ModalBodySection className="builder-section">
{isRuleEQLSequenceStatement && (
<>
<EuiCallOut
data-test-subj="eql-sequence-callout"
title={i18n.ADD_EXCEPTION_SEQUENCE_WARNING}
/>
<EuiSpacer />
</>
)}
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
<EuiSpacer />
<ExceptionBuilderComponent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,11 @@ export const EXCEPTION_BUILDER_INFO = i18n.translate(
defaultMessage: "Alerts are generated when the rule's conditions are met, except when:",
}
);

export const ADD_EXCEPTION_SEQUENCE_WARNING = i18n.translate(
'xpack.securitySolution.exceptions.addException.sequenceWarning',
{
defaultMessage:
"This rule's query contains an EQL sequence statement. The exception created will apply to all events in the sequence.",
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import { useSignalIndex } from '../../../../detections/containers/detection_engi
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
import { EntriesArray } from '../../../../../../lists/common/schemas/types';
import * as builder from '../builder';
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('../../../../common/lib/kibana');
jest.mock('../../../../detections/containers/detection_engine/rules');
Expand All @@ -30,6 +35,7 @@ jest.mock('../../../containers/source');
jest.mock('../use_fetch_or_create_rule_exception_list');
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
jest.mock('../builder');
jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async');

describe('When the edit exception modal is opened', () => {
const ruleName = 'test rule';
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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(
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
<EditExceptionModal
ruleIndices={['filebeat-*']}
ruleId="123"
ruleName={ruleName}
exceptionListType={'detection'}
onCancel={jest.fn()}
onConfirm={jest.fn()}
exceptionItem={getExceptionListItemSchemaMock()}
/>
</ThemeProvider>
);
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(
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<EuiOverlayMask onClick={onCancel}>
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
Expand All @@ -265,6 +273,15 @@ export const EditExceptionModal = memo(function EditExceptionModal({
{!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && (
<>
<ModalBodySection className="builder-section">
{isRuleEQLSequenceStatement && (
<>
<EuiCallOut
data-test-subj="eql-sequence-callout"
title={i18n.EDIT_EXCEPTION_SEQUENCE_WARNING}
/>
<EuiSpacer />
</>
)}
<EuiText>{i18n.EXCEPTION_BUILDER_INFO}</EuiText>
<EuiSpacer />
<ExceptionBuilderComponent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,11 @@ export const VERSION_CONFLICT_ERROR_DESCRIPTION = i18n.translate(
"It appears this exception was updated since you first selected to edit it. Try clicking 'Cancel' and editing the exception again.",
}
);

export const EDIT_EXCEPTION_SEQUENCE_WARNING = i18n.translate(
'xpack.securitySolution.exceptions.editException.sequenceWarning',
{
defaultMessage:
"This rule's query contains an EQL sequence statement. The exception modified will apply to all events in the sequence.",
}
);