From 137352387d6da290d0ad472cf18dac3ada725838 Mon Sep 17 00:00:00 2001 From: Jovan Cvetkovic Date: Tue, 30 May 2023 21:30:35 +0200 Subject: [PATCH] Update selection panel component for the "Create detection rule" (#594) * [FEATURE] Update selection panel component for the "Create detection rule" page #587 Signed-off-by: Jovan Cvetkovic * [FEATURE] Update selection panel component for the "Create detection rule" page #587 Signed-off-by: Jovan Cvetkovic * [FEATURE] Update selection panel component for the "Create detection rule" page #587 Signed-off-by: Jovan Cvetkovic * [FEATURE] Update selection panel component for the "Create detection rule" page #587 Signed-off-by: Jovan Cvetkovic --------- Signed-off-by: Jovan Cvetkovic --- cypress/integration/2_rules.spec.js | 11 +- public/app.scss | 1 + .../RuleEditor/DetectionVisualEditor.scss | 51 +++ .../RuleEditor/DetectionVisualEditor.tsx | 352 ++++++++++-------- .../components/RuleEditor/RuleEditorForm.tsx | 2 +- .../RuleTagsComboBox.tsx | 24 +- .../containers/CreateRule/CreateRule.tsx | 7 +- 7 files changed, 256 insertions(+), 192 deletions(-) create mode 100644 public/pages/Rules/components/RuleEditor/DetectionVisualEditor.scss diff --git a/cypress/integration/2_rules.spec.js b/cypress/integration/2_rules.spec.js index 4adae0242..84a919338 100644 --- a/cypress/integration/2_rules.spec.js +++ b/cypress/integration/2_rules.spec.js @@ -10,11 +10,9 @@ const SAMPLE_RULE = { name: `Cypress test rule ${uniqueId}`, logType: 'windows', description: 'This is a rule used to test the rule creation workflow.', - detection: - "condition: selection\nselection:\nProvider_Name|contains:\n- Service Control Manager\nEventID|contains:\n- '7045'\nServiceName|contains:\n- ZzNetSvc", detectionLine: [ - 'condition: selection', - 'selection:', + 'condition: Selection_1', + 'Selection_1:', 'Provider_Name|contains:', '- Service Control Manager', 'EventID|contains:', @@ -48,7 +46,7 @@ const YAML_RULE_LINES = [ `- '${SAMPLE_RULE.references}'`, `author: ${SAMPLE_RULE.author}`, `detection:`, - ...SAMPLE_RULE.detection.replaceAll(' ', '').replaceAll('{backspace}', '').split('\n'), + ...SAMPLE_RULE.detectionLine, ]; const checkRulesFlyout = () => { @@ -184,7 +182,6 @@ describe('Rules', () => { cy.get('[data-test-subj="rule_author_field"]').type(`${SAMPLE_RULE.author}{enter}`); cy.get('[data-test-subj="detection-visual-editor-0"]').within(() => { - cy.getFieldByLabel('Name').type('selection'); cy.getFieldByLabel('Key').type('Provider_Name'); cy.getInputByPlaceholder('Value').type('Service Control Manager'); @@ -200,7 +197,7 @@ describe('Rules', () => { cy.getInputByPlaceholder('Value').type('ZzNetSvc'); }); }); - cy.get('[data-test-subj="rule_detection_field"] textarea').type('selection', { + cy.get('[data-test-subj="rule_detection_field"] textarea').type('Selection_1', { force: true, }); diff --git a/public/app.scss b/public/app.scss index 01c242a5d..c879d2fbc 100644 --- a/public/app.scss +++ b/public/app.scss @@ -18,6 +18,7 @@ $euiTextColor: $euiColorDarkestShade !default; @import "./pages/Correlations/Correlations.scss"; @import "./pages/Correlations/components/FindingCard.scss"; @import "./pages/Findings/components/CorrelationsTable/CorrelationsTable.scss"; +@import "./pages/Rules/components/RuleEditor/DetectionVisualEditor.scss"; .selected-radio-panel { background-color: tintOrShade($euiColorPrimary, 90%, 70%); diff --git a/public/pages/Rules/components/RuleEditor/DetectionVisualEditor.scss b/public/pages/Rules/components/RuleEditor/DetectionVisualEditor.scss new file mode 100644 index 000000000..cecedbf9e --- /dev/null +++ b/public/pages/Rules/components/RuleEditor/DetectionVisualEditor.scss @@ -0,0 +1,51 @@ + +.detection-visual-editor { + .euiAccordionForm:nth-of-type(1) { + border-top: 1px solid #D3DAE6; + } + + .euiAccordionForm { + border-top: 0 !important; + } + + .detection-visual-editor-accordion-wrapper { + width: 100%; + .detection-visual-editor-form-row { + max-width: 100%; + .detection-visual-editor-textarea { + max-width: 100%; + padding: 0; + min-height: 100px; + } + } + + .detection-visual-editor-textarea-clear-btn { + align-items: flex-end; + } + + .detection-visual-editor-accordion { + .euiAccordion__childWrapper { + height: auto !important; + } + } + } + + .detection-visual-editor-name { + box-shadow: none; + background-color: transparent; + padding: 0; + } + + .detection-visual-editor-delete-selection { + margin-top: 0 !important; + } + + .euiButtonIcon--danger { + color: $ouiTextSubduedColor !important; + + &:hover { + color: $ouiColorDanger !important; + background-color: transparent !important; + } + } +} diff --git a/public/pages/Rules/components/RuleEditor/DetectionVisualEditor.tsx b/public/pages/Rules/components/RuleEditor/DetectionVisualEditor.tsx index 644b845de..f3b21f420 100644 --- a/public/pages/Rules/components/RuleEditor/DetectionVisualEditor.tsx +++ b/public/pages/Rules/components/RuleEditor/DetectionVisualEditor.tsx @@ -29,6 +29,7 @@ import { EuiModalFooter, EuiFilePicker, EuiCodeEditor, + EuiButtonEmpty, } from '@elastic/eui'; import _ from 'lodash'; import { validateCondition, validateDetectionFieldName } from '../../../../utils/validation'; @@ -275,6 +276,10 @@ export class DetectionVisualEditor extends React.Component< const { errors } = this.state; const selection = selections[selectionIdx]; + if (!selection.name) { + selection.name = `Selection_${selectionIdx + 1}`; + } + delete errors.fields['name']; if (!selection.name) { errors.fields['name'] = 'Selection name is required'; @@ -426,23 +431,38 @@ export class DetectionVisualEditor extends React.Component< } = this.state; return ( - + {selections.map((selection, selectionIdx) => { return ( -
+
- -

{selection.name || `Selection_${selectionIdx + 1}`}

-
+ + this.updateSelection(selectionIdx, { name: e.target.value })} + onBlur={(e) => this.updateSelection(selectionIdx, { name: e.target.value })} + value={selection.name || `Selection_${selectionIdx + 1}`} + /> +

Define the search identifier in your data the rule will be applied to.

- + {selections.length > 1 && ( { @@ -466,184 +486,195 @@ export class DetectionVisualEditor extends React.Component< - Name} - > - this.updateSelection(selectionIdx, { name: e.target.value })} - onBlur={(e) => this.updateSelection(selectionIdx, { name: e.target.value })} - value={selection.name} - /> - - - - {selection.data.map((datum, idx) => { const radioGroupOptions = this.createRadioGroupOptions(selectionIdx, idx); const fieldName = `field_${selectionIdx}_${idx}`; const valueId = `value_${selectionIdx}_${idx}`; return ( - 1 ? ( - - { - const newData = [...selection.data]; - newData.splice(idx, 1); - this.updateSelection(selectionIdx, { data: newData }); - }} - /> - - ) : null - } - style={{ maxWidth: '500px' }} - > - - - - - Key} - > - + Map {idx + 1}} + extraAction={ + selection.data.length > 1 ? ( + + { + const newData = [...selection.data]; + newData.splice(idx, 1); + this.updateSelection(selectionIdx, { data: newData }); + }} + /> + + ) : null + } + style={{ maxWidth: '70%' }} + > + + + - this.updateDatumInState(selectionIdx, idx, { - field: e.target.value, - }) - } - onBlur={(e) => - this.updateDatumInState(selectionIdx, idx, { - field: e.target.value, - }) - } - value={datum.field} - /> - - - - Modifier}> - { - this.updateDatumInState(selectionIdx, idx, { - modifier: e[0].value, - }); - }} - onBlur={(e) => {}} - selectedOptions={ - datum.modifier - ? [{ value: datum.modifier, label: datum.modifier }] - : [detectionModifierOptions[0]] - } - /> - - - - - - { - this.updateDatumInState(selectionIdx, idx, { - selectedRadioId: id as SelectionMapValueRadioId, - }); - }} - /> - - - {datum.selectedRadioId?.includes('list') ? ( - <> - { - this.setState({ - fileUploadModalState: { - selectionIdx, - dataIdx: idx, - }, - }); - }} - > - Upload file - - + error={errors.fields[fieldName]} + label={Key} + > + + this.updateDatumInState(selectionIdx, idx, { + field: e.target.value, + }) + } + onBlur={(e) => + this.updateDatumInState(selectionIdx, idx, { + field: e.target.value, + }) + } + value={datum.field} + /> + + + + Modifier}> + { + this.updateDatumInState(selectionIdx, idx, { + modifier: e[0].value, + }); + }} + onBlur={(e) => {}} + selectedOptions={ + datum.modifier + ? [{ value: datum.modifier, label: datum.modifier }] + : [detectionModifierOptions[0]] + } + /> + + + + + + + { + this.updateDatumInState(selectionIdx, idx, { + selectedRadioId: id as SelectionMapValueRadioId, + }); + }} + /> + + + + {datum.selectedRadioId?.includes('list') ? ( + <> + + + { + this.setState({ + fileUploadModalState: { + selectionIdx, + dataIdx: idx, + }, + }); + }} + > + Upload file + + + + + { + this.updateDatumInState(selectionIdx, idx, { + values: [], + }); + }} + > + Clear list + + + + + + { + const values = e.target.value.split('\n'); + this.updateDatumInState(selectionIdx, idx, { + values, + }); + }} + onBlur={(e) => { + const values = e.target.value.split('\n'); + this.updateDatumInState(selectionIdx, idx, { + values, + }); + }} + value={datum.values.join('\n')} + compressed={true} + isInvalid={errors.touched[valueId] && !!errors.fields[valueId]} + /> + + + ) : ( - { - const values = e.target.value.split('\n'); - console.log(values); this.updateDatumInState(selectionIdx, idx, { - values, + values: [e.target.value, ...datum.values.slice(1)], }); }} onBlur={(e) => { - const values = e.target.value.split('\n'); - console.log(values); this.updateDatumInState(selectionIdx, idx, { - values, + values: [e.target.value, ...datum.values.slice(1)], }); }} - value={datum.values.join('\n')} - compressed={true} - isInvalid={errors.touched[valueId] && !!errors.fields[valueId]} + value={datum.values[0]} /> - - ) : ( - - { - this.updateDatumInState(selectionIdx, idx, { - values: [e.target.value, ...datum.values.slice(1)], - }); - }} - onBlur={(e) => { - this.updateDatumInState(selectionIdx, idx, { - values: [e.target.value, ...datum.values.slice(1)], - }); - }} - value={datum.values[0]} - /> - - )} - - - - + )} + + + +
); })} + + { const newData = [ @@ -674,6 +705,7 @@ export class DetectionVisualEditor extends React.Component< ...selections, { ...defaultDetectionObj.selections[0], + name: `Selection_${selections.length + 1}`, }, ], }, diff --git a/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx index 2c611865c..2ec6a6c72 100644 --- a/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx +++ b/public/pages/Rules/components/RuleEditor/RuleEditorForm.tsx @@ -400,7 +400,7 @@ export const RuleEditorForm: React.FC = ({ selectedOptions={ props.values.status ? [{ value: props.values.status, label: props.values.status }] - : [] + : [{ value: ruleStatus[0], label: ruleStatus[0] }] } /> diff --git a/public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/RuleTagsComboBox.tsx b/public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/RuleTagsComboBox.tsx index d0e1e207c..ad0a9570e 100644 --- a/public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/RuleTagsComboBox.tsx +++ b/public/pages/Rules/components/RuleEditor/components/YamlRuleEditorComponent/RuleTagsComboBox.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import React from 'react'; import { EuiFormRow, EuiText, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; export interface RuleTagsComboBoxProps { @@ -16,25 +16,12 @@ export interface RuleTagsComboBoxProps { selectedOptions: EuiComboBoxOptionOption[]; } -const STARTS_WITH = 'attack.'; - -const isValid = (value: string) => { - if (value === '') return true; - return value.startsWith(STARTS_WITH) && value.length > STARTS_WITH.length; -}; - export const RuleTagsComboBox: React.FC = ({ onCreateOption, onBlur, onChange, selectedOptions, }) => { - const [isCurrentlyTypingValueInvalid, setIsCurrentlyTypingValueInvalid] = useState(false); - - const onSearchChange = (searchValue: string) => { - setIsCurrentlyTypingValueInvalid(!isValid(searchValue)); - }; - return ( <> = ({ - optional } - isInvalid={isCurrentlyTypingValueInvalid} - error={isCurrentlyTypingValueInvalid ? 'Invalid tag' : ''} - helpText={`Tags must start with '${STARTS_WITH}'`} > - isValid(searchValue) && onCreateOption(searchValue, options) - } + onCreateOption={(searchValue, options) => onCreateOption(searchValue, options)} onBlur={onBlur} data-test-subj={'rule_tags_dropdown'} selectedOptions={selectedOptions} - isInvalid={isCurrentlyTypingValueInvalid} /> diff --git a/public/pages/Rules/containers/CreateRule/CreateRule.tsx b/public/pages/Rules/containers/CreateRule/CreateRule.tsx index c44fd2504..118b15b5b 100644 --- a/public/pages/Rules/containers/CreateRule/CreateRule.tsx +++ b/public/pages/Rules/containers/CreateRule/CreateRule.tsx @@ -5,7 +5,7 @@ import { BrowserServices } from '../../../../models/interfaces'; import { RuleEditorContainer } from '../../components/RuleEditor/RuleEditorContainer'; -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { BREADCRUMBS } from '../../../../utils/constants'; import { CoreServicesContext } from '../../../../components/core_services'; @@ -20,7 +20,10 @@ export interface CreateRuleProps { export const CreateRule: React.FC = ({ history, services, notifications }) => { const context = useContext(CoreServicesContext); - setBreadCrumb(BREADCRUMBS.RULES_CREATE, context?.chrome.setBreadcrumbs); + + useEffect(() => { + setBreadCrumb(BREADCRUMBS.RULES_CREATE, context?.chrome.setBreadcrumbs); + }); return (