Skip to content

Commit ce76ca3

Browse files
authored
[7.9] [Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#76012) (#76052)
* [Security Solution][Exceptions] - Improve UX for missing exception list associated with rule (#76012) ## 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 051252f commit ce76ca3

File tree

12 files changed

+701
-52
lines changed

12 files changed

+701
-52
lines changed

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

Lines changed: 64 additions & 25 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 &&
@@ -375,19 +412,21 @@ export const AddExceptionModal = memo(function AddExceptionModal({
375412
</ModalBodySection>
376413
</>
377414
)}
415+
{fetchOrCreateListError == null && (
416+
<EuiModalFooter>
417+
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
378418

379-
<EuiModalFooter>
380-
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
381-
382-
<EuiButton
383-
onClick={onAddExceptionConfirm}
384-
isLoading={addExceptionIsLoading}
385-
isDisabled={isSubmitButtonDisabled}
386-
fill
387-
>
388-
{i18n.ADD_EXCEPTION}
389-
</EuiButton>
390-
</EuiModalFooter>
419+
<EuiButton
420+
data-test-subj="add-exception-confirm-button"
421+
onClick={onAddExceptionConfirm}
422+
isLoading={addExceptionIsLoading}
423+
isDisabled={isSubmitButtonDisabled}
424+
fill
425+
>
426+
{i18n.ADD_EXCEPTION}
427+
</EuiButton>
428+
</EuiModalFooter>
429+
)}
391430
</Modal>
392431
</EuiOverlayMask>
393432
);

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

Lines changed: 68 additions & 22 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">
@@ -279,27 +312,40 @@ export const EditExceptionModal = memo(function EditExceptionModal({
279312
</ModalBodySection>
280313
</>
281314
)}
282-
315+
{updateError != null && (
316+
<ModalBodySection>
317+
<ErrorCallout
318+
http={http}
319+
errorInfo={updateError}
320+
rule={maybeRule}
321+
onCancel={onCancel}
322+
onSuccess={handleDissasociationSuccess}
323+
onError={handleDissasociationError}
324+
/>
325+
</ModalBodySection>
326+
)}
283327
{hasVersionConflict && (
284328
<ModalBodySection>
285329
<EuiCallOut title={i18n.VERSION_CONFLICT_ERROR_TITLE} color="danger" iconType="alert">
286330
<p>{i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}</p>
287331
</EuiCallOut>
288332
</ModalBodySection>
289333
)}
334+
{updateError == null && (
335+
<EuiModalFooter>
336+
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
290337

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

0 commit comments

Comments
 (0)