/* * 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, EuiAccordion, EuiComboBox, EuiButtonGroup, EuiText, EuiTitle, EuiPanel, EuiIcon, } from '@elastic/eui'; import { ContentPanel } from '../../../../components/ContentPanel'; import { FieldTextArray } from './components/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 { DetectionVisualEditor } from './DetectionVisualEditor'; import { useCallback } from 'react'; import { DataStore } from '../../../../store/DataStore'; export interface VisualRuleEditorProps { initialValue: RuleEditorFormModel; notifications?: NotificationsStart; submit: (values: RuleEditorFormModel) => void; cancel: () => void; mode: 'create' | 'edit'; title: string | JSX.Element; } const editorTypes = [ { id: 'visual', label: 'Visual Editor', }, { id: 'yaml', label: 'YAML Editor', }, ]; export const TAGS_PREFIX = 'attack.'; export const RuleEditorForm: React.FC = ({ initialValue, notifications, submit, cancel, mode, title, }) => { const [selectedEditorType, setSelectedEditorType] = useState('visual'); const [isDetectionInvalid, setIsDetectionInvalid] = useState(false); const [logTypeOptions, setLogTypeOptions] = useState( ruleTypes.map(({ value, label }) => ({ value, label })) ); const onEditorTypeChange = (optionId: string) => { setSelectedEditorType(optionId); }; const refreshLogTypeOptions = useCallback(async () => { const logTypes = await DataStore.logTypes.getLogTypes(); setLogTypeOptions(logTypes.map(({ id, name }) => ({ value: id, label: name }))); }, []); const validateTags = (fields: string[]) => { let isValid = true; let tag; for (let i = 0; i < fields.length; i++) { tag = fields[i]; if (tag.length && !(tag.startsWith(TAGS_PREFIX) && tag.length > TAGS_PREFIX.length)) { isValid = false; break; } } return isValid; }; 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 = 'Description should only consist of upper and lowercase letters, numbers 0-9, commas, hyphens, periods, spaces, and underscores. Max limit of 500 characters.'; } 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'; } if (!validateTags(values.tags)) { errors.tags = `Tags must start with '${TAGS_PREFIX}'`; } return errors; }} onSubmit={(values, { setSubmitting }) => { if (isDetectionInvalid) { return; } 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 overview

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} /> Description - optional } isInvalid={!!props.errors?.description} error={props.errors.description} > { props.handleChange('description')(e.target.value); }} onBlur={props.handleBlur('description')} value={props.values.description} placeholder={'Detects ...'} /> Author } helpText="Combine multiple authors separated with a comma" isInvalid={props.touched.author && !!props.errors?.author} error={props.errors.author} > { props.handleChange('author')(e); }} onBlur={props.handleBlur('author')} value={props.values.author} />

Details

Log type } isInvalid={props.touched.logType && !!props.errors?.logType} error={props.errors.logType} > { props.handleChange('logType')(e[0]?.label ? e[0].label : ''); }} onFocus={refreshLogTypeOptions} onBlur={props.handleBlur('logType')} selectedOptions={ props.values.logType ? [{ value: props.values.logType, label: props.values.logType }] : [] } /> Manage Rule level (severity) } 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 }] : [] } /> 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 }] : [] } />

Detection

Define the detection criteria for the rule

{ if (isInvalid) { props.errors.detection = 'Invalid detection entries'; } else { delete props.errors.detection; } setIsDetectionInvalid(isInvalid); }} onChange={(detection: string) => { props.handleChange('detection')(detection); }} /> Additional details - optional } >
Tags - optional Tag } addButtonName="Add tag" fields={props.values.tags} error={props.errors.tags} isInvalid={props.touched.tags && !!props.errors.tags} onChange={(tags) => { props.touched.tags = true; props.setFieldValue('tags', tags); }} data-test-subj={'rule_tags_field'} /> References - optional URL } addButtonName="Add URL" fields={props.values.references} error={props.errors.references} isInvalid={props.touched.references && !!props.errors.references} onChange={(references) => { props.touched.references = true; props.setFieldValue('references', references); }} data-test-subj={'rule_references_field'} /> False positive cases - optional Description } addButtonName="Add false positive" fields={props.values.falsePositives} error={props.errors.falsePositives} isInvalid={props.touched.falsePositives && !!props.errors.falsePositives} onChange={(falsePositives) => { props.touched.falsePositives = true; props.setFieldValue('falsePositives', falsePositives); }} data-test-subj={'rule_falsePositives_field'} />
)}
Cancel props.handleSubmit()} data-test-subj={'submit_rule_form_button'} fill > {mode === 'create' ? 'Create detection rule' : 'Save changes'} )}
); };