[8.x] [Security Solution] `FinalEdit`: Add fields that are common for all rule types (#196642) (#199743)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution] `FinalEdit`: Add fields that are common
for all rule types
(#196642)](https://github.com/elastic/kibana/pull/196642)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Nikita
Indik","email":"nikita.indik@elastic.co"},"sourceCommit":{"committedDate":"2024-11-12T10:04:10Z","message":"[Security
Solution] `FinalEdit`: Add fields that are common for all rule types
(#196642)\n\n**Partially addresses:
https://github.com/elastic/kibana/issues/171520**\r\n**Is a follow-up
to: https://github.com/elastic/kibana/pull/196326**\r\n\r\nThis PR
enables editing of common fields in the new \"Updates\" tab of the rule
upgrade flyout. The common fields are fields applicable to all rule
types.\r\n\r\n## Summary\r\nThese fields are editable now:\r\n -
`building_block`\r\n - `description`\r\n - `false_positives`\r\n -
`investigation_fields`\r\n - `max_signals`\r\n - `note`\r\n -
`references`\r\n - `related_integrations`\r\n - `required_fields`\r\n -
`risk_score`\r\n - `risk_score_mapping`\r\n - `rule_name_override`\r\n -
`rule_schedule`\r\n - `setup`\r\n - `severity`\r\n -
`severity_mapping`\r\n - `tags`\r\n - `threat`\r\n -
`timeline_template`\r\n - `timestamp_override`\r\n\r\n<img
width=\"2672\" alt=\"Scherm­afbeelding 2024-10-16 om 17 32 06\"
src=\"https://github.com/user-attachments/assets/6dd615e2-6e84-4e1f-b674-f42d03f575e7\">\r\n\r\n###
Testing\r\n - Ensure the `prebuiltRulesCustomizationEnabled` feature
flag is enabled.\r\n - To simulate the availability of prebuilt rule
upgrades, downgrade a currently installed prebuilt rule using the `PATCH
api/detection_engine/rules` API. \r\n - Set `version: 1` in the request
body to downgrade it to version 1.\r\n - Modify other rule fields in the
request body as needed to test the
changes.","sha":"3d3b32faf6992f95805a37230e7e7e552e19a801","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Detections
and Resp","Team: SecuritySolution","Team:Detection Rule
Management","Feature:Prebuilt Detection
Rules","backport:prev-minor"],"title":"[Security Solution] `FinalEdit`:
Add fields that are common for all rule
types","number":196642,"url":"https://github.com/elastic/kibana/pull/196642","mergeCommit":{"message":"[Security
Solution] `FinalEdit`: Add fields that are common for all rule types
(#196642)\n\n**Partially addresses:
https://github.com/elastic/kibana/issues/171520**\r\n**Is a follow-up
to: https://github.com/elastic/kibana/pull/196326**\r\n\r\nThis PR
enables editing of common fields in the new \"Updates\" tab of the rule
upgrade flyout. The common fields are fields applicable to all rule
types.\r\n\r\n## Summary\r\nThese fields are editable now:\r\n -
`building_block`\r\n - `description`\r\n - `false_positives`\r\n -
`investigation_fields`\r\n - `max_signals`\r\n - `note`\r\n -
`references`\r\n - `related_integrations`\r\n - `required_fields`\r\n -
`risk_score`\r\n - `risk_score_mapping`\r\n - `rule_name_override`\r\n -
`rule_schedule`\r\n - `setup`\r\n - `severity`\r\n -
`severity_mapping`\r\n - `tags`\r\n - `threat`\r\n -
`timeline_template`\r\n - `timestamp_override`\r\n\r\n<img
width=\"2672\" alt=\"Scherm­afbeelding 2024-10-16 om 17 32 06\"
src=\"https://github.com/user-attachments/assets/6dd615e2-6e84-4e1f-b674-f42d03f575e7\">\r\n\r\n###
Testing\r\n - Ensure the `prebuiltRulesCustomizationEnabled` feature
flag is enabled.\r\n - To simulate the availability of prebuilt rule
upgrades, downgrade a currently installed prebuilt rule using the `PATCH
api/detection_engine/rules` API. \r\n - Set `version: 1` in the request
body to downgrade it to version 1.\r\n - Modify other rule fields in the
request body as needed to test the
changes.","sha":"3d3b32faf6992f95805a37230e7e7e552e19a801"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196642","number":196642,"mergeCommit":{"message":"[Security
Solution] `FinalEdit`: Add fields that are common for all rule types
(#196642)\n\n**Partially addresses:
https://github.com/elastic/kibana/issues/171520**\r\n**Is a follow-up
to: https://github.com/elastic/kibana/pull/196326**\r\n\r\nThis PR
enables editing of common fields in the new \"Updates\" tab of the rule
upgrade flyout. The common fields are fields applicable to all rule
types.\r\n\r\n## Summary\r\nThese fields are editable now:\r\n -
`building_block`\r\n - `description`\r\n - `false_positives`\r\n -
`investigation_fields`\r\n - `max_signals`\r\n - `note`\r\n -
`references`\r\n - `related_integrations`\r\n - `required_fields`\r\n -
`risk_score`\r\n - `risk_score_mapping`\r\n - `rule_name_override`\r\n -
`rule_schedule`\r\n - `setup`\r\n - `severity`\r\n -
`severity_mapping`\r\n - `tags`\r\n - `threat`\r\n -
`timeline_template`\r\n - `timestamp_override`\r\n\r\n<img
width=\"2672\" alt=\"Scherm­afbeelding 2024-10-16 om 17 32 06\"
src=\"https://github.com/user-attachments/assets/6dd615e2-6e84-4e1f-b674-f42d03f575e7\">\r\n\r\n###
Testing\r\n - Ensure the `prebuiltRulesCustomizationEnabled` feature
flag is enabled.\r\n - To simulate the availability of prebuilt rule
upgrades, downgrade a currently installed prebuilt rule using the `PATCH
api/detection_engine/rules` API. \r\n - Set `version: 1` in the request
body to downgrade it to version 1.\r\n - Modify other rule fields in the
request body as needed to test the
changes.","sha":"3d3b32faf6992f95805a37230e7e7e552e19a801"}}]}]
BACKPORT-->

Co-authored-by: Nikita Indik <nikita.indik@elastic.co>
This commit is contained in:
Kibana Machine 2024-11-12 22:51:58 +11:00 committed by GitHub
parent 04a6dd94a6
commit bb3e3c7693
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 2248 additions and 534 deletions

View file

@ -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';

View file

@ -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(
<FieldComponent
<EsFieldSelector
isClearable={false}
isDisabled={false}
isLoading={false}
@ -38,7 +38,7 @@ describe('FieldComponent', () => {
});
it('should render the component disabled if isDisabled is true', () => {
const wrapper = render(
<FieldComponent
<EsFieldSelector
isClearable={false}
isDisabled={true}
isLoading={false}
@ -57,7 +57,7 @@ describe('FieldComponent', () => {
});
it('should render the loading spinner if isLoading is true when clicked', () => {
const wrapper = render(
<FieldComponent
<EsFieldSelector
isClearable={false}
isDisabled={true}
isLoading={true}
@ -78,7 +78,7 @@ describe('FieldComponent', () => {
});
it('should allow user to clear values if isClearable is true', () => {
const wrapper = render(
<FieldComponent
<EsFieldSelector
indexPattern={{
fields,
id: '1234',
@ -97,7 +97,7 @@ describe('FieldComponent', () => {
});
it('should change the selected value', async () => {
const wrapper = render(
<FieldComponent
<EsFieldSelector
isClearable={false}
isDisabled={true}
isLoading={false}
@ -119,7 +119,7 @@ describe('FieldComponent', () => {
it('it allows custom user input if "acceptsCustomOptions" is "true"', async () => {
const mockOnChange = jest.fn();
const wrapper = render(
<FieldComponent
<EsFieldSelector
indexPattern={{
fields,
id: '1234',

View file

@ -16,7 +16,7 @@ import TestRenderer from 'react-test-renderer';
const { act: actTestRenderer } = TestRenderer;
import { fields } from '../../fields/index.mock';
import { useField } from '../use_field';
import { useEsField } from '../use_es_field';
jest.mock('../../translations', () => ({
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();

View file

@ -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<FieldProps> = ({
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<FieldProps> = ({
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<FieldProps> = ({
aria-label={ariaLabel}
/>
);
};
FieldComponent.displayName = 'Field';
}

View file

@ -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[];

View file

@ -115,7 +115,7 @@ const getComboBoxProps = (fields: ComboBoxFields): GetFieldComboBoxPropsReturn =
};
};
export const useField = ({
export const useEsField = ({
indexPattern,
fieldTypeFilter,
isRequired,

View file

@ -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<AutocompleteFieldMatchPro
selectedField,
selectedValue,
indexPattern,
isLoading,
isLoading = false,
isDisabled = false,
isClearable = false,
isRequired = false,

View file

@ -48,7 +48,7 @@ import {
AutocompleteFieldMatchAnyComponent,
AutocompleteFieldMatchComponent,
AutocompleteFieldWildcardComponent,
FieldComponent,
EsFieldSelector,
OperatorComponent,
} from '@kbn/securitysolution-autocomplete';
import {
@ -207,7 +207,7 @@ export const BuilderEntryItem: React.FC<EntryItemProps> = ({
(isFirst: boolean): JSX.Element => {
const filteredIndexPatterns = getFilteredIndexPatterns(indexPattern, entry);
const comboBox = (
<FieldComponent
<EsFieldSelector
placeholder={
entry.nested != null
? i18n.EXCEPTION_FIELD_NESTED_PLACEHOLDER

View file

@ -9,7 +9,7 @@ import React, { useCallback, useMemo } from 'react';
import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { FieldComponent } from '@kbn/securitysolution-autocomplete';
import { EsFieldSelector } from '@kbn/securitysolution-autocomplete';
import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query';
import type { FormattedEntry, Entry } from './types';
import * as i18n from './translations';
@ -57,7 +57,7 @@ export const EntryItem: React.FC<EntryItemProps> = ({
const renderFieldInput = useMemo(() => {
const comboBox = (
<FieldComponent
<EsFieldSelector
placeholder={i18n.FIELD_PLACEHOLDER}
indexPattern={indexPattern}
selectedField={entry.field}
@ -87,7 +87,7 @@ export const EntryItem: React.FC<EntryItemProps> = ({
const renderThreatFieldInput = useMemo(() => {
const comboBox = (
<FieldComponent
<EsFieldSelector
placeholder={i18n.FIELD_PLACEHOLDER}
indexPattern={threatIndexPatterns}
selectedField={entry.value}

View file

@ -6,8 +6,9 @@
*/
import type { RequiredFieldInput } from '../../../../../common/api/detection_engine/model/rule_schema/common_attributes.gen';
import type { ERROR_CODE, FormData, ValidationFunc } from '../../../../shared_imports';
import type { ERROR_CODE, FormData, FormHook, ValidationFunc } from '../../../../shared_imports';
import * as i18n from './translations';
import { getFlattenedArrayFieldNames } from './utils';
export function makeValidateRequiredField(parentFieldPath: string) {
return function validateRequiredField(
@ -15,11 +16,10 @@ export function makeValidateRequiredField(parentFieldPath: string) {
): ReturnType<ValidationFunc<{}, ERROR_CODE>> | 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[];
}

View file

@ -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.

View file

@ -44,14 +44,6 @@ export const RequiredFieldRow = ({
const rowFieldConfig: FieldConfig<RequiredField | RequiredFieldInput, {}, RequiredFieldInput> =
useMemo(
() => ({
deserializer: (value) => {
const rowValueWithoutEcs: RequiredFieldInput = {
name: value.name,
type: value.type,
};
return rowValueWithoutEcs;
},
validations: [{ validator: makeValidateRequiredField(parentFieldPath) }],
defaultValue: { name: '', type: '' },
}),

View file

@ -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);
}

View file

@ -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<string>;
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 (
<EuiFormRow
data-test-subj={dataTestSubj}
describedByIds={idAria ? [idAria] : undefined}
describedByIds={describedByIds}
fullWidth
helpText={field.helpText}
label={field.label}
labelAppend={field.labelAppend}
>
<FieldComponent
<EsFieldSelector
placeholder={placeholder ?? ''}
indexPattern={indices}
selectedField={selectedField}

View file

@ -8,11 +8,15 @@
import React, { useMemo, useCallback } from 'react';
import type { EuiFieldNumberProps } from '@elastic/eui';
import { EuiTextColor, EuiFormRow, EuiFieldNumber, EuiIcon } from '@elastic/eui';
import type { FieldHook } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import {
getFieldValidityAndErrorMessage,
type FieldHook,
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { css } from '@emotion/css';
import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants';
import * as i18n from './translations';
import { useKibana } from '../../../../common/lib/kibana';
import { MIN_VALUE } from '../../validators/max_signals_validator_factory';
interface MaxSignalsFieldProps {
dataTestSubj: string;
@ -35,12 +39,7 @@ export const MaxSignals: React.FC<MaxSignalsFieldProps> = ({
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<MaxSignalsFieldProps> = ({
return textToRender;
}, [hasWarning, maxAlertsPerRun]);
const describedByIds = useMemo(() => (idAria ? [idAria] : undefined), [idAria]);
return (
<EuiFormRow
css={css`
@ -74,13 +75,13 @@ export const MaxSignals: React.FC<MaxSignalsFieldProps> = ({
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}
>
<EuiFieldNumber
isInvalid={isInvalid}
@ -91,6 +92,7 @@ export const MaxSignals: React.FC<MaxSignalsFieldProps> = ({
data-test-subj={dataTestSubj}
disabled={isDisabled}
append={hasWarning ? <EuiIcon size="s" type="warning" color="warning" /> : undefined}
min={MIN_VALUE}
/>
</EuiFormRow>
);

View file

@ -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',

View file

@ -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<NonNullable<EuiRangeProps['onChange']>>(
(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 (
<EuiFlexItem>
<EuiFormRow
label={<DefaultRiskScoreLabel />}
error={errorMessage}
isInvalid={!!errorMessage}
fullWidth
data-test-subj={`${dataTestSubj}-defaultRisk`}
describedByIds={idAria ? [idAria] : undefined}
>
<EuiRange
value={value}
onChange={handleChange}
max={MAX_RISK_SCORE}
min={MIN_RISK_SCORE}
showRange
showInput
fullWidth={false}
showTicks
tickInterval={25}
data-test-subj={`${dataTestSubj}-defaultRiskRange`}
/>
</EuiFormRow>
</EuiFlexItem>
);
}
function DefaultRiskScoreLabel() {
return (
<div>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>{i18n.DEFAULT_RISK_SCORE}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<EuiText size={'xs'}>{i18n.RISK_SCORE_DESCRIPTION}</EuiText>
</div>
);
}

View file

@ -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<NonNullable<EuiRangeProps['onChange']>>(
(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 (
<div>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>{i18n.DEFAULT_RISK_SCORE}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<EuiText size={'xs'}>{i18n.RISK_SCORE_DESCRIPTION}</EuiText>
</div>
);
}, []);
const riskScoreMappingLabel = useMemo(() => {
return (
<div>
<EuiFlexGroup
alignItems="center"
gutterSize="s"
onClick={!isDisabled ? handleRiskScoreMappingChecked : noop}
>
<EuiFlexItem grow={false}>
<EuiCheckbox
id={`risk_score-mapping-override`}
checked={isMappingChecked}
disabled={isDisabled}
onChange={handleRiskScoreMappingChecked}
/>
</EuiFlexItem>
<EuiFlexItem>{i18n.RISK_SCORE_MAPPING}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<NestedContent>
<EuiText size={'xs'}>{i18n.RISK_SCORE_MAPPING_DESCRIPTION}</EuiText>
</NestedContent>
</div>
);
}, [isMappingChecked, handleRiskScoreMappingChecked, isDisabled]);
const errorMessage = getFieldValidityAndErrorMessage(field).errorMessage ?? undefined;
return (
<EuiFlexGroup direction={'column'}>
<DefaultRiskScore
dataTestSubj={dataTestSubj}
idAria={idAria}
onChange={handleDefaultRiskScoreChange}
value={value}
errorMessage={errorMessage}
/>
<EuiFlexItem>
<EuiFormRow
label={riskScoreLabel}
labelAppend={field.labelAppend}
helpText={field.helpText}
error={'errorMessage'}
isInvalid={false}
fullWidth
data-test-subj={`${dataTestSubj}-defaultRisk`}
describedByIds={idAria ? [idAria] : undefined}
>
<EuiRange
value={value}
onChange={handleDefaultRiskScoreChange}
max={100}
min={0}
showRange
showInput
fullWidth={false}
showTicks
tickInterval={25}
data-test-subj={`${dataTestSubj}-defaultRiskRange`}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={riskScoreMappingLabel}
labelAppend={field.labelAppend}
helpText={
isMappingChecked ? <NestedContent>{i18n.RISK_SCORE_MAPPING_DETAILS}</NestedContent> : ''
}
error={'errorMessage'}
isInvalid={false}
fullWidth
data-test-subj={`${dataTestSubj}-riskOverride`}
describedByIds={idAria ? [idAria] : undefined}
>
<NestedContent>
<EuiSpacer size="s" />
{isMappingChecked && (
<EuiFlexGroup direction={'column'} gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItemComboBoxColumn>
<EuiFormLabel>{i18n.SOURCE_FIELD}</EuiFormLabel>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemIconColumn grow={false} />
<EuiFlexItemRiskScoreColumn grow={false}>
<EuiFormLabel>{i18n.DEFAULT_RISK_SCORE}</EuiFormLabel>
</EuiFlexItemRiskScoreColumn>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItemComboBoxColumn>
<FieldComponent
placeholder={placeholder ?? ''}
indexPattern={indices}
selectedField={selectedField}
fieldTypeFilter={fieldTypeFilter}
isLoading={false}
isClearable={false}
isDisabled={isDisabled}
onChange={handleRiskScoreMappingChange}
data-test-subj={dataTestSubj}
aria-label={idAria}
/>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemIconColumn grow={false}>
<EuiIcon type={'sortRight'} />
</EuiFlexItemIconColumn>
<EuiFlexItemRiskScoreColumn grow={false}>
<EuiText size={'s'}>{i18n.RISK_SCORE_FIELD}</EuiText>
</EuiFlexItemRiskScoreColumn>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
)}
</NestedContent>
</EuiFormRow>
<RiskScoreOverride
isMappingChecked={isMappingChecked}
onToggleMappingChecked={handleRiskScoreMappingChecked}
onMappingChange={handleRiskScoreMappingChange}
dataTestSubj={dataTestSubj}
idAria={idAria}
mapping={mapping}
indices={indices}
isDisabled={isDisabled}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
/**
* 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' };
};

View file

@ -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 (
<div>
<EuiFlexGroup
alignItems="center"
gutterSize="s"
onClick={!isDisabled ? onToggleMappingChecked : noop}
>
<EuiFlexItem grow={false}>
<EuiCheckbox
id="risk_score-mapping-override"
checked={isMappingChecked}
onChange={onToggleMappingChecked}
/>
</EuiFlexItem>
<EuiFlexItem>{i18n.RISK_SCORE_MAPPING}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<NestedContent>
<EuiText size="xs">{i18n.RISK_SCORE_MAPPING_DESCRIPTION}</EuiText>
</NestedContent>
</div>
);
}, [isDisabled, isMappingChecked, onToggleMappingChecked]);
const describedByIds = useMemo(() => (idAria ? [idAria] : undefined), [idAria]);
const selectedField = useMemo(() => getFieldTypeByMapping(mapping, indices), [mapping, indices]);
return (
<EuiFormRow
label={riskScoreMappingLabel}
helpText={
isMappingChecked ? <NestedContent>{i18n.RISK_SCORE_MAPPING_DETAILS}</NestedContent> : ''
}
fullWidth
data-test-subj={`${dataTestSubj}-riskOverride`}
describedByIds={describedByIds}
>
<NestedContent>
<EuiSpacer size="s" />
{isMappingChecked && (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItemComboBoxColumn>
<EuiFormLabel>{i18n.SOURCE_FIELD}</EuiFormLabel>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemIconColumn grow={false} />
<EuiFlexItemRiskScoreColumn grow={false}>
<EuiFormLabel>{i18n.DEFAULT_RISK_SCORE}</EuiFormLabel>
</EuiFlexItemRiskScoreColumn>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItemComboBoxColumn>
<EsFieldSelector
placeholder=""
indexPattern={indices}
selectedField={selectedField}
fieldTypeFilter={fieldTypeFilter}
isDisabled={isDisabled}
onChange={onMappingChange}
data-test-subj={dataTestSubj}
aria-label={idAria}
/>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemIconColumn grow={false}>
<EuiIcon type="sortRight" />
</EuiFlexItemIconColumn>
<EuiFlexItemRiskScoreColumn grow={false}>
<EuiText size="s">{i18n.RISK_SCORE_FIELD}</EuiText>
</EuiFlexItemRiskScoreColumn>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
)}
</NestedContent>
</EuiFormRow>
);
}
/**
* 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' };
};

View file

@ -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 (
<EuiFlexItem>
<EuiFormRow
label={<DefaultSeverityLabel />}
fullWidth
data-test-subj="detectionEngineStepAboutRuleSeverity"
describedByIds={describedByIds}
>
<EuiSuperSelect
fullWidth={false}
disabled={false}
valueOfSelected={value}
onChange={onChange}
options={severityOptions}
data-test-subj="select"
/>
</EuiFormRow>
</EuiFlexItem>
);
}
function DefaultSeverityLabel() {
return (
<div>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>{i18n.SEVERITY}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<EuiText size={'xs'}>{i18n.SEVERITY_DESCRIPTION}</EuiText>
</div>
);
}

View file

@ -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 (
<div>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>{i18n.SEVERITY}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<EuiText size={'xs'}>{i18n.SEVERITY_DESCRIPTION}</EuiText>
</div>
);
}, []);
const severityMappingLabel = useMemo(() => {
return (
<div>
<EuiFlexGroup
alignItems="center"
gutterSize="s"
onClick={!isDisabled ? handleSeverityMappingChecked : noop}
>
<EuiFlexItem grow={false}>
<EuiCheckbox
id={`severity-mapping-override`}
checked={isMappingChecked}
disabled={isDisabled}
onChange={handleSeverityMappingChecked}
/>
</EuiFlexItem>
<EuiFlexItem>{i18n.SEVERITY_MAPPING}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<NestedContent>
<EuiText size={'xs'}>{i18n.SEVERITY_MAPPING_DESCRIPTION}</EuiText>
</NestedContent>
</div>
);
}, [handleSeverityMappingChecked, isDisabled, isMappingChecked]);
return (
<EuiFlexGroup direction={'column'}>
<DefaultSeverity value={value} onChange={handleDefaultSeverityChange} />
<EuiFlexItem>
<EuiFormRow
label={severityLabel}
labelAppend={field.labelAppend}
helpText={field.helpText}
error={'errorMessage'}
isInvalid={false}
fullWidth
data-test-subj="detectionEngineStepAboutRuleSeverity"
describedByIds={['detectionEngineStepAboutRuleSeverity']}
>
<EuiSuperSelect
fullWidth={false}
disabled={false}
valueOfSelected={value}
onChange={handleDefaultSeverityChange}
options={options}
data-test-subj="select"
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={severityMappingLabel}
labelAppend={field.labelAppend}
helpText={
isMappingChecked ? <NestedContent>{i18n.SEVERITY_MAPPING_DETAILS}</NestedContent> : ''
}
error={'errorMessage'}
isInvalid={false}
fullWidth
data-test-subj={`${dataTestSubj}-severityOverride`}
describedByIds={idAria ? [idAria] : undefined}
>
<NestedContent>
<EuiSpacer size="s" />
{isMappingChecked && (
<EuiFlexGroup direction={'column'} gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItemComboBoxColumn>
<EuiFormLabel>{i18n.SOURCE_FIELD}</EuiFormLabel>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemComboBoxColumn>
<EuiFormLabel>{i18n.SOURCE_VALUE}</EuiFormLabel>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemIconColumn grow={false} />
<EuiFlexItemSeverityColumn grow={false}>
<EuiFormLabel>{i18n.DEFAULT_SEVERITY}</EuiFormLabel>
</EuiFlexItemSeverityColumn>
</EuiFlexGroup>
</EuiFlexItem>
{mapping.map((severityMappingItem: SeverityMappingItem, index) => (
<EuiFlexItem key={`${severityMappingItem.severity}-${index}`}>
<EuiFlexGroup
data-test-subj="severityOverrideRow"
alignItems="center"
gutterSize="s"
>
<EuiFlexItemComboBoxColumn>
<FieldComponent
placeholder={''}
selectedField={getFieldTypeByMapping(severityMappingItem, indices)}
isLoading={false}
isDisabled={isDisabled}
isClearable={false}
indexPattern={indices}
onChange={handleFieldChange.bind(
null,
index,
severityMappingItem.severity
)}
data-test-subj={`detectionEngineStepAboutRuleSeverityMappingField-${severityMappingItem.severity}-${index}`}
aria-label={`detectionEngineStepAboutRuleSeverityMappingField-${severityMappingItem.severity}-${index}`}
/>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemComboBoxColumn>
<AutocompleteFieldMatchComponent
autocompleteService={services.unifiedSearch.autocomplete}
placeholder={''}
selectedField={getFieldTypeByMapping(severityMappingItem, indices)}
selectedValue={severityMappingItem.value}
isClearable={false}
isDisabled={isDisabled}
isLoading={false}
indexPattern={indices}
onChange={handleFieldMatchValueChange.bind(
null,
index,
severityMappingItem.severity
)}
data-test-subj={`detectionEngineStepAboutRuleSeverityMappingValue-${severityMappingItem.severity}-${index}`}
aria-label={`detectionEngineStepAboutRuleSeverityMappingValue-${severityMappingItem.severity}-${index}`}
/>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemIconColumn grow={false}>
<EuiIcon type={'sortRight'} />
</EuiFlexItemIconColumn>
<EuiFlexItemSeverityColumn grow={false}>
{
options.find((o) => o.value === severityMappingItem.severity)
?.inputDisplay
}
</EuiFlexItemSeverityColumn>
</EuiFlexGroup>
</EuiFlexItem>
))}
</EuiFlexGroup>
)}
</NestedContent>
</EuiFormRow>
<SeverityOverride
isDisabled={isDisabled}
onSeverityMappingChecked={handleSeverityMappingChecked}
onFieldChange={handleFieldChange}
onFieldMatchValueChange={handleFieldMatchValueChange}
isMappingChecked={isMappingChecked}
dataTestSubj={dataTestSubj}
idAria={idAria}
mapping={mapping}
indices={indices}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};
/**
* 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' };
};

View file

@ -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 (
<div>
<EuiFlexGroup
alignItems="center"
gutterSize="s"
onClick={isDisabled ? undefined : onSeverityMappingChecked}
>
<EuiFlexItem grow={false}>
<EuiCheckbox
id="severity-mapping-override"
checked={isMappingChecked}
disabled={isDisabled}
onChange={onSeverityMappingChecked}
/>
</EuiFlexItem>
<EuiFlexItem>{i18n.SEVERITY_MAPPING}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<NestedContent>
<EuiText size="xs">{i18n.SEVERITY_MAPPING_DESCRIPTION}</EuiText>
</NestedContent>
</div>
);
}, [onSeverityMappingChecked, isDisabled, isMappingChecked]);
const describedByIds = useMemo(() => (idAria ? [idAria] : undefined), [idAria]);
return (
<EuiFormRow
label={severityMappingLabel}
helpText={
isMappingChecked ? <NestedContent>{i18n.SEVERITY_MAPPING_DETAILS}</NestedContent> : ''
}
fullWidth
data-test-subj={`${dataTestSubj}-severityOverride`}
describedByIds={describedByIds}
>
<NestedContent>
<EuiSpacer size="s" />
{isMappingChecked && (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItemComboBoxColumn>
<EuiFormLabel>{i18n.SOURCE_FIELD}</EuiFormLabel>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemComboBoxColumn>
<EuiFormLabel>{i18n.SOURCE_VALUE}</EuiFormLabel>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemIconColumn />
<EuiFlexItemSeverityColumn>
<EuiFormLabel>{i18n.DEFAULT_SEVERITY}</EuiFormLabel>
</EuiFlexItemSeverityColumn>
</EuiFlexGroup>
</EuiFlexItem>
{mapping.map((severityMappingItem, index) => (
<SeverityMappingRow
key={index}
severityMappingItem={severityMappingItem}
index={index}
indices={indices}
isDisabled={isDisabled}
onFieldChange={onFieldChange}
onFieldMatchValueChange={onFieldMatchValueChange}
/>
))}
</EuiFlexGroup>
)}
</NestedContent>
</EuiFormRow>
);
}
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 (
<EuiFlexItem key={`${severityMappingItem.severity}-${index}`}>
<EuiFlexGroup data-test-subj="severityOverrideRow" alignItems="center" gutterSize="s">
<EuiFlexItemComboBoxColumn>
<EsFieldSelector
placeholder=""
selectedField={getFieldTypeByMapping(severityMappingItem, indices)}
isDisabled={isDisabled}
indexPattern={indices}
onChange={handleFieldChange}
aria-label={i18n.SOURCE_FIELD}
/>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemComboBoxColumn>
<AutocompleteFieldMatchComponent
autocompleteService={services.unifiedSearch.autocomplete}
placeholder=""
selectedField={getFieldTypeByMapping(severityMappingItem, indices)}
selectedValue={severityMappingItem.value}
isClearable={true}
isDisabled={isDisabled}
indexPattern={indices}
onChange={handleFieldMatchValueChange}
aria-label={i18n.SOURCE_VALUE}
/>
</EuiFlexItemComboBoxColumn>
<EuiFlexItemIconColumn>
<EuiIcon type="sortRight" />
</EuiFlexItemIconColumn>
<EuiFlexItemSeverityColumn>
{severityOptions.find((o) => o.value === severityMappingItem.severity)?.inputDisplay}
</EuiFlexItemSeverityColumn>
</EuiFlexGroup>
</EuiFlexItem>
);
}
const NestedContent: React.FC<React.PropsWithChildren> = ({ children }) => (
<div className={styles.nestedContent}>{children}</div>
);
const EuiFlexItemComboBoxColumn: React.FC<React.PropsWithChildren> = ({ children }) => (
<EuiFlexItem className={styles.comboBoxColumn}>{children}</EuiFlexItem>
);
const EuiFlexItemIconColumn: React.FC<React.PropsWithChildren> = ({ children }) => (
<EuiFlexItem className={styles.iconColumn} grow={false}>
{children}
</EuiFlexItem>
);
const EuiFlexItemSeverityColumn: React.FC<React.PropsWithChildren> = ({ children }) => (
<EuiFlexItem className={styles.severityColumn} grow={false}>
{children}
</EuiFlexItem>
);
/**
* 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' };
};

View file

@ -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;
`;

View file

@ -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<StepAboutRuleProps> = ({
dataTestSubj: 'detectionEngineStepAboutRuleSeverityField',
idAria: 'detectionEngineStepAboutRuleSeverityField',
isDisabled: isLoading || indexPatternLoading,
options: severityOptions,
indices: indexPattern,
setRiskScore,
}}
@ -376,14 +375,13 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
) : (
<UseField
path="ruleNameOverride"
component={AutocompleteField}
component={EsFieldSelectorField}
componentProps={{
dataTestSubj: 'detectionEngineStepAboutRuleRuleNameOverride',
fieldType: 'string',
idAria: 'detectionEngineStepAboutRuleRuleNameOverride',
indices: indexPattern,
isDisabled: isLoading || indexPatternLoading,
placeholder: '',
}}
/>
)}
@ -391,14 +389,13 @@ const StepAboutRuleComponent: FC<StepAboutRuleProps> = ({
<EuiSpacer size="l" />
<UseField
path="timestampOverride"
component={AutocompleteField}
component={EsFieldSelectorField}
componentProps={{
dataTestSubj: 'detectionEngineStepAboutRuleTimestampOverride',
fieldType: 'date',
idAria: 'detectionEngineStepAboutRuleTimestampOverride',
indices: indexPattern,
isDisabled: isLoading || indexPatternLoading,
placeholder: '',
}}
/>
{!!timestampOverride && timestampOverride !== '@timestamp' && (

View file

@ -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<AboutStepRule> = {
}
),
labelAppend: OptionalFieldLabel,
validations: [
{
validator: maxSignalsValidatorFactory(),
},
],
},
isAssociatedToEndpointList: {
type: FIELD_TYPES.CHECKBOX,
@ -129,6 +144,18 @@ export const schema: FormSchema<AboutStepRule> = {
value: {},
mapping: {},
isMappingChecked: {},
validations: [
{
validator: (
...args: Parameters<ValidationFunc<{}, ERROR_CODE, AboutStepRiskScore>>
): ValidationError | undefined => {
const [{ value: fieldValue, path }] = args;
const defaultRiskScore = fieldValue.value;
return defaultRiskScoreValidator(defaultRiskScore, path);
},
},
],
},
references: {
label: i18n.translate(

View file

@ -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
);

View file

@ -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

View file

@ -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;
}

View file

@ -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<FormData, ERROR_CODE, unknown> {
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}.',
}
),
});
}

View file

@ -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 (
<RuleFieldEditFormWrapper
component={BuildingBlockEdit}
ruleFieldFormSchema={buildingBlockSchema}
serializer={buildingBlockSerializer}
deserializer={buildingBlockDeserializer}
/>
);
case 'description':
return (
<RuleFieldEditFormWrapper
component={DescriptionEdit}
ruleFieldFormSchema={descriptionSchema}
/>
);
case 'false_positives':
return (
<RuleFieldEditFormWrapper
component={FalsePositivesEdit}
ruleFieldFormSchema={falsePositivesSchema}
serializer={falsePositivesSerializer}
deserializer={falsePositivesDeserializer}
/>
);
case 'investigation_fields':
return (
<RuleFieldEditFormWrapper
component={InvestigationFieldsEdit}
ruleFieldFormSchema={investigationFieldsSchema}
serializer={investigationFieldsSerializer}
deserializer={investigationFieldsDeserializer}
/>
);
case 'max_signals':
return (
<RuleFieldEditFormWrapper
component={MaxSignalsEdit}
ruleFieldFormSchema={maxSignalsSchema}
serializer={maxSignalsSerializer}
deserializer={maxSignalsDeserializer}
/>
);
case 'name':
return <RuleFieldEditFormWrapper component={NameEdit} ruleFieldFormSchema={nameSchema} />;
case 'note':
return <RuleFieldEditFormWrapper component={NoteEdit} ruleFieldFormSchema={noteSchema} />;
case 'references':
return (
<RuleFieldEditFormWrapper
component={ReferencesEdit}
ruleFieldFormSchema={referencesSchema}
serializer={referencesSerializer}
/>
);
case 'related_integrations':
return (
<RuleFieldEditFormWrapper
component={RelatedIntegrationsEdit}
ruleFieldFormSchema={relatedIntegrationsSchema}
serializer={relatedIntegrationsSerializer}
deserializer={relatedIntegrationsDeserializer}
/>
);
case 'required_fields':
return (
<RuleFieldEditFormWrapper
component={RequiredFieldsEdit}
ruleFieldFormSchema={requiredFieldsSchema}
serializer={requiredFieldsSerializer}
deserializer={requiredFieldsDeserializer}
/>
);
case 'risk_score':
return (
<RuleFieldEditFormWrapper
component={RiskScoreEdit}
ruleFieldFormSchema={riskScoreSchema}
serializer={riskScoreSerializer}
deserializer={riskScoreDeserializer}
/>
);
case 'risk_score_mapping':
return (
<RuleFieldEditFormWrapper
component={RiskScoreMappingEdit}
serializer={riskScoreMappingSerializer}
deserializer={riskScoreMappingDeserializer}
/>
);
case 'rule_name_override':
return (
<RuleFieldEditFormWrapper
component={RuleNameOverrideEdit}
ruleFieldFormSchema={ruleNameOverrideSchema}
serializer={ruleNameOverrideSerializer}
deserializer={ruleNameOverrideDeserializer}
/>
);
case 'rule_schedule':
return (
<RuleFieldEditFormWrapper
component={RuleScheduleEdit}
ruleFieldFormSchema={ruleScheduleSchema}
serializer={ruleScheduleSerializer}
deserializer={ruleScheduleDeserializer}
/>
);
case 'setup':
return <RuleFieldEditFormWrapper component={SetupEdit} ruleFieldFormSchema={setupSchema} />;
case 'severity':
return <RuleFieldEditFormWrapper component={SeverityEdit} />;
case 'severity_mapping':
return (
<RuleFieldEditFormWrapper
component={SeverityMappingEdit}
serializer={severityMappingSerializer}
deserializer={severityMappingDeserializer}
/>
);
case 'tags':
return <RuleFieldEditFormWrapper component={TagsEdit} ruleFieldFormSchema={tagsSchema} />;
case 'timeline_template':
return (
<RuleFieldEditFormWrapper
component={TimelineTemplateEdit}
ruleFieldFormSchema={timelineTemplateSchema}
serializer={timelineTemplateSerializer}
deserializer={timelineTemplateDeserializer}
/>
);
case 'timestamp_override':
return (
<RuleFieldEditFormWrapper
component={TimestampOverrideEdit}
ruleFieldFormSchema={timestampOverrideSchema}
serializer={timestampOverrideSerializer}
deserializer={timestampOverrideDeserializer}
/>
);
case 'threat':
return (
<RuleFieldEditFormWrapper
component={ThreatEdit}
ruleFieldFormSchema={threatSchema}
serializer={threatSerializer}
/>
);
default:
return null; // Will be replaced with `assertUnreachable(fieldName)` once all fields are implemented
}

View file

@ -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 <UseField path="isBuildingBlock" component={Field} />;
}
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 };
}

View file

@ -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 <UseField path="description" component={Field} componentProps={componentProps} />;
}

View file

@ -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 <UseField path="falsePositives" component={AddItem} componentProps={componentProps} />;
}
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)),
};
}

View file

@ -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 (
<UseField
path="investigationFields"
component={MultiSelectFieldsAutocomplete}
componentProps={{
browserFields: investigationFields,
isDisabled: isIndexPatternLoading || isInvestigationFieldsLoading,
fullWidth: true,
}}
/>
);
}
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,
};
}

View file

@ -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 <UseField path="maxSignals" component={MaxSignals} componentProps={componentProps} />;
}
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,
};
}

View file

@ -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 (
<UseField
path="name"
component={Field}
componentProps={{
euiFieldProps: {
fullWidth: true,
},
}}
/>
);
return <UseField path="name" component={Field} componentProps={componentProps} />;
}

View file

@ -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 <UseField path="note" component={MarkdownEditorForm} componentProps={componentProps} />;
}

View file

@ -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 <UseField path="references" component={AddItem} componentProps={componentProps} />;
}
export function referencesSerializer(formData: FormData): {
references: RuleReferenceArray;
} {
return {
/* Remove empty items from the references array */
references: compact(formData.references),
};
}

View file

@ -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 <RelatedIntegrations path="relatedIntegrations" />;
}
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),
};
}

View file

@ -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 (
<RequiredFields
path="requiredFields"
indexPatternFields={indexPattern.fields}
isIndexPatternLoading={isIndexPatternLoading}
/>
);
}
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),
};
}

View file

@ -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 <UseField path="riskScore" component={RiskScoreEditField} />;
}
interface RiskScoreEditFieldProps {
field: FieldHook<RiskScore>;
}
function RiskScoreEditField({ field }: RiskScoreEditFieldProps) {
const { value, setValue } = field;
const errorMessage = getFieldValidityAndErrorMessage(field).errorMessage ?? undefined;
return <DefaultRiskScore value={value} onChange={setValue} errorMessage={errorMessage} />;
}
export function riskScoreDeserializer(defaultValue: FormData) {
return {
riskScore: defaultValue.risk_score,
};
}
export function riskScoreSerializer(formData: FormData): {
risk_score: RiskScore;
} {
return {
risk_score: formData.riskScore,
};
}

View file

@ -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 (
<UseField
path="riskScoreMapping"
component={RiskScoreMappingField}
componentProps={{
finalDiffableRule,
}}
/>
);
}
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 (
<RiskScoreOverride
isMappingChecked={value.isMappingChecked}
mapping={value.mapping}
onToggleMappingChecked={handleToggleMappingChecked}
onMappingChange={handleMappingChange}
indices={indexPattern}
isDisabled={isIndexPatternLoading}
/>
);
}
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)
: [],
};
}

View file

@ -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.

View file

@ -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 (
<UseField
path="ruleNameOverride"
component={EsFieldSelectorField}
componentProps={componentProps}
/>
);
}
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,
};
}

View file

@ -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 (
<>
<UseField path="interval" component={ScheduleItem} componentProps={componentProps} />
<UseField path="from" component={ScheduleItem} componentProps={componentProps} />
</>
);
}
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,
},
};
}

View file

@ -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 <UseField path="setup" component={MarkdownEditorForm} componentProps={componentProps} />;
}

View file

@ -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 <UseField path="severity" component={SeverityEditField} />;
}
interface SeverityEditFieldProps {
field: FieldHook<Severity>;
}
function SeverityEditField({ field }: SeverityEditFieldProps) {
const { value, setValue } = field;
return <DefaultSeverity value={value} onChange={setValue} />;
}

View file

@ -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 (
<UseField
path="severityMapping"
component={SeverityMappingField}
componentProps={{
finalDiffableRule,
}}
/>
);
}
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 (
<SeverityOverride
isDisabled={isIndexPatternLoading}
onSeverityMappingChecked={handleSeverityMappingChecked}
onFieldChange={handleFieldChange}
onFieldMatchValueChange={handleFieldMatchValueChange}
isMappingChecked={value.isMappingChecked}
mapping={value.mapping}
indices={indexPattern}
/>
);
}
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)
: [],
};
}

View file

@ -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 <UseField path="tags" component={Field} componentProps={componentProps} />;
}

View file

@ -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 <UseField path="threat" component={AddMitreAttackThreat} />;
}
export function threatSerializer(formData: FormData): {
threat: ThreatArray;
} {
return {
threat: filterEmptyThreats(formData.threat).map((singleThreat) => ({
...singleThreat,
framework: 'MITRE ATT&CK',
})),
};
}

View file

@ -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 <UseField path="timeline" component={PickTimeline} />;
}
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,
},
};
}

View file

@ -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 (
<>
<UseField
path="timestampOverride"
component={EsFieldSelectorField}
componentProps={componentProps}
/>
<TimestampFallbackDisabled />
</>
);
}
function TimestampFallbackDisabled() {
const [formData] = useFormData();
const { timestampOverride } = formData;
if (timestampOverride && timestampOverride !== '@timestamp') {
return <UseField path="timestampOverrideFallbackDisabled" component={Field} />;
}
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,
},
};
}

View file

@ -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,
};
}

View file

@ -45,7 +45,7 @@ export function CommonRuleFieldReadOnly({
}: CommonRuleFieldReadOnlyProps) {
switch (fieldName) {
case 'building_block':
return <BuildingBlockReadOnly />;
return <BuildingBlockReadOnly buildingBlock={finalDiffableRule.building_block} />;
case 'description':
return <DescriptionReadOnly description={finalDiffableRule.description} />;
case 'investigation_fields':

View file

@ -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 (
<EuiDescriptionList
listItems={[

View file

@ -16,6 +16,10 @@ interface FalsePositivesReadOnlyProps {
}
export function FalsePositivesReadOnly({ falsePositives }: FalsePositivesReadOnlyProps) {
if (falsePositives.length === 0) {
return null;
}
return (
<EuiDescriptionList
listItems={[

View file

@ -16,6 +16,10 @@ interface NoteReadOnlyProps {
}
export function NoteReadOnly({ note }: NoteReadOnlyProps) {
if (!note) {
return null;
}
return (
<EuiDescriptionList
listItems={[

View file

@ -16,6 +16,10 @@ interface ReferencesReadOnlyProps {
}
export function ReferencesReadOnly({ references }: ReferencesReadOnlyProps) {
if (references.length === 0) {
return null;
}
return (
<EuiDescriptionList
listItems={[

View file

@ -16,6 +16,10 @@ interface RelatedIntegrationsReadOnly {
}
export function RelatedIntegrationsReadOnly({ relatedIntegrations }: RelatedIntegrationsReadOnly) {
if (!relatedIntegrations.length) {
return null;
}
return (
<EuiDescriptionList
listItems={[

View file

@ -16,6 +16,10 @@ interface SetupReadOnlyProps {
}
export function SetupReadOnly({ setup }: SetupReadOnlyProps) {
if (!setup) {
return null;
}
return (
<EuiDescriptionList
listItems={[

View file

@ -16,6 +16,10 @@ interface TagsReadOnlyProps {
}
export function TagsReadOnly({ tags }: TagsReadOnlyProps) {
if (tags.length === 0) {
return null;
}
return (
<EuiDescriptionList
listItems={[

View file

@ -16,6 +16,10 @@ export interface ThreatReadOnlyProps {
}
export const ThreatReadOnly = ({ threat }: ThreatReadOnlyProps) => {
if (threat.length === 0) {
return null;
}
return (
<EuiDescriptionList
listItems={[

View file

@ -8,7 +8,7 @@
import type { Type } from '@kbn/securitysolution-io-ts-alerting-types';
import { isThreatMatchRule } from '../../../../../common/detection_engine/utils';
import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations';
import { DEFAULT_THREAT_MATCH_QUERY } from '../../../../../common/constants';
import { DEFAULT_MAX_SIGNALS, DEFAULT_THREAT_MATCH_QUERY } from '../../../../../common/constants';
import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../common/detection_engine/constants';
import type { AboutStepRule, DefineStepRule, RuleStepsOrder, ScheduleStepRule } from './types';
import { DataSourceType, GroupByOptions, RuleStep } from './types';
@ -96,6 +96,7 @@ export const stepAboutDefaultValue: AboutStepRule = {
setup: '',
threatIndicatorPath: undefined,
timestampOverrideFallbackDisabled: undefined,
maxSignals: DEFAULT_MAX_SIGNALS,
};
const DEFAULT_INTERVAL = '5m';

View file

@ -38158,7 +38158,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimestampOverrideLabel": "Remplacement de l'horodatage",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideHelpText": "Fournissez des informations utiles aux analystes qui étudient les alertes de détection. Ce guide s'affichera sur la page de détails de la règle et dans les chronologies (sous forme de notes) créés à partir des alertes de détection générées par cette règle.",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "Guide d'investigation",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldGreaterThanError": "Le nombre maximum d'alertes doit être supérieur à 0.",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibana ne permet qu'un maximum de {maxNumber} {maxNumber, plural, =1 {alerte} other {alertes}} par exécution de règle.",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "Nom obligatoire.",
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "Ajouter un guide d'investigation sur les règles...",

View file

@ -38124,7 +38124,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimestampOverrideLabel": "タイムスタンプ無効化",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideHelpText": "検出アラートの調査を実施するアナリストに役立つ情報を提供します。このガイドは、ルールの詳細ページとこのルールで生成された検出アラートから(メモとして)作成されたタイムラインに表示されます。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "調査ガイド",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldGreaterThanError": "最大アラート数は0よりも大きい値でなければなりません。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibanaで許可される最大数は、1回の実行につき、{maxNumber} {maxNumber, plural, other {アラート}}です。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名前が必要です。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "ルール調査ガイドを追加...",

View file

@ -38194,7 +38194,6 @@
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimestampOverrideLabel": "时间戳覆盖",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideHelpText": "为正在调查检测告警的分析人员提供有用的信息。本指南(作为备注)将显示在规则详情页面上以及从此规则所生成的检测告警创建的时间线中。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.guideLabel": "调查指南",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldGreaterThanError": "最大告警数必须大于 0。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "每次规则运行时Kibana 最多只允许 {maxNumber} 个{maxNumber, plural, other {告警}}。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名称必填。",
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "添加规则调查指南......",