Skip to content

Commit

Permalink
[Security Solution] Allow users to save rule query with non critical …
Browse files Browse the repository at this point in the history
…validation errors (elastic#202544)

**Addresses:** elastic#171520

## Summary

This PR adds functionality to allow users save EQL and ES|QL queries in Prebuilt Rule Customization workflow by displaying a confirmation modal with non critical validation errors (a.k.a warnings). It also refactors confirmation modal usage in rule creation/editing forms for better reusability.

## Screenshots

<img width="1909" alt="Screenshot 2024-12-03 at 14 01 36" src="https://github.com/user-attachments/assets/c0054fc1-b52b-400f-80d7-af42391f4e18">

https://github.com/user-attachments/assets/2a20fcfe-ffc0-4547-8621-7ac6873c8dc9

https://github.com/user-attachments/assets/50b5cf5a-ea3f-4c22-a443-b5d4056a92c8

<img width="2541" alt="Screenshot 2024-12-03 at 14 06 29" src="https://github.com/user-attachments/assets/dde3fd60-6c69-4f8e-a65a-837b2319e4ac">

<img width="2552" alt="Screenshot 2024-12-03 at 14 06 51" src="https://github.com/user-attachments/assets/220817a6-991f-4361-88d2-ee3a47a36ad6">

<img width="2555" alt="Screenshot 2024-12-03 at 14 07 52" src="https://github.com/user-attachments/assets/c46fc49c-9ce1-4472-bdea-f9507aa62ece">

<img width="2553" alt="Screenshot 2024-12-03 at 14 08 18" src="https://github.com/user-attachments/assets/92388c56-8644-4c54-8727-b9a73b3497d1">
  • Loading branch information
maximpn authored and CAWilson94 committed Dec 9, 2024
1 parent 9bd7808 commit c8247d1
Show file tree
Hide file tree
Showing 33 changed files with 827 additions and 708 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -2283,6 +2283,7 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/
/x-pack/plugins/security_solution/public/common/components/with_hover_actions @elastic/security-threat-hunting-explore
/x-pack/plugins/security_solution/public/common/containers/matrix_histogram @elastic/security-threat-hunting-explore
/x-pack/plugins/security_solution/public/common/lib/cell_actions @elastic/security-threat-hunting-explore
/x-pack/plugins/security_solution/public/common/hooks/use_form_with_warn @elastic/security-detection-rule-management
/x-pack/plugins/security_solution/public/cases @elastic/security-threat-hunting-explore
/x-pack/plugins/security_solution/public/explore @elastic/security-threat-hunting-explore
/x-pack/plugins/security_solution/public/overview @elastic/security-threat-hunting-explore
Expand Down
17 changes: 12 additions & 5 deletions x-pack/plugins/security_solution/public/common/hooks/eql/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@ interface Params {
signal?: AbortSignal;
}

export interface EqlResponseError {
code: EQL_ERROR_CODES;
messages?: string[];
error?: Error;
}
export type EqlResponseError =
| {
code:
| EQL_ERROR_CODES.INVALID_SYNTAX
| EQL_ERROR_CODES.INVALID_EQL
| EQL_ERROR_CODES.MISSING_DATA_SOURCE;
messages: string[];
}
| {
code: EQL_ERROR_CODES.FAILED_REQUEST;
error: Error;
};

export interface ValidateEqlResponse {
valid: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,44 @@
* 2.0.
*/

import React from 'react';

import React, { memo } from 'react';
import { EuiConfirmModal, EuiSpacer, EuiText } from '@elastic/eui';

import * as i18n from './translations';

interface SaveWithErrorsModalProps {
interface ConfirmValidationErrorsModalProps {
errors: string[];
onCancel: () => void;
onConfirm: () => void;
}

const SaveWithErrorsModalComponent = ({
export const ConfirmValidationErrorsModal = memo(function ConfirmValidationErrorsModal({
errors,
onCancel,
onConfirm,
}: SaveWithErrorsModalProps) => {
}: ConfirmValidationErrorsModalProps): JSX.Element {
return (
<EuiConfirmModal
data-test-subj="save-with-errors-confirmation-modal"
title={i18n.SAVE_WITH_ERRORS_MODAL_TITLE}
onCancel={onCancel}
onConfirm={onConfirm}
cancelButtonText={i18n.SAVE_WITH_ERRORS_CANCEL_BUTTON}
confirmButtonText={i18n.SAVE_WITH_ERRORS_CONFIRM_BUTTON}
cancelButtonText={i18n.CANCEL}
confirmButtonText={i18n.CONFIRM}
defaultFocusedButton="confirm"
>
<>
{i18n.SAVE_WITH_ERRORS_MODAL_MESSAGE(errors.length)}
{i18n.SAVE_WITH_ERRORS_MESSAGE(errors.length)}
<EuiSpacer size="s" />
<ul>
{errors.map((validationError, idx) => {
{errors.map((error) => {
return (
<li key={idx}>
<EuiText>{validationError}</EuiText>
<li key={error}>
<EuiText>{error}</EuiText>
</li>
);
})}
</ul>
</>
</EuiConfirmModal>
);
};

export const SaveWithErrorsModal = React.memo(SaveWithErrorsModalComponent);
SaveWithErrorsModal.displayName = 'SaveWithErrorsModal';
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './use_confirm_validation_errors_modal';
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';

export const SAVE_WITH_ERRORS_MODAL_TITLE = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.saveWithErrorsConfirmationModal.title',
{
defaultMessage: 'There are validation errors',
}
);

export const CANCEL = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.saveWithErrorsConfirmationModal.cancel',
{
defaultMessage: 'Cancel',
}
);

export const CONFIRM = i18n.translate(
'xpack.securitySolution.detectionEngine.rules.upgradeRules.saveWithErrorsConfirmationModal.confirm',
{
defaultMessage: 'Confirm',
}
);

export const SAVE_WITH_ERRORS_MESSAGE = (errorsCount: number) =>
i18n.translate('xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalMessage', {
defaultMessage:
'There {errorsCount, plural, one {is} other {are}} {errorsCount} validation {errorsCount, plural, one {error} other {errors}} which can lead to failed rule executions, save anyway?',
values: { errorsCount },
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { ReactNode } from 'react';
import React, { useCallback, useState, useMemo } from 'react';
import { useBoolean } from '@kbn/react-hooks';
import { useAsyncConfirmation } from '../../../detection_engine/rule_management_ui/components/rules_table/rules_table/use_async_confirmation';
import { ConfirmValidationErrorsModal } from './confirm_validation_errors_modal';

interface UseFieldConfirmValidationErrorsModalResult {
modal: ReactNode;
confirmValidationErrors: (errorMessages: string[]) => Promise<boolean>;
}

export function useConfirmValidationErrorsModal(): UseFieldConfirmValidationErrorsModalResult {
const [visible, { on: showModal, off: hideModal }] = useBoolean(false);
const [initModal, confirm, cancel] = useAsyncConfirmation({
onInit: showModal,
onFinish: hideModal,
});
const [errorsToConfirm, setErrorsToConfirm] = useState<string[]>([]);

const confirmValidationErrors = useCallback(
(errorMessages: string[]) => {
if (errorMessages.length === 0) {
return Promise.resolve(true);
}

setErrorsToConfirm(errorMessages);

return initModal();
},
[initModal, setErrorsToConfirm]
);

const modal = useMemo(
() =>
visible ? (
<ConfirmValidationErrorsModal
errors={errorsToConfirm}
onConfirm={confirm}
onCancel={cancel}
/>
) : null,
[visible, errorsToConfirm, confirm, cancel]
);

return {
modal,
confirmValidationErrors,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { FieldHook, ValidationError } from '../../../shared_imports';
import type { ValidationResults } from './validation_results';

export function extractValidationResults(
formFields: Readonly<FieldHook[]>,
warningValidationCodes: Readonly<string[]>
): ValidationResults {
const warningValidationCodesSet = new Set(warningValidationCodes);
const errors: ValidationError[] = [];
const warnings: ValidationError[] = [];

for (const field of formFields) {
for (const error of field.errors) {
const path = error.path ?? field.path;

if (!error.code || !warningValidationCodesSet.has(error.code)) {
errors.push({ ...error, path });
} else {
warnings.push({ ...error, path });
}
}
}

return {
errors,
warnings,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { FormHook, FormData, ValidationError } from '../../../shared_imports';

export interface FormHookWithWarnings<T extends FormData = FormData, I extends FormData = T>
extends FormHook<T, I> {
getValidationWarnings(): ValidationError[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export type * from './form_hook_with_warnings';
export * from './use_form_with_warnings';
Loading

0 comments on commit c8247d1

Please sign in to comment.