diff --git a/packages/kbn-securitysolution-autocomplete/index.ts b/packages/kbn-securitysolution-autocomplete/index.ts index fef857773701c..e47113719176f 100644 --- a/packages/kbn-securitysolution-autocomplete/index.ts +++ b/packages/kbn-securitysolution-autocomplete/index.ts @@ -8,7 +8,7 @@ */ export * from './src/check_empty_value'; -export * from './src/field'; +export * from './src/es_field_selector'; export * from './src/field_value_exists'; export * from './src/field_value_lists'; export * from './src/field_value_match'; diff --git a/packages/kbn-securitysolution-autocomplete/src/field/__tests__/__snapshots__/index.test.tsx.snap b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/__tests__/__snapshots__/index.test.tsx.snap similarity index 100% rename from packages/kbn-securitysolution-autocomplete/src/field/__tests__/__snapshots__/index.test.tsx.snap rename to packages/kbn-securitysolution-autocomplete/src/es_field_selector/__tests__/__snapshots__/index.test.tsx.snap diff --git a/packages/kbn-securitysolution-autocomplete/src/field/__tests__/disabled_types_with_tooltip_text.test.ts b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/__tests__/disabled_types_with_tooltip_text.test.ts similarity index 100% rename from packages/kbn-securitysolution-autocomplete/src/field/__tests__/disabled_types_with_tooltip_text.test.ts rename to packages/kbn-securitysolution-autocomplete/src/es_field_selector/__tests__/disabled_types_with_tooltip_text.test.ts diff --git a/packages/kbn-securitysolution-autocomplete/src/field/__tests__/index.test.tsx b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/__tests__/index.test.tsx similarity index 96% rename from packages/kbn-securitysolution-autocomplete/src/field/__tests__/index.test.tsx rename to packages/kbn-securitysolution-autocomplete/src/es_field_selector/__tests__/index.test.tsx index b795abc5842f2..1b33b4b294644 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field/__tests__/index.test.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/__tests__/index.test.tsx @@ -11,13 +11,13 @@ import React from 'react'; import { fireEvent, render, waitFor, within } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { FieldComponent } from '..'; +import { EsFieldSelector } from '..'; import { fields, getField } from '../../fields/index.mock'; describe('FieldComponent', () => { it('should render the component enabled and displays the selected field correctly', () => { const wrapper = render( - { }); it('should render the component disabled if isDisabled is true', () => { const wrapper = render( - { }); it('should render the loading spinner if isLoading is true when clicked', () => { const wrapper = render( - { }); it('should allow user to clear values if isClearable is true', () => { const wrapper = render( - { }); it('should change the selected value', async () => { const wrapper = render( - { it('it allows custom user input if "acceptsCustomOptions" is "true"', async () => { const mockOnChange = jest.fn(); const wrapper = render( - ({ BINARY_TYPE_NOT_SUPPORTED: 'Binary fields are currently unsupported', @@ -33,7 +33,7 @@ describe('useField', () => { describe('comboOptions and selectedComboOptions', () => { it('should return default values', () => { - const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock })); + const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock })); const { isInvalid, comboOptions, selectedComboOptions, fieldWidth } = result.current; expect(isInvalid).toBeFalsy(); expect(comboOptions.length).toEqual(30); @@ -79,7 +79,7 @@ describe('useField', () => { }; const { result } = renderHook(() => - useField({ indexPattern: newIndexPattern, onChange: onChangeMock }) + useEsField({ indexPattern: newIndexPattern, onChange: onChangeMock }) ); const { comboOptions, selectedComboOptions } = result.current; expect(comboOptions).toEqual([{ label: 'bytes' }, { label: 'ssl' }, { label: '@timestamp' }]); @@ -124,7 +124,7 @@ describe('useField', () => { }; const { result } = renderHook(() => - useField({ + useEsField({ indexPattern: newIndexPattern, onChange: onChangeMock, selectedField: { name: '', type: 'keyword' }, @@ -173,7 +173,7 @@ describe('useField', () => { }; const { result } = renderHook(() => - useField({ + useEsField({ indexPattern: newIndexPattern, onChange: onChangeMock, selectedField: { name: ' ', type: 'keyword' }, @@ -222,7 +222,7 @@ describe('useField', () => { }; const { result } = renderHook(() => - useField({ indexPattern: newIndexPattern, onChange: onChangeMock, selectedField }) + useEsField({ indexPattern: newIndexPattern, onChange: onChangeMock, selectedField }) ); const { comboOptions, selectedComboOptions } = result.current; expect(comboOptions).toEqual([{ label: 'bytes' }, { label: 'ssl' }, { label: '@timestamp' }]); @@ -273,7 +273,7 @@ describe('useField', () => { readFromDocValues: true, }, ] as unknown as DataViewFieldBase[]; - const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock })); + const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock })); const { comboOptions, renderFields } = result.current; expect(comboOptions).toEqual([ { label: 'blob' }, @@ -328,7 +328,7 @@ describe('useField', () => { readFromDocValues: true, }, ] as unknown as DataViewFieldBase[]; - const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock })); + const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock })); const { comboOptions, renderFields } = result.current; expect(comboOptions).toEqual([ { label: 'blob' }, @@ -374,7 +374,7 @@ describe('useField', () => { readFromDocValues: true, }, ] as unknown as DataViewFieldBase[]; - const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock })); + const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock })); const { comboOptions, renderFields } = result.current; expect(comboOptions).toEqual([{ label: 'bytes' }, { label: 'ssl' }, { label: '@timestamp' }]); act(() => { @@ -389,7 +389,7 @@ describe('useField', () => { jest.resetModules(); }); it('should invoke onChange with one value if one option is sent', () => { - const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock })); + const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock })); act(() => { result.current.handleValuesChange([ { @@ -411,7 +411,7 @@ describe('useField', () => { }); }); it('should invoke onChange with array value if more than an option', () => { - const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock })); + const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock })); act(() => { result.current.handleValuesChange([ { @@ -446,7 +446,7 @@ describe('useField', () => { }); }); it('should invoke onChange with custom option if one is sent', () => { - const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock })); + const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock })); act(() => { result.current.handleCreateCustomOption('madeUpField'); expect(onChangeMock).toHaveBeenCalledWith([ @@ -462,13 +462,13 @@ describe('useField', () => { describe('fieldWidth', () => { it('should return object has width prop', () => { const { result } = renderHook(() => - useField({ indexPattern, onChange: onChangeMock, fieldInputWidth: 100 }) + useEsField({ indexPattern, onChange: onChangeMock, fieldInputWidth: 100 }) ); expect(result.current.fieldWidth).toEqual({ width: '100px' }); }); it('should return empty object', () => { const { result } = renderHook(() => - useField({ indexPattern, onChange: onChangeMock, fieldInputWidth: 0 }) + useEsField({ indexPattern, onChange: onChangeMock, fieldInputWidth: 0 }) ); expect(result.current.fieldWidth).toEqual({}); }); @@ -477,7 +477,7 @@ describe('useField', () => { describe('isInvalid with handleTouch', () => { it('should return isInvalid equals true when calling with no selectedField and isRequired is true', () => { const { result } = renderHook(() => - useField({ indexPattern, onChange: onChangeMock, isRequired: true }) + useEsField({ indexPattern, onChange: onChangeMock, isRequired: true }) ); actTestRenderer(() => { @@ -487,7 +487,7 @@ describe('useField', () => { }); it('should return isInvalid equals false with selectedField and isRequired is true', () => { const { result } = renderHook(() => - useField({ indexPattern, onChange: onChangeMock, isRequired: true, selectedField }) + useEsField({ indexPattern, onChange: onChangeMock, isRequired: true, selectedField }) ); actTestRenderer(() => { @@ -496,7 +496,7 @@ describe('useField', () => { expect(result.current.isInvalid).toBeFalsy(); }); it('should return isInvalid equals false when isRequired is false', () => { - const { result } = renderHook(() => useField({ indexPattern, onChange: onChangeMock })); + const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock })); actTestRenderer(() => { result.current.handleTouch(); diff --git a/packages/kbn-securitysolution-autocomplete/src/field/disabled_types_with_tooltip_text.ts b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/disabled_types_with_tooltip_text.ts similarity index 100% rename from packages/kbn-securitysolution-autocomplete/src/field/disabled_types_with_tooltip_text.ts rename to packages/kbn-securitysolution-autocomplete/src/es_field_selector/disabled_types_with_tooltip_text.ts diff --git a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/index.tsx similarity index 85% rename from packages/kbn-securitysolution-autocomplete/src/field/index.tsx rename to packages/kbn-securitysolution-autocomplete/src/es_field_selector/index.tsx index 7c830264af302..31efaa23b62df 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/index.tsx @@ -11,12 +11,22 @@ import React from 'react'; import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldProps } from './types'; -import { useField } from './use_field'; +import { FieldBaseProps } from './types'; +import { useEsField } from './use_es_field'; const AS_PLAIN_TEXT = { asPlainText: true }; -export const FieldComponent: React.FC = ({ +interface EsFieldSelectorProps extends FieldBaseProps { + isClearable?: boolean; + isDisabled?: boolean; + isLoading?: boolean; + placeholder: string; + acceptsCustomOptions?: boolean; + showMappingConflicts?: boolean; + 'aria-label'?: string; +} + +export function EsFieldSelector({ fieldInputWidth, fieldTypeFilter = [], indexPattern, @@ -30,18 +40,17 @@ export const FieldComponent: React.FC = ({ acceptsCustomOptions = false, showMappingConflicts = false, 'aria-label': ariaLabel, -}): JSX.Element => { +}: EsFieldSelectorProps): JSX.Element { const { isInvalid, comboOptions, selectedComboOptions, fieldWidth, - renderFields, handleTouch, handleValuesChange, handleCreateCustomOption, - } = useField({ + } = useEsField({ indexPattern, fieldTypeFilter, isRequired, @@ -97,6 +106,4 @@ export const FieldComponent: React.FC = ({ aria-label={ariaLabel} /> ); -}; - -FieldComponent.displayName = 'Field'; +} diff --git a/packages/kbn-securitysolution-autocomplete/src/field/types.ts b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/types.ts similarity index 85% rename from packages/kbn-securitysolution-autocomplete/src/field/types.ts rename to packages/kbn-securitysolution-autocomplete/src/es_field_selector/types.ts index 26e0eb9697705..b0f1ab56e8079 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field/types.ts +++ b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/types.ts @@ -11,15 +11,6 @@ import { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; import { FieldConflictsInfo } from '@kbn/securitysolution-list-utils'; import { GetGenericComboBoxPropsReturn } from '../get_generic_combo_box_props'; -export interface FieldProps extends FieldBaseProps { - isClearable: boolean; - isDisabled: boolean; - isLoading: boolean; - placeholder: string; - acceptsCustomOptions?: boolean; - showMappingConflicts?: boolean; - 'aria-label'?: string; -} export interface FieldBaseProps { indexPattern: DataViewBase | undefined; fieldTypeFilter?: string[]; diff --git a/packages/kbn-securitysolution-autocomplete/src/field/use_field.tsx b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/use_es_field.tsx similarity index 99% rename from packages/kbn-securitysolution-autocomplete/src/field/use_field.tsx rename to packages/kbn-securitysolution-autocomplete/src/es_field_selector/use_es_field.tsx index 7a0b7eb5b00af..615571d989607 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field/use_field.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/es_field_selector/use_es_field.tsx @@ -115,7 +115,7 @@ const getComboBoxProps = (fields: ComboBoxFields): GetFieldComboBoxPropsReturn = }; }; -export const useField = ({ +export const useEsField = ({ indexPattern, fieldTypeFilter, isRequired, diff --git a/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx index 6a831376b3cbf..5b627451db191 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field_value_match/index.tsx @@ -48,9 +48,9 @@ interface AutocompleteFieldMatchProps { selectedField: DataViewFieldBase | undefined; selectedValue: string | undefined; indexPattern: DataViewBase | undefined; - isLoading: boolean; - isDisabled: boolean; - isClearable: boolean; + isLoading?: boolean; + isDisabled?: boolean; + isClearable?: boolean; isRequired?: boolean; fieldInputWidth?: number; rowLabel?: string; @@ -68,7 +68,7 @@ export const AutocompleteFieldMatchComponent: React.FC = ({ (isFirst: boolean): JSX.Element => { const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry); const comboBox = ( - = ({ const renderFieldInput = useMemo(() => { const comboBox = ( - = ({ const renderThreatFieldInput = useMemo(() => { const comboBox = ( - > | undefined { const [{ value, path, form }] = args; - const formData = form.getFormData(); - const parentFieldData: RequiredFieldInput[] = formData[parentFieldPath]; + const allRequiredFields = getAllRequiredFieldsValues(form, parentFieldPath); const isFieldNameUsedMoreThanOnce = - parentFieldData.filter((field) => field.name === value.name).length > 1; + allRequiredFields.filter((field) => field.name === value.name).length > 1; if (isFieldNameUsedMoreThanOnce) { return { @@ -51,3 +51,20 @@ export function makeValidateRequiredField(parentFieldPath: string) { } }; } + +function getAllRequiredFieldsValues( + form: { getFields: FormHook['getFields'] }, + parentFieldPath: string +) { + /* + Getting values for required fields via flattened fields instead of using `getFormData`. + This is because `getFormData` applies a serializer function to field values, which might update values. + Using flattened fields allows us to get the original values before the serializer function is applied. + */ + const flattenedFieldNames = getFlattenedArrayFieldNames(form, parentFieldPath); + const fields = form.getFields(); + + return flattenedFieldNames.map( + (fieldName) => fields[fieldName]?.value ?? {} + ) as RequiredFieldInput[]; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx index f53c41ce98d00..f2f4c08a35813 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields.tsx @@ -16,6 +16,7 @@ import { RequiredFieldsHelpInfo } from './required_fields_help_info'; import { RequiredFieldRow } from './required_fields_row'; import * as defineRuleI18n from '../../../rule_creation_ui/components/step_define_rule/translations'; import * as i18n from './translations'; +import { getFlattenedArrayFieldNames } from './utils'; interface RequiredFieldsComponentProps { path: string; @@ -65,7 +66,7 @@ const RequiredFieldsList = ({ form, }: RequiredFieldsListProps) => { /* - This component should only re-render when either the "index" form field (index patterns) or the required fields change. + This component should only re-render when either the "index" form field (index patterns) or the required fields change. By default, the `useFormData` hook triggers a re-render whenever any form field changes. It also allows optimization by passing a "watch" array of field names. The component then only re-renders when these specified fields change. @@ -77,10 +78,7 @@ const RequiredFieldsList = ({ To work around this, we manually construct a list of "flattened" field names to watch, based on the current state of the form. This is a temporary solution and ideally, `useFormData` should be updated to handle this scenario. */ - - const internalField = form.getFields()[`${path}__array__`] ?? {}; - const internalFieldValue = (internalField?.value ?? []) as ArrayItem[]; - const flattenedFieldNames = internalFieldValue.map((item) => item.path); + const flattenedFieldNames = getFlattenedArrayFieldNames(form, path); /* Not using "watch" for the initial render, to let row components render and initialize form fields. diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields_row.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields_row.tsx index 755f1de413760..39b08ac17e49f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields_row.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/required_fields_row.tsx @@ -44,14 +44,6 @@ export const RequiredFieldRow = ({ const rowFieldConfig: FieldConfig = useMemo( () => ({ - deserializer: (value) => { - const rowValueWithoutEcs: RequiredFieldInput = { - name: value.name, - type: value.type, - }; - - return rowValueWithoutEcs; - }, validations: [{ validator: makeValidateRequiredField(parentFieldPath) }], defaultValue: { name: '', type: '' }, }), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/utils.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/utils.ts index 55beca264e120..38820e992fa69 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/utils.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation/components/required_fields/utils.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { FormHook, ArrayItem } from '../../../../shared_imports'; + interface PickTypeForNameParameters { name: string; type: string; @@ -26,3 +28,26 @@ export function pickTypeForName({ name, type, typesByFieldName = {} }: PickTypeF */ return typesAvailableForName[0] ?? type; } + +/** + * Returns a list of flattened field names for a given array field of a form. + * Flattened field name is a string that represents the path to an item in an array field. + * For example, a field "myArrayField" can be represented as "myArrayField[0]", "myArrayField[1]", etc. + * + * Flattened field names are useful: + * - when you need to subscribe to changes in an array field using `useFormData` "watch" option + * - when you need to retrieve form data before serializer function is applied + * + * @param {Object} form - Form object. + * @param {string} arrayFieldName - Path to the array field. + * @returns {string[]} - Flattened array field names. + */ +export function getFlattenedArrayFieldNames( + form: { getFields: FormHook['getFields'] }, + arrayFieldName: string +): string[] { + const internalField = form.getFields()[`${arrayFieldName}__array__`] ?? {}; + const internalFieldValue = (internalField?.value ?? []) as ArrayItem[]; + + return internalFieldValue.map((item) => item.path); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/es_field_selector_field/index.tsx similarity index 70% rename from x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/autocomplete_field/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/es_field_selector_field/index.tsx index 37c19f5724ec8..03d0a3021dc33 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/autocomplete_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/es_field_selector_field/index.tsx @@ -7,13 +7,13 @@ import React, { useCallback, useMemo } from 'react'; import { EuiFormRow } from '@elastic/eui'; -import { FieldComponent } from '@kbn/securitysolution-autocomplete'; +import { EsFieldSelector } from '@kbn/securitysolution-autocomplete'; import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -interface AutocompleteFieldProps { +interface EsFieldSelectorFieldProps { dataTestSubj: string; - field: FieldHook; + field: FieldHook; idAria: string; indices: DataViewBase; isDisabled: boolean; @@ -21,7 +21,7 @@ interface AutocompleteFieldProps { placeholder?: string; } -export const AutocompleteField = ({ +export const EsFieldSelectorField = ({ dataTestSubj, field, idAria, @@ -29,35 +29,37 @@ export const AutocompleteField = ({ isDisabled, fieldType, placeholder, -}: AutocompleteFieldProps) => { +}: EsFieldSelectorFieldProps) => { + const fieldTypeFilter = useMemo(() => [fieldType], [fieldType]); + const handleFieldChange = useCallback( ([newField]: DataViewFieldBase[]): void => { - // TODO: Update onChange type in FieldComponent as newField can be undefined field.setValue(newField?.name ?? ''); }, [field] ); - const selectedField = useMemo(() => { - const existingField = (field.value as string) ?? ''; - const [newSelectedField] = indices.fields.filter( - ({ name }) => existingField != null && existingField === name - ); - return newSelectedField; - }, [field.value, indices]); + const selectedField = useMemo( + () => + indices.fields.find(({ name }) => field.value === name) ?? { + name: field.value, + type: fieldType, + }, + [field.value, indices, fieldType] + ); - const fieldTypeFilter = useMemo(() => [fieldType], [fieldType]); + const describedByIds = useMemo(() => (idAria ? [idAria] : undefined), [idAria]); return ( - = ({ const { alerting } = useKibana().services; const maxAlertsPerRun = alerting.getMaxAlertsPerRun(); - const [isInvalid, error] = useMemo(() => { - if (typeof value === 'number' && !isNaN(value) && value <= 0) { - return [true, i18n.GREATER_THAN_ERROR]; - } - return [false]; - }, [value]); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); const hasWarning = useMemo( () => typeof value === 'number' && !isNaN(value) && value > maxAlertsPerRun, @@ -67,6 +66,8 @@ export const MaxSignals: React.FC = ({ return textToRender; }, [hasWarning, maxAlertsPerRun]); + const describedByIds = useMemo(() => (idAria ? [idAria] : undefined), [idAria]); + return ( = ({ width: ${MAX_SIGNALS_FIELD_WIDTH}px; } `} - describedByIds={idAria ? [idAria] : undefined} + describedByIds={describedByIds} fullWidth helpText={helpText} label={field.label} labelAppend={field.labelAppend} isInvalid={isInvalid} - error={error} + error={errorMessage} > = ({ data-test-subj={dataTestSubj} disabled={isDisabled} append={hasWarning ? : undefined} + min={MIN_VALUE} /> ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/translations.ts index b69c0557a051f..b5b135d456f34 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/max_signals/translations.ts @@ -7,13 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const GREATER_THAN_ERROR = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldGreaterThanError', - { - defaultMessage: 'Max alerts must be greater than 0.', - } -); - export const LESS_THAN_WARNING = (maxNumber: number) => i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning', diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/default_risk_score.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/default_risk_score.tsx new file mode 100644 index 0000000000000..94899f35e2728 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/default_risk_score.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiFormRow, EuiText, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiRange } from '@elastic/eui'; +import type { EuiRangeProps } from '@elastic/eui'; +import { MAX_RISK_SCORE, MIN_RISK_SCORE } from '../../validators/default_risk_score_validator'; +import * as i18n from './translations'; + +interface DefaultRiskScoreProps { + value: number; + onChange: (newValue: number) => void; + errorMessage?: string; + idAria?: string; + dataTestSubj?: string; +} + +export function DefaultRiskScore({ + value, + onChange, + errorMessage, + idAria, + dataTestSubj = 'defaultRiskScore', +}: DefaultRiskScoreProps) { + const handleChange = useCallback>( + (event) => { + const eventValue = (event.target as HTMLInputElement).value; + const intOrNanValue = Number.parseInt(eventValue.trim(), 10); + const intValue = Number.isNaN(intOrNanValue) ? MIN_RISK_SCORE : intOrNanValue; + + onChange(intValue); + }, + [onChange] + ); + + return ( + + } + error={errorMessage} + isInvalid={!!errorMessage} + fullWidth + data-test-subj={`${dataTestSubj}-defaultRisk`} + describedByIds={idAria ? [idAria] : undefined} + > + + + + ); +} + +function DefaultRiskScoreLabel() { + return ( +
+ + {i18n.DEFAULT_RISK_SCORE} + + + {i18n.RISK_SCORE_DESCRIPTION} +
+ ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/index.tsx index 1cd775c2e80e5..d5360bbf04001 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/index.tsx @@ -5,45 +5,16 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; -import { noop } from 'lodash/fp'; -import { - EuiFormRow, - EuiCheckbox, - EuiText, - EuiFlexGroup, - EuiFlexItem, - EuiFormLabel, - EuiIcon, - EuiSpacer, - EuiRange, -} from '@elastic/eui'; -import type { EuiRangeProps } from '@elastic/eui'; - +import React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; -import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { FieldComponent } from '@kbn/securitysolution-autocomplete'; -import type { RiskScoreMapping } from '@kbn/securitysolution-io-ts-alerting-types'; - +import { + getFieldValidityAndErrorMessage, + type FieldHook, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import type { AboutStepRiskScore } from '../../../../detections/pages/detection_engine/rules/types'; -import * as i18n from './translations'; - -const NestedContent = styled.div` - margin-left: 24px; -`; - -const EuiFlexItemComboBoxColumn = styled(EuiFlexItem)` - max-width: 376px; -`; - -const EuiFlexItemIconColumn = styled(EuiFlexItem)` - width: 20px; -`; - -const EuiFlexItemRiskScoreColumn = styled(EuiFlexItem)` - width: 160px; -`; +import { DefaultRiskScore } from './default_risk_score'; +import { RiskScoreOverride } from './risk_score_override'; interface RiskScoreFieldProps { dataTestSubj: string; @@ -51,7 +22,6 @@ interface RiskScoreFieldProps { idAria: string; indices: DataViewBase; isDisabled: boolean; - placeholder?: string; } export const RiskScoreField = ({ @@ -60,19 +30,14 @@ export const RiskScoreField = ({ idAria, indices, isDisabled, - placeholder, }: RiskScoreFieldProps) => { const { value, isMappingChecked, mapping } = field.value; const { setValue } = field; - const fieldTypeFilter = useMemo(() => ['number'], []); - const selectedField = useMemo(() => getFieldTypeByMapping(mapping, indices), [mapping, indices]); - - const handleDefaultRiskScoreChange = useCallback>( - (e) => { - const range = (e.target as HTMLInputElement).value; + const handleDefaultRiskScoreChange = useCallback( + (newDefaultRiskScoreValue: number) => { setValue({ - value: Number(range.trim()), + value: newDefaultRiskScoreValue, isMappingChecked, mapping, }); @@ -106,146 +71,29 @@ export const RiskScoreField = ({ }); }, [setValue, value, isMappingChecked, mapping]); - const riskScoreLabel = useMemo(() => { - return ( -
- - {i18n.DEFAULT_RISK_SCORE} - - - {i18n.RISK_SCORE_DESCRIPTION} -
- ); - }, []); - - const riskScoreMappingLabel = useMemo(() => { - return ( -
- - - - - {i18n.RISK_SCORE_MAPPING} - - - - {i18n.RISK_SCORE_MAPPING_DESCRIPTION} - -
- ); - }, [isMappingChecked, handleRiskScoreMappingChecked, isDisabled]); + const errorMessage = getFieldValidityAndErrorMessage(field).errorMessage ?? undefined; return ( + - - - - - - {i18n.RISK_SCORE_MAPPING_DETAILS} : '' - } - error={'errorMessage'} - isInvalid={false} - fullWidth - data-test-subj={`${dataTestSubj}-riskOverride`} - describedByIds={idAria ? [idAria] : undefined} - > - - - {isMappingChecked && ( - - - - - {i18n.SOURCE_FIELD} - - - - {i18n.DEFAULT_RISK_SCORE} - - - - - - - - - - - - - - {i18n.RISK_SCORE_FIELD} - - - - - )} - - + ); }; - -/** - * Looks for field metadata (DataViewFieldBase) in existing index pattern. - * If specified field doesn't exist, returns a stub DataViewFieldBase created based on the mapping -- - * because the field might not have been indexed yet, but we still need to display the mapping. - * - * @param mapping Mapping of a specified field name to risk score. - * @param pattern Existing index pattern. - */ -const getFieldTypeByMapping = ( - mapping: RiskScoreMapping, - pattern: DataViewBase -): DataViewFieldBase => { - const field = mapping?.[0]?.field ?? ''; - const [knownFieldType] = pattern.fields.filter(({ name }) => field != null && field === name); - return knownFieldType ?? { name: field, type: 'number' }; -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/risk_score_override.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/risk_score_override.tsx new file mode 100644 index 0000000000000..d6067062685f7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/risk_score_mapping/risk_score_override.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { noop } from 'lodash/fp'; +import styled from 'styled-components'; +import { + EuiFormRow, + EuiCheckbox, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiFormLabel, + EuiIcon, + EuiSpacer, +} from '@elastic/eui'; +import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; +import { EsFieldSelector } from '@kbn/securitysolution-autocomplete'; +import * as i18n from './translations'; +import type { RiskScoreMapping } from '../../../../../common/api/detection_engine'; + +const NestedContent = styled.div` + margin-left: 24px; +`; + +const EuiFlexItemComboBoxColumn = styled(EuiFlexItem)` + max-width: 376px; +`; + +const EuiFlexItemIconColumn = styled(EuiFlexItem)` + width: 20px; +`; + +const EuiFlexItemRiskScoreColumn = styled(EuiFlexItem)` + width: 160px; +`; + +const fieldTypeFilter = ['number']; + +interface RiskScoreOverrideProps { + isMappingChecked: boolean; + onToggleMappingChecked: () => void; + onMappingChange: ([newField]: DataViewFieldBase[]) => void; + mapping: RiskScoreMapping; + indices: DataViewBase; + dataTestSubj?: string; + idAria?: string; + isDisabled: boolean; +} + +export function RiskScoreOverride({ + isMappingChecked, + onToggleMappingChecked, + onMappingChange, + mapping, + indices, + dataTestSubj = 'riskScoreOverride', + idAria, + isDisabled, +}: RiskScoreOverrideProps) { + const riskScoreMappingLabel = useMemo(() => { + return ( +
+ + + + + {i18n.RISK_SCORE_MAPPING} + + + + {i18n.RISK_SCORE_MAPPING_DESCRIPTION} + +
+ ); + }, [isDisabled, isMappingChecked, onToggleMappingChecked]); + + const describedByIds = useMemo(() => (idAria ? [idAria] : undefined), [idAria]); + const selectedField = useMemo(() => getFieldTypeByMapping(mapping, indices), [mapping, indices]); + + return ( + {i18n.RISK_SCORE_MAPPING_DETAILS} : '' + } + fullWidth + data-test-subj={`${dataTestSubj}-riskOverride`} + describedByIds={describedByIds} + > + + + {isMappingChecked && ( + + + + + {i18n.SOURCE_FIELD} + + + + {i18n.DEFAULT_RISK_SCORE} + + + + + + + + + + + + + + {i18n.RISK_SCORE_FIELD} + + + + + )} + + + ); +} + +/** + * Looks for field metadata (DataViewFieldBase) in existing index pattern. + * If specified field doesn't exist, returns a stub DataViewFieldBase created based on the mapping -- + * because the field might not have been indexed yet, but we still need to display the mapping. + * + * @param mapping Mapping of a specified field name to risk score. + * @param pattern Existing index pattern. + */ +const getFieldTypeByMapping = ( + mapping: RiskScoreMapping, + pattern: DataViewBase +): DataViewFieldBase => { + const field = mapping?.[0]?.field ?? ''; + const [knownFieldType] = pattern.fields.filter(({ name }) => field != null && field === name); + return knownFieldType ?? { name: field, type: 'number' }; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/default_severity.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/default_severity.tsx new file mode 100644 index 0000000000000..477b4bcd0f519 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/default_severity.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFormRow, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSuperSelect, +} from '@elastic/eui'; +import React from 'react'; +import type { Severity } from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen'; +import { severityOptions } from '../step_about_rule/data'; +import * as i18n from './translations'; + +const describedByIds = ['detectionEngineStepAboutRuleSeverity']; + +interface DefaultSeverityProps { + value: Severity; + onChange: (newValue: Severity) => void; +} + +export function DefaultSeverity({ value, onChange }: DefaultSeverityProps) { + return ( + + } + fullWidth + data-test-subj="detectionEngineStepAboutRuleSeverity" + describedByIds={describedByIds} + > + + + + ); +} + +function DefaultSeverityLabel() { + return ( +
+ + {i18n.SEVERITY} + + + {i18n.SEVERITY_DESCRIPTION} +
+ ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/index.tsx index 425564e1ed5f2..28d0a2c622414 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/index.tsx @@ -5,53 +5,14 @@ * 2.0. */ -import { - EuiFormRow, - EuiCheckbox, - EuiText, - EuiFlexGroup, - EuiFlexItem, - EuiFormLabel, - EuiIcon, - EuiSpacer, - EuiSuperSelect, -} from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; - +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback } from 'react'; import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import { - FieldComponent, - AutocompleteFieldMatchComponent, -} from '@kbn/securitysolution-autocomplete'; -import type { - Severity, - SeverityMapping, - SeverityMappingItem, -} from '@kbn/securitysolution-io-ts-alerting-types'; - -import type { SeverityOptionItem } from '../step_about_rule/data'; +import type { Severity, SeverityMapping } from '@kbn/securitysolution-io-ts-alerting-types'; import type { AboutStepSeverity } from '../../../../detections/pages/detection_engine/rules/types'; -import { useKibana } from '../../../../common/lib/kibana'; -import * as i18n from './translations'; - -const NestedContent = styled.div` - margin-left: 24px; -`; - -const EuiFlexItemComboBoxColumn = styled(EuiFlexItem)` - max-width: 376px; -`; - -const EuiFlexItemIconColumn = styled(EuiFlexItem)` - width: 20px; -`; - -const EuiFlexItemSeverityColumn = styled(EuiFlexItem)` - width: 80px; -`; +import { DefaultSeverity } from './default_severity'; +import { SeverityOverride } from './severity_override'; interface SeverityFieldProps { dataTestSubj: string; @@ -59,7 +20,6 @@ interface SeverityFieldProps { idAria: string; indices: DataViewBase; isDisabled: boolean; - options: SeverityOptionItem[]; setRiskScore: (severity: Severity) => void; } @@ -69,10 +29,8 @@ export const SeverityField = ({ idAria, indices, isDisabled, - options, setRiskScore, }: SeverityFieldProps) => { - const { services } = useKibana(); const { value, isMappingChecked, mapping } = field.value; const { setValue } = field; @@ -126,6 +84,7 @@ export const SeverityField = ({ severity, }, ]; + handleFieldValueChange(newMappingItems, index); }, [mapping, handleFieldValueChange] @@ -139,178 +98,22 @@ export const SeverityField = ({ }); }, [isMappingChecked, mapping, value, setValue]); - const severityLabel = useMemo(() => { - return ( -
- - {i18n.SEVERITY} - - - {i18n.SEVERITY_DESCRIPTION} -
- ); - }, []); - - const severityMappingLabel = useMemo(() => { - return ( -
- - - - - {i18n.SEVERITY_MAPPING} - - - - {i18n.SEVERITY_MAPPING_DESCRIPTION} - -
- ); - }, [handleSeverityMappingChecked, isDisabled, isMappingChecked]); - return ( + - - - - - - - {i18n.SEVERITY_MAPPING_DETAILS} : '' - } - error={'errorMessage'} - isInvalid={false} - fullWidth - data-test-subj={`${dataTestSubj}-severityOverride`} - describedByIds={idAria ? [idAria] : undefined} - > - - - {isMappingChecked && ( - - - - - {i18n.SOURCE_FIELD} - - - {i18n.SOURCE_VALUE} - - - - {i18n.DEFAULT_SEVERITY} - - - - - {mapping.map((severityMappingItem: SeverityMappingItem, index) => ( - - - - - - - - - - - - - - { - options.find((o) => o.value === severityMappingItem.severity) - ?.inputDisplay - } - - - - ))} - - )} - - + ); }; - -/** - * Looks for field metadata (DataViewFieldBase) in existing index pattern. - * If specified field doesn't exist, returns a stub DataViewFieldBase created based on the mapping -- - * because the field might not have been indexed yet, but we still need to display the mapping. - * - * @param mapping Mapping of a specified field name + value to a certain severity value. - * @param pattern Existing index pattern. - */ -const getFieldTypeByMapping = ( - mapping: SeverityMappingItem, - pattern: DataViewBase -): DataViewFieldBase => { - const { field } = mapping; - const [knownFieldType] = pattern.fields.filter(({ name }) => field === name); - return knownFieldType ?? { name: field, type: 'string' }; -}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/severity_override.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/severity_override.tsx new file mode 100644 index 0000000000000..046f360c3af4d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/severity_override.tsx @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFormRow, + EuiCheckbox, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiFormLabel, + EuiIcon, + EuiSpacer, +} from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; +import { + EsFieldSelector, + AutocompleteFieldMatchComponent, +} from '@kbn/securitysolution-autocomplete'; +import type { Severity, SeverityMappingItem } from '@kbn/securitysolution-io-ts-alerting-types'; +import { severityOptions } from '../step_about_rule/data'; +import { useKibana } from '../../../../common/lib/kibana'; +import * as styles from './styles'; +import * as i18n from './translations'; + +interface SeverityOverrideProps { + isDisabled: boolean; + onSeverityMappingChecked: () => void; + onFieldChange: (index: number, severity: Severity, [newField]: DataViewFieldBase[]) => void; + onFieldMatchValueChange: (index: number, severity: Severity, newMatchValue: string) => void; + isMappingChecked: boolean; + dataTestSubj?: string; + idAria?: string; + mapping: SeverityMappingItem[]; + indices: DataViewBase; +} + +export function SeverityOverride({ + isDisabled, + onSeverityMappingChecked, + onFieldChange, + onFieldMatchValueChange, + isMappingChecked, + dataTestSubj = 'severity', + idAria, + mapping, + indices, +}: SeverityOverrideProps) { + const severityMappingLabel = useMemo(() => { + return ( +
+ + + + + {i18n.SEVERITY_MAPPING} + + + + {i18n.SEVERITY_MAPPING_DESCRIPTION} + +
+ ); + }, [onSeverityMappingChecked, isDisabled, isMappingChecked]); + + const describedByIds = useMemo(() => (idAria ? [idAria] : undefined), [idAria]); + + return ( + {i18n.SEVERITY_MAPPING_DETAILS} : '' + } + fullWidth + data-test-subj={`${dataTestSubj}-severityOverride`} + describedByIds={describedByIds} + > + + + {isMappingChecked && ( + + + + + {i18n.SOURCE_FIELD} + + + {i18n.SOURCE_VALUE} + + + + {i18n.DEFAULT_SEVERITY} + + + + + {mapping.map((severityMappingItem, index) => ( + + ))} + + )} + + + ); +} + +interface SeverityMappingRowProps { + severityMappingItem: SeverityMappingItem; + index: number; + indices: DataViewBase; + isDisabled: boolean; + onFieldChange: (index: number, severity: Severity, [newField]: DataViewFieldBase[]) => void; + onFieldMatchValueChange: (index: number, severity: Severity, newMatchValue: string) => void; +} + +function SeverityMappingRow({ + severityMappingItem, + index, + indices, + isDisabled, + onFieldChange, + onFieldMatchValueChange, +}: SeverityMappingRowProps) { + const { services } = useKibana(); + + const handleFieldChange = useCallback( + (newField: DataViewFieldBase[]) => { + onFieldChange(index, severityMappingItem.severity, newField); + }, + [index, severityMappingItem.severity, onFieldChange] + ); + + const handleFieldMatchValueChange = useCallback( + (newMatchValue: string) => { + onFieldMatchValueChange(index, severityMappingItem.severity, newMatchValue); + }, + [index, severityMappingItem.severity, onFieldMatchValueChange] + ); + + return ( + + + + + + + + + + + + + + {severityOptions.find((o) => o.value === severityMappingItem.severity)?.inputDisplay} + + + + ); +} + +const NestedContent: React.FC = ({ children }) => ( +
{children}
+); + +const EuiFlexItemComboBoxColumn: React.FC = ({ children }) => ( + {children} +); + +const EuiFlexItemIconColumn: React.FC = ({ children }) => ( + + {children} + +); + +const EuiFlexItemSeverityColumn: React.FC = ({ children }) => ( + + {children} + +); + +/** + * Looks for field metadata (DataViewFieldBase) in existing index pattern. + * If specified field doesn't exist, returns a stub DataViewFieldBase created based on the mapping -- + * because the field might not have been indexed yet, but we still need to display the mapping. + * + * @param mapping Mapping of a specified field name + value to a certain severity value. + * @param pattern Existing index pattern. + */ +const getFieldTypeByMapping = ( + mapping: SeverityMappingItem, + pattern: DataViewBase +): DataViewFieldBase => { + const { field } = mapping; + const [knownFieldType] = pattern.fields.filter(({ name }) => field === name); + return knownFieldType ?? { name: field, type: 'string' }; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/styles.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/styles.ts new file mode 100644 index 0000000000000..480a442b250d9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/severity_mapping/styles.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { css } from '@emotion/css'; + +export const nestedContent = css` + margin-left: 24px; +`; + +export const comboBoxColumn = css` + max-width: 376px; +`; + +export const iconColumn = css` + width: 20px; +`; + +export const severityColumn = css` + width: 80px; +`; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx index 7666a9ba8aee3..ac5c91aa8a25a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx @@ -24,7 +24,7 @@ import { AddMitreAttackThreat } from '../mitre'; import type { FieldHook, FormHook } from '../../../../shared_imports'; import { Field, Form, getUseField, UseField } from '../../../../shared_imports'; -import { defaultRiskScoreBySeverity, severityOptions } from './data'; +import { defaultRiskScoreBySeverity } from './data'; import { isUrlInvalid } from '../../../../common/utils/validators'; import { schema as defaultSchema } from './schema'; import * as I18n from './translations'; @@ -32,7 +32,7 @@ import { StepContentWrapper } from '../../../rule_creation/components/step_conte import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/eui_form'; import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; -import { AutocompleteField } from '../autocomplete_field'; +import { EsFieldSelectorField } from '../es_field_selector_field'; import { useFetchIndex } from '../../../../common/containers/source'; import { DEFAULT_INDICATOR_SOURCE_PATH, @@ -176,7 +176,6 @@ const StepAboutRuleComponent: FC = ({ dataTestSubj: 'detectionEngineStepAboutRuleSeverityField', idAria: 'detectionEngineStepAboutRuleSeverityField', isDisabled: isLoading || indexPatternLoading, - options: severityOptions, indices: indexPattern, setRiskScore, }} @@ -376,14 +375,13 @@ const StepAboutRuleComponent: FC = ({ ) : ( )} @@ -391,14 +389,13 @@ const StepAboutRuleComponent: FC = ({ {!!timestampOverride && timestampOverride !== '@timestamp' && ( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx index 83d79debb757d..66c599d0dc721 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/schema.tsx @@ -7,12 +7,22 @@ import { i18n } from '@kbn/i18n'; -import type { FormSchema, ValidationFunc, ERROR_CODE } from '../../../../shared_imports'; +import type { + FormSchema, + ValidationFunc, + ERROR_CODE, + ValidationError, +} from '../../../../shared_imports'; import { FIELD_TYPES, fieldValidators, VALIDATION_TYPES } from '../../../../shared_imports'; -import type { AboutStepRule } from '../../../../detections/pages/detection_engine/rules/types'; +import type { + AboutStepRiskScore, + AboutStepRule, +} from '../../../../detections/pages/detection_engine/rules/types'; import { OptionalFieldLabel } from '../optional_field_label'; import { isUrlInvalid } from '../../../../common/utils/validators'; import * as I18n from './translations'; +import { defaultRiskScoreValidator } from '../../validators/default_risk_score_validator'; +import { maxSignalsValidatorFactory } from '../../validators/max_signals_validator_factory'; const { emptyField } = fieldValidators; @@ -109,6 +119,11 @@ export const schema: FormSchema = { } ), labelAppend: OptionalFieldLabel, + validations: [ + { + validator: maxSignalsValidatorFactory(), + }, + ], }, isAssociatedToEndpointList: { type: FIELD_TYPES.CHECKBOX, @@ -129,6 +144,18 @@ export const schema: FormSchema = { value: {}, mapping: {}, isMappingChecked: {}, + validations: [ + { + validator: ( + ...args: Parameters> + ): ValidationError | undefined => { + const [{ value: fieldValue, path }] = args; + const defaultRiskScore = fieldValue.value; + + return defaultRiskScoreValidator(defaultRiskScore, path); + }, + }, + ], }, references: { label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx index 9bcf35fdb13ca..f1dcfc74e7923 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx @@ -365,7 +365,7 @@ describe.skip('StepDefineRule', () => { ); }); - it('submits saved early required fields without the "ecs" property', async () => { + it('submits saved earlier required fields', async () => { const initialState = { index: ['test-index'], queryBar: { @@ -390,7 +390,7 @@ describe.skip('StepDefineRule', () => { expect(handleSubmit).toHaveBeenCalledWith( expect.objectContaining({ - requiredFields: [{ name: 'host.name', type: 'string' }], + requiredFields: initialState.requiredFields, }), true ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts index 46dde209804f2..7d80ce7423257 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts @@ -19,6 +19,7 @@ import type { List, } from '@kbn/securitysolution-io-ts-list-types'; import type { + RiskScoreMappingItem, Threats, ThreatSubtechnique, ThreatTechnique, @@ -57,6 +58,8 @@ import type { RuleCreateProps, AlertSuppression, RequiredFieldInput, + SeverityMapping, + RelatedIntegrationArray, } from '../../../../../common/api/detection_engine/model/rule_schema'; import { stepActionsDefaultValue } from '../../../rule_creation/components/step_rule_actions'; import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../common/detection_engine/constants'; @@ -407,8 +410,9 @@ export const getStepDataDataSource = ( /** * Strips away form rows that were not filled out by the user */ -const removeEmptyRequiredFields = (requiredFields: RequiredFieldInput[]): RequiredFieldInput[] => - requiredFields.filter((field) => field.name !== '' && field.type !== ''); +export const removeEmptyRequiredFields = ( + requiredFields: RequiredFieldInput[] +): RequiredFieldInput[] => requiredFields.filter((field) => field.name !== '' && field.type !== ''); export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { const stepData = getStepDataDataSource(defineStepData); @@ -418,7 +422,9 @@ export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStep const baseFields = { type: ruleType, - related_integrations: defineStepData.relatedIntegrations?.filter((ri) => !isEmpty(ri.package)), + related_integrations: defineStepData.relatedIntegrations + ? filterOutEmptyRelatedIntegrations(defineStepData.relatedIntegrations) + : undefined, ...(timeline.id != null && timeline.title != null && { timeline_id: timeline.id, @@ -624,12 +630,12 @@ export const formatAboutStepData = ( : { field_names: investigationFields }, risk_score: riskScore.value, risk_score_mapping: riskScore.isMappingChecked - ? riskScore.mapping.filter((m) => m.field != null && m.field !== '') + ? filterOutEmptyRiskScoreMappingItems(riskScore.mapping) : [], rule_name_override: ruleNameOverride !== '' ? ruleNameOverride : undefined, severity: severity.value, severity_mapping: severity.isMappingChecked - ? severity.mapping.filter((m) => m.field != null && m.field !== '' && m.value != null) + ? filterOutEmptySeverityMappingItems(severity.mapping) : [], threat: filterEmptyThreats(threat).map((singleThreat) => ({ ...singleThreat, @@ -645,6 +651,15 @@ export const formatAboutStepData = ( return resp; }; +export const filterOutEmptyRiskScoreMappingItems = (riskScoreMapping: RiskScoreMappingItem[]) => + riskScoreMapping.filter((m) => m.field != null && m.field !== ''); + +export const filterOutEmptySeverityMappingItems = (severityMapping: SeverityMapping) => + severityMapping.filter((m) => m.field != null && m.field !== '' && m.value != null); + +export const filterOutEmptyRelatedIntegrations = (relatedIntegrations: RelatedIntegrationArray) => + relatedIntegrations.filter((ri) => !isEmpty(ri.package)); + export const isRuleAction = ( action: AlertingRuleAction | AlertingRuleSystemAction, actionTypeRegistry: ActionTypeRegistryContract diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/default_risk_score_validator.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/default_risk_score_validator.ts new file mode 100644 index 0000000000000..6fd7255d48294 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/default_risk_score_validator.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const MIN_RISK_SCORE = 0; +export const MAX_RISK_SCORE = 100; + +export function defaultRiskScoreValidator(defaultRiskScore: unknown, path: string) { + return isDefaultRiskScoreWithinRange(defaultRiskScore) + ? undefined + : { + path, + message: i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleCreation.validation.defaultRiskScoreOutOfRangeValidationError', + { + values: { min: MIN_RISK_SCORE, max: MAX_RISK_SCORE }, + defaultMessage: 'Risk score must be between {min} and {max}.', + } + ), + }; +} + +function isDefaultRiskScoreWithinRange(value: unknown) { + return typeof value === 'number' && value >= MIN_RISK_SCORE && value <= MAX_RISK_SCORE; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/max_signals_validator_factory.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/max_signals_validator_factory.ts new file mode 100644 index 0000000000000..b50e0dc4662a5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/validators/max_signals_validator_factory.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { ERROR_CODE } from '../../../shared_imports'; +import { fieldValidators, type FormData, type ValidationFunc } from '../../../shared_imports'; + +export const MIN_VALUE = 1; + +export function maxSignalsValidatorFactory(): ValidationFunc { + return fieldValidators.numberGreaterThanField({ + than: MIN_VALUE, + allowEquality: true, + message: i18n.translate( + 'xpack.securitySolution.ruleManagement.ruleCreation.validation.maxSignals.greaterThanError', + { + values: { min: MIN_VALUE }, + defaultMessage: 'Max alerts must be greater than {min}.', + } + ), + }); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/common_rule_field_edit.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/common_rule_field_edit.tsx index fefd35fcfaf65..3f10ce014d82e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/common_rule_field_edit.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/common_rule_field_edit.tsx @@ -7,16 +7,247 @@ import React from 'react'; import { RuleFieldEditFormWrapper } from './fields/rule_field_edit_form_wrapper'; -import { NameEdit, nameSchema } from './fields/name'; import type { UpgradeableCommonFields } from '../../../../model/prebuilt_rule_upgrade/fields'; +import { + BuildingBlockEdit, + buildingBlockSchema, + buildingBlockDeserializer, + buildingBlockSerializer, +} from './fields/building_block'; +import { DescriptionEdit, descriptionSchema } from './fields/description'; +import { + FalsePositivesEdit, + falsePositivesSchema, + falsePositivesSerializer, + falsePositivesDeserializer, +} from './fields/false_positives'; +import { + InvestigationFieldsEdit, + investigationFieldsSchema, + investigationFieldsDeserializer, + investigationFieldsSerializer, +} from './fields/investigation_fields'; +import { + MaxSignalsEdit, + maxSignalsDeserializer, + maxSignalsSchema, + maxSignalsSerializer, +} from './fields/max_signals'; +import { NameEdit, nameSchema } from './fields/name'; +import { NoteEdit, noteSchema } from './fields/note'; +import { ReferencesEdit, referencesSchema, referencesSerializer } from './fields/references'; +import { + RelatedIntegrationsEdit, + relatedIntegrationsSchema, + relatedIntegrationsDeserializer, + relatedIntegrationsSerializer, +} from './fields/related_integrations'; +import { + RequiredFieldsEdit, + requiredFieldsSchema, + requiredFieldsDeserializer, + requiredFieldsSerializer, +} from './fields/required_fields'; +import { + RiskScoreEdit, + riskScoreDeserializer, + riskScoreSchema, + riskScoreSerializer, +} from './fields/risk_score'; +import { + RiskScoreMappingEdit, + riskScoreMappingDeserializer, + riskScoreMappingSerializer, +} from './fields/risk_score_mapping'; +import { + RuleNameOverrideEdit, + ruleNameOverrideDeserializer, + ruleNameOverrideSerializer, + ruleNameOverrideSchema, +} from './fields/rule_name_override'; +import { + RuleScheduleEdit, + ruleScheduleSchema, + ruleScheduleDeserializer, + ruleScheduleSerializer, +} from './fields/rule_schedule'; +import { SetupEdit, setupSchema } from './fields/setup'; +import { SeverityEdit } from './fields/severity'; +import { + SeverityMappingEdit, + severityMappingDeserializer, + severityMappingSerializer, +} from './fields/severity_mapping'; +import { TagsEdit, tagsSchema } from './fields/tags'; +import { ThreatEdit, threatSchema, threatSerializer } from './fields/threat'; +import { + TimelineTemplateEdit, + timelineTemplateDeserializer, + timelineTemplateSchema, + timelineTemplateSerializer, +} from './fields/timeline_template'; +import { + TimestampOverrideEdit, + timestampOverrideDeserializer, + timestampOverrideSerializer, + timestampOverrideSchema, +} from './fields/timestamp_override'; + interface CommonRuleFieldEditProps { fieldName: UpgradeableCommonFields; } +/* eslint-disable-next-line complexity */ export function CommonRuleFieldEdit({ fieldName }: CommonRuleFieldEditProps) { switch (fieldName) { + case 'building_block': + return ( + + ); + case 'description': + return ( + + ); + case 'false_positives': + return ( + + ); + case 'investigation_fields': + return ( + + ); + case 'max_signals': + return ( + + ); case 'name': return ; + case 'note': + return ; + case 'references': + return ( + + ); + case 'related_integrations': + return ( + + ); + case 'required_fields': + return ( + + ); + case 'risk_score': + return ( + + ); + case 'risk_score_mapping': + return ( + + ); + case 'rule_name_override': + return ( + + ); + case 'rule_schedule': + return ( + + ); + case 'setup': + return ; + case 'severity': + return ; + case 'severity_mapping': + return ( + + ); + case 'tags': + return ; + case 'timeline_template': + return ( + + ); + case 'timestamp_override': + return ( + + ); + case 'threat': + return ( + + ); default: return null; // Will be replaced with `assertUnreachable(fieldName)` once all fields are implemented } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/building_block.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/building_block.tsx new file mode 100644 index 0000000000000..3b21ff76f07a8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/building_block.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { Field, UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { BuildingBlockObject } from '../../../../../../../../common/api/detection_engine'; + +export const buildingBlockSchema = { isBuildingBlock: schema.isBuildingBlock } as FormSchema<{ + isBuildingBlock: boolean; +}>; + +export function BuildingBlockEdit(): JSX.Element { + return ; +} + +export function buildingBlockDeserializer(defaultValue: FormData) { + return { + isBuildingBlock: defaultValue.building_block, + }; +} + +export function buildingBlockSerializer(formData: FormData): { + building_block: BuildingBlockObject | undefined; +} { + return { building_block: formData.isBuildingBlock ? { type: 'default' } : undefined }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/description.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/description.tsx new file mode 100644 index 0000000000000..c2d279c0d72e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/description.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema } from '../../../../../../../shared_imports'; +import { Field, UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { RuleDescription } from '../../../../../../../../common/api/detection_engine'; + +export const descriptionSchema = { description: schema.description } as FormSchema<{ + description: RuleDescription; +}>; + +const componentProps = { + euiFieldProps: { + fullWidth: true, + compressed: true, + }, +}; + +export function DescriptionEdit(): JSX.Element { + return ; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/false_positives.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/false_positives.tsx new file mode 100644 index 0000000000000..ddb23732b1871 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/false_positives.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { isEmpty } from 'lodash'; +import * as i18n from '../../../../../../rule_creation_ui/components/step_about_rule/translations'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { RuleFalsePositiveArray } from '../../../../../../../../common/api/detection_engine'; +import { AddItem } from '../../../../../../rule_creation_ui/components/add_item_form'; + +export const falsePositivesSchema = { falsePositives: schema.falsePositives } as FormSchema<{ + falsePositives: RuleFalsePositiveArray; +}>; + +const componentProps = { + addText: i18n.ADD_FALSE_POSITIVE, +}; + +export function FalsePositivesEdit(): JSX.Element { + return ; +} + +export function falsePositivesDeserializer(defaultValue: FormData) { + return { + falsePositives: defaultValue.false_positives, + }; +} + +export function falsePositivesSerializer(formData: FormData): { + false_positives: RuleFalsePositiveArray; +} { + const falsePositives: RuleFalsePositiveArray = formData.falsePositives; + + return { + /* Remove empty items from the falsePositives array */ + false_positives: falsePositives.filter((item) => !isEmpty(item)), + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/investigation_fields.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/investigation_fields.tsx new file mode 100644 index 0000000000000..971174f455390 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/investigation_fields.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { + DiffableRule, + InvestigationFields, + RuleFalsePositiveArray, +} from '../../../../../../../../common/api/detection_engine'; +import { MultiSelectFieldsAutocomplete } from '../../../../../../rule_creation_ui/components/multi_select_fields'; +import { useAllEsqlRuleFields } from '../../../../../../rule_creation_ui/hooks'; +import { useDefaultIndexPattern } from '../../../../../hooks/use_default_index_pattern'; +import { useRuleIndexPattern } from '../../../../../../rule_creation_ui/pages/form'; +import { getUseRuleIndexPatternParameters } from '../utils'; + +export const investigationFieldsSchema = { + investigationFields: schema.investigationFields, +} as FormSchema<{ + investigationFields: RuleFalsePositiveArray; +}>; + +interface InvestigationFieldsEditProps { + finalDiffableRule: DiffableRule; +} + +export function InvestigationFieldsEdit({ + finalDiffableRule, +}: InvestigationFieldsEditProps): JSX.Element { + const { type } = finalDiffableRule; + + const defaultIndexPattern = useDefaultIndexPattern(); + const indexPatternParameters = getUseRuleIndexPatternParameters( + finalDiffableRule, + defaultIndexPattern + ); + const { indexPattern, isIndexPatternLoading } = useRuleIndexPattern(indexPatternParameters); + + const { fields: investigationFields, isLoading: isInvestigationFieldsLoading } = + useAllEsqlRuleFields({ + esqlQuery: type === 'esql' ? finalDiffableRule.esql_query.query : undefined, + indexPatternsFields: indexPattern.fields, + }); + + return ( + + ); +} + +export function investigationFieldsDeserializer(defaultValue: FormData) { + return { + investigationFields: defaultValue.investigation_fields?.field_names ?? [], + }; +} + +export function investigationFieldsSerializer(formData: FormData): { + investigation_fields: InvestigationFields | undefined; +} { + const hasInvestigationFields = formData.investigationFields.length > 0; + + return { + investigation_fields: hasInvestigationFields + ? { + field_names: formData.investigationFields, + } + : undefined, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/max_signals.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/max_signals.tsx new file mode 100644 index 0000000000000..dd7774eaa5ac4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/max_signals.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { MaxSignals as MaxSignalsType } from '../../../../../../../../common/api/detection_engine'; +import { DEFAULT_MAX_SIGNALS } from '../../../../../../../../common/constants'; +import { MaxSignals } from '../../../../../../rule_creation_ui/components/max_signals'; + +export const maxSignalsSchema = { maxSignals: schema.maxSignals } as FormSchema<{ + maxSignals: boolean; +}>; + +const componentProps = { + placeholder: DEFAULT_MAX_SIGNALS, +}; + +export function MaxSignalsEdit(): JSX.Element { + return ; +} + +export function maxSignalsDeserializer(defaultValue: FormData) { + return { + maxSignals: defaultValue.max_signals, + }; +} + +export function maxSignalsSerializer(formData: FormData): { + max_signals: MaxSignalsType; +} { + return { + max_signals: Number.isSafeInteger(formData.maxSignals) + ? formData.maxSignals + : DEFAULT_MAX_SIGNALS, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/name.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/name.tsx index 10ae6cffbe50d..d602da90f34b0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/name.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/name.tsx @@ -13,16 +13,12 @@ import type { RuleName } from '../../../../../../../../common/api/detection_engi export const nameSchema = { name: schema.name } as FormSchema<{ name: RuleName }>; +const componentProps = { + euiFieldProps: { + fullWidth: true, + }, +}; + export function NameEdit(): JSX.Element { - return ( - - ); + return ; } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/note.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/note.tsx new file mode 100644 index 0000000000000..3a560fd28f27c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/note.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema } from '../../../../../../../shared_imports'; +import { UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { InvestigationGuide } from '../../../../../../../../common/api/detection_engine'; +import * as i18n from '../../../../../../rule_creation_ui/components/step_about_rule/translations'; +import { MarkdownEditorForm } from '../../../../../../../common/components/markdown_editor'; + +export const noteSchema = { note: schema.note } as FormSchema<{ + note: InvestigationGuide; +}>; + +const componentProps = { + placeholder: i18n.ADD_RULE_NOTE_HELP_TEXT, +}; + +export function NoteEdit(): JSX.Element { + return ; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/references.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/references.tsx new file mode 100644 index 0000000000000..afa4fba09d890 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/references.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { compact } from 'lodash'; +import * as i18n from '../../../../../../rule_creation_ui/components/step_about_rule/translations'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { RuleReferenceArray } from '../../../../../../../../common/api/detection_engine'; +import { AddItem } from '../../../../../../rule_creation_ui/components/add_item_form'; +import { isUrlInvalid } from '../../../../../../../common/utils/validators'; + +export const referencesSchema = { references: schema.references } as FormSchema<{ + references: RuleReferenceArray; +}>; + +const componentProps = { + addText: i18n.ADD_REFERENCE, + validate: isUrlInvalid, +}; + +export function ReferencesEdit(): JSX.Element { + return ; +} + +export function referencesSerializer(formData: FormData): { + references: RuleReferenceArray; +} { + return { + /* Remove empty items from the references array */ + references: compact(formData.references), + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/related_integrations.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/related_integrations.tsx new file mode 100644 index 0000000000000..443f0b4bb7752 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/related_integrations.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_define_rule/schema'; +import { RelatedIntegrations } from '../../../../../../rule_creation/components/related_integrations'; +import type { RelatedIntegrationArray } from '../../../../../../../../common/api/detection_engine'; +import { filterOutEmptyRelatedIntegrations } from '../../../../../../rule_creation_ui/pages/rule_creation/helpers'; + +export const relatedIntegrationsSchema = { + relatedIntegrations: schema.relatedIntegrations, +} as FormSchema<{ + relatedIntegrations: RelatedIntegrationArray; +}>; + +export function RelatedIntegrationsEdit(): JSX.Element { + return ; +} + +export function relatedIntegrationsDeserializer(defaultValue: FormData) { + return { + relatedIntegrations: defaultValue.related_integrations, + }; +} + +export function relatedIntegrationsSerializer(formData: FormData): { + related_integrations: RelatedIntegrationArray; +} { + const relatedIntegrations = (formData.relatedIntegrations ?? []) as RelatedIntegrationArray; + + return { + related_integrations: filterOutEmptyRelatedIntegrations(relatedIntegrations), + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/required_fields.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/required_fields.tsx new file mode 100644 index 0000000000000..3e6809e35a4f4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/required_fields.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_define_rule/schema'; +import type { + DiffableRule, + RequiredFieldInput, +} from '../../../../../../../../common/api/detection_engine'; +import { RequiredFields } from '../../../../../../rule_creation/components/required_fields'; +import { useDefaultIndexPattern } from '../../../../../hooks/use_default_index_pattern'; +import { getUseRuleIndexPatternParameters } from '../utils'; +import { useRuleIndexPattern } from '../../../../../../rule_creation_ui/pages/form'; +import { removeEmptyRequiredFields } from '../../../../../../rule_creation_ui/pages/rule_creation/helpers'; + +export const requiredFieldsSchema = { + requiredFields: schema.requiredFields, +} as FormSchema<{ + requiredFields: RequiredFieldInput[]; +}>; + +interface RequiredFieldsEditProps { + finalDiffableRule: DiffableRule; +} + +export function RequiredFieldsEdit({ finalDiffableRule }: RequiredFieldsEditProps): JSX.Element { + const defaultIndexPattern = useDefaultIndexPattern(); + const indexPatternParameters = getUseRuleIndexPatternParameters( + finalDiffableRule, + defaultIndexPattern + ); + const { indexPattern, isIndexPatternLoading } = useRuleIndexPattern(indexPatternParameters); + + return ( + + ); +} + +export function requiredFieldsDeserializer(defaultValue: FormData) { + return { + requiredFields: defaultValue.required_fields, + }; +} + +export function requiredFieldsSerializer(formData: FormData): { + required_fields: RequiredFieldInput[]; +} { + const requiredFields = (formData.requiredFields ?? []) as RequiredFieldInput[]; + return { + required_fields: removeEmptyRequiredFields(requiredFields), + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/risk_score.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/risk_score.tsx new file mode 100644 index 0000000000000..7575213706051 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/risk_score.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + type FormData, + type FieldHook, + UseField, + getFieldValidityAndErrorMessage, +} from '../../../../../../../shared_imports'; +import type { RiskScore } from '../../../../../../../../common/api/detection_engine'; +import { DefaultRiskScore } from '../../../../../../rule_creation_ui/components/risk_score_mapping/default_risk_score'; +import { defaultRiskScoreValidator } from '../../../../../../rule_creation_ui/validators/default_risk_score_validator'; + +export const riskScoreSchema = { + riskScore: { + validations: [ + { + validator: ({ path, value }: { path: string; value: unknown }) => + defaultRiskScoreValidator(value, path), + }, + ], + }, +}; + +export function RiskScoreEdit(): JSX.Element { + return ; +} + +interface RiskScoreEditFieldProps { + field: FieldHook; +} + +function RiskScoreEditField({ field }: RiskScoreEditFieldProps) { + const { value, setValue } = field; + const errorMessage = getFieldValidityAndErrorMessage(field).errorMessage ?? undefined; + + return ; +} + +export function riskScoreDeserializer(defaultValue: FormData) { + return { + riskScore: defaultValue.risk_score, + }; +} + +export function riskScoreSerializer(formData: FormData): { + risk_score: RiskScore; +} { + return { + risk_score: formData.riskScore, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/risk_score_mapping.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/risk_score_mapping.tsx new file mode 100644 index 0000000000000..a1e5e3f80630b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/risk_score_mapping.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import type { DataViewFieldBase } from '@kbn/es-query'; +import { type FormData, type FieldHook, UseField } from '../../../../../../../shared_imports'; +import type { + DiffableRule, + RiskScoreMapping, +} from '../../../../../../../../common/api/detection_engine'; +import { RiskScoreOverride } from '../../../../../../rule_creation_ui/components/risk_score_mapping/risk_score_override'; +import { useDefaultIndexPattern } from '../../../../../hooks/use_default_index_pattern'; +import { getUseRuleIndexPatternParameters } from '../utils'; +import { useRuleIndexPattern } from '../../../../../../rule_creation_ui/pages/form'; +import { filterOutEmptyRiskScoreMappingItems } from '../../../../../../rule_creation_ui/pages/rule_creation/helpers'; + +interface RiskScoreMappingEditProps { + finalDiffableRule: DiffableRule; +} + +export function RiskScoreMappingEdit({ finalDiffableRule }: RiskScoreMappingEditProps) { + return ( + + ); +} + +interface RiskScoreMappingFieldProps { + field: FieldHook<{ isMappingChecked: boolean; mapping: RiskScoreMapping }>; + finalDiffableRule: DiffableRule; +} + +function RiskScoreMappingField({ field, finalDiffableRule }: RiskScoreMappingFieldProps) { + const defaultIndexPattern = useDefaultIndexPattern(); + const indexPatternParameters = getUseRuleIndexPatternParameters( + finalDiffableRule, + defaultIndexPattern + ); + const { indexPattern, isIndexPatternLoading } = useRuleIndexPattern(indexPatternParameters); + + const { value, setValue } = field; + + const handleToggleMappingChecked = useCallback(() => { + setValue((prevValue) => ({ + ...prevValue, + isMappingChecked: !prevValue.isMappingChecked, + })); + }, [setValue]); + + const handleMappingChange = useCallback( + ([newField]: DataViewFieldBase[]): void => { + const mapping = [ + { + field: newField?.name ?? '', + operator: 'equals' as const, + value: '', + }, + ]; + + setValue((prevValue) => ({ + ...prevValue, + mapping, + })); + }, + [setValue] + ); + + return ( + + ); +} + +export function riskScoreMappingDeserializer(defaultValue: FormData) { + return { + riskScoreMapping: { + isMappingChecked: defaultValue.risk_score_mapping.length > 0, + mapping: defaultValue.risk_score_mapping, + }, + }; +} + +export function riskScoreMappingSerializer(formData: FormData): { + risk_score_mapping: RiskScoreMapping; +} { + return { + risk_score_mapping: formData.riskScoreMapping.isMappingChecked + ? filterOutEmptyRiskScoreMappingItems(formData.riskScoreMapping.mapping) + : [], + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_field_edit_form_wrapper.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_field_edit_form_wrapper.tsx index 26a2574489b16..1b45bea28880f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_field_edit_form_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_field_edit_form_wrapper.tsx @@ -27,13 +27,13 @@ export type FieldDeserializerFn = ( interface RuleFieldEditFormWrapperProps { component: RuleFieldEditComponent; - ruleFieldFormSchema: FormSchema; + ruleFieldFormSchema?: FormSchema; deserializer?: FieldDeserializerFn; serializer?: (formData: FormData) => FormData; } /** - * FieldFormWrapper component manages form state and renders "Save" and "Cancel" buttons. + * RuleFieldEditFormWrapper component manages form state and renders "Save" and "Cancel" buttons. * * @param {Object} props - Component props. * @param {React.ComponentType} props.component - Field component to be wrapped. diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_name_override.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_name_override.tsx new file mode 100644 index 0000000000000..d133956436505 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_name_override.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { + DiffableRule, + RuleNameOverrideObject, +} from '../../../../../../../../common/api/detection_engine'; +import { EsFieldSelectorField } from '../../../../../../rule_creation_ui/components/es_field_selector_field'; +import { useRuleIndexPattern } from '../../../../../../rule_creation_ui/pages/form'; +import { getUseRuleIndexPatternParameters } from '../utils'; +import { useDefaultIndexPattern } from '../../../../../hooks/use_default_index_pattern'; + +export const ruleNameOverrideSchema = { ruleNameOverride: schema.ruleNameOverride } as FormSchema<{ + ruleNameOverride: string; +}>; + +interface RuleNameOverrideEditProps { + finalDiffableRule: DiffableRule; +} + +export function RuleNameOverrideEdit({ + finalDiffableRule, +}: RuleNameOverrideEditProps): JSX.Element { + const defaultIndexPattern = useDefaultIndexPattern(); + const indexPatternParameters = getUseRuleIndexPatternParameters( + finalDiffableRule, + defaultIndexPattern + ); + const { indexPattern, isIndexPatternLoading } = useRuleIndexPattern(indexPatternParameters); + + const componentProps = useMemo( + () => ({ + fieldType: 'string', + indices: indexPattern, + isDisabled: isIndexPatternLoading, + }), + [indexPattern, isIndexPatternLoading] + ); + + return ( + + ); +} + +export function ruleNameOverrideDeserializer(defaultValue: FormData) { + return { + ruleNameOverride: defaultValue.rule_name_override?.field_name ?? '', + }; +} + +export function ruleNameOverrideSerializer(formData: FormData): { + rule_name_override: RuleNameOverrideObject | undefined; +} { + return { + rule_name_override: formData.ruleNameOverride + ? { field_name: formData.ruleNameOverride } + : undefined, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_schedule.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_schedule.tsx new file mode 100644 index 0000000000000..12e7bbea9a20a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/rule_schedule.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { parseDuration } from '@kbn/alerting-plugin/common'; +import { type FormSchema, type FormData, UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_schedule_rule/schema'; +import type { RuleSchedule } from '../../../../../../../../common/api/detection_engine'; +import { ScheduleItem } from '../../../../../../rule_creation/components/schedule_item_form'; +import { secondsToDurationString } from '../../../../../../../detections/pages/detection_engine/rules/helpers'; + +export const ruleScheduleSchema = { + interval: schema.interval, + from: schema.from, +} as FormSchema<{ + interval: string; + from: string; +}>; + +const componentProps = { + minimumValue: 1, +}; + +export function RuleScheduleEdit(): JSX.Element { + return ( + <> + + + + ); +} + +export function ruleScheduleDeserializer(defaultValue: FormData) { + const lookbackSeconds = parseDuration(defaultValue.rule_schedule.lookback) / 1000; + const lookbackHumanized = secondsToDurationString(lookbackSeconds); + + return { + interval: defaultValue.rule_schedule.interval, + from: lookbackHumanized, + }; +} + +export function ruleScheduleSerializer(formData: FormData): { + rule_schedule: RuleSchedule; +} { + return { + rule_schedule: { + interval: formData.interval, + lookback: formData.from, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/setup.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/setup.tsx new file mode 100644 index 0000000000000..8304641d1b168 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/setup.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema } from '../../../../../../../shared_imports'; +import { UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { SetupGuide } from '../../../../../../../../common/api/detection_engine'; +import * as i18n from '../../../../../../rule_creation_ui/components/step_about_rule/translations'; +import { MarkdownEditorForm } from '../../../../../../../common/components/markdown_editor'; + +export const setupSchema = { setup: schema.setup } as FormSchema<{ + setup: SetupGuide; +}>; + +const componentProps = { + placeholder: i18n.ADD_RULE_SETUP_HELP_TEXT, + includePlugins: false, +}; + +export function SetupEdit(): JSX.Element { + return ; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/severity.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/severity.tsx new file mode 100644 index 0000000000000..1a6bf03ea67f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/severity.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { type FieldHook, UseField } from '../../../../../../../shared_imports'; +import type { Severity } from '../../../../../../../../common/api/detection_engine'; +import { DefaultSeverity } from '../../../../../../rule_creation_ui/components/severity_mapping/default_severity'; + +export function SeverityEdit(): JSX.Element { + return ; +} + +interface SeverityEditFieldProps { + field: FieldHook; +} + +function SeverityEditField({ field }: SeverityEditFieldProps) { + const { value, setValue } = field; + + return ; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/severity_mapping.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/severity_mapping.tsx new file mode 100644 index 0000000000000..d4206549bfa1e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/severity_mapping.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import type { DataViewFieldBase } from '@kbn/es-query'; +import { type FieldHook, type FormData, UseField } from '../../../../../../../shared_imports'; +import type { + DiffableRule, + Severity, + SeverityMapping, +} from '../../../../../../../../common/api/detection_engine'; +import { SeverityOverride } from '../../../../../../rule_creation_ui/components/severity_mapping/severity_override'; +import { useDefaultIndexPattern } from '../../../../../hooks/use_default_index_pattern'; +import { getUseRuleIndexPatternParameters } from '../utils'; +import { useRuleIndexPattern } from '../../../../../../rule_creation_ui/pages/form'; +import { fillEmptySeverityMappings } from '../../../../../../../detections/pages/detection_engine/rules/helpers'; +import { filterOutEmptySeverityMappingItems } from '../../../../../../rule_creation_ui/pages/rule_creation/helpers'; + +interface SeverityMappingEditProps { + finalDiffableRule: DiffableRule; +} + +export function SeverityMappingEdit({ finalDiffableRule }: SeverityMappingEditProps): JSX.Element { + return ( + + ); +} + +interface SeverityMappingFieldProps { + field: FieldHook<{ + isMappingChecked: boolean; + mapping: SeverityMapping; + }>; + finalDiffableRule: DiffableRule; +} + +function SeverityMappingField({ field, finalDiffableRule }: SeverityMappingFieldProps) { + const defaultIndexPattern = useDefaultIndexPattern(); + const indexPatternParameters = getUseRuleIndexPatternParameters( + finalDiffableRule, + defaultIndexPattern + ); + const { indexPattern, isIndexPatternLoading } = useRuleIndexPattern(indexPatternParameters); + + const { value, setValue } = field; + + const handleFieldChange = useCallback( + (index: number, severity: Severity, [newField]: DataViewFieldBase[]): void => { + setValue((prevValue) => { + const newMappingItem: SeverityMapping = [ + { + ...prevValue.mapping[index], + field: newField?.name ?? '', + value: newField != null ? prevValue.mapping[index].value : '', + operator: 'equals', + severity, + }, + ]; + + return { + ...prevValue, + mapping: [ + ...prevValue.mapping.slice(0, index), + ...newMappingItem, + ...prevValue.mapping.slice(index + 1), + ], + }; + }); + }, + [setValue] + ); + + const handleFieldMatchValueChange = useCallback( + (index: number, severity: Severity, newMatchValue: string): void => { + setValue((prevValue) => { + const newMappingItem: SeverityMapping = [ + { + ...prevValue.mapping[index], + field: prevValue.mapping[index].field, + value: + prevValue.mapping[index].field != null && prevValue.mapping[index].field !== '' + ? newMatchValue + : '', + operator: 'equals', + severity, + }, + ]; + + return { + ...prevValue, + mapping: [ + ...prevValue.mapping.slice(0, index), + ...newMappingItem, + ...prevValue.mapping.slice(index + 1), + ], + }; + }); + }, + [setValue] + ); + + const handleSeverityMappingChecked = useCallback(() => { + setValue((prevValue) => ({ + ...prevValue, + isMappingChecked: !prevValue.isMappingChecked, + })); + }, [setValue]); + + return ( + + ); +} + +export function severityMappingDeserializer(defaultValue: FormData) { + return { + severityMapping: { + isMappingChecked: defaultValue.severity_mapping.length > 0, + mapping: fillEmptySeverityMappings(defaultValue.severity_mapping as SeverityMapping), + }, + }; +} + +export function severityMappingSerializer(formData: FormData): { + severity_mapping: SeverityMapping; +} { + return { + severity_mapping: formData.severityMapping.isMappingChecked + ? filterOutEmptySeverityMappingItems(formData.severityMapping.mapping) + : [], + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/tags.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/tags.tsx new file mode 100644 index 0000000000000..063b7f4f48b80 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/tags.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema } from '../../../../../../../shared_imports'; +import { Field, UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { RuleTagArray } from '../../../../../../../../common/api/detection_engine'; + +export const tagsSchema = { tags: schema.tags } as FormSchema<{ name: RuleTagArray }>; + +const componentProps = { + euiFieldProps: { + fullWidth: true, + placeholder: '', + }, +}; + +export function TagsEdit(): JSX.Element { + return ; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat.tsx new file mode 100644 index 0000000000000..fbc0541ff50fc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/threat.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { type FormSchema, type FormData, UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import type { ThreatArray } from '../../../../../../../../common/api/detection_engine'; +import { AddMitreAttackThreat } from '../../../../../../rule_creation_ui/components/mitre'; +import { filterEmptyThreats } from '../../../../../../rule_creation_ui/pages/rule_creation/helpers'; + +export const threatSchema = { threat: schema.threat } as FormSchema<{ threat: ThreatArray }>; + +export function ThreatEdit(): JSX.Element { + return ; +} + +export function threatSerializer(formData: FormData): { + threat: ThreatArray; +} { + return { + threat: filterEmptyThreats(formData.threat).map((singleThreat) => ({ + ...singleThreat, + framework: 'MITRE ATT&CK', + })), + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/timeline_template.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/timeline_template.tsx new file mode 100644 index 0000000000000..1f7ef2eaf05b1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/timeline_template.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { UseField } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_define_rule/schema'; +import type { TimelineTemplateReference } from '../../../../../../../../common/api/detection_engine'; +import { + PickTimeline, + type FieldValueTimeline, +} from '../../../../../../rule_creation/components/pick_timeline'; + +export const timelineTemplateSchema = { timeline: schema.timeline } as FormSchema<{ + timeline: FieldValueTimeline; +}>; + +export function TimelineTemplateEdit(): JSX.Element { + return ; +} + +export function timelineTemplateDeserializer(defaultValue: FormData) { + return { + timeline: { + id: defaultValue.timeline_template?.timeline_id ?? null, + title: defaultValue.timeline_template?.timeline_title ?? null, + }, + }; +} + +export function timelineTemplateSerializer(formData: FormData): { + timeline_template: TimelineTemplateReference | undefined; +} { + if (!formData.timeline.id) { + return { timeline_template: undefined }; + } + + return { + timeline_template: { + timeline_id: formData.timeline.id, + timeline_title: formData.timeline.title, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/timestamp_override.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/timestamp_override.tsx new file mode 100644 index 0000000000000..a57d538fab47c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/fields/timestamp_override.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import type { FormSchema, FormData } from '../../../../../../../shared_imports'; +import { Field, UseField, useFormData } from '../../../../../../../shared_imports'; +import { schema } from '../../../../../../rule_creation_ui/components/step_about_rule/schema'; +import { EsFieldSelectorField } from '../../../../../../rule_creation_ui/components/es_field_selector_field'; +import { useDefaultIndexPattern } from '../../../../../hooks/use_default_index_pattern'; +import { getUseRuleIndexPatternParameters } from '../utils'; +import { useRuleIndexPattern } from '../../../../../../rule_creation_ui/pages/form'; +import type { + DiffableRule, + TimestampOverrideObject, +} from '../../../../../../../../common/api/detection_engine'; + +export const timestampOverrideSchema = { + timestampOverride: schema.timestampOverride, + timestampOverrideFallbackDisabled: schema.timestampOverrideFallbackDisabled, +} as FormSchema<{ + timestampOverride: string; + timestampOverrideFallbackDisabled: boolean | undefined; +}>; + +interface TimestampOverrideEditProps { + finalDiffableRule: DiffableRule; +} + +export function TimestampOverrideEdit({ + finalDiffableRule, +}: TimestampOverrideEditProps): JSX.Element { + const defaultIndexPattern = useDefaultIndexPattern(); + const indexPatternParameters = getUseRuleIndexPatternParameters( + finalDiffableRule, + defaultIndexPattern + ); + const { indexPattern, isIndexPatternLoading } = useRuleIndexPattern(indexPatternParameters); + + const componentProps = useMemo( + () => ({ + fieldType: 'date', + indices: indexPattern, + isDisabled: isIndexPatternLoading, + }), + [indexPattern, isIndexPatternLoading] + ); + + return ( + <> + + + + ); +} + +function TimestampFallbackDisabled() { + const [formData] = useFormData(); + const { timestampOverride } = formData; + + if (timestampOverride && timestampOverride !== '@timestamp') { + return ; + } + + return null; +} + +export function timestampOverrideDeserializer(defaultValue: FormData) { + return { + timestampOverride: defaultValue.timestamp_override.field_name, + timestampOverrideFallbackDisabled: defaultValue.timestamp_override.fallback_disabled ?? false, + }; +} + +export function timestampOverrideSerializer(formData: FormData): { + timestamp_override: TimestampOverrideObject | undefined; +} { + if (formData.timestampOverride === '') { + return { timestamp_override: undefined }; + } + + return { + timestamp_override: { + field_name: formData.timestampOverride, + fallback_disabled: formData.timestampOverrideFallbackDisabled, + }, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/utils.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/utils.ts new file mode 100644 index 0000000000000..bd78bb5e9ed26 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_edit/utils.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataSourceType } from '../../../../../../detections/pages/detection_engine/rules/types'; +import { DataSourceType as DataSourceTypeSnakeCase } from '../../../../../../../common/api/detection_engine'; +import type { DiffableRule } from '../../../../../../../common/api/detection_engine'; + +interface UseRuleIndexPatternParameters { + dataSourceType: DataSourceType; + index: string[]; + dataViewId: string | undefined; +} + +export function getUseRuleIndexPatternParameters( + finalDiffableRule: DiffableRule, + defaultIndexPattern: string[] +): UseRuleIndexPatternParameters { + if (!('data_source' in finalDiffableRule) || !finalDiffableRule.data_source) { + return { + dataSourceType: DataSourceType.IndexPatterns, + index: defaultIndexPattern, + dataViewId: undefined, + }; + } + if (finalDiffableRule.data_source.type === DataSourceTypeSnakeCase.data_view) { + return { + dataSourceType: DataSourceType.DataView, + index: [], + dataViewId: finalDiffableRule.data_source.data_view_id, + }; + } + return { + dataSourceType: DataSourceType.IndexPatterns, + index: finalDiffableRule.data_source.index_patterns, + dataViewId: undefined, + }; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx index bc4f1928ef9ba..6c502b2e41780 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/common_rule_field_readonly.tsx @@ -45,7 +45,7 @@ export function CommonRuleFieldReadOnly({ }: CommonRuleFieldReadOnlyProps) { switch (fieldName) { case 'building_block': - return ; + return ; case 'description': return ; case 'investigation_fields': diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.tsx index 84edd7932a2d7..7e64c140e6d70 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/rule_details/three_way_diff/final_readonly/fields/building_block/building_block.tsx @@ -9,8 +9,17 @@ import React from 'react'; import { EuiDescriptionList } from '@elastic/eui'; import * as ruleDetailsI18n from '../../../../translations'; import { BuildingBlock } from '../../../../rule_about_section'; +import type { BuildingBlockObject } from '../../../../../../../../../common/api/detection_engine'; + +interface BuildingBlockReadOnlyProps { + buildingBlock?: BuildingBlockObject; +} + +export function BuildingBlockReadOnly({ buildingBlock }: BuildingBlockReadOnlyProps) { + if (!buildingBlock || !buildingBlock.type) { + return null; + } -export function BuildingBlockReadOnly() { return ( { + if (threat.length === 0) { + return null; + } + return (