mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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=\"Schermafbeelding 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=\"Schermafbeelding 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=\"Schermafbeelding 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:
parent
04a6dd94a6
commit
bb3e3c7693
69 changed files with 2248 additions and 534 deletions
|
@ -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';
|
||||
|
|
|
@ -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',
|
|
@ -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();
|
|
@ -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';
|
||||
}
|
|
@ -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[];
|
|
@ -115,7 +115,7 @@ const getComboBoxProps = (fields: ComboBoxFields): GetFieldComboBoxPropsReturn =
|
|||
};
|
||||
};
|
||||
|
||||
export const useField = ({
|
||||
export const useEsField = ({
|
||||
indexPattern,
|
||||
fieldTypeFilter,
|
||||
isRequired,
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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: '' },
|
||||
}),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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' };
|
||||
};
|
||||
|
|
|
@ -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' };
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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' };
|
||||
};
|
||||
|
|
|
@ -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' };
|
||||
};
|
|
@ -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;
|
||||
`;
|
|
@ -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' && (
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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}.',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
|
@ -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} />;
|
||||
}
|
|
@ -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)),
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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} />;
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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)
|
||||
: [],
|
||||
};
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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} />;
|
||||
}
|
|
@ -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} />;
|
||||
}
|
|
@ -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)
|
||||
: [],
|
||||
};
|
||||
}
|
|
@ -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} />;
|
||||
}
|
|
@ -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',
|
||||
})),
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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':
|
||||
|
|
|
@ -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={[
|
||||
|
|
|
@ -16,6 +16,10 @@ interface FalsePositivesReadOnlyProps {
|
|||
}
|
||||
|
||||
export function FalsePositivesReadOnly({ falsePositives }: FalsePositivesReadOnlyProps) {
|
||||
if (falsePositives.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
|
|
|
@ -16,6 +16,10 @@ interface NoteReadOnlyProps {
|
|||
}
|
||||
|
||||
export function NoteReadOnly({ note }: NoteReadOnlyProps) {
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
|
|
|
@ -16,6 +16,10 @@ interface ReferencesReadOnlyProps {
|
|||
}
|
||||
|
||||
export function ReferencesReadOnly({ references }: ReferencesReadOnlyProps) {
|
||||
if (references.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
|
|
|
@ -16,6 +16,10 @@ interface RelatedIntegrationsReadOnly {
|
|||
}
|
||||
|
||||
export function RelatedIntegrationsReadOnly({ relatedIntegrations }: RelatedIntegrationsReadOnly) {
|
||||
if (!relatedIntegrations.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
|
|
|
@ -16,6 +16,10 @@ interface SetupReadOnlyProps {
|
|||
}
|
||||
|
||||
export function SetupReadOnly({ setup }: SetupReadOnlyProps) {
|
||||
if (!setup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
|
|
|
@ -16,6 +16,10 @@ interface TagsReadOnlyProps {
|
|||
}
|
||||
|
||||
export function TagsReadOnly({ tags }: TagsReadOnlyProps) {
|
||||
if (tags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
|
|
|
@ -16,6 +16,10 @@ export interface ThreatReadOnlyProps {
|
|||
}
|
||||
|
||||
export const ThreatReadOnly = ({ threat }: ThreatReadOnlyProps) => {
|
||||
if (threat.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
listItems={[
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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": "ルール調査ガイドを追加...",
|
||||
|
|
|
@ -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": "添加规则调查指南......",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue