Skip to content

Commit

Permalink
[Security Solution] Editing rules independently of source data (elast…
Browse files Browse the repository at this point in the history
  • Loading branch information
e40pud committed Aug 27, 2024
1 parent 1e428ad commit 010088d
Show file tree
Hide file tree
Showing 13 changed files with 566 additions and 108 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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 React from 'react';

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

import * as i18n from './translations';

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

const SaveWithErrorsModalComponent = ({
errors,
onCancel,
onConfirm,
}: SaveWithErrorsModalProps) => {
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}
defaultFocusedButton="confirm"
>
<>
{i18n.SAVE_WITH_ERRORS_MODAL_MESSAGE(errors.length)}
<EuiSpacer size="s" />
<ul>
{errors.map((validationError, idx) => {
return (
<li key={idx}>
<EuiText>{validationError}</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,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.createRule.saveWithErrorsModalTitle',
{
defaultMessage: 'This rule has validation errors',
}
);

export const SAVE_WITH_ERRORS_CANCEL_BUTTON = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.saveWithErrorsCancelButton',
{
defaultMessage: 'Cancel',
}
);

export const SAVE_WITH_ERRORS_CONFIRM_BUTTON = i18n.translate(
'xpack.securitySolution.detectionEngine.createRule.saveWithErrorsConfirmButton',
{
defaultMessage: 'Confirm',
}
);

export const SAVE_WITH_ERRORS_MODAL_MESSAGE = (errorsCount: number) =>
i18n.translate('xpack.securitySolution.detectionEngine.createRule.saveWithErrorsModalMessage', {
defaultMessage:
'This rule has {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
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { useState, useMemo, useEffect } from 'react';
import { useState, useMemo, useEffect, useCallback } from 'react';
import type { DataViewBase } from '@kbn/es-query';
import { isThreatMatchRule } from '../../../../common/detection_engine/utils';
import type {
Expand All @@ -16,6 +16,7 @@ import type {
} from '../../../detections/pages/detection_engine/rules/types';
import { DataSourceType } from '../../../detections/pages/detection_engine/rules/types';
import { useKibana } from '../../../common/lib/kibana';
import type { FormHook, ValidationError } from '../../../shared_imports';
import { useForm, useFormData } from '../../../shared_imports';
import { schema as defineRuleSchema } from '../components/step_define_rule/schema';
import type { EqlOptionsSelected } from '../../../../common/search_strategy';
Expand All @@ -26,6 +27,9 @@ import {
import { schema as scheduleRuleSchema } from '../components/step_schedule_rule/schema';
import { getSchema as getActionsRuleSchema } from '../../rule_creation/components/step_rule_actions/get_schema';
import { useFetchIndex } from '../../../common/containers/source';
import { ERROR_CODES as ESQL_ERROR_CODES } from '../../rule_creation/logic/esql_validator';
import { EQL_ERROR_CODES } from '../../../common/hooks/eql/api';
import * as i18n from './translations';

export interface UseRuleFormsProps {
defineStepDefault: DefineStepRule;
Expand Down Expand Up @@ -156,3 +160,84 @@ export const useRuleIndexPattern = ({
}, [dataSourceType, isIndexPatternLoading, data, dataViewId, initIndexPattern]);
return { indexPattern, isIndexPatternLoading, browserFields };
};

export interface UseRuleFormsErrors {
defineStepForm?: FormHook<DefineStepRule, DefineStepRule>;
aboutStepForm?: FormHook<AboutStepRule, AboutStepRule>;
scheduleStepForm?: FormHook<ScheduleStepRule, ScheduleStepRule>;
actionsStepForm?: FormHook<ActionsStepRule, ActionsStepRule>;
}

const getFieldErrorMessages = (fieldError: ValidationError) => {
if (fieldError.message.length > 0) {
return [fieldError.message];
} else if (Array.isArray(fieldError.messages)) {
return fieldError.messages.map((message) => JSON.stringify(message));
}
return [];
};

const NON_BLOCKING_QUERY_BAR_ERROR_CODES = [
ESQL_ERROR_CODES.INVALID_ESQL,
EQL_ERROR_CODES.FAILED_REQUEST,
EQL_ERROR_CODES.INVALID_EQL,
EQL_ERROR_CODES.MISSING_DATA_SOURCE,
];

const isNonBlockingQueryBarErrorCode = (errorCode?: string) => {
return !!NON_BLOCKING_QUERY_BAR_ERROR_CODES.find((code) => code === errorCode);
};

const NON_BLOCKING_ERROR_CODES = [...NON_BLOCKING_QUERY_BAR_ERROR_CODES];

const isNonBlockingErrorCode = (errorCode?: string) => {
return !!NON_BLOCKING_ERROR_CODES.find((code) => code === errorCode);
};

const transformValidationError = ({
errorCode,
errorMessage,
}: {
errorCode?: string;
errorMessage: string;
}) => {
if (isNonBlockingQueryBarErrorCode(errorCode)) {
return i18n.QUERY_BAR_VALIDATION_ERROR(errorMessage);
}
return errorMessage;
};

export const useRuleFormsErrors = () => {
const getRuleFormsErrors = useCallback(
({ defineStepForm, aboutStepForm, scheduleStepForm, actionsStepForm }: UseRuleFormsErrors) => {
const blockingErrors: string[] = [];
const nonBlockingErrors: string[] = [];

for (const [_, fieldHook] of Object.entries(defineStepForm?.getFields() ?? {})) {
fieldHook.errors.forEach((fieldError) => {
const messages = getFieldErrorMessages(fieldError);
if (isNonBlockingErrorCode(fieldError.code)) {
nonBlockingErrors.push(
...messages.map((message) =>
transformValidationError({ errorCode: fieldError.code, errorMessage: message })
)
);
} else {
blockingErrors.push(...messages);
}
});
}

const blockingForms = [aboutStepForm, scheduleStepForm, actionsStepForm];
blockingForms.forEach((form) => {
for (const [_, fieldHook] of Object.entries(form?.getFields() ?? {})) {
blockingErrors.push(...fieldHook.errors.map((fieldError) => fieldError.message));
}
});
return { blockingErrors, nonBlockingErrors };
},
[]
);

return { getRuleFormsErrors };
};
Loading

0 comments on commit 010088d

Please sign in to comment.