mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Security Solution] [Exceptions] Fix Exception Auto-populate from Alert actions (#159908)
## Summary - Addresses https://github.com/elastic/kibana/issues/159784 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
5c0db11260
commit
fdd709b025
8 changed files with 237 additions and 36 deletions
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { HttpStart } from '@kbn/core/public';
|
||||
|
@ -34,6 +34,7 @@ import {
|
|||
} from '@kbn/securitysolution-list-utils';
|
||||
import { DataViewBase } from '@kbn/es-query';
|
||||
import type { AutocompleteStart } from '@kbn/unified-search-plugin/public';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { AndOrBadge } from '../and_or_badge';
|
||||
|
||||
|
@ -41,7 +42,6 @@ import { BuilderExceptionListItemComponent } from './exception_item_renderer';
|
|||
import { BuilderLogicButtons } from './logic_buttons';
|
||||
import { getTotalErrorExist } from './selectors';
|
||||
import { EntryFieldError, State, exceptionsBuilderReducer } from './reducer';
|
||||
|
||||
const MyInvisibleAndBadge = styled(EuiFlexItem)`
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
@ -131,6 +131,7 @@ export const ExceptionBuilderComponent = ({
|
|||
disableNested: isNestedDisabled,
|
||||
disableOr: isOrDisabled,
|
||||
});
|
||||
const [areAllEntriesDeleted, setAreAllEntriesDeleted] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
addNested,
|
||||
|
@ -252,6 +253,7 @@ export const ExceptionBuilderComponent = ({
|
|||
// just add a default entry to it
|
||||
if (updatedExceptions.length === 0) {
|
||||
setDefaultExceptions(item);
|
||||
setAreAllEntriesDeleted(true);
|
||||
} else if (updatedExceptions.length > 0 && exceptionListItemSchema.is(item)) {
|
||||
setUpdateExceptionsToDelete([...exceptionsToDelete, item]);
|
||||
} else {
|
||||
|
@ -394,12 +396,36 @@ export const ExceptionBuilderComponent = ({
|
|||
}
|
||||
}, [exceptions, handleAddNewExceptionItem]);
|
||||
|
||||
/**
|
||||
* This component relies on the "exceptionListItems" to pre-fill its entries,
|
||||
* but any subsequent updates to the entries are not reflected back to
|
||||
* the "exceptionListItems". To ensure correct behavior, we need to only
|
||||
* fill the entries from the "exceptionListItems" during initialization.
|
||||
*
|
||||
* In the initialization phase, if there are "exceptionListItems" with
|
||||
* pre-filled entries, the exceptions array will be empty. However,
|
||||
* there are cases where the "exceptionListItems" may not be sent
|
||||
* correctly during initialization, leading to the exceptions
|
||||
* array being filled with empty entries. Therefore, we need to
|
||||
* check if the exception is correctly populated with a valid
|
||||
* "field" when the "exceptionListItems" has entries. that's why
|
||||
* "exceptionsEntriesPopulated" is used
|
||||
*
|
||||
* It's important to differentiate this case from when the user
|
||||
* deletes all the entries and the "exceptionListItems" has pre-filled values.
|
||||
* that's why "allEntriesDeleted" is used
|
||||
*
|
||||
* deepEqual(exceptionListItems, exceptions) to handle the exceptionListItems in
|
||||
* the EventFiltersFlyout
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (exceptionListItems.length > 0) {
|
||||
if (!exceptionListItems.length || deepEqual(exceptionListItems, exceptions)) return;
|
||||
const exceptionsEntriesPopulated = exceptions.some((exception) =>
|
||||
exception.entries.some((entry) => entry.field)
|
||||
);
|
||||
if (!exceptionsEntriesPopulated && !areAllEntriesDeleted)
|
||||
setUpdateExceptions(exceptionListItems);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [areAllEntriesDeleted, exceptionListItems, exceptions, setUpdateExceptions]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" direction="column" data-test-subj="exceptionsBuilderWrapper">
|
||||
|
|
|
@ -7,8 +7,10 @@
|
|||
|
||||
import { deleteAlertsAndRules } from '../../../tasks/common';
|
||||
import {
|
||||
expandFirstAlert,
|
||||
goToClosedAlertsOnRuleDetailsPage,
|
||||
goToOpenedAlertsOnRuleDetailsPage,
|
||||
openAddEndpointExceptionFromAlertActionButton,
|
||||
openAddEndpointExceptionFromFirstAlert,
|
||||
} from '../../../tasks/alerts';
|
||||
import { login, visitWithoutDateRange } from '../../../tasks/login';
|
||||
|
@ -26,13 +28,22 @@ import {
|
|||
} from '../../../tasks/es_archiver';
|
||||
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation';
|
||||
import {
|
||||
addExceptionEntryFieldValue,
|
||||
addExceptionEntryFieldValueValue,
|
||||
addExceptionFlyoutItemName,
|
||||
editExceptionFlyoutItemName,
|
||||
selectCloseSingleAlerts,
|
||||
submitNewExceptionItem,
|
||||
validateExceptionConditionField,
|
||||
} from '../../../tasks/exceptions';
|
||||
import { ALERTS_COUNT, EMPTY_ALERT_TABLE } from '../../../screens/alerts';
|
||||
import { NO_EXCEPTIONS_EXIST_PROMPT } from '../../../screens/exceptions';
|
||||
import {
|
||||
ADD_AND_BTN,
|
||||
EXCEPTION_CARD_ITEM_CONDITIONS,
|
||||
EXCEPTION_CARD_ITEM_NAME,
|
||||
EXCEPTION_ITEM_VIEWER_CONTAINER,
|
||||
NO_EXCEPTIONS_EXIST_PROMPT,
|
||||
} from '../../../screens/exceptions';
|
||||
import {
|
||||
removeException,
|
||||
goToAlertsTab,
|
||||
|
@ -41,10 +52,11 @@ import {
|
|||
|
||||
describe('Endpoint Exceptions workflows from Alert', () => {
|
||||
const expectedNumberOfAlerts = 1;
|
||||
before(() => {
|
||||
esArchiverResetKibana();
|
||||
});
|
||||
const ITEM_NAME = 'Sample Exception List Item';
|
||||
const ITEM_NAME_EDIT = 'Sample Exception List Item';
|
||||
const ADDITIONAL_ENTRY = 'host.hostname';
|
||||
beforeEach(() => {
|
||||
esArchiverResetKibana();
|
||||
login();
|
||||
deleteAlertsAndRules();
|
||||
esArchiverLoad('endpoint');
|
||||
|
@ -69,7 +81,7 @@ describe('Endpoint Exceptions workflows from Alert', () => {
|
|||
validateExceptionConditionField('file.Ext.code_signature');
|
||||
|
||||
selectCloseSingleAlerts();
|
||||
addExceptionFlyoutItemName('Sample Exception');
|
||||
addExceptionFlyoutItemName(ITEM_NAME);
|
||||
submitNewExceptionItem();
|
||||
|
||||
// Alerts table should now be empty from having added exception and closed
|
||||
|
@ -100,4 +112,39 @@ describe('Endpoint Exceptions workflows from Alert', () => {
|
|||
|
||||
cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alert`);
|
||||
});
|
||||
|
||||
it('Should be able to create Endpoint exception from Alerts take action button, and change multiple exception items without resetting to initial auto-prefilled entries', () => {
|
||||
// Open first Alert Summary
|
||||
expandFirstAlert();
|
||||
|
||||
// The Endpoint should populated with predefined fields
|
||||
openAddEndpointExceptionFromAlertActionButton();
|
||||
|
||||
// As the endpoint.alerts-* is used to trigger the alert the
|
||||
// file.Ext.code_signature will be auto-populated
|
||||
validateExceptionConditionField('file.Ext.code_signature');
|
||||
addExceptionFlyoutItemName(ITEM_NAME);
|
||||
|
||||
cy.get(ADD_AND_BTN).click();
|
||||
// edit conditions
|
||||
addExceptionEntryFieldValue(ADDITIONAL_ENTRY, 6);
|
||||
addExceptionEntryFieldValueValue('foo', 4);
|
||||
|
||||
// Change the name again
|
||||
editExceptionFlyoutItemName(ITEM_NAME_EDIT);
|
||||
|
||||
// validate the condition is still "agent.name" or got rest after the name is changed
|
||||
validateExceptionConditionField(ADDITIONAL_ENTRY);
|
||||
|
||||
selectCloseSingleAlerts();
|
||||
submitNewExceptionItem();
|
||||
|
||||
// Endpoint Exception will move to Endpoint List under Exception tab of rule
|
||||
goToEndpointExceptionsTab();
|
||||
|
||||
// new exception item displays
|
||||
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1);
|
||||
cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME_EDIT);
|
||||
cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).contains('span', ADDITIONAL_ENTRY);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,8 +12,10 @@ import { createRule } from '../../../tasks/api_calls/rules';
|
|||
import { goToRuleDetails } from '../../../tasks/alerts_detection_rules';
|
||||
import {
|
||||
addExceptionFromFirstAlert,
|
||||
expandFirstAlert,
|
||||
goToClosedAlertsOnRuleDetailsPage,
|
||||
goToOpenedAlertsOnRuleDetailsPage,
|
||||
openAddRuleExceptionFromAlertActionButton,
|
||||
} from '../../../tasks/alerts';
|
||||
import {
|
||||
addExceptionEntryFieldValue,
|
||||
|
@ -26,6 +28,9 @@ import {
|
|||
validateExceptionItemAffectsTheCorrectRulesInRulePage,
|
||||
validateExceptionConditionField,
|
||||
validateExceptionCommentCountAndText,
|
||||
editExceptionFlyoutItemName,
|
||||
validateHighlightedFieldsPopulatedAsExceptionConditions,
|
||||
validateEmptyExceptionConditionField,
|
||||
} from '../../../tasks/exceptions';
|
||||
import {
|
||||
esArchiverLoad,
|
||||
|
@ -42,26 +47,44 @@ import {
|
|||
|
||||
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation';
|
||||
import { postDataView, deleteAlertsAndRules } from '../../../tasks/common';
|
||||
import { NO_EXCEPTIONS_EXIST_PROMPT } from '../../../screens/exceptions';
|
||||
import {
|
||||
ADD_AND_BTN,
|
||||
ENTRY_DELETE_BTN,
|
||||
EXCEPTION_CARD_ITEM_CONDITIONS,
|
||||
EXCEPTION_CARD_ITEM_NAME,
|
||||
EXCEPTION_ITEM_VIEWER_CONTAINER,
|
||||
NO_EXCEPTIONS_EXIST_PROMPT,
|
||||
} from '../../../screens/exceptions';
|
||||
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';
|
||||
|
||||
const loadEndpointRuleAndAlerts = () => {
|
||||
esArchiverLoad('endpoint');
|
||||
login();
|
||||
createRule(getEndpointRule());
|
||||
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
|
||||
goToRuleDetails();
|
||||
waitForAlertsToPopulate();
|
||||
};
|
||||
|
||||
describe('Rule Exceptions workflows from Alert', () => {
|
||||
const EXPECTED_NUMBER_OF_ALERTS = '1 alert';
|
||||
const ITEM_NAME = 'Sample Exception List Item';
|
||||
const ITEM_NAME = 'Sample Exception Item';
|
||||
const ITEM_NAME_EDIT = 'Sample Exception Item Edit';
|
||||
const ADDITIONAL_ENTRY = 'host.hostname';
|
||||
const newRule = getNewRule();
|
||||
|
||||
beforeEach(() => {
|
||||
esArchiverResetKibana();
|
||||
deleteAlertsAndRules();
|
||||
});
|
||||
after(() => {
|
||||
esArchiverUnload('exceptions');
|
||||
deleteAlertsAndRules();
|
||||
});
|
||||
afterEach(() => {
|
||||
esArchiverUnload('exceptions_2');
|
||||
});
|
||||
|
||||
it('Creates an exception item from alert actions overflow menu and close all matching alerts', () => {
|
||||
it('Should create a Rule exception item from alert actions overflow menu and close all matching alerts', () => {
|
||||
esArchiverLoad('exceptions');
|
||||
login();
|
||||
postDataView('exceptions-*');
|
||||
|
@ -119,14 +142,8 @@ describe('Rule Exceptions workflows from Alert', () => {
|
|||
|
||||
cy.get(ALERTS_COUNT).should('have.text', '2 alerts');
|
||||
});
|
||||
|
||||
it('Creates an exception item from alert actions overflow menu and auto populate the conditions using alert Highlighted fields ', () => {
|
||||
esArchiverLoad('endpoint');
|
||||
login();
|
||||
createRule(getEndpointRule());
|
||||
visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL);
|
||||
goToRuleDetails();
|
||||
waitForAlertsToPopulate();
|
||||
it('Should create a Rule exception item from alert actions overflow menu and auto populate the conditions using alert Highlighted fields', () => {
|
||||
loadEndpointRuleAndAlerts();
|
||||
|
||||
cy.get(LOADING_INDICATOR).should('not.exist');
|
||||
addExceptionFromFirstAlert();
|
||||
|
@ -144,9 +161,7 @@ describe('Rule Exceptions workflows from Alert', () => {
|
|||
* fields are based on the alert document that should be generated
|
||||
* when the endpoint rule runs
|
||||
*/
|
||||
highlightedFieldsBasedOnAlertDoc.forEach((field, index) => {
|
||||
validateExceptionConditionField(field);
|
||||
});
|
||||
validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc);
|
||||
|
||||
/**
|
||||
* Validate that the comments are opened by default with one comment added
|
||||
|
@ -154,10 +169,112 @@ describe('Rule Exceptions workflows from Alert', () => {
|
|||
*/
|
||||
validateExceptionCommentCountAndText(
|
||||
1,
|
||||
'Exception conditions are pre-filled with relevant data from'
|
||||
'Exception conditions are pre-filled with relevant data from alert with "id"'
|
||||
);
|
||||
|
||||
addExceptionFlyoutItemName(ITEM_NAME);
|
||||
submitNewExceptionItem();
|
||||
});
|
||||
it('Should create a Rule exception from Alerts take action button and change multiple exception items without resetting to initial auto-prefilled entries', () => {
|
||||
loadEndpointRuleAndAlerts();
|
||||
|
||||
cy.get(LOADING_INDICATOR).should('not.exist');
|
||||
|
||||
// Open first Alert Summary
|
||||
expandFirstAlert();
|
||||
|
||||
// The Rule exception should populated with highlighted fields
|
||||
openAddRuleExceptionFromAlertActionButton();
|
||||
|
||||
const highlightedFieldsBasedOnAlertDoc = [
|
||||
'host.name',
|
||||
'agent.id',
|
||||
'user.name',
|
||||
'process.executable',
|
||||
'file.path',
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate the highlighted fields are auto populated, these
|
||||
* fields are based on the alert document that should be generated
|
||||
* when the endpoint rule runs
|
||||
*/
|
||||
validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc);
|
||||
|
||||
/**
|
||||
* Validate that the comments are opened by default with one comment added
|
||||
* showing a text contains information about the pre-filled conditions
|
||||
*/
|
||||
validateExceptionCommentCountAndText(
|
||||
1,
|
||||
'Exception conditions are pre-filled with relevant data from alert with "id"'
|
||||
);
|
||||
|
||||
addExceptionFlyoutItemName(ITEM_NAME);
|
||||
|
||||
cy.get(ADD_AND_BTN).click();
|
||||
|
||||
// edit conditions
|
||||
addExceptionEntryFieldValue(ADDITIONAL_ENTRY, 5);
|
||||
addExceptionEntryFieldValueValue('foo', 5);
|
||||
|
||||
// Change the name again
|
||||
editExceptionFlyoutItemName(ITEM_NAME_EDIT);
|
||||
|
||||
// validate the condition is still 'host.hostname' or got rest after the name is changed
|
||||
validateExceptionConditionField(ADDITIONAL_ENTRY);
|
||||
|
||||
submitNewExceptionItem();
|
||||
|
||||
goToExceptionsTab();
|
||||
|
||||
// new exception item displays
|
||||
cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1);
|
||||
cy.get(EXCEPTION_CARD_ITEM_NAME).should('have.text', ITEM_NAME_EDIT);
|
||||
cy.get(EXCEPTION_CARD_ITEM_CONDITIONS).contains('span', 'host.hostname');
|
||||
});
|
||||
it('Should delete all prefilled exception entries when creating a Rule exception from Alerts take action button without resetting to initial auto-prefilled entries', () => {
|
||||
loadEndpointRuleAndAlerts();
|
||||
|
||||
cy.get(LOADING_INDICATOR).should('not.exist');
|
||||
|
||||
// Open first Alert Summary
|
||||
expandFirstAlert();
|
||||
|
||||
// The Rule exception should populated with highlighted fields
|
||||
openAddRuleExceptionFromAlertActionButton();
|
||||
|
||||
const highlightedFieldsBasedOnAlertDoc = [
|
||||
'host.name',
|
||||
'agent.id',
|
||||
'user.name',
|
||||
'process.executable',
|
||||
'file.path',
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate the highlighted fields are auto populated, these
|
||||
* fields are based on the alert document that should be generated
|
||||
* when the endpoint rule runs
|
||||
*/
|
||||
validateHighlightedFieldsPopulatedAsExceptionConditions(highlightedFieldsBasedOnAlertDoc);
|
||||
|
||||
/**
|
||||
* Delete all the highlighted fields to see if any condition
|
||||
* will prefuilled again.
|
||||
*/
|
||||
const highlightedFieldsCount = highlightedFieldsBasedOnAlertDoc.length - 1;
|
||||
highlightedFieldsBasedOnAlertDoc.forEach((_, index) =>
|
||||
cy
|
||||
.get(ENTRY_DELETE_BTN)
|
||||
.eq(highlightedFieldsCount - index)
|
||||
.click()
|
||||
);
|
||||
|
||||
/**
|
||||
* Validate that there are no highlighted fields are auto populated
|
||||
* after the deletion
|
||||
*/
|
||||
validateEmptyExceptionConditionField();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -92,16 +92,18 @@ export const openAddEndpointExceptionFromFirstAlert = () => {
|
|||
cy.get(FIELD_INPUT).should('be.visible');
|
||||
};
|
||||
|
||||
export const openAddExceptionFromAlertDetails = () => {
|
||||
cy.get(EXPAND_ALERT_BTN).first().click({ force: true });
|
||||
|
||||
export const openAddRuleExceptionFromAlertActionButton = () => {
|
||||
cy.get(TAKE_ACTION_BTN).click();
|
||||
cy.get(TAKE_ACTION_MENU).should('be.visible');
|
||||
|
||||
cy.get(ADD_EXCEPTION_BTN).click();
|
||||
cy.get(ADD_EXCEPTION_BTN).should('not.be.visible');
|
||||
cy.get(ADD_EXCEPTION_BTN, { timeout: 10000 }).first().click();
|
||||
};
|
||||
|
||||
export const openAddEndpointExceptionFromAlertActionButton = () => {
|
||||
cy.get(TAKE_ACTION_BTN).click();
|
||||
cy.get(TAKE_ACTION_MENU).should('be.visible');
|
||||
cy.get(ADD_ENDPOINT_EXCEPTION_BTN, { timeout: 10000 }).first().click();
|
||||
};
|
||||
export const closeFirstAlert = () => {
|
||||
expandFirstAlertActions();
|
||||
cy.get(CLOSE_ALERT_BTN).click();
|
||||
|
|
|
@ -167,6 +167,9 @@ export const addExceptionConditions = (exception: Exception) => {
|
|||
export const validateExceptionConditionField = (value: string) => {
|
||||
cy.get(EXCEPTION_ITEM_CONTAINER).contains('span', value);
|
||||
};
|
||||
export const validateEmptyExceptionConditionField = () => {
|
||||
cy.get(FIELD_INPUT).should('be.empty');
|
||||
};
|
||||
export const submitNewExceptionItem = () => {
|
||||
cy.get(CONFIRM_BTN).click();
|
||||
cy.get(CONFIRM_BTN).should('not.exist');
|
||||
|
@ -279,3 +282,8 @@ export const deleteFirstExceptionItemInListDetailPage = () => {
|
|||
// Delete exception
|
||||
cy.get(EXCEPTION_ITEM_OVERFLOW_ACTION_DELETE).click();
|
||||
};
|
||||
export const validateHighlightedFieldsPopulatedAsExceptionConditions = (
|
||||
highlightedFields: string[]
|
||||
) => {
|
||||
return highlightedFields.every((field) => validateExceptionConditionField(field));
|
||||
};
|
||||
|
|
|
@ -89,6 +89,7 @@ export const ADD_RULE_EXCEPTION_FROM_ALERT_COMMENT = (alertId: string) =>
|
|||
'xpack.securitySolution.ruleExceptions.addExceptionFlyout.addRuleExceptionFromAlertComment',
|
||||
{
|
||||
values: { alertId },
|
||||
defaultMessage: 'Exception conditions are pre-filled with relevant data from {alertId}.',
|
||||
defaultMessage:
|
||||
'Exception conditions are pre-filled with relevant data from alert with "id" {alertId}.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -96,7 +96,7 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
|
|||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [shouldShowComments, exceptionItemComments]);
|
||||
}, [exceptionItemComments, shouldShowComments]);
|
||||
|
||||
const formattedComments = useMemo((): EuiCommentProps[] => {
|
||||
if (exceptionItemComments && exceptionItemComments.length > 0) {
|
||||
|
@ -105,11 +105,10 @@ export const ExceptionItemComments = memo(function ExceptionItemComments({
|
|||
return [];
|
||||
}
|
||||
}, [exceptionItemComments]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CommentAccordion
|
||||
initialIsOpen={initialIsOpen}
|
||||
initialIsOpen={initialIsOpen && !!newCommentValue}
|
||||
id={'add-exception-comments-accordion'}
|
||||
buttonClassName={COMMENT_ACCORDION_BUTTON_CLASS_NAME}
|
||||
buttonContent={accordionTitle ?? commentsAccordionTitle}
|
||||
|
|
|
@ -419,6 +419,7 @@ export const EventFiltersForm: React.FC<ArtifactFormComponentProps & { allowSele
|
|||
comments: exception?.comments ?? [],
|
||||
os_types: exception?.os_types ?? [OperatingSystem.WINDOWS],
|
||||
tags: exception?.tags ?? [],
|
||||
meta: exception.meta,
|
||||
}
|
||||
: exception;
|
||||
const hasValidConditions =
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue