[Security Solution][Detections] Adds sequence callout in the exceptions modals for eql rule types (#79007)
This commit is contained in:
parent
b9613af6a8
commit
6a173ba19d
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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(
|
||||
<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', () => {
|
||||
|
|
|
@ -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 (
|
||||
<EuiOverlayMask onClick={onCancel}>
|
||||
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
|
||||
|
@ -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
|
||||
|
|
|
@ -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.",
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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');
|
||||
|
@ -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';
|
||||
|
@ -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(
|
||||
<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(
|
||||
|
@ -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', () => {
|
||||
|
|
|
@ -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 (
|
||||
<EuiOverlayMask onClick={onCancel}>
|
||||
<Modal onClose={onCancel} data-test-subj="add-exception-modal">
|
||||
|
@ -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
|
||||
|
|
|
@ -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.",
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue