From 65de4df70561ceff6f4612ee3c38a629fa52287d Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Wed, 23 Nov 2022 21:07:49 +0100 Subject: [PATCH 01/15] remove unused service Signed-off-by: Aleksandar Djindjic --- public/pages/Rules/components/RuleEditor/RuleEditor.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx index 305569059..ba54db1aa 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { BrowserServices } from '../../../../models/interfaces'; import React, { ChangeEvent, useState } from 'react'; import { ContentPanel } from '../../../../components/ContentPanel'; import { @@ -31,7 +30,6 @@ import { } from '../../../../utils/validation'; export interface RuleEditorProps { - services: BrowserServices; title: string; FooterActions: React.FC<{ rule: Rule }>; rule?: Rule; From 7d8f057649901921734cefdb39027d96ed1dcd03 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Thu, 24 Nov 2022 19:57:27 +0100 Subject: [PATCH 02/15] refactor form state Signed-off-by: Aleksandar Djindjic --- .../components/RuleEditor/RuleEditor.tsx | 255 ++++++++++++------ .../DuplicateRule/DuplicateRule.tsx | 1 - .../Rules/containers/EditRule/EditRule.tsx | 1 - .../containers/ImportRule/ImportRule.tsx | 7 +- 4 files changed, 179 insertions(+), 85 deletions(-) diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx index ba54db1aa..a0a0a33d2 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx @@ -35,123 +35,216 @@ export interface RuleEditorProps { rule?: Rule; } +export interface VisualEditorFormState { + id: string; + log_source: string; + + logType: string; + name: string; + description: string; + status: string; + author: string; + references: string[]; + tags: EuiComboBoxOptionOption[]; + detection: string; + level: string; + falsePositives: string[]; +} + +export interface VisualEditorFormErrorsState { + nameError: string | null; + descriptionError: string | null; + authorError: string | null; +} + +const mapFormToRule = (formState: VisualEditorFormState): Rule => { + return { + id: formState.id, + category: formState.logType, + title: formState.name, + description: formState.description, + status: formState.status, + author: formState.author, + references: formState.references.map((ref) => ({ value: ref })), + tags: formState.tags.map((tag) => ({ value: tag.label })), + log_source: formState.log_source, + detection: formState.detection, + level: formState.level, + false_positives: formState.falsePositives.map((falsePositive) => ({ + value: falsePositive, + })), + }; +}; + +const mapRuleToForm = (rule: Rule): VisualEditorFormState => { + return { + id: rule.id, + log_source: rule.log_source, + logType: rule.category, + name: rule.title, + description: rule.description, + status: rule.status, + author: rule.author, + references: rule.references.map((ref) => ref.value), + tags: rule.tags.map((tag) => ({ label: tag.value })), + detection: rule.detection, + level: rule.level, + falsePositives: rule.false_positives.map((falsePositive) => falsePositive.value), + }; +}; + +const newRuyleDefaultState: VisualEditorFormState = { + id: '25b9c01c-350d-4b95-bed1-836d04a4f324', + log_source: '', + logType: '', + name: '', + description: '', + status: '', + author: '', + references: [''], + tags: [], + detection: '', + level: '', + falsePositives: [''], +}; + export const RuleEditor: React.FC = ({ title, rule, FooterActions }) => { - const [name, setName] = useState(rule?.title || ''); + const [visualEditorFormState, setVisualEditorFormState] = useState( + rule ? mapRuleToForm(rule) : newRuyleDefaultState + ); + + const [visualEditorErrors, setVisualEditorErrors] = useState({ + nameError: null, + descriptionError: null, + authorError: null, + }); + + const getRule = (): Rule => { + return mapFormToRule(visualEditorFormState); + }; + const onNameChange = (e: ChangeEvent) => { - setName(e.target.value); + const { value: name } = e.target; + setVisualEditorFormState((prevState) => ({ ...prevState, name })); }; - const [nameError, setNameError] = useState(''); const onNameBlur = (e: ChangeEvent) => { if (!validateName(e.target.value)) { - setNameError(nameErrorString); + setVisualEditorErrors((prevState) => ({ ...prevState, nameError: nameErrorString })); } else { - setNameError(''); + setVisualEditorErrors((prevState) => ({ ...prevState, nameError: null })); } }; - const [logType, setLogType] = useState(rule?.category || ''); const onLogTypeChange = (e: ChangeEvent) => { - setLogType(e.target.value); + const { value: logType } = e.target; + setVisualEditorFormState((prevState) => ({ ...prevState, logType })); }; - const [description, setDescription] = useState(rule?.description || ''); const onDescriptionChange = (e: ChangeEvent) => { - setDescription(e.target.value); + const { value: description } = e.target; + setVisualEditorFormState((prevState) => ({ ...prevState, description })); }; - const [descriptionError, setDescriptionError] = useState(''); const onDescriptionBlur = (e: ChangeEvent) => { if (!validateDescription(e.target.value)) { - setDescriptionError(descriptionErrorString); + setVisualEditorErrors((prevState) => ({ + ...prevState, + descriptionError: descriptionErrorString, + })); } else { - setDescriptionError(''); + setVisualEditorErrors((prevState) => ({ ...prevState, descriptionError: null })); } }; - const [level, setLevel] = useState(rule?.level || ''); const onLevelChange = (e: ChangeEvent) => { - setLevel(e.target.value); + const { value: level } = e.target; + setVisualEditorFormState((prevState) => ({ ...prevState, level })); }; - const [tags, setTags] = useState(rule?.tags.map((tag) => ({ label: tag.value })) || []); const onTagsChange = (selectedOptions: EuiComboBoxOptionOption[]) => { - setTags(selectedOptions.map((option) => ({ label: option.label }))); + const tags = selectedOptions.map((option) => ({ label: option.label })); + setVisualEditorFormState((prevState) => ({ ...prevState, tags })); }; const onCreateTag = (value: string) => { - setTags([...tags, { label: value }]); + setVisualEditorFormState((prevState) => ({ + ...prevState, + tags: [...prevState.tags, { label: value }], + })); }; - const [author, setAuthor] = useState(rule?.author || ''); const onAuthorChange = (e: ChangeEvent) => { - setAuthor(e.target.value); + const { value: author } = e.target; + setVisualEditorFormState((prevState) => ({ ...prevState, author })); }; - const [authorError, setAuthorError] = useState(''); + const onAuthorBlur = (e: ChangeEvent) => { if (!validateName(e.target.value, AUTHOR_REGEX)) { - setAuthorError(authorErrorString); + setVisualEditorErrors((prevState) => ({ ...prevState, authorError: authorErrorString })); } else { - setAuthorError(''); + setVisualEditorErrors((prevState) => ({ ...prevState, authorError: null })); } }; - const [status, setRuleStatus] = useState(rule?.status || ''); const onStatusChange = (e: ChangeEvent) => { - setRuleStatus(e.target.value); + const { value: status } = e.target; + setVisualEditorFormState((prevState) => ({ ...prevState, status })); }; - const [detection, setDetection] = useState(rule?.detection || ''); const onDetectionChange = (value: string) => { - setDetection(value); + setVisualEditorFormState((prevState) => ({ ...prevState, detection: value })); }; - const [references, setReferences] = useState( - rule?.references.map((ref) => ref.value) || [''] - ); const onReferenceAdd = () => { - setReferences([...references, '']); + setVisualEditorFormState((prevState) => ({ + ...prevState, + references: [...prevState.references, ''], + })); }; const onReferenceEdit = (value: string, index: number) => { - setReferences([...references.slice(0, index), value, ...references.slice(index + 1)]); + setVisualEditorFormState((prevState) => ({ + ...prevState, + references: [ + ...prevState.references.slice(0, index), + value, + ...prevState.references.slice(index + 1), + ], + })); }; const onReferenceRemove = (index: number) => { - const newRefs = [...references]; - newRefs.splice(index, 1); - setReferences(newRefs); + setVisualEditorFormState((prevState) => { + const newRefs = [...prevState.references]; + newRefs.splice(index, 1); + return { + ...prevState, + references: newRefs, + }; + }); }; - const [falsePositives, setFalsePositives] = useState( - rule?.false_positives.map((falsePositive) => falsePositive.value) || [''] - ); const onFalsePositiveAdd = () => { - setFalsePositives([...falsePositives, '']); + setVisualEditorFormState((prevState) => ({ + ...prevState, + falsePositives: [...prevState.falsePositives, ''], + })); }; const onFalsePositiveEdit = (value: string, index: number) => { - setFalsePositives([ - ...falsePositives.slice(0, index), - value, - ...falsePositives.slice(index + 1), - ]); + setVisualEditorFormState((prevState) => ({ + ...prevState, + falsePositives: [ + ...prevState.falsePositives.slice(0, index), + value, + ...prevState.falsePositives.slice(index + 1), + ], + })); }; const onFalsePositiveRemove = (index: number) => { - const newFalsePositives = [...falsePositives]; - newFalsePositives.splice(index, 1); - setFalsePositives(newFalsePositives); - }; - - const getRule = (): Rule => { - return { - id: '25b9c01c-350d-4b95-bed1-836d04a4f324', - category: logType, - title: name, - description: description, - status: status, - author: author, - references: references.map((ref) => ({ value: ref })), - tags: tags.map((tag) => ({ value: tag.label })), - log_source: '', - detection: detection, - level: level, - false_positives: falsePositives.map((falsePositive) => ({ value: falsePositive })), - }; + setVisualEditorFormState((prevState) => { + const newFalsePositives = [...prevState.falsePositives]; + newFalsePositives.splice(index, 1); + return { + ...prevState, + falsePositives: newFalsePositives, + }; + }); }; return ( @@ -159,10 +252,14 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio - + = ({ title, rule, FooterActio hasNoInitialSelection={true} options={ruleTypes.map((type: string) => ({ value: type, text: type }))} onChange={onLogTypeChange} - value={logType} + value={visualEditorFormState.logType} required data-test-subj={'rule_type_dropdown'} /> @@ -189,11 +286,11 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio = ({ title, rule, FooterActio @@ -224,7 +321,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio { value: 'low', text: 'Low' }, ]} onChange={onLevelChange} - value={level} + value={visualEditorFormState.level} required data-test-subj={'rule_severity_dropdown'} /> @@ -235,7 +332,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio = ({ title, rule, FooterActio = ({ title, rule, FooterActio - + = ({ title, rule, FooterActio hasNoInitialSelection={true} options={ruleStatus.map((status: string) => ({ value: status, text: status }))} onChange={onStatusChange} - value={status} + value={visualEditorFormState.status} required data-test-subj={'rule_status_dropdown'} /> diff --git a/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx b/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx index 3c389679f..ea4a09edf 100644 --- a/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx +++ b/public/pages/Rules/containers/DuplicateRule/DuplicateRule.tsx @@ -61,7 +61,6 @@ export const DuplicateRule: React.FC = ({ return ( diff --git a/public/pages/Rules/containers/EditRule/EditRule.tsx b/public/pages/Rules/containers/EditRule/EditRule.tsx index 6eb092cba..f9b788cdc 100644 --- a/public/pages/Rules/containers/EditRule/EditRule.tsx +++ b/public/pages/Rules/containers/EditRule/EditRule.tsx @@ -66,7 +66,6 @@ export const EditRule: React.FC = ({ return ( diff --git a/public/pages/Rules/containers/ImportRule/ImportRule.tsx b/public/pages/Rules/containers/ImportRule/ImportRule.tsx index e85cfb4de..e4fd86e76 100644 --- a/public/pages/Rules/containers/ImportRule/ImportRule.tsx +++ b/public/pages/Rules/containers/ImportRule/ImportRule.tsx @@ -72,12 +72,7 @@ export const ImportRule: React.FC = ({ history, services, notif })) || [], }; setContent( - + ); } catch (error: any) { setFileError('Invalid file content'); From 91c07b7d1a36a4e5fa78c7f3dfbdf954c75f703a Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Fri, 25 Nov 2022 22:43:56 +0100 Subject: [PATCH 03/15] extract model and mappers Signed-off-by: Aleksandar Djindjic --- .../components/RuleEditor/RuleEditor.tsx | 110 +++++------------- .../RuleEditor/RuleEditorFormState.model.ts | 21 ++++ .../Rules/components/RuleEditor/mappers.ts | 42 +++++++ 3 files changed, 93 insertions(+), 80 deletions(-) create mode 100644 public/pages/Rules/components/RuleEditor/RuleEditorFormState.model.ts create mode 100644 public/pages/Rules/components/RuleEditor/mappers.ts diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx index a0a0a33d2..d6299c3de 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx @@ -28,6 +28,8 @@ import { validateDescription, validateName, } from '../../../../utils/validation'; +import { RuleEditorFormState } from './RuleEditorFormState.model'; +import { mapFormToRule, mapRuleToForm } from './mappers'; export interface RuleEditorProps { title: string; @@ -35,65 +37,13 @@ export interface RuleEditorProps { rule?: Rule; } -export interface VisualEditorFormState { - id: string; - log_source: string; - - logType: string; - name: string; - description: string; - status: string; - author: string; - references: string[]; - tags: EuiComboBoxOptionOption[]; - detection: string; - level: string; - falsePositives: string[]; -} - export interface VisualEditorFormErrorsState { nameError: string | null; descriptionError: string | null; authorError: string | null; } -const mapFormToRule = (formState: VisualEditorFormState): Rule => { - return { - id: formState.id, - category: formState.logType, - title: formState.name, - description: formState.description, - status: formState.status, - author: formState.author, - references: formState.references.map((ref) => ({ value: ref })), - tags: formState.tags.map((tag) => ({ value: tag.label })), - log_source: formState.log_source, - detection: formState.detection, - level: formState.level, - false_positives: formState.falsePositives.map((falsePositive) => ({ - value: falsePositive, - })), - }; -}; - -const mapRuleToForm = (rule: Rule): VisualEditorFormState => { - return { - id: rule.id, - log_source: rule.log_source, - logType: rule.category, - name: rule.title, - description: rule.description, - status: rule.status, - author: rule.author, - references: rule.references.map((ref) => ref.value), - tags: rule.tags.map((tag) => ({ label: tag.value })), - detection: rule.detection, - level: rule.level, - falsePositives: rule.false_positives.map((falsePositive) => falsePositive.value), - }; -}; - -const newRuyleDefaultState: VisualEditorFormState = { +const newRuyleDefaultState: RuleEditorFormState = { id: '25b9c01c-350d-4b95-bed1-836d04a4f324', log_source: '', logType: '', @@ -109,7 +59,7 @@ const newRuyleDefaultState: VisualEditorFormState = { }; export const RuleEditor: React.FC = ({ title, rule, FooterActions }) => { - const [visualEditorFormState, setVisualEditorFormState] = useState( + const [ruleEditorFormState, setRuleEditorFormState] = useState( rule ? mapRuleToForm(rule) : newRuyleDefaultState ); @@ -120,12 +70,12 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio }); const getRule = (): Rule => { - return mapFormToRule(visualEditorFormState); + return mapFormToRule(ruleEditorFormState); }; const onNameChange = (e: ChangeEvent) => { const { value: name } = e.target; - setVisualEditorFormState((prevState) => ({ ...prevState, name })); + setRuleEditorFormState((prevState) => ({ ...prevState, name })); }; const onNameBlur = (e: ChangeEvent) => { if (!validateName(e.target.value)) { @@ -137,12 +87,12 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio const onLogTypeChange = (e: ChangeEvent) => { const { value: logType } = e.target; - setVisualEditorFormState((prevState) => ({ ...prevState, logType })); + setRuleEditorFormState((prevState) => ({ ...prevState, logType })); }; const onDescriptionChange = (e: ChangeEvent) => { const { value: description } = e.target; - setVisualEditorFormState((prevState) => ({ ...prevState, description })); + setRuleEditorFormState((prevState) => ({ ...prevState, description })); }; const onDescriptionBlur = (e: ChangeEvent) => { if (!validateDescription(e.target.value)) { @@ -157,15 +107,15 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio const onLevelChange = (e: ChangeEvent) => { const { value: level } = e.target; - setVisualEditorFormState((prevState) => ({ ...prevState, level })); + setRuleEditorFormState((prevState) => ({ ...prevState, level })); }; const onTagsChange = (selectedOptions: EuiComboBoxOptionOption[]) => { const tags = selectedOptions.map((option) => ({ label: option.label })); - setVisualEditorFormState((prevState) => ({ ...prevState, tags })); + setRuleEditorFormState((prevState) => ({ ...prevState, tags })); }; const onCreateTag = (value: string) => { - setVisualEditorFormState((prevState) => ({ + setRuleEditorFormState((prevState) => ({ ...prevState, tags: [...prevState.tags, { label: value }], })); @@ -173,7 +123,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio const onAuthorChange = (e: ChangeEvent) => { const { value: author } = e.target; - setVisualEditorFormState((prevState) => ({ ...prevState, author })); + setRuleEditorFormState((prevState) => ({ ...prevState, author })); }; const onAuthorBlur = (e: ChangeEvent) => { @@ -186,21 +136,21 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio const onStatusChange = (e: ChangeEvent) => { const { value: status } = e.target; - setVisualEditorFormState((prevState) => ({ ...prevState, status })); + setRuleEditorFormState((prevState) => ({ ...prevState, status })); }; const onDetectionChange = (value: string) => { - setVisualEditorFormState((prevState) => ({ ...prevState, detection: value })); + setRuleEditorFormState((prevState) => ({ ...prevState, detection: value })); }; const onReferenceAdd = () => { - setVisualEditorFormState((prevState) => ({ + setRuleEditorFormState((prevState) => ({ ...prevState, references: [...prevState.references, ''], })); }; const onReferenceEdit = (value: string, index: number) => { - setVisualEditorFormState((prevState) => ({ + setRuleEditorFormState((prevState) => ({ ...prevState, references: [ ...prevState.references.slice(0, index), @@ -210,7 +160,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio })); }; const onReferenceRemove = (index: number) => { - setVisualEditorFormState((prevState) => { + setRuleEditorFormState((prevState) => { const newRefs = [...prevState.references]; newRefs.splice(index, 1); return { @@ -221,13 +171,13 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio }; const onFalsePositiveAdd = () => { - setVisualEditorFormState((prevState) => ({ + setRuleEditorFormState((prevState) => ({ ...prevState, falsePositives: [...prevState.falsePositives, ''], })); }; const onFalsePositiveEdit = (value: string, index: number) => { - setVisualEditorFormState((prevState) => ({ + setRuleEditorFormState((prevState) => ({ ...prevState, falsePositives: [ ...prevState.falsePositives.slice(0, index), @@ -237,7 +187,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio })); }; const onFalsePositiveRemove = (index: number) => { - setVisualEditorFormState((prevState) => { + setRuleEditorFormState((prevState) => { const newFalsePositives = [...prevState.falsePositives]; newFalsePositives.splice(index, 1); return { @@ -259,7 +209,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio > = ({ title, rule, FooterActio hasNoInitialSelection={true} options={ruleTypes.map((type: string) => ({ value: type, text: type }))} onChange={onLogTypeChange} - value={visualEditorFormState.logType} + value={ruleEditorFormState.logType} required data-test-subj={'rule_type_dropdown'} /> @@ -290,7 +240,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio error={visualEditorErrors.descriptionError} > = ({ title, rule, FooterActio @@ -321,7 +271,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio { value: 'low', text: 'Low' }, ]} onChange={onLevelChange} - value={visualEditorFormState.level} + value={ruleEditorFormState.level} required data-test-subj={'rule_severity_dropdown'} /> @@ -332,7 +282,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio = ({ title, rule, FooterActio = ({ title, rule, FooterActio = ({ title, rule, FooterActio > = ({ title, rule, FooterActio hasNoInitialSelection={true} options={ruleStatus.map((status: string) => ({ value: status, text: status }))} onChange={onStatusChange} - value={visualEditorFormState.status} + value={ruleEditorFormState.status} required data-test-subj={'rule_status_dropdown'} /> diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorFormState.model.ts b/public/pages/Rules/components/RuleEditor/RuleEditorFormState.model.ts new file mode 100644 index 000000000..91f0ed822 --- /dev/null +++ b/public/pages/Rules/components/RuleEditor/RuleEditorFormState.model.ts @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiComboBoxOptionOption } from '@elastic/eui'; + +export interface RuleEditorFormState { + id: string; + log_source: string; + logType: string; + name: string; + description: string; + status: string; + author: string; + references: string[]; + tags: EuiComboBoxOptionOption[]; + detection: string; + level: string; + falsePositives: string[]; +} diff --git a/public/pages/Rules/components/RuleEditor/mappers.ts b/public/pages/Rules/components/RuleEditor/mappers.ts new file mode 100644 index 000000000..4a8240b4a --- /dev/null +++ b/public/pages/Rules/components/RuleEditor/mappers.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Rule } from '../../../../../models/interfaces'; +import { RuleEditorFormState } from './RuleEditorFormState.model'; + +export const mapFormToRule = (formState: RuleEditorFormState): Rule => { + return { + id: formState.id, + category: formState.logType, + title: formState.name, + description: formState.description, + status: formState.status, + author: formState.author, + references: formState.references.map((ref) => ({ value: ref })), + tags: formState.tags.map((tag) => ({ value: tag.label })), + log_source: formState.log_source, + detection: formState.detection, + level: formState.level, + false_positives: formState.falsePositives.map((falsePositive) => ({ + value: falsePositive, + })), + }; +}; + +export const mapRuleToForm = (rule: Rule): RuleEditorFormState => { + return { + id: rule.id, + log_source: rule.log_source, + logType: rule.category, + name: rule.title, + description: rule.description, + status: rule.status, + author: rule.author, + references: rule.references.map((ref) => ref.value), + tags: rule.tags.map((tag) => ({ label: tag.value })), + detection: rule.detection, + level: rule.level, + falsePositives: rule.false_positives.map((falsePositive) => falsePositive.value), + }; +}; From cfb07b1e1e314e5b600e84303307da43e65256f5 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Sat, 26 Nov 2022 00:51:04 +0100 Subject: [PATCH 04/15] Extract Visual Rule Editor Component Signed-off-by: Aleksandar Djindjic --- .../components/RuleEditor/RuleEditor.tsx | 294 +--------------- .../RuleEditor/VisualRuleEditor.tsx | 318 ++++++++++++++++++ 2 files changed, 324 insertions(+), 288 deletions(-) create mode 100644 public/pages/Rules/components/RuleEditor/VisualRuleEditor.tsx diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx index d6299c3de..eab69e2dd 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx @@ -3,33 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { ChangeEvent, useState } from 'react'; +import React, { useState } from 'react'; import { ContentPanel } from '../../../../components/ContentPanel'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldText, - EuiSelect, - EuiSpacer, - EuiTextArea, - EuiComboBox, - EuiCodeEditor, - EuiComboBoxOptionOption, -} from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; import { Rule } from '../../../../../models/interfaces'; -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.model'; import { mapFormToRule, mapRuleToForm } from './mappers'; +import { VisualRuleEditor } from './VisualRuleEditor'; export interface RuleEditorProps { title: string; @@ -63,280 +43,18 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio rule ? mapRuleToForm(rule) : newRuyleDefaultState ); - const [visualEditorErrors, setVisualEditorErrors] = useState({ - nameError: null, - descriptionError: null, - authorError: null, - }); - const getRule = (): Rule => { return mapFormToRule(ruleEditorFormState); }; - const onNameChange = (e: ChangeEvent) => { - const { value: name } = e.target; - setRuleEditorFormState((prevState) => ({ ...prevState, name })); - }; - const onNameBlur = (e: ChangeEvent) => { - if (!validateName(e.target.value)) { - setVisualEditorErrors((prevState) => ({ ...prevState, nameError: nameErrorString })); - } else { - setVisualEditorErrors((prevState) => ({ ...prevState, nameError: null })); - } - }; - - const onLogTypeChange = (e: ChangeEvent) => { - const { value: logType } = e.target; - setRuleEditorFormState((prevState) => ({ ...prevState, logType })); - }; - - const onDescriptionChange = (e: ChangeEvent) => { - const { value: description } = e.target; - setRuleEditorFormState((prevState) => ({ ...prevState, description })); - }; - const onDescriptionBlur = (e: ChangeEvent) => { - if (!validateDescription(e.target.value)) { - setVisualEditorErrors((prevState) => ({ - ...prevState, - descriptionError: descriptionErrorString, - })); - } else { - setVisualEditorErrors((prevState) => ({ ...prevState, descriptionError: null })); - } - }; - - const onLevelChange = (e: ChangeEvent) => { - const { value: level } = e.target; - setRuleEditorFormState((prevState) => ({ ...prevState, level })); - }; - - const onTagsChange = (selectedOptions: EuiComboBoxOptionOption[]) => { - const tags = selectedOptions.map((option) => ({ label: option.label })); - setRuleEditorFormState((prevState) => ({ ...prevState, tags })); - }; - const onCreateTag = (value: string) => { - setRuleEditorFormState((prevState) => ({ - ...prevState, - tags: [...prevState.tags, { label: value }], - })); - }; - - const onAuthorChange = (e: ChangeEvent) => { - const { value: author } = e.target; - setRuleEditorFormState((prevState) => ({ ...prevState, author })); - }; - - const onAuthorBlur = (e: ChangeEvent) => { - if (!validateName(e.target.value, AUTHOR_REGEX)) { - setVisualEditorErrors((prevState) => ({ ...prevState, authorError: authorErrorString })); - } else { - setVisualEditorErrors((prevState) => ({ ...prevState, authorError: null })); - } - }; - - const onStatusChange = (e: ChangeEvent) => { - const { value: status } = e.target; - setRuleEditorFormState((prevState) => ({ ...prevState, status })); - }; - - const onDetectionChange = (value: string) => { - setRuleEditorFormState((prevState) => ({ ...prevState, detection: value })); - }; - - const onReferenceAdd = () => { - setRuleEditorFormState((prevState) => ({ - ...prevState, - references: [...prevState.references, ''], - })); - }; - const onReferenceEdit = (value: string, index: number) => { - setRuleEditorFormState((prevState) => ({ - ...prevState, - references: [ - ...prevState.references.slice(0, index), - value, - ...prevState.references.slice(index + 1), - ], - })); - }; - const onReferenceRemove = (index: number) => { - setRuleEditorFormState((prevState) => { - const newRefs = [...prevState.references]; - newRefs.splice(index, 1); - return { - ...prevState, - references: newRefs, - }; - }); - }; - - const onFalsePositiveAdd = () => { - setRuleEditorFormState((prevState) => ({ - ...prevState, - falsePositives: [...prevState.falsePositives, ''], - })); - }; - const onFalsePositiveEdit = (value: string, index: number) => { - setRuleEditorFormState((prevState) => ({ - ...prevState, - falsePositives: [ - ...prevState.falsePositives.slice(0, index), - value, - ...prevState.falsePositives.slice(index + 1), - ], - })); - }; - const onFalsePositiveRemove = (index: number) => { - setRuleEditorFormState((prevState) => { - const newFalsePositives = [...prevState.falsePositives]; - newFalsePositives.splice(index, 1); - return { - ...prevState, - falsePositives: newFalsePositives, - }; - }); - }; - return ( <> - - - - - - - - - ({ value: type, text: type }))} - onChange={onLogTypeChange} - value={ruleEditorFormState.logType} - required - data-test-subj={'rule_type_dropdown'} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ value: status, text: status }))} - onChange={onStatusChange} - value={ruleEditorFormState.status} - required - data-test-subj={'rule_status_dropdown'} - /> - - diff --git a/public/pages/Rules/components/RuleEditor/VisualRuleEditor.tsx b/public/pages/Rules/components/RuleEditor/VisualRuleEditor.tsx new file mode 100644 index 000000000..6b2845d07 --- /dev/null +++ b/public/pages/Rules/components/RuleEditor/VisualRuleEditor.tsx @@ -0,0 +1,318 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ChangeEvent, useState } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiSelect, + EuiSpacer, + EuiTextArea, + EuiComboBox, + EuiCodeEditor, + EuiComboBoxOptionOption, +} 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.model'; + +export interface VisualRuleEditorProps { + ruleEditorFormState: RuleEditorFormState; + setRuleEditorFormState: React.Dispatch>; +} + +export interface VisualEditorFormErrorsState { + nameError: string | null; + descriptionError: string | null; + authorError: string | null; +} + +export const VisualRuleEditor: React.FC = ({ + ruleEditorFormState, + setRuleEditorFormState, +}) => { + const [visualEditorErrors, setVisualEditorErrors] = useState({ + nameError: null, + descriptionError: null, + authorError: null, + }); + + const onNameChange = (e: ChangeEvent) => { + const { value: name } = e.target; + setRuleEditorFormState((prevState) => ({ ...prevState, name })); + }; + const onNameBlur = (e: ChangeEvent) => { + if (!validateName(e.target.value)) { + setVisualEditorErrors((prevState) => ({ ...prevState, nameError: nameErrorString })); + } else { + setVisualEditorErrors((prevState) => ({ ...prevState, nameError: null })); + } + }; + + const onLogTypeChange = (e: ChangeEvent) => { + const { value: logType } = e.target; + setRuleEditorFormState((prevState) => ({ ...prevState, logType })); + }; + + const onDescriptionChange = (e: ChangeEvent) => { + const { value: description } = e.target; + setRuleEditorFormState((prevState) => ({ ...prevState, description })); + }; + const onDescriptionBlur = (e: ChangeEvent) => { + if (!validateDescription(e.target.value)) { + setVisualEditorErrors((prevState) => ({ + ...prevState, + descriptionError: descriptionErrorString, + })); + } else { + setVisualEditorErrors((prevState) => ({ ...prevState, descriptionError: null })); + } + }; + + const onLevelChange = (e: ChangeEvent) => { + const { value: level } = e.target; + setRuleEditorFormState((prevState) => ({ ...prevState, level })); + }; + + const onTagsChange = (selectedOptions: EuiComboBoxOptionOption[]) => { + const tags = selectedOptions.map((option) => ({ label: option.label })); + setRuleEditorFormState((prevState) => ({ ...prevState, tags })); + }; + const onCreateTag = (value: string) => { + setRuleEditorFormState((prevState) => ({ + ...prevState, + tags: [...prevState.tags, { label: value }], + })); + }; + + const onAuthorChange = (e: ChangeEvent) => { + const { value: author } = e.target; + setRuleEditorFormState((prevState) => ({ ...prevState, author })); + }; + + const onAuthorBlur = (e: ChangeEvent) => { + if (!validateName(e.target.value, AUTHOR_REGEX)) { + setVisualEditorErrors((prevState) => ({ ...prevState, authorError: authorErrorString })); + } else { + setVisualEditorErrors((prevState) => ({ ...prevState, authorError: null })); + } + }; + + const onStatusChange = (e: ChangeEvent) => { + const { value: status } = e.target; + setRuleEditorFormState((prevState) => ({ ...prevState, status })); + }; + + const onDetectionChange = (value: string) => { + setRuleEditorFormState((prevState) => ({ ...prevState, detection: value })); + }; + + const onReferenceAdd = () => { + setRuleEditorFormState((prevState) => ({ + ...prevState, + references: [...prevState.references, ''], + })); + }; + const onReferenceEdit = (value: string, index: number) => { + setRuleEditorFormState((prevState) => ({ + ...prevState, + references: [ + ...prevState.references.slice(0, index), + value, + ...prevState.references.slice(index + 1), + ], + })); + }; + const onReferenceRemove = (index: number) => { + setRuleEditorFormState((prevState) => { + const newRefs = [...prevState.references]; + newRefs.splice(index, 1); + return { + ...prevState, + references: newRefs, + }; + }); + }; + + const onFalsePositiveAdd = () => { + setRuleEditorFormState((prevState) => ({ + ...prevState, + falsePositives: [...prevState.falsePositives, ''], + })); + }; + const onFalsePositiveEdit = (value: string, index: number) => { + setRuleEditorFormState((prevState) => ({ + ...prevState, + falsePositives: [ + ...prevState.falsePositives.slice(0, index), + value, + ...prevState.falsePositives.slice(index + 1), + ], + })); + }; + const onFalsePositiveRemove = (index: number) => { + setRuleEditorFormState((prevState) => { + const newFalsePositives = [...prevState.falsePositives]; + newFalsePositives.splice(index, 1); + return { + ...prevState, + falsePositives: newFalsePositives, + }; + }); + }; + + return ( + <> + + + + + + + + + ({ value: type, text: type }))} + onChange={onLogTypeChange} + value={ruleEditorFormState.logType} + required + data-test-subj={'rule_type_dropdown'} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ value: status, text: status }))} + onChange={onStatusChange} + value={ruleEditorFormState.status} + required + data-test-subj={'rule_status_dropdown'} + /> + + + + + ); +}; From ff45157d83929d2180f24fc1ef6634d3b7cbf688 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Sat, 26 Nov 2022 00:59:14 +0100 Subject: [PATCH 05/15] fix missing default id Signed-off-by: Aleksandar Djindjic --- public/pages/Rules/components/RuleEditor/RuleEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx index eab69e2dd..e16ce5428 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx @@ -40,7 +40,7 @@ const newRuyleDefaultState: RuleEditorFormState = { export const RuleEditor: React.FC = ({ title, rule, FooterActions }) => { const [ruleEditorFormState, setRuleEditorFormState] = useState( - rule ? mapRuleToForm(rule) : newRuyleDefaultState + rule ? { ...mapRuleToForm(rule), id: newRuyleDefaultState.id } : newRuyleDefaultState ); const getRule = (): Rule => { From 013531433844a3a0db7feee45d98d07fee532d18 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Mon, 28 Nov 2022 22:51:11 +0100 Subject: [PATCH 06/15] yaml editor Signed-off-by: Aleksandar Djindjic --- .../components/RuleEditor/RuleEditor.tsx | 42 ++++++++-- .../components/RuleEditor/YamlRuleEditor.tsx | 79 +++++++++++++++++++ 2 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx index e16ce5428..7c3e43a47 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx @@ -5,11 +5,12 @@ import React, { useState } from 'react'; import { ContentPanel } from '../../../../components/ContentPanel'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiButtonGroup } from '@elastic/eui'; import { Rule } from '../../../../../models/interfaces'; import { RuleEditorFormState } from './RuleEditorFormState.model'; import { mapFormToRule, mapRuleToForm } from './mappers'; import { VisualRuleEditor } from './VisualRuleEditor'; +import { YamlRuleEditor } from './YamlRuleEditor'; export interface RuleEditorProps { title: string; @@ -38,11 +39,28 @@ const newRuyleDefaultState: RuleEditorFormState = { falsePositives: [''], }; +const editorTypes = [ + { + id: 'visual', + label: 'Visual Editor', + }, + { + id: 'yaml', + label: 'YAML Editor', + }, +]; + export const RuleEditor: React.FC = ({ title, rule, FooterActions }) => { const [ruleEditorFormState, setRuleEditorFormState] = useState( rule ? { ...mapRuleToForm(rule), id: newRuyleDefaultState.id } : newRuyleDefaultState ); + const [selectedEditorType, setSelectedEditorType] = useState('visual'); + + const onEditorTypeChange = (optionId: string) => { + setSelectedEditorType(optionId); + }; + const getRule = (): Rule => { return mapFormToRule(ruleEditorFormState); }; @@ -50,11 +68,25 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio return ( <> - onEditorTypeChange(id)} /> - + + {selectedEditorType === 'visual' && ( + + )} + {selectedEditorType === 'yaml' && ( + + )} diff --git a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx new file mode 100644 index 000000000..f341b0762 --- /dev/null +++ b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx @@ -0,0 +1,79 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { dump, load } from 'js-yaml'; +import { EuiFormRow, EuiCodeEditor, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { RuleEditorFormState } from './RuleEditorFormState.model'; +import FormFieldHeader from '../../../../components/FormFieldHeader'; + +export interface YamlRuleEditorProps { + ruleEditorFormState: RuleEditorFormState; + setRuleEditorFormState: React.Dispatch>; +} + +export interface YamlEditorErrorsState { + yamlEditorError: string | null; +} + +const formToYaml = (formState: RuleEditorFormState): string => { + try { + const yamlString = dump(formState); + + return yamlString; + } catch (error: any) { + console.warn('Security Analytics - Rule Eritor - Yaml dump', error); + return ''; + } +}; + +export const YamlRuleEditor: React.FC = ({ + ruleEditorFormState, + setRuleEditorFormState, +}) => { + const [yamlEditorError, setYamlEditorError] = useState({ + yamlEditorError: null, + }); + + const onYamlRuleChange = (value: string) => { + if (value === '') { + setYamlEditorError(() => ({ yamlEditorError: 'Required Field' })); + return; + } + try { + const newRuleEditorFormState = load(value); + setRuleEditorFormState(() => newRuleEditorFormState); + setYamlEditorError(() => ({ yamlEditorError: null })); + } catch (error) { + setYamlEditorError(() => ({ yamlEditorError: 'Invalid YAML' })); + + console.warn('Security Analytics - Rule Eritor - Yaml load', error); + } + }; + + return ( + <> + } fullWidth={true}> + <> + + Use the YAML editor to define a sigma rule. See{' '} + + Sigma specification + {' '} + for rule structure and schema. + + + + + + + ); +}; From 491e7a2e702a4ffb4bd78cddb861c242c9dda667 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Tue, 6 Dec 2022 18:56:47 +0100 Subject: [PATCH 07/15] yaml rule editor mappings Signed-off-by: Aleksandar Djindjic --- .../components/RuleEditor/RuleEditor.tsx | 9 +- .../components/RuleEditor/YamlRuleEditor.tsx | 96 ++++++++++++++----- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx index 7c3e43a47..ca4fef832 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx @@ -65,6 +65,11 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio return mapFormToRule(ruleEditorFormState); }; + const onYamlRuleEditorChange = (value: Rule) => { + const formState = mapRuleToForm(value); + setRuleEditorFormState(formState); + }; + return ( <> @@ -83,8 +88,8 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio )} {selectedEditorType === 'yaml' && ( )} diff --git a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx index f341b0762..07ddb2d27 100644 --- a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx @@ -6,21 +6,22 @@ import React, { useState } from 'react'; import { dump, load } from 'js-yaml'; import { EuiFormRow, EuiCodeEditor, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; -import { RuleEditorFormState } from './RuleEditorFormState.model'; import FormFieldHeader from '../../../../components/FormFieldHeader'; +import { Rule } from '../../../../../models/interfaces'; export interface YamlRuleEditorProps { - ruleEditorFormState: RuleEditorFormState; - setRuleEditorFormState: React.Dispatch>; + rule: Rule; + change: React.Dispatch; } -export interface YamlEditorErrorsState { - yamlEditorError: string | null; +export interface YamlEditorState { + error: string | null; + value?: string; } -const formToYaml = (formState: RuleEditorFormState): string => { +const mapYamlObjectToYamlString = (rule: Rule): string => { try { - const yamlString = dump(formState); + const yamlString = dump(rule); return yamlString; } catch (error: any) { @@ -29,25 +30,70 @@ const formToYaml = (formState: RuleEditorFormState): string => { } }; -export const YamlRuleEditor: React.FC = ({ - ruleEditorFormState, - setRuleEditorFormState, -}) => { - const [yamlEditorError, setYamlEditorError] = useState({ - yamlEditorError: null, +const mapRuleToYamlObject = (rule: Rule): any => { + const yamlObject: any = { + id: rule.id, + logsource: { product: rule.category }, + title: rule.title, + description: rule.description, + tags: rule.tags.map((tag) => tag.value), + falsepositives: rule.false_positives.map((falsePositive) => falsePositive.value), + level: rule.level, + status: rule.status, + references: rule.references.map((reference) => reference.value), + author: rule.author, + detection: load(rule.detection), + }; + + return yamlObject; +}; + +const mapYamlObjectToRule = (obj: any): Rule => { + const rule: Rule = { + id: obj.id, + category: obj.logsource.product, + log_source: '', + title: obj.title, + description: obj.description, + tags: obj.tags.map((tag: string) => ({ value: tag })), + false_positives: obj.falsepositives.map((falsePositive: string) => ({ value: falsePositive })), + level: obj.level, + status: obj.status, + references: obj.references.map((reference: string) => ({ value: reference })), + author: obj.author, + detection: dump(obj.detection), + }; + + return rule; +}; + +export const YamlRuleEditor: React.FC = ({ rule, change }) => { + const yamlObject = mapRuleToYamlObject(rule); + + const [state, setState] = useState({ + error: null, + value: mapYamlObjectToYamlString(yamlObject), }); - const onYamlRuleChange = (value: string) => { - if (value === '') { - setYamlEditorError(() => ({ yamlEditorError: 'Required Field' })); + const onChange = (value: string) => { + setState((prevState) => ({ ...prevState, value })); + }; + + const onBlur = () => { + if (!state.value) { + setState((prevState) => ({ ...prevState, error: 'Required Field' })); return; } try { - const newRuleEditorFormState = load(value); - setRuleEditorFormState(() => newRuleEditorFormState); - setYamlEditorError(() => ({ yamlEditorError: null })); + const yamlObject = load(state.value); + + const rule = mapYamlObjectToRule(yamlObject); + + console.log('onBlur rule load', yamlObject, rule); + change(rule); + setState((prevState) => ({ ...prevState, error: null })); } catch (error) { - setYamlEditorError(() => ({ yamlEditorError: 'Invalid YAML' })); + setState((prevState) => ({ ...prevState, error: 'Invalid YAML' })); console.warn('Security Analytics - Rule Eritor - Yaml load', error); } @@ -65,11 +111,17 @@ export const YamlRuleEditor: React.FC = ({ for rule structure and schema. + {state.error && ( + + {state.error} + + )} From e28a554bc39ea52a8c61e95db51d2355041b4eb9 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Tue, 6 Dec 2022 20:34:33 +0100 Subject: [PATCH 08/15] more mapping guards Signed-off-by: Aleksandar Djindjic --- models/interfaces.ts | 6 +-- .../components/RuleEditor/RuleEditor.tsx | 21 ++------- .../RuleEditor/RuleEditorFormState.model.ts | 15 +++++++ .../components/RuleEditor/YamlRuleEditor.tsx | 43 ++++++++++++++----- .../Rules/components/RuleEditor/mappers.ts | 14 ++++-- 5 files changed, 65 insertions(+), 34 deletions(-) diff --git a/models/interfaces.ts b/models/interfaces.ts index a2982c8fc..3c393422f 100644 --- a/models/interfaces.ts +++ b/models/interfaces.ts @@ -9,11 +9,11 @@ export interface Rule { log_source: string; title: string; description: string; - tags: { value: string }[]; - false_positives: { value: string }[]; + tags: Array<{ value: string }>; + false_positives: Array<{ value: string }>; level: string; status: string; - references: { value: string }[]; + references: Array<{ value: string }>; author: string; detection: string; } diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx index ca4fef832..d6a354770 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { ContentPanel } from '../../../../components/ContentPanel'; import { EuiSpacer, EuiButtonGroup } from '@elastic/eui'; import { Rule } from '../../../../../models/interfaces'; -import { RuleEditorFormState } from './RuleEditorFormState.model'; +import { RuleEditorFormState, ruleEditorStateDefaultValue } from './RuleEditorFormState.model'; import { mapFormToRule, mapRuleToForm } from './mappers'; import { VisualRuleEditor } from './VisualRuleEditor'; import { YamlRuleEditor } from './YamlRuleEditor'; @@ -24,21 +24,6 @@ export interface VisualEditorFormErrorsState { authorError: string | null; } -const newRuyleDefaultState: RuleEditorFormState = { - id: '25b9c01c-350d-4b95-bed1-836d04a4f324', - log_source: '', - logType: '', - name: '', - description: '', - status: '', - author: '', - references: [''], - tags: [], - detection: '', - level: '', - falsePositives: [''], -}; - const editorTypes = [ { id: 'visual', @@ -52,7 +37,9 @@ const editorTypes = [ export const RuleEditor: React.FC = ({ title, rule, FooterActions }) => { const [ruleEditorFormState, setRuleEditorFormState] = useState( - rule ? { ...mapRuleToForm(rule), id: newRuyleDefaultState.id } : newRuyleDefaultState + rule + ? { ...mapRuleToForm(rule), id: ruleEditorStateDefaultValue.id } + : ruleEditorStateDefaultValue ); const [selectedEditorType, setSelectedEditorType] = useState('visual'); diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorFormState.model.ts b/public/pages/Rules/components/RuleEditor/RuleEditorFormState.model.ts index 91f0ed822..ccd8dd0c4 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorFormState.model.ts +++ b/public/pages/Rules/components/RuleEditor/RuleEditorFormState.model.ts @@ -19,3 +19,18 @@ export interface RuleEditorFormState { level: string; falsePositives: string[]; } + +export const ruleEditorStateDefaultValue: RuleEditorFormState = { + id: '25b9c01c-350d-4b95-bed1-836d04a4f324', + log_source: '', + logType: '', + name: '', + description: '', + status: '', + author: '', + references: [''], + tags: [], + detection: '', + level: '', + falsePositives: [''], +}; diff --git a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx index 07ddb2d27..645d09f98 100644 --- a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx @@ -20,10 +20,15 @@ export interface YamlEditorState { } const mapYamlObjectToYamlString = (rule: Rule): string => { - try { - const yamlString = dump(rule); + console.log('mapYamlObjectToYamlString', rule); - return yamlString; + try { + if (!rule.detection) { + const { detection, ...ruleWithoutDetection } = rule; + return dump(ruleWithoutDetection); + } else { + return dump(rule); + } } catch (error: any) { console.warn('Security Analytics - Rule Eritor - Yaml dump', error); return ''; @@ -31,6 +36,15 @@ const mapYamlObjectToYamlString = (rule: Rule): string => { }; const mapRuleToYamlObject = (rule: Rule): any => { + console.log('mapRuleToYamlObject', rule); + + let detection = undefined; + if (rule.detection) { + try { + detection = load(rule.detection); + } catch {} + } + const yamlObject: any = { id: rule.id, logsource: { product: rule.category }, @@ -42,26 +56,36 @@ const mapRuleToYamlObject = (rule: Rule): any => { status: rule.status, references: rule.references.map((reference) => reference.value), author: rule.author, - detection: load(rule.detection), + detection, }; return yamlObject; }; const mapYamlObjectToRule = (obj: any): Rule => { + let detection = ''; + if (obj.detection) { + try { + detection = dump(obj.detection); + } catch {} + } const rule: Rule = { id: obj.id, - category: obj.logsource.product, + category: obj.logsource ? obj.logsource.product : undefined, log_source: '', title: obj.title, description: obj.description, - tags: obj.tags.map((tag: string) => ({ value: tag })), - false_positives: obj.falsepositives.map((falsePositive: string) => ({ value: falsePositive })), + tags: obj.tags ? obj.tags.map((tag: string) => ({ value: tag })) : undefined, + false_positives: obj.falsepositives + ? obj.falsepositives.map((falsePositive: string) => ({ value: falsePositive })) + : undefined, level: obj.level, status: obj.status, - references: obj.references.map((reference: string) => ({ value: reference })), + references: obj.references + ? obj.references.map((reference: string) => ({ value: reference })) + : undefined, author: obj.author, - detection: dump(obj.detection), + detection, }; return rule; @@ -89,7 +113,6 @@ export const YamlRuleEditor: React.FC = ({ rule, change }) const rule = mapYamlObjectToRule(yamlObject); - console.log('onBlur rule load', yamlObject, rule); change(rule); setState((prevState) => ({ ...prevState, error: null })); } catch (error) { diff --git a/public/pages/Rules/components/RuleEditor/mappers.ts b/public/pages/Rules/components/RuleEditor/mappers.ts index 4a8240b4a..7da1b4db1 100644 --- a/public/pages/Rules/components/RuleEditor/mappers.ts +++ b/public/pages/Rules/components/RuleEditor/mappers.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import { Rule } from '../../../../../models/interfaces'; -import { RuleEditorFormState } from './RuleEditorFormState.model'; +import { RuleEditorFormState, ruleEditorStateDefaultValue } from './RuleEditorFormState.model'; export const mapFormToRule = (formState: RuleEditorFormState): Rule => { return { @@ -33,10 +33,16 @@ export const mapRuleToForm = (rule: Rule): RuleEditorFormState => { description: rule.description, status: rule.status, author: rule.author, - references: rule.references.map((ref) => ref.value), - tags: rule.tags.map((tag) => ({ label: tag.value })), + references: rule.references + ? rule.references.map((ref) => ref.value) + : ruleEditorStateDefaultValue.references, + tags: rule.tags + ? rule.tags.map((tag) => ({ label: tag.value })) + : ruleEditorStateDefaultValue.tags, detection: rule.detection, level: rule.level, - falsePositives: rule.false_positives.map((falsePositive) => falsePositive.value), + falsePositives: rule.false_positives + ? rule.false_positives.map((falsePositive) => falsePositive.value) + : ruleEditorStateDefaultValue.falsePositives, }; }; From 55f3b5f2f020dea85955e96a70a2551384b2cf57 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Tue, 6 Dec 2022 20:37:20 +0100 Subject: [PATCH 09/15] remove console.log's Signed-off-by: Aleksandar Djindjic --- public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx index 645d09f98..e52cb6288 100644 --- a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.tsx @@ -20,8 +20,6 @@ export interface YamlEditorState { } const mapYamlObjectToYamlString = (rule: Rule): string => { - console.log('mapYamlObjectToYamlString', rule); - try { if (!rule.detection) { const { detection, ...ruleWithoutDetection } = rule; @@ -36,8 +34,6 @@ const mapYamlObjectToYamlString = (rule: Rule): string => { }; const mapRuleToYamlObject = (rule: Rule): any => { - console.log('mapRuleToYamlObject', rule); - let detection = undefined; if (rule.detection) { try { From c4f546b782b4223f22bc9145879c7bf2524302d1 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Thu, 8 Dec 2022 08:34:44 +0100 Subject: [PATCH 10/15] YAML editor - cypress test Signed-off-by: Aleksandar Djindjic --- cypress/integration/2_rules.spec.js | 37 ++++++++++++++++++- .../components/RuleEditor/RuleEditor.tsx | 1 + .../components/RuleEditor/YamlRuleEditor.tsx | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js index afe1b461e..21f5f271f 100644 --- a/cypress/integration/2_rules.spec.js +++ b/cypress/integration/2_rules.spec.js @@ -5,8 +5,9 @@ import { PLUGIN_NAME, TWENTY_SECONDS_TIMEOUT } from '../support/constants'; +const uniqueId = Cypress._.random(0, 1e6); const SAMPLE_RULE = { - name: 'Cypress test rule', + name: `Cypress test rule ${uniqueId}`, logType: 'windows', description: 'This is a rule used to test the rule creation workflow. Not for production use.', detection: @@ -26,6 +27,26 @@ const SAMPLE_RULE = { status: 'experimental', }; +const YAML_RULE_LINES = [ + `title: ${SAMPLE_RULE.name}`, + `description:`, + `${SAMPLE_RULE.description}`, + `level: ${SAMPLE_RULE.severity}`, + `tags:`, + `- ${SAMPLE_RULE.tags[0]}`, + `- ${SAMPLE_RULE.tags[1]}`, + `- ${SAMPLE_RULE.tags[2]}`, + `references:`, + `- '${SAMPLE_RULE.references}'`, + `falsepositives:`, + `- ${SAMPLE_RULE.falsePositive}`, + `author: ${SAMPLE_RULE.author}`, + `status: ${SAMPLE_RULE.status}`, + `logsource:`, + `product: ${SAMPLE_RULE.logType}`, + ...SAMPLE_RULE.detection.replaceAll(' ', '').replaceAll('{backspace}', '').split('\n'), +]; + describe('Rules', () => { before(() => { // Deleting pre-existing test rules @@ -93,6 +114,20 @@ describe('Rules', () => { SAMPLE_RULE.status ); + // Switch to YAML editor + cy.get( + '[data-test-subj="change-editor-type"] label:nth-child(2)', + TWENTY_SECONDS_TIMEOUT + ).click({ + force: true, + }); + + YAML_RULE_LINES.forEach((line) => + cy + .get('[data-test-subj="rule_yaml_editor"]', TWENTY_SECONDS_TIMEOUT) + .contains(line, TWENTY_SECONDS_TIMEOUT) + ); + // Click "create" button cy.get('[data-test-subj="create_rule_button"]', TWENTY_SECONDS_TIMEOUT).click({ force: true, diff --git a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx index d6a354770..76509cbfd 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditor.tsx @@ -61,6 +61,7 @@ export const RuleEditor: React.FC = ({ title, rule, FooterActio <> = ({ rule, change }) value={state.value} onChange={onChange} onBlur={onBlur} - data-test-subj={'rule_detection_field'} + data-test-subj={'rule_yaml_editor'} /> From 76f8e73c8f3e8503f9ab8c43a9f5a95cc5ec2988 Mon Sep 17 00:00:00 2001 From: Aleksandar Djindjic Date: Thu, 8 Dec 2022 19:16:32 +0100 Subject: [PATCH 11/15] yaml editor snapshot test Signed-off-by: Aleksandar Djindjic --- .../RuleEditor/YamlRuleEditor.test.tsx | 54 ++++++ .../YamlRuleEditor.test.tsx.snap | 162 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 public/pages/Rules/components/RuleEditor/YamlRuleEditor.test.tsx create mode 100644 public/pages/Rules/components/RuleEditor/__snapshots__/YamlRuleEditor.test.tsx.snap diff --git a/public/pages/Rules/components/RuleEditor/YamlRuleEditor.test.tsx b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.test.tsx new file mode 100644 index 000000000..1c7bfb84a --- /dev/null +++ b/public/pages/Rules/components/RuleEditor/YamlRuleEditor.test.tsx @@ -0,0 +1,54 @@ +/* + * 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/__snapshots__/YamlRuleEditor.test.tsx.snap b/public/pages/Rules/components/RuleEditor/__snapshots__/YamlRuleEditor.test.tsx.snap new file mode 100644 index 000000000..64902488b --- /dev/null +++ b/public/pages/Rules/components/RuleEditor/__snapshots__/YamlRuleEditor.test.tsx.snap @@ -0,0 +1,162 @@ +// 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. +
+
+
+
+ +
+