From 6da7c8298c0ae499750a4c50f27f532a2726d282 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Mon, 9 Jan 2023 05:30:18 +0100 Subject: [PATCH] [FEATURE] More validations on YAML rule editor (#279) * yaml editor as custom form component Signed-off-by: Aleksandar Djindjic * refactor file structure for rule editor Signed-off-by: Aleksandar Djindjic * rename RuleEditorFormState to RuleEditorFormModel Signed-off-by: Aleksandar Djindjic * refactor yaml rule editor custom form component Signed-off-by: Aleksandar Djindjic * pr comments fixes and snapshot tests Signed-off-by: Aleksandar Djindjic * more snapshot tests Signed-off-by: Aleksandar Djindjic * align fields labels styling with create detector Signed-off-by: Aleksandar Djindjic * aligne formik version with alerting plugin Signed-off-by: Aleksandar Djindjic * addresing validation UX issues Signed-off-by: Aleksandar Djindjic * validate tag format Signed-off-by: Aleksandar Djindjic Signed-off-by: Aleksandar Djindjic --- cypress/integration/2_rules.spec.js | 6 +- package.json | 2 +- .../components/UpdateRules/UpdateRules.tsx | 1 - .../components/RuleEditor/FieldTextArray.tsx | 6 +- .../FormSubmitionErrorToastNotification.tsx | 2 +- .../components/RuleEditor/RuleEditor.tsx | 140 ------ .../RuleEditor/RuleEditorContainer.tsx | 92 ++++ .../components/RuleEditor/RuleEditorForm.tsx | 423 ++++++++++++++++++ ...torFormState.ts => RuleEditorFormModel.ts} | 4 +- .../RuleEditor/VisualRuleEditor.tsx | 385 ---------------- .../RuleEditor/YamlRuleEditor.test.tsx | 54 --- .../components/RuleEditor/YamlRuleEditor.tsx | 175 -------- .../YamlRuleEditor.test.tsx.snap | 210 --------- .../RuleTagsComboBox.tsx | 67 +++ .../YamlRuleEditorComponent.test.tsx | 101 +++++ .../YamlRuleEditorComponent.tsx | 119 +++++ .../YamlRuleEditorComponent.test.tsx.snap | 36 ++ .../Rules/components/RuleEditor/mappers.ts | 6 +- .../containers/CreateRule/CreateRule.tsx | 4 +- .../DuplicateRule/DuplicateRule.tsx | 9 +- .../Rules/containers/EditRule/EditRule.tsx | 4 +- .../containers/ImportRule/ImportRule.tsx | 11 +- public/pages/Rules/utils/mappers.ts | 14 +- yarn.lock | 2 +- 24 files changed, 878 insertions(+), 995 deletions(-) delete mode 100644 public/pages/Rules/components/RuleEditor/RuleEditor.tsx create mode 100644 public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx create mode 100644 public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx rename public/pages/Rules/components/RuleEditor/{RuleEditorFormState.ts => RuleEditorFormModel.ts} (86%) delete mode 100644 public/pages/Rules/components/RuleEditor/VisualRuleEditor.tsx delete mode 100644 public/pages/Rules/components/RuleEditor/YamlRuleEditor.test.tsx delete mode 100644 public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx delete mode 100644 public/pages/Rules/components/RuleEditor/__snapshots__/YamlRuleEditor.test.tsx.snap create mode 100644 public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/RuleTagsComboBox.tsx create mode 100644 public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/YamlRuleEditorComponent.test.tsx create mode 100644 public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/YamlRuleEditorComponent.tsx create mode 100644 public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/__snapshots__/YamlRuleEditorComponent.test.tsx.snap diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js index 921534e57..eff0cff07 100644 --- a/cypress/integration/2_rules.spec.js +++ b/cypress/integration/2_rules.spec.js @@ -84,12 +84,10 @@ describe('Rules', () => { ); // Enter the reference - cy.get('[data-test-subj="rule_references_-_optional_field_0"]').type(SAMPLE_RULE.references); + cy.get('[data-test-subj="rule_references_field_0"]').type(SAMPLE_RULE.references); // Enter the false positive cases - cy.get('[data-test-subj="rule_false_positive_cases_-_optional_field_0"]').type( - SAMPLE_RULE.falsePositive - ); + cy.get('[data-test-subj="rule_false_positives_field_0"]').type(SAMPLE_RULE.falsePositive); // Enter the author cy.get('[data-test-subj="rule_author_field"]').type(SAMPLE_RULE.author); diff --git a/package.json b/package.json index ae065b02b..585b85ce0 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,6 @@ "yarn": "^1.21.1" }, "dependencies": { - "formik": "^2.2.9" + "formik": "^2.2.6" } } diff --git a/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx index bd1f2ecf3..63b7fbb4e 100644 --- a/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx +++ b/public/pages/Detectors/components/UpdateRules/UpdateRules.tsx @@ -217,7 +217,6 @@ export const UpdateDetectorRules: React.FC = (props) = const ruleItems = prePackagedRuleItems.concat(customRuleItems); const onRuleDetails = (ruleItem: RuleItem) => { - console.log('onRuleDetails', ruleItem); setFlyoutData(() => ({ title: ruleItem.name, level: ruleItem.severity, diff --git a/public/pages/Rules/components/RuleEditor/FieldTextArray.tsx b/public/pages/Rules/components/RuleEditor/FieldTextArray.tsx index 1c8499622..65d3aeb48 100644 --- a/public/pages/Rules/components/RuleEditor/FieldTextArray.tsx +++ b/public/pages/Rules/components/RuleEditor/FieldTextArray.tsx @@ -14,7 +14,8 @@ import { import React, { ChangeEvent } from 'react'; export interface FieldTextArrayProps { - label: string; + label: string | React.ReactNode; + name: string; fields: string[]; addButtonName: string; onFieldEdit: (value: string, fieldIndex: number) => void; @@ -25,6 +26,7 @@ export interface FieldTextArrayProps { export const FieldTextArray: React.FC = ({ addButtonName, label, + name, fields, onFieldEdit, onFieldRemove, @@ -43,7 +45,7 @@ export const FieldTextArray: React.FC = ({ onChange={(e: ChangeEvent) => { onFieldEdit(e.target.value, index); }} - data-test-subj={`rule_${label + data-test-subj={`rule_${name .toLowerCase() .replaceAll(' ', '_')}_field_${index}`} /> diff --git a/public/pages/Rules/components/RuleEditor/FormSubmitionErrorToastNotification.tsx b/public/pages/Rules/components/RuleEditor/FormSubmitionErrorToastNotification.tsx index f664629e0..5ed467456 100644 --- a/public/pages/Rules/components/RuleEditor/FormSubmitionErrorToastNotification.tsx +++ b/public/pages/Rules/components/RuleEditor/FormSubmitionErrorToastNotification.tsx @@ -7,7 +7,7 @@ import { useFormikContext } from 'formik'; import { NotificationsStart } from 'opensearch-dashboards/public'; import { errorNotificationToast } from '../../../../utils/helpers'; -export const FormSubmitionErrorToastNotification = ({ +export const FormSubmissionErrorToastNotification = ({ notifications, }: { notifications?: NotificationsStart; diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx deleted file mode 100644 index 7fbdb55aa..000000000 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState, useCallback } from 'react'; -import { RouteComponentProps } from 'react-router-dom'; -import { NotificationsStart } from 'opensearch-dashboards/public'; -import { RuleService } from '../../../../services'; -import { ROUTES } from '../../../../utils/constants'; -import { ContentPanel } from '../../../../components/ContentPanel'; -import { EuiSpacer, EuiButtonGroup } from '@elastic/eui'; -import { Rule } from '../../../../../models/interfaces'; -import { RuleEditorFormState, ruleEditorStateDefaultValue } from './RuleEditorFormState'; -import { mapFormToRule, mapRuleToForm } from './mappers'; -import { VisualRuleEditor } from './VisualRuleEditor'; -import { YamlRuleEditor } from './YamlRuleEditor'; -import { validateRule } from '../../utils/helpers'; -import { errorNotificationToast } from '../../../../utils/helpers'; - -export interface RuleEditorProps { - title: string; - rule?: Rule; - history: RouteComponentProps['history']; - notifications?: NotificationsStart; - ruleService: RuleService; - mode: 'create' | 'edit'; -} - -export interface VisualEditorFormErrorsState { - nameError: string | null; - descriptionError: string | null; - authorError: string | null; -} - -const editorTypes = [ - { - id: 'visual', - label: 'Visual Editor', - }, - { - id: 'yaml', - label: 'YAML Editor', - }, -]; - -export const RuleEditor: React.FC = ({ - history, - notifications, - title, - rule, - ruleService, - mode, -}) => { - const [ruleEditorFormState, setRuleEditorFormState] = useState( - rule - ? { ...mapRuleToForm(rule), id: ruleEditorStateDefaultValue.id } - : ruleEditorStateDefaultValue - ); - - const [selectedEditorType, setSelectedEditorType] = useState('visual'); - - const onEditorTypeChange = (optionId: string) => { - setSelectedEditorType(optionId); - }; - - const onYamlRuleEditorChange = (value: Rule) => { - const formState = mapRuleToForm(value); - setRuleEditorFormState(formState); - }; - - const onSubmit = async () => { - const submitingRule = mapFormToRule(ruleEditorFormState); - if (!validateRule(submitingRule, notifications!, 'create')) { - return; - } - - let result; - if (mode === 'edit') { - if (!rule) { - console.error('No rule id found'); - return; - } - result = await ruleService.updateRule(rule?.id, submitingRule.category, submitingRule); - } else { - result = await ruleService.createRule(submitingRule); - } - - if (!result.ok) { - errorNotificationToast( - notifications!, - mode === 'create' ? 'create' : 'save', - 'rule', - result.error - ); - } else { - history.replace(ROUTES.RULES); - } - }; - - const goToRulesList = useCallback(() => { - history.replace(ROUTES.RULES); - }, [history]); - - return ( - <> - - onEditorTypeChange(id)} - /> - - {selectedEditorType === 'visual' && ( - - )} - {selectedEditorType === 'yaml' && ( - - )} - - - - - ); -}; diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx new file mode 100644 index 000000000..55b6f639b --- /dev/null +++ b/public/pages/Rules/components/RuleEditor/RuleEditorContainer.tsx @@ -0,0 +1,92 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { RuleService } from '../../../../services'; +import { ROUTES } from '../../../../utils/constants'; +import { EuiSpacer } from '@elastic/eui'; +import { Rule } from '../../../../../models/interfaces'; +import { RuleEditorFormModel, ruleEditorStateDefaultValue } from './RuleEditorFormModel'; +import { mapFormToRule, mapRuleToForm } from './mappers'; +import { RuleEditorForm } from './RuleEditorForm'; +import { validateRule } from '../../utils/helpers'; +import { errorNotificationToast } from '../../../../utils/helpers'; + +export interface RuleEditorProps { + title: string; + rule?: Rule; + history: RouteComponentProps['history']; + notifications?: NotificationsStart; + ruleService: RuleService; + mode: 'create' | 'edit'; +} + +export interface VisualEditorFormErrorsState { + nameError: string | null; + descriptionError: string | null; + authorError: string | null; +} + +export const RuleEditorContainer: React.FC = ({ + history, + notifications, + title, + rule, + ruleService, + mode, +}) => { + const initialRuleValue = rule + ? { ...mapRuleToForm(rule), id: ruleEditorStateDefaultValue.id } + : ruleEditorStateDefaultValue; + + const onSubmit = async (values: RuleEditorFormModel) => { + const submitingRule = mapFormToRule(values); + if (!validateRule(submitingRule, notifications!, 'create')) { + return; + } + + let result; + if (mode === 'edit') { + if (!rule) { + console.error('No rule id found'); + return; + } + result = await ruleService.updateRule(rule?.id, submitingRule.category, submitingRule); + } else { + result = await ruleService.createRule(submitingRule); + } + + if (!result.ok) { + errorNotificationToast( + notifications!, + mode === 'create' ? 'create' : 'save', + 'rule', + result.error + ); + } else { + history.replace(ROUTES.RULES); + } + }; + + const goToRulesList = useCallback(() => { + history.replace(ROUTES.RULES); + }, [history]); + + return ( + <> + + + + ); +}; diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx new file mode 100644 index 000000000..6a04dbd20 --- /dev/null +++ b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx @@ -0,0 +1,423 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { Formik, Form, FormikErrors } from 'formik'; +import { NotificationsStart } from 'opensearch-dashboards/public'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiButton, + EuiSpacer, + EuiTextArea, + EuiComboBox, + EuiCodeEditor, + EuiButtonGroup, + EuiText, +} from '@elastic/eui'; +import { ContentPanel } from '../../../../components/ContentPanel'; +import { FieldTextArray } from './FieldTextArray'; +import { ruleStatus, ruleTypes } from '../../utils/constants'; +import { AUTHOR_REGEX, validateDescription, validateName } from '../../../../utils/validation'; +import { RuleEditorFormModel } from './RuleEditorFormModel'; +import { FormSubmissionErrorToastNotification } from './FormSubmitionErrorToastNotification'; +import { YamlRuleEditorComponent } from './components/YamlRuleEditorComponent/YamlRuleEditorComponent'; +import { mapFormToRule, mapRuleToForm } from './mappers'; +import { RuleTagsComboBox } from './components/YamlRuleEditorComponent/RuleTagsComboBox'; + +export interface VisualRuleEditorProps { + initialValue: RuleEditorFormModel; + notifications?: NotificationsStart; + submit: (values: RuleEditorFormModel) => void; + cancel: () => void; + mode: 'create' | 'edit'; + title: string; +} + +const editorTypes = [ + { + id: 'visual', + label: 'Visual Editor', + }, + { + id: 'yaml', + label: 'YAML Editor', + }, +]; + +export const RuleEditorForm: React.FC = ({ + initialValue, + notifications, + submit, + cancel, + mode, + title, +}) => { + const [selectedEditorType, setSelectedEditorType] = useState('visual'); + + const onEditorTypeChange = (optionId: string) => { + setSelectedEditorType(optionId); + }; + + return ( + { + const errors: FormikErrors = {}; + + if (!values.name) { + errors.name = 'Rule name is required'; + } else { + if (!validateName(values.name)) { + errors.name = 'Invalid rule name.'; + } + } + + if (values.description && !validateDescription(values.description)) { + errors.description = 'Invalid description.'; + } + + if (!values.logType) { + errors.logType = 'Log type is required'; + } + + if (!values.detection) { + errors.detection = 'Detection is required'; + } + + if (!values.level) { + errors.level = 'Rule level is required'; + } + + if (!values.author) { + errors.author = 'Author name is required'; + } else { + if (!validateName(values.author, AUTHOR_REGEX)) { + errors.author = 'Invalid author.'; + } + } + + if (!values.status) { + errors.status = 'Rule status is required'; + } + + return errors; + }} + onSubmit={(values, { setSubmitting }) => { + setSubmitting(false); + submit(values); + }} + > + {(props) => ( +
+ + onEditorTypeChange(id)} + /> + + + {selectedEditorType === 'yaml' && ( + 0} + errors={Object.keys(props.errors).map( + (key) => props.errors[key as keyof RuleEditorFormModel] as string + )} + change={(e) => { + const formState = mapRuleToForm(e); + props.setValues(formState); + }} + > + )} + + {selectedEditorType === 'visual' && ( + <> + + + + Rule name + + } + isInvalid={props.touched.name && !!props.errors?.name} + error={props.errors.name} + helpText="Rule name must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, and underscores." + > + { + props.handleChange('name')(e); + }} + onBlur={props.handleBlur('name')} + value={props.values.name} + /> + + + + + Log type + + } + isInvalid={props.touched.logType && !!props.errors?.logType} + error={props.errors.logType} + > + ({ value: type, label: type }))} + singleSelection={{ asPlainText: true }} + onChange={(e) => { + props.handleChange('logType')(e[0]?.value ? e[0].value : ''); + }} + onBlur={props.handleBlur('logType')} + selectedOptions={ + props.values.logType + ? [{ value: props.values.logType, label: props.values.logType }] + : [] + } + /> + + + + + + + + Description + - optional + + } + helpText="Description must contain 5-500 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, dots, commas, and underscores." + isInvalid={!!props.errors?.description} + error={props.errors.description} + > + { + props.handleChange('description')(e.target.value); + }} + onBlur={props.handleBlur('description')} + value={props.values.description} + /> + + + + + + Detection + + } + isInvalid={props.touched.detection && !!props.errors?.detection} + error={props.errors.detection} + > + { + props.handleChange('detection')(value); + }} + onBlur={props.handleBlur('detection')} + data-test-subj={'rule_detection_field'} + /> + + + + + Rule level + + } + isInvalid={props.touched.level && !!props.errors?.level} + error={props.errors.level} + > + { + props.handleChange('level')(e[0]?.value ? e[0].value : ''); + }} + onBlur={props.handleBlur('level')} + selectedOptions={ + props.values.level + ? [{ value: props.values.level, label: props.values.level }] + : [] + } + /> + + + + + { + const tags = value.map((option) => ({ label: option.label })); + props.setFieldValue('tags', tags); + }} + onCreateOption={(newTag) => { + props.setFieldValue('tags', [...props.values.tags, { label: newTag }]); + }} + onBlur={props.handleBlur('tags')} + /> + + + + References + - optional + + } + addButtonName="Add another URL" + fields={props.values.references} + onFieldAdd={() => { + props.setFieldValue('references', [...props.values.references, '']); + }} + onFieldEdit={(value: string, index: number) => { + props.setFieldValue('references', [ + ...props.values.references.slice(0, index), + value, + ...props.values.references.slice(index + 1), + ]); + }} + onFieldRemove={(index: number) => { + const newRefs = [...props.values.references]; + newRefs.splice(index, 1); + + props.setFieldValue('references', newRefs); + }} + data-test-subj={'rule_references_field'} + /> + + + False positive cases + - optional + + } + addButtonName="Add another case" + fields={props.values.falsePositives} + onFieldAdd={() => { + props.setFieldValue('falsePositives', [...props.values.falsePositives, '']); + }} + onFieldEdit={(value: string, index: number) => { + props.setFieldValue('falsePositives', [ + ...props.values.falsePositives.slice(0, index), + value, + ...props.values.falsePositives.slice(index + 1), + ]); + }} + onFieldRemove={(index: number) => { + const newCases = [...props.values.falsePositives]; + newCases.splice(index, 1); + + props.setFieldValue('falsePositives', newCases); + }} + data-test-subj={'rule_falsePositives_field'} + /> + + + Author + + } + helpText="Author must contain 5-50 characters. Valid characters are a-z, A-Z, 0-9, hyphens, spaces, commas, and underscores." + isInvalid={props.touched.author && !!props.errors?.author} + error={props.errors.author} + > + { + props.handleChange('author')(e); + }} + onBlur={props.handleBlur('author')} + value={props.values.author} + /> + + + + + + Rule Status + + } + isInvalid={props.touched.status && !!props.errors?.status} + error={props.errors.status} + > + ({ value: type, label: type }))} + singleSelection={{ asPlainText: true }} + onChange={(e) => { + props.handleChange('status')(e[0]?.value ? e[0].value : ''); + }} + onBlur={props.handleBlur('status')} + selectedOptions={ + props.values.status + ? [{ value: props.values.status, label: props.values.status }] + : [] + } + /> + + + )} + + + + + + + Cancel + + + props.handleSubmit()} + data-test-subj={'submit_rule_form_button'} + fill + > + {mode === 'create' ? 'Create' : 'Save changes'} + + + + + )} +
+ ); +}; diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorFormState.ts b/public/pages/Rules/components/RuleEditor/RuleEditorFormModel.ts similarity index 86% rename from public/pages/Rules/components/RuleEditor/RuleEditorFormState.ts rename to public/pages/Rules/components/RuleEditor/RuleEditorFormModel.ts index ccd8dd0c4..a46a199bb 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorFormState.ts +++ b/public/pages/Rules/components/RuleEditor/RuleEditorFormModel.ts @@ -5,7 +5,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; -export interface RuleEditorFormState { +export interface RuleEditorFormModel { id: string; log_source: string; logType: string; @@ -20,7 +20,7 @@ export interface RuleEditorFormState { falsePositives: string[]; } -export const ruleEditorStateDefaultValue: RuleEditorFormState = { +export const ruleEditorStateDefaultValue: RuleEditorFormModel = { id: '25b9c01c-350d-4b95-bed1-836d04a4f324', log_source: '', logType: '', diff --git a/public/pages/Rules/components/RuleEditor/VisualRuleEditor.tsx b/public/pages/Rules/components/RuleEditor/VisualRuleEditor.tsx deleted file mode 100644 index 5708eb1c1..000000000 --- a/public/pages/Rules/components/RuleEditor/VisualRuleEditor.tsx +++ /dev/null @@ -1,385 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { Formik, Form, FormikErrors } from 'formik'; -import { NotificationsStart } from 'opensearch-dashboards/public'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldText, - EuiButton, - EuiSpacer, - EuiTextArea, - EuiComboBox, - EuiCodeEditor, -} from '@elastic/eui'; -import { FieldTextArray } from './FieldTextArray'; -import { ruleStatus, ruleTypes } from '../../utils/constants'; -import { - authorErrorString, - AUTHOR_REGEX, - descriptionErrorString, - nameErrorString, - validateDescription, - validateName, -} from '../../../../utils/validation'; -import { RuleEditorFormState } from './RuleEditorFormState'; -import { FormSubmitionErrorToastNotification } from './FormSubmitionErrorToastNotification'; - -export interface VisualRuleEditorProps { - ruleEditorFormState: RuleEditorFormState; - setRuleEditorFormState: React.Dispatch>; - notifications?: NotificationsStart; - submit: () => void; - cancel: () => void; - mode: 'create' | 'edit'; -} - -export const VisualRuleEditor: React.FC = ({ - ruleEditorFormState, - setRuleEditorFormState, - notifications, - submit, - cancel, - mode, -}) => { - return ( - { - const errors: FormikErrors = {}; - - if (!values.name) { - errors.name = 'Rule name is required'; - } else { - if (!validateName(values.name)) { - errors.name = nameErrorString; - } - } - - if (!validateDescription(values.description)) { - errors.description = descriptionErrorString; - } - - if (!values.logType) { - errors.logType = 'Log type is required'; - } - - if (!values.detection) { - errors.detection = 'Detection is required'; - } - - if (!values.level) { - errors.level = 'Rule level is required'; - } - - if (!values.author) { - errors.author = 'Author name is required'; - } else { - if (!validateName(values.author, AUTHOR_REGEX)) { - errors.author = authorErrorString; - } - } - - if (!values.status) { - errors.status = 'Rule status is required'; - } - - return errors; - }} - onSubmit={(values, { setSubmitting }) => { - setSubmitting(false); - submit(); - }} - > - {(props) => ( -
- - - - - { - props.handleChange('name')(e); - setRuleEditorFormState({ ...props.values, name: e.target.value }); - }} - onBlur={props.handleBlur('name')} - value={props.values.name} - /> - - - - - ({ value: type, label: type }))} - singleSelection={{ asPlainText: true }} - onChange={(e) => { - props.handleChange('logType')(e[0]?.value ? e[0].value : ''); - setRuleEditorFormState({ ...props.values, logType: e[0]?.value || '' }); - }} - onBlur={props.handleBlur('logType')} - selectedOptions={ - props.values.logType - ? [{ value: props.values.logType, label: props.values.logType }] - : [] - } - /> - - - - - - - - { - props.handleChange('description')(e.target.value); - setRuleEditorFormState({ ...props.values, description: e.target.value }); - }} - onBlur={props.handleBlur('description')} - value={props.values.description} - /> - - - - - - { - props.handleChange('detection')(value); - setRuleEditorFormState({ ...props.values, detection: value }); - }} - onBlur={props.handleBlur('detection')} - data-test-subj={'rule_detection_field'} - /> - - - - - { - props.handleChange('level')(e[0]?.value ? e[0].value : ''); - setRuleEditorFormState({ ...props.values, level: e[0]?.value || '' }); - }} - onBlur={props.handleBlur('level')} - selectedOptions={ - props.values.level ? [{ value: props.values.level, label: props.values.level }] : [] - } - /> - - - - - - { - const tags = value.map((option) => ({ label: option.label })); - props.setFieldValue('tags', tags); - setRuleEditorFormState({ - ...props.values, - tags, - }); - }} - onCreateOption={(newTag) => { - props.setFieldValue('tags', [...props.values.tags, { label: newTag }]); - setRuleEditorFormState((prevState) => ({ - ...prevState, - tags: [...prevState.tags, { label: newTag }], - })); - }} - onBlur={props.handleBlur('tags')} - data-test-subj={'rule_tags_dropdown'} - selectedOptions={props.values.tags} - /> - - - - { - props.setFieldValue('references', [...props.values.references, '']); - setRuleEditorFormState((prevState) => ({ - ...prevState, - references: [...prevState.references, ''], - })); - }} - onFieldEdit={(value: string, index: number) => { - props.setFieldValue('references', [ - ...props.values.references.slice(0, index), - value, - ...props.values.references.slice(index + 1), - ]); - setRuleEditorFormState((prevState) => ({ - ...prevState, - references: [ - ...prevState.references.slice(0, index), - value, - ...prevState.references.slice(index + 1), - ], - })); - }} - onFieldRemove={(index: number) => { - const newRefs = [...props.values.references]; - newRefs.splice(index, 1); - - props.setFieldValue('references', newRefs); - setRuleEditorFormState((prevState) => ({ - ...prevState, - references: newRefs, - })); - }} - data-test-subj={'rule_references_field'} - /> - - { - props.setFieldValue('falsePositives', [...props.values.falsePositives, '']); - setRuleEditorFormState((prevState) => ({ - ...prevState, - falsePositives: [...prevState.falsePositives, ''], - })); - }} - onFieldEdit={(value: string, index: number) => { - props.setFieldValue('falsePositives', [ - ...props.values.falsePositives.slice(0, index), - value, - ...props.values.falsePositives.slice(index + 1), - ]); - setRuleEditorFormState((prevState) => ({ - ...prevState, - falsePositives: [ - ...prevState.falsePositives.slice(0, index), - value, - ...prevState.falsePositives.slice(index + 1), - ], - })); - }} - onFieldRemove={(index: number) => { - const newCases = [...props.values.falsePositives]; - newCases.splice(index, 1); - - props.setFieldValue('falsePositives', newCases); - setRuleEditorFormState((prevState) => ({ - ...prevState, - falsePositives: newCases, - })); - }} - data-test-subj={'rule_falsePositives_field'} - /> - - - { - props.handleChange('author')(e); - setRuleEditorFormState({ ...props.values, author: e.target.value }); - }} - onBlur={props.handleBlur('author')} - value={props.values.author} - /> - - - - - - ({ value: type, label: type }))} - singleSelection={{ asPlainText: true }} - onChange={(e) => { - props.handleChange('status')(e[0]?.value ? e[0].value : ''); - setRuleEditorFormState({ ...props.values, status: e[0]?.value || '' }); - }} - onBlur={props.handleBlur('status')} - selectedOptions={ - props.values.status - ? [{ value: props.values.status, label: props.values.status }] - : [] - } - /> - - - - - - - Cancel - - - props.handleSubmit()} - data-test-subj={'submit_rule_form_button'} - fill - > - {mode === 'create' ? 'Create' : 'Save changes'} - - - - - )} -
- ); -}; diff --git a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.test.tsx b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.test.tsx deleted file mode 100644 index 1c7bfb84a..000000000 --- a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { YamlRuleEditor } from './YamlRuleEditor'; - -describe(' spec', () => { - it('renders the component', () => { - const { container } = render( - {}} - rule={{ - id: '25b9c01c-350d-4b95-bed1-836d04a4f324', - category: 'windows', - title: 'Testing rule', - description: 'Testing Description', - status: 'experimental', - author: 'Bhabesh Raj', - references: [ - { - value: 'https://securelist.com/operation-tunnelsnake-and-moriya-rootkit/101831', - }, - ], - tags: [ - { - value: 'attack.persistence', - }, - { - value: 'attack.privilege_escalation', - }, - { - value: 'attack.t1543.003', - }, - ], - log_source: '', - detection: - 'selection:\n Provider_Name: Service Control Manager\n EventID: 7045\n ServiceName: ZzNetSvc\ncondition: selection\n', - level: 'high', - false_positives: [ - { - value: 'Unknown', - }, - ], - }} - > -
Testing YamlRuleEditor
-
- ); - expect(container.firstChild).toMatchSnapshot(); - }); -}); diff --git a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx deleted file mode 100644 index a1033dea7..000000000 --- a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState } from 'react'; -import { load } from 'js-yaml'; -import { - EuiFormRow, - EuiCodeEditor, - EuiLink, - EuiSpacer, - EuiText, - EuiForm, - EuiFlexGroup, - EuiButton, - EuiFlexItem, -} from '@elastic/eui'; -import FormFieldHeader from '../../../../components/FormFieldHeader'; -import { Rule } from '../../../../../models/interfaces'; -import { - AUTHOR_REGEX, - validateDescription, - validateName, - authorErrorString, - descriptionErrorString, - titleErrorString, -} from '../../../../utils/validation'; -import { - mapRuleToYamlObject, - mapYamlObjectToYamlString, - mapYamlObjectToRule, -} from '../../utils/mappers'; - -export interface YamlRuleEditorProps { - rule: Rule; - change: React.Dispatch; - submit: () => void; - cancel: () => void; - mode: 'create' | 'edit'; -} - -export interface YamlEditorState { - errors: string[] | null; - value?: string; -} - -const validateRule = (rule: Rule): string[] | null => { - const requiredFiledsValidationErrors: Array = []; - - if (!rule.title) { - requiredFiledsValidationErrors.push('Title is required'); - } - if (!rule.category) { - requiredFiledsValidationErrors.push('Logsource is required'); - } - if (!rule.level) { - requiredFiledsValidationErrors.push('Level is required'); - } - if (!rule.author) { - requiredFiledsValidationErrors.push('Author is required'); - } - if (!rule.status) { - requiredFiledsValidationErrors.push('Status is required'); - } - - if (requiredFiledsValidationErrors.length > 0) { - return requiredFiledsValidationErrors; - } - - if (!validateName(rule.title, AUTHOR_REGEX)) { - return [titleErrorString]; - } - if (!validateDescription(rule.description)) { - return [descriptionErrorString]; - } - if (!validateName(rule.author, AUTHOR_REGEX)) { - return [authorErrorString]; - } - - return null; -}; - -export const YamlRuleEditor: React.FC = ({ - rule, - change, - submit, - cancel, - mode, -}) => { - const yamlObject = mapRuleToYamlObject(rule); - - const [state, setState] = useState({ - errors: null, - value: mapYamlObjectToYamlString(yamlObject), - }); - - const onChange = (value: string) => { - setState((prevState) => ({ ...prevState, value })); - }; - - const onBlur = () => { - if (!state.value) { - setState((prevState) => ({ ...prevState, errors: ['Rule cannot be empty'] })); - return; - } - try { - const yamlObject = load(state.value); - - const rule = mapYamlObjectToRule(yamlObject); - - change(rule); - - const errors = validateRule(rule); - - if (errors && errors.length > 0) { - setState((prevState) => ({ ...prevState, errors: errors })); - return; - } - - setState((prevState) => ({ ...prevState, errors: null })); - } catch (error) { - setState((prevState) => ({ ...prevState, errors: ['Invalid YAML'] })); - - console.warn('Security Analytics - Rule Eritor - Yaml load', error); - } - }; - - return ( - <> - 0} - error={state.errors} - component="form" - > - } - fullWidth={true} - > - <> - - Use the YAML editor to define a sigma rule. See{' '} - - Sigma specification - {' '} - for rule structure and schema. - - - - - - - - - - - Cancel - - - - {mode === 'create' ? 'Create' : 'Save changes'} - - - - - - ); -}; diff --git a/public/pages/Rules/components/RuleEditor/__snapshots__/YamlRuleEditor.test.tsx.snap b/public/pages/Rules/components/RuleEditor/__snapshots__/YamlRuleEditor.test.tsx.snap deleted file mode 100644 index 32142ba87..000000000 --- a/public/pages/Rules/components/RuleEditor/__snapshots__/YamlRuleEditor.test.tsx.snap +++ /dev/null @@ -1,210 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` spec renders the component 1`] = ` -
-
-
- -
-
-
-
- Use the YAML editor to define a sigma rule. See - - - Sigma specification - - - for rule structure and schema. -
-
-
-
- -
-