Skip to content

Commit b9c8201

Browse files
authored
[Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#75898)
## Summary **Current behavior:** - **Scenario 1:** User is in the exceptions viewer flow, they select to edit an exception item, but the list the item is associated with has since been deleted (let's say by another user) - a user is able to open modal to edit exception item and on save, an error toaster shows but no information is given to the user to indicate the issue. - **Scenario 2:** User exports rules from space 'X' and imports into space 'Y'. The exception lists associated with their newly imported rules do not exist in space 'Y' - a user goes to add an exception item and gets a modal with an error, unable to add any exceptions. - **Workaround:** current workaround exists only via API - user would need to remove the exception list from their rule via API **New behavior:** - **Scenario 1:** User is still able to oped edit modal, but on save they see an error explaining that the associated exception list does not exist and prompts them to remove the exception list --> now they're able to add exceptions to their rule - **Scenario 2:** User navigates to exceptions after importing their rule, tries to add exception, modal pops up with error informing them that they need to remove association to missing exception list, button prompts them to do so --> now can continue adding exceptions to rule
1 parent 4e1b1b5 commit b9c8201

File tree

13 files changed

+706
-54
lines changed

13 files changed

+706
-54
lines changed

x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
EuiCheckbox,
1919
EuiSpacer,
2020
EuiFormRow,
21-
EuiCallOut,
2221
EuiText,
2322
} from '@elastic/eui';
2423
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
@@ -28,13 +27,15 @@ import {
2827
ExceptionListType,
2928
} from '../../../../../public/lists_plugin_deps';
3029
import * as i18n from './translations';
30+
import * as sharedI18n from '../translations';
3131
import { TimelineNonEcsData, Ecs } from '../../../../graphql/types';
3232
import { useAppToasts } from '../../../hooks/use_app_toasts';
3333
import { useKibana } from '../../../lib/kibana';
3434
import { ExceptionBuilderComponent } from '../builder';
3535
import { Loader } from '../../loader';
3636
import { useAddOrUpdateException } from '../use_add_exception';
3737
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
38+
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
3839
import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list';
3940
import { AddExceptionComments } from '../add_exception_comments';
4041
import {
@@ -46,6 +47,7 @@ import {
4647
entryHasNonEcsType,
4748
getMappedNonEcsValue,
4849
} from '../helpers';
50+
import { ErrorInfo, ErrorCallout } from '../error_callout';
4951
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
5052

5153
export interface AddExceptionModalBaseProps {
@@ -107,13 +109,14 @@ export const AddExceptionModal = memo(function AddExceptionModal({
107109
}: AddExceptionModalProps) {
108110
const { http } = useKibana().services;
109111
const [comment, setComment] = useState('');
112+
const { rule: maybeRule } = useRuleAsync(ruleId);
110113
const [shouldCloseAlert, setShouldCloseAlert] = useState(false);
111114
const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false);
112115
const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false);
113116
const [exceptionItemsToAdd, setExceptionItemsToAdd] = useState<
114117
Array<ExceptionListItemSchema | CreateExceptionListItemSchema>
115118
>([]);
116-
const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false);
119+
const [fetchOrCreateListError, setFetchOrCreateListError] = useState<ErrorInfo | null>(null);
117120
const { addError, addSuccess } = useAppToasts();
118121
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
119122
const [
@@ -164,17 +167,41 @@ export const AddExceptionModal = memo(function AddExceptionModal({
164167
},
165168
[onRuleChange]
166169
);
167-
const onFetchOrCreateExceptionListError = useCallback(
168-
(error: Error) => {
169-
setFetchOrCreateListError(true);
170+
171+
const handleDissasociationSuccess = useCallback(
172+
(id: string): void => {
173+
handleRuleChange(true);
174+
addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id));
175+
onCancel();
176+
},
177+
[handleRuleChange, addSuccess, onCancel]
178+
);
179+
180+
const handleDissasociationError = useCallback(
181+
(error: Error): void => {
182+
addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR });
183+
onCancel();
184+
},
185+
[addError, onCancel]
186+
);
187+
188+
const handleFetchOrCreateExceptionListError = useCallback(
189+
(error: Error, statusCode: number | null, message: string | null) => {
190+
setFetchOrCreateListError({
191+
reason: error.message,
192+
code: statusCode,
193+
details: message,
194+
listListId: null,
195+
});
170196
},
171197
[setFetchOrCreateListError]
172198
);
199+
173200
const [isLoadingExceptionList, ruleExceptionList] = useFetchOrCreateRuleExceptionList({
174201
http,
175202
ruleId,
176203
exceptionListType,
177-
onError: onFetchOrCreateExceptionListError,
204+
onError: handleFetchOrCreateExceptionListError,
178205
onSuccess: handleRuleChange,
179206
});
180207

@@ -279,7 +306,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({
279306
]);
280307

281308
const isSubmitButtonDisabled = useMemo(
282-
() => fetchOrCreateListError || exceptionItemsToAdd.every((item) => item.entries.length === 0),
309+
() =>
310+
fetchOrCreateListError != null ||
311+
exceptionItemsToAdd.every((item) => item.entries.length === 0),
283312
[fetchOrCreateListError, exceptionItemsToAdd]
284313
);
285314

@@ -295,19 +324,27 @@ export const AddExceptionModal = memo(function AddExceptionModal({
295324
</ModalHeaderSubtitle>
296325
</ModalHeader>
297326

298-
{fetchOrCreateListError === true && (
299-
<EuiCallOut title={i18n.ADD_EXCEPTION_FETCH_ERROR_TITLE} color="danger" iconType="alert">
300-
<p>{i18n.ADD_EXCEPTION_FETCH_ERROR}</p>
301-
</EuiCallOut>
327+
{fetchOrCreateListError != null && (
328+
<EuiModalFooter>
329+
<ErrorCallout
330+
http={http}
331+
errorInfo={fetchOrCreateListError}
332+
rule={maybeRule}
333+
onCancel={onCancel}
334+
onSuccess={handleDissasociationSuccess}
335+
onError={handleDissasociationError}
336+
data-test-subj="addExceptionModalErrorCallout"
337+
/>
338+
</EuiModalFooter>
302339
)}
303-
{fetchOrCreateListError === false &&
340+
{fetchOrCreateListError == null &&
304341
(isLoadingExceptionList ||
305342
isIndexPatternLoading ||
306343
isSignalIndexLoading ||
307344
isSignalIndexPatternLoading) && (
308345
<Loader data-test-subj="loadingAddExceptionModal" size="xl" />
309346
)}
310-
{fetchOrCreateListError === false &&
347+
{fetchOrCreateListError == null &&
311348
!isSignalIndexLoading &&
312349
!isSignalIndexPatternLoading &&
313350
!isLoadingExceptionList &&
@@ -377,20 +414,21 @@ export const AddExceptionModal = memo(function AddExceptionModal({
377414
</ModalBodySection>
378415
</>
379416
)}
417+
{fetchOrCreateListError == null && (
418+
<EuiModalFooter>
419+
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
380420

381-
<EuiModalFooter>
382-
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
383-
384-
<EuiButton
385-
data-test-subj="add-exception-confirm-button"
386-
onClick={onAddExceptionConfirm}
387-
isLoading={addExceptionIsLoading}
388-
isDisabled={isSubmitButtonDisabled}
389-
fill
390-
>
391-
{i18n.ADD_EXCEPTION}
392-
</EuiButton>
393-
</EuiModalFooter>
421+
<EuiButton
422+
data-test-subj="add-exception-confirm-button"
423+
onClick={onAddExceptionConfirm}
424+
isLoading={addExceptionIsLoading}
425+
isDisabled={isSubmitButtonDisabled}
426+
fill
427+
>
428+
{i18n.ADD_EXCEPTION}
429+
</EuiButton>
430+
</EuiModalFooter>
431+
)}
394432
</Modal>
395433
</EuiOverlayMask>
396434
);

x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ describe('When the edit exception modal is opened', () => {
7777
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
7878
<EditExceptionModal
7979
ruleIndices={[]}
80+
ruleId="123"
8081
ruleName={ruleName}
8182
exceptionListType={'endpoint'}
8283
onCancel={jest.fn()}
@@ -105,6 +106,7 @@ describe('When the edit exception modal is opened', () => {
105106
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
106107
<EditExceptionModal
107108
ruleIndices={['filebeat-*']}
109+
ruleId="123"
108110
ruleName={ruleName}
109111
exceptionListType={'endpoint'}
110112
onCancel={jest.fn()}
@@ -147,6 +149,7 @@ describe('When the edit exception modal is opened', () => {
147149
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
148150
<EditExceptionModal
149151
ruleIndices={['filebeat-*']}
152+
ruleId="123"
150153
ruleName={ruleName}
151154
exceptionListType={'endpoint'}
152155
onCancel={jest.fn()}
@@ -190,6 +193,7 @@ describe('When the edit exception modal is opened', () => {
190193
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
191194
<EditExceptionModal
192195
ruleIndices={['filebeat-*']}
196+
ruleId="123"
193197
ruleName={ruleName}
194198
exceptionListType={'detection'}
195199
onCancel={jest.fn()}
@@ -229,6 +233,7 @@ describe('When the edit exception modal is opened', () => {
229233
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
230234
<EditExceptionModal
231235
ruleIndices={['filebeat-*']}
236+
ruleId="123"
232237
ruleName={ruleName}
233238
exceptionListType={'detection'}
234239
onCancel={jest.fn()}

x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ import {
2323
} from '@elastic/eui';
2424
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
2525
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
26+
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
2627
import {
2728
ExceptionListItemSchema,
2829
CreateExceptionListItemSchema,
2930
ExceptionListType,
3031
} from '../../../../../public/lists_plugin_deps';
3132
import * as i18n from './translations';
33+
import * as sharedI18n from '../translations';
3234
import { useKibana } from '../../../lib/kibana';
3335
import { useAppToasts } from '../../../hooks/use_app_toasts';
3436
import { ExceptionBuilderComponent } from '../builder';
@@ -43,14 +45,17 @@ import {
4345
lowercaseHashValues,
4446
} from '../helpers';
4547
import { Loader } from '../../loader';
48+
import { ErrorInfo, ErrorCallout } from '../error_callout';
4649

4750
interface EditExceptionModalProps {
4851
ruleName: string;
52+
ruleId: string;
4953
ruleIndices: string[];
5054
exceptionItem: ExceptionListItemSchema;
5155
exceptionListType: ExceptionListType;
5256
onCancel: () => void;
5357
onConfirm: () => void;
58+
onRuleChange?: () => void;
5459
}
5560

5661
const Modal = styled(EuiModal)`
@@ -83,14 +88,18 @@ const ModalBodySection = styled.section`
8388

8489
export const EditExceptionModal = memo(function EditExceptionModal({
8590
ruleName,
91+
ruleId,
8692
ruleIndices,
8793
exceptionItem,
8894
exceptionListType,
8995
onCancel,
9096
onConfirm,
97+
onRuleChange,
9198
}: EditExceptionModalProps) {
9299
const { http } = useKibana().services;
93100
const [comment, setComment] = useState('');
101+
const { rule: maybeRule } = useRuleAsync(ruleId);
102+
const [updateError, setUpdateError] = useState<ErrorInfo | null>(null);
94103
const [hasVersionConflict, setHasVersionConflict] = useState(false);
95104
const [shouldBulkCloseAlert, setShouldBulkCloseAlert] = useState(false);
96105
const [shouldDisableBulkClose, setShouldDisableBulkClose] = useState(false);
@@ -108,27 +117,53 @@ export const EditExceptionModal = memo(function EditExceptionModal({
108117
'rules'
109118
);
110119

111-
const onError = useCallback(
112-
(error) => {
120+
const handleExceptionUpdateError = useCallback(
121+
(error: Error, statusCode: number | null, message: string | null) => {
113122
if (error.message.includes('Conflict')) {
114123
setHasVersionConflict(true);
115124
} else {
116-
addError(error, { title: i18n.EDIT_EXCEPTION_ERROR });
117-
onCancel();
125+
setUpdateError({
126+
reason: error.message,
127+
code: statusCode,
128+
details: message,
129+
listListId: exceptionItem.list_id,
130+
});
118131
}
119132
},
133+
[setUpdateError, setHasVersionConflict, exceptionItem.list_id]
134+
);
135+
136+
const handleDissasociationSuccess = useCallback(
137+
(id: string): void => {
138+
addSuccess(sharedI18n.DISSASOCIATE_LIST_SUCCESS(id));
139+
140+
if (onRuleChange) {
141+
onRuleChange();
142+
}
143+
144+
onCancel();
145+
},
146+
[addSuccess, onCancel, onRuleChange]
147+
);
148+
149+
const handleDissasociationError = useCallback(
150+
(error: Error): void => {
151+
addError(error, { title: sharedI18n.DISSASOCIATE_EXCEPTION_LIST_ERROR });
152+
onCancel();
153+
},
120154
[addError, onCancel]
121155
);
122-
const onSuccess = useCallback(() => {
156+
157+
const handleExceptionUpdateSuccess = useCallback((): void => {
123158
addSuccess(i18n.EDIT_EXCEPTION_SUCCESS);
124159
onConfirm();
125160
}, [addSuccess, onConfirm]);
126161

127162
const [{ isLoading: addExceptionIsLoading }, addOrUpdateExceptionItems] = useAddOrUpdateException(
128163
{
129164
http,
130-
onSuccess,
131-
onError,
165+
onSuccess: handleExceptionUpdateSuccess,
166+
onError: handleExceptionUpdateError,
132167
}
133168
);
134169

@@ -222,11 +257,9 @@ export const EditExceptionModal = memo(function EditExceptionModal({
222257
{ruleName}
223258
</ModalHeaderSubtitle>
224259
</ModalHeader>
225-
226260
{(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && (
227261
<Loader data-test-subj="loadingEditExceptionModal" size="xl" />
228262
)}
229-
230263
{!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && (
231264
<>
232265
<ModalBodySection className="builder-section">
@@ -280,28 +313,40 @@ export const EditExceptionModal = memo(function EditExceptionModal({
280313
</ModalBodySection>
281314
</>
282315
)}
283-
316+
{updateError != null && (
317+
<ModalBodySection>
318+
<ErrorCallout
319+
http={http}
320+
errorInfo={updateError}
321+
rule={maybeRule}
322+
onCancel={onCancel}
323+
onSuccess={handleDissasociationSuccess}
324+
onError={handleDissasociationError}
325+
/>
326+
</ModalBodySection>
327+
)}
284328
{hasVersionConflict && (
285329
<ModalBodySection>
286330
<EuiCallOut title={i18n.VERSION_CONFLICT_ERROR_TITLE} color="danger" iconType="alert">
287331
<p>{i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}</p>
288332
</EuiCallOut>
289333
</ModalBodySection>
290334
)}
335+
{updateError == null && (
336+
<EuiModalFooter>
337+
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
291338

292-
<EuiModalFooter>
293-
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
294-
295-
<EuiButton
296-
data-test-subj="edit-exception-confirm-button"
297-
onClick={onEditExceptionConfirm}
298-
isLoading={addExceptionIsLoading}
299-
isDisabled={isSubmitButtonDisabled}
300-
fill
301-
>
302-
{i18n.EDIT_EXCEPTION_SAVE_BUTTON}
303-
</EuiButton>
304-
</EuiModalFooter>
339+
<EuiButton
340+
data-test-subj="edit-exception-confirm-button"
341+
onClick={onEditExceptionConfirm}
342+
isLoading={addExceptionIsLoading}
343+
isDisabled={isSubmitButtonDisabled}
344+
fill
345+
>
346+
{i18n.EDIT_EXCEPTION_SAVE_BUTTON}
347+
</EuiButton>
348+
</EuiModalFooter>
349+
)}
305350
</Modal>
306351
</EuiOverlayMask>
307352
);

0 commit comments

Comments
 (0)