diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 65cadef10c054..9de12c30d413b 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -37684,7 +37684,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel": "Compte", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel": "Valeurs uniques", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "Sélectionner un champ pour vérifier la cardinalité", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "Seuil", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ga.enableThresholdSuppressionForFieldsLabel": "Supprimer les alertes par champs sélectionnés : {fieldsString}", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ga.enableThresholdSuppressionLabel": "Supprimer les alertes", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "Supprimer les alertes pour", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index ac68add10421a..490647e4149a7 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -37542,7 +37542,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel": "カウント", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel": "一意の値", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "カーディナリティを確認するフィールドを選択します", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "しきい値", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ga.enableThresholdSuppressionForFieldsLabel": "選択したフィールドでアラートを非表示:{fieldsString}", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ga.enableThresholdSuppressionLabel": "アラートを非表示", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "アラートを非表示", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 24bce6642cf87..7683137782bae 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -36974,7 +36974,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel": "计数", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel": "唯一值", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText": "选择字段以检查基数", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdLabel": "阈值", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ga.enableThresholdSuppressionForFieldsLabel": "对选定字段阻止告警:{fieldsString}", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ga.enableThresholdSuppressionLabel": "阻止告警", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.groupByDurationValueLabel": "阻止以下项的告警", diff --git a/x-pack/solutions/security/plugins/security_solution/public/common/utils/normalize_machine_learning_job_id.ts b/x-pack/solutions/security/plugins/security_solution/public/common/utils/normalize_machine_learning_job_id.ts new file mode 100644 index 0000000000000..7733e2be08c97 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/common/utils/normalize_machine_learning_job_id.ts @@ -0,0 +1,12 @@ +/* + * 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 type { MachineLearningJobId } from '../../../common/api/detection_engine'; + +export function normalizeMachineLearningJobId(jobId: MachineLearningJobId): string[] { + return typeof jobId === 'string' ? [jobId] : jobId; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/anomaly_threshold_edit/anomaly_threshold_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/anomaly_threshold_edit/anomaly_threshold_edit.tsx new file mode 100644 index 0000000000000..d2fa7bdfba9ee --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/anomaly_threshold_edit/anomaly_threshold_edit.tsx @@ -0,0 +1,34 @@ +/* + * 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 { UseField } from '../../../../shared_imports'; +import { AnomalyThresholdSlider } from '../../../rule_creation_ui/components/anomaly_threshold_slider'; +import * as i18n from './translations'; + +const componentProps = { + describedByIds: ['anomalyThreshold'], +}; + +interface AnomalyThresholdEditProps { + path: string; +} + +export function AnomalyThresholdEdit({ path }: AnomalyThresholdEditProps): JSX.Element { + return ( + + ); +} + +const ANOMALY_THRESHOLD_FIELD_CONFIG = { + label: i18n.ANOMALY_THRESHOLD_LABEL, +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/anomaly_threshold_edit/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/anomaly_threshold_edit/index.ts new file mode 100644 index 0000000000000..406e555795656 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/anomaly_threshold_edit/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { AnomalyThresholdEdit } from './anomaly_threshold_edit'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/anomaly_threshold_edit/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/anomaly_threshold_edit/translations.ts new file mode 100644 index 0000000000000..dc22c04f45055 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/anomaly_threshold_edit/translations.ts @@ -0,0 +1,15 @@ +/* + * 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 ANOMALY_THRESHOLD_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', + { + defaultMessage: 'Anomaly score threshold', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/create_ml_job_button/create_ml_job_button.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/create_ml_job_button/create_ml_job_button.tsx new file mode 100644 index 0000000000000..23ced049a5290 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/create_ml_job_button/create_ml_job_button.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { useKibana } from '../../../../common/lib/kibana'; +import * as i18n from './translations'; + +export function CreateCustomMlJobButton(): JSX.Element { + const { navigateToApp } = useKibana().services.application; + + return ( + navigateToApp('ml', { openInNewTab: true })} + > + {i18n.CREATE_CUSTOM_JOB_BUTTON_TITLE} + + ); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/create_ml_job_button/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/create_ml_job_button/translations.ts new file mode 100644 index 0000000000000..470f0a9b66394 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/create_ml_job_button/translations.ts @@ -0,0 +1,15 @@ +/* + * 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 CREATE_CUSTOM_JOB_BUTTON_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.mlSelectJob.createCustomJobButtonTitle', + { + defaultMessage: 'Create custom job', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/index.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/index.ts new file mode 100644 index 0000000000000..0e1f05a9e7a1b --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { MachineLearningJobIdEdit } from './machine_learning_job_id_edit'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_id_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_id_edit.tsx new file mode 100644 index 0000000000000..299e2c8dd87e6 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/machine_learning_job_id_edit.tsx @@ -0,0 +1,53 @@ +/* + * 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 { UseField, fieldValidators } from '../../../../shared_imports'; +import { MlJobSelect } from '../ml_job_select'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; +import * as i18n from './translations'; + +interface MachineLearningJobIdEditProps { + path: string; + shouldShowHelpText?: boolean; +} + +export function MachineLearningJobIdEdit({ + path, + shouldShowHelpText, +}: MachineLearningJobIdEditProps): JSX.Element { + const { loading, jobs } = useSecurityJobs(); + + const componentProps = useMemo( + () => ({ + jobs, + loading, + shouldShowHelpText, + }), + [jobs, loading, shouldShowHelpText] + ); + + return ( + + ); +} + +const MACHINE_LEARNING_JOB_ID_FIELD_CONFIG = { + label: i18n.MACHINE_LEARNING_JOB_ID_LABEL, + validations: [ + { + validator: fieldValidators.emptyField( + i18n.MACHINE_LEARNING_JOB_ID_EMPTY_FIELD_VALIDATION_ERROR + ), + }, + ], +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/translations.ts new file mode 100644 index 0000000000000..ceb1bd6335549 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/machine_learning_job_id_edit/translations.ts @@ -0,0 +1,22 @@ +/* + * 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 MACHINE_LEARNING_JOB_ID_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', + { + defaultMessage: 'Machine Learning job', + } +); + +export const MACHINE_LEARNING_JOB_ID_EMPTY_FIELD_VALIDATION_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', + { + defaultMessage: 'A Machine Learning job is required.', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/help_text.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/help_text.test.tsx index 3db1beb5bb743..716d9e1411608 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/help_text.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/help_text.test.tsx @@ -8,15 +8,46 @@ import React from 'react'; import { shallow } from 'enzyme'; import { HelpText } from './help_text'; +import type { SecurityJob } from '../../../../common/components/ml_popover/types'; + +jest.mock('../../../../common/lib/kibana', () => { + return { + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + getUrlForApp: () => '/app/ml', + }, + }, + }), + }; +}); describe('MlJobSelect help text', () => { it('does not show warning if all jobs are running', () => { - const wrapper = shallow(); + const jobs = [ + { + id: 'test-id', + jobState: 'opened', + datafeedState: 'opened', + }, + ] as SecurityJob[]; + const selectedJobIds = ['test-id']; + + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ml-warning-not-running-jobs"]')).toHaveLength(0); }); it('shows warning if there are jobs not running', () => { - const wrapper = shallow(); + const jobs = [ + { + id: 'test-id', + jobState: 'closed', + datafeedState: 'stopped', + }, + ] as SecurityJob[]; + const selectedJobIds = ['test-id']; + + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ml-warning-not-running-jobs"]')).toHaveLength(1); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/help_text.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/help_text.tsx index 17d869183f96e..e53a8f251850c 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/help_text.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/help_text.tsx @@ -5,63 +5,84 @@ * 2.0. */ -import React from 'react'; +import React, { memo, useMemo } from 'react'; import { EuiLink, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; +import { css } from '@emotion/css'; import { FormattedMessage } from '@kbn/i18n-react'; +import { isJobStarted } from '../../../../../common/machine_learning/helpers'; +import type { SecurityJob } from '../../../../common/components/ml_popover/types'; +import { useKibana } from '../../../../common/lib/kibana'; +interface HelpTextProps { + jobs: SecurityJob[]; + selectedJobIds: string[]; +} -const HelpTextWarningContainer = styled.div` - margin-top: 10px; -`; +export const HelpText = memo(function HelpText({ + jobs, + selectedJobIds, +}: HelpTextProps): JSX.Element { + const { getUrlForApp } = useKibana().services.application; + const mlUrl = getUrlForApp('ml'); -const HelpTextComponent: React.FC<{ href: string; notRunningJobIds: string[] }> = ({ - href, - notRunningJobIds, -}) => ( - <> - - - - ), - }} - /> - {notRunningJobIds.length > 0 && ( - - - - {notRunningJobIds.length === 1 ? ( - - ) : ( + const notRunningJobIds = useMemo(() => { + const selectedJobs = jobs.filter(({ id }) => selectedJobIds.includes(id)); + return selectedJobs.reduce((acc, job) => { + if (!isJobStarted(job.jobState, job.datafeedState)) { + acc.push(job.id); + } + return acc; + }, [] as string[]); + }, [jobs, selectedJobIds]); + + return ( + <> + acc + (i < array.length - 1 ? ', ' : ', and ') + value - ), - }} + id="xpack.securitySolution.components.mlJobSelect.machineLearningLink" + defaultMessage="Machine Learning" /> - )} - - - - )} - -); + + ), + }} + /> + {notRunningJobIds.length > 0 && ( +
+ + + {notRunningJobIds.length === 1 ? ( + + ) : ( + + acc + (i < array.length - 1 ? ', ' : ', and ') + value + ), + }} + /> + )} + + +
+ )} + + ); +}); -export const HelpText = React.memo(HelpTextComponent); +const warningContainerClassName = css` + margin-top: 10px; +`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.tsx index e9c22457d7465..187f72db3562e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.tsx @@ -5,153 +5,4 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; -import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { - EuiButton, - EuiComboBox, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiToolTip, - EuiText, -} from '@elastic/eui'; - -import styled from 'styled-components'; -import { isJobStarted } from '../../../../../common/machine_learning/helpers'; -import type { FieldHook } from '../../../../shared_imports'; -import { getFieldValidityAndErrorMessage } from '../../../../shared_imports'; -import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; -import { useKibana } from '../../../../common/lib/kibana'; -import { HelpText } from './help_text'; - -import * as i18n from './translations'; - -interface MlJobValue { - id: string; - description: string; - name?: string; -} - -const JobDisplayContainer = styled.div` - width: 100%; - height: 100%; - display: flex; - flex-direction: column; -`; - -type MlJobOption = EuiComboBoxOptionOption; - -const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)` - margin-bottom: 5px; -`; - -const MlJobEuiButton = styled(EuiButton)` - margin-top: 20px; -`; - -const JobDisplay: React.FC = ({ description, name, id }) => ( - - {name ?? id} - - -

{description}

-
-
-
-); - -interface MlJobSelectProps { - describedByIds: string[]; - field: FieldHook; -} - -const renderJobOption = (option: MlJobOption) => ( - -); - -export const MlJobSelect: React.FC = ({ describedByIds = [], field }) => { - const jobIds = field.value as string[]; - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const { loading, jobs } = useSecurityJobs(); - const { getUrlForApp, navigateToApp } = useKibana().services.application; - const mlUrl = getUrlForApp('ml'); - const handleJobSelect = useCallback( - (selectedJobOptions: MlJobOption[]): void => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const selectedJobIds = selectedJobOptions.map((option) => option.value!.id); - field.setValue(selectedJobIds); - }, - [field] - ); - - const jobOptions = jobs.map((job) => ({ - value: { - id: job.id, - description: job.description, - name: job.customSettings?.security_app_display_name, - }, - // Make sure users can search for id or name. - // The label contains the name and id because EuiComboBox uses it for the textual search. - label: `${job.customSettings?.security_app_display_name} ${job.id}`, - })); - - const selectedJobOptions = jobOptions - .filter((option) => jobIds.includes(option.value.id)) - // 'label' defines what is rendered inside the selected ComboBoxPill - .map((options) => ({ ...options, label: options.value.name ?? options.value.id })); - - const notRunningJobIds = useMemo(() => { - const selectedJobs = jobs.filter(({ id }) => jobIds.includes(id)); - return selectedJobs.reduce((acc, job) => { - if (!isJobStarted(job.jobState, job.datafeedState)) { - acc.push(job.id); - } - return acc; - }, [] as string[]); - }, [jobs, jobIds]); - - return ( - - - } - isInvalid={isInvalid} - error={errorMessage} - data-test-subj="mlJobSelect" - describedByIds={describedByIds} - > - - - - - - - - - navigateToApp('ml', { openInNewTab: true })} - > - {i18n.CREATE_CUSTOM_JOB_BUTTON_TITLE} - - - - ); -}; +export { MlJobSelect } from './ml_job_select'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_select.test.tsx similarity index 89% rename from x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.test.tsx rename to x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_select.test.tsx index 5323ac16bff4f..ddbb6c8b70708 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_select.test.tsx @@ -21,9 +21,9 @@ describe('MlJobSelect', () => { it('renders correctly', () => { const Component = () => { - const field = useFormFieldMock(); + const field = useFormFieldMock(); - return ; + return ; }; const wrapper = shallow(); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_select.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_select.tsx new file mode 100644 index 0000000000000..160178a5b782d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/ml_job_select.tsx @@ -0,0 +1,113 @@ +/* + * 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 { + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiText, + EuiToolTip, +} from '@elastic/eui'; + +import type { FieldHook } from '../../../../shared_imports'; +import { getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { HelpText } from './help_text'; +import * as i18n from './translations'; +import type { SecurityJob } from '../../../../common/components/ml_popover/types'; +import type { MlJobOption, MlJobValue } from './types'; +import * as styles from './styles'; + +interface MlJobSelectProps { + field: FieldHook; + shouldShowHelpText?: boolean; + loading: boolean; + jobs: SecurityJob[]; +} + +export const MlJobSelect: React.FC = ({ + field, + shouldShowHelpText = true, + loading, + jobs, +}) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const selectedJobIds = field.value; + + const handleJobSelect = (selectedJobOptions: MlJobOption[]): void => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const newlySelectedJobIds = selectedJobOptions.map((option) => option.value!.id); + field.setValue(newlySelectedJobIds); + }; + + const jobOptions = jobs.map((job) => ({ + value: { + id: job.id, + description: job.description, + name: job.customSettings?.security_app_display_name, + }, + // Make sure users can search for id or name. + // The label contains the name and id because EuiComboBox uses it for the textual search. + label: `${job.customSettings?.security_app_display_name} ${job.id}`, + })); + + const selectedJobOptions = jobOptions + .filter((option) => selectedJobIds.includes(option.value.id)) + // 'label' defines what is rendered inside the selected ComboBoxPill + .map((options) => ({ ...options, label: options.value.name ?? options.value.id })); + + return ( + + + } + isInvalid={isInvalid} + error={errorMessage} + data-test-subj="mlJobSelect" + > + + + + + + + + + ); +}; + +const renderJobOption = (option: MlJobOption) => ( + +); + +const JobDisplay: React.FC = ({ description, name, id }) => ( +
+ {name ?? id} + + +

{description}

+
+
+
+); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/styles.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/styles.ts new file mode 100644 index 0000000000000..ffc6d7738fa6a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/styles.ts @@ -0,0 +1,19 @@ +/* + * 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 mlJobSelectClassName = css` + margin-bottom: 5px; +`; + +export const jobDisplayClassName = css` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +`; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/translations.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/translations.tsx index e0beadc019b4f..2d089fea04dfc 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/translations.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/translations.tsx @@ -7,13 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const CREATE_CUSTOM_JOB_BUTTON_TITLE = i18n.translate( - 'xpack.securitySolution.detectionEngine.mlSelectJob.createCustomJobButtonTitle', - { - defaultMessage: 'Create custom job', - } -); - export const ML_JOB_SELECT_PLACEHOLDER_TEXT = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.mlJobSelectPlaceholderText', { diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/types.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/types.ts new file mode 100644 index 0000000000000..a10a9da862d2c --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/ml_job_select/types.ts @@ -0,0 +1,16 @@ +/* + * 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 type { EuiComboBoxOptionOption } from '@elastic/eui'; + +export interface MlJobValue { + id: string; + description: string; + name?: string; +} + +export type MlJobOption = EuiComboBoxOptionOption; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/field_configs.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/field_configs.ts new file mode 100644 index 0000000000000..5bf7500ea1164 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/field_configs.ts @@ -0,0 +1,91 @@ +/* + * 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 type { ValidationFunc } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import type { ERROR_CODE } from '@kbn/es-ui-shared-plugin/static/forms/helpers/field_validators/types'; +import { isEmpty } from 'lodash'; +import { fieldValidators } from '../../../../shared_imports'; +import * as i18n from './translations'; + +export const THRESHOLD_FIELD_CONFIG = { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.THRESHOLD_FIELD_LABEL, + helpText: i18n.THRESHOLD_FIELD_HELP_TEXT, + validations: [ + { + validator: fieldValidators.maxLengthField({ + length: 3, + message: i18n.THRESHOLD_FIELD_COUNT_VALIDATION_ERROR, + }), + }, + ], +}; + +export const THRESHOLD_VALUE_CONFIG = { + type: FIELD_TYPES.NUMBER, + label: i18n.THRESHOLD_VALUE_LABEL, + validations: [ + { + validator: fieldValidators.numberGreaterThanField({ + than: 1, + message: i18n.THRESHOLD_VALUE_VALIDATION_ERROR, + allowEquality: true, + }), + }, + ], +}; + +export function getCardinalityFieldConfig(path: string) { + return { + defaultValue: [] as unknown, + fieldsToValidateOnChange: [`${path}.cardinality.field`, `${path}.cardinality.value`], + type: FIELD_TYPES.COMBO_BOX, + label: i18n.CARDINALITY_FIELD_LABEL, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + + if (!isEmpty(formData[`${path}.cardinality.value`])) { + return fieldValidators.emptyField(i18n.CARDINALITY_FIELD_MISSING_VALIDATION_ERROR)( + ...args + ); + } + }, + }, + ], + helpText: i18n.CARDINALITY_FIELD_HELP_TEXT, + }; +} + +export function getCardinalityValueConfig(path: string) { + return { + fieldsToValidateOnChange: [`${path}.cardinality.field`, `${path}.cardinality.value`], + type: FIELD_TYPES.NUMBER, + label: i18n.CARDINALITY_VALUE_LABEL, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + + if (!isEmpty(formData[`${path}.cardinality.field`])) { + return fieldValidators.numberGreaterThanField({ + than: 1, + message: i18n.CARDINALITY_VALUE_VALIDATION_ERROR, + allowEquality: true, + })(...args); + } + }, + }, + ], + }; +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/index.tsx new file mode 100644 index 0000000000000..aad44c4a8b842 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/index.tsx @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { ThresholdEdit } from './threshold_edit'; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/threshold_edit.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/threshold_edit.tsx new file mode 100644 index 0000000000000..3545bc120f95e --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/threshold_edit.tsx @@ -0,0 +1,75 @@ +/* + * 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, useMemo } from 'react'; +import { type FieldSpec } from '@kbn/data-views-plugin/common'; +import { type FieldHook, UseMultiFields } from '../../../../shared_imports'; +import { ThresholdInput } from '../../../rule_creation_ui/components/threshold_input'; +import { + THRESHOLD_FIELD_CONFIG, + THRESHOLD_VALUE_CONFIG, + getCardinalityFieldConfig, + getCardinalityValueConfig, +} from './field_configs'; + +interface ThresholdEditProps { + path: string; + esFields: FieldSpec[]; +} + +export function ThresholdEdit({ path, esFields }: ThresholdEditProps): JSX.Element { + const aggregatableFields = useMemo( + () => esFields.filter((field) => field.aggregatable === true), + [esFields] + ); + + const ThresholdInputChildren = useCallback( + ({ + thresholdField, + thresholdValue, + thresholdCardinalityField, + thresholdCardinalityValue, + }: Record) => ( + + ), + [aggregatableFields] + ); + + const cardinalityFieldConfig = useMemo(() => getCardinalityFieldConfig(path), [path]); + const cardinalityValueConfig = useMemo(() => getCardinalityValueConfig(path), [path]); + + return ( + + {ThresholdInputChildren} + + ); +} diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/translations.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/translations.ts new file mode 100644 index 0000000000000..26981afd15e3a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation/components/threshold_edit/translations.ts @@ -0,0 +1,78 @@ +/* + * 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 THRESHOLD_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldLabel', + { + defaultMessage: 'Group by', + } +); + +export const THRESHOLD_FIELD_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdFieldHelpText', + { + defaultMessage: "Select fields to group by. Fields are joined together with 'AND'", + } +); + +export const THRESHOLD_FIELD_COUNT_VALIDATION_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdFieldFieldData.arrayLengthGreaterThanMaxErrorMessage', + { + defaultMessage: 'Number of fields must be 3 or less.', + } +); + +export const THRESHOLD_VALUE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldThresholdValueLabel', + { + defaultMessage: 'Threshold', + } +); + +export const THRESHOLD_VALUE_VALIDATION_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdValueFieldData.numberGreaterThanOrEqualOneErrorMessage', + { + defaultMessage: 'Value must be greater than or equal to one.', + } +); + +export const CARDINALITY_FIELD_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityFieldLabel', + { + defaultMessage: 'Count', + } +); + +export const CARDINALITY_FIELD_MISSING_VALIDATION_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdCardinalityFieldFieldData.thresholdCardinalityFieldNotSuppliedMessage', + { + defaultMessage: 'A Cardinality Field is required.', + } +); + +export const CARDINALITY_FIELD_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdFieldCardinalityFieldHelpText', + { + defaultMessage: 'Select a field to check cardinality', + } +); + +export const CARDINALITY_VALUE_LABEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.fieldThresholdCardinalityValueFieldLabel', + { + defaultMessage: 'Unique values', + } +); + +export const CARDINALITY_VALUE_VALIDATION_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.thresholdCardinalityValueFieldData.numberGreaterThanOrEqualOneErrorMessage', + { + defaultMessage: 'Value must be greater than or equal to one.', + } +); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx index b45f1a50414ac..90afe8d95fa37 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.test.tsx @@ -412,6 +412,8 @@ describe('description_step', () => { }); describe('threshold', () => { + const thresholdLabel = 'Threshold'; + test('returns threshold description when threshold exist and field is empty', () => { const mockThreshold = { threshold: { @@ -421,13 +423,13 @@ describe('description_step', () => { }; const result: ListItems[] = getDescriptionItem( 'threshold', - 'Threshold label', + thresholdLabel, mockThreshold, mockFilterManager, mockLicenseService ); - expect(result[0].title).toEqual('Threshold label'); + expect(result[0].title).toEqual(thresholdLabel); expect(React.isValidElement(result[0].description)).toBeTruthy(); expect(mount(result[0].description as React.ReactElement).html()).toContain( 'All results >= 100' @@ -443,13 +445,13 @@ describe('description_step', () => { }; const result: ListItems[] = getDescriptionItem( 'threshold', - 'Threshold label', + thresholdLabel, mockThreshold, mockFilterManager, mockLicenseService ); - expect(result[0].title).toEqual('Threshold label'); + expect(result[0].title).toEqual(thresholdLabel); expect(React.isValidElement(result[0].description)).toBeTruthy(); expect(mount(result[0].description as React.ReactElement).html()).toEqual( 'Results aggregated by user.name >= 100' @@ -469,13 +471,13 @@ describe('description_step', () => { }; const result: ListItems[] = getDescriptionItem( 'threshold', - 'Threshold label', + thresholdLabel, mockThreshold, mockFilterManager, mockLicenseService ); - expect(result[0].title).toEqual('Threshold label'); + expect(result[0].title).toEqual(thresholdLabel); expect(React.isValidElement(result[0].description)).toBeTruthy(); expect(mount(result[0].description as React.ReactElement).html()).toEqual( 'Results aggregated by user.name >= 100' @@ -495,13 +497,13 @@ describe('description_step', () => { }; const result: ListItems[] = getDescriptionItem( 'threshold', - 'Threshold label', + thresholdLabel, mockThreshold, mockFilterManager, mockLicenseService ); - expect(result[0].title).toEqual('Threshold label'); + expect(result[0].title).toEqual(thresholdLabel); expect(React.isValidElement(result[0].description)).toBeTruthy(); expect(mount(result[0].description as React.ReactElement).html()).toContain( 'Results aggregated by user.name >= 100 when unique values count of host.test_value >= 10' diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx index a91487afc4486..46ed1f18f70ac 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/description_step/index.tsx @@ -72,8 +72,11 @@ import { ALERT_SUPPRESSION_MISSING_FIELDS_FIELD_NAME, } from '../../../rule_creation/components/alert_suppression_edit'; import { THRESHOLD_ALERT_SUPPRESSION_ENABLED } from '../../../rule_creation/components/threshold_alert_suppression_edit'; +import { THRESHOLD_VALUE_LABEL } from '../../../rule_creation/components/threshold_edit/translations'; import { NEW_TERMS_FIELDS_LABEL } from '../../../rule_creation/components/new_terms_fields_edit/translations'; import { HISTORY_WINDOW_START_LABEL } from '../../../rule_creation/components/history_window_start_edit/translations'; +import { MACHINE_LEARNING_JOB_ID_LABEL } from '../../../rule_creation/components/machine_learning_job_id_edit/translations'; +import { ANOMALY_THRESHOLD_LABEL } from '../../../rule_creation/components/anomaly_threshold_edit/translations'; import type { FieldValueQueryBar } from '../query_bar_field'; const DescriptionListContainer = styled(EuiDescriptionList)` @@ -108,10 +111,7 @@ export const StepRuleDescriptionComponent = ({ if (key === 'machineLearningJobId') { return [ ...acc, - buildMlJobsDescription( - get(key, data) as string[], - (get(key, schema) as { label: string }).label - ), + buildMlJobsDescription(get(key, data) as string[], MACHINE_LEARNING_JOB_ID_LABEL), ]; } @@ -286,7 +286,7 @@ export const getDescriptionItem = ( return buildThreatDescription({ label, threat: filterEmptyThreats(threats) }); } else if (field === 'threshold') { const threshold = get(field, data); - return buildThresholdDescription(label, threshold); + return buildThresholdDescription(THRESHOLD_VALUE_LABEL, threshold); } else if (field === 'references') { const urls: string[] = get(field, data); return buildUrlsDescription(label, urls); @@ -362,6 +362,9 @@ export const getDescriptionItem = ( } else if (field === 'maxSignals') { const value: number | undefined = get(field, data); return value ? [{ title: label, description: value }] : []; + } else if (field === 'anomalyThreshold') { + const value: number | undefined = get(field, data); + return value ? [{ title: ANOMALY_THRESHOLD_LABEL, description: value }] : []; } else if (field === 'historyWindowSize') { const value: number = get(field, data); return value ? [{ title: HISTORY_WINDOW_START_LABEL, description: value }] : []; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/helpers.ts b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/helpers.ts index fcbdfdf4f86b7..c45022ebb96b5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/helpers.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/rule_preview/helpers.ts @@ -117,7 +117,7 @@ export const getIsRulePreviewDisabled = ({ dataSourceType: DataSourceType; threatIndex: string[]; threatMapping: ThreatMapping; - machineLearningJobId: string[]; + machineLearningJobId: string[] | undefined; queryBar: FieldValueQueryBar; newTermsFields: string[]; }) => { @@ -125,7 +125,7 @@ export const getIsRulePreviewDisabled = ({ return isEsqlPreviewDisabled({ isQueryBarValid, queryBar }); } if (ruleType === 'machine_learning') { - return machineLearningJobId.length === 0; + return !machineLearningJobId ?? machineLearningJobId?.length === 0; } if ( !isQueryBarValid || diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx index 2d2ef8c8930d6..6e20138256d7f 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_about_rule/index.tsx @@ -50,7 +50,7 @@ const CommonUseField = getUseField({ component: Field }); interface StepAboutRuleProps extends RuleStepProps { ruleType: Type; - machineLearningJobId: string[]; + machineLearningJobId: string[] | undefined; index: string[]; dataViewId: string | undefined; timestampOverride: string; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx index fa7f9a8952cac..65384f838ac9b 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/index.test.tsx @@ -830,7 +830,6 @@ function TestForm({ shouldLoadQueryDynamically={stepDefineDefaultValue.shouldLoadQueryDynamically} queryBarTitle="" queryBarSavedId="" - thresholdFields={[]} {...formProps} />